card gasp
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled

This commit is contained in:
Nyavokevin 2025-09-19 15:32:25 +03:00
parent 9e5cc5ab1a
commit 1504c841e0
4 changed files with 402 additions and 87 deletions

BIN
public/hero-tarot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

View File

@ -0,0 +1,186 @@
<template>
<!-- Fixed, pointer-events none so it stays behind the content -->
<div ref="container" class="pointer-events-none fixed inset-0 z-0 overflow-hidden opacity-0 transition-opacity duration-500">
<div
v-for="(src, i) in images"
:key="i"
class="absolute rounded-lg bg-card opacity-70 shadow-2xl"
:style="{
left: `${(i * 83) % 100}%`,
top: `${(i * 37) % 100}%`,
transform: `translate(-50%, -50%) rotate(${(i % 2 ? 1 : -1) * 8}deg)`,
backgroundImage: `url(${src})`,
}"
></div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const container = ref<HTMLDivElement | null>(null);
function loadScript(src: string) {
return new Promise<void>((resolve, reject) => {
const s = document.createElement('script');
s.src = src;
s.async = true;
s.onload = () => resolve();
s.onerror = () => reject(new Error(`Failed to load ${src}`));
document.head.appendChild(s);
});
}
async function loadGsap() {
const w = window as any;
if (!w.gsap) {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js');
}
if (!w.ScrollTrigger) {
await loadScript('https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/ScrollTrigger.min.js');
}
if (w.gsap && w.ScrollTrigger) {
w.gsap.registerPlugin(w.ScrollTrigger);
}
return w.gsap as typeof import('gsap');
}
const images = Array.from({ length: 5 }, (_, i) => `/cards/${i + 1}.png`);
onMounted(async () => {
try {
const gsap = await loadGsap();
const w = window as any;
const ScrollTrigger = w.ScrollTrigger;
const cards = container.value?.querySelectorAll('.bg-card');
cards?.forEach((el: Element, idx: number) => {
// Random starting positions and properties
const startY = gsap.utils.random(-150, 150);
const startX = gsap.utils.random(-100, 100);
const rot = gsap.utils.random(-15, 15);
const drift = gsap.utils.random(120, 280);
const scale = gsap.utils.random(0.8, 1.2);
const duration = gsap.utils.random(15, 25);
// Set initial state
gsap.set(el, {
y: startY,
x: startX,
rotation: rot,
opacity: 0.5,
scale: scale,
});
// Create floating animation
gsap.to(el, {
y: startY + drift,
x: startX + gsap.utils.random(-40, 40),
rotation: rot + gsap.utils.random(-20, 20),
opacity: gsap.utils.random(0.6, 0.9),
scale: scale + gsap.utils.random(-0.1, 0.1),
ease: 'sine.inOut',
duration: duration,
yoyo: true,
repeat: -1,
delay: idx * 0.3,
});
// Scroll-based animation
gsap.to(el, {
y: startY + drift * 1.5,
rotation: rot + gsap.utils.random(-10, 10),
opacity: 0.85,
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
});
});
// Subtle parallax drift for the whole layer
gsap.to(container.value, {
y: 60,
ease: 'none',
scrollTrigger: {
trigger: document.body,
start: 'top top',
end: 'bottom bottom',
scrub: true,
},
});
// Add pulsing glow effect to container (will be visible only when activated)
gsap.to(container.value, {
duration: 4,
opacity: 0.9,
repeat: -1,
yoyo: true,
ease: 'sine.inOut',
});
// Show only after hero: observe the sentinel placed after hero
const sentinel = document.getElementById('after-hero-sentinel');
if (sentinel) {
const ob = new IntersectionObserver(
(entries) => {
entries.forEach((e) => {
if (e.isIntersecting) {
container.value?.classList.remove('opacity-0');
container.value?.classList.add('opacity-100');
} else {
container.value?.classList.add('opacity-0');
container.value?.classList.remove('opacity-100');
}
});
},
{ threshold: 0.1 }
);
ob.observe(sentinel);
}
} catch (e) {
console.error('GSAP load/animation failed:', e);
}
});
</script>
<style scoped>
.bg-card {
background-size: cover;
background-position: center;
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.35));
mix-blend-mode: screen;
width: 8rem;
height: 12rem;
transition: filter 0.5s ease;
}
.bg-card:hover {
filter: drop-shadow(0 15px 35px rgba(100, 100, 255, 0.5)) brightness(1.1);
}
@media (min-width: 1024px) {
.bg-card {
width: 10rem;
height: 15rem;
}
}
/* Subtle glow animation for the container */
::v-deep(.fixed) {
animation: subtleGlow 8s ease-in-out infinite;
}
@keyframes subtleGlow {
0%,
100% {
opacity: 0.8;
}
50% {
opacity: 1;
}
}
</style>

