Compare commits

...

4 Commits

Author SHA1 Message Date
Mohamad
727da919a5 more changes 2025-01-30 21:17:29 +01:00
mohamad
e7eaf2a592 changes 2025-01-30 08:30:39 +01:00
Mohamad
f903569a60 still prototype ig, but improved hehe 2025-01-29 18:15:27 +01:00
mohamad
38c19728e8 prototype ig 2025-01-29 00:18:39 +01:00
31 changed files with 6708 additions and 673 deletions

File diff suppressed because it is too large Load Diff

View File

@ -20,13 +20,20 @@
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@playwright/test": "^1.49.1",
"@sveltejs/adapter-auto": "^3.0.0",
"@sveltejs/adapter-node": "^5.2.11",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"@types/canvas-confetti": "^1.9.0",
"@types/leaflet": "^1.9.16",
"@types/phoenix": "^1.6.6",
"@types/pikaday": "^1.7.9",
"@types/three": "^0.172.0",
"autoprefixer": "^10.4.20",
"daisyui": "^4.12.23",
"eslint": "^9.18.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^2.46.1",
@ -43,6 +50,31 @@
"vitest": "^3.0.0"
},
"dependencies": {
"@inlang/paraglide-sveltekit": "^0.15.5"
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
"@capacitor/cli": "^7.0.1",
"@capacitor/core": "^7.0.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@inlang/paraglide-sveltekit": "^0.15.5",
"@svelte-plugins/datepicker": "^1.0.11",
"@uppy/core": "^4.4.1",
"@uppy/dashboard": "^4.3.1",
"@uppy/svelte": "^4.3.0",
"@uwdata/vgplot": "^0.12.2",
"canvas-confetti": "^1.9.3",
"date-fns": "^4.1.0",
"globe.gl": "^2.39.2",
"gsap": "^3.12.7",
"leaflet": "^1.9.4",
"lucide-svelte": "^0.474.0",
"maplibre-gl": "^5.1.0",
"motion": "^12.0.6",
"phoenix": "^1.7.18",
"pikaday": "^1.8.2",
"sonner": "^1.7.3",
"svelte-motion": "^0.12.2",
"svelte-transitions": "^1.2.0",
"three": "^0.172.0"
}
}

1
frontend/project.inlang/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
cache

View File

@ -0,0 +1 @@
3dce976a8e4e53c64d644fc466bfa4deade063ca8a755bb4c9eb5ab60d38c377

View File

@ -1,3 +1,12 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
/* Add to your global CSS */
.screen-content {
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(10px);
border-radius: 1rem;
padding: 2rem;
}

View File

@ -0,0 +1,172 @@
<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>

139
frontend/src/lib/Nav.svelte Normal file
View File

@ -0,0 +1,139 @@
<script lang="ts">
import { page } from '$app/stores';
import { fade } from 'svelte/transition';
interface NavItem {
id: 'home' | 'search' | 'add' | 'notifications' | 'messages';
path: string;
icon: {
active: string;
inactive: string;
};
label: string;
badge?: number;
}
// Configurable props
export let showLabels = true;
export let animate = true;
export let darkMode = false;
// Icons with distinct active/inactive states
const icons = {
home: {
active: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
inactive: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6',
},
search: {
active: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
inactive: 'M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z',
},
add: {
active: 'M12 4v16m8-8H4',
inactive: 'M12 4v16m8-8H4',
},
notifications: {
active: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
inactive: 'M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9',
},
messages: {
active: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8m-18 8h18a2 2 0 002-2V6a2 2 0 00-2-2H3a2 2 0 00-2 2v8a2 2 0 002 2z',
inactive: 'M3 8l7.89 5.26a2 2 0 002.22 0L21 8m-18 8h18a2 2 0 002-2V6a2 2 0 00-2-2H3a2 2 0 00-2 2v8a2 2 0 002 2z',
}
};
const navItems: NavItem[] = [
{ id: 'home', path: '/home', icon: icons.home, label: 'Home' },
{ id: 'search', path: '/search', icon: icons.search, label: 'Search' },
{ id: 'add', path: '/add', icon: icons.add, label: '+' },
{ id: 'notifications', path: '/notifications', icon: icons.notifications, label: 'Notifications', badge: 3 },
{ id: 'messages', path: '/dm', icon: icons.messages, label: 'DM' }
];
// Check if current route matches
const isActive = (path: string) => {
return $page.url.pathname === path;
};
</script>
<nav
class="btm-nav fixed bottom-0 z-50 w-full border-t {darkMode ? 'border-gray-700 bg-gray-900' : 'border-base-200 bg-base-100'}"
aria-label="Main navigation"
>
{#each navItems as tab (tab.id)}
<a
href={tab.path}
class="btm-nav-btn relative flex flex-col items-center justify-center gap-1 p-2 {isActive(tab.path) ? 'active text-primary' : 'text-base-content'}"
aria-label={tab.label}
aria-current={isActive(tab.path) ? 'page' : undefined}
>
<div class="relative">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 transition-all duration-200 {animate && isActive(tab.path) ? 'scale-110' : 'scale-105'}"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
stroke-width={isActive(tab.path) ? 2 : 1.5}
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d={isActive(tab.path) ? tab.icon.active : tab.icon.inactive}
/>
</svg>
{#if tab.badge}
<span
class="absolute -right-2 -top-2 flex h-4 w-4 items-center justify-center rounded-full bg-red-500 text-xs text-white"
transition:fade
>
{tab.badge}
</span>
{/if}
</div>
{#if showLabels}
<span
class="btm-nav-label text-xs font-medium transition-opacity {isActive(tab.path) ? 'opacity-100' : 'opacity-0'}"
>
{tab.label}
</span>
{/if}
</a>
{/each}
</nav>
<style>
.btm-nav {
height: 64px;
display: grid;
grid-template-columns: repeat(5, 1fr);
padding-bottom: env(safe-area-inset-bottom);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1);
}
.btm-nav-btn {
position: relative;
transition: all 0.2s ease;
text-decoration: none;
}
/* .btm-nav-btn.active::after {
content: '';
position: absolute;
bottom: 8px;
width: 24px;
height: 2px;
background: currentColor;
border-radius: 2px;
} */
/* @media (min-width: 640px) {
.btm-nav {
max-width: 480px;
margin: 0 auto;
border-radius: 16px 16px 0 0;
}
} */
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

View File

@ -0,0 +1,24 @@
export interface Profile {
id: number;
name: string;
age: number;
bio: string;
img: string;
}
export const profiles: Profile[] = [
{
id: 1,
name: "Sarah",
age: 28,
bio: "Adventure seeker | Coffee lover",
img: "https://randomuser.me/api/portraits/women/1.jpg"
},
{
id: 2,
name: "Mike",
age: 32,
bio: "Hiker | Guitar player",
img: "https://randomuser.me/api/portraits/men/1.jpg"
}
];

30
frontend/src/lib/types.ts Normal file
View File

@ -0,0 +1,30 @@
// types.ts
export interface User {
id: number;
name: string;
avatar: string;
online: boolean;
}
export interface Conversation {
id: number;
user: User;
lastMessage: string;
timestamp: string;
unread: number;
}
export interface Message {
id: number;
text: string;
sender: 'me' | number;
timestamp: string;
}
export interface Notification {
id: number;
type: 'message' | 'alert' | 'system';
content: string;
read: boolean;
timestamp: string;
}

View File

@ -0,0 +1,34 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { i18n } from '$lib/i18n';
import Nav from '$lib/Nav.svelte';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
import { fade } from 'svelte/transition';
let { children } = $props();
</script>
<ParaglideJS {i18n}>
<div class="min-h-full fixed w-full bg-gradient-to-b from-sky-100 to-emerald-50 z-30">
<header class="flex items-center justify-between mb-6 px-6 pt-6 sticky top-0 w-full z-10" in:fade>
<button class="btn btn-circle btn-primary" onclick={() => {window.history.back()}}></button>
<h1 class="text-2xl font-bold text-sky-800 drop-shadow-sm">Boundless</h1>
<div class="w-8"></div>
</header>
<div class="fillscreen">
{@render children()}
</div>
<Nav
showLabels={false}
animate={true}
darkMode={false}
/>
</div>
</ParaglideJS>
<style>
.fillscreen{
height: calc(100svh - 64px);
}
</style>

View File

@ -0,0 +1,332 @@
<script lang="ts">
import { onMount } from 'svelte';
// Types
interface Traveler {
id: string;
name: string;
avatar: string;
location: string;
interests: string[];
currentMission?: string;
impactPoints: number;
}
interface Serendipity {
id: string;
type: 'local_tip' | 'chance_meetup' | 'hidden_gem' | 'help_needed';
title: string;
description: string;
location: string;
creator: Traveler;
timePosted: Date;
}
interface JourneyCircle {
id: string;
name: string;
theme: string;
members: Traveler[];
nextDestination?: string;
impactGoal: string;
}
// State
let activeTab: 'magic' | 'allies' | 'impact' = 'magic';
let globeInstance: any;
let globeContainer: HTMLElement;
// Dummy data
const nearbyTravelers: Traveler[] = [
{
id: '1',
name: 'Sofia Chen',
avatar:
'https://w7.pngwing.com/pngs/48/259/png-transparent-profile-man-male-photo-face-portrait-illustration-vector-people-blue.png',
location: 'Currently in: Kyoto, Japan',
interests: ['Cultural Preservation', 'Street Photography', 'Local Markets'],
currentMission: 'Documenting traditional craftspeople',
impactPoints: 2840
},
{
id: '2',
name: 'Marco Rivera',
avatar:
'https://w7.pngwing.com/pngs/48/259/png-transparent-profile-man-male-photo-face-portrait-illustration-vector-people-blue.png',
location: 'Currently in: Kyoto, Japan',
interests: ['Sustainable Travel', 'Language Exchange', 'Hidden Cafes'],
currentMission: 'Teaching free English classes',
impactPoints: 1920
}
];
const serendipityFeeds: Serendipity[] = [
{
id: '1',
type: 'hidden_gem',
title: 'Secret Bamboo Grove Meditation',
description:
'Found a local monk who offers sunrise meditation sessions in a hidden bamboo grove. Space for 2 more people tomorrow!',
location: 'Arashiyama, Kyoto',
creator: nearbyTravelers[0],
timePosted: new Date()
},
{
id: '2',
type: 'help_needed',
title: 'Translation Help at Local Market',
description:
'Elderly shop owner needs help translating her traditional recipe collection. Perfect for Japanese speakers!',
location: 'Nishiki Market, Kyoto',
creator: nearbyTravelers[1],
timePosted: new Date()
}
];
const journeyCircles: JourneyCircle[] = [
{
id: '1',
name: 'Sustainable Storytellers',
theme: 'Documenting Environmental Initiatives',
members: [nearbyTravelers[0]],
nextDestination: 'Rural Hokkaido',
impactGoal: 'Create a photo series of local sustainable farming practices'
},
{
id: '2',
name: 'Culture Bridges',
theme: 'Connecting Communities Through Art',
members: [nearbyTravelers[1]],
nextDestination: 'Osaka',
impactGoal: 'Organize a collaborative mural project with local artists'
}
];
</script>
<div class="h-full overflow-scroll ">
<main class="container mx-auto px-4 py-6">
<!-- Navigation Tabs -->
<div class="tabs-boxed tabs my-4 justify-center bg-transparent">
{#each [{ id: 'magic', icon: '🔮', label: 'Magic' }, { id: 'allies', icon: '👥', label: 'Friends<3' }, { id: 'impact', icon: '🏆', label: 'Impact' }] as tab}
<button
class="tab-lg content-center tab p-10 gap-2 text-sm font-bold"
class:tab-active={activeTab === tab.id}
on:click={() => (activeTab = tab.id as any)}
>
<span class="text-lg">{tab.icon}</span>
{tab.label}
</button>
{/each}
</div>
{#if activeTab === 'magic'}
<div class="grid gap-6 md:grid-cols-2">
<!-- Serendipity Feed -->
<div class="space-y-4">
<h2 class="mb-4 text-xl font-bold">Today's Magic Opportunities ✨</h2>
{#each serendipityFeeds as feed}
<div class="card bg-base-100 shadow-sm transition-all hover:shadow-md">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="avatar placeholder">
<div class="w-12 rounded-full bg-primary text-primary-content">
{feed.type === 'hidden_gem' ? '💎' : '🛎️'}
</div>
</div>
<div class="flex-1">
<div class="mb-2 flex items-start justify-between">
<h3 class="card-title text-base">{feed.title}</h3>
<span class="text-xs opacity-50">
{feed.timePosted.toLocaleTimeString()}
</span>
</div>
<p class="mb-4 text-sm opacity-75">{feed.description}</p>
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="avatar">
<a href="/profile">
<div class="w-8 rounded-full">
<img src={feed.creator.avatar} alt={feed.creator.name} />
</div>
</a>
</div>
<a href="/profile">
<span class="text-sm font-medium">{feed.creator.name}</span>
</a>
</div>
<button class="btn btn-primary btn-sm">
Join Now <span class="ml-1"></span>
</button>
</div>
</div>
</div>
</div>
</div>
{/each}
</div>
<!-- Globe Section -->
<div class="card sticky top-4 bg-base-100 shadow-sm">
</div>
</div>
{:else if activeTab === 'allies'}
<!-- Allies Section -->
<div class="grid gap-6 md:grid-cols-2">
<div class="space-y-6">
<h2 class="text-xl font-bold">Potential Allies ({nearbyTravelers.length})</h2>
{#each nearbyTravelers as traveler}
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="avatar">
<div class="w-12 rounded-xl" >
<img src={traveler.avatar} alt={traveler.name} />
</div>
</div>
<div class="flex-1">
<div class="mb-2 flex items-start justify-between">
<h3 class="card-title text-base">{traveler.name}</h3>
<div class="badge badge-primary gap-1">
{traveler.impactPoints}
</div>
</div>
<p class="mb-3 text-sm opacity-75">{traveler.location}</p>
<div class="mb-4 flex flex-wrap gap-2">
{#each traveler.interests as interest}
<div class="badge badge-outline">#{interest}</div>
{/each}
</div>
<button class="btn btn-primary btn-sm btn-block"> Connect 🤝 </button>
</div>
</div>
</div>
</div>
{/each}
</div>
<!-- Journey Circles -->
<div class="space-y-6">
<div class="flex items-center justify-between">
<h2 class="text-xl font-bold">Featured Journey Circles</h2>
<button class="btn btn-link btn-sm">
See all <span class="ml-1"></span>
</button>
</div>
{#each journeyCircles as circle}
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-start gap-4">
<div class="avatar placeholder">
<div
class="w-12 rounded-xl bg-gradient-to-br from-primary to-secondary text-primary-content"
>
<span>🌟</span>
</div>
</div>
<div class="flex-1">
<h3 class="card-title text-base">{circle.name}</h3>
<p class="mb-3 text-sm opacity-75">{circle.theme}</p>
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-2">
<span class="text-primary">▶︎ Next:</span>
<span class="font-medium">{circle.nextDestination}</span>
</div>
<div class="badge badge-ghost">
{circle.members.length} members
</div>
</div>
<div class="mb-4 flex items-center gap-2">
<progress class="progress progress-primary w-full" value="65" max="100"
></progress>
<span class="text-sm font-bold text-primary">65%</span>
</div>
<button class="btn btn-primary btn-sm btn-block">
Join Circle <span class="ml-1"></span>
</button>
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<!-- Impact Section -->
<div class="grid gap-6 md:grid-cols-2">
<div class="space-y-6">
<h2 class="text-xl font-bold">Global Impact Dashboard</h2>
<div class="grid grid-cols-2 gap-4">
<div class="card bg-primary text-primary-content">
<div class="card-body p-4">
<div class="text-2xl font-bold">12,847</div>
<div class="text-sm">Kind Acts</div>
<progress class="progress progress-info mt-2" value="82" max="100"></progress>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body p-4">
<div class="text-2xl font-bold text-primary">47k+</div>
<div class="text-sm opacity-75">Lives Touched</div>
<div class="badge badge-success badge-sm mt-2 gap-1">▲ 12% from last month</div>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body p-4">
<div class="text-2xl font-bold text-primary">284</div>
<div class="text-sm opacity-75">Projects</div>
<div class="mt-2 flex gap-1">
<div class="h-2 w-4 rounded-full bg-primary"></div>
<div class="h-2 w-4 rounded-full bg-primary/30"></div>
<div class="h-2 w-4 rounded-full bg-primary/30"></div>
</div>
</div>
</div>
<div class="card bg-base-100 shadow-sm">
<div class="card-body p-4">
<div class="text-2xl font-bold text-primary">89%</div>
<div class="text-sm opacity-75">Carbon Offset</div>
<div class="badge badge-ghost mt-2 gap-1">🌳 12k trees planted</div>
</div>
</div>
</div>
</div>
<div class="space-y-6">
<h2 class="text-xl font-bold">Your Impact Journey</h2>
<div class="card bg-base-100 shadow-sm">
<div class="card-body">
<div class="flex items-center gap-4">
<div class="avatar placeholder">
<div class="w-12 rounded-lg bg-primary text-primary-content">🏅</div>
</div>
<div class="flex-1">
<div class="flex items-center justify-between">
<div>
<div class="font-bold">Global Citizen</div>
<div class="text-sm opacity-75">Level 24 Explorer</div>
</div>
<div class="text-right">
<div class="text-sm opacity-75">Next Level</div>
<div class="text-lg font-bold text-primary">+1,240 XP</div>
</div>
</div>
<progress class="progress progress-primary mt-4" value="65" max="100"></progress>
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
</main>
</div>
<style>
:global(body) {
--rounded-box: 1rem;
--rounded-btn: 0.5rem;
--rounded-badge: 1.25rem;
}
</style>

View File

@ -0,0 +1,203 @@
<script lang="ts">
import { onMount, onDestroy, tick } from 'svelte';
import { writable, derived } from 'svelte/store';
interface User {
id: string;
name: string;
avatar: string;
lastMessage: string;
unread: number;
online: boolean;
}
interface Message {
id: string;
text: string;
senderId: string;
timestamp: string;
}
const isMobile = writable(false);
let selectedUserId: string | null = null;
$: showUserList = derived([isMobile], ([$isMobile]) => !$isMobile || !selectedUserId);
const users: User[] = [
{ id: '1', name: 'John Doe', avatar: 'https://i.pravatar.cc/40?img=1', lastMessage: 'Hey, how are you?', unread: 2, online: true },
{ id: '2', name: 'Jane Smith', avatar: 'https://i.pravatar.cc/40?img=2', lastMessage: 'See you tomorrow!', unread: 0, online: false },
];
function checkMobile() {
isMobile.set(window.innerWidth < 768);
}
function selectUser(userId: string) {
selectedUserId = userId;
}
function goBack() {
selectedUserId = null;
}
onMount(() => {
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
});
let newMessage = '';
let messageContainer: HTMLDivElement;
let messages: Message[] = [];
$: messages = getMessages(selectedUserId);
function getMessages(userId: string | null): Message[] {
if (!userId) return [];
return userId === '1'
? [
{ id: '1', text: 'Hey, how are you?', senderId: '1', timestamp: '10:30 AM' },
{ id: '2', text: "I'm good, thanks!", senderId: 'me', timestamp: '10:31 AM' },
]
: [
{ id: '3', text: 'Ready for tomorrow?', senderId: '2', timestamp: '2:45 PM' },
{ id: '4', text: 'Yes, see you then!', senderId: 'me', timestamp: '2:46 PM' },
];
}
async function sendMessage() {
if (newMessage.trim()) {
messages = [
...messages,
{
id: Date.now().toString(),
text: newMessage,
senderId: 'me',
timestamp: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
},
];
newMessage = '';
await tick();
messageContainer.scrollTo({ top: messageContainer.scrollHeight, behavior: 'smooth' });
}
}
</script>
<style>
/* Smooth transition for mobile menu */
.menu-transition {
transition: transform 0.3s ease-in-out;
}
/* Increase button touch areas */
button {
min-height: 44px;
}
/* Handle text overflow */
.truncate {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
<!-- Main Layout -->
<div class="flex flex-col md:flex-row h-full ">
<!-- Mobile Header -->
<div class="md:hidden navbar bg-base-100 border-b border-base-300 px-4">
<div class="flex items-center w-full">
{#if selectedUserId}
<button on:click={goBack} class="btn btn-ghost btn-circle" aria-label="Back to conversations">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" />
</svg>
</button>
<div class="flex items-center flex-wrap gap-2">
<div class="w-10 rounded-full relative">
<img class=" rounded-lg" src={users.find(u => u.id === selectedUserId)?.avatar} alt={users.find(u => u.id === selectedUserId)?.name} />
{#if users.find(u => u.id === selectedUserId)?.online}
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-base-100 z-10"></div>
{/if}
</div>
<h1 class="max-h-fit text-lg font-semibold truncate flex-1 text-center max-w-fit">
{users.find(u => u.id === selectedUserId)?.name}
</h1>
</div>
{:else}
<h1 class="text-lg font-semibold flex-1 text-center">Messages</h1>
{/if}
</div>
</div>
<!-- User List -->
<div class={`w-full md:w-80 bg-base-100 border-t border-r rounded-tr-lg border-base-300 fixed md:relative h-full menu-transition ${$showUserList ? 'translate-x-0' : '-translate-x-full'}`}>
<div class="hidden md:block p-4 bg-neutral-50">
<h1 class="text-xl font-bold">Direct Messages</h1>
</div>
<div class="menu p-2 overflow-y-auto h-[calc(100vh-4rem)]">
{#each users as user (user.id)}
<button
class="flex items-center gap-3 p-3 hover:bg-base-200 rounded-lg {selectedUserId === user.id ? 'bg-base-200' : ''}"
on:click={() => selectUser(user.id)}
>
<div class="avatar online">
<div class="w-10 rounded-full relative">
<img src={user.avatar} alt={user.name} />
{#if user.online}
<div class="absolute bottom-0 right-0 w-3 h-3 bg-green-500 rounded-full border-2 border-base-100 z-10"></div>
{/if}
</div>
</div>
<div class="flex-1 text-left min-w-0">
<div class="font-semibold truncate">{user.name}</div>
<div class="text-sm text-gray-500 truncate">{user.lastMessage}</div>
</div>
{#if user.unread > 0}
<div class="badge badge-primary">{user.unread}</div>
{/if}
</button>
{/each}
</div>
</div>
<!-- Conversation Area -->
<div class="flex-1 h-[calc(100vh-4rem)]">
{#if selectedUserId}
<div class="h-full flex flex-col">
<div class="flex-1 p-4 space-y-4 overflow-y-auto" bind:this={messageContainer}>
{#each messages as message (message.id)}
<div class="flex {message.senderId === 'me' ? 'justify-end' : 'justify-start'}">
<div class="max-w-[90%] px-3 py-2 rounded-lg {message.senderId === 'me' ? 'bg-primary text-primary-content' : 'bg-base-300'}">
<div class="break-words">{message.text}</div>
<div class="text-[0.7rem] mt-1 opacity-50">{message.timestamp}</div>
</div>
</div>
{/each}
</div>
<div class="p-4 border-t border-base-300 bg-base-100">
<div class="flex gap-2">
<input
type="text"
bind:value={newMessage}
placeholder="Type a message..."
class="flex-1 input input-bordered input-sm"
on:keydown={(e) => e.key === 'Enter' && sendMessage()}
/>
<button on:click={sendMessage} class="btn btn-primary btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
</svg>
</button>
</div>
</div>
</div>
{:else}
<div class="hidden md:flex items-center justify-center h-full">
<div class="text-center text-gray-500">
<div class="text-2xl">Select a conversation</div>
<div class="text-sm">Start chatting with your friends</div>
</div>
</div>
{/if}
</div>
</div>

View File

@ -0,0 +1,112 @@
<script>
// @ts-nocheck
import { goto } from "$app/navigation";
// Fleets data
const fleets = [
{ user: 'Swipe', handle: '@user1', avatar: '😊' , url: '/home/swipe'},
{ user: 'Dinner', handle: '@user2', avatar: '😎', url: '/home/dinner'},
{ user: 'Chat', handle: '@user3', avatar: '🤩', url: '/home/chat'},
{ user: 'Plan', handle: '@user4', avatar: '🥳', url: '/home/plan'},
];
// Tweets data
const tweets = [
{
user: 'SvelteFan',
handle: '@svelte_dev',
avatar: '🚀',
content: "Just discovered Svelte and it's amazing! #webdev",
likes: 42,
retweets: 12,
time: '2h'
},
{
user: 'WebDev',
handle: '@webdev',
avatar: '💻',
content: 'CSS grid is a game changer for layouts. Change my mind.',
likes: 89,
retweets: 24,
time: '4h'
},
];
</script>
<div class="container mx-auto p-4 max-w-2xl">
<!-- Fleets Section -->
<div class="flex space-x-4 pb-4 overflow-x-auto scrollbar-hide">
{#each fleets as fleet}
<div class="flex flex-col items-center space-y-2 flex-shrink-0">
<div class="p-1 rounded-full bg-gradient-to-tr from-primary to-secondary hover:from-secondary hover:to-primary">
<button on:click={goto(fleet.url)} class="avatar bg-base-200 rounded-full w-16 h-16 flex items-center justify-center text-2xl hover:opacity-90">
{fleet.avatar}
</button>
</div>
<span class="text-sm font-medium">{fleet.user}</span>
</div>
{/each}
</div>
<!-- Tweets Section -->
<div class="space-y-4">
{#each tweets as tweet}
<div class="bg-white rounded-xl shadow-sm p-4">
<div class="flex gap-3">
<!-- Avatar -->
<div class="avatar bg-base-200 rounded-full w-12 h-12 flex items-center justify-center text-xl">
{tweet.avatar}
</div>
<!-- Tweet Content -->
<div class="flex-1">
<div class="flex items-center gap-2">
<span class="font-bold">{tweet.user}</span>
<span class="text-gray-600">{tweet.handle}</span>
<span class="text-gray-600">· {tweet.time}</span>
</div>
<p class="mt-2">{tweet.content}</p>
<!-- Tweet Actions -->
<div class="flex justify-between mt-4 text-gray-600 max-w-md">
<button class="hover:text-primary flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
{tweet.retweets}
</button>
<button class="hover:text-green-500 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
</svg>
{tweet.retweets}
</button>
<button class="hover:text-red-500 flex items-center gap-1">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
{tweet.likes}
</button>
</div>
</div>
</div>
</div>
{/each}
</div>
</div>
<style global>
/* Custom scrollbar hide */
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 658 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

View File

@ -0,0 +1,78 @@
<script lang="ts">
let messages: any = [];
let newMessage = '';
let selectedLanguage = 'en';
const languages = [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'de', label: 'Deutsch' },
{ value: 'ja', label: '日本語' }
];
function addMessage() {
if (!newMessage.trim()) return;
// Add user message
messages = [...messages, {
id: Date.now(),
content: newMessage,
isUser: true,
timestamp: new Date().toLocaleTimeString()
}];
// Simulate bot response
setTimeout(() => {
messages = [...messages, {
id: Date.now() + 1,
content: `Translated to ${selectedLanguage}: [${newMessage}]`,
isUser: false,
timestamp: new Date().toLocaleTimeString()
}];
}, 1000);
newMessage = '';
}
</script>
<div class="flex flex-col h-full overflow-scroll bg-base-200">
<!-- Language Selector -->
<div class="flex justify-end p-4 bg-base-100 shadow-sm">
<select
bind:value={selectedLanguage}
class="select select-primary select-sm w-full max-w-xs"
>
{#each languages as lang}
<option value={lang.value}>{lang.label}</option>
{/each}
</select>
</div>
<!-- Chat Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
{#each messages as message}
<div class="chat {message.isUser ? 'chat-end' : 'chat-start'}">
<div class="chat-bubble {message.isUser ? 'chat-bubble-primary' : 'chat-bubble-secondary'}">
{message.content}
<div class="text-xs opacity-50 mt-1">{message.timestamp}</div>
</div>
</div>
{/each}
</div>
<!-- Message Input -->
<div class="p-4 bg-base-100 border-t">
<form on:submit|preventDefault={addMessage} class="flex gap-2">
<input
type="text"
bind:value={newMessage}
placeholder="Type a message..."
class="input input-bordered flex-1"
/>
<button type="submit" class="btn btn-primary">
Send
</button>
</form>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

View File

@ -0,0 +1,113 @@
<script>
let currentCity = "Lisbon"; // Dynamic location
const expeditions = [
{
name: "Tram 28 Tascas Crawl",
seats: 3,
cuisine: "Petiscos",
highlights: ["Sardines", "Green Wine", "Local Artists"],
meeting: "Praça Luís de Camões, 7pm"
},
{
name: "Alfama Family Feast",
seats: 5,
cuisine: "Seafood",
highlights: ["Cataplana Stew", "Fado Music", "Viewpoint"],
meeting: "Miradouro Santa Luzia, 7:30pm"
}
];
</script>
<div class="h-full overflow-scroll bg-base-100 font-[Inter]">
<!-- Header -->
<!-- Local Context Bar -->
<div class="bg-primary/10 py-3 px-4 flex items-center gap-4">
<div class="flex-1">
<div class="text-sm opacity-75">Currently Exploring</div>
<div class="text-xl font-bold text-primary">{currentCity}</div>
</div>
<div class="text-4xl">🍴</div>
</div>
<!-- Expedition Cards -->
<main class="p-4 space-y-6">
{#each expeditions as expedition}
<div class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body">
<!-- Expedition Header -->
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-lg">{expedition.name}</h2>
<div class="badge badge-outline badge-sm mt-1">
{expedition.cuisine}
</div>
</div>
<div class="text-2xl">
{#if expedition.seats > 3}
🍽️
{:else}
🔥
{/if}
</div>
</div>
<!-- Cultural Highlights -->
<div class="my-4 space-y-2">
{#each expedition.highlights as item}
<div class="flex items-center gap-2 text-sm">
<div class="text-primary"></div>
<span>{item}</span>
</div>
{/each}
</div>
<!-- Group Status -->
<div class="flex items-center justify-between border-t pt-4">
<div class="flex -space-x-3">
{#each Array(expedition.seats) as _, i}
<div class="avatar placeholder">
<div class="w-8 h-8 bg-neutral text-neutral-content">
{i === 0 ? '👤' : '?'}
</div>
</div>
{/each}
</div>
<button class="btn btn-primary btn-sm">
Claim Seat ({expedition.seats} left)
</button>
</div>
<!-- Meeting Details -->
<div class="mt-4 text-xs opacity-75 flex items-center gap-2">
<span class="flex-1">📍 {expedition.meeting}</span>
<span class="badge badge-ghost badge-sm">Local Guide: Maria</span>
</div>
</div>
</div>
{/each}
</main>
<!-- Cultural Footprint -->
<div class="p-4 bg-base-200 mt-8">
<div class="max-w-md mx-auto text-center space-y-4">
<div class="text-3xl">🌐</div>
<h3 class="font-bold">Your Lisbon Journey</h3>
<div class="flex justify-center gap-4 text-sm">
<div class="space-y-1">
<div class="text-primary font-bold">5</div>
<div class="opacity-75">New Allies</div>
</div>
<div class="space-y-1">
<div class="text-primary font-bold">9</div>
<div class="opacity-75">Local Dishes</div>
</div>
<div class="space-y-1">
<div class="text-primary font-bold">3</div>
<div class="opacity-75">Neighborhoods</div>
</div>
</div>
</div>
</div>
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -0,0 +1,220 @@
<script lang="ts">
import { fade } from 'svelte/transition';
let currentTab = 'pack';
let xp = 420;
let streak = 8;
let packedItems: any = [];
let newItem = '';
let destinations: any = [];
let newDestination = '';
let expenses: any = [];
let newExpense = '';
// Duolingo-style achievement system
const achievements = [
{ name: 'Packing Pro', earned: true },
{ name: 'Globetrotter', earned: false },
{ name: 'Budget Master', earned: false }
];
function addItem() {
if (newItem) {
packedItems = [...packedItems, {
name: newItem,
packed: false,
xp: 10,
category: ['🧳 Clothing', '📱 Tech', '🧼 Toiletry'][Math.floor(Math.random() * 3)]
}];
newItem = '';
}
}
function togglePacked(item: any) {
packedItems = packedItems.map((i: { packed: any; }) =>
i === item ? {...i, packed: !i.packed} : i
);
if (!item.packed) xp += item.xp;
}
function addDestination() {
destinations = [...destinations, {
name: newDestination,
date: new Date().toISOString().split('T')[0],
emoji: '🌍'
}];
newDestination = '';
}
function addExpense() {
expenses = [...expenses, {
description: newExpense,
amount: Math.floor(Math.random() * 100),
paidBy: 'You',
splitWith: ['Travel Buddy 1', 'Travel Buddy 2']
}];
newExpense = '';
}
</script>
<svelte:head>
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.2/dist/full.css" rel="stylesheet">
</svelte:head>
<div class="min-h-screen bg-base-200 p-8 font-[Poppins]">
<!-- XP & Streak Header -->
<div class="flex justify-between items-center mb-8">
<div class="stats shadow bg-primary text-primary-content">
<div class="stat">
<div class="stat-title">Travel XP</div>
<div class="stat-value">{xp}</div>
<div class="stat-desc">Level {Math.floor(xp/100)}</div>
</div>
</div>
<div class="indicator">
<span class="indicator-item badge badge-secondary">{streak}🔥</span>
<div class="grid w-32 h-20 bg-base-100 place-items-center rounded-box">
Streak!
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="tabs tabs-boxed bg-accent mb-8">
<button
class:tab-active={currentTab === 'pack'}
on:click={() => currentTab = 'pack'}
class="tab text-lg"
>
🎒 Pack
</button>
<button
class:tab-active={currentTab === 'plan'}
on:click={() => currentTab = 'plan'}
class="tab text-lg"
>
🗺 Plan
</button>
<button
class:tab-active={currentTab === 'split'}
on:click={() => currentTab = 'split'}
class="tab text-lg"
>
💸 Split
</button>
</div>
<!-- Packing List Section -->
{#if currentTab === 'pack'}
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newItem}
class="join-item input input-bordered w-full"
placeholder="Add item (e.g. passport)"
/>
<button on:click={addItem} class="join-item btn btn-primary">
<span class="text-2xl">+</span>
</button>
</div>
{#each packedItems as item (item.name)}
<div transition:fade class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<div class="flex items-center gap-4">
<input
type="checkbox"
checked={item.packed}
on:change={() => togglePacked(item)}
class="checkbox checkbox-primary checkbox-lg"
/>
<div class="flex-1">
<h3 class="text-xl font-bold">{item.name}</h3>
<div class="badge badge-outline">{item.category}</div>
</div>
<div class="text-success">+{item.xp}XP</div>
</div>
</div>
</div>
{/each}
</div>
{:else if currentTab === 'plan'}
<!-- Trip Planning Section -->
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newDestination}
class="join-item input input-bordered w-full"
placeholder="Add destination (e.g. Paris)"
/>
<button on:click={addDestination} class="join-item btn btn-secondary">
<span class="text-2xl">+</span>
</button>
</div>
<div class="carousel rounded-box gap-4">
{#each destinations as destination (destination.name)}
<div class="carousel-item">
<div class="card bg-base-100 shadow-xl w-64">
<div class="card-body items-center text-center">
<div class="text-6xl mb-2">{destination.emoji}</div>
<h2 class="card-title">{destination.name}</h2>
<p>{destination.date}</p>
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<!-- Expense Splitting Section -->
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newExpense}
class="join-item input input-bordered w-full"
placeholder="Add expense (e.g. Hotel)"
/>
<button on:click={addExpense} class="join-item btn btn-accent">
<span class="text-2xl">+</span>
</button>
</div>
{#each expenses as expense (expense.description)}
<div class="collapse collapse-arrow bg-base-100">
<input type="radio" name="expenses" />
<div class="collapse-title text-xl font-medium">
💰 {expense.description} - ${expense.amount}
</div>
<div class="collapse-content">
<p>Paid by: {expense.paidBy}</p>
<div class="divider"></div>
<div class="flex gap-2 flex-wrap">
{#each expense.splitWith as person}
<div class="badge badge-lg badge-outline">
👤 {person}
</div>
{/each}
</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- Achievements Floating Button -->
<div class="fixed bottom-4 right-4">
<div class="dropdown dropdown-top dropdown-end">
<div tabindex="0" role="button" class="btn btn-circle btn-primary text-2xl">
🏆
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64">
{#each achievements as achievement}
<li class={achievement.earned ? 'text-success' : 'text-base-content/50'}>
<a>{achievement.name}</a>
</li>
{/each}
</ul>
</div>
</div>
</div>

View File

@ -0,0 +1,308 @@
<script lang="ts">
import { profiles } from '$lib/profiles';
import { flip } from 'svelte/animate';
import { quintOut } from 'svelte/easing';
import { spring } from 'svelte/motion';
interface Profile {
id: string;
name: string;
interests: string[];
moodImages?: string[];
location?: {
x: number;
y: number;
};
}
// Use proper typing for the stack
let currentIndex = 0;
let stack: Profile[] = profiles;
let connections: Profile[] = [];
let dragging: Profile | null = null;
let dragStartOffset = { x: 0, y: 0 };
// Add proper typing to spring
let dragCoords = spring<{x: number, y: number}>({ x: 0, y: 0 }, {
stiffness: 0.2,
damping: 0.7
});
// Add proper return type
const calculateConnectionPath = (connection: Profile): string => {
const start = connection.location || { x: 500, y: 250 };
// Store window dimensions to avoid repeated access
const windowWidth = window.innerWidth;
const windowHeight = window.innerHeight;
const end = { x: windowWidth - 100, y: windowHeight - 100 };
const controlPoint = {
x: (start.x + end.x) / 2,
y: Math.min(start.y, end.y) - 100
};
return `M ${start.x},${start.y} Q ${controlPoint.x},${controlPoint.y} ${end.x},${end.y}`;
};
// Type the action parameter properly
const handleAction = (action: 'skip' | 'decide' | 'add'): void => {
if (stack.length === 0) return;
const profile = stack[currentIndex];
if (action === 'add') {
const draggedLocation = { x: $dragCoords.x, y: $dragCoords.y };
connections = [...connections, { ...profile, location: draggedLocation }];
const suitcase = document.querySelector('#suitcase');
const profileCard = document.querySelector(`#profile-${profile.id}`);
if (suitcase && profileCard) {
animateProfileToSuitcase(profileCard, suitcase);
}
}
// Remove the current profile and update the stack
stack = stack.filter((_, i) => i !== currentIndex);
currentIndex = Math.min(currentIndex, stack.length - 1);
};
// Extract animation logic to a separate function
const animateProfileToSuitcase = (profileCard: Element, suitcase: Element): void => {
const clone = profileCard.cloneNode(true) as HTMLElement;
document.body.appendChild(clone);
const start = profileCard.getBoundingClientRect();
const end = suitcase.getBoundingClientRect();
clone.style.position = 'fixed';
clone.style.left = `${start.left}px`;
clone.style.top = `${start.top}px`;
clone.style.width = `${start.width}px`;
clone.style.transition = 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)';
clone.style.zIndex = '9999';
requestAnimationFrame(() => {
clone.style.transform = `translate(${end.left - start.left}px, ${end.top - start.top}px) scale(0.1) rotate(360deg)`;
clone.style.opacity = '0';
});
setTimeout(() => clone.remove(), 500);
};
const startDrag = (event: MouseEvent, profile: Profile): void => {
const element = event.currentTarget as HTMLElement;
const rect = element.getBoundingClientRect();
dragStartOffset = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
dragging = profile;
dragCoords.set({
x: event.clientX - dragStartOffset.x,
y: event.clientY - dragStartOffset.y
});
document.body.style.cursor = 'grabbing';
};
function handleMouseMove(event: MouseEvent): void {
if (dragging) {
dragCoords.set({
x: event.clientX - dragStartOffset.x,
y: event.clientY - dragStartOffset.y
});
}
}
function handleMouseUp(): void {
if (dragging) {
const suitcase = document.querySelector('#suitcase');
if (suitcase) {
const suitcaseRect = suitcase.getBoundingClientRect();
const dragRect = document.querySelector(`#profile-${dragging.id}`)?.getBoundingClientRect();
if (dragRect && isOverlapping(dragRect, suitcaseRect)) {
handleAction('add');
}
}
dragging = null;
document.body.style.cursor = '';
}
}
function isOverlapping(rect1: DOMRect, rect2: DOMRect): boolean {
return !(rect1.right < rect2.left ||
rect1.left > rect2.right ||
rect1.bottom < rect2.top ||
rect1.top > rect2.bottom);
}
function handleKeyDown(event: KeyboardEvent, profile: Profile): void {
if (event.key === 'Enter' || event.key === ' ') {
const element = event.currentTarget as HTMLElement;
const rect = element.getBoundingClientRect();
startDrag(new MouseEvent('mousedown', {
clientX: rect.left + rect.width / 2,
clientY: rect.top + rect.height / 2
}), profile);
}
}
// Add cleanup for window event listeners
import { onDestroy } from 'svelte';
onDestroy(() => {
document.body.style.cursor = '';
});
</script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
<div class="h-full font-handwritten relative overflow-hidden">
<!-- Interactive World Map Background -->
<div class="absolute inset-0 opacity-25 z-0 pointer-events-none">
<svg viewBox="0 0 1000 500" class="w-full h-full" role="presentation">
{#each connections as connection (connection.id)}
<path d={calculateConnectionPath(connection)}
class="stroke-pink-400/40 stroke-2 animate-dash"
fill="none" />
{/each}
</svg>
</div>
<!-- Suitcase Collector -->
<div id="suitcase"
role="button"
tabindex="0"
aria-label="Collected connections: {connections.length}"
class="fixed bottom-8 right-8 w-24 h-16 bg-amber-500/80 rotate-12 shadow-xl rounded-lg z-30
transition-all hover:rotate-6 hover:scale-110 hover:shadow-2xl
{dragging ? 'scale-125 ring-4 ring-green-400 ring-opacity-50' : ''}"
style="transform-origin: center;">
<span class="absolute -top-2 -left-2 text-4xl" aria-hidden="true">🧳</span>
<span class="absolute bottom-1 right-1 text-sm font-bold text-white" aria-hidden="true">
{connections.length}
</span>
</div>
<!-- Profile Stack -->
<div class="relative h-screen flex items-center justify-center">
{#each stack as profile, i (profile.id)}
<div id={`profile-${profile.id}`}
role="button"
tabindex="0"
aria-label="Profile card for {profile.name}"
class="absolute cursor-grab active:cursor-grabbing transition-transform duration-200
{dragging === profile ? 'z-50' : ''}"
style={`transform: ${dragging !== profile ?
`rotate(${Math.random() * 8 - 4}deg)
translate(${Math.random() * 20 - 10}px, ${Math.random() * 20 - 10}px)` :
'none'};
z-index: ${1000 - i};
${dragging === profile ?
`position: fixed; left: ${$dragCoords.x}px; top: ${$dragCoords.y}px; transform: none;` :
''}`}
animate:flip={{ delay: i * 50, duration: 800, easing: quintOut }}
on:mousedown={(e) => startDrag(e, profile)}
on:keydown={(e) => handleKeyDown(e, profile)}>
<div class="bg-white p-6 rounded-lg shadow-xl border-4 border-double border-amber-700
transition-all hover:shadow-2xl hover:scale-105 origin-center
backdrop-blur-sm bg-opacity-90"
style="width: min(80vw, 400px);">
<h3 class="text-3xl mb-4 text-cyan-800">{profile.name}</h3>
<div class="flex flex-wrap gap-2 mb-4">
{#each profile.interests as interest (interest)}
<span class="px-3 py-1 bg-yellow-200/50 rounded-full
border-2 border-dashed border-amber-600/30">
{interest}
</span>
{/each}
</div>
<div class="h-48 bg-stone-50 rounded border-2 border-stone-200 mb-4 relative overflow-hidden">
{#if profile.moodImages}
{#each profile.moodImages as img, index (img)}
<img src={img}
alt={`Mood image ${index + 1} for ${profile.name}`}
class="absolute w-16 h-16 object-cover shadow-lg border-4 border-white"
style={`left: ${Math.random() * 60}%;
top: ${Math.random() * 60}%;
transform: rotate(${Math.random() * 24 - 12}deg);`} />
{/each}
{/if}
</div>
</div>
</div>
{/each}
</div>
<!-- Action Buttons -->
<div class="fixed bottom-8 left-8 flex gap-4 z-30">
<button on:click={() => handleAction('skip')}
aria-label="Skip profile"
class="h-24 w-24 bg-red-500/90 rotate-12 shadow-xl rounded-lg
flex items-center justify-center text-white text-4xl
hover:rotate-0 hover:scale-125 transition-all active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed"
disabled={stack.length === 0}>
<span class="rotate--12" aria-hidden="true"></span>
</button>
<button on:click={() => handleAction('decide')}
aria-label="Decide later"
class="h-24 w-24 bg-purple-500/90 -rotate-6 shadow-xl rounded-lg
flex items-center justify-center text-white text-4xl
hover:rotate-3 hover:scale-125 transition-all active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed"
disabled={stack.length === 0}>
<span class="rotate-6" aria-hidden="true"></span>
</button>
<button on:click={() => handleAction('add')}
aria-label="Add to connections"
class="h-24 w-24 bg-green-500/90 rotate-3 shadow-xl rounded-lg
flex items-center justify-center text-white text-4xl
hover:-rotate-12 hover:scale-125 transition-all active:scale-95
disabled:opacity-50 disabled:cursor-not-allowed"
disabled={stack.length === 0}>
<span class="rotate--3" aria-hidden="true"></span>
</button>
</div>
<!-- Dragging Preview -->
{#if dragging}
<div class="fixed pointer-events-none z-50"
role="status"
aria-live="polite"
style={`left: ${$dragCoords.x}px; top: ${$dragCoords.y}px;
transform: translate(-50%, -50%) rotate(${Math.random() * 25 - 12.5}deg);`}>
<div class="bg-white p-4 rounded shadow-xl border-2 border-dashed border-amber-600">
Taking {dragging.name} on adventure!
</div>
</div>
{/if}
</div>
<style>
@keyframes dash {
to {
stroke-dashoffset: -100;
}
}
.animate-dash {
stroke-dasharray: 10;
animation: dash 10s linear infinite;
}
.cursor-grab {
cursor: grab;
user-select: none;
}
.cursor-grabbing {
cursor: grabbing;
user-select: none;
}
</style>

View File

@ -0,0 +1,76 @@
<script>
import { BellIcon, CheckIcon, GlobeIcon, UserIcon } from 'lucide-svelte';
import { fly } from 'svelte/transition';
let notifications = [
{ type: 'alert', message: 'Lena accepted your Paris trip invite!', timestamp: '2m ago', read: false },
{ type: 'update', message: 'New local experience in Kyoto: Tea ceremony with master Takano', timestamp: '1h ago', read: false },
{ type: 'friend', message: 'Marc from your Barcelona hostel wants to connect', timestamp: '4h ago', read: true },
{ type: 'success', message: 'Your Bali itinerary is ready! 3 locals joined the plan', timestamp: '1d ago', read: true }
];
function markAllRead() {
notifications = notifications.map(n => ({ ...n, read: true }));
}
</script>
<div class="min-h-screen ">
<main class="max-w-2xl mx-auto p-4 space-y-6">
<div class="flex items-center justify-between mb-8">
<h1 class="text-3xl font-bold flex items-center gap-2">
<GlobeIcon class="w-8 h-8 text-accent" />
Journey Updates
</h1>
<button on:click={markAllRead} class="btn btn-sm btn-outline">Mark all as read</button>
</div>
{#if notifications.length === 0}
<div class="text-center p-8 rounded-xl bg-base-100">
<p class="text-lg text-gray-500">No new updates - your next adventure is waiting!</p>
</div>
{:else}
<div class="space-y-4">
{#each notifications as notification (notification.timestamp)}
<div
transition:fly={{ y: 20, duration: 300 }}
class:opacity-50={notification.read}
class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow cursor-pointer group"
>
<div class="card-body p-4">
<div class="flex items-start gap-4">
{#if notification.type === 'alert'}
<div class="text-warning">
<UserIcon class="w-6 h-6" />
</div>
{:else if notification.type === 'update'}
<div class="text-info">
<GlobeIcon class="w-6 h-6" />
</div>
{:else if notification.type === 'friend'}
<div class="text-secondary">
<UserIcon class="w-6 h-6" />
</div>
{:else}
<div class="text-success">
<CheckIcon class="w-6 h-6" />
</div>
{/if}
<div class="flex-1">
<p class="font-medium">{notification.message}</p>
<p class="text-sm text-gray-500">{notification.timestamp}</p>
</div>
{#if !notification.read}
<div class="w-2 h-2 bg-primary rounded-full self-center ml-4 animate-pulse" />
{/if}
</div>
</div>
</div>
{/each}
</div>
{/if}
</main>
</div>

View File

@ -0,0 +1,562 @@
<script lang="ts">
import { onMount } from 'svelte';
import { animate } from 'motion';
import {
Globe,
Users,
Plane,
Calendar,
Heart,
MessageCircle,
Instagram,
Twitter,
Youtube,
Flame,
Crown,
Languages,
Trophy,
Clock
,
ListMusic
,
Music
,
Play
,
RepeatIcon
,
Shuffle
} from 'lucide-svelte';
import { fade } from 'svelte/transition';
let floating = false;
let isFollowing = false;
const user = {
name: 'Adventure Alex',
bio: '🌍 Global Wanderer | Making friends one journey at a time',
location: 'Currently in Bali, Indonesia',
stats: [
{ label: 'Countries', value: 27, icon: Globe },
{ label: 'Friends', value: 143, icon: Users },
{ label: 'Trips', value: 89, icon: Plane }
],
upcomingTrips: [
{ id: 1, destination: 'Tokyo, Japan', date: 'March 15, 2024' },
{ id: 2, destination: 'Patagonia, Chile', date: 'April 20, 2024' }
],
socials: [
{ name: 'Instagram', handle: '@adventure_alex', url: '#', icon: Instagram },
{ name: 'Twitter', handle: '@wanderlust_alex', url: '#', icon: Twitter },
{ name: 'YouTube', handle: 'Adventure Alex', url: '#', icon: Youtube }
],
posts: [
{
id: 1,
title: 'Sunset in Uluwatu 🌅',
content: 'Chasing golden hours with amazing new friends! #BaliVibes',
likes: 142,
comments: 28,
date: '2024-02-20'
},
{
id: 2,
title: 'Mountain Trekking 🏔️',
content: 'Conquered Mount Batur at sunrise! Worth every step.',
likes: 89,
comments: 15,
date: '2024-02-18'
}
],
duolingo: {
streakDays: 145,
totalXP: 24680,
crown: 78,
languages: [
{
name: 'Spanish',
level: 'Advanced',
progress: 85,
xp: 12400,
streakWeeks: 12
},
{
name: 'Japanese',
level: 'Intermediate',
progress: 65,
xp: 8200,
streakWeeks: 8
},
{
name: 'French',
level: 'Beginner',
progress: 25,
xp: 4080,
streakWeeks: 4
}
]
},
spotify: {
topArtists: [
{ name: 'The Lumineers', genre: 'Folk Rock', imageUrl: '/api/placeholder/60/60' },
{ name: 'Coldplay', genre: 'Alternative Rock', imageUrl: '/api/placeholder/60/60' },
{ name: 'Ed Sheeran', genre: 'Pop', imageUrl: '/api/placeholder/60/60' }
],
recentlyPlayed: [
{
title: 'Ho Hey',
artist: 'The Lumineers',
album: 'The Lumineers',
duration: '2:43',
imageUrl: '/api/placeholder/50/50'
},
{
title: 'Viva La Vida',
artist: 'Coldplay',
album: 'Viva la Vida',
duration: '3:45',
imageUrl: '/api/placeholder/50/50'
},
{
title: 'Shape of You',
artist: 'Ed Sheeran',
album: '÷ (Divide)',
duration: '3:53',
imageUrl: '/api/placeholder/50/50'
}
],
playlists: [
{
name: 'Travel Vibes',
tracks: 45,
imageUrl: '/api/placeholder/80/80'
},
{
name: 'Workout Mix',
tracks: 32,
imageUrl: '/api/placeholder/80/80'
},
{
name: 'Chill Evening',
tracks: 28,
imageUrl: '/api/placeholder/80/80'
}
],
currentlyPlaying: {
title: 'Ho Hey',
artist: 'The Lumineers',
album: 'The Lumineers',
progress: 65,
imageUrl: '/api/placeholder/60/60'
}
}
};
const toggleFollow = () => {
isFollowing = !isFollowing;
};
onMount(() => {
const header = document.querySelector('.profile-header') as any;
const stats = document.querySelectorAll('.stat-card') as any;
const posts = document.querySelectorAll('.post-card') as any;
const trips = document.querySelectorAll('.trip-card') as any;
const plane = document.querySelector('.floating-plane') as any;
if (header) {
animate(header, { opacity: [0, 1], y: [-20, 0] }, {
duration: 0.8,
easing: 'easeOut'
} as any);
}
stats.forEach((stat: any, index: any) => {
animate(stat, { opacity: [0, 1], scale: [0.8, 1] }, {
delay: index * 0.1,
duration: 0.5,
easing: 'easeOut'
} as any);
});
posts.forEach((post: any, index: any) => {
animate(post, { opacity: [0, 1], y: [20, 0] }, {
delay: 0.3 + index * 0.1,
duration: 0.6,
easing: 'easeOut'
} as any);
});
trips.forEach((trip: any, index: any) => {
animate(trip, { opacity: [0, 1], x: [-20, 0] }, {
delay: 0.4 + index * 0.1,
duration: 0.7,
easing: 'easeOut'
} as any);
});
if (plane) {
setInterval(() => {
animate(plane, { y: floating ? -10 : 0 }, { duration: 1.5, easing: 'easeInOut' } as any);
floating = !floating;
}, 3000);
}
const languageCards = document.querySelectorAll('.language-card') as any;
languageCards.forEach((card: any, index: any) => {
animate(card, { opacity: [0, 1], x: [20, 0] }, {
delay: 0.5 + index * 0.1,
duration: 0.6,
easing: 'easeOut'
} as any);
});
const spotifyCards = document.querySelectorAll('.spotify-card') as any;
const trackItems = document.querySelectorAll('.track-item') as any;
spotifyCards.forEach((card: any, index: any) => {
animate(card, { opacity: [0, 1], y: [20, 0] }, {
delay: 0.3 + index * 0.1,
duration: 0.6,
easing: 'easeOut'
} as any);
});
trackItems.forEach((item: any, index: any) => {
animate(item, { opacity: [0, 1], x: [-20, 0] }, {
delay: 0.4 + index * 0.1,
duration: 0.5,
easing: 'easeOut'
} as any);
});
});
let isPlaying = false;
const togglePlay = () => {
isPlaying = !isPlaying;
};
const formatDate = (dateString: string) => {
const date = new Date(dateString);
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
};
</script>
<div class="h-full overflow-scroll py-8 mb-14">
<!-- Floating Airplane -->
<div class="floating-plane absolute right-10 top-20 text-blue-500">
<Plane size={48} class="-rotate-45 transform" />
</div>
<main class="mx-auto max-w-4xl overflow-hidden rounded-xl bg-white shadow-xl">
<!-- Profile Header -->
<section
class="profile-header relative bg-gradient-to-b from-blue-100 to-white p-8 text-center"
>
<div class="mx-auto h-32 w-32 overflow-hidden rounded-full border-4 border-blue-500">
<img
src="https://w7.pngwing.com/pngs/48/259/png-transparent-profile-man-male-photo-face-portrait-illustration-vector-people-blue.png"
alt="Adventure Alex"
class="h-full w-full object-cover"
/>
</div>
<h1 class="mt-4 text-2xl font-bold text-gray-900">{user.name}</h1>
<p class="max-h-fit text-sm text-gray-500">{user.socials[0].handle}</p>
<p class="text-sm text-gray-500">{user.location}</p>
<p class="text-gray-600">{user.bio}</p>
<!-- Follow Button -->
<button
on:click={toggleFollow}
class="btn btn-primary mt-4 transform rounded-full px-6 py-2 text-sm font-medium transition-all hover:scale-105 focus:outline-none
{isFollowing ? 'btn-secondary' : ''}"
>
{isFollowing ? 'Following' : 'Follow'}
</button>
</section>
<!-- Social Media Links -->
<div in:fade={{ duration: 400 }} class="flex justify-center gap-6 bg-blue-50 py-4">
{#each user.socials as social}
<a
href={social.url}
target="_blank"
rel="noopener noreferrer"
class="flex items-center gap-2 text-gray-600 transition-colors hover:text-blue-500"
aria-label={`Visit ${social.name} profile`}
>
<svelte:component this={social.icon} size={20} />
<span class="text-sm font-medium">{social.handle}</span>
</a>
{/each}
</div>
<!-- Stats -->
<section class="grid grid-cols-3 gap-4 p-8">
{#each user.stats as stat}
<div class="stat-card rounded-lg bg-white p-6 text-center shadow-md">
<svelte:component this={stat.icon} class="mb-2 text-blue-500" size={24} />
<h3 class="text-3xl font-bold text-gray-900">{stat.value}</h3>
<p class="text-sm text-gray-600">{stat.label}</p>
</div>
{/each}
</section>
<!-- Recent Posts -->
<section class="p-6">
<h2 class="mb-6 text-2xl font-bold text-gray-900">Recent Adventures</h2>
<div class="space-y-4">
{#each user.posts as post}
<div
class="post-card rounded-xl border border-gray-200 bg-white p-4 transition-colors hover:bg-gray-50"
>
<div class="flex gap-3">
<!-- User Avatar -->
<div class="flex-shrink-0">
<img
src="https://w7.pngwing.com/pngs/48/259/png-transparent-profile-man-male-photo-face-portrait-illustration-vector-people-blue.png"
alt="Adventure Alex"
class="h-12 w-12 rounded-full border-2 border-blue-200"
/>
</div>
<!-- Tweet Content -->
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 text-sm">
<span class="font-bold text-gray-900">{user.name}</span>
<span class="text-gray-500">
{user.socials.find((s) => s.name === 'Twitter')?.handle}
</span>
<span class="text-gray-400">·</span>
<time class="text-gray-500" datetime={post.date}>
{formatDate(post.date)}
</time>
</div>
<!-- Tweet Body -->
<p class="mt-2 leading-relaxed text-gray-900">
{post.content}
</p>
<!-- Tweet Actions -->
<div class="mt-3 flex gap-6 text-gray-500">
<button class="flex items-center gap-2 transition-colors hover:text-blue-500">
<MessageCircle size={18} />
<span class="text-sm">{post.comments}</span>
</button>
<button class="flex items-center gap-2 transition-colors hover:text-red-500">
<Heart size={18} />
<span class="text-sm">{post.likes}</span>
</button>
</div>
</div>
</div>
</div>
{/each}
</div>
</section>
<!-- Duolingo Section -->
<section class="bg-green-50 p-8">
<div class="mb-6 flex items-center gap-3">
<img
src="https://cdn.worldvectorlogo.com/logos/duolingo-symbol-icon.svg"
alt="Duolingo"
class="mx-4 h-12"
/>
<h2 class="text-2xl font-bold text-gray-900">Language Learning Journey</h2>
</div>
<!-- Stats Cards -->
<div class="mb-8 grid grid-cols-3 gap-4">
<div class="rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg">
<div class="flex items-center gap-3">
<Flame class="text-orange-500" size={24} />
<div>
<h3 class="text-3xl font-bold text-gray-900">{user.duolingo.streakDays}</h3>
<p class="text-sm text-gray-600">Day Streak</p>
</div>
</div>
</div>
<div class="rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg">
<div class="flex items-center gap-3">
<Trophy class="text-yellow-500" size={24} />
<div>
<h3 class="text-3xl font-bold text-gray-900">{user.duolingo.totalXP}</h3>
<p class="text-sm text-gray-600">Total XP</p>
</div>
</div>
</div>
<div class="rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg">
<div class="flex items-center gap-3">
<Crown class="text-purple-500" size={24} />
<div>
<h3 class="text-3xl font-bold text-gray-900">{user.duolingo.crown}</h3>
<p class="text-sm text-gray-600">Crown Level</p>
</div>
</div>
</div>
</div>
<!-- Language Progress Cards -->
<div class="space-y-4">
{#each user.duolingo.languages as language}
<div
class="language-card rounded-xl bg-white p-6 shadow-md transition-all hover:shadow-lg"
>
<div class="mb-4 flex items-center justify-between">
<div class="flex items-center gap-3">
<Languages class="text-green-500" size={24} />
<div>
<h3 class="text-xl font-bold text-gray-900">{language.name}</h3>
<p class="text-sm text-gray-600">{language.level}</p>
</div>
</div>
<div class="text-right">
<p class="text-lg font-bold text-green-500">{language.xp} XP</p>
<p class="text-sm text-gray-600">{language.streakWeeks} week streak</p>
</div>
</div>
<!-- Progress Bar -->
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200">
<div
class="h-full bg-green-500 transition-all duration-500"
style="width: {language.progress}%"
/>
</div>
</div>
{/each}
</div>
</section>
<!-- Spotify Section -->
<section class="bg-gradient-to-b from-green-900 to-black p-8 text-white">
<div class="mb-6 flex items-center gap-3">
<svg class="h-8 w-8" viewBox="0 0 24 24" fill="currentColor">
<path
d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"
/>
</svg>
<h2 class="text-2xl font-bold">Music Profile</h2>
</div>
<div class="grid gap-6 lg:grid-cols-2">
<!-- Top Artists -->
<div class="spotify-card rounded-xl bg-white bg-opacity-20 p-6">
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
<Music size={20} />
Top Artists
</h3>
<div class="space-y-4">
{#each user.spotify.topArtists as artist}
<div class="flex items-center gap-4">
<img src={artist.imageUrl} alt={artist.name} class="h-12 w-12 rounded-full" />
<div>
<h4 class="font-semibold">{artist.name}</h4>
<p class="text-sm text-gray-300">{artist.genre}</p>
</div>
</div>
{/each}
</div>
</div>
<!-- Recently Played -->
<div class="spotify-card rounded-xl bg-white bg-opacity-20 p-6">
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
<Clock size={20} />
Current Obsession
</h3>
<div class="space-y-4">
{#each user.spotify.recentlyPlayed as track}
<div class="track-item flex items-center gap-4">
<img src={track.imageUrl} alt={track.title} class="h-10 w-10 rounded" />
<div class="flex-1">
<h4 class="font-semibold">{track.title}</h4>
<p class="text-sm text-gray-300">{track.artist}</p>
</div>
<span class="text-sm text-gray-300">{track.duration}</span>
</div>
{/each}
</div>
</div>
</div>
<!-- Playlists -->
<div class="my-7">
<h3 class="mb-4 flex items-center gap-2 text-lg font-semibold">
<ListMusic size={20} />
Public Playlists
</h3>
<div class="grid grid-cols-2 gap-4 sm:grid-cols-3">
{#each user.spotify.playlists as playlist}
<div
class="spotify-card rounded-xl bg-white bg-opacity-20 p-4 transition-all hover:bg-opacity-30"
>
<img
src={playlist.imageUrl}
alt={playlist.name}
class="mb-5 h-full w-full rounded-lg object-cover"
/>
<h4 class="font-semibold">{playlist.name}</h4>
<p class="text-sm text-gray-300">{playlist.tracks} tracks</p>
</div>
{/each}
</div>
</div>
</section>
<!-- Upcoming Trips -->
<section class="p-8">
<h2 class="mb-6 text-2xl font-bold text-gray-900">Upcoming Trips</h2>
<div class="space-y-6">
{#each user.upcomingTrips as trip}
<div
class="trip-card flex items-center justify-between rounded-lg bg-white p-6 shadow-lg"
>
<div>
<h3 class="text-xl font-bold text-gray-900">{trip.destination}</h3>
<div class="flex items-center gap-2 text-gray-600">
<Calendar size={16} /> <time datetime={trip.date}>{trip.date}</time>
-
<Calendar size={16} /> <time datetime={trip.date}>{trip.date}</time>
</div>
</div>
<button class="btn btn-primary rounded-full px-6 py-2 text-sm font-medium">
Join Trip
</button>
</div>
{/each}
</div>
</section>
</main>
</div>
<style>
/* Custom hover effect */
.hover-scale {
transition: transform 150ms ease-out;
}
.hover-scale:hover {
transform: translateY(-2px);
}
.language-card {
transition: all 0.2s ease-in-out;
}
.language-card:hover {
transform: translateY(-2px);
}
</style>

View File

@ -0,0 +1,157 @@
<!-- Search.svelte -->
<script lang="ts">
import { fly } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { onMount } from 'svelte';
let searchQuery = '';
let showError = false;
type FilterKeys = 'accommodation' | 'activities' | 'localGuides' | 'safetyTips' | 'meetups';
type Filters = Record<FilterKeys, boolean>;
const filterKeys: FilterKeys[] = ['accommodation', 'activities', 'localGuides', 'safetyTips', 'meetups'];
let selectedFilters: Filters = {
accommodation: false,
activities: false,
localGuides: false,
safetyTips: false,
meetups: false
};
let recentSearches: string[] = [];
onMount(() => {
const savedSearches = localStorage.getItem('recentSearches');
recentSearches = savedSearches ? JSON.parse(savedSearches) : [
'Local guides in Barcelona',
'Safe hostels Tokyo',
'Community meetups Paris'
];
});
function handleSearch() {
if (!searchQuery.trim() && !Object.values(selectedFilters).some(v => v)) {
showError = true;
return;
}
showError = false;
// Save search query to recent searches
if (searchQuery.trim()) {
const newSearch = searchQuery.trim();
if (!recentSearches.includes(newSearch)) {
recentSearches = [newSearch, ...recentSearches].slice(0, 3);
localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
}
}
console.log('Searching:', searchQuery, selectedFilters);
}
function clearSearch() {
searchQuery = '';
showError = false;
}
</script>
<div class="h-full p-4 md:p-8">
<!-- Hero Section -->
<div class="hero bg-base-100 rounded-box mb-8 p-4 md:p-6" in:fly={{ y: 50, duration: 500 }}>
<div class="hero-content text-center">
<div>
<h1 class="text-3xl md:text-4xl font-bold mb-4">Discover Your Community</h1>
<p class="text-base md:text-lg opacity-80 max-w-md mx-auto">
Connect with fellow travelers, find safe spaces, and create serendipitous moments
</p>
</div>
</div>
</div>
<!-- Search Bar -->
<div class="form-control mx-10 mb-4 ">
<div class="input-group flex gap-4">
<input
type="text"
bind:value={searchQuery}
on:keydown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="Search for local guides, safe spaces, or community meetups..."
class="input input-bordered w-full"
aria-label="Search input"
/>
{#if searchQuery}
<button class="btn btn-square" on:click={clearSearch} aria-label="Clear search">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
{/if}
<button class="btn btn-primary" on:click={handleSearch} aria-label="Search">
Search
</button>
</div>
{#if showError}
<div class="text-error text-sm mt-2 text-center">
Please enter a search term or select at least one filter
</div>
{/if}
</div>
<!-- Filter Tags -->
<div class="flex flex-wrap gap-2 justify-center mb-6">
{#each filterKeys as filter}
<label class="cursor-pointer">
<input
type="checkbox"
class="hidden"
bind:checked={selectedFilters[filter]}
aria-label={`Filter by ${filter}`}
/>
<div
class="badge badge-lg {selectedFilters[filter] ? 'badge-primary' : 'badge-outline'} p-4"
role="checkbox"
aria-checked={selectedFilters[filter]}
>
{filter.charAt(0).toUpperCase() + filter.slice(1)}
</div>
</label>
{/each}
</div>
<!-- Recent Searches -->
<div class="card bg-base-100 shadow-xl max-w-3xl mx-auto">
<div class="card-body">
<h2 class="card-title mb-4">Recent Community Searches</h2>
<div class="space-y-2">
{#each recentSearches as search (search)}
<div
animate:flip={{ duration: 300 }}
class="flex items-center justify-between p-3 hover:bg-base-200 active:bg-base-300 rounded-lg cursor-pointer transition-colors"
on:click={() => {
searchQuery = search;
handleSearch();
}}
>
<span>{search}</span>
<button class="btn btn-ghost btn-sm" aria-label="Apply search">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
{/each}
</div>
</div>
</div>
</div>
<style>
:global(.badge) {
transition: all 0.2s ease-in-out;
}
:global(.badge:hover) {
transform: scale(1.05);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
</style>

View File

@ -1,2 +1,238 @@
<h1>Welcome to SvelteKit</h1>
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
<script lang="ts">
import { onMount } from 'svelte';
import { animate, stagger, inView } from 'motion';
import { goto } from '$app/navigation';
onMount(() => {
// Hero section animations
const heroTextAnimation = {
initial: { opacity: 0, y: 50 },
animate: {
opacity: 1,
y: 0,
duration: 0.8,
delay: 0.2,
easing: [0.17, 0.55, 0.55, 1] // Smooth easing curve instead of spring
}
};
const heroImageAnimation = {
initial: { opacity: 0, scale: 0.9 },
animate: {
opacity: 1,
scale: 1,
duration: 1.2,
easing: 'ease-out'
}
};
// Initialize hero animations
animate('.hero-text', heroTextAnimation.initial);
animate('.hero-text', heroTextAnimation.animate);
animate('.hero-image', heroImageAnimation.initial);
animate('.hero-image', heroImageAnimation.animate);
// Feature cards animation
const featureCardAnimation = {
initial: { opacity: 0, y: 30 },
animate: {
opacity: 1,
y: 0,
duration: 0.6,
easing: 'ease-out'
}
};
// Setup intersection observer for feature grid
inView('.feature-grid', (info: any) => {
const cards = info.target.querySelectorAll('.feature-card');
cards.forEach((card: any, index: any) => {
animate(card,
featureCardAnimation.initial,
{
...featureCardAnimation.animate,
delay: index * 0.15 // Manual stagger implementation
}
);
});
}, {
amount: 0.2 // Trigger when 20% of element is visible
});
// CTA section animation
const ctaAnimation = {
initial: { opacity: 0, scale: 0.95 },
animate: {
opacity: 1,
scale: 1,
duration: 0.8,
easing: 'ease-out'
}
};
inView('.cta-section', (info: any) => {
animate(info.target, ctaAnimation.initial);
animate(info.target, ctaAnimation.animate);
}, {
amount: 0.3 // Trigger when 30% of element is visible
});
});
</script>
<svelte:head>
<title>Boundless | Travel with Soul</title>
<meta name="description" content="Where strangers become allies and journeys create magic. Travel Together, Stay Boundless." />
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&display=swap" rel="stylesheet" />
</svelte:head>
<div class="font-[Space+Grotesk] bg-base-100 overflow-hidden">
<!-- Navigation -->
<nav class="navbar bg-base-100/90 backdrop-blur-sm sticky top-0 z-50 shadow-sm px-4 md:px-8">
<div class="flex-1">
<a class="btn btn-ghost text-xl gap-2 hover:-translate-y-1 transition-transform">
<span class="text-3xl">🌍</span>
<span class="font-bold bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent">
Boundless
</span>
</a>
</div>
<div class="flex-none">
<ul class="menu menu-horizontal px-1 gap-4 hidden md:flex">
<li>
<a class="font-medium hover:text-primary transition-colors">
How it works
</a>
</li>
<li>
<a class="font-medium hover:text-primary transition-colors">
Stories
</a>
</li>
<li>
<button on:click={() => {goto('/onboarding')}} class="btn btn-primary btn-sm hover:scale-105 transition-transform">
Join Beta
</button>
</li>
</ul>
<div class="dropdown dropdown-end md:hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-circle">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" /></svg>
</div>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li><a>How it works</a></li>
<li><a>Stories</a></li>
<li><button on:click={() => {goto('/onboarding')}}>Join Beta</button></li>
</ul>
</div>
</div>
</nav>
<!-- Hero Section -->
<section class="hero min-h-[90vh] relative">
<div class="absolute inset-0 bg-gradient-to-br from-primary/10 to-secondary/10 -skew-y-3 scale-125 opacity-50"></div>
<div class="hero-content flex-col lg:flex-row-reverse gap-8 lg:gap-16 relative z-10 px-4 md:px-8">
<div class="hero-image">
<div class="mask-parallelogram relative group">
<img
src="src\lib\assets\h.png"
class="w-full max-w-2xl rounded-2xl shadow-2xl transform hover:rotate-1 transition-[transform] duration-300"
alt="Happy travelers"
/>
<div class="absolute inset-0 bg-gradient-to-r from-primary/20 to-secondary/20 rounded-2xl"></div>
</div>
</div>
<div class="hero-text text-center lg:text-left">
<h1 class="text-4xl md:text-6xl font-bold leading-tight mb-6">
Find Your
<span class="relative inline-block">
<span class="bg-gradient-to-r from-primary to-accent text-transparent bg-clip-text">
Wander Tribe
</span>
<div class="absolute -bottom-2 left-0 w-full h-2 bg-primary/20 -skew-x-12"></div>
</span>
</h1>
<p class="text-lg md:text-xl mb-8 opacity-90 leading-relaxed">
Where strangers become allies and every journey leaves the world kinder.
</p>
<div class="flex flex-col md:flex-row gap-4 justify-center lg:justify-start">
<button on:click={() => {goto('/onboarding')}} class="btn btn-primary px-8 gap-2 hover:scale-[1.02] transition-transform">
<span class="text-xl"></span>
Start Your Journey
</button>
<button class="btn btn-outline btn-secondary px-8 group">
How It Works
<span class="group-hover:translate-x-1 transition-transform"></span>
</button>
</div>
</div>
</div>
</section>
<!-- Features Grid -->
<div class="container mx-auto px-4 py-16 md:py-24">
<div class="feature-grid grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
<div class="feature-card card bg-base-100 p-8 border border-base-300/20 hover:border-primary/20 transition-all transform hover:-translate-y-2">
<div class="text-6xl mb-6 animate-pulse">🗺️</div>
<h3 class="text-2xl font-bold mb-4">Community Routes</h3>
<p class="text-gray-500">Follow trails blazed by fellow wanderers</p>
</div>
<div class="feature-card card bg-base-100 p-8 border border-base-300/20 hover:border-primary/20 transition-all transform hover:-translate-y-2">
<div class="text-6xl mb-6 animate-pulse">🔮</div>
<h3 class="text-2xl font-bold mb-4">Magic Detours</h3>
<p class="text-gray-500">Algorithm-curated serendipity</p>
</div>
<div class="feature-card card bg-base-100 p-8 border border-base-300/20 hover:border-primary/20 transition-all transform hover:-translate-y-2">
<div class="text-6xl mb-6 animate-pulse">❤️</div>
<h3 class="text-2xl font-bold mb-4">Impact Legacy</h3>
<p class="text-gray-500">Travel that gives back</p>
</div>
</div>
</div>
<!-- CTA Section -->
<div class="cta-section bg-gradient-to-br from-primary to-secondary text-primary-content py-16 md:py-24">
<div class="text-center max-w-2xl mx-auto px-4">
<h2 class="text-3xl md:text-4xl font-bold mb-8 leading-tight">
Ready to Travel<br>
<span class="text-4xl md:text-5xl font-black">Like You Mean It?</span>
</h2>
<button on:click={() => {goto('/onboarding')}} class="btn btn-accent btn-lg gap-2 transform hover:scale-105 transition-transform">
🚀 Claim Beta Spot
</button>
</div>
</div>
<!-- Footer -->
<footer class="footer items-center p-8 bg-neutral text-neutral-content">
<!-- ... (keep previous footer content) ... -->
</footer>
</div>
<style global>
:root {
--font-family: 'Space Grotesk', system-ui;
}
body {
font-family: var(--font-family);
}
.mask-parallelogram {
mask-image: linear-gradient(to bottom right, transparent 2%, black 15%);
-webkit-mask-image: linear-gradient(to bottom right, transparent 2%, black 15%);
}
@keyframes gradient-pulse {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animate-gradient-pulse {
background-size: 200% auto;
animation: gradient-pulse 8s ease infinite;
}
</style>

View File

@ -1 +0,0 @@
<a href="/demo/paraglide">paraglide</a>

View File

@ -1,18 +0,0 @@
<script lang="ts">
import type { AvailableLanguageTag } from '$lib/paraglide/runtime';
import { i18n } from '$lib/i18n';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import * as m from '$lib/paraglide/messages.js';
function switchToLanguage(newLanguage: AvailableLanguageTag) {
const canonicalPath = i18n.route(page.url.pathname);
const localisedPath = i18n.resolveRoute(canonicalPath, newLanguage);
goto(localisedPath);
}
</script>
<h1>{m.hello_world({ name: 'SvelteKit User' })}</h1>
<div>
<button onclick={() => switchToLanguage('en')}>en</button>
</div>

View File

@ -0,0 +1,324 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { animate } from 'motion';
import { onMount } from 'svelte';
// Form data store
let travelerData = {
name: '',
intention: '',
interests: [] as string[],
travelStyle: ''
};
// DOM elements
let welcomeEl: HTMLElement;
let registerEl: HTMLElement;
let intentionEl: HTMLElement;
let interestsEl: HTMLElement;
let finalEl: HTMLElement;
let confettiContainer: HTMLElement;
// Step management
let currentStep = 0;
const steps = ['welcome', 'register', 'intention', 'interests', 'final'];
const stepElements = [welcomeEl, registerEl, intentionEl, interestsEl, finalEl];
// Form options
const travelIntentions = [
'Seeking Adventure',
'Cultural Exchange',
'Personal Growth',
'Meeting People',
'Digital Nomad'
];
const travelInterests = [
'Local Food',
'Hiking',
'Photography',
'Language Exchange',
'Art & Museums',
'Sustainable Travel',
'Local Markets',
'Festivals & Events'
];
const travelStyles = [
'Spontaneous',
'Planned but Flexible',
'Off the Beaten Path',
'Mix of Everything'
];
// Animation controller
const animateSequence = async (from: HTMLElement, to: HTMLElement) => {
from.style.zIndex = '10';
to.style.zIndex = '20';
const fromAnimation = animate(
from,
{ y: [0, '-100vh'] },
{ duration: 0.7, easing: [0.4, 0, 0.2, 1] }
);
const toAnimation = animate(
to,
{ y: ['100vh', 0] },
{ duration: 0.7, easing: [0.4, 0, 0.2, 1] }
);
await Promise.all([fromAnimation, toAnimation]);
from.style.zIndex = '0';
currentStep = steps.indexOf(to.id);
};
const handleInterestToggle = (e: MouseEvent, interest: string) => {
if (travelerData.interests.includes(interest)) {
travelerData.interests = travelerData.interests.filter((i) => i !== interest);
} else if (travelerData.interests.length < 3) {
travelerData.interests = [...travelerData.interests, interest];
}
animate(e.currentTarget as HTMLElement, { scale: [1, 1.1, 1] }, { duration: 0.3 });
};
const isValid = () => {
return (
travelerData.name.length >= 2 &&
travelerData.intention &&
travelerData.interests.length > 0 &&
travelerData.travelStyle
);
};
const createConfetti = () => {
for (let i = 0; i < 30; i++) {
const confetti = document.createElement('div');
confetti.className = 'absolute w-2 h-2 bg-white rounded-full';
confetti.style.left = `${Math.random() * 100}%`;
confetti.style.top = `${Math.random() * 100}%`;
confettiContainer.appendChild(confetti);
animate(
confetti,
{
x: Math.random() * 400 - 200,
y: Math.random() * -400 - 100,
opacity: [1, 0],
scale: [1, 1.5]
},
{ duration: 1.5, easing: 'ease-out' }
).then(() => confetti.remove());
}
setTimeout(() => {
goto('/home');
}, 750);
};
onMount(() => {
animate(welcomeEl, { scale: [0.8, 1], opacity: [0, 1] }, { duration: 0.5 });
});
</script>
<!-- Progress Indicator -->
<div class="absolute top-6 z-30 w-full px-8">
<div class="h-1.5 rounded-full bg-white/20">
<div
class="h-full rounded-full bg-white/80 transition-all duration-500"
style={`width: ${(currentStep / (steps.length - 1)) * 100}%`}
/>
</div>
</div>
<!-- Welcome Screen -->
<div
bind:this={welcomeEl}
id="welcome"
class="gradient-animate fixed inset-0 flex h-screen w-screen flex-col items-center justify-center bg-gradient-to-br from-pink-400 via-purple-400 to-indigo-400"
>
{#each Array(12) as _, i}
<div
class="floating-shape absolute h-8 w-8 rounded-full bg-white/10"
style={`left: ${Math.random() * 100}%; animation-delay: ${Math.random() * 2}s;`}
/>
{/each}
<h1 class="mb-4 animate-[slideDown_0.5s] text-5xl font-bold text-white">Boundless 🌍✨</h1>
<button
on:click={() => animateSequence(welcomeEl, registerEl)}
class="animate-[bounce_2s_infinite] rounded-full bg-white/20 px-8 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30"
>
Start Exploring →
</button>
</div>
<!-- Name Input -->
<div
bind:this={registerEl}
id="register"
class="gradient-animate fixed inset-0 flex h-screen w-screen translate-y-full flex-col items-center justify-center bg-gradient-to-br from-purple-400 via-pink-400 to-rose-400"
>
<div class="space-y-8 text-center">
<h2 class="text-3xl font-semibold text-white">What's your travel name?</h2>
<label for="nameInput" class="sr-only">Enter your travel name</label>
<input
id="nameInput"
bind:value={travelerData.name}
class="w-64 rounded-full bg-white/20 px-6 py-3 text-center text-white placeholder-white/70 backdrop-blur-sm focus:outline-none focus:ring-2 focus:ring-white/50"
type="text"
placeholder="Adventure Seeker"
/>
<button
on:click={() => animateSequence(registerEl, intentionEl)}
class="rounded-full bg-white/20 px-8 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30"
disabled={!travelerData.name}
>
Next →
</button>
</div>
</div>
<!-- Travel Intention -->
<div
bind:this={intentionEl}
id="intention"
class="gradient-animate fixed inset-0 flex h-screen w-screen translate-y-full flex-col items-center justify-center bg-gradient-to-br from-rose-400 via-orange-400 to-amber-400"
>
<div class="space-y-8 text-center">
<h2 class="text-3xl font-semibold text-white">Why do you wander?</h2>
<div class="grid w-72 grid-cols-1 gap-3">
{#each travelIntentions as intention}
<button
on:click={(e) => {
travelerData.intention = intention;
animate(e.currentTarget, { scale: [1, 0.95, 1] }, { duration: 0.3 });
}}
class="rounded-full bg-white/20 px-6 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30 {travelerData.intention ===
intention
? '!bg-white/40 ring-2 ring-white'
: ''}"
>
{intention}
</button>
{/each}
</div>
<button
on:click={() => animateSequence(intentionEl, interestsEl)}
class="rounded-full bg-white/20 px-8 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30"
disabled={!travelerData.intention}
>
Next →
</button>
</div>
</div>
<!-- Interests Selection -->
<div
bind:this={interestsEl}
id="interests"
class="gradient-animate fixed inset-0 flex h-screen w-screen translate-y-full flex-col items-center justify-center bg-gradient-to-br from-purple-400 via-pink-400 to-gray-400"
>
<div class="space-y-8 text-center">
<h2 class="text-3xl font-semibold text-white">Pick your passions</h2>
<div class="grid w-80 grid-cols-2 gap-3">
{#each travelInterests as interest}
<button
on:click={(e) => handleInterestToggle(e, interest)}
class="rounded-full bg-white/20 px-4 py-2 text-sm text-white backdrop-blur-sm transition-all hover:bg-white/30
{travelerData.interests.includes(interest) ? '!bg-white/40 ring-2 ring-white' : ''}"
aria-pressed={travelerData.interests.includes(interest)}
>
{interest}
</button>
{/each}
</div>
<button
on:click={() => animateSequence(interestsEl, finalEl)}
class="rounded-full bg-white/20 px-8 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30"
disabled={travelerData.interests.length === 0}
>
Next →
</button>
</div>
</div>
<!-- Final Step -->
<div
bind:this={finalEl}
id="final"
class="gradient-animate fixed inset-0 flex h-screen w-screen translate-y-full flex-col items-center justify-center bg-gradient-to-br from-lime-400 via-emerald-400 to-cyan-400"
>
<div class="space-y-8 text-center">
<h2 class="text-3xl font-semibold text-white">Travel style?</h2>
<div class="grid w-72 grid-cols-1 gap-3">
{#each travelStyles as style}
<button
on:click={(e) => {
travelerData.travelStyle = style;
animate(e.currentTarget, { scale: [1, 0.95, 1] }, { duration: 0.3 });
}}
class="rounded-full bg-white/20 px-6 py-3 text-white backdrop-blur-sm transition-all hover:bg-white/30 {travelerData.travelStyle ===
style
? '!bg-white/40 ring-2 ring-white'
: ''}"
>
{style}
</button>
{/each}
</div>
<div bind:this={confettiContainer} class="pointer-events-none absolute inset-0" />
<button
on:click={() => createConfetti()}
class="rounded-full bg-white/20 px-8 py-3 text-white ring-2 ring-white/50 backdrop-blur-sm transition-all hover:bg-white/30 hover:ring-white/80 {isValid()
? 'animate-pulse'
: ''}"
disabled={!isValid()}
>
Start Exploring 🌟
</button>
</div>
</div>
<style global>
@keyframes gradient-pan {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.gradient-animate {
background-size: 200% 200%;
animation: gradient-pan 10s ease infinite;
}
@keyframes float {
0% {
transform: translateY(100vh) rotate(0deg);
opacity: 0;
}
100% {
transform: translateY(-100vh) rotate(360deg);
opacity: 0.4;
}
}
.floating-shape {
animation: float 8s linear infinite;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border-width: 0;
}
</style>

View File

@ -0,0 +1,123 @@
<script>
import { animate, inView } from 'motion';
import { onMount } from 'svelte';
const planePath = "M13 11.5V7.5L16 8.5L19 4.5L22 8.5L25 7.5V11.5L22 12.5L25 16.5L22 20.5L19 16.5L16 20.5L13 16.5V11.5Z";
let email = '';
let password = '';
function handleSubmit() {
// Add your signin logic
}
function floatPlane() {
animate("#paper-plane",
// @ts-ignore
{ y: [-10, 10] },
{ duration: 2, repeat: Infinity, direction: "alternate" }
);
}
onMount(() => {
// Page load animations
animate("#form-container",
{ y: [50, 0], opacity: [0, 1] },
{ duration: 0.8 }
);
animate("#paper-plane",
{ x: [-100, 0], rotate: [-45, 0] },
{ duration: 1 }
);
floatPlane();
animate(".route-line",
{ strokeDashoffset: [100, 0] },
{ duration: 2, delay: 0.5 }
);
});
</script>
<div class="min-h-screen bg-base-200 relative overflow-hidden">
<!-- Background Elements -->
<div class="absolute inset-0 opacity-10">
<!-- Add your map/route background SVG here -->
<svg class="w-full h-full">
<path
d="M100,200 Q250,150 400,200"
class="route-line fill-none stroke-primary stroke-2"
style="stroke-dasharray: 100px;"
/>
</svg>
</div>
<!-- Main Content -->
<div class="flex items-center justify-center min-h-screen">
<!-- Paper Plane Decor -->
<div id="paper-plane" class="absolute left-1/4 top-1/3 opacity-50">
<svg class="w-32 h-32 text-primary" viewBox="0 0 32 32">
<path fill="currentColor" d="{planePath}" />
</svg>
</div>
<!-- Form Container -->
<div id="form-container" class="card bg-base-100 shadow-xl w-96 relative z-10">
<div class="card-body">
<h2 class="card-title text-3xl mb-6">Welcome Back, Explorer! ✈️</h2>
<form on:submit|preventDefault={handleSubmit}>
<div class="form-control">
<label class="label">
<span class="label-text">Email</span>
</label>
<input
type="email"
bind:value={email}
placeholder="your.email@adventure.com"
class="input input-bordered"
/>
</div>
<div class="form-control mt-4">
<label class="label">
<span class="label-text">Password</span>
</label>
<input
type="password"
bind:value={password}
placeholder="••••••••"
class="input input-bordered"
/>
</div>
<button
class="btn btn-primary mt-6 w-full"
>
Let's Travel Together! 🌍
</button>
</form>
<div class="divider my-6">New Adventurer?</div>
<a
href="/signup"
class="btn btn-outline btn-secondary w-full"
>
Create Travel Profile 🧳
</a>
</div>
</div>
</div>
</div>
<style>
/* Add custom paper texture if needed */
.paper-texture {
background-image: url('paper-texture.png');
mix-blend-mode: multiply;
}
</style>

View File

@ -10,5 +10,16 @@ export default {
extend: {}
},
plugins: [typography, forms, containerQueries]
daisyui: {
themes: ["fantasy"], // false: only light + dark | true: all themes | array: specific themes like this ["light", "dark", "cupcake"]
darkTheme: "dark", // name of one of the included themes for dark mode
base: true, // applies background color and foreground color for root element by default
styled: true, // include daisyUI colors and design decisions for all components
utils: true, // adds responsive and modifier utility classes
prefix: "", // prefix for daisyUI classnames (components, modifiers and responsive class names. Not colors)
logs: true, // Shows info about daisyUI version and used config in the console when building your CSS
themeRoot: ":root", // The element that receives theme color CSS variables
},
plugins: [typography, forms, containerQueries, require('daisyui'),]
} satisfies Config;