add des betises
All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s

This commit is contained in:
ordinarthur 2026-05-09 20:11:33 +02:00
parent fb248553a8
commit c3c9dbb408
5 changed files with 550 additions and 1 deletions

View 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

View File

@ -17,6 +17,7 @@ body:json {
"fullName": "{{fullName}}"
}
}
script:post-response {

View File

@ -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 {

View 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:
}

View 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>