Files
XIP/frontend/src/composables/useShop.ts
kerboul aca608e520
All checks were successful
Deploy XIP / deploy (push) Successful in 43s
feat: thème WhatsApp + fix envoi rich/compact + nav shop + refactor
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>
2026-05-31 19:51:24 +02:00

134 lines
3.5 KiB
TypeScript

import { ref } from 'vue';
import { useWallet } from './useWallet';
import { refreshMyPerks } from './useMessages';
import { parseMeta, type ProductMeta } from './useMeta';
/** Marketplace client: catalogue, my entitlements, purchase flow. */
export interface Product {
id: string;
category: string;
name: string;
subtitle?: string | null;
kind: string;
basePrice: number; // centi-credits
promoPrice?: number | null;
badge?: string | null;
stockLimit?: number | null;
stockSold: number;
sortOrder: number;
metaJson?: string | null;
}
export interface Entitlement {
id: string;
ip: string;
kind: string;
active: boolean;
expiresAt?: string | null;
metaJson?: string | null;
createdAt: string;
}
export interface PurchaseOptions {
plan?: 'monthly' | 'annual';
durationDays?: number;
format?: 'static' | 'gif';
url?: string;
petDesign?: string;
petChar?: string;
petPosition?: 'left' | 'right' | 'both';
}
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
export function useShop() {
const products = ref<Product[]>([]);
const entitlements = ref<Entitlement[]>([]);
const loading = ref(false);
const buying = ref<string | null>(null); // productId currently being purchased
const lastError = ref<string | null>(null);
const lastSuccess = ref<string | null>(null);
const { fetchWallet } = useWallet();
async function fetchProducts(): Promise<void> {
loading.value = true;
try {
const res = await fetch(`${API_URL}/api/shop/products`);
if (res.ok) products.value = (await res.json()) as Product[];
} finally {
loading.value = false;
}
}
async function fetchMe(): Promise<void> {
try {
const res = await fetch(`${API_URL}/api/shop/me`);
if (res.ok) {
const data = await res.json();
entitlements.value = data.entitlements ?? [];
}
} catch {
/* ignore */
}
}
function owns(kind: string): boolean {
return entitlements.value.some((e) => e.kind === kind && e.active);
}
function petCount(): number {
return entitlements.value.filter((e) => e.kind === 'pet' && e.active).length;
}
function ownedPetChars(): string[] {
return entitlements.value
.filter((e) => e.kind === 'pet' && e.active)
.map((e) => parseMeta<ProductMeta>(e.metaJson).char ?? '')
.filter(Boolean);
}
async function purchase(productId: string, options: PurchaseOptions = {}): Promise<boolean> {
buying.value = productId;
lastError.value = null;
lastSuccess.value = null;
try {
const res = await fetch(`${API_URL}/api/shop/purchase`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ productId, options }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) {
lastError.value = data.error || 'Achat impossible';
return false;
}
lastSuccess.value = `Acheté : ${productId}`;
// Refresh wallet + my entitlements + myPerks (WS also pushes wallet, this is belt-and-braces).
await Promise.all([fetchWallet(), fetchMe(), fetchProducts(), refreshMyPerks()]);
return true;
} catch {
lastError.value = 'Réseau indisponible';
return false;
} finally {
buying.value = null;
}
}
return {
products,
entitlements,
loading,
buying,
lastError,
lastSuccess,
fetchProducts,
fetchMe,
owns,
petCount,
ownedPetChars,
purchase,
};
}