View File

@ -1,96 +1,180 @@
<script lang="ts" setup>
import { router } from '@inertiajs/vue3';
import { onMounted, ref } from 'vue';
const isMounted = ref(false);
const buttonHover = ref(false);
onMounted(() => {
setTimeout(() => {
isMounted.value = true;
}, 100);
});
const goToShuffle = () => {
router.visit('/tirage');
};
</script>
<template> <template>
<section class="relative flex min-h-[60vh] items-center justify-center overflow-hidden px-4 py-20 text-center md:min-h-[80vh]"> <section
<!-- Animated background elements --> class="relative flex min-h-screen w-full items-center justify-center overflow-hidden bg-cover bg-center bg-no-repeat px-4 py-24 text-center"
<div class="absolute inset-0 bg-black/50"></div> >
<div class="absolute top-0 left-0 h-full w-full"> <div class="pointer-events-none absolute inset-0 bg-gradient-to-b from-[var(--c-purple)]/60 via-[var(--c-deep-navy)]/60 to-black/80"></div>
<div
class="absolute top-1/4 left-1/4 h-72 w-72 animate-pulse rounded-full bg-purple-500 opacity-30 mix-blend-soft-light blur-xl filter"
></div>
<div
class="absolute right-1/4 bottom-1/3 h-72 w-72 animate-bounce rounded-full bg-yellow-300 opacity-30 mix-blend-soft-light blur-xl filter delay-1000"
style="animation-duration: 15s"
></div>
</div>
<div class="relative z-10 mx-auto max-w-3xl"> <div class="relative z-10 mx-auto flex max-w-5xl flex-col items-center justify-between lg:flex-row">
<!-- Animated heading with hover effect --> <!-- Text Content -->
<div class="mb-10 lg:mb-0 lg:w-1/2">
<h1 <h1
class="mb-4 transform text-4xl font-bold text-white transition-all duration-700 md:text-6xl" class="mb-4 transform text-4xl font-black text-white transition-all duration-700 md:text-6xl"
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'" :class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
> >
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">Libérez</span> Révélez Votre Voyage Intérieur
<span class="mx-2 inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">votre</span>
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-yellow-300">potentiel</span>
<span class="mt-2 block"></span>
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">avec Toutes</span>
<span class="mx-2 inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-purple-200">les</span>
<span class="inline-block cursor-default transition-all duration-300 hover:scale-105 hover:text-yellow-300">Clés du Succès</span>
</h1> </h1>
<!-- Animated paragraph -->
<p <p
class="mb-8 transform text-lg text-white/80 transition-all delay-200 duration-1000 md:text-xl" class="mb-8 transform text-lg text-white/80 transition-all delay-200 duration-1000 md:text-xl"
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'" :class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
> >
Gagnez en clarté et en direction avec un coaching personnalisé et des tirages d'oracle. Embrassez la sagesse intemporelle de l'Oracle de Kris Saint Ange, un guide stratégique pour naviguer votre destin avec clarté et
<span class="mt-2 block md:inline-block">Découvrez votre chemin vers le succès et l'épanouissement.</span> confiance.
</p> </p>
<!-- Animated buttons with enhanced hover effects --> <!-- Buttons -->
<div <div
class="flex transform flex-wrap justify-center gap-4 transition-all delay-500 duration-1000" class="flex transform flex-wrap justify-center gap-6 transition-all delay-500 duration-1000"
:class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'" :class="isMounted ? 'translate-y-0 opacity-100' : 'translate-y-10 opacity-0'"
> >
<button <button
@mouseenter="buttonHover = true"
@mouseleave="buttonHover = false"
@click="goToShuffle" @click="goToShuffle"
class="group relative flex h-14 max-w-[480px] min-w-[180px] cursor-pointer items-center justify-center overflow-hidden rounded-xl bg-gradient-to-r from-purple-600 to-purple-800 px-8 text-base font-bold text-white shadow-lg shadow-purple-500/30 transition-all duration-500 hover:from-purple-700 hover:to-purple-900 hover:shadow-xl hover:shadow-purple-500/50 focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-black focus:outline-none" class="group relative inline-flex h-14 min-w-[180px] items-center justify-center overflow-hidden rounded-full bg-gradient-to-r from-[var(--c-gold)] to-yellow-400 px-8 font-bold tracking-wide text-[var(--c-purple)] shadow-lg transition-all duration-300 hover:-translate-y-0.5 hover:shadow-[var(--c-gold)]/40"
> >
<span class="relative z-10 truncate transition-all duration-300 group-hover:scale-105">Essayez 1 carte gratuite</span> <span class="relative z-10 truncate">Essayez 1 carte gratuite</span>
<div <span
class="absolute inset-0 bg-gradient-to-r from-yellow-400 to-purple-500 opacity-0 transition-opacity duration-500 group-hover:opacity-30" class="pointer-events-none absolute inset-0 translate-x-[-120%] -skew-x-12 bg-gradient-to-r from-transparent via-white/30 to-transparent transition-transform duration-700 group-hover:translate-x-[120%]"
></div> ></span>
</button> </button>
<button <button
@mouseenter="buttonHover = true"
@mouseleave="buttonHover = false"
@click="goToShuffle" @click="goToShuffle"
class="group relative flex h-14 max-w-[480px] min-w-[180px] cursor-pointer items-center justify-center overflow-hidden rounded-xl border-2 border-purple-500 bg-transparent px-8 text-base font-bold text-white transition-all duration-500 hover:border-transparent hover:bg-gradient-to-r hover:from-purple-600 hover:to-purple-800 hover:text-white focus:ring-2 focus:ring-purple-400 focus:ring-offset-2 focus:ring-offset-black focus:outline-none" class="inline-flex h-14 min-w-[180px] items-center justify-center rounded-full border-2 border-[var(--c-purple)] bg-transparent px-8 font-bold text-white transition-all duration-300 hover:-translate-y-0.5 hover:bg-[var(--c-purple)]"
> >
<span class="relative z-10 truncate transition-all duration-300 group-hover:scale-105">Découvrir les tirages</span> <span class="truncate">Découvrir les tirages</span>
<div class="absolute inset-0 bg-white opacity-0 transition-opacity duration-500 group-hover:opacity-10"></div>
</button> </button>
</div> </div>
</div> </div>
<!-- Card Stack -->
<div class="relative flex h-96 items-center justify-center lg:w-1/2">
<div class="perspective-1000 relative h-96 w-64" @click.self="randomizeStack" title="Cliquez pour mélanger">
<!-- Card Stack -->
<div
v-for="(card, index) in cardStack"
:key="card.id"
class="absolute h-full w-full cursor-pointer transition-all duration-500"
:style="{
transform: `translateY(${index * -4}px) rotate(${index * -2}deg)`,
zIndex: cardStack.length - index,
}"
@click="flipCard(card)"
>
<div
class="preserve-3d h-full w-full rounded-xl shadow-xl transition-transform duration-700"
:class="{ 'rotate-y-180': card.isFlipped }"
>
<!-- Card Back -->
<div
class="border-gold/50 absolute h-full w-full overflow-hidden rounded-xl border-2 bg-gradient-to-br from-purple-900 via-blue-900 to-indigo-900 backface-hidden"
>
<img src="/cards/1.png" alt="Card Back" class="card-fill" />
</div>
<!-- Card Front -->
<div class="absolute h-full w-full rotate-y-180 overflow-hidden rounded-xl backface-hidden">
<img :src="card.image" class="card-fill" :alt="'Card ' + card.id" />
</div>
</div>
</div>
</div>
</div>
</div>
</section> </section>
</template> </template>
<style scoped> <script setup lang="ts">
.gradient-bg { import { router } from '@inertiajs/vue3';
background: linear-gradient(135deg, #1a202c 0%, #2d3748 50%, #4a5568 100%); import { onMounted, ref } from 'vue';
const isMounted = ref(false);
const container = ref<HTMLDivElement | null>(null);
// Sample card data
const cardStack = ref([
{ id: 2, title: "L'Intuition", image: '/cards/2.png', isFlipped: false },
{ id: 3, title: 'La Sagesse', image: '/cards/3.png', isFlipped: false },
{ id: 4, title: 'Le Destin', image: '/cards/4.png', isFlipped: false },
]);
// Background images
const backgroundImages = Array.from({ length: 12 }, (_, i) => `/cards/${(i % 4) + 1}.png`);
onMounted(() => {
setTimeout(() => {
isMounted.value = true;
}, 100);
// Initialize GSAP animations for background
if (container.value) {
const cards = container.value.querySelectorAll('.bg-card');
cards.forEach((el: Element, idx: number) => {
const startY = Math.random() * 300 - 150;
const rot = Math.random() * 30 - 15;
const drift = Math.random() * 160 + 120;
// Set initial state
(el as HTMLElement).style.transform = `translate(-50%, -50%) rotate(${rot}deg) translateY(${startY}px)`;
(el as HTMLElement).style.opacity = '0.5';
// Animate with requestAnimationFrame for simplicity
// In a real implementation, you would use GSAP here
let startTime: number | null = null;
const duration = 20000 + Math.random() * 10000;
function animate(timestamp: number) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = (elapsed % duration) / duration;
const yPos = startY + drift * progress;
const rotation = rot + 10 * Math.sin(progress * Math.PI * 2);
(el as HTMLElement).style.transform = `translate(-50%, -50%) rotate(${rotation}deg) translateY(${yPos}px)`;
(el as HTMLElement).style.opacity = `${0.5 + 0.35 * Math.sin(progress * Math.PI)}`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
});
}
});
const goToShuffle = () => {
router.visit('/tirage');
};
function randomInt(min: number, max: number) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function randomizeStack() {
const picked = new Set<number>();
while (picked.size < 3) picked.add(randomInt(1, 22));
const ids = Array.from(picked);
cardStack.value = ids.map((id, idx) => ({ id, title: `Carte ${id}`, image: `/cards/${id}.png`, isFlipped: false }));
}
const flipCard = (card: any) => {
card.isFlipped = !card.isFlipped;
};
</script>
<style scoped>
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
33% {
transform: translateY(-20px) rotate(5deg);
}
66% {
transform: translateY(10px) rotate(-5deg);
}
} }
/* Custom animation for text elements */
@keyframes textShine { @keyframes textShine {
0% { 0% {
background-position: 0% 50%; background-position: 0% 50%;
@ -100,12 +184,54 @@ const goToShuffle = () => {
} }
} }
.animate-float {
animation: float 10s ease-in-out infinite;
}
.animate-text-shine { .animate-text-shine {
background: linear-gradient(to right, #ffffff 20%, #d8b4fe 30%, #fef08a 70%, #ffffff 80%); animation: textShine 2s ease-in-out infinite alternate;
-webkit-background-clip: text; }
background-clip: text;
-webkit-text-fill-color: transparent; .card-fill {
background-size: 500% auto; width: 100%;
animation: textShine 5s ease-in-out infinite alternate; height: 100%;
object-fit: cover;
}
.perspective-1000 {
perspective: 1000px;
}
.preserve-3d {
transform-style: preserve-3d;
}
.rotate-y-180 {
transform: rotateY(180deg);
}
.backface-hidden {
backface-visibility: hidden;
}
.bg-card {
background-size: cover;
background-position: center;
filter: drop-shadow(0 12px 30px rgba(0, 0, 0, 0.35));
mix-blend-mode: screen;
}
/* Custom color variables */
:root {
--c-purple: #4c1d95;
--c-deep-navy: #1e1b4b;
--c-gold: rgba(245, 158, 11, 0.7);
}
@media (min-width: 1024px) {
.bg-card {
width: 10rem;
height: 15rem;
}
} }
</style> </style>

View File

@ -5,6 +5,8 @@
<main class="flex flex-col items-center"> <main class="flex flex-col items-center">
<div class="w-full max-w-7xl px-4 sm:px-6 lg:px-8"> <div class="w-full max-w-7xl px-4 sm:px-6 lg:px-8">
<HeroSection primaryButtonLink="/tirage" secondaryButtonLink="/tirage" minHeight="70vh" /> <HeroSection primaryButtonLink="/tirage" secondaryButtonLink="/tirage" minHeight="70vh" />
<div id="after-hero-sentinel" class="h-px w-full"></div>
<BackgroundCards />
<ManuscritSection /> <ManuscritSection />
<OfferSection /> <OfferSection />
<HowSection /> <HowSection />
@ -23,5 +25,6 @@ import HowSection from '@/components/landing/HowSection.vue';
import ManuscritSection from '@/components/landing/ManuscritSection.vue'; import ManuscritSection from '@/components/landing/ManuscritSection.vue';
import OfferSection from '@/components/landing/OfferSection.vue'; import OfferSection from '@/components/landing/OfferSection.vue';
import TestimonialsSection from '@/components/landing/TestimonialsSection.vue'; import TestimonialsSection from '@/components/landing/TestimonialsSection.vue';
import BackgroundCards from '@/components/landing/BackgroundCards.vue';
import LandingLayout from '@/layouts/app/LandingLayout.vue'; import LandingLayout from '@/layouts/app/LandingLayout.vue';
</script> </script>