Compare commits
2 Commits
3c4a292db2
...
d50f06d65a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d50f06d65a | ||
|
|
d88b71b2c6 |
@@ -145,6 +145,77 @@ const PRODUCTS = [
|
|||||||
sortOrder: 90,
|
sortOrder: 90,
|
||||||
metaJson: JSON.stringify({ cooldownMs: 60000, maxDurationMs: 5000 }),
|
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;
|
] as const;
|
||||||
|
|
||||||
// ── Ad inventory (the 4 hardcoded joke ads, now real data) ──────────────────
|
// ── Ad inventory (the 4 hardcoded joke ads, now real data) ──────────────────
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ export async function purchase(
|
|||||||
if ((await countActiveEntitlements(ip, product.id)) >= 1)
|
if ((await countActiveEntitlements(ip, product.id)) >= 1)
|
||||||
throw new PurchaseError("Déjà débloqué", 409);
|
throw new PurchaseError("Déjà débloqué", 409);
|
||||||
grants.push({ kind: product.id });
|
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;
|
break;
|
||||||
}
|
}
|
||||||
case "consumable": {
|
case "consumable": {
|
||||||
@@ -176,6 +176,14 @@ export async function purchase(
|
|||||||
}
|
}
|
||||||
break;
|
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": {
|
case "bundle": {
|
||||||
// Cosmetic bundle: Style Doré + 1 pet.
|
// Cosmetic bundle: Style Doré + 1 pet.
|
||||||
if ((await countActiveEntitlements(ip, "style-dore")) < 1)
|
if ((await countActiveEntitlements(ip, "style-dore")) < 1)
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
@click="$emit('send')"
|
@click="$emit('send')"
|
||||||
@contextmenu.prevent="onRightClick"
|
@contextmenu.prevent="onRightClick"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
<span v-if="activeSkinChar" class="skin-char">{{ activeSkinChar }}</span>
|
||||||
|
<svg v-else width="18" height="18" viewBox="0 0 18 18" fill="none" aria-hidden="true">
|
||||||
<polygon points="4,5 15,9 4,13 7,9" fill="currentColor" />
|
<polygon points="4,5 15,9 4,13 7,9" fill="currentColor" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@@ -27,6 +28,12 @@ defineEmits<{ send: [] }>();
|
|||||||
const { prefs } = useCustomStyles();
|
const { prefs } = useCustomStyles();
|
||||||
const { myPerks } = useMyPerks();
|
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 btnStyle = computed(() => {
|
||||||
const p = SEND_BUTTON_PRESETS[prefs.sendButton];
|
const p = SEND_BUTTON_PRESETS[prefs.sendButton];
|
||||||
return { background: p.bg, color: p.color, borderRadius: p.radius };
|
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:hover:not(:disabled) { filter: brightness(1.3); }
|
||||||
.send-btn:active:not(:disabled) { filter: brightness(0.85); }
|
.send-btn:active:not(:disabled) { filter: brightness(0.85); }
|
||||||
.send-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
.send-btn:disabled { opacity: 0.35; cursor: not-allowed; }
|
||||||
|
.skin-char { font-size: 18px; line-height: 1; }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -42,11 +42,44 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- ── Couleur de l'IP ────────────────────────────────────────── -->
|
<!-- ── Skin du bouton d'envoi ───────────────────────────────────── -->
|
||||||
<section class="section" :class="{ locked: !myPerks.elementSkin }">
|
<section class="section" :class="{ locked: !hasSendSkins }">
|
||||||
|
<h2 class="section-title">
|
||||||
|
🖱️ Skin du bouton d'envoi
|
||||||
|
<span v-if="!hasSendSkins" class="lock-badge">Achetez un skin dans le shop</span>
|
||||||
|
</h2>
|
||||||
|
<template v-if="hasSendSkins">
|
||||||
|
<div class="style-grid">
|
||||||
|
<button
|
||||||
|
class="style-tile"
|
||||||
|
:class="{ 'style-tile--active': prefs.sendSkin === '' }"
|
||||||
|
@click="prefs.sendSkin = ''"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span class="style-swatch" style="font-size:14px">►</span>
|
||||||
|
<span class="style-label">Défaut</span>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
v-for="s in myPerks.sendSkins"
|
||||||
|
:key="s.id"
|
||||||
|
class="style-tile"
|
||||||
|
:class="{ 'style-tile--active': prefs.sendSkin === s.id }"
|
||||||
|
@click="prefs.sendSkin = s.id"
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<span class="style-swatch" style="font-size:20px">{{ s.char }}</span>
|
||||||
|
<span class="style-label">{{ s.label ?? s.id.replace('send-skin-', '') }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<p v-else class="section-sub">Aucun skin possédé pour l'instant.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- ── Couleur de l'IP ─────────────────────────────────────── -->
|
||||||
|
<section class="section" :class="{ locked: !myPerks.ipColors }">
|
||||||
<h2 class="section-title">
|
<h2 class="section-title">
|
||||||
🎨 Couleur de mon IP
|
🎨 Couleur de mon IP
|
||||||
<span v-if="!myPerks.elementSkin" class="lock-badge">🔒 Skin d'éléments requis</span>
|
<span v-if="!myPerks.ipColors" class="lock-badge">🔒 Palette IP requise</span>
|
||||||
</h2>
|
</h2>
|
||||||
<p v-if="myIp" class="section-sub">IP : <code class="code-ip" :style="ipPreviewStyle">{{ myIp }}</code></p>
|
<p v-if="myIp" class="section-sub">IP : <code class="code-ip" :style="ipPreviewStyle">{{ myIp }}</code></p>
|
||||||
<div class="style-grid">
|
<div class="style-grid">
|
||||||
@@ -55,7 +88,7 @@
|
|||||||
:key="opt.value"
|
:key="opt.value"
|
||||||
class="style-tile"
|
class="style-tile"
|
||||||
:class="{ 'style-tile--active': currentIpColor === opt.value }"
|
:class="{ 'style-tile--active': currentIpColor === opt.value }"
|
||||||
:disabled="!myPerks.elementSkin"
|
:disabled="!myPerks.ipColors"
|
||||||
@click="setIpColor(opt.value)"
|
@click="setIpColor(opt.value)"
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
@@ -144,6 +177,7 @@ const ownedPets = computed(() => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
const hasPets = computed(() => ownedPets.value.length > 0);
|
const hasPets = computed(() => ownedPets.value.length > 0);
|
||||||
|
const hasSendSkins = computed(() => (myPerks.value.sendSkins?.length ?? 0) > 0);
|
||||||
const activePet = computed(() =>
|
const activePet = computed(() =>
|
||||||
myIp.value && myIp.value in prefs.ipPets ? prefs.ipPets[myIp.value] : (ownedPets.value[0]?.char ?? '')
|
myIp.value && myIp.value in prefs.ipPets ? prefs.ipPets[myIp.value] : (ownedPets.value[0]?.char ?? '')
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -48,11 +48,11 @@
|
|||||||
<input v-model="url" class="opt-input" type="text" placeholder="URL de destination (optionnel)" />
|
<input v-model="url" class="opt-input" type="text" placeholder="URL de destination (optionnel)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Options : Pet (grille + position) -->
|
<!-- Options : Pet (grille des designs non encore possédés) -->
|
||||||
<div v-if="product.kind === 'pet'" class="opts">
|
<div v-if="product.kind === 'pet'" class="opts">
|
||||||
<div class="pet-grid">
|
<div class="pet-grid">
|
||||||
<button
|
<button
|
||||||
v-for="d in designs"
|
v-for="d in availableDesigns"
|
||||||
:key="d.id"
|
:key="d.id"
|
||||||
class="pet-cell"
|
class="pet-cell"
|
||||||
:class="{ active: petDesign === d.id }"
|
:class="{ active: petDesign === d.id }"
|
||||||
@@ -60,12 +60,11 @@
|
|||||||
type="button"
|
type="button"
|
||||||
>{{ d.char }}</button>
|
>{{ d.char }}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="pet-pos">
|
|
||||||
<label v-for="pos in positions" :key="pos" class="opt-radio opt-radio--sm" :class="{ active: petPosition === pos }">
|
|
||||||
<input type="radio" :value="pos" v-model="petPosition" />
|
|
||||||
<span>{{ posLabel(pos) }}</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Preview : Skin de bouton -->
|
||||||
|
<div v-if="product.kind === 'send-skin'" class="send-skin-preview">
|
||||||
|
<div class="skin-btn-demo">{{ meta.char }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Stock limité -->
|
<!-- Stock limité -->
|
||||||
@@ -107,13 +106,14 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref } from 'vue';
|
import { computed, ref, watch } from 'vue';
|
||||||
import type { Product, PurchaseOptions } from '@/composables/useShop';
|
import type { Product, PurchaseOptions } from '@/composables/useShop';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
product: Product;
|
product: Product;
|
||||||
buying: boolean;
|
buying: boolean;
|
||||||
owns: (kind: string) => boolean;
|
owns: (kind: string) => boolean;
|
||||||
|
ownedPetChars: string[];
|
||||||
petCount: number;
|
petCount: number;
|
||||||
freeMode: boolean;
|
freeMode: boolean;
|
||||||
}>();
|
}>();
|
||||||
@@ -141,11 +141,19 @@ const url = ref('');
|
|||||||
|
|
||||||
// Pet
|
// Pet
|
||||||
const designs = computed(() => meta.value.designs ?? []);
|
const designs = computed(() => meta.value.designs ?? []);
|
||||||
const positions = computed<string[]>(() => meta.value.positions ?? ['left', 'right', 'both']);
|
|
||||||
const petDesign = ref<string>('');
|
const petDesign = ref<string>('');
|
||||||
const petPosition = ref<'left' | 'right' | 'both'>('left');
|
const availableDesigns = computed(() =>
|
||||||
|
designs.value.filter((d: any) => !props.ownedPetChars.includes(d.char))
|
||||||
|
);
|
||||||
|
watch(availableDesigns, (ds) => {
|
||||||
|
if (ds.length > 0 && !ds.find((d: any) => d.id === petDesign.value)) {
|
||||||
|
petDesign.value = (ds[0] as any).id;
|
||||||
|
}
|
||||||
|
}, { immediate: true });
|
||||||
|
|
||||||
const icon = computed(() => {
|
const icon = computed(() => {
|
||||||
|
if (props.product.id === 'ip-colors') return '🎨';
|
||||||
|
if (props.product.kind === 'send-skin') return meta.value.char ?? '🖱️';
|
||||||
switch (props.product.kind) {
|
switch (props.product.kind) {
|
||||||
case 'ad-frame': return '📣';
|
case 'ad-frame': return '📣';
|
||||||
case 'subscription': return '🚫';
|
case 'subscription': return '🚫';
|
||||||
@@ -180,6 +188,7 @@ const ownedAlready = computed(() => {
|
|||||||
if (k === 'rich') return props.owns(props.product.id);
|
if (k === 'rich') return props.owns(props.product.id);
|
||||||
if (k === 'unlock') return props.owns(props.product.id);
|
if (k === 'unlock') return props.owns(props.product.id);
|
||||||
if (k === 'ad-frame') return props.owns('ad-frame');
|
if (k === 'ad-frame') return props.owns('ad-frame');
|
||||||
|
if (k === 'send-skin') return props.owns(props.product.id);
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -201,9 +210,6 @@ const stockPct = computed(() =>
|
|||||||
function fmt(centi: number): string {
|
function fmt(centi: number): string {
|
||||||
return (centi / 100).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
return (centi / 100).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 });
|
||||||
}
|
}
|
||||||
function posLabel(p: string): string {
|
|
||||||
return p === 'left' ? 'Gauche' : p === 'right' ? 'Droite' : 'Les deux';
|
|
||||||
}
|
|
||||||
|
|
||||||
function onBuy(): void {
|
function onBuy(): void {
|
||||||
const options: PurchaseOptions = {};
|
const options: PurchaseOptions = {};
|
||||||
@@ -214,9 +220,8 @@ function onBuy(): void {
|
|||||||
options.url = url.value || undefined;
|
options.url = url.value || undefined;
|
||||||
}
|
}
|
||||||
if (props.product.kind === 'pet' || props.product.id === 'bundle-cosmetic') {
|
if (props.product.kind === 'pet' || props.product.id === 'bundle-cosmetic') {
|
||||||
const d = designs.value.find((x: any) => x.id === petDesign.value) ?? designs.value[0];
|
const d = availableDesigns.value.find((x: any) => x.id === petDesign.value) ?? availableDesigns.value[0];
|
||||||
if (d) { options.petDesign = d.id; options.petChar = d.char; }
|
if (d) { options.petDesign = (d as any).id; options.petChar = (d as any).char; }
|
||||||
options.petPosition = petPosition.value;
|
|
||||||
}
|
}
|
||||||
emit('buy', props.product.id, options);
|
emit('buy', props.product.id, options);
|
||||||
}
|
}
|
||||||
@@ -291,6 +296,14 @@ function onBuy(): void {
|
|||||||
.pet-cell.active { border-color: #8844aa; }
|
.pet-cell.active { border-color: #8844aa; }
|
||||||
.pet-pos { display: flex; gap: 6px; }
|
.pet-pos { display: flex; gap: 6px; }
|
||||||
|
|
||||||
|
.send-skin-preview { display: flex; justify-content: center; padding: 8px 0; }
|
||||||
|
.skin-btn-demo {
|
||||||
|
width: 52px; height: 52px; border-radius: 50%;
|
||||||
|
background: #151525; border: 1px solid #30306a;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
.stock { display: flex; flex-direction: column; gap: 4px; }
|
.stock { display: flex; flex-direction: column; gap: 4px; }
|
||||||
.stock-bar { height: 6px; background: #1a1a2a; border-radius: 3px; overflow: hidden; }
|
.stock-bar { height: 6px; background: #1a1a2a; border-radius: 3px; overflow: hidden; }
|
||||||
.stock-fill { height: 100%; background: linear-gradient(90deg, #ffaa00, #ff4444); }
|
.stock-fill { height: 100%; background: linear-gradient(90deg, #ffaa00, #ff4444); }
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const PET_OPTIONS: { value: string; label: string }[] = [
|
|||||||
|
|
||||||
export interface CustomStylePrefs {
|
export interface CustomStylePrefs {
|
||||||
sendButton: SendButtonKey;
|
sendButton: SendButtonKey;
|
||||||
|
sendSkin: string; // send-skin product id, or '' for default arrow
|
||||||
adFrame: AdFrameKey;
|
adFrame: AdFrameKey;
|
||||||
ipColors: Record<string, string>; // ip → hex or 'auto'
|
ipColors: Record<string, string>; // ip → hex or 'auto'
|
||||||
ipPets: Record<string, string>; // ip → emoji or ''
|
ipPets: Record<string, string>; // ip → emoji or ''
|
||||||
@@ -57,7 +58,7 @@ export interface CustomStylePrefs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaults(): CustomStylePrefs {
|
function defaults(): CustomStylePrefs {
|
||||||
return { sendButton: 'default', adFrame: 'default', ipColors: {}, ipPets: {}, chatBgUrl: '' };
|
return { sendButton: 'default', sendSkin: '', adFrame: 'default', ipColors: {}, ipPets: {}, chatBgUrl: '' };
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(): CustomStylePrefs {
|
function load(): CustomStylePrefs {
|
||||||
|
|||||||
@@ -55,8 +55,13 @@ export async function refreshMyPerks(): Promise<void> {
|
|||||||
if (e.kind === 'noads') { p.noads = true; if (meta.plan === 'annual') p.badge = true; }
|
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 === 'style-dore') p.skin = 'gold';
|
||||||
if (e.kind === 'pet' && meta.char) pets.push({ char: meta.char, position: meta.position ?? 'left' });
|
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 === 'element-skin') p.elementSkin = true; if (e.kind === 'ip-colors') p.ipColors = true;
|
||||||
if (e.kind === 'rich-htmlcss') p.richHtmlcss = true;
|
if (e.kind.startsWith('send-skin-')) {
|
||||||
|
let meta2: any = {};
|
||||||
|
try { meta2 = e.metaJson ? JSON.parse(e.metaJson) : {}; } catch {}
|
||||||
|
if (!p.sendSkins) p.sendSkins = [];
|
||||||
|
p.sendSkins.push({ id: e.kind, char: meta2.char ?? '?', label: meta2.label });
|
||||||
|
} if (e.kind === 'rich-htmlcss') p.richHtmlcss = true;
|
||||||
if (e.kind === 'rich-js') p.richJs = true;
|
if (e.kind === 'rich-js') p.richJs = true;
|
||||||
if (e.kind === 'no-file-limit') p.noFileLimit = true;
|
if (e.kind === 'no-file-limit') p.noFileLimit = true;
|
||||||
if (e.kind === 'audio-alert') p.audioAlert = true;
|
if (e.kind === 'audio-alert') p.audioAlert = true;
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ export interface Perks {
|
|||||||
elementSkin?: boolean;
|
elementSkin?: boolean;
|
||||||
richHtmlcss?: boolean;
|
richHtmlcss?: boolean;
|
||||||
richJs?: boolean;
|
richJs?: boolean;
|
||||||
|
ipColors?: boolean;
|
||||||
|
sendSkins?: { id: string; char: string; label?: string }[];
|
||||||
noFileLimit?: boolean;
|
noFileLimit?: boolean;
|
||||||
audioAlert?: boolean;
|
audioAlert?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,16 @@ export function useShop() {
|
|||||||
return entitlements.value.filter((e) => e.kind === 'pet' && e.active).length;
|
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) => {
|
||||||
|
try { return (JSON.parse(e.metaJson ?? '{}') as any).char ?? ''; }
|
||||||
|
catch { return ''; }
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
async function purchase(productId: string, options: PurchaseOptions = {}): Promise<boolean> {
|
async function purchase(productId: string, options: PurchaseOptions = {}): Promise<boolean> {
|
||||||
buying.value = productId;
|
buying.value = productId;
|
||||||
lastError.value = null;
|
lastError.value = null;
|
||||||
@@ -119,6 +129,7 @@ export function useShop() {
|
|||||||
fetchMe,
|
fetchMe,
|
||||||
owns,
|
owns,
|
||||||
petCount,
|
petCount,
|
||||||
|
ownedPetChars,
|
||||||
purchase,
|
purchase,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,36 @@
|
|||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://fonts.gstatic.com/s/lato/v25/S6uyw4BMUTPHjxAwXjeu.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://fonts.gstatic.com/s/lato/v25/S6uyw4BMUTPHjx4wXg.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(https://fonts.gstatic.com/s/lato/v25/S6u9w4BMUTPHh6UVSwaPGR_p.woff2) format('woff2');
|
||||||
|
unicode-range: U+0100-02BA, U+02BD-02C5, U+02C7-02CC, U+02CE-02D7, U+02DD-02FF, U+0304, U+0308, U+0329, U+1D00-1DBF, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Lato';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 700;
|
||||||
|
src: url(https://fonts.gstatic.com/s/lato/v25/S6u9w4BMUTPHh6UVSwiPGQ.woff2) format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
*, *::before, *::after {
|
*, *::before, *::after {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -10,4 +43,5 @@ body,
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #080808;
|
background: #080808;
|
||||||
|
font-family: 'Lato', sans-serif;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@
|
|||||||
:buying="buying === p.id"
|
:buying="buying === p.id"
|
||||||
:owns="owns"
|
:owns="owns"
|
||||||
:pet-count="petCount()"
|
:pet-count="petCount()"
|
||||||
|
:owned-pet-chars="ownedPetChars()"
|
||||||
:free-mode="freeMode"
|
:free-mode="freeMode"
|
||||||
@buy="onBuy"
|
@buy="onBuy"
|
||||||
@go-perso="activeCat = 'perso'"
|
@go-perso="activeCat = 'perso'"
|
||||||
@@ -78,7 +79,7 @@ import { useWallet } from '@/composables/useWallet';
|
|||||||
import ProductCard from '@/components/shop/ProductCard.vue';
|
import ProductCard from '@/components/shop/ProductCard.vue';
|
||||||
import MesPersos from '@/components/shop/MesPersos.vue';
|
import MesPersos from '@/components/shop/MesPersos.vue';
|
||||||
|
|
||||||
const { products, loading, buying, lastError, lastSuccess, fetchProducts, fetchMe, owns, petCount, purchase } = useShop();
|
const { products, loading, buying, lastError, lastSuccess, fetchProducts, fetchMe, owns, petCount, ownedPetChars, purchase } = useShop();
|
||||||
const { ip, freeMode, displayBalance, fetchWallet, topUp: walletTopUp } = useWallet();
|
const { ip, freeMode, displayBalance, fetchWallet, topUp: walletTopUp } = useWallet();
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
@@ -92,11 +93,19 @@ const categories = [
|
|||||||
];
|
];
|
||||||
const activeCat = ref('all');
|
const activeCat = ref('all');
|
||||||
|
|
||||||
const visibleProducts = computed(() =>
|
const visibleProducts = computed(() => {
|
||||||
activeCat.value === 'all'
|
const chars = ownedPetChars();
|
||||||
|
const base = activeCat.value === 'all'
|
||||||
? products.value
|
? products.value
|
||||||
: products.value.filter((p) => p.category === activeCat.value)
|
: products.value.filter((p) => p.category === activeCat.value);
|
||||||
);
|
return base.filter((p) => {
|
||||||
|
if (p.kind !== 'pet') return true;
|
||||||
|
try {
|
||||||
|
const designs: any[] = JSON.parse((p as any).metaJson ?? '{}').designs ?? [];
|
||||||
|
return designs.some((d) => !chars.includes(d.char));
|
||||||
|
} catch { return true; }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
async function onBuy(productId: string, options: PurchaseOptions): Promise<void> {
|
async function onBuy(productId: string, options: PurchaseOptions): Promise<void> {
|
||||||
await purchase(productId, options);
|
await purchase(productId, options);
|
||||||
|
|||||||
Reference in New Issue
Block a user