This commit is contained in:
kevin 2025-12-18 15:24:46 +03:00
parent d0bbcfca7c
commit 5bfdc64d13
13 changed files with 406 additions and 147 deletions

View File

@ -112,27 +112,75 @@ class StripeController extends Controller
} }
} }
public function validatePayment(Request $request) public function validatePayment(Request $request)
{ {
$clientSessionId = $request->query('client_session_id'); $clientSessionId = $request->query('client_session_id');
$payment = Payment::where('client_session_id', $clientSessionId) if (!$clientSessionId) {
->where('status', 'succeeded') return response()->json(['error' => 'Client session ID is required'], 400);
->first(); }
if ($payment) { $payment = Payment::where('client_session_id', $clientSessionId)->first();
// Si la vérification réussit, retournez le nombre de tirages.
if (!$payment) {
return response()->json([
'success' => false,
'message' => 'Payment not found.',
], 404);
}
// If payment is already succeeded in our database
if ($payment->status === 'succeeded') {
return response()->json([ return response()->json([
'success' => true, 'success' => true,
'drawCount' => $payment->draw_count, 'drawCount' => $payment->draw_count,
'cached' => true,
]); ]);
} }
// Si la vérification échoue, retournez une erreur. // If payment is pending, check with Stripe directly to handle race condition
if ($payment->status === 'pending') {
try {
Stripe::setApiKey(env('STRIPE_SECRET_KEY'));
$session = Session::retrieve($clientSessionId);
// Check if payment is completed on Stripe side
if ($session->payment_status === 'paid' && $session->status === 'complete') {
// Update our payment record and mark as succeeded
$payment->update(['status' => 'succeeded']);
return response()->json([
'success' => true,
'drawCount' => $payment->draw_count,
'updated' => true,
]);
}
// Payment not completed yet, return pending status
return response()->json([
'success' => false,
'message' => 'Payment is still being processed.',
'status' => 'pending',
], 202);
} catch (\Exception $e) {
\Log::error('Stripe validation failed: ' . $e->getMessage(), [
'client_session_id' => $clientSessionId,
'payment_id' => $payment->id
]);
return response()->json([
'success' => false,
'message' => 'Payment validation error.',
], 500);
}
}
// Payment failed or has other status
return response()->json([ return response()->json([
'success' => false, 'success' => false,
'message' => 'Paiement non validé.', 'message' => 'Payment failed or cancelled.',
], 404); 'status' => $payment->status,
], 402);
} }
public function getCards(Request $request) public function getCards(Request $request)

View File

@ -73,7 +73,9 @@ class CardRepository implements CardRepositoryInterface
'name' => $card->name, 'name' => $card->name,
'image_url' => $card->image_url, 'image_url' => $card->image_url,
'orientation' => $isReversed ? 'reversed' : 'upright', 'orientation' => $isReversed ? 'reversed' : 'upright',
'description' => $isReversed ? $card->description_reversed : $card->description_upright, 'description' => $card->description,
'description_reversed' => $card->description_reversed,
'description_upright' => $card->description_upright,
'symbolism' => $card->symbolism, 'symbolism' => $card->symbolism,
'created_at' => now(), 'created_at' => now(),
]; ];

Binary file not shown.

View File

