180 lines
6.4 KiB
Vue
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>
|