feat: update styles and enhance pet purchase flow in marketplace components

This commit is contained in:
arussac
2026-05-31 15:15:48 +02:00
parent 02bba16285
commit 21e35107c7
11 changed files with 99 additions and 99 deletions

View File

@@ -132,8 +132,6 @@ export async function purchase(
break;
}
case "pet": {
if ((await countActiveEntitlements(ip, "pet")) >= 3)
throw new PurchaseError("Maximum 3 pets actifs", 409);
const char = options.petChar ?? "♥";
grants.push({
kind: "pet",

View File

@@ -115,11 +115,11 @@ onMounted(fetchAds);
font-weight: bold;
margin: 0;
}
.ad-brand--blue { color: #5555cc; text-shadow: 0 0 8px #4444aa; }
.ad-brand--green { color: #33aa55; text-shadow: 0 0 8px #225533; }
.ad-brand--purple { color: #9944dd; text-shadow: 0 0 8px #6622aa; }
.ad-brand--user { color: #ffcc44; text-shadow: 0 0 8px #aa8822; }
.ad-brand--casino { color: #ff5533; text-shadow: 0 0 8px #aa2200; }
.ad-brand--blue { color: #4455aa; }
.ad-brand--green { color: #336644; }
.ad-brand--purple { color: #6633aa; }
.ad-brand--user { color: #998833; }
.ad-brand--casino { color: #884433; }
.ad-sub {
font-family: Arial, sans-serif;

View File

@@ -57,8 +57,7 @@ const { ip, freeMode, displayBalance } = useWallet();
font-family: Arial, sans-serif;
font-size: 18px;
font-weight: bold;
color: #00eeff;
text-shadow: 0 0 10px #00ccff99;
color: #7ab8cc;
}
.chat-label {
@@ -72,14 +71,13 @@ const { ip, freeMode, displayBalance } = useWallet();
width: 8px;
height: 8px;
border-radius: 50%;
background: #00ff88;
box-shadow: 0 0 6px #00ff44;
background: #44aa66;
}
.online-count {
font-family: Arial, sans-serif;
font-size: 11px;
color: #33ff66;
color: #557766;
}
.me-ip {
@@ -98,26 +96,25 @@ const { ip, freeMode, displayBalance } = useWallet();
padding: 3px 10px;
font-family: 'Courier New', monospace;
}
.balance-coin { color: #ffcc44; font-size: 11px; }
.balance-val { color: #ffdd66; font-size: 13px; font-weight: bold; text-shadow: 0 0 8px #ffaa0055; }
.balance-coin { color: #aa8833; font-size: 11px; }
.balance-val { color: #ccaa44; font-size: 13px; font-weight: bold; }
.balance-unit { color: #886633; font-size: 9px; }
.balance--free .balance-val { color: #33ff99; text-shadow: 0 0 8px #00ff6655; }
.balance--free .balance-coin { color: #33ff99; }
.balance--free .balance-val { color: #44aa77; }
.balance--free .balance-coin { color: #44aa77; }
.shop-link {
font-family: Arial, sans-serif;
font-size: 12px;
font-weight: bold;
color: #00eeff;
color: #6699aa;
text-decoration: none;
border: 1px solid #00eeff55;
border: 1px solid #33445566;
border-radius: 12px;
padding: 4px 12px;
transition: background 0.15s, box-shadow 0.15s;
transition: background 0.15s;
}
.shop-link:hover {
background: #00eeff14;
box-shadow: 0 0 10px #00ccff44;
background: #1a2530;
}
.channel-badge {

View File

@@ -47,7 +47,7 @@ onMounted(fetchAds);
background: #100400;
border: 2px solid #ff2200;
border-radius: 6px;
box-shadow: 0 0 18px #ff220055;
box-shadow: none;
}
/* ── En-tête rouge ── */
@@ -64,7 +64,7 @@ onMounted(fetchAds);
font-size: 15px;
font-weight: bold;
color: #ff5533;
text-shadow: 0 0 8px #ff2200;
margin: 0;
}
@@ -87,7 +87,7 @@ onMounted(fetchAds);
font-size: 32px;
font-weight: bold;
color: #ffdd00;
text-shadow: 0 0 14px #99660099;
margin: 0;
}
@@ -117,7 +117,7 @@ onMounted(fetchAds);
font-size: 30px;
font-weight: bold;
color: #ffffff;
text-shadow: 0 0 10px #ffdd00;
}
/* ── CTA ── */
@@ -136,13 +136,11 @@ onMounted(fetchAds);
cursor: pointer;
text-align: center;
text-decoration: none;
text-shadow: 0 0 6px #ff2200;
box-shadow: 0 0 8px #ff220044;
transition: box-shadow 0.15s;
transition: background 0.15s;
}
.casino-cta:hover {
box-shadow: 0 0 16px #ff220088;
}
.disclaimer {

View File

@@ -95,8 +95,8 @@ const items = computed<Chip[]>(() => {
display: flex;
align-items: stretch;
background: #0a0a12;
border-bottom: 1px solid #00eeff33;
box-shadow: inset 0 -1px 0 #00eeff14, 0 2px 14px #00000066;
border-bottom: 1px solid #1a1a2a;
box-shadow: 0 2px 8px #00000066;
overflow: hidden;
}
@@ -110,15 +110,14 @@ const items = computed<Chip[]>(() => {
gap: 7px;
padding: 0 14px;
background: #0e0e18;
border-right: 1px solid #00eeff33;
box-shadow: 6px 0 12px #0a0a12;
border-right: 1px solid #1a1a2a;
box-shadow: 4px 0 8px #0a0a12;
}
.ticker-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #00ff88;
box-shadow: 0 0 8px #00ff66;
background: #44996655;
animation: blink 1.5s ease-in-out infinite;
}
.ticker-badge-txt {
@@ -126,17 +125,14 @@ const items = computed<Chip[]>(() => {
font-size: 11px;
font-weight: bold;
letter-spacing: 2px;
color: #00ff88;
text-shadow: 0 0 8px #00ff4466;
color: #448866;
}
.ticker.is-off .ticker-dot {
background: #ff3344;
box-shadow: 0 0 8px #ff2233;
background: #884444;
animation: none;
}
.ticker.is-off .ticker-badge-txt {
color: #ff5566;
text-shadow: none;
color: #885555;
}
@keyframes blink {
0%, 100% { opacity: 1; }
@@ -206,10 +202,10 @@ const items = computed<Chip[]>(() => {
color: #50506e;
}
.chip--cyan .chip-val { color: #00eeff; text-shadow: 0 0 9px #00ccff55; }
.chip--green .chip-val { color: #33ff77; text-shadow: 0 0 9px #00ff4455; }
.chip--magenta .chip-val { color: #ff44cc; text-shadow: 0 0 9px #ff22aa55; }
.chip--orange .chip-val { color: #ffaa44; text-shadow: 0 0 9px #ff880055; }
.chip--cyan .chip-val { color: #5599aa; }
.chip--green .chip-val { color: #447755; }
.chip--magenta .chip-val { color: #885588; }
.chip--orange .chip-val { color: #997744; }
/* Accessibilité : pas de défilement si l'utilisateur le refuse */
@media (prefers-reduced-motion: reduce) {

View File

@@ -81,13 +81,20 @@
<span class="price-now">{{ fmt(effectivePrice) }}</span>
<span class="price-unit">cr</span>
</div>
<!-- Pets: redirige vers Mes Persos au lieu d'acheter -->
<!-- Pets: bouton acheter + lien Mes Persos -->
<template v-if="product.kind === 'pet'">
<button
class="buy"
:disabled="disabled"
@click="onBuy"
type="button"
>{{ buyLabel }}</button>
<button
v-if="product.kind === 'pet'"
class="buy buy--perso"
@click="$emit('goPerso')"
type="button"
> Mes Persos</button>
</template>
<button
v-else
class="buy"
@@ -176,16 +183,14 @@ const ownedAlready = computed(() => {
return false;
});
const petFull = computed(() => props.product.kind === 'pet' && props.petCount >= 3);
const soldOut = computed(() => props.product.stockLimit != null && props.product.stockSold >= props.product.stockLimit);
const disabled = computed(() => props.buying || ownedAlready.value || petFull.value || soldOut.value);
const disabled = computed(() => props.buying || ownedAlready.value || soldOut.value);
const buyLabel = computed(() => {
if (props.buying) return '...';
if (soldOut.value) return 'Épuisé';
if (ownedAlready.value) return 'Possédé ✓';
if (petFull.value) return 'Max 3 pets';
return props.freeMode ? 'Obtenir (gratuit)' : 'Acheter';
});
@@ -242,7 +247,7 @@ function onBuy(): void {
letter-spacing: 0.5px;
padding: 3px 9px;
border-radius: 8px;
box-shadow: 0 0 10px #ff226688;
box-shadow: none;
}
.card-head { display: flex; gap: 12px; align-items: flex-start; }
@@ -256,7 +261,7 @@ function onBuy(): void {
}
.prev-ip { font-family: 'Courier New', monospace; font-size: 12px; font-weight: bold; }
.prev-plain { color: #666688; }
.prev-gold { color: #ffdd44; text-shadow: 0 0 8px #ffaa00cc; }
.prev-gold { color: #aa8833; }
.prev-arrow { color: #444466; }
.opts { display: flex; flex-direction: column; gap: 8px; }
@@ -283,7 +288,7 @@ function onBuy(): void {
aspect-ratio: 1; background: #0c0c16; border: 1px solid #20203a; border-radius: 6px;
font-size: 18px; color: #ccccee; cursor: pointer; display: flex; align-items: center; justify-content: center;
}
.pet-cell.active { border-color: #ff44cc; box-shadow: 0 0 8px #ff44cc55; }
.pet-cell.active { border-color: #8844aa; }
.pet-pos { display: flex; gap: 6px; }
.stock { display: flex; flex-direction: column; gap: 4px; }
@@ -291,18 +296,18 @@ function onBuy(): void {
.stock-fill { height: 100%; background: linear-gradient(90deg, #ffaa00, #ff4444); }
.stock-txt { font-size: 10px; color: #886644; }
.card-foot { display: flex; align-items: center; justify-content: space-between; margin-top: auto; padding-top: 6px; }
.card-foot { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 6px; margin-top: auto; padding-top: 6px; }
.price { display: flex; align-items: baseline; gap: 6px; }
.price-old { font-size: 12px; color: #555; text-decoration: line-through; }
.price-now { font-size: 20px; font-weight: bold; color: #ffdd66; font-family: 'Courier New', monospace; text-shadow: 0 0 10px #ffaa0044; }
.price-now { font-size: 20px; font-weight: bold; color: #ccaa44; font-family: 'Courier New', monospace; }
.price-unit { font-size: 11px; color: #886633; }
.buy {
background: #004488; border: 1px solid #0066aa; color: #00ddff;
font-size: 13px; font-weight: bold; padding: 9px 18px; border-radius: 20px; cursor: pointer;
box-shadow: 0 0 12px #00448855; transition: background 0.15s, box-shadow 0.15s;
transition: background 0.15s;
}
.buy:hover:not(:disabled) { background: #005599; box-shadow: 0 0 18px #00ddff55; }
.buy:hover:not(:disabled) { background: #1a4466; }
.buy:disabled { opacity: 0.4; cursor: not-allowed; box-shadow: none; }
.buy--perso {

View File

@@ -1,5 +1,5 @@
/** Couleurs assignées de façon déterministe à chaque adresse IP */
const PALETTE = ['#666688', '#00ddff', '#ff00cc', '#00ee77', '#ff8844'] as const;
const PALETTE = ['#7777aa', '#4499bb', '#aa4499', '#338866', '#aa6633'] as const;
export function getIpColor(ip: string): string {
// djb2 hash
@@ -11,7 +11,7 @@ export function getIpColor(ip: string): string {
}
export function getIpGlow(color: string): string {
return color === '#666688' ? 'none' : `0 0 8px ${color}80`;
return 'none';
}
/** Gold skin (Style Doré) — overrides the djb2 palette for owners. */
@@ -28,6 +28,5 @@ export function getIpColorWithPerks(ip: string, perks?: PerkLike | null): string
}
export function getIpGlowWithPerks(ip: string, perks?: PerkLike | null): string {
if (perks?.skin === 'gold') return `0 0 10px ${GOLD}cc`;
return getIpGlow(getIpColor(ip));
return 'none';
}

View File

@@ -39,6 +39,37 @@ export interface Message extends Reply {
const API_URL = import.meta.env.VITE_API_URL ?? 'http://localhost:3000';
/** Refresh the viewer's own perks from the server (callable from anywhere). */
export async function refreshMyPerks(): Promise<void> {
try {
const res = await fetch(`${API_URL}/api/shop/me`);
if (!res.ok) return;
const { entitlements } = (await res.json()) as {
entitlements: { kind: string; metaJson?: string | null }[];
};
const p: Perks = {};
const pets: { char: string; position: 'left' | 'right' | 'both' }[] = [];
for (const e of entitlements) {
let meta: any = {};
try { meta = e.metaJson ? JSON.parse(e.metaJson) : {}; } catch { /* */ }
if (e.kind === 'noads') { p.noads = true; if (meta.plan === 'annual') p.badge = true; }
if (e.kind === 'style-dore') p.skin = 'gold';
if (e.kind === 'pet' && meta.char) pets.push({ char: meta.char, position: meta.position ?? 'left' });
if (e.kind === 'element-skin') p.elementSkin = true;
if (e.kind === 'rich-htmlcss') p.richHtmlcss = true;
if (e.kind === 'rich-js') p.richJs = true;
if (e.kind === 'no-file-limit') p.noFileLimit = true;
if (e.kind === 'audio-alert') p.audioAlert = true;
}
if (pets.length) p.pets = pets;
myPerks.value = p;
const { ip } = useWallet();
if (ip.value) setPerks(ip.value, p);
} catch {
/* ignore */
}
}
export function useMessages() {
const messages = ref<Message[]>([]);
const loading = ref(false);
@@ -102,32 +133,7 @@ export function useMessages() {
// myPerks is module-level; this ref is the same reference.
async function fetchMyPerks(): Promise<void> {
try {
const res = await fetch(`${API_URL}/api/shop/me`);
if (!res.ok) return;
const { entitlements } = (await res.json()) as {
entitlements: { kind: string; metaJson?: string | null }[];
};
const p: Perks = {};
const pets: { char: string; position: 'left' | 'right' | 'both' }[] = [];
for (const e of entitlements) {
let meta: any = {};
try { meta = e.metaJson ? JSON.parse(e.metaJson) : {}; } catch { /* */ }
if (e.kind === 'noads') { p.noads = true; if (meta.plan === 'annual') p.badge = true; }
if (e.kind === 'style-dore') p.skin = 'gold';
if (e.kind === 'pet' && meta.char) pets.push({ char: meta.char, position: meta.position ?? 'left' });
if (e.kind === 'element-skin') p.elementSkin = true;
if (e.kind === 'rich-htmlcss') p.richHtmlcss = true;
if (e.kind === 'rich-js') p.richJs = true;
if (e.kind === 'no-file-limit') p.noFileLimit = true;
if (e.kind === 'audio-alert') p.audioAlert = true;
}
if (pets.length) p.pets = pets.slice(0, 3);
myPerks.value = p;
if (myIp.value) setPerks(myIp.value, p);
} catch {
/* ignore */
}
return refreshMyPerks();
}
const { stats, connected, sendTyping } = useRealtime({

View File

@@ -1,5 +1,6 @@
import { ref } from 'vue';
import { useWallet } from './useWallet';
import { refreshMyPerks } from './useMessages';
/** Marketplace client: catalogue, my entitlements, purchase flow. */
@@ -96,8 +97,8 @@ export function useShop() {
return false;
}
lastSuccess.value = `Acheté : ${productId}`;
// Refresh wallet + my entitlements (WS also pushes wallet, this is belt-and-braces).
await Promise.all([fetchWallet(), fetchMe(), fetchProducts()]);
// 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';

View File

@@ -281,7 +281,7 @@ async function submit(): Promise<void> {
}
.icon-btn:hover { background: #1c1c2e; }
.icon-btn--alert { border-color: #aa3344; }
.icon-btn--alert:hover { background: #2a1418; box-shadow: 0 0 10px #ff224455; }
.icon-btn--alert:hover { background: #1e1218; }
.field-wrap { flex: 1; position: relative; display: flex; align-items: center; }
.input-field {

View File

@@ -156,17 +156,17 @@ onUnmounted(() => { if (timer) clearInterval(timer); });
border: 1px solid #00aaff44; border-radius: 10px; padding: 4px 10px;
}
.sh-back:hover { background: #00aaff14; }
.sh-title { font-size: 18px; font-weight: bold; color: #00eeff; text-shadow: 0 0 10px #00ccff99; }
.sh-title { font-size: 18px; font-weight: bold; color: #6699aa; }
.sh-sub { font-size: 13px; color: #8888aa; }
.sh-right { display: flex; align-items: center; gap: 12px; }
.sh-ip { font-family: 'Courier New', monospace; font-size: 11px; color: #5566aa; }
.sh-balance { font-family: 'Courier New', monospace; font-size: 15px; font-weight: bold; color: #ffdd66; text-shadow: 0 0 10px #ffaa0044; }
.sh-balance.free { color: #33ff99; text-shadow: 0 0 10px #00ff6644; }
.sh-balance { font-family: 'Courier New', monospace; font-size: 15px; font-weight: bold; color: #ccaa44; }
.sh-balance.free { color: #44aa77; }
.sh-cr { font-size: 10px; color: #886633; }
.sh-topup {
background: #1a2a1a; border: 1px solid #33aa55; color: #44dd77;
font-size: 12px; font-weight: bold; padding: 6px 14px; border-radius: 16px; cursor: pointer;
box-shadow: 0 0 10px #33aa5533;
box-shadow: none;
}
.sh-topup:hover { background: #234a23; }