@ -91,10 +91,13 @@
</ul> </ul>
<button <button
class="relative z-10 mt-4 flex h-12 w-full items-center justify-center overflow-hidden rounded-full border border-[var(--c-purple)]/40 bg-[#2d1b69] px-8 font-bold tracking-wide text-[var(--c-white)] transition-all duration-300 group-hover:bg-[#3d2485] group-hover:shadow-lg" class="relative z-10 mt-4 flex h-12 w-full items-center justify-center overflow-hidden rounded-full border border-[var(--c-purple)]/40 px-8 font-bold tracking-wide text-[var(--c-white)] transition-all duration-300 group-hover:shadow-lg"
@click="goToTirage" :class="hasUsedFreeDraw ? 'bg-[var(--c-gold)]/80 group-hover:bg-[var(--c-gold)]' : 'bg-[#2d1b69] group-hover:bg-[#3d2485]'"
@click="handleFreeClick"
> >
<span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">BEGIN</span> <span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
{{ hasUsedFreeDraw ? 'VIEW MY READING' : 'BEGIN' }}
</span>
<svg <svg
class="relative z-10 ml-2 h-5 w-5 -translate-x-2 transform opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100" class="relative z-10 ml-2 h-5 w-5 -translate-x-2 transform opacity-0 transition-all duration-300 group-hover:translate-x-0 group-hover:opacity-100"
fill="none" fill="none"
@ -280,7 +283,11 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useTarotStore } from '@/stores/tarot';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import { computed, onBeforeMount } from 'vue';
const tarotStore = useTarotStore();
// Define features for each tier // Define features for each tier
const freeFeatures = ['Singlecard Reading', 'General Interpretation', 'Quick Tips']; const freeFeatures = ['Singlecard Reading', 'General Interpretation', 'Quick Tips'];
@ -289,10 +296,29 @@ const featuredFeatures = ['sixcard Reading', 'Personalized Analysis', 'Tailor
const premiumFeatures = ['Eigtheencard reading', 'Indepth exploration', 'Complete strategy']; const premiumFeatures = ['Eigtheencard reading', 'Indepth exploration', 'Complete strategy'];
// Check if user has already used their free draw (either card is saved, or draws remaining is 0)
const hasUsedFreeDraw = computed(() => tarotStore.freeDrawCard !== null || tarotStore.freeDrawsRemaining === 0);
// Navigate to tirage page // Navigate to tirage page
const goToTirage = () => { const goToTirage = () => {
router.visit('/tirage'); router.visit('/tirage');
}; };
// Navigate to free card result (to view previous draw)
const goToFreeResult = () => {
router.visit('/resultat-gratuit');
};
// Handle free tier button click
const handleFreeClick = () => {
if (hasUsedFreeDraw.value) {
goToFreeResult();
} else {
goToTirage();
}
};
onBeforeMount(() => console.log(tarotStore.freeDrawCard));
</script> </script>
<style scoped> <style scoped>

View File

@ -1,12 +1,12 @@
<template> <template>
<div <div
class="flex flex-col items-center gap-6 md:gap-8 rounded-2xl bg-gradient-to-br from-purple-900/40 to-purple-800/30 border border-purple-700/30 p-6 md:p-8 shadow-2xl transition-all duration-500 hover:shadow-3xl md:flex-row backdrop-blur-sm hover:border-purple-600/50 group" class="flex flex-col items-center gap-6 md:gap-8 rounded-2xl bg-gradient-to-br from-purple-900/40 to-purple-800/30 border border-purple-700/30 p-6 md:p-8 shadow-2xl transition-all duration-500 hover:shadow-3xl md:flex-row backdrop-blur-sm hover:border-purple-600/50 group card-container"
> >
<!-- Card Image Container --> <!-- Card Image Container -->
<div class="relative"> <div class="relative">
<!-- Card Image with Glow Effect --> <!-- Card Image with Glow Effect -->
<div <div
class="h-56 w-36 sm:h-64 sm:w-40 md:h-72 md:w-48 flex-shrink-0 rounded-xl bg-cover bg-center bg-no-repeat transition-all duration-500 group-hover:scale-105 relative overflow-hidden" class="h-56 w-36 sm:h-64 sm:w-40 md:h-72 md:w-48 flex-shrink-0 rounded-xl bg-cover bg-center bg-no-repeat transition-all duration-500 group-hover:scale-105 relative overflow-hidden card-image"
:style="{ :style="{
'background-image': `url(${imageUrl || '/cards/' + (cardNumber + 1) + '.png'})`, 'background-image': `url(${imageUrl || '/cards/' + (cardNumber + 1) + '.png'})`,
transform: orientation === 'reversed' ? 'rotate(180deg)' : 'none' transform: orientation === 'reversed' ? 'rotate(180deg)' : 'none'
@ -19,9 +19,7 @@
<div <div
class="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500" class="absolute inset-0 rounded-xl opacity-0 group-hover:opacity-100 transition-opacity duration-500"
:style="{ :style="{
'box-shadow': orientation === 'reversed' 'box-shadow': '0 0 30px 10px rgba(168, 85, 247, 0.4)'
? '0 0 30px 10px rgba(239, 68, 68, 0.4)'
: '0 0 30px 10px rgba(168, 85, 247, 0.4)'
}" }"
></div> ></div>
</div> </div>
@ -29,40 +27,99 @@
<!-- Orientation Badge --> <!-- Orientation Badge -->
<div <div
v-if="orientation === 'reversed'" v-if="orientation === 'reversed'"
class="absolute top-3 right-3 bg-gradient-to-r from-red-600 to-red-700 text-red-100 text-xs font-semibold px-3 py-1.5 rounded-full border border-red-500/30 shadow-lg" class="absolute top-3 right-3 bg-gradient-to-r from-purple-600 to-purple-700 text-purple-100 text-xs font-semibold px-3 py-1.5 rounded-full border border-purple-500/30 shadow-lg"
> >
Inversée Reversed
</div> </div>
<div <div
v-else v-else
class="absolute top-3 right-3 bg-gradient-to-r from-purple-600 to-purple-700 text-purple-100 text-xs font-semibold px-3 py-1.5 rounded-full border border-purple-500/30 shadow-lg" class="absolute top-3 right-3 bg-gradient-to-r from-purple-600 to-purple-700 text-purple-100 text-xs font-semibold px-3 py-1.5 rounded-full border border-purple-500/30 shadow-lg"
> >
Droite Upright
</div> </div>
<!-- Card Number Badge --> <!-- Card Number Badge -->
<div class="absolute -bottom-2 -left-2 bg-gradient-to-r from-purple-700 to-indigo-700 text-purple-100 text-sm font-bold w-10 h-10 rounded-full flex items-center justify-center border-2 border-purple-900/50 shadow-lg"> <div
class="absolute -bottom-2 -left-2 text-purple-100 text-sm font-bold w-10 h-10 rounded-full flex items-center justify-center border-2 border-purple-900/50 shadow-lg card-number bg-gradient-to-r from-purple-700 to-indigo-700"
>
{{ cardNumber }} {{ cardNumber }}
</div> </div>
</div> </div>
<!-- Card Content --> <!-- Card Content -->
<div class="flex flex-col gap-4 text-center md:text-left flex-1"> <div class="flex flex-col gap-6 text-center md:text-left flex-1 w-full">
<!-- Card Number --> <!-- Card Header -->
<p class="text-purple-300/70 text-xs sm:text-sm tracking-widest uppercase font-semibold"> <div class="space-y-2">
Carte #{{ cardNumber }} <p class="text-purple-300/70 text-xs sm:text-sm tracking-widest uppercase font-semibold">
</p> Card #{{ cardNumber }}
</p>
<!-- Card Name --> <h3
<h3 class="text-2xl sm:text-3xl font-black bg-gradient-to-r from-purple-200 to-purple-100 bg-clip-text text-transparent leading-tight"> class="text-2xl sm:text-3xl font-black bg-clip-text text-transparent leading-tight bg-gradient-to-r from-purple-200 to-purple-100"
{{ name }} >
</h3> {{ name }}
</h3>
</div>
<!-- Description --> <!-- Description Content -->
<div class="text-purple-300/90 text-base sm:text-lg leading-relaxed font-medium" v-html="description"></div> <div class="space-y-4">
<!-- Description Header -->
<div class="flex items-center gap-2">
<div class="text-lg font-bold flex items-center gap-2 text-purple-200">
<span class="text-purple-400">
{{ orientation === 'reversed' ? '🔄' : '📖' }}
</span>
{{ orientation === 'reversed' ? 'Reversed Meaning' : 'Upright Meaning' }}
</div>
<span class="text-xs px-2 py-1 rounded-full bg-purple-800/30 text-purple-300">
{{ orientation === 'reversed' ? 'Challenge' : 'Positive' }}
</span>
</div>
<!-- Description Content -->
<div class="min-h-[200px] max-h-[300px] overflow-y-auto pr-2 custom-scrollbar">
<div class="space-y-4 animate-fadeIn">
<div
class="text-purple-300/90 leading-relaxed font-medium pl-4 border-l-2 border-purple-600/50 p-3 rounded-r-lg bg-gradient-to-r from-purple-900/10 to-transparent"
v-html="orientation === 'reversed' ? description_reversed : description_upright"
></div>
<!-- Key Points -->
<div class="space-y-2">
<h4 class="font-semibold text-sm opacity-80 text-purple-300">
Key Points:
</h4>
<ul class="space-y-1 text-sm pl-4 text-purple-300/80">
<li v-if="orientation === 'reversed'">
Represents challenges or blockages
</li>
<li v-if="orientation === 'reversed'">
Indicates reversed or delayed energy
</li>
<li v-if="orientation !== 'reversed'">
Represents the card's natural energy
</li>
<li v-if="orientation !== 'reversed'">
Indicates direct manifestation
</li>
<li>
Traditional tarot interpretation
</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Decorative Divider --> <!-- Decorative Divider -->
<div class="w-16 h-0.5 bg-gradient-to-r from-purple-600 to-transparent rounded-full mt-2 mx-auto md:mx-0"></div> <div class="w-16 h-0.5 rounded-full mt-2 bg-gradient-to-r from-purple-600 to-transparent"></div>
<!-- Footer Note -->
<p class="text-xs opacity-70 italic text-purple-300/70">
{{ orientation === 'reversed'
? 'This reversed card suggests special attention is needed'
: 'This upright card indicates smooth, manifested energy' }}
</p>
</div> </div>
<!-- Hover Effect Background --> <!-- Hover Effect Background -->
@ -76,14 +133,17 @@ interface Props {
name: string; name: string;
imageUrl: string; imageUrl: string;
orientation?: string; orientation?: string;
description: string; description_upright: string;
description_reversed: string;
} }
defineProps<Props>(); const props = withDefaults(defineProps<Props>(), {
orientation: 'upright'
});
</script> </script>
<style scoped> <style scoped>
div { .card-container {
animation: cardAppear 0.6s ease-out; animation: cardAppear 0.6s ease-out;
} }
@ -98,36 +158,74 @@ div {
} }
} }
/* Custom shadow for hover effect */ @keyframes fadeIn {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fadeIn {
animation: fadeIn 0.3s ease-out;
}
.shadow-3xl { .shadow-3xl {
box-shadow: box-shadow:
0 25px 50px -12px rgba(0, 0, 0, 0.5), 0 25px 50px -12px rgba(0, 0, 0, 0.5),
0 0 30px rgba(168, 85, 247, 0.3); 0 0 30px rgba(168, 85, 247, 0.3);
} }
/* Smooth transitions for all interactive elements */ .custom-scrollbar {
* { scrollbar-width: thin;
}
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: rgba(168, 85, 247, 0.1);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: rgba(168, 85, 247, 0.4);
border-radius: 3px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: rgba(168, 85, 247, 0.6);
}
/* Smooth transitions */
.card-container * {
transition-property: all; transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 300ms; transition-duration: 300ms;
} }
/* Custom scrollbar for description */ /* Card hover effects */
.text-base::-webkit-scrollbar { .card-image:hover {
width: 4px; transform: scale(1.05);
}
:deep(strong),
:deep(b) {
font-weight: 700 !important; /* Forces bold weight */
color: white; /* Optional: Makes it fully white vs the 80% opacity body text */
} }
.text-base::-webkit-scrollbar-track { /* Optional: If you want to style lists inside description too */
background: rgba(168, 85, 247, 0.1); :deep(ul),
border-radius: 2px; :deep(ol) {
padding-left: 1.5rem;
list-style-type: disc;
} }
.text-base::-webkit-scrollbar-thumb { :deep(li) {
background: rgba(168, 85, 247, 0.4); margin-bottom: 0.5rem;
border-radius: 2px;
}
.text-base::-webkit-scrollbar-thumb:hover {
background: rgba(168, 85, 247, 0.6);
} }
</style> </style>

