multi carte
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Nyavokevin 2025-09-26 17:57:55 +03:00
parent cf89535c14
commit fa94499268
9 changed files with 559 additions and 41 deletions

View File

@ -30,7 +30,7 @@ class StripeController extends Controller
$priceIds = [ $priceIds = [
3 => 'price_1S51zxGaZ3yeYkzWYb0wSt4j', 3 => 'price_1S51zxGaZ3yeYkzWYb0wSt4j',
4 => 'price_1S5464GaZ3yeYkzWh8RuJfab', 21 => 'price_1S5464GaZ3yeYkzWh8RuJfab',
]; ];
if (!isset($priceIds[$count])) { if (!isset($priceIds[$count])) {

View File

@ -8,9 +8,13 @@
@custom-variant dark (&:is(.dark *)); @custom-variant dark (&:is(.dark *));
@theme inline { @theme inline {
--font-heading: 'Montserrat', sans-serif; --font-title: 'Montserrat ExtraBold', sans-serif;
--font-body: 'Montserrat', sans-serif; --font-sub-title: 'Poppins', cursive;
--font-accent: 'Poppins', cursive; --font-text: 'Roboto', sans-serif;
--font-body: var(--font-text);
--font-heading: var(--font-title);
--font-accent: var(--font-sub-title);
--font-sans: var(--font-body); --font-sans: var(--font-body);
@ -95,10 +99,10 @@
:root { :root {
/* Brand variables */ /* Brand variables */
--c-black: #000000; --c-black: #000000;
--c-green: #9ACE8A; --c-green: #9ace8a;
--c-purple: #7D5BA6; /* softer purple primary */ --c-purple: #7d5ba6; /* softer purple primary */
--c-gold: #F0BA2D; --c-gold: #f0ba2d;
--c-white: #FFFFFF; --c-white: #ffffff;
/* Additional accent stops for hero gradient */ /* Additional accent stops for hero gradient */
--c-magenta: #d94f93; --c-magenta: #d94f93;
--c-red: #e54b4d; --c-red: #e54b4d;
@ -124,7 +128,7 @@
--muted: #eee7f7; --muted: #eee7f7;
--muted-foreground: #5b4b72; --muted-foreground: #5b4b72;
--accent: #B89BD6; --accent: #b89bd6;
--accent-foreground: #1f1630; --accent-foreground: #1f1630;
--destructive: #ef4444; --destructive: #ef4444;
@ -135,7 +139,7 @@
--ring: var(--c-purple); --ring: var(--c-purple);
--chart-1: var(--c-purple); --chart-1: var(--c-purple);
--chart-2: #B89BD6; --chart-2: #b89bd6;
--chart-3: var(--c-green); --chart-3: var(--c-green);
--chart-4: #7a6b91; --chart-4: #7a6b91;
--chart-5: #523f6a; --chart-5: #523f6a;
@ -147,7 +151,7 @@
--sidebar-foreground: #1f1630; --sidebar-foreground: #1f1630;
--sidebar-primary: var(--c-purple); --sidebar-primary: var(--c-purple);
--sidebar-primary-foreground: var(--c-white); --sidebar-primary-foreground: var(--c-white);
--sidebar-accent: #B89BD6; --sidebar-accent: #b89bd6;
--sidebar-accent-foreground: #1f1630; --sidebar-accent-foreground: #1f1630;
--sidebar-border: #e2d7f2; --sidebar-border: #e2d7f2;
--sidebar-ring: var(--c-purple); --sidebar-ring: var(--c-purple);
@ -173,7 +177,7 @@
--muted: #3a2d49; --muted: #3a2d49;
--muted-foreground: #c8bfe0; --muted-foreground: #c8bfe0;
--accent: #B89BD6; --accent: #b89bd6;
--accent-foreground: #1f1630; --accent-foreground: #1f1630;
--destructive: #ff7676; --destructive: #ff7676;
@ -184,7 +188,7 @@
--ring: #9c7ca5; --ring: #9c7ca5;
--chart-1: var(--c-purple); --chart-1: var(--c-purple);
--chart-2: #B89BD6; --chart-2: #b89bd6;
--chart-3: var(--c-green); --chart-3: var(--c-green);
--chart-4: #7a6b91; --chart-4: #7a6b91;
--chart-5: #523f6a; --chart-5: #523f6a;
@ -193,7 +197,7 @@
--sidebar-foreground: #f1ecf7; --sidebar-foreground: #f1ecf7;
--sidebar-primary: var(--c-purple); --sidebar-primary: var(--c-purple);
--sidebar-primary-foreground: var(--c-white); --sidebar-primary-foreground: var(--c-white);
--sidebar-accent: #B89BD6; --sidebar-accent: #b89bd6;
--sidebar-accent-foreground: #1f1630; --sidebar-accent-foreground: #1f1630;
--sidebar-border: #4c3a60; --sidebar-border: #4c3a60;
--sidebar-ring: #9c7ca5; --sidebar-ring: #9c7ca5;
@ -229,8 +233,20 @@
font-family: var(--font-accent), cursive; font-family: var(--font-accent), cursive;
} }
.font-title {
font-family: var(--font-title), sans-serif;
}
.font-sub-title {
font-family: var(--font-sub-title), cursive;
}
.font-text {
font-family: var(--font-text), sans-serif;
}
.citadel-script { .citadel-script {
font-family: var(--font-accent); font-family: var(--font-accent), cursive;
} }
.bg-blur { .bg-blur {
@ -250,7 +266,7 @@
/* Softer lilac gradient for headings */ /* Softer lilac gradient for headings */
.text-accent-gradient { .text-accent-gradient {
background: linear-gradient(45deg, #B89BD6, #7D5BA6); background: linear-gradient(45deg, #b89bd6, #7d5ba6);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
background-clip: text; background-clip: text;
@ -300,3 +316,11 @@
font-style: normal; font-style: normal;
font-display: swap; font-display: swap;
} }
@font-face {
font-family: 'Roboto';
src: url('@/fonts/Roboto/static/Roboto-Medium.ttf') format('truetype');
font-weight: 400;
font-style: normal;
font-display: swap;
}

View File

@ -61,7 +61,7 @@
<style scoped> <style scoped>
.citadel-script { .citadel-script {
font-family: 'Dancing Script', cursive; font-family: var(--font-sub-title), cursive;
font-weight: 700; font-weight: 700;
} }

View File

@ -2,7 +2,7 @@
import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue'; import CardShuffleTemplate from '@/components/template/CardShuffleTemplate.vue';
import { Card } from '@/types/cart'; import { Card } from '@/types/cart';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { computed, ref, watchEffect } from 'vue'; import { computed, onMounted, ref, watchEffect } from 'vue';
const props = defineProps<{ const props = defineProps<{
drawCount?: number; // Optional: number of cards expected to be drawn drawCount?: number; // Optional: number of cards expected to be drawn
@ -20,9 +20,53 @@ const drawingIndex = ref<number | null>(null);
const revealedCards = ref<Card[]>([]); const revealedCards = ref<Card[]>([]);
const isFlipped = ref<boolean[]>([]); const isFlipped = ref<boolean[]>([]);
const isRestacking = ref(false); const isRestacking = ref(false);
const isAutoDrawing = ref(false);
const autoDrawProgress = ref(0);
const remainingDeckSize = computed(() => Math.max(0, deckSize - revealedCards.value.length)); const remainingDeckSize = computed(() => Math.max(0, deckSize - revealedCards.value.length));
const isFinished = computed(() => !!props.drawnCards && revealedCards.value.length >= props.drawnCards.length); const isFinished = computed(() => !!props.drawnCards && revealedCards.value.length >= props.drawnCards.length);
const shouldAutoDraw = computed(() => props.drawnCards?.length > 15);
console.log(props.drawnCards?.length);
// Auto-draw cards with beautiful animation
const startAutoDraw = async () => {
if (!props.drawnCards || isAutoDrawing.value) return;
isAutoDrawing.value = true;
isSpread.value = false;
const totalCards = props.drawnCards.length;
const delayBetweenCards = 200; // ms between each card
for (let i = 0; i < totalCards; i++) {
if (!isAutoDrawing.value) break; // Allow cancellation
autoDrawProgress.value = (i / totalCards) * 100;
// Add card with animation
const nextCard = props.drawnCards[i];
revealedCards.value.push(nextCard);
isFlipped.value.push(false);
// Wait before next card
if (i < totalCards - 1) {
await new Promise((resolve) => setTimeout(resolve, delayBetweenCards));
}
}
autoDrawProgress.value = 100;
isAutoDrawing.value = false;
// Auto-flip all cards after drawing
setTimeout(() => {
for (let i = 0; i < revealedCards.value.length; i++) {
setTimeout(() => {
isFlipped.value[i] = true;
}, i * 150);
}
}, 500);
};
const startDraw = (index: number) => { const startDraw = (index: number) => {
if (!props.drawnCards || revealedCards.value.length >= props.drawnCards.length) return; if (!props.drawnCards || revealedCards.value.length >= props.drawnCards.length) return;
@ -50,9 +94,15 @@ const startDraw = (index: number) => {
}; };
const handleDeckClick = () => { const handleDeckClick = () => {
if (isDrawing.value) return; if (isDrawing.value || isAutoDrawing.value) return;
if (isFinished.value) return; if (isFinished.value) return;
// If auto-draw is available and not started, start it
if (shouldAutoDraw.value && !isAutoDrawing.value && revealedCards.value.length === 0) {
startAutoDraw();
return;
}
// First click spreads the deck // First click spreads the deck
if (!isSpread.value) { if (!isSpread.value) {
isSpread.value = true; isSpread.value = true;
@ -75,7 +125,7 @@ const handleDeckClick = () => {
const onCardClick = (i: number, e?: MouseEvent) => { const onCardClick = (i: number, e?: MouseEvent) => {
// Only handle and stop propagation when spread; otherwise let the deck click handler spread the deck // Only handle and stop propagation when spread; otherwise let the deck click handler spread the deck
if (!isSpread.value || isDrawing.value) return; if (!isSpread.value || isDrawing.value || isAutoDrawing.value) return;
if (e) e.stopPropagation(); if (e) e.stopPropagation();
if (!props.drawnCards || revealedCards.value.length >= props.drawnCards.length) return; if (!props.drawnCards || revealedCards.value.length >= props.drawnCards.length) return;
startDraw(i); startDraw(i);
@ -116,12 +166,26 @@ watchEffect(() => {
drawingIndex.value = null; drawingIndex.value = null;
revealedCards.value = []; revealedCards.value = [];
isFlipped.value = []; isFlipped.value = [];
isAutoDrawing.value = false;
autoDrawProgress.value = 0;
} else { } else {
isSpread.value = false; isSpread.value = false;
isDrawing.value = false; isDrawing.value = false;
drawingIndex.value = null; drawingIndex.value = null;
revealedCards.value = []; revealedCards.value = [];
isFlipped.value = []; isFlipped.value = [];
isAutoDrawing.value = false;
autoDrawProgress.value = 0;
}
});
// Auto-start drawing if conditions are met
onMounted(() => {
if (shouldAutoDraw.value && props.drawnCards && props.drawnCards.length > 0) {
// Small delay to allow initial render
setTimeout(() => {
startAutoDraw();
}, 800);
} }
}); });
@ -145,6 +209,15 @@ const setDrawnCards = (cardData: Card[]) => {
drawingIndex.value = null; drawingIndex.value = null;
revealedCards.value = []; revealedCards.value = [];
isFlipped.value = new Array(cardData.length).fill(false); isFlipped.value = new Array(cardData.length).fill(false);
isAutoDrawing.value = false;
autoDrawProgress.value = 0;
// Auto-start if conditions are met
if (shouldAutoDraw.value) {
setTimeout(() => {
startAutoDraw();
}, 800);
}
} }
}; };
@ -155,15 +228,36 @@ defineExpose({ setDrawnCards });
<CardShuffleTemplate> <CardShuffleTemplate>
<template #card-shuffle-slot> <template #card-shuffle-slot>
<div class="card-container"> <div class="card-container">
<!-- Auto-draw progress indicator -->
<div v-if="shouldAutoDraw && isAutoDrawing" class="auto-draw-progress">
<div class="progress-bar">
<div class="progress-fill" :style="{ width: `${autoDrawProgress}%` }"></div>
</div>
<div class="progress-text">Distribution automatique en cours... {{ Math.round(autoDrawProgress) }}%</div>
</div>
<!-- Always show the card stack --> <!-- Always show the card stack -->
<transition class="card-stack-fade"> <transition class="card-stack-fade">
<div <div
class="card-stack relative mt-6 mb-8 flex h-[520px] w-[320px] items-center justify-center rounded-2xl border border-[var(--c-purple)]/30 bg-gradient-to-b from-[var(--card)] to-[var(--card)]/90 p-4 shadow-xl ring-1 ring-[var(--c-purple)]/40" class="card-stack relative mt-6 mb-8 flex h-[520px] w-[320px] items-center justify-center rounded-2xl border border-[var(--c-purple)]/30 bg-gradient-to-b from-[var(--card)] to-[var(--card)]/90 p-4 shadow-xl ring-1 ring-[var(--c-purple)]/40"
:class="{ spread: isSpread, drawing: isDrawing, restacking: isRestacking, finished: isFinished }" :class="{
spread: isSpread,
drawing: isDrawing,
restacking: isRestacking,
finished: isFinished,
'auto-drawing': isAutoDrawing,
}"
@click="handleDeckClick" @click="handleDeckClick"
> >
<div class="pointer-events-none absolute -top-16 -left-16 h-40 w-40 rounded-full bg-[var(--c-purple)]/20 blur-3xl"></div> <div class="pointer-events-none absolute -top-16 -left-16 h-40 w-40 rounded-full bg-[var(--c-purple)]/20 blur-3xl"></div>
<div class="pointer-events-none absolute -right-16 -bottom-16 h-40 w-40 rounded-full bg-[var(--c-gold)]/15 blur-3xl"></div> <div class="pointer-events-none absolute -right-16 -bottom-16 h-40 w-40 rounded-full bg-[var(--c-gold)]/15 blur-3xl"></div>
<!-- Auto-draw prompt -->
<div v-if="shouldAutoDraw && !isAutoDrawing && revealedCards.length === 0" class="auto-draw-prompt">
<div class="prompt-icon"></div>
<div class="prompt-text">Cliquez pour une distribution automatique</div>
</div>
<div v-for="i in remainingDeckSize" :key="i" class="card group/card" :style="getCardStyle(i)" @click="onCardClick(i, $event)"> <div v-for="i in remainingDeckSize" :key="i" class="card group/card" :style="getCardStyle(i)" @click="onCardClick(i, $event)">
<div class="card-back"> <div class="card-back">
<div class="card-inner-content"> <div class="card-inner-content">
@ -173,11 +267,12 @@ defineExpose({ setDrawnCards });
</div> </div>
</div> </div>
</transition> </transition>
<!-- Show result only after clicking and when drawnCards is available --> <!-- Show result only after clicking and when drawnCards is available -->
<transition class="card-result-slide"> <transition class="card-result-slide">
<div v-if="revealedCards.length > 0" class="cards-result-container"> <div v-if="revealedCards.length > 0" class="cards-result-container" :class="{ 'many-cards': shouldAutoDraw }">
<div v-for="(card, index) in revealedCards" :key="index" class="card-result-wrapper"> <div v-for="(card, index) in revealedCards" :key="index" class="card-result-wrapper" :class="`card-${index}`">
<div class="result-card" :class="{ flipped: isFlipped[index] }" @click="flipCard(index)"> <div class="result-card" :class="{ flipped: isFlipped[index], 'auto-flipped': isAutoDrawing }" @click="flipCard(index)">
<div class="card-face card-unknown-front"> <div class="card-face card-unknown-front">
<div class="card-inner-content"> <div class="card-inner-content">
<img src="cards/1.png" alt="Card Back" class="card-back-image" /> <img src="cards/1.png" alt="Card Back" class="card-back-image" />
@ -190,6 +285,7 @@ defineExpose({ setDrawnCards });
</div> </div>
</div> </div>
</transition> </transition>
<button <button
@click="goToSelection" @click="goToSelection"
class="group mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full border border-[var(--c-purple)]/40 bg-gradient-to-r from-[var(--c-purple)]/20 to-[var(--c-purple)]/30 px-8 text-base font-bold tracking-wide text-[var(--c-white)] transition-all duration-300 hover:from-[var(--c-purple)]/40 hover:to-[var(--c-purple)]/50 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-60" class="group mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full border border-[var(--c-purple)]/40 bg-gradient-to-r from-[var(--c-purple)]/20 to-[var(--c-purple)]/30 px-8 text-base font-bold tracking-wide text-[var(--c-white)] transition-all duration-300 hover:from-[var(--c-purple)]/40 hover:to-[var(--c-purple)]/50 hover:shadow-lg disabled:cursor-not-allowed disabled:opacity-60"
@ -221,6 +317,64 @@ defineExpose({ setDrawnCards });
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
min-height: 100vh;
padding: 2rem 1rem;
}
/* Auto-draw progress indicator */
.auto-draw-progress {
width: 100%;
max-width: 500px;
margin-bottom: 2rem;
text-align: center;
}
.progress-bar {
width: 100%;
height: 8px;
background: rgba(128, 90, 213, 0.2);
border-radius: 4px;
overflow: hidden;
margin-bottom: 0.5rem;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--c-purple), var(--c-gold));
border-radius: 4px;
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.9rem;
color: var(--c-white);
opacity: 0.8;
}
/* Auto-draw prompt */
.auto-draw-prompt {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
z-index: 5;
background: rgba(0, 0, 0, 0.7);
padding: 1rem 1.5rem;
border-radius: 12px;
backdrop-filter: blur(10px);
border: 1px solid rgba(240, 186, 45, 0.3);
}
.prompt-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
.prompt-text {
color: var(--c-white);
font-size: 0.9rem;
font-weight: 500;
} }
.card { .card {
@ -251,26 +405,42 @@ defineExpose({ setDrawnCards });
} }
/* Hover sur la pile */ /* Hover sur la pile */
.card-stack:not(.spread):hover { .card-stack:not(.spread):not(.auto-drawing):hover {
transform: translateY(-10px) rotateX(2deg) rotateY(-2deg); transform: translateY(-10px) rotateX(2deg) rotateY(-2deg);
} }
.card-stack:not(.spread):hover .card:nth-child(1) { .card-stack:not(.spread):not(.auto-drawing):hover .card:nth-child(1) {
transform: rotateY(-5deg) rotateX(5deg) translateZ(30px) translateX(-20px); transform: rotateY(-5deg) rotateX(5deg) translateZ(30px) translateX(-20px);
} }
.card-stack:not(.spread):hover .card:nth-child(2) { .card-stack:not(.spread):not(.auto-drawing):hover .card:nth-child(2) {
transform: rotateY(0deg) rotateX(2deg) translateZ(20px); transform: rotateY(0deg) rotateX(2deg) translateZ(20px);
} }
.card-stack:not(.spread):hover .card:nth-child(3) { .card-stack:not(.spread):not(.auto-drawing):hover .card:nth-child(3) {
transform: rotateY(5deg) rotateX(5deg) translateZ(10px) translateX(20px); transform: rotateY(5deg) rotateX(5deg) translateZ(10px) translateX(20px);
} }
/* Glow doré subtil au hover */ /* Glow doré subtil au hover */
.card-stack:hover .card-back { .card-stack:not(.auto-drawing):hover .card-back {
box-shadow: 0 0 20px rgba(215, 186, 141, 0.6); box-shadow: 0 0 20px rgba(215, 186, 141, 0.6);
transition: box-shadow 0.6s ease-in-out; transition: box-shadow 0.6s ease-in-out;
} }
/* Auto-drawing state */
.card-stack.auto-drawing {
cursor: default;
animation: gentle-pulse 2s infinite;
}
@keyframes gentle-pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.02);
}
}
/* Animation click */ /* Animation click */
@keyframes card-click-tilt { @keyframes card-click-tilt {
0% { 0% {
@ -374,10 +544,15 @@ defineExpose({ setDrawnCards });
} }
.card-result-wrapper { .card-result-wrapper {
width: 250px; width: clamp(140px, 12vw, 160px); /* target ~67 per row on wide screens */
height: 400px; aspect-ratio: 5 / 8; /* maintain 250x400 proportion */
perspective: 1000px; perspective: 1000px;
border-radius: 18px; border-radius: 18px;
transition: transform 0.5s ease;
}
.card-result-wrapper:hover {
transform: translateY(-10px);
} }
.card-back-design-wrapper { .card-back-design-wrapper {
@ -429,7 +604,6 @@ defineExpose({ setDrawnCards });
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform: rotateY(180deg); transform: rotateY(180deg);
position: relative; position: relative;
border-radius: 18px; border-radius: 18px;
@ -460,10 +634,24 @@ defineExpose({ setDrawnCards });
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 20px; gap: 15px; /* tighter gaps to fit 67 per row */
margin-top: 2rem; margin-top: 2rem;
position: relative; position: relative;
z-index: 10; z-index: 10;
max-width: 100%;
padding: 0 1rem;
}
/* Enhanced layout for many cards */
.cards-result-container.many-cards {
gap: 15px;
justify-content: center; /* keep centered alignment even for many cards */
}
.cards-result-container.many-cards .card-result-wrapper {
width: clamp(140px, 12vw, 160px);
aspect-ratio: 5 / 8; /* keeps card proportions */
margin-bottom: 10px;
} }
.card-result-container { .card-result-container {
@ -483,6 +671,10 @@ defineExpose({ setDrawnCards });
transform: rotateY(180deg); transform: rotateY(180deg);
} }
.result-card.auto-flipped {
transition: transform 0.8s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.card-front, .card-front,
.card-back-info { .card-back-info {
position: absolute; position: absolute;
@ -613,4 +805,143 @@ defineExpose({ setDrawnCards });
opacity: 0; opacity: 0;
transform: translateY(50px); transform: translateY(50px);
} }
/* Responsive design */
@media (max-width: 1200px) {
.cards-result-container .card-result-wrapper {
width: clamp(140px, 12vw, 160px);
/* height handled by aspect-ratio */
}
}
@media (max-width: 992px) {
.cards-result-container .card-result-wrapper {
width: clamp(140px, 20vw, 160px);
/* height handled by aspect-ratio */
}
.card {
width: 220px;
height: 352px;
}
.card-stack {
width: 280px;
height: 460px;
}
}
@media (max-width: 768px) {
.cards-result-container {
gap: 15px;
}
.cards-result-container .card-result-wrapper {
width: clamp(140px, 26vw, 160px);
/* height handled by aspect-ratio */
}
.card {
width: 200px;
height: 320px;
}
.card-stack {
width: 260px;
height: 420px;
}
.card-description-overlay h3 {
font-size: 1.4rem;
}
}
@media (max-width: 576px) {
.cards-result-container {
gap: 10px;
}
.cards-result-container .card-result-wrapper {
width: clamp(140px, 36vw, 160px);
/* height handled by aspect-ratio */
}
.card {
width: 180px;
height: 288px;
}
.card-stack {
width: 240px;
height: 380px;
}
.card-description-overlay h3 {
font-size: 1.2rem;
padding: 8px;
}
.auto-draw-prompt {
padding: 0.8rem 1rem;
}
.prompt-icon {
font-size: 1.5rem;
}
.prompt-text {
font-size: 0.8rem;
}
}
/* Animation for auto-drawing cards */
@keyframes card-appear {
0% {
opacity: 0;
transform: scale(0.8) translateY(20px);
}
100% {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.cards-result-container .card-result-wrapper {
animation: card-appear 0.5s ease forwards;
}
/* Stagger animation for multiple cards */
.cards-result-container .card-result-wrapper:nth-child(1) {
animation-delay: 0.1s;
}
.cards-result-container .card-result-wrapper:nth-child(2) {
animation-delay: 0.2s;
}
.cards-result-container .card-result-wrapper:nth-child(3) {
animation-delay: 0.3s;
}
.cards-result-container .card-result-wrapper:nth-child(4) {
animation-delay: 0.4s;
}
.cards-result-container .card-result-wrapper:nth-child(5) {
animation-delay: 0.5s;
}
.cards-result-container .card-result-wrapper:nth-child(6) {
animation-delay: 0.6s;
}
.cards-result-container .card-result-wrapper:nth-child(7) {
animation-delay: 0.7s;
}
.cards-result-container .card-result-wrapper:nth-child(8) {
animation-delay: 0.8s;
}
.cards-result-container .card-result-wrapper:nth-child(9) {
animation-delay: 0.9s;
}
.cards-result-container .card-result-wrapper:nth-child(10) {
animation-delay: 1s;
}
.cards-result-container .card-result-wrapper:nth-child(n + 11) {
animation-delay: 1.1s;
}
</style> </style>

View File

@ -1,17 +1,18 @@
<template> <template>
<main class="flex flex-1 flex-col items-center justify-center px-4 pt-12 pb-24 sm:px-6 lg:px-8"> <main class="relative flex min-h-screen w-full flex-col px-4 pt-12 pb-24 sm:px-6 lg:px-8">
<div class="relative z-10 flex w-full max-w-4xl flex-col items-center justify-center text-center"> <!-- Header / intro kept centered and constrained -->
<div class="relative z-10 mx-auto max-w-4xl text-center">
<h1 class="text-5xl font-bold text-[var(--midnight-blue)] md:text-6xl"> <h1 class="text-5xl font-bold text-[var(--midnight-blue)] md:text-6xl">
L'Oracle de votre<span class="citadel-script ml-4 text-6xl text-[var(--spiritual-earth)] md:text-7xl">Destinée</span> L'Oracle de votre<span class="citadel-script ml-4 text-6xl text-[var(--spiritual-earth)] md:text-7xl">Destinée</span>
</h1> </h1>
<p class="mt-4 max-w-2xl text-lg text-[var(--midnight-blue)]/80"> <p class="mt-4 mx-auto max-w-2xl text-lg text-[var(--midnight-blue)]/80">
Puisez dans la sagesse ancestrale pour éclairer votre chemin. Tirez une carte et recevez le message qui vous est destiné aujourd'hui. Puisez dans la sagesse ancestrale pour éclairer votre chemin. Tirez une carte et recevez le message qui vous est destiné aujourd'hui.
</p> </p>
</div>
<!-- Card shuffle slot --> <!-- Card shuffle slot now full width -->
<div class="relative z-10 mt-8 w-full">
<slot name="card-shuffle-slot" /> <slot name="card-shuffle-slot" />
<!-- Button tirage -->
</div> </div>
</main> </main>
</template> </template>

View File

@ -0,0 +1,81 @@
<template>
<article
class="group relative flex h-full flex-col overflow-hidden rounded-2xl border border-[var(--color-border)]/40 bg-[var(--card)]/95 shadow-sm transition-transform duration-300 hover:-translate-y-0.5 hover:shadow-xl"
>
<!-- Header / image -->
<div class="flex items-start gap-4 p-4 sm:p-5">
<div class="relative shrink-0">
<div
class="h-40 w-28 rounded-xl bg-cover bg-center bg-no-repeat shadow-md ring-1 ring-[var(--color-border)]/50"
:style="{
backgroundImage: `url(${resolvedImageUrl})`,
transform: orientation === 'reversed' ? 'rotate(180deg)' : 'none'
}"
></div>
<div
class="absolute -bottom-2 -left-2 flex h-8 w-8 items-center justify-center rounded-full border-2 border-[var(--color-border)]/60 bg-[var(--secondary)] text-xs font-black text-[var(--foreground)] shadow"
:title="`Carte #${cardNumber}`"
>
{{ cardNumber }}
</div>
<div
v-if="orientation"
class="absolute top-2 right-2 rounded-full px-2 py-0.5 text-[10px] font-bold tracking-wide text-white shadow"
:class="orientation === 'reversed' ? 'bg-red-600/90' : 'bg-[var(--primary)]'"
>
{{ orientation === 'reversed' ? 'Inversée' : 'Droite' }}
</div>
</div>
<div class="min-w-0 flex-1">
<h3 class="font-title text-lg font-black leading-snug text-[var(--foreground)]">
{{ name }}
</h3>
<p class="mt-1 text-xs uppercase tracking-wider text-[var(--muted-foreground)]">
Carte #{{ cardNumber }}
</p>
</div>
</div>
<!-- Description -->
<div class="relative px-4 pb-4 sm:px-5 sm:pb-5">
<div :class="[ collapsed ? 'max-h-40 overflow-hidden' : 'max-h-none', 'text-sm leading-relaxed text-[var(--foreground)]/85']">
<div v-html="description"></div>
</div>
<!-- Fade overlay when collapsed -->
<div
v-if="collapsed"
class="pointer-events-none absolute inset-x-0 bottom-12 h-16 bg-gradient-to-t from-[var(--card)] to-transparent"
></div>
<div class="mt-3 flex justify-end">
<button
type="button"
@click="collapsed = !collapsed"
class="rounded-full border border-[var(--color-border)]/60 bg-[var(--secondary)]/60 px-3 py-1 text-xs font-semibold text-[var(--foreground)] transition-colors hover:bg-[var(--secondary)]"
>
{{ collapsed ? 'Lire plus' : 'Réduire' }}
</button>
</div>
</div>
</article>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
interface Props {
cardNumber: number
name: string
imageUrl?: string
orientation?: string
description: string
}
const props = defineProps<Props>()
const resolvedImageUrl = computed(() => props.imageUrl || `/cards/${props.cardNumber + 1}.png`)
const collapsed = ref(true)
</script>

View File

@ -0,0 +1,81 @@
<script setup lang="ts">
import FullCardItem from '@/components/ui/card/FullCardItem.vue'
import LandingLayout from '@/layouts/app/LandingLayout.vue'
import axios from 'axios'
import { computed, onMounted, ref } from 'vue'
const cards = ref<Array<any>>([])
const error = ref<string | null>(null)
const loading = ref(true)
const params = new URLSearchParams(window.location.search)
const clientSessionId = params.get('client_session_id')
const hasCards = computed(() => cards.value.length > 0)
onMounted(async () => {
try {
if (!clientSessionId) {
error.value = 'No session ID provided. Please return to the payment page.'
return
}
const response = await axios.get(`/api/get-cards?client_session_id=${clientSessionId}`)
if (response.data.success) {
cards.value = response.data.cards
} else {
error.value = response.data.message || 'An error occurred while validating payment.'
}
} catch (err: any) {
console.error('Card fetch error:', err)
error.value = err.response?.data?.message || 'Failed to get cards from the server. Please contact support.'
} finally {
loading.value = false
}
})
</script>
<template>
<LandingLayout>
<main class="min-h-screen bg-gradient-to-br from-[var(--background)] to-[var(--background)]/95 py-8">
<div class="container mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
<!-- Title / meta -->
<div class="mb-10 text-center md:mb-14">
<h1 class="font-title bg-gradient-to-r from-[var(--c-purple)] to-[var(--c-purple)]/70 bg-clip-text text-3xl font-black text-transparent sm:text-4xl md:text-5xl">
Lecture complète
</h1>
<p class="mt-2 text-[var(--muted-foreground)]">
{{ hasCards ? `${cards.length} cartes révélées` : '' }}
</p>
</div>
<!-- Loading -->
<div v-if="loading" class="flex items-center justify-center py-24">
<div class="h-12 w-12 animate-spin rounded-full border-2 border-[var(--c-purple)]/30 border-t-[var(--c-purple)]"></div>
</div>
<!-- Error -->
<div v-else-if="error" class="mx-auto max-w-2xl rounded-2xl border border-red-500/30 bg-red-900/20 p-6 text-center">
<p class="font-semibold text-red-200">{{ error }}</p>
</div>
<!-- Cards Grid -->
<div v-else-if="hasCards" class="grid justify-items-center gap-5 grid-cols-[repeat(auto-fit,minmax(160px,1fr))] sm:gap-6">
<FullCardItem
v-for="(card, index) in cards"
:key="card.id || index"
:card-number="card.id"
:name="card.name"
:image-url="card.image_url"
:orientation="card.orientation"
:description="card.description"
/>
</div>
<!-- Empty -->
<div v-else class="mx-auto max-w-2xl rounded-2xl border border-[var(--color-border)]/40 bg-[var(--card)]/80 p-10 text-center">
<p class="text-[var(--muted-foreground)]">Aucune carte trouvée</p>
</div>
</div>
</main>
</LandingLayout>
</template>

View File

@ -254,7 +254,7 @@ const clearHover = () => {
</svg> </svg>
</div> </div>
</div> </div>
<h3 class="text-xl font-bold text-white md:text-2xl">Quadrige Doré</h3> <h3 class="text-xl font-bold text-white md:text-2xl">Tirage complet</h3>
<p class="mt-2 text-4xl font-bold text-[var(--c-gold)] md:text-5xl">99</p> <p class="mt-2 text-4xl font-bold text-[var(--c-gold)] md:text-5xl">99</p>
<p class="mt-4 text-sm text-gray-300 md:text-base"> <p class="mt-4 text-sm text-gray-300 md:text-base">
Une lecture complète sur tous les aspects de votre vie : amour, carrière, spiritualité et abondance. Une lecture complète sur tous les aspects de votre vie : amour, carrière, spiritualité et abondance.
@ -262,7 +262,7 @@ const clearHover = () => {
</div> </div>
<button <button
class="group relative mt-2 flex h-12 w-full items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-gray-800 to-gray-900 px-6 font-bold tracking-wide text-white transition-all duration-300 hover:from-[var(--c-purple)] hover:to-[var(--c-purple)]/80 hover:text-white" class="group relative mt-2 flex h-12 w-full items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-gray-800 to-gray-900 px-6 font-bold tracking-wide text-white transition-all duration-300 hover:from-[var(--c-purple)] hover:to-[var(--c-purple)]/80 hover:text-white"
@click="handleSelection(4)" @click="handleSelection(21)"
> >
<span class="relative z-10">Explorer</span> <span class="relative z-10">Explorer</span>
<div <div