Enhance user profile and world travel pages with improved UI and functionality

- Updated user profile page to include achievement calculations and enhanced styling for user information and statistics.
- Added icons for better visual representation of user stats and achievements.
- Improved layout for displaying adventures and collections with conditional rendering for empty states.
- Refactored world travel page to include search and filter functionality for cities, with a sidebar for progress and stats.
- Implemented completion percentage and progress bars for visited cities.
- Enhanced map integration with markers for visited and not visited cities, including toggle options for map labels.
This commit is contained in:
Sean Morley
2025-06-14 11:10:59 -04:00
parent d4c76f8718
commit 151c76dbd1
21 changed files with 2209 additions and 2461 deletions

View File

@@ -5,124 +5,270 @@
import { onMount } from 'svelte';
import { gsap } from 'gsap';
// Initial animation for page load
onMount(() => {
// Stat animations with quicker duration
gsap.from('.stat', {
opacity: 0,
y: 50,
duration: 0.6, // Quicker animation duration
stagger: 0.1, // Faster staggering between elements
ease: 'power2.out' // Slightly sharper easing for quicker feel
});
gsap.from('.stat-title', {
opacity: 0,
x: -50, // Smaller movement for quicker animation
duration: 0.6, // Quicker animation duration
stagger: 0.1, // Faster staggering
ease: 'power2.out' // Slightly sharper easing for quicker feel
});
// Stat values with faster reveal and snappier effect
gsap.from('.stat-value', {
opacity: 0,
scale: 0.8, // Slightly less scaling for a snappier effect
duration: 1, // Shorter duration
stagger: 0.2, // Faster staggering
ease: 'elastic.out(0.75, 0.5)', // Slightly snappier bounce
delay: 0 // Faster delay for quicker sequencing
});
// Adventure card animations with quicker reveal
gsap.from('.adventure-card', {
opacity: 0,
y: 50, // Less movement for snappier feel
duration: 0.8, // Quicker duration
stagger: 0.1, // Faster staggering
ease: 'power2.out',
delay: 0 // Shorter delay for quicker appearance
});
});
export let data: PageData;
// Icons
import FlagCheckeredVariantIcon from '~icons/mdi/flag-checkered-variant';
import Airplane from '~icons/mdi/airplane';
import CityVariantOutline from '~icons/mdi/city-variant-outline';
import MapMarkerStarOutline from '~icons/mdi/map-marker-star-outline';
import TrendingUp from '~icons/mdi/trending-up';
import CalendarClock from '~icons/mdi/calendar-clock';
import Plus from '~icons/mdi/plus';
export let data: PageData;
const user = data.user;
const recentAdventures = data.props.adventures;
const stats = data.props.stats;
// Calculate completion percentage
$: completionPercentage =
stats.visited_country_count > 0 ? Math.round((stats.visited_country_count / 195) * 100) : 0; // Assuming ~195 countries worldwide
</script>
<div class="container mx-auto p-4">
<!-- Welcome Message -->
<div class="mb-8">
<h1 class="text-4xl font-extrabold">
{$t('dashboard.welcome_back')}, {user?.first_name ? `${user.first_name}` : user?.username}!
</h1>
</div>
<!-- Stats -->
<div class="stats shadow mb-8 w-full bg-neutral">
<div class="stat">
<div class="stat-figure text-secondary">
<Airplane class="w-10 h-10 inline-block" />
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.total_adventures')}</div>
<div class="stat-value text-secondary">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-figure text-primary">
<FlagCheckeredVariantIcon class="w-10 h-10 inline-block" />
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.countries_visited')}</div>
<div class="stat-value text-primary">{stats.visited_country_count}</div>
</div>
<div class="stat">
<div class="stat-figure text-success">
<MapMarkerStarOutline class="w-10 h-10 inline-block" />
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.total_visited_regions')}</div>
<div class="stat-value text-success">{stats.visited_region_count}</div>
</div>
<div class="stat">
<div class="stat-figure text-info">
<CityVariantOutline class="w-10 h-10 inline-block" />
</div>
<div class="stat-title text-neutral-content">{$t('dashboard.total_visited_cities')}</div>
<div class="stat-value text-info">{stats.visited_city_count}</div>
</div>
</div>
<!-- Recent Adventures -->
{#if recentAdventures.length > 0}
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.recent_adventures')}</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
{#each recentAdventures as adventure}
<div class="adventure-card">
<AdventureCard {adventure} user={data.user} readOnly />
</div>
{/each}
</div>
{/if}
<!-- Inspiration if there are no recent adventures -->
{#if recentAdventures.length === 0}
<div
class="inspiration flex flex-col items-center justify-center bg-neutral shadow p-8 mb-8 rounded-lg text-neutral-content"
>
<h2 class="text-3xl font-semibold mb-4">{$t('dashboard.no_recent_adventures')}</h2>
<p class="text-lg text-center">
{$t('dashboard.add_some')}
</p>
<a href="/adventures" class="btn btn-primary mt-4">{$t('map.add_adventure')}</a>
</div>
{/if}
</div>
<svelte:head>
<title>Dashboard | AdventureLog</title>
<meta name="description" content="Home dashboard for AdventureLog." />
</svelte:head>
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
<div class="container mx-auto px-6 py-8">
<!-- Welcome Section -->
<div class="welcome-section mb-12">
<div class="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-6">
<div>
<div class="flex items-center gap-4 mb-4">
<div class="avatar placeholder">
<div class="bg-primary text-primary-content rounded-full w-16 h-16">
<span class="text-xl font-bold">
{user?.first_name?.charAt(0) || user?.username?.charAt(0) || 'A'}
</span>
</div>
</div>
<div>
<h1
class="text-4xl lg:text-5xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent"
>
{$t('dashboard.welcome_back')}, {user?.first_name
? `${user.first_name}`
: user?.username}!
</h1>
<p class="text-lg text-base-content/60 mt-2">
{#if stats.adventure_count > 0}
You've been on <span class="font-semibold text-primary"
>{stats.adventure_count}</span
> adventures so far
{:else}
Ready to start your adventure journey?
{/if}
</p>
</div>
</div>
</div>
<!-- Quick Action -->
<div class="flex flex-col sm:flex-row gap-3">
<a
href="/adventures"
class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300"
>
<Plus class="w-5 h-5" />
{$t('map.add_adventure')}
</a>
<a href="/worldtravel" class="btn btn-outline btn-lg gap-2">
<FlagCheckeredVariantIcon class="w-5 h-5" />
Explore World
</a>
</div>
</div>
</div>
<!-- Stats Grid -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6 mb-12">
<!-- Countries Visited -->
<div
class="stat-card card bg-gradient-to-br from-primary/10 to-primary/5 shadow-xl border border-primary/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div class="flex-1">
<div class="stat-title text-primary/70 font-medium">
{$t('dashboard.countries_visited')}
</div>
<div class="stat-value text-3xl font-bold text-primary">
{stats.visited_country_count}
</div>
<div class="stat-desc text-primary/60 mt-2">
<div class="flex items-center justify-between">
<span class="font-medium">{completionPercentage}% of world</span>
</div>
<progress
class="progress progress-primary w-full mt-1"
value={stats.visited_country_count}
max="195"
></progress>
</div>
</div>
<div class="p-4 bg-primary/20 rounded-2xl">
<FlagCheckeredVariantIcon class="w-8 h-8 text-primary" />
</div>
</div>
</div>
</div>
<!-- Regions Visited -->
<div
class="stat-card card bg-gradient-to-br from-success/10 to-success/5 shadow-xl border border-success/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="stat-title text-success/70 font-medium">
{$t('dashboard.total_visited_regions')}
</div>
<div class="stat-value text-3xl font-bold text-success">
{stats.visited_region_count}
</div>
<div class="stat-desc text-success/60 mt-2">
<div class="flex items-center gap-1">
<MapMarkerStarOutline class="w-4 h-4" />
Unique regions
</div>
</div>
</div>
<div class="p-4 bg-success/20 rounded-2xl">
<MapMarkerStarOutline class="w-8 h-8 text-success" />
</div>
</div>
</div>
</div>
<!-- Cities Visited -->
<div
class="stat-card card bg-gradient-to-br from-info/10 to-info/5 shadow-xl border border-info/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="stat-title text-info/70 font-medium">
{$t('dashboard.total_visited_cities')}
</div>
<div class="stat-value text-3xl font-bold text-info">{stats.visited_city_count}</div>
<div class="stat-desc text-info/60 mt-2">
<div class="flex items-center gap-1">
<CityVariantOutline class="w-4 h-4" />
Urban adventures
</div>
</div>
</div>
<div class="p-4 bg-info/20 rounded-2xl">
<CityVariantOutline class="w-8 h-8 text-info" />
</div>
</div>
</div>
</div>
</div>
<!-- Recent Adventures Section -->
{#if recentAdventures.length > 0}
<div class="mb-8">
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary/10 rounded-xl">
<CalendarClock class="w-6 h-6 text-primary" />
</div>
<div>
<h2 class="text-3xl font-bold">{$t('dashboard.recent_adventures')}</h2>
<p class="text-base-content/60">Your latest travel experiences</p>
</div>
</div>
<a href="/adventures" class="btn btn-ghost gap-2">
View All
<span class="badge badge-primary">{stats.adventure_count}</span>
</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{#each recentAdventures as adventure}
<div class="adventure-card">
<AdventureCard {adventure} user={data.user} readOnly />
</div>
{/each}
</div>
</div>
{/if}
<!-- Empty State / Inspiration -->
{#if recentAdventures.length === 0}
<div
class="empty-state card bg-gradient-to-br from-base-100 to-base-200 shadow-2xl border border-base-300"
>
<div class="card-body p-12 text-center">
<div class="flex justify-center mb-6">
<div class="p-6 bg-primary/10 rounded-3xl">
<Airplane class="w-16 h-16 text-primary" />
</div>
</div>
<h2
class="text-3xl font-bold mb-4 bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
>
{$t('dashboard.no_recent_adventures')}
</h2>
<p class="text-lg text-base-content/60 mb-8 max-w-md mx-auto leading-relaxed">
{$t('dashboard.add_some')} Start documenting your travels and build your personal adventure
map!
</p>
<div class="flex flex-col sm:flex-row gap-4 justify-center">
<a
href="/adventures"
class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300"
>
<Plus class="w-5 h-5" />
{$t('map.add_adventure')}
</a>
<a href="/worldtravel" class="btn btn-outline btn-lg gap-2">
<FlagCheckeredVariantIcon class="w-5 h-5" />
Explore World Map
</a>
</div>
<!-- Inspiration Cards -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-12 max-w-4xl mx-auto">
<div class="card bg-base-100/50 shadow-lg">
<div class="card-body p-6 text-center">
<div class="p-3 bg-primary/10 rounded-xl w-fit mx-auto mb-4">
<Airplane class="w-6 h-6 text-primary" />
</div>
<h3 class="font-semibold text-primary">Plan Adventures</h3>
<p class="text-sm text-base-content/60 mt-2">
Create and organize your travel plans
</p>
</div>
</div>
<div class="card bg-base-100/50 shadow-lg">
<div class="card-body p-6 text-center">
<div class="p-3 bg-secondary/10 rounded-xl w-fit mx-auto mb-4">
<FlagCheckeredVariantIcon class="w-6 h-6 text-secondary" />
</div>
<h3 class="font-semibold text-secondary">Track Countries</h3>
<p class="text-sm text-base-content/60 mt-2">Mark visited countries and regions</p>
</div>
</div>
<div class="card bg-base-100/50 shadow-lg">
<div class="card-body p-6 text-center">
<div class="p-3 bg-success/10 rounded-xl w-fit mx-auto mb-4">
<MapMarkerStarOutline class="w-6 h-6 text-success" />
</div>
<h3 class="font-semibold text-success">Share Memories</h3>
<p class="text-sm text-base-content/60 mt-2">Document and share your experiences</p>
</div>
</div>
</div>
</div>
</div>
{/if}
</div>
</div>