feat: thème WhatsApp + fix envoi rich/compact + nav shop + refactor
All checks were successful
Deploy XIP / deploy (push) Successful in 43s
All checks were successful
Deploy XIP / deploy (push) Successful in 43s
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 <component :is="undefined">). - 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<T>() pour les metaJson (moins de any). - vue-tsc --noEmit : 0 erreur. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<Perks> {
|
||||
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);
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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 }
|
||||
|
||||
Reference in New Issue
Block a user