From aca608e520709427275c4812c957f05cc5856ad3 Mon Sep 17 00:00:00 2001 From: kerboul Date: Sun, 31 May 2026 19:51:24 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20th=C3=A8me=20WhatsApp=20+=20fix=20envoi?= =?UTF-8?q?=20rich/compact=20+=20nav=20shop=20+=20refactor?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Theming - Thème global piloté par variables CSS (:root + [data-theme]) appliqué via un attribut data-theme sur la racine app. Ajout du thème "WhatsApp" (bulles + palette verte, bulle sortante #005c4b) sans nouveau composant message. - useTheme: type Theme étendu + THEME_LAYOUT (whatsapp = layout bulles). - MessageList: sélection du composant par layout avec garde de repli (fini le ). - Fix du thème "compact" cassé : nouveau MessageItemCompact.vue (variante dense). - Surfaces migrées en variables : fond app/chat, header, bouton d'envoi, bulles. Corrections - Bug envoi rich/fichier : le backend exigeait un content texte non vide même en mode HTML/CSS/JS. Validation par présence (texte OU rich OU piece jointe) ; le front n'envoie plus d'espace bidon. Plus besoin de faux texte. - Shop : suppression de "Tout voir", navigation forcee par categorie (defaut: Publicite). Refactor (lisibilite) - Parite perks backend (ip-colors, audio-alert, send-skin-*) ; /api/shop/me renvoie myPerks precalcule ; le front consomme directement (suppression de la derivation dupliquee + nettoyage d'un artefact de merge dans useMessages). - Coherence composable-singleton : myPerks lu via useMyPerks() partout. - Extraction du composer de HomePage vers ChatComposer.vue (HomePage = layout). - Helper type parseMeta() pour les metaJson (moins de any). - vue-tsc --noEmit : 0 erreur. Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/src/lib/perks.ts | 19 ++ backend/src/routes/messages.ts | 18 +- backend/src/routes/shop.ts | 6 +- frontend/src/components/ChatComposer.vue | 243 +++++++++++++++++ frontend/src/components/ChatHeader.vue | 4 +- frontend/src/components/MessageItemBubble.vue | 12 +- .../src/components/MessageItemCompact.vue | 95 +++++++ frontend/src/components/MessageList.vue | 17 +- frontend/src/components/SendButton.vue | 7 + frontend/src/composables/ipColor.ts | 5 +- frontend/src/composables/useMessages.ts | 39 +-- frontend/src/composables/useMeta.ts | 28 ++ frontend/src/composables/useShop.ts | 6 +- frontend/src/composables/useTheme.ts | 26 +- frontend/src/style.css | 33 ++- frontend/src/views/HomePage.vue | 246 ++---------------- frontend/src/views/ShopPage.vue | 23 +- 17 files changed, 524 insertions(+), 303 deletions(-) create mode 100644 frontend/src/components/ChatComposer.vue create mode 100644 frontend/src/components/MessageItemCompact.vue create mode 100644 frontend/src/composables/useMeta.ts diff --git a/backend/src/lib/perks.ts b/backend/src/lib/perks.ts index 3212bc9..c7f4dd1 100644 --- a/backend/src/lib/perks.ts +++ b/backend/src/lib/perks.ts @@ -25,6 +25,9 @@ export interface Perks { richHtmlcss?: boolean; richJs?: boolean; noFileLimit?: boolean; + ipColors?: boolean; + audioAlert?: boolean; + sendSkins?: { id: string; char: string; label?: string }[]; } const perksKey = (ip: string) => `xip:perks:${ip}`; @@ -88,6 +91,22 @@ export async function getPerksForIp(ip: string): Promise { case "no-file-limit": perks.noFileLimit = true; break; + case "ip-colors": + perks.ipColors = true; + break; + case "audio-alert": + perks.audioAlert = true; + break; + } + + // Send-button skins use a prefixed kind (send-skin-rocket, …), so they + // can't be matched by the switch above. + if (e.kind.startsWith("send-skin-")) { + (perks.sendSkins ??= []).push({ + id: e.kind, + char: meta.char ?? "?", + label: meta.label, + }); } } if (pets.length) perks.pets = pets.slice(0, 3); diff --git a/backend/src/routes/messages.ts b/backend/src/routes/messages.ts index e919f8a..429f185 100644 --- a/backend/src/routes/messages.ts +++ b/backend/src/routes/messages.ts @@ -66,17 +66,25 @@ messages.post("/", async (c) => { const ip = getClientIp(c); const body = await c.req.json<{ - content: string; + content?: string; parentId?: string; richMode?: "htmlcss" | "js"; richContent?: string; attachmentIds?: string[]; }>(); - if (!body.content || body.content.trim().length === 0) { - return c.json({ error: "Content is required" }, 400); + // A message is valid if it has ANY of: plain text, rich content, or attachments. + // (Rich-only and file-only messages are legitimate — no need for placeholder text.) + const hasContent = typeof body.content === "string" && body.content.trim().length > 0; + const hasRich = + !!body.richMode && !!body.richContent && body.richContent.trim().length > 0; + const hasAttachments = + Array.isArray(body.attachmentIds) && body.attachmentIds.length > 0; + + if (!hasContent && !hasRich && !hasAttachments) { + return c.json({ error: "Message vide" }, 400); } - if (body.content.length > 267) { + if (hasContent && body.content!.trim().length > 267) { return c.json({ error: "Content exceeds 267 characters" }, 400); } @@ -97,7 +105,7 @@ messages.post("/", async (c) => { richContent = body.richContent; } - const content = body.content.trim(); + const content = (body.content ?? "").trim(); const parentId = body.parentId ?? null; const message = await prisma.message.create({ diff --git a/backend/src/routes/shop.ts b/backend/src/routes/shop.ts index 21c832c..5e0f213 100644 --- a/backend/src/routes/shop.ts +++ b/backend/src/routes/shop.ts @@ -11,6 +11,7 @@ import { type PurchaseOptions, } from "../lib/catalog"; import { broadcast, broadcastToIp } from "../realtime"; +import { getPerksForIp } from "../lib/perks"; const shop = new Hono(); @@ -30,11 +31,12 @@ shop.get("/products/:id", async (c) => { // GET /api/shop/me — my balance + owned entitlements shop.get("/me", async (c) => { const ip = getClientIp(c); - const [wallet, entitlements] = await Promise.all([ + const [wallet, entitlements, myPerks] = await Promise.all([ getWallet(ip), getEntitlements(ip), + getPerksForIp(ip), ]); - return c.json({ wallet, entitlements }); + return c.json({ wallet, entitlements, myPerks }); }); // POST /api/shop/purchase { productId, options } diff --git a/frontend/src/components/ChatComposer.vue b/frontend/src/components/ChatComposer.vue new file mode 100644 index 0000000..5783fb1 --- /dev/null +++ b/frontend/src/components/ChatComposer.vue @@ -0,0 +1,243 @@ + +