Files
FamilyHUB/frontend/src/components/SwipeCards.vue
T

180 lines
6.4 KiB
Vue

<script setup lang="ts">
import { computed, ref } from 'vue';
import { Clock, Heart, MapPin, Sparkles, Star, X } from 'lucide-vue-next';
interface SwipeItem {
id: number;
type: 'food' | 'movie';
title: string;
subtitle: string;
rating: number;
location?: string;
duration?: string;
image: string;
}
const cards: SwipeItem[] = [
{
id: 1,
type: 'food',
title: 'Italian Restaurant',
subtitle: 'Authentic pasta & pizza',
rating: 4.8,
location: '2.3 km away',
image: 'https://images.unsplash.com/photo-1555396273-367ea4eb4db5?w=800&q=80',
},
{
id: 2,
type: 'movie',
title: 'Dune: Part Two',
subtitle: 'Sci-Fi Epic',
rating: 4.9,
duration: '2h 46m',
image: 'https://images.unsplash.com/photo-1536440136628-849c177e76a1?w=800&q=80',
},
{
id: 3,
type: 'food',
title: 'Sushi House',
subtitle: 'Fresh Japanese cuisine',
rating: 4.7,
location: '1.8 km away',
image: 'https://images.unsplash.com/photo-1579584425555-c3ce17fd4351?w=800&q=80',
},
];
const currentIndex = ref(0);
const swipeDirection = ref<'left' | 'right' | null>(null);
const pointerStartX = ref<number | null>(null);
const currentCard = computed(() => cards[currentIndex.value]);
function nextCard() {
currentIndex.value = (currentIndex.value + 1) % cards.length;
}
function handleSwipe(direction: 'left' | 'right') {
swipeDirection.value = direction;
window.setTimeout(() => {
nextCard();
swipeDirection.value = null;
}, 250);
}
function onPointerDown(event: PointerEvent) {
pointerStartX.value = event.clientX;
}
function onPointerUp(event: PointerEvent) {
if (pointerStartX.value === null) {
return;
}
const deltaX = event.clientX - pointerStartX.value;
pointerStartX.value = null;
if (deltaX > 60) {
handleSwipe('right');
} else if (deltaX < -60) {
handleSwipe('left');
}
}
</script>
<template>
<div class="rounded-[18px] border border-white/[0.06] bg-[#16161F] p-5 shadow-[0_4px_20px_rgba(0,0,0,0.4)]">
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="flex h-9 w-9 items-center justify-center rounded-[12px] border border-purple-500/20 bg-gradient-to-br from-purple-500/20 to-blue-500/20">
<Sparkles class="h-[18px] w-[18px] text-purple-400" :stroke-width="2" />
</div>
<div>
<h3 class="text-[15px] font-semibold text-white">Vote Now</h3>
<p class="text-[11px] text-zinc-500">Swipe to decide</p>
</div>
</div>
<div class="flex items-center gap-1">
<div
v-for="(_, i) in cards"
:key="i"
:class="[
'h-1.5 rounded-full transition-all',
i === currentIndex ? 'w-6 bg-gradient-to-r from-purple-500 to-blue-500' : 'w-1.5 bg-zinc-700',
]"
/>
</div>
</div>
<div class="relative mb-4 h-[360px]">
<div
class="absolute inset-0 cursor-grab overflow-hidden rounded-[18px] shadow-xl active:cursor-grabbing"
:class="swipeDirection ? 'transition-all duration-200 ease-out opacity-0 scale-95' : ''"
@pointerdown="onPointerDown"
@pointerup="onPointerUp"
>
<img :src="currentCard.image" :alt="currentCard.title" class="h-full w-full object-cover" />
<div class="absolute inset-0 bg-gradient-to-t from-black via-black/50 to-transparent" />
<div class="absolute top-4 right-4 flex items-center gap-1.5 rounded-full border border-white/10 bg-black/60 px-3 py-1.5 backdrop-blur-md">
<Star class="h-3.5 w-3.5 fill-amber-400 text-amber-400" :stroke-width="2" />
<span class="text-[13px] font-bold text-white">{{ currentCard.rating }}</span>
</div>
<div class="absolute bottom-0 left-0 right-0 p-5">
<div class="mb-3">
<span class="mb-3 inline-flex items-center gap-1.5 rounded-lg border border-purple-500/20 bg-purple-500/20 px-2.5 py-1.5 text-[11px] font-semibold text-purple-300 backdrop-blur-sm">
{{ currentCard.type === 'food' ? 'Restaurant' : 'Movie' }}
</span>
<h4 class="mb-1 text-[24px] font-bold leading-tight text-white">{{ currentCard.title }}</h4>
<p class="mb-3 text-[14px] text-zinc-300">{{ currentCard.subtitle }}</p>
</div>
<div class="flex items-center gap-4 text-[12px] text-zinc-400">
<div v-if="currentCard.location" class="flex items-center gap-1.5">
<MapPin class="h-3.5 w-3.5" :stroke-width="2" />
<span class="font-medium">{{ currentCard.location }}</span>
</div>
<div v-if="currentCard.duration" class="flex items-center gap-1.5">
<Clock class="h-3.5 w-3.5" :stroke-width="2" />
<span class="font-medium">{{ currentCard.duration }}</span>
</div>
</div>
</div>
</div>
<div v-if="swipeDirection" class="pointer-events-none absolute inset-0 z-10 flex items-center justify-center">
<div
:class="[
'flex h-24 w-24 items-center justify-center rounded-full border-[3px] backdrop-blur-sm shadow-2xl',
swipeDirection === 'left' ? 'border-rose-500 bg-rose-500/20' : 'border-emerald-500 bg-emerald-500/20',
]"
>
<X v-if="swipeDirection === 'left'" class="h-12 w-12 text-rose-500" :stroke-width="3" />
<Heart
v-else
class="h-12 w-12 fill-emerald-500 text-emerald-500"
:stroke-width="3"
/>
</div>
</div>
</div>
<div class="flex items-center justify-center gap-4">
<button
type="button"
@click="handleSwipe('left')"
class="group flex h-14 w-14 items-center justify-center rounded-full border border-white/[0.08] bg-white/[0.06] transition-all hover:border-rose-500/30 hover:bg-rose-500/10 active:scale-95"
>
<X class="h-6 w-6 text-zinc-400 transition-colors group-hover:text-rose-400" :stroke-width="2.5" />
</button>
<button
type="button"
@click="handleSwipe('right')"
class="flex h-16 w-16 items-center justify-center rounded-full bg-gradient-to-br from-purple-500 to-blue-500 shadow-[0_8px_24px_rgba(139,92,246,0.35)] transition-all hover:from-purple-600 hover:to-blue-600 hover:shadow-[0_12px_32px_rgba(139,92,246,0.5)] active:scale-95"
>
<Heart class="h-7 w-7 text-white" :stroke-width="2.5" />
</button>
</div>
</div>
</template>