Boundless/frontend/src/lib/Card.svelte
2025-01-29 00:18:39 +01:00

172 lines
4.1 KiB
Svelte

<script lang="ts">
import { animate, spring } from "motion";
import { onMount, createEventDispatcher } from 'svelte';
import type { Profile } from "./profiles";
export let profile: Profile;
export let active = false;
let element: HTMLDivElement;
let x = 0;
let y = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
let rotate = 0;
let opacity = 1;
let overlayColor = '';
const dispatch = createEventDispatcher<{
swipe: boolean;
}>();
onMount(() => {
if (active) {
setupDrag();
}
// Type the animation parameters properly
animate(
element,
{ scale: [0.9, 1] },
{ duration: 0.3, easing: 'ease-out' }
);
return () => {
// Cleanup listeners on component destroy
if (active) {
element?.removeEventListener('pointerdown', startDrag);
document.removeEventListener('pointerup', endDrag);
document.removeEventListener('pointermove', updateDrag);
}
};
});
function setupDrag() {
element?.addEventListener('pointerdown', startDrag);
document.addEventListener('pointerup', endDrag);
document.addEventListener('pointermove', updateDrag);
}
function startDrag(e: PointerEvent) {
if (!element) return;
isDragging = true;
startX = e.clientX - x;
startY = e.clientY - y;
element.style.cursor = 'grabbing';
}
function updateDrag(e: PointerEvent) {
if (!isDragging || !element) return;
x = e.clientX - startX;
y = e.clientY - startY;
rotate = x * 0.1;
opacity = Math.max(0.2, 1 - Math.abs(x) / 300);
overlayColor = x > 0
? `linear-gradient(90deg, rgba(34,197,94,0.2) 0%, rgba(255,255,255,0) 60%)`
: `linear-gradient(270deg, rgba(239,68,68,0.2) 0%, rgba(255,255,255,0) 60%)`;
updateElementTransform();
}
function updateElementTransform() {
if (!element) return;
element.style.transform = `translate(${x}px, ${y}px) rotate(${rotate}deg)`;
element.style.opacity = opacity.toString();
}
function endDrag() {
if (!isDragging || !element) return;
isDragging = false;
element.style.cursor = 'grab';
const threshold = 100;
if (Math.abs(x) > threshold) {
swipe(x > 0);
} else {
resetCardPosition();
}
}
function resetCardPosition() {
animate(
element,
{
x: 0,
y: 0,
rotate: 0,
opacity: 1
} as any,
{
easing: spring({
stiffness: 300,
damping: 20
} as any)
} as any
);
overlayColor = '';
}
function swipe(right: boolean) {
const direction = right ? 500 : -500;
animate(
element,
{
x: direction,
y: y + (Math.random() - 0.5) * 100,
rotate: direction / 4,
opacity: 0,
scale: 0.8
},
{
easing: spring(5),
onComplete: () => {
x = 0;
y = 0;
rotate = 0;
opacity = 1;
overlayColor = '';
dispatch('swipe', right);
}
}
);
}
</script>
<div
bind:this={element}
class="card w-96 bg-base-100 shadow-2xl absolute cursor-grab
transition-transform duration-200 hover:shadow-primary/20
hover:-translate-y-1"
style="transform: translate({x}px, {y}px) rotate({rotate}deg);
opacity: {opacity}"
>
<div
class="absolute inset-0 rounded-xl"
style="background: {overlayColor}"
></div>
<figure class="h-64 overflow-hidden">
<img
src={profile.img}
alt={profile.name}
class="w-full h-full object-cover transition-transform
duration-300 group-hover:scale-105"
/>
</figure>
<div class="card-body bg-gradient-to-t from-base-100
via-base-100/90 to-transparent">
<div class="flex items-center gap-2">
<h2 class="card-title text-3xl font-bold drop-shadow-sm">
{profile.name}
</h2>
<div class="badge badge-primary badge-lg p-4">
{profile.age}
</div>
</div>
<p class="text-lg text-base-content/80">{profile.bio}</p>
</div>
</div>