All checks were successful
Build & Deploy Web / build-and-deploy (push) Successful in 27s
518 lines
16 KiB
HTML
518 lines
16 KiB
HTML
<!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>
|