diff --git a/backend/prisma/seed.ts b/backend/prisma/seed.ts index 0995327..0fd9c02 100644 --- a/backend/prisma/seed.ts +++ b/backend/prisma/seed.ts @@ -145,6 +145,77 @@ const PRODUCTS = [ sortOrder: 90, metaJson: JSON.stringify({ cooldownMs: 60000, maxDurationMs: 5000 }), }, + // ── Cosmetics: IP color + send button skins ────────────────────────────── + { + id: "ip-colors", + category: "cosmetiques", + name: "Palette IP", + subtitle: "Personnalise la couleur de ton adresse IP dans le chat", + kind: "unlock", + basePrice: 299, + sortOrder: 46, + metaJson: JSON.stringify({}), + }, + { + id: "send-skin-honker", + category: "cosmetiques", + name: "Doigt d'honneur", + subtitle: "Bouton d'envoi qui exprime tout", + kind: "send-skin", + basePrice: 149, + sortOrder: 47, + metaJson: JSON.stringify({ char: "🖕", label: "Doigt d'honneur" }), + }, + { + id: "send-skin-skull", + category: "cosmetiques", + name: "Crâne", + subtitle: "Envoyer avec style... macabre", + kind: "send-skin", + basePrice: 149, + sortOrder: 48, + metaJson: JSON.stringify({ char: "💀", label: "Crâne" }), + }, + { + id: "send-skin-rocket", + category: "cosmetiques", + name: "Fusée", + subtitle: "Tes messages décollent", + kind: "send-skin", + basePrice: 149, + sortOrder: 49, + metaJson: JSON.stringify({ char: "🚀", label: "Fusée" }), + }, + { + id: "send-skin-ghost", + category: "cosmetiques", + name: "Fantôme", + subtitle: "Boo !", + kind: "send-skin", + basePrice: 149, + sortOrder: 50, + metaJson: JSON.stringify({ char: "👻", label: "Fantôme" }), + }, + { + id: "send-skin-bomb", + category: "cosmetiques", + name: "Bombe", + subtitle: "Message explosif", + kind: "send-skin", + basePrice: 149, + sortOrder: 51, + metaJson: JSON.stringify({ char: "💣", label: "Bombe" }), + }, + { + id: "send-skin-sword", + category: "cosmetiques", + name: "Épée", + subtitle: "Tranche le silence", + kind: "send-skin", + basePrice: 149, + sortOrder: 52, + metaJson: JSON.stringify({ char: "⚔️", label: "Épée" }), + }, ] as const; // ── Ad inventory (the 4 hardcoded joke ads, now real data) ────────────────── diff --git a/backend/src/lib/catalog.ts b/backend/src/lib/catalog.ts index 6b47936..5299917 100644 --- a/backend/src/lib/catalog.ts +++ b/backend/src/lib/catalog.ts @@ -163,7 +163,7 @@ export async function purchase( if ((await countActiveEntitlements(ip, product.id)) >= 1) throw new PurchaseError("Déjà débloqué", 409); grants.push({ kind: product.id }); - if (product.id === "element-skin") visiblePerkChanged = false; // viewer-scoped + if (product.id === "element-skin" || product.id === "ip-colors") visiblePerkChanged = false; // viewer-scoped break; } case "consumable": { @@ -176,6 +176,14 @@ export async function purchase( } break; } + case "send-skin": { + if ((await countActiveEntitlements(ip, product.id)) >= 1) + throw new PurchaseError("Déjà débloqué", 409); + let skinMeta: any = {}; + try { skinMeta = product.metaJson ? JSON.parse(product.metaJson) : {}; } catch {} + grants.push({ kind: product.id, meta: skinMeta }); + break; + } case "bundle": { // Cosmetic bundle: Style Doré + 1 pet. if ((await countActiveEntitlements(ip, "style-dore")) < 1) diff --git a/frontend/src/components/SendButton.vue b/frontend/src/components/SendButton.vue index 88400fb..5bd5bfb 100644 --- a/frontend/src/components/SendButton.vue +++ b/frontend/src/components/SendButton.vue @@ -9,7 +9,8 @@ @click="$emit('send')" @contextmenu.prevent="onRightClick" > - {{ activeSkinChar }} + @@ -27,6 +28,12 @@ defineEmits<{ send: [] }>(); const { prefs } = useCustomStyles(); const { myPerks } = useMyPerks(); +const activeSkinChar = computed(() => { + const skinId = prefs.sendSkin; + if (!skinId) return null; + return myPerks.value.sendSkins?.find((s) => s.id === skinId)?.char ?? null; +}); + const btnStyle = computed(() => { const p = SEND_BUTTON_PRESETS[prefs.sendButton]; return { background: p.bg, color: p.color, borderRadius: p.radius }; @@ -64,4 +71,5 @@ function onRightClick(e: MouseEvent): void { .send-btn:hover:not(:disabled) { filter: brightness(1.3); } .send-btn:active:not(:disabled) { filter: brightness(0.85); } .send-btn:disabled { opacity: 0.35; cursor: not-allowed; } +.skin-char { font-size: 18px; line-height: 1; } diff --git a/frontend/src/components/shop/MesPersos.vue b/frontend/src/components/shop/MesPersos.vue index 9b17c2f..2d6637c 100644 --- a/frontend/src/components/shop/MesPersos.vue +++ b/frontend/src/components/shop/MesPersos.vue @@ -42,11 +42,44 @@ - -
+ +
+

+ 🖱️ Skin du bouton d'envoi + Achetez un skin dans le shop +

+ +

Aucun skin possédé pour l'instant.

+
+ + +

🎨 Couleur de mon IP - 🔒 Skin d'éléments requis + 🔒 Palette IP requise

IP : {{ myIp }}

@@ -55,7 +88,7 @@ :key="opt.value" class="style-tile" :class="{ 'style-tile--active': currentIpColor === opt.value }" - :disabled="!myPerks.elementSkin" + :disabled="!myPerks.ipColors" @click="setIpColor(opt.value)" type="button" > @@ -144,6 +177,7 @@ const ownedPets = computed(() => { }); }); const hasPets = computed(() => ownedPets.value.length > 0); +const hasSendSkins = computed(() => (myPerks.value.sendSkins?.length ?? 0) > 0); const activePet = computed(() => myIp.value && myIp.value in prefs.ipPets ? prefs.ipPets[myIp.value] : (ownedPets.value[0]?.char ?? '') ); diff --git a/frontend/src/components/shop/ProductCard.vue b/frontend/src/components/shop/ProductCard.vue index ff404f8..e7cb8ee 100644 --- a/frontend/src/components/shop/ProductCard.vue +++ b/frontend/src/components/shop/ProductCard.vue @@ -48,11 +48,11 @@
- +
-
- -
+
+ + +
+
{{ meta.char }}
@@ -107,13 +106,14 @@