add des betises
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
This commit is contained in:
parent
fb248553a8
commit
c3c9dbb408
14
apps/web/public/icon-without-bg.svg
Normal file
14
apps/web/public/icon-without-bg.svg
Normal file
@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" width="512" height="512">
|
||||
<g transform="translate(76 76) scale(3.6)">
|
||||
<!-- Facette haut-gauche (lumière) -->
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1"/>
|
||||
<!-- Facette haut-droite -->
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8"/>
|
||||
<!-- Facette bas-gauche -->
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65"/>
|
||||
<!-- Facette bas-droite (ombre) -->
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48"/>
|
||||
<!-- Contour propre -->
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 742 B |
@ -17,6 +17,7 @@ body:json {
|
||||
"fullName": "{{fullName}}"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
|
||||
@ -5,7 +5,7 @@ meta {
|
||||
}
|
||||
|
||||
post {
|
||||
url: {{baseUrl}}/api/v1/auth/login
|
||||
url: {{baseUrl}}/api/v1/auth/login2
|
||||
body: json
|
||||
auth: none
|
||||
}
|
||||
@ -17,6 +17,8 @@ body:json {
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
script:post-response {
|
||||
|
||||
15
bruno/environments/prod.bru
Normal file
15
bruno/environments/prod.bru
Normal file
@ -0,0 +1,15 @@
|
||||
vars {
|
||||
baseUrl: https://app.rubis.pro
|
||||
email: alice@bruno.test
|
||||
password: password123
|
||||
fullName: Alice Bruno
|
||||
token:
|
||||
userId:
|
||||
organizationId:
|
||||
clientId:
|
||||
planSlug: standard-30j
|
||||
invoiceId:
|
||||
batchId:
|
||||
draftId:
|
||||
checkinToken:
|
||||
}
|
||||
517
docs/marketing/assets/linkedin-banner.html
Normal file
517
docs/marketing/assets/linkedin-banner.html
Normal file
@ -0,0 +1,517 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Bannière LinkedIn personnelle — 1584 × 396 px (ratio 4:1).
|
||||
Hauteur calée sur la zone réellement visible par LinkedIn sur le profil
|
||||
(LinkedIn rogne haut/bas la version 4:1 d'origine), donc upload direct sans crop.
|
||||
|
||||
Ouvre ce fichier dans Chrome et clique sur les boutons en haut :
|
||||
- "Télécharger PDF" → fichier rubis-linkedin-banner.pdf, dimensions exactes
|
||||
- "Télécharger PNG" → fichier rubis-linkedin-banner.png, prêt à uploader LinkedIn
|
||||
|
||||
Zone "safe" pour la photo de profil LinkedIn (overlap bas-gauche) :
|
||||
~200 × 100 px en bas à gauche → laissée libre dans cette compo.
|
||||
-->
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Bannière LinkedIn — Rubis sur l'ongle</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,500;12..96,600;12..96,700;12..96,800&family=Inter:wght@400;500;600;700&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<style>
|
||||
:root {
|
||||
--rubis: #9f1239;
|
||||
--rubis-deep: #771328;
|
||||
--rubis-glow: #fbe4ea;
|
||||
--ink: #1a1410;
|
||||
--ink-2: #4f4640;
|
||||
--ink-3: #8a7f76;
|
||||
--line: #e8e0d6;
|
||||
--cream: #faf7f2;
|
||||
--cream-2: #f5efe7;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #d9d4cb;
|
||||
font-family: "Inter", -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
/* Barre d'export — masquée à la capture */
|
||||
.controls {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.controls h2 {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: #1a1410;
|
||||
margin: 0 16px 0 0;
|
||||
letter-spacing: -0.012em;
|
||||
}
|
||||
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 10px 18px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-family: "Inter", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s, transform 0.1s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #9f1239;
|
||||
color: white;
|
||||
box-shadow: 0 2px 6px rgba(159, 18, 57, 0.25);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #771328;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: white;
|
||||
color: #1a1410;
|
||||
border: 1px solid #e8e0d6;
|
||||
}
|
||||
|
||||
.btn-secondary:hover {
|
||||
background: #faf7f2;
|
||||
}
|
||||
|
||||
.btn:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.hint {
|
||||
font-size: 12.5px;
|
||||
color: #4f4640;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.banner {
|
||||
width: 1584px;
|
||||
height: 396px;
|
||||
background: var(--cream);
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr auto;
|
||||
gap: 72px;
|
||||
padding: 56px 80px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Halo rubis discret en arrière-plan */
|
||||
.banner::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -200px;
|
||||
right: -160px;
|
||||
width: 460px;
|
||||
height: 460px;
|
||||
background: radial-gradient(circle, var(--rubis-glow) 0%, transparent 65%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Gem géant en watermark, très transparent */
|
||||
.banner::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
bottom: -200px;
|
||||
left: 30%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
transform: rotate(45deg);
|
||||
background: var(--rubis);
|
||||
opacity: 0.04;
|
||||
border-radius: 24px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* ===== Left side : brand + tagline ===== */
|
||||
/* Décalé de 300px à droite pour éviter le chevauchement avec la photo de profil LinkedIn */
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
padding-left: 300px;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.gem-svg {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
filter: drop-shadow(0 2px 4px rgba(159, 18, 57, 0.18));
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 800;
|
||||
font-size: 28px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.02em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.brand-name .suffix {
|
||||
font-style: italic;
|
||||
font-weight: 500;
|
||||
color: var(--ink-3);
|
||||
font-size: 18px;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.tagline {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 42px;
|
||||
color: var(--ink);
|
||||
line-height: 1.12;
|
||||
letter-spacing: -0.026em;
|
||||
max-width: 580px;
|
||||
}
|
||||
|
||||
.tagline em {
|
||||
font-style: italic;
|
||||
color: var(--rubis);
|
||||
}
|
||||
|
||||
.url {
|
||||
margin-top: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
color: var(--ink-2);
|
||||
background: white;
|
||||
padding: 9px 16px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--line);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.url::before {
|
||||
content: "";
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
background: var(--rubis);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
/* ===== Right side : dashboard preview ===== */
|
||||
.right {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: flex-end;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.preview {
|
||||
width: 100%;
|
||||
max-width: 520px;
|
||||
background: white;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 14px;
|
||||
padding: 22px 26px;
|
||||
box-shadow:
|
||||
0 14px 32px -14px rgba(26, 20, 16, 0.18),
|
||||
0 4px 10px -4px rgba(26, 20, 16, 0.08);
|
||||
}
|
||||
|
||||
/* Hero rubis */
|
||||
.rubis-hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
|
||||
.rubis-hero .gem-big {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
filter: drop-shadow(0 4px 8px rgba(159, 18, 57, 0.3));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rubis-count {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 30px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.022em;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rubis-sub {
|
||||
font-size: 14px;
|
||||
color: var(--ink-2);
|
||||
margin-top: 6px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.rubis-sub b {
|
||||
font-weight: 600;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
/* KPIs */
|
||||
.kpis {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 22px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.kpi-label {
|
||||
font-size: 13px;
|
||||
color: var(--ink-3);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.kpi-value {
|
||||
font-family: "Bricolage Grotesque", sans-serif;
|
||||
font-weight: 700;
|
||||
font-size: 22px;
|
||||
color: var(--ink);
|
||||
letter-spacing: -0.015em;
|
||||
margin-top: 4px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.kpi-delta {
|
||||
font-size: 13px;
|
||||
color: var(--rubis);
|
||||
font-weight: 500;
|
||||
margin-top: 3px;
|
||||
}
|
||||
|
||||
/* Activity feed */
|
||||
.activity {
|
||||
margin-top: 14px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px dashed var(--line);
|
||||
font-size: 14px;
|
||||
color: var(--ink-2);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.activity b {
|
||||
color: var(--ink);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activity time {
|
||||
color: var(--ink-3);
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="controls">
|
||||
<h2>Bannière LinkedIn — 1584 × 396</h2>
|
||||
<button id="exportPng2x" class="btn btn-primary">🖼️ PNG 2× (3168×792) — recommandé</button>
|
||||
<button id="exportPng4x" class="btn btn-secondary">🖼️ PNG 4× (6336×1584)</button>
|
||||
<button id="exportPng1x" class="btn btn-secondary">🖼️ PNG 1× (1584×396)</button>
|
||||
<button id="exportPdf" class="btn btn-secondary">📄 PDF</button>
|
||||
<span class="hint">LinkedIn re-encode en JPEG basse qualité. Upload <b>2×</b> ou <b>4×</b> → leur downscale serveur préserve mieux le texte qu'un upload 1×.</span>
|
||||
</div>
|
||||
|
||||
<div class="banner">
|
||||
<!-- ========== LEFT : brand + tagline ========== -->
|
||||
<div class="left">
|
||||
<div class="brand-row">
|
||||
<svg class="gem-svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- 4 facettes avec opacités progressives + contour -->
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1" />
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8" />
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65" />
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48" />
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<span class="brand-name">Rubis<span class="suffix">sur l'ongle</span></span>
|
||||
</div>
|
||||
|
||||
<h1 class="tagline">
|
||||
Vos factures relancées <em>toutes seules</em> pendant que vous travaillez.
|
||||
</h1>
|
||||
|
||||
<div class="url">rubis.pro</div>
|
||||
</div>
|
||||
|
||||
<!-- ========== RIGHT : mini dashboard ========== -->
|
||||
<div class="right">
|
||||
<div class="preview">
|
||||
<div class="rubis-hero">
|
||||
<svg class="gem-big" viewBox="0 0 100 100">
|
||||
<!-- 4 facettes avec opacités progressives + contour -->
|
||||
<polygon points="50,6 6,50 50,50" fill="#9F1239" fill-opacity="1" />
|
||||
<polygon points="50,6 94,50 50,50" fill="#9F1239" fill-opacity="0.8" />
|
||||
<polygon points="6,50 50,50 50,94" fill="#9F1239" fill-opacity="0.65" />
|
||||
<polygon points="50,50 94,50 50,94" fill="#9F1239" fill-opacity="0.48" />
|
||||
<polygon points="50,6 94,50 50,94 6,50" fill="none" stroke="#9F1239" stroke-width="1.6" stroke-linejoin="round" />
|
||||
</svg>
|
||||
<div>
|
||||
<div class="rubis-count">124 rubis</div>
|
||||
<div class="rubis-sub">≈ <b>24 h 48</b> libérées ce mois</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpis">
|
||||
<div>
|
||||
<div class="kpi-label">Encaissé</div>
|
||||
<div class="kpi-value">14 320 €</div>
|
||||
<div class="kpi-delta">+ 2 800 € vs avril</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="kpi-label">DSO</div>
|
||||
<div class="kpi-value">38 j</div>
|
||||
<div class="kpi-delta">↘ −6 j depuis Rubis</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="activity">
|
||||
<span>✓ Facture <b>F-2024-035</b> encaissée</span>
|
||||
<time>10:02</time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Libs CDN pour export PDF + PNG -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/html-to-image@1.11.13/dist/html-to-image.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.2/dist/jspdf.umd.min.js"></script>
|
||||
|
||||
<script>
|
||||
const banner = document.querySelector(".banner");
|
||||
const btnPng1x = document.getElementById("exportPng1x");
|
||||
const btnPng2x = document.getElementById("exportPng2x");
|
||||
const btnPng4x = document.getElementById("exportPng4x");
|
||||
const btnPdf = document.getElementById("exportPdf");
|
||||
|
||||
// Dimensions logiques de la bannière (CSS)
|
||||
const BASE_W = 1584;
|
||||
const BASE_H = 396;
|
||||
|
||||
/**
|
||||
* Pourquoi rendre plus grand que 1584×396 ?
|
||||
* LinkedIn re-encode tous les uploads en JPEG basse qualité après
|
||||
* downscale serveur. Si on upload pile 1584×396, leur réencodage
|
||||
* détruit le texte fin. Si on upload 2× ou 4× plus grand, leur
|
||||
* downscale serveur a plus de pixels d'origine et le texte reste net.
|
||||
*
|
||||
* Recommandation 2026 (testée par la communauté) : uploader en 2× ou 3×
|
||||
* pour les bannières LinkedIn. Le PNG résultant fait 1-3 MB, largement
|
||||
* sous la limite 8 MB de LinkedIn.
|
||||
*/
|
||||
async function renderAtScale(scale) {
|
||||
await document.fonts.ready;
|
||||
// On laisse html-to-image rendre directement à la résolution voulue
|
||||
// (sans downscale canvas ensuite — le rastériseur du navigateur est
|
||||
// de meilleure qualité qu'un drawImage downscale).
|
||||
return await htmlToImage.toPng(banner, {
|
||||
width: BASE_W,
|
||||
height: BASE_H,
|
||||
canvasWidth: BASE_W * scale,
|
||||
canvasHeight: BASE_H * scale,
|
||||
pixelRatio: scale,
|
||||
backgroundColor: "#FAF7F2",
|
||||
cacheBust: true,
|
||||
});
|
||||
}
|
||||
|
||||
function makeExporter(btn, scale, label) {
|
||||
return async function () {
|
||||
btn.disabled = true;
|
||||
const original = btn.textContent;
|
||||
btn.textContent = "⏳ Génération…";
|
||||
try {
|
||||
const dataUrl = await renderAtScale(scale);
|
||||
const link = document.createElement("a");
|
||||
link.download = `rubis-linkedin-banner-${BASE_W * scale}x${BASE_H * scale}.png`;
|
||||
link.href = dataUrl;
|
||||
link.click();
|
||||
btn.textContent = `✓ ${label}`;
|
||||
setTimeout(() => {
|
||||
btn.textContent = original;
|
||||
btn.disabled = false;
|
||||
}, 2200);
|
||||
} catch (err) {
|
||||
console.error("Export PNG :", err);
|
||||
alert("Erreur export PNG : " + err.message);
|
||||
btn.disabled = false;
|
||||
btn.textContent = original;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
btnPng1x.addEventListener("click", makeExporter(btnPng1x, 1, "1× téléchargé"));
|
||||
btnPng2x.addEventListener("click", makeExporter(btnPng2x, 2, "2× téléchargé"));
|
||||
btnPng4x.addEventListener("click", makeExporter(btnPng4x, 4, "4× téléchargé"));
|
||||
|
||||
btnPdf.addEventListener("click", async function () {
|
||||
btnPdf.disabled = true;
|
||||
btnPdf.textContent = "⏳ Génération…";
|
||||
try {
|
||||
// Pour le PDF on rend en 2× pour avoir une image bien nette dedans
|
||||
const dataUrl = await renderAtScale(2);
|
||||
const { jsPDF } = window.jspdf;
|
||||
const pdf = new jsPDF({
|
||||
orientation: "landscape",
|
||||
unit: "px",
|
||||
format: [BASE_W, BASE_H],
|
||||
hotfixes: ["px_scaling"],
|
||||
});
|
||||
pdf.addImage(dataUrl, "PNG", 0, 0, BASE_W, BASE_H);
|
||||
pdf.save(`rubis-linkedin-banner-${BASE_W}x${BASE_H}.pdf`);
|
||||
btnPdf.textContent = "✓ PDF téléchargé";
|
||||
setTimeout(() => {
|
||||
btnPdf.textContent = "📄 PDF";
|
||||
btnPdf.disabled = false;
|
||||
}, 2200);
|
||||
} catch (err) {
|
||||
console.error("Export PDF :", err);
|
||||
alert("Erreur export PDF : " + err.message);
|
||||
btnPdf.disabled = false;
|
||||
btnPdf.textContent = "📄 PDF";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
x
Reference in New Issue
Block a user