View File

@ -14,7 +14,7 @@
></div> ></div>
<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" 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}`" :title="`Card #${cardNumber}`"
> >
{{ cardNumber }} {{ cardNumber }}
</div> </div>
@ -23,7 +23,7 @@
class="absolute top-2 right-2 rounded-full px-2 py-0.5 text-[10px] font-bold tracking-wide text-white shadow" 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)]'" :class="orientation === 'reversed' ? 'bg-red-600/90' : 'bg-[var(--primary)]'"
> >
{{ orientation === 'reversed' ? 'Inversée' : 'Droite' }} {{ orientation === 'reversed' ? 'Reversed' : 'Upright' }}
</div> </div>
</div> </div>
@ -32,7 +32,7 @@
{{ name }} {{ name }}
</h3> </h3>
<p class="mt-1 text-xs uppercase tracking-wider text-[var(--muted-foreground)]"> <p class="mt-1 text-xs uppercase tracking-wider text-[var(--muted-foreground)]">
Carte #{{ cardNumber }} Card #{{ cardNumber }}
</p> </p>
</div> </div>
</div> </div>
@ -55,7 +55,7 @@
@click="collapsed = !collapsed" @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)]" 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' }} {{ collapsed ? 'Read more' : 'Show less' }}
</button> </button>
</div> </div>
</div> </div>

