// SEO GUY Web V2 — composants partagés
const { useState, useEffect, useRef } = React;
function Icon({ name, size = 18 }) {
const paths = {
home: "M3 10.5 12 3l9 7.5M5 9.5V21h5v-6h4v6h5V9.5",
docs: "M7 3h7l5 5v13H7zM14 3v5h5M9.5 13h6M9.5 16.5h6",
chart: "M4 20V4M4 20h16M8 16v-5M12 16V7M16 16v-8",
gear: "M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5M12 3v2.5M12 18.5V21M5.6 5.6l1.8 1.8M16.6 16.6l1.8 1.8M3 12h2.5M18.5 12H21M5.6 18.4l1.8-1.8M16.6 7.4l1.8-1.8",
check: "M5 12.5l4.5 4.5L19 7.5",
bolt: "M13 3 5 13h6l-1 8 8-10h-6z",
spark: "M12 3v6M12 15v6M3 12h6M15 12h6",
arrow: "M5 12h14M13 6l6 6-6 6",
calendar: "M4 6h16v15H4zM4 10h16M8 3v4M16 3v4",
eye: "M2 12s3.5-6.5 10-6.5S22 12 22 12s-3.5 6.5-10 6.5S2 12 2 12M12 9.5a2.5 2.5 0 1 0 0 5 2.5 2.5 0 0 0 0-5",
target: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8M12 11.5a.5.5 0 1 0 0 1 .5.5 0 0 0 0-1",
mic: "M12 3a3 3 0 0 0-3 3v6a3 3 0 0 0 6 0V6a3 3 0 0 0-3-3M5 11v1a7 7 0 0 0 14 0v-1M12 19v3M8.5 22h7",
users: "M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8M23 21v-2a4 4 0 0 0-3-3.87M16 3.13a4 4 0 0 1 0 7.75",
trending: "M3 17l4-8 4 4 4-6 4 4",
plus: "M12 5v14M5 12h14",
globe: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18M3 12h18",
star: "M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01z",
wand: "M15 4V2M15 16v-2M8 9h2M20 9h2M17.8 11.8 19 13M17.8 6.2 19 5M3 21l9-9M12.2 6.2 11 5",
card: "M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 0 0 3-3V8a3 3 0 0 0-3-3H6a3 3 0 0 0-3 3v8a3 3 0 0 0 3 3",
user: "M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2M12 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8",
send: "M22 2L11 13M22 2l-7 20-4-9-9-4 20-7z",
menu: "M4 7h16M4 12h16M4 17h16",
close: "M6 6l12 12M18 6 6 18",
image: "M4 5h16v14H4zM4 15l4.2-4.2 3 3L16 9.5 20 14.5M9 9a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3",
};
const p = paths[name];
if (!p) return null;
return (
);
}
function StatusPill({ children, tone = "live" }) {
return {tone === "live" && }{children};
}
function Pill({ children, tone }) {
return {children};
}
/** Anneau score SEO : jusqu'à 85 % en bleu, 15 % bonus humanisation en or. */
const SEO_SCORE_HUMAN_NOTE = "Contenu suffisamment humanisé pour performer";
const SEO_SCORE_CAP = 85;
const SEO_SCORE_BONUS = 15;
const SEO_RING_BLUE = "#2563eb";
const SEO_RING_GOLD = "#f5c518";
let __sgRingUid = 0;
function ScoreRing({ value, size = 44 }) {
const v = Math.min(100, Math.max(0, Number(value) || 0));
const r = size / 2 - 4;
const c = 2 * Math.PI * r;
const cx = size / 2;
const cy = size / 2;
const seoPct = Math.min(v, SEO_SCORE_CAP);
const blueLen = (seoPct / 100) * c;
const goldLen = (SEO_SCORE_BONUS / 100) * c;
const goldRotate = -90 + (SEO_SCORE_CAP / 100) * 360;
const uidRef = React.useRef(null);
if (!uidRef.current) uidRef.current = `sg-ring-gold-${++__sgRingUid}`;
const uid = uidRef.current;
return (
{Math.round(v)}
);
}
function SeoScoreHumanNote({ className = "" }) {
return (
{SEO_SCORE_HUMAN_NOTE}
);
}
const NAV_MAIN = [
{ id: "home", label: "Accueil", icon: "home" },
{ id: "assistants", label: "Assistants Premium", icon: "wand" },
{ id: "performance", label: "Performance", icon: "chart" },
{ id: "articles", label: "Articles", icon: "docs" },
];
const NAV_ADVANCED = [
{ id: "settings", label: "Réglages", icon: "gear" },
{ id: "gsc", label: "Search Console", icon: "globe" },
{ id: "config", label: "Ciblage marketing", icon: "target" },
{ id: "billing", label: "Abonnements", icon: "card" },
];
function Sidebar({ active, onNav, showMascot, onAccount, siteLabel, planLabel, mobileOpen, onClose }) {
function navigate(id) {
onNav(id);
onClose?.();
}
return (
<>
>
);
}
function MobileTopBar({ onMenu, title }) {
return (
S
SEOGUY
{title ? {title} : }
);
}
function PageHeader({ greeting, title, sub, status, actions }) {
return (
);
}
function HeroBand({ guyImage, eyebrow, title, tip, pills, actions, showMascot }) {
if (!showMascot) return null;
return (
{eyebrow &&
{eyebrow}
}
{title &&
{title}
}
{tip &&
{tip}
}
{pills?.length > 0 && (
{pills.map((p) => {p})}
)}
{actions &&
{actions}
}
);
}
function AlertList({ alerts, onNav }) {
if (!alerts?.length) return null;
return (
{alerts.map((a) => (
))}
);
}
function KpiCard({ icon, label, value, delta, up }) {
const isEmoji = typeof icon === "string" && icon.length <= 4;
return (
{isEmoji ? {icon} :
}
{delta}
{label}
{value}
);
}
/** Éditeur HTML léger (contentEditable) — conserve le balisage. */
function HtmlEditor({ value, onChange, disabled }) {
const ref = useRef(null);
const skipSync = useRef(false);
const ready = useRef(false);
useEffect(() => {
if (!ref.current) return;
if (!ready.current) {
ref.current.innerHTML = value || "";
ready.current = true;
return;
}
if (skipSync.current) {
skipSync.current = false;
return;
}
if (document.activeElement === ref.current) return;
const next = value || "";
if (ref.current.innerHTML !== next) ref.current.innerHTML = next;
}, [value]);
function emit() {
if (!ref.current) return;
skipSync.current = true;
onChange?.(ref.current.innerHTML);
}
function run(cmd, arg) {
if (disabled) return;
ref.current?.focus();
try {
document.execCommand(cmd, false, arg);
} catch {
/* ignore */
}
emit();
}
function setBlock(tag) {
// execCommand attend souvent la forme , etc.
run("formatBlock", `<${tag}>`);
}
function addLink() {
const url = window.prompt("URL du lien :", "https://");
if (!url) return;
run("createLink", url);
}
// Titre 1 = H2, Titre 2 = H3, Titre 3 = H4 (H1 réservé au titre de l'article)
const tools = [
{ label: "Titre 1", title: "Titre 1 (H2)", onClick: () => setBlock("h2") },
{ label: "Titre 2", title: "Titre 2 (H3)", onClick: () => setBlock("h3") },
{ label: "Titre 3", title: "Titre 3 (H4)", onClick: () => setBlock("h4") },
{ label: "¶", title: "Paragraphe", onClick: () => setBlock("p") },
{ label: "« »", title: "Citation", onClick: () => setBlock("blockquote") },
{ sep: true },
{ label: "B", title: "Gras", className: "is-bold", onClick: () => run("bold") },
{ label: "I", title: "Italique", className: "is-italic", onClick: () => run("italic") },
{ label: "S", title: "Souligné", className: "is-underline", onClick: () => run("underline") },
{ sep: true },
{ label: "A−", title: "Réduire", onClick: () => run("fontSize", "2") },
{ label: "A+", title: "Agrandir", onClick: () => run("fontSize", "5") },
{ label: "🔗", title: "Lien", onClick: addLink },
{ label: "• Liste", title: "Liste à puces", onClick: () => run("insertUnorderedList") },
];
return (
{tools.map((t, i) =>
t.sep ? (
) : (
),
)}
);
}
const BLOG_COVER_HINT = "Paysage 16:9, idéalement 1600 × 900 px. JPEG, PNG ou WebP, 8 Mo max. Guy recadre au format blog.";
const BLOG_COVER_W = 1600;
const BLOG_COVER_H = 900;
const BLOG_COVER_FILE_MAX = 8 * 1024 * 1024;
async function resizeBlogCoverFile(file) {
if (!file || !/^image\/(jpeg|jpg|png|webp)$/i.test(file.type)) {
throw new Error("Formats acceptés : JPEG, PNG ou WebP.");
}
if (file.size > BLOG_COVER_FILE_MAX) {
throw new Error("Image trop lourde (max 8 Mo). Réduisez-la avant l’envoi.");
}
const bitmap = await createImageBitmap(file);
const canvas = document.createElement("canvas");
canvas.width = BLOG_COVER_W;
canvas.height = BLOG_COVER_H;
const ctx = canvas.getContext("2d");
if (!ctx) {
bitmap.close();
throw new Error("Canvas indisponible.");
}
const targetRatio = BLOG_COVER_W / BLOG_COVER_H;
const srcRatio = bitmap.width / bitmap.height;
let sx = 0;
let sy = 0;
let sw = bitmap.width;
let sh = bitmap.height;
if (srcRatio > targetRatio) {
sw = sh * targetRatio;
sx = (bitmap.width - sw) / 2;
} else {
sh = sw / targetRatio;
sy = (bitmap.height - sh) / 2;
}
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, BLOG_COVER_W, BLOG_COVER_H);
bitmap.close();
let quality = 0.82;
let dataUrl = canvas.toDataURL("image/jpeg", quality);
while (dataUrl.length > 5_500_000 && quality > 0.5) {
quality -= 0.08;
dataUrl = canvas.toDataURL("image/jpeg", quality);
}
if (dataUrl.length > 5_500_000) {
throw new Error("Image trop détaillée après compression. Choisissez un fichier plus léger.");
}
return dataUrl;
}
async function resizeProductRefFile(file) {
if (!file || !/^image\/(jpeg|jpg|png|webp)$/i.test(file.type)) {
throw new Error("Formats acceptés : JPEG, PNG ou WebP.");
}
if (file.size > BLOG_COVER_FILE_MAX) {
throw new Error("Image trop lourde (max 8 Mo). Réduisez-la avant l’envoi.");
}
const bitmap = await createImageBitmap(file);
const maxEdge = 1600;
let w = bitmap.width;
let h = bitmap.height;
if (w > maxEdge || h > maxEdge) {
const scale = maxEdge / Math.max(w, h);
w = Math.round(w * scale);
h = Math.round(h * scale);
}
const canvas = document.createElement("canvas");
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
if (!ctx) {
bitmap.close();
throw new Error("Canvas indisponible.");
}
ctx.drawImage(bitmap, 0, 0, w, h);
bitmap.close();
let quality = 0.82;
let dataUrl = canvas.toDataURL("image/jpeg", quality);
while (dataUrl.length > 5_500_000 && quality > 0.5) {
quality -= 0.08;
dataUrl = canvas.toDataURL("image/jpeg", quality);
}
if (dataUrl.length > 5_500_000) {
throw new Error("Image trop détaillée après compression. Choisissez un fichier plus léger.");
}
return dataUrl;
}
Object.assign(window, {
Icon, StatusPill, Pill, ScoreRing, SeoScoreHumanNote, SEO_SCORE_HUMAN_NOTE, Sidebar, MobileTopBar, PageHeader, HeroBand, AlertList, KpiCard,
HtmlEditor,
NAV_MAIN, NAV_ADVANCED,
resizeBlogCoverFile, BLOG_COVER_HINT,
resizeProductRefFile,
});