changes
This commit is contained in:
parent
f903569a60
commit
e7eaf2a592
2264
frontend/package-lock.json
generated
2264
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,11 @@
|
||||
"@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",
|
||||
@ -48,7 +53,21 @@
|
||||
"@capacitor/cli": "^7.0.1",
|
||||
"@capacitor/core": "^7.0.1",
|
||||
"@inlang/paraglide-sveltekit": "^0.15.5",
|
||||
"@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",
|
||||
"globe.gl": "^2.39.2",
|
||||
"gsap": "^3.12.7",
|
||||
"leaflet": "^1.9.4",
|
||||
"lucide-svelte": "^0.474.0",
|
||||
"motion": "^12.0.6"
|
||||
"maplibre-gl": "^5.1.0",
|
||||
"motion": "^12.0.6",
|
||||
"phoenix": "^1.7.18",
|
||||
"pikaday": "^1.8.2",
|
||||
"svelte-motion": "^0.12.2",
|
||||
"svelte-transitions": "^1.2.0",
|
||||
"three": "^0.172.0"
|
||||
}
|
||||
}
|
||||
|
@ -9,3 +9,4 @@
|
||||
border-radius: 1rem;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,21 @@
|
||||
<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>
|
||||
@ -15,9 +24,9 @@
|
||||
animate={true}
|
||||
darkMode={false}
|
||||
/>
|
||||
</div>
|
||||
</ParaglideJS>
|
||||
|
||||
|
||||
<style>
|
||||
.fillscreen{
|
||||
height: calc(100svh - 64px);
|
||||
|
@ -1,95 +1,334 @@
|
||||
<script lang="ts">
|
||||
import { Globe, Users, Plane, Instagram, Twitter, Youtube, MessageCircle, Heart } from "lucide-svelte";
|
||||
import Globe from 'globe.gl';
|
||||
import { onMount } from 'svelte';
|
||||
import * as THREE from 'three';
|
||||
|
||||
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' },
|
||||
{ id: 3, title: 'Ocean Dive Adventure 🌊', content: 'Explored the coral reefs today. An unforgettable underwater experience!', likes: 134, comments: 24, date: '2024-02-25' },
|
||||
{ id: 4, title: 'Cultural Exploration in Ubud 🏯', content: 'Spent the day visiting temples and enjoying the local culture. Bali, you are beautiful.', likes: 210, comments: 37, date: '2024-02-22' },
|
||||
{ id: 5, title: 'Relaxing Beach Day 🏖️', content: 'Laid back by the waves with a coconut in hand. Pure bliss.', likes: 180, comments: 31, date: '2024-02-23' },
|
||||
{ id: 6, title: 'Island Hopping 🌴', content: 'Visited Nusa Penida today. The cliffs and beaches are otherworldly!', likes: 250, comments: 48, date: '2024-02-24' },
|
||||
{ id: 7, title: 'Bali Swing Adventure 🏞️', content: 'Took a swing over the jungle in Ubud. Pure adrenaline!', likes: 320, comments: 55, date: '2024-01-15' },
|
||||
{ id: 8, title: 'Coffee Tasting in Ubud ☕', content: 'Explored the world of Bali coffee today! The Luwak coffee was a highlight!', likes: 145, comments: 22, date: '2024-01-17' },
|
||||
{ id: 9, title: 'Sunrise at Tanah Lot ⛅', content: 'Witnessed one of the most breathtaking sunrises at Tanah Lot Temple. A must-see!', likes: 198, comments: 40, date: '2024-01-19' },
|
||||
{ id: 10, title: 'Exploring Bali’s Waterfalls 🌿', content: 'Trekking through the jungle to reach these stunning waterfalls. A perfect adventure!', likes: 215, comments: 38, date: '2024-01-21' },
|
||||
{ id: 11, title: 'Bali Beach Party 🎉', content: 'Danced the night away on the beach with new friends! Bali nights are unforgettable.', likes: 350, comments: 72, date: '2024-01-23' },
|
||||
{ id: 12, title: 'Yoga Retreat in Ubud 🧘', content: 'Spent the weekend focusing on mindfulness and yoga. Feeling recharged!', likes: 215, comments: 50, date: '2024-01-25' }
|
||||
]
|
||||
// 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;
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' });
|
||||
};
|
||||
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>
|
||||
|
||||
<section class="p-6">
|
||||
<h2 class="mb-6 text-2xl font-bold text-gray-900">Recent Posts</h2>
|
||||
<div class="space-y-4">
|
||||
{#each user.posts as post}
|
||||
<div class="post-card border border-gray-200 rounded-xl bg-white p-4 hover:bg-gray-50 transition-colors">
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- Tweet Content -->
|
||||
<div class="flex-1 min-w-0">
|
||||
<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>
|
||||
{#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>
|
||||
|
||||
<!-- Tweet Body -->
|
||||
<p class="mt-2 text-gray-900 leading-relaxed">
|
||||
{post.content}
|
||||
</p>
|
||||
<!-- Globe Section -->
|
||||
<div class="card sticky top-4 bg-base-100 shadow-sm">
|
||||
|
||||
<!-- Tweet Actions -->
|
||||
<div class="mt-3 flex gap-6 text-gray-500">
|
||||
<button class="flex items-center gap-2 hover:text-blue-500 transition-colors">
|
||||
<MessageCircle size={18} />
|
||||
<span class="text-sm">{post.comments}</span>
|
||||
</button>
|
||||
<button class="flex items-center gap-2 hover:text-red-500 transition-colors">
|
||||
<Heart size={18} />
|
||||
<span class="text-sm">{post.likes}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</section>
|
||||
</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>
|
||||
|
@ -102,7 +102,7 @@
|
||||
</style>
|
||||
|
||||
<!-- Main Layout -->
|
||||
<div class="flex flex-col md:flex-row h-full bg-base-200">
|
||||
<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">
|
||||
@ -130,8 +130,8 @@
|
||||
</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">
|
||||
<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)]">
|
||||
|
@ -1,43 +1,112 @@
|
||||
<!-- +page.svelte -->
|
||||
<script>
|
||||
import { goto } from "$app/navigation";
|
||||
// @ts-nocheck
|
||||
|
||||
// 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>
|
||||
import { goto } from "$app/navigation";
|
||||
|
||||
<!-- 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>
|
||||
// Fleets data
|
||||
const fleets = [
|
||||
{ user: 'Swipe', handle: '@user1', avatar: '😊' },
|
||||
{ user: 'feature', handle: '@user2', avatar: '😎' },
|
||||
{ user: 'feature', handle: '@user3', avatar: '🤩' },
|
||||
{ user: 'feature', handle: '@user4', avatar: '🥳' },
|
||||
];
|
||||
|
||||
// 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("/swipe")} 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>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style global>
|
||||
/* Optional custom styles */
|
||||
:global(body) {
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
</style>
|
||||
<!-- 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>
|
||||
|
||||
|
BIN
frontend/src/routes/(authed)/home/bg.png
Normal file
BIN
frontend/src/routes/(authed)/home/bg.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 658 KiB |
BIN
frontend/src/routes/(authed)/home/bu.jpg
Normal file
BIN
frontend/src/routes/(authed)/home/bu.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 403 KiB |
BIN
frontend/src/routes/(authed)/home/clouds.png
Normal file
BIN
frontend/src/routes/(authed)/home/clouds.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 MiB |
BIN
frontend/src/routes/(authed)/home/ea.jpg
Normal file
BIN
frontend/src/routes/(authed)/home/ea.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.3 MiB |
@ -1,28 +1,76 @@
|
||||
<script lang="ts">
|
||||
import type { Notification } from '$lib/types';
|
||||
<script>
|
||||
import { BellIcon, CheckIcon, GlobeIcon, UserIcon } from 'lucide-svelte';
|
||||
import { fly } from 'svelte/transition';
|
||||
|
||||
export let notifications: Notification[];
|
||||
</script>
|
||||
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 }
|
||||
];
|
||||
|
||||
<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}
|
||||
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>
|
||||
</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>
|
@ -10,7 +10,29 @@
|
||||
MessageCircle,
|
||||
Instagram,
|
||||
Twitter,
|
||||
Youtube
|
||||
Youtube,
|
||||
Flame,
|
||||
Crown,
|
||||
Languages,
|
||||
Trophy,
|
||||
|
||||
Clock
|
||||
,
|
||||
|
||||
ListMusic
|
||||
,
|
||||
|
||||
Music
|
||||
,
|
||||
|
||||
Play
|
||||
,
|
||||
|
||||
RepeatIcon
|
||||
,
|
||||
|
||||
Shuffle
|
||||
|
||||
} from 'lucide-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
@ -52,7 +74,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 = () => {
|
||||
@ -103,15 +207,49 @@
|
||||
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 bg-gray-100 py-8">
|
||||
<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" />
|
||||
@ -226,6 +364,159 @@
|
||||
</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>
|
||||
@ -260,4 +551,12 @@
|
||||
.hover-scale:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.language-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.language-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
</style>
|
||||
|
@ -55,7 +55,7 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="min-h-screen bg-base-200 p-4 md:p-8">
|
||||
<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">
|
||||
@ -69,7 +69,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Search Bar -->
|
||||
<div class="form-control w-fit max-w-3xl mx-10 mb-4 ">
|
||||
<div class="form-control mx-10 mb-4 ">
|
||||
<div class="input-group flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
|
@ -15,20 +15,26 @@
|
||||
};
|
||||
}
|
||||
|
||||
// Use proper typing for the stack
|
||||
let currentIndex = 0;
|
||||
let stack: any[] = profiles;
|
||||
let stack: Profile[] = profiles;
|
||||
let connections: Profile[] = [];
|
||||
let dragging: Profile | null = null;
|
||||
let dragStartOffset = { x: 0, y: 0 };
|
||||
|
||||
let dragCoords = spring({ 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
|
||||
});
|
||||
|
||||
const calculateConnectionPath = (connection: Profile) => {
|
||||
// Add proper return type
|
||||
const calculateConnectionPath = (connection: Profile): string => {
|
||||
const start = connection.location || { x: 500, y: 250 };
|
||||
const end = { x: window.innerWidth - 100, y: window.innerHeight - 100 };
|
||||
// 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
|
||||
@ -36,45 +42,53 @@
|
||||
return `M ${start.x},${start.y} Q ${controlPoint.x},${controlPoint.y} ${end.x},${end.y}`;
|
||||
};
|
||||
|
||||
const handleAction = (action: 'skip' | 'decide' | 'add') => {
|
||||
// Type the action parameter properly
|
||||
const handleAction = (action: 'skip' | 'decide' | 'add'): void => {
|
||||
if (stack.length === 0) return;
|
||||
|
||||
const profile = stack[currentIndex];
|
||||
|
||||
if (action === 'add') {
|
||||
connections = [...connections, { ...profile, location: { ...$dragCoords } }];
|
||||
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) {
|
||||
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);
|
||||
animateProfileToSuitcase(profileCard, suitcase);
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the current profile and update the stack
|
||||
stack = stack.filter((_, i) => i !== currentIndex);
|
||||
currentIndex = Math.min(currentIndex, stack.length - 1);
|
||||
};
|
||||
|
||||
const startDrag = (event: MouseEvent, profile: Profile) => {
|
||||
// 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();
|
||||
|
||||
@ -92,7 +106,7 @@
|
||||
document.body.style.cursor = 'grabbing';
|
||||
};
|
||||
|
||||
function handleMouseMove(event: MouseEvent) {
|
||||
function handleMouseMove(event: MouseEvent): void {
|
||||
if (dragging) {
|
||||
dragCoords.set({
|
||||
x: event.clientX - dragStartOffset.x,
|
||||
@ -101,7 +115,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
function handleMouseUp() {
|
||||
function handleMouseUp(): void {
|
||||
if (dragging) {
|
||||
const suitcase = document.querySelector('#suitcase');
|
||||
if (suitcase) {
|
||||
@ -117,14 +131,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isOverlapping(rect1: DOMRect, rect2: DOMRect) {
|
||||
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) {
|
||||
function handleKeyDown(event: KeyboardEvent, profile: Profile): void {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
const element = event.currentTarget as HTMLElement;
|
||||
const rect = element.getBoundingClientRect();
|
||||
@ -135,15 +149,22 @@
|
||||
}), 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="min-h-screen bg-stone-100 font-handwritten relative overflow-hidden">
|
||||
<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}
|
||||
{#each connections as connection (connection.id)}
|
||||
<path d={calculateConnectionPath(connection)}
|
||||
class="stroke-pink-400/40 stroke-2 animate-dash"
|
||||
fill="none" />
|
||||
@ -186,14 +207,13 @@
|
||||
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}
|
||||
{#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}
|
||||
@ -202,7 +222,7 @@
|
||||
</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}
|
||||
{#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"
|
||||
|
@ -141,7 +141,7 @@
|
||||
/>
|
||||
{/each}
|
||||
|
||||
<h1 class="mb-4 animate-[slideDown_0.5s] text-5xl font-bold text-white">🌍 WanderLink</h1>
|
||||
<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"
|
||||
|
Loading…
Reference in New Issue
Block a user