View File

@ -14,14 +14,25 @@
</div> </div>
<!-- Error State --> <!-- Error State -->
<div v-else-if="error" class="mb-8 rounded-lg bg-red-50 p-6 text-center"> <div v-else-if="error" class="mb-8 rounded-lg border border-red-500/30 bg-red-900/30 p-6 text-center">
<div class="mb-2 font-medium text-red-700">Erreur</div> <div class="mb-2 font-medium text-red-300">Error</div>
<p class="text-red-600">{{ error }}</p> <p class="text-red-200">{{ error }}</p>
</div> </div>
<!-- Empty State --> <!-- Empty State - No card found, offer to do a new reading -->
<div v-else-if="!hasCard" class="rounded-lg bg-gray-50 p-8 text-center"> <div
<p class="text-gray-600">Aucune carte n'a été trouvée pour votre session.</p> v-else-if="!hasCard"
class="rounded-3xl border border-white/10 bg-gradient-to-br from-[var(--c-purple)]/20 to-[var(--c-gold)]/10 p-8 text-center shadow-2xl backdrop-blur-lg"
>
<div class="mb-4 text-6xl">🔮</div>
<h3 class="mb-4 text-2xl font-bold text-white">No Reading Found</h3>
<p class="mb-6 text-white/70">Your previous reading could not be retrieved. Would you like to do a new free reading?</p>
<button
@click="resetAndNewReading"
class="mystic-btn mystic-btn-primary group relative inline-flex h-12 min-w-[200px] items-center justify-center overflow-hidden rounded-full px-8 text-base font-bold text-white shadow-2xl transition-all duration-300 hover:-translate-y-1"
>
<span class="relative z-10">Start New Reading</span>
</button>
</div> </div>
<!-- Single Card Display --> <!-- Single Card Display -->
@ -36,9 +47,6 @@
<!-- Card Header --> <!-- Card Header -->
<div class="relative z-10 mb-6 text-center"> <div class="relative z-10 mb-6 text-center">
<h2 class="text-3xl font-black text-white md:text-4xl">{{ card.name }}</h2> <h2 class="text-3xl font-black text-white md:text-4xl">{{ card.name }}</h2>
<div v-if="card.orientation" class="mt-2 text-sm text-white/70">
<div v-html="card.orientation === 'reversed' ? 'Inversée' : 'Droite'"></div>
</div>
</div> </div>
<!-- Card Content --> <!-- Card Content -->
@ -51,11 +59,6 @@
:alt="card.name" :alt="card.name"
class="absolute inset-0 h-full w-full rounded-2xl object-cover" class="absolute inset-0 h-full w-full rounded-2xl object-cover"
/> />
<div
class="absolute inset-x-0 bottom-0 rounded-b-2xl bg-gradient-to-t from-black/80 to-transparent p-4 text-center"
>
<p class="text-sm font-bold text-white">{{ card.name }}</p>
</div>
</div> </div>
</div> </div>
@ -65,24 +68,14 @@
<div class="prose prose-invert max-w-none text-white/80" v-html="card.description"></div> <div class="prose prose-invert max-w-none text-white/80" v-html="card.description"></div>
</div> </div>
<div class="mb-8"> <div v-if="card.description_upright" class="mb-8">
<h3 class="mb-3 text-lg font-black text-white"> <h3 class="mb-3 text-lg font-black text-white">Upright Meaning</h3>
{{ card.orientation === 'reversed' ? 'Signification Inversée' : 'Signification Droite' }} <div class="prose prose-invert max-w-none text-white/80" v-html="card.description_upright"></div>
</h3>
<div
class="prose prose-invert max-w-none text-white/80"
v-html="card.orientation === 'reversed' ? card.description_reversed : card.description_upright"
></div>
</div> </div>
<div class="mb-6"> <div v-if="card.description_reversed" class="mb-8">
<h3 class="mb-3 text-lg font-black text-white"> <h3 class="mb-3 text-lg font-black text-white">Reversed Meaning</h3>
{{ card.orientation === 'reversed' ? 'Signification Droite' : 'Signification Inversée' }} <div class="prose prose-invert max-w-none text-white/80" v-html="card.description_reversed"></div>
</h3>
<div
class="prose prose-invert max-w-none text-white/80"
v-html="card.orientation === 'reversed' ? card.description_upright : card.description_reversed"
></div>
</div> </div>
<!-- Symbolism --> <!-- Symbolism -->
@ -194,6 +187,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import LandingLayout from '@/layouts/app/LandingLayout.vue'; import LandingLayout from '@/layouts/app/LandingLayout.vue';
import { useTarotStore } from '@/stores/tarot';
import { router } from '@inertiajs/vue3'; import { router } from '@inertiajs/vue3';
import axios from 'axios'; import axios from 'axios';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
@ -202,6 +196,7 @@ const goToBooking = () => {
router.visit('/rendez-vous'); router.visit('/rendez-vous');
}; };
const tarotStore = useTarotStore();
const card = ref<any>(null); const card = ref<any>(null);
const error = ref<string | null>(null); const error = ref<string | null>(null);
const loading = ref(true); const loading = ref(true);
@ -212,16 +207,35 @@ const cardId = params.get('id');
const hasCard = computed(() => card.value !== null); const hasCard = computed(() => card.value !== null);
onMounted(async () => { onMounted(async () => {
try { // First, check if we have a persisted free draw card in the store
const response = await axios.get(`/api/get-card/${cardId}`); if (tarotStore.freeDrawCard) {
card.value = response.data.cards; card.value = tarotStore.freeDrawCard;
} catch (err: any) { loading.value = false;
console.error('Card fetch error:', err); return;
error.value = err.response?.data?.message || 'Failed to get cards from the server. Please contact support.'; }
} finally {
// Fallback: If no persisted card but we have an ID in the URL, try to fetch from API
if (cardId) {
try {
const response = await axios.get(`/api/get-card/${cardId}`);
card.value = response.data.cards;
} 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;
}
} else {
// No card in store and no ID in URL - just stop loading
loading.value = false; loading.value = false;
} }
}); });
// Reset the store and navigate to do a new reading
const resetAndNewReading = () => {
tarotStore.clearFreeDrawCard();
router.visit('/tirage');
};
</script> </script>
<style scoped> <style scoped>
/* Button styles */ /* Button styles */
@ -267,4 +281,21 @@ onMounted(async () => {
.mystic-btn:hover::before { .mystic-btn:hover::before {
transform: translateX(100%); transform: translateX(100%);
} }
:deep(strong),
:deep(b) {
font-weight: 700 !important; /* Forces bold weight */
color: white; /* Optional: Makes it fully white vs the 80% opacity body text */
}
/* Optional: If you want to style lists inside description too */
:deep(ul),
:deep(ol) {
padding-left: 1.5rem;
list-style-type: disc;
}
:deep(li) {
margin-bottom: 0.5rem;
}
</style> </style>

View File

@ -1,8 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { useTarotStore } from '@/stores/tarot'; import { useTarotStore } from '@/stores/tarot';
import { router } from '@inertiajs/vue3';
import { loadStripe } from '@stripe/stripe-js'; import { loadStripe } from '@stripe/stripe-js';
import axios from 'axios'; import axios from 'axios';
import { computed, ref } from 'vue'; import { computed, onBeforeMount, ref } from 'vue';
const tarotStore = useTarotStore(); const tarotStore = useTarotStore();
const isSelectionScreen = ref(true); const isSelectionScreen = ref(true);
@ -87,6 +88,15 @@ const redirectToWisePayment = async (count: number) => {
// Computed to disable the free draw button if used // Computed to disable the free draw button if used
const isFreeDrawUsed = computed(() => tarotStore.freeDrawsRemaining <= 0); const isFreeDrawUsed = computed(() => tarotStore.freeDrawsRemaining <= 0);
// Redirect to FreeCardResult.vue with card id
function handleViewMyReading() {
if (tarotStore.freeDrawCard && tarotStore.freeDrawCard.id !== undefined) {
router.visit(`/resultat-gratuit?id=${tarotStore.freeDrawCard.id}`);
} else {
router.visit('/resultat-gratuit');
}
}
// Hover state for cards // Hover state for cards
const hoveredCard = ref(null); const hoveredCard = ref(null);
@ -97,6 +107,10 @@ const setHover = (index) => {
const clearHover = () => { const clearHover = () => {
hoveredCard.value = null; hoveredCard.value = null;
}; };
onBeforeMount(() => {
console.log(tarotStore.freeDrawCard);
});
</script> </script>
<template> <template>
<section class="flex min-h-screen flex-col items-center justify-center px-4 py-16 sm:py-20"> <section class="flex min-h-screen flex-col items-center justify-center px-4 py-16 sm:py-20">
@ -156,14 +170,15 @@ const clearHover = () => {
</div> </div>
<h3 class="text-xl font-bold text-white md:text-2xl">Reveal your inner power</h3> <h3 class="text-xl font-bold text-white md:text-2xl">Reveal your inner power</h3>
<p class="mt-2 text-4xl font-bold text-[var(--c-gold)] md:text-5xl">Free</p> <p class="mt-2 text-4xl font-bold text-[var(--c-gold)] md:text-5xl">Free</p>
<p class="mt-4 text-sm text-gray-300 md:text-base">Singlecard Reading General Interpretation Quick Tips</p> <p class="mt-4 text-sm text-gray-300 md:text-base">Singlecard Reading Interpretation</p>
</div> </div>
<button <button
:disabled="isFreeDrawUsed"
class="group relative mt-auto 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 disabled:cursor-not-allowed disabled:opacity-50" class="group relative mt-auto 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 disabled:cursor-not-allowed disabled:opacity-50"
@click="handleSelection(1)" @click="isFreeDrawUsed ? handleViewMyReading() : handleSelection(1)"
> >
<span class="relative z-10">Begin</span> <span class="relative z-10 transition-transform duration-300 group-hover:translate-x-1">
{{ isFreeDrawUsed ? 'VIEW MY READING' : 'BEGIN' }}
</span>
<div <div
class="absolute inset-0 bg-gradient-to-r from-[var(--c-gold)]/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100" class="absolute inset-0 bg-gradient-to-r from-[var(--c-gold)]/20 to-transparent opacity-0 transition-opacity duration-300 group-hover:opacity-100"
></div> ></div>

View File

@ -124,6 +124,8 @@ onMounted(async () => {
:image-url="card.image_url" :image-url="card.image_url"
:orientation="card.orientation" :orientation="card.orientation"
:description="card.description" :description="card.description"
:description_upright="card.description_upright"
:description_reversed="card.description_reversed"
/> />
</div> </div>
</div> </div>

View File

@ -63,6 +63,10 @@ const handleSelection = async (count: number) => {
}); });
if (response.data.success) { if (response.data.success) {
cards.value = response.data.cards; cards.value = response.data.cards;
// Store the free draw card for persistence (so user can view it again later)
if (response.data.cards.length > 0) {
tarotStore.setFreeDrawCard(response.data.cards[0]);
}
// Only use the free draw after a successful API call // Only use the free draw after a successful API call
tarotStore.useFreeDraw(); tarotStore.useFreeDraw();
isSelectionScreen.value = false; isSelectionScreen.value = false;

View File

@ -17,7 +17,7 @@ const maxPolls = 30; // Poll for 5 minutes (30 * 10 seconds)
// Poll payment status every 10 seconds // Poll payment status every 10 seconds
const checkPaymentStatus = async () => { const checkPaymentStatus = async () => {
try { try {
const endpoint = props.paymentProvider === 'wise' ? '/wise/validate-payment' : '/stripe/validate-payment'; const endpoint = props.paymentProvider === 'wise' ? '/wise/validate-payment' : '/api/validate-payment';
const response = await axios.get(endpoint, { const response = await axios.get(endpoint, {
params: { client_session_id: props.clientSessionId }, params: { client_session_id: props.clientSessionId },

View File

@ -2,37 +2,32 @@
<LandingLayout> <LandingLayout>
<main class="flex flex-grow items-center justify-center"> <main class="flex flex-grow items-center justify-center">
<div class="w-full max-w-2xl px-4 py-20 sm:px-6 lg:px-8"> <div class="w-full max-w-2xl px-4 py-20 sm:px-6 lg:px-8">
<div class="relative flex flex-col items-center overflow-hidden rounded-3xl border border-[var(--c-purple)]/30 bg-gradient-to-br from-[var(--card)] to-[var(--card)]/90 p-12 text-center shadow-2xl ring-1 ring-[var(--c-purple)]/40"> <div
<div class="pointer-events-none absolute -left-16 -top-16 h-40 w-40 rounded-full bg-[var(--c-purple)]/20 blur-3xl"></div> class="relative flex flex-col items-center overflow-hidden rounded-3xl border border-[var(--c-purple)]/30 bg-gradient-to-br from-[var(--c-purple)]/20 to-[var(--c-gold)]/10 p-12 text-center shadow-2xl ring-1 ring-[var(--c-purple)]/40"
>
<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>
<div class="relative z-10 mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[var(--c-gold)] shadow-lg"> <div class="relative z-10 mb-8 flex h-20 w-20 items-center justify-center rounded-full bg-[var(--c-gold)] shadow-lg">
<svg <svg class="h-10 w-10 text-[var(--c-purple)]" fill="currentColor" viewBox="0 0 256 256" xmlns="http://www.w3.org/2000/svg">
class="h-10 w-10 text-[var(--c-purple)]"
fill="currentColor"
viewBox="0 0 256 256"
xmlns="http://www.w3.org/2000/svg"
>
<path <path
d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z"
></path> ></path>
</svg> </svg>
</div> </div>
<h1 class="text-4xl font-black text-white md:text-5xl">Paiement Réussi</h1> <h1 class="text-4xl font-black text-white md:text-5xl">Paiement Réussi</h1>
<p class="mx-auto mt-4 max-w-md text-lg text-white/80"> <p class="mx-auto mt-4 max-w-md text-lg text-white/80">Votre transaction a été complétée avec succès. L'univers vous attend.</p>
Votre transaction a été complétée avec succès. L'univers vous attend.
</p>
<div class="mt-10 w-full border-t border-[var(--linen)]"></div> <div class="mt-10 w-full border-t border-[var(--linen)]"></div>
<p v-if="loading" class="mt-10 text-lg font-medium text-white/90">Vérification de votre paiement...</p> <p v-if="loading" class="mt-10 text-lg font-medium text-white/90">Vérification de votre paiement...</p>
<p v-else class="mt-10 text-lg font-medium text-white/90"> <p v-else class="mt-10 text-lg font-medium text-white/90">Vous pouvez maintenant procéder à votre tirage de cartes.</p>
Vous pouvez maintenant procéder à votre tirage de cartes.
</p>
<button <button
@click="proceedToDraw" @click="proceedToDraw"
:disabled="loading" :disabled="loading"
class="group relative mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-[var(--c-gold)] to-yellow-400 px-8 text-base font-bold tracking-wide text-[var(--c-purple)] shadow-lg transition-all duration-300 hover:shadow-[var(--c-gold)]/40 disabled:opacity-60" class="group relative mt-8 flex h-12 max-w-[480px] min-w-[200px] cursor-pointer items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-[var(--c-gold)] to-yellow-400 px-8 text-base font-bold tracking-wide text-[var(--c-purple)] shadow-lg transition-all duration-300 hover:shadow-[var(--c-gold)]/40 disabled:opacity-60"
> >
<span class="relative z-10">Tirer les cartes</span> <span class="relative z-10">Tirer les cartes</span>
<span class="absolute inset-0 translate-x-[-100%] -skew-x-12 bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-700 group-hover:translate-x-[100%]"></span> <span
class="absolute inset-0 translate-x-[-100%] -skew-x-12 bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-700 group-hover:translate-x-[100%]"
></span>
</button> </button>
</div> </div>
</div> </div>
@ -66,7 +61,7 @@ onMounted(async () => {
} }
} catch (error) { } catch (error) {
console.error('Erreur lors de la validation du paiement:', error); console.error('Erreur lors de la validation du paiement:', error);
alert('Erreur de validation. Veuillez réessayer.');
router.visit('/cancel'); router.visit('/cancel');
} finally { } finally {
loading.value = false; loading.value = false;

View File

@ -1,40 +1,78 @@
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { ref } from 'vue'; import { ref } from 'vue';
export const useTarotStore = defineStore('tarot', () => { // Interface pour la carte tirée
// State interface DrawnCard {
const freeDrawsRemaining = ref(1); id: number;
const paidDrawsRemaining = ref(0); name: string;
image_url: string;
orientation: 'upright' | 'reversed';
description: string;
description_upright?: string;
description_reversed?: string;
symbolism?: Record<string, string>;
created_at: string;
}
// Actions export const useTarotStore = defineStore(
function useFreeDraw() { 'tarot',
if (freeDrawsRemaining.value > 0) { () => {
freeDrawsRemaining.value--; // State
return true; const freeDrawsRemaining = ref(1);
const paidDrawsRemaining = ref(0);
const freeDrawCard = ref<DrawnCard | null>(null);
// Actions
function useFreeDraw() {
if (freeDrawsRemaining.value > 0) {
freeDrawsRemaining.value--;
return true;
}
return false;
} }
return false;
}
// Modified usePaidDraw to handle the correct number of cards and reset the state // Store the free draw card for persistence
function usePaidDraw(count: number) { function setFreeDrawCard(card: DrawnCard) {
if (paidDrawsRemaining.value >= count) { freeDrawCard.value = card;
// Since the draws are 'used', we set the remaining to 0.
// This assumes a user pays for a set number of cards in one go.
paidDrawsRemaining.value = 0;
return true;
} }
return false;
}
function addPaidDraws(count: number) { // Clear the free draw card (e.g., for a new session)
paidDrawsRemaining.value += count; function clearFreeDrawCard() {
} freeDrawCard.value = null;
freeDrawsRemaining.value = 1;
}
return { // Modified usePaidDraw to handle the correct number of cards and reset the state
freeDrawsRemaining, function usePaidDraw(count: number) {
paidDrawsRemaining, if (paidDrawsRemaining.value >= count) {
useFreeDraw, // Since the draws are 'used', we set the remaining to 0.
usePaidDraw, // This assumes a user pays for a set number of cards in one go.
addPaidDraws, paidDrawsRemaining.value = 0;
}; return true;
}); }
return false;
}
function addPaidDraws(count: number) {
paidDrawsRemaining.value += count;
}
return {
freeDrawsRemaining,
paidDrawsRemaining,
freeDrawCard,
useFreeDraw,
setFreeDrawCard,
clearFreeDrawCard,
usePaidDraw,
addPaidDraws,
};
},
{
persist: {
key: 'tarot-storage',
storage: typeof window !== 'undefined' ? localStorage : undefined,
pick: ['freeDrawsRemaining', 'freeDrawCard'],
},
}
);