prototype ig

This commit is contained in:
mohamad 2025-01-29 00:18:39 +01:00
parent 398d62abd0
commit 38c19728e8
20 changed files with 2708 additions and 675 deletions

File diff suppressed because it is too large Load Diff

View File

@ -20,6 +20,7 @@
"@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",
@ -27,6 +28,7 @@
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/typography": "^0.5.16",
"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 +45,9 @@
"vitest": "^3.0.0"
},
"dependencies": {
"@inlang/paraglide-sveltekit": "^0.15.5"
"@capacitor/cli": "^7.0.1",
"@capacitor/core": "^7.0.1",
"@inlang/paraglide-sveltekit": "^0.15.5",
"motion": "^12.0.6"
}
}

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,11 @@
@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,25 @@
<script lang="ts">
import { i18n } from '$lib/i18n';
import Nav from '$lib/Nav.svelte';
import { ParaglideJS } from '@inlang/paraglide-sveltekit';
let { children } = $props();
</script>
<ParaglideJS {i18n}>
<div class="fillscreen">
{@render children()}
</div>
<Nav
showLabels={false}
animate={true}
darkMode={false}
/>
</ParaglideJS>
<style>
.fillscreen{
height: calc(100svh - 64px);
}
</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 bg-base-200">
<!-- 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-r 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-base-200">
<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,43 @@
<!-- +page.svelte -->
<script>
import { goto } from "$app/navigation";
// You can add your data or functions here if needed
const cards = [
{ title: 'Swipe', description: 'swipe and make friends', btn: 'lesgooo' },
];
</script>
<div class="container mx-auto p-4">
<!-- Hero Section -->
<div class="text-center py-12">
<h1 class="text-4xl font-bold mb-4">Welcome user</h1>
</div>
<!-- Card Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{#each cards as card}
<div class="card bg-base-100 shadow-xl hover:shadow-2xl transition-shadow duration-200">
<figure class="px-6 pt-6">
<div class="h-32 w-full bg-base-200 rounded-lg flex items-center justify-center">
<span class="text-4xl">😎</span> <!-- Replace with actual icon/image -->
</div>
</figure>
<div class="card-body items-center text-center">
<h2 class="card-title">{card.title}</h2>
<p class="text-gray-600">{card.description}</p>
<div class="card-actions mt-4">
<button on:click={() => {goto('/swipe')}} class="btn btn-primary">{card.btn}</button>
</div>
</div>
</div>
{/each}
</div>
</div>
<style global>
/* Optional custom styles */
:global(body) {
background-color: #f5f5f5;
}
</style>

View File

@ -0,0 +1,28 @@
<script lang="ts">
import type { Notification } from '$lib/types';
export let notifications: Notification[];
</script>
<div class="p-4 max-w-3xl mx-auto">
<h1 class="text-2xl font-bold mb-6">Notifications</h1>
<div class="space-y-4">
{#each notifications as notification (notification.id)}
<div
class="p-4 rounded-lg {!notification.read ? 'bg-primary/10 border-l-4 border-primary' : 'bg-base-200'}"
>
<div class="flex justify-between items-start">
<div>
<p class="font-medium">{notification.content}</p>
<p class="text-sm text-base-content/50 mt-1">
{notification.timestamp}
</p>
</div>
{#if !notification.read}
<span class="badge badge-primary badge-sm">New</span>
{/if}
</div>
</div>
{/each}
</div>
</div>

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="min-h-screen bg-base-200 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 w-fit max-w-3xl 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

@ -0,0 +1,288 @@
<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;
};
}
let currentIndex = 0;
let stack: any[] = profiles;
let connections: Profile[] = [];
let dragging: Profile | null = null;
let dragStartOffset = { x: 0, y: 0 };
let dragCoords = spring({ x: 0, y: 0 }, {
stiffness: 0.2,
damping: 0.7
});
const calculateConnectionPath = (connection: Profile) => {
const start = connection.location || { x: 500, y: 250 };
const end = { x: window.innerWidth - 100, y: window.innerHeight - 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}`;
};
const handleAction = (action: 'skip' | 'decide' | 'add') => {
if (stack.length === 0) return;
const profile = stack[currentIndex];
if (action === 'add') {
connections = [...connections, { ...profile, location: { ...$dragCoords } }];
const suitcase = document.querySelector('#suitcase');
const profileCard = document.querySelector(`#profile-${profile.id}`);
if (suitcase && profileCard) {
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);
}
}
stack = stack.filter((_, i) => i !== currentIndex);
currentIndex = Math.min(currentIndex, stack.length - 1);
};
const startDrag = (event: MouseEvent, profile: Profile) => {
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) {
if (dragging) {
dragCoords.set({
x: event.clientX - dragStartOffset.x,
y: event.clientY - dragStartOffset.y
});
}
}
function handleMouseUp() {
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) {
return !(rect1.right < rect2.left ||
rect1.left > rect2.right ||
rect1.bottom < rect2.top ||
rect1.top > rect2.bottom);
}
function handleKeyDown(event: KeyboardEvent, profile: Profile) {
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);
}
}
</script>
<svelte:window on:mousemove={handleMouseMove} on:mouseup={handleMouseUp} />
<div class="min-h-screen bg-stone-100 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}
<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)}>
<!-- Profile Card -->
<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}
<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 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

@ -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

@ -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">🌍 WanderLink</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

@ -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;