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

@@ -4,6 +4,20 @@
import CollectionCard from '$lib/components/CollectionCard.svelte';
import type { Adventure, Collection, User } from '$lib/types.js';
import { t } from 'svelte-i18n';
import { onMount } from 'svelte';
import { gsap } from 'gsap';
// Icons
import Calendar from '~icons/mdi/calendar';
import MapMarker from '~icons/mdi/map-marker';
import Airplane from '~icons/mdi/airplane';
import FlagCheckered from '~icons/mdi/flag-checkered-variant';
import CityVariant from '~icons/mdi/city-variant-outline';
import MapMarkerStar from '~icons/mdi/map-marker-star-outline';
import CollectionIcon from '~icons/mdi/folder-multiple';
import TrendingUp from '~icons/mdi/trending-up';
import Share from '~icons/mdi/share-variant';
import Award from '~icons/mdi/award';
let stats: {
visited_country_count: number;
@@ -20,165 +34,377 @@
const adventures: Adventure[] = data.adventures;
const collections: Collection[] = data.collections;
stats = data.stats || null;
// Calculate achievements
$: worldExplorationPercentage = stats
? Math.round((stats.visited_country_count / stats.total_countries) * 100)
: 0;
$: regionExplorationPercentage = stats
? Math.round((stats.visited_region_count / stats.total_regions) * 100)
: 0;
$: cityExplorationPercentage = stats
? Math.round((stats.visited_city_count / stats.total_cities) * 100)
: 0;
// Achievement levels
$: achievementLevel =
(stats?.adventure_count ?? 0) >= 50
? 'Explorer Master'
: (stats?.adventure_count ?? 0) >= 25
? 'Seasoned Traveler'
: (stats?.adventure_count ?? 0) >= 10
? 'Adventure Seeker'
: (stats?.adventure_count ?? 0) >= 5
? 'Journey Starter'
: 'Travel Enthusiast';
$: achievementColor =
(stats?.adventure_count ?? 0) >= 50
? 'text-warning'
: (stats?.adventure_count ?? 0) >= 25
? 'text-success'
: (stats?.adventure_count ?? 0) >= 10
? 'text-info'
: (stats?.adventure_count ?? 0) >= 5
? 'text-secondary'
: 'text-primary';
</script>
<section class="min-h-screen bg-base-100 py-8 px-4">
<div class="flex flex-col items-center">
<!-- Profile Picture -->
{#if user.profile_pic}
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
<img src={user.profile_pic} alt="Profile" />
</div>
</div>
{:else}
<!-- show first last initial -->
<div class="avatar">
<div
class="w-24 rounded-full ring ring-primary ring-offset-base-100 ring-offset-2 shadow-md"
>
{#if user.first_name && user.last_name}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.first_name}+${user.last_name}&size=250`}
alt="Profile"
/>
{:else}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.username}&size=250`}
alt="Profile"
/>
{/if}
</div>
</div>
{/if}
<!-- User Name -->
{#if user && user.first_name && user.last_name}
<h1 class="text-4xl font-bold text-primary mt-4">
{user.first_name}
{user.last_name}
</h1>
{/if}
<p class="text-lg text-base-content mt-2">{user.username}</p>
<!-- Member Since -->
{#if user && user.date_joined}
<div class="mt-4 flex items-center text-center text-base-content">
<p class="text-lg font-medium">{$t('profile.member_since')}</p>
<div class="flex items-center ml-2">
<iconify-icon icon="mdi:calendar" class="text-2xl text-primary"></iconify-icon>
<p class="ml-2 text-lg">
{new Date(user.date_joined).toLocaleDateString(undefined, { timeZone: 'UTC' })}
</p>
</div>
</div>
{/if}
</div>
<!-- Stats Section -->
{#if stats}
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('profile.user_stats')}
</h2>
<div class="flex justify-center">
<div class="stats stats-vertical lg:stats-horizontal shadow bg-base-200">
<div class="stat">
<div class="stat-title">{$t('navbar.adventures')}</div>
<div class="stat-value text-center">{stats.adventure_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('navbar.collections')}</div>
<div class="stat-value text-center">{stats.trips_count}</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_countries')}</div>
<div class="stat-value text-center">
{stats.visited_country_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_country_count / stats.total_countries) * 100)}% {$t(
'adventures.of'
)}
{stats.total_countries}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_regions')}</div>
<div class="stat-value text-center">
{stats.visited_region_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_region_count / stats.total_regions) * 100)}% {$t(
'adventures.of'
)}
{stats.total_regions}
</div>
</div>
<div class="stat">
<div class="stat-title">{$t('profile.visited_cities')}</div>
<div class="stat-value text-center">
{stats.visited_city_count}
</div>
<div class="stat-desc text-center">
{Math.round((stats.visited_city_count / stats.total_cities) * 100)}% {$t(
'adventures.of'
)}
{stats.total_cities}
</div>
</div>
</div>
</div>
{/if}
<!-- Adventures Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_adventures')}
</h2>
{#if adventures && adventures.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_adventures')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each adventures as adventure}
<AdventureCard {adventure} user={null} />
{/each}
</div>
{/if}
<!-- Collections Section -->
<div class="divider my-8"></div>
<h2 class="text-2xl font-bold text-center mb-6 text-primary">
{$t('auth.user_collections')}
</h2>
{#if collections && collections.length === 0}
<p class="text-lg text-center text-base-content">
{$t('auth.no_public_collections')}
</p>
{:else}
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
{#each collections as collection}
<CollectionCard {collection} type={''} />
{/each}
</div>
{/if}
</section>
<svelte:head>
<title>{user.first_name || user.username}'s Profile | AdventureLog</title>
<meta name="description" content="User Profile" />
</svelte:head>
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
<!-- Hero Profile Section -->
<div class="relative overflow-hidden">
<!-- Background Pattern -->
<div class="absolute inset-0 bg-gradient-to-r from-primary/5 via-secondary/5 to-accent/5"></div>
<div class="container mx-auto px-6 py-16 relative">
<div class="profile-header flex flex-col items-center text-center">
<!-- Profile Picture with Enhanced Styling -->
<div class="relative mb-6">
{#if user.profile_pic}
<div class="avatar">
<div
class="w-32 h-32 rounded-full ring-4 ring-primary ring-offset-4 ring-offset-base-100 shadow-2xl"
>
<img src={user.profile_pic} alt="Profile" />
</div>
</div>
{:else}
<div class="avatar">
<div
class="w-32 h-32 rounded-full ring-4 ring-primary ring-offset-4 ring-offset-base-100 shadow-2xl bg-gradient-to-br from-primary to-secondary"
>
{#if user.first_name && user.last_name}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.first_name}+${user.last_name}&size=250&background=random`}
alt="Profile"
/>
{:else}
<img
src={`https://eu.ui-avatars.com/api/?name=${user.username}&size=250&background=random`}
alt="Profile"
/>
{/if}
</div>
</div>
{/if}
</div>
<!-- User Info -->
<div class="space-y-4">
{#if user && user.first_name && user.last_name}
<h1
class="text-5xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent"
>
{user.first_name}
{user.last_name}
</h1>
{:else}
<h1
class="text-5xl font-bold bg-gradient-to-r from-primary via-secondary to-accent bg-clip-text text-transparent"
>
{user.username}
</h1>
{/if}
<p class="text-xl text-base-content/70">@{user.username}</p>
<!-- Member Since -->
{#if user && user.date_joined}
<div class="flex items-center justify-center gap-2 text-base-content/60">
<Calendar class="w-5 h-5" />
<span class="text-lg">
{$t('profile.member_since')}
{new Date(user.date_joined).toLocaleDateString(undefined, {
timeZone: 'UTC',
year: 'numeric',
month: 'long'
})}
</span>
</div>
{/if}
<!-- User rank achievement -->
{#if stats && stats.adventure_count > 0}
<div class="flex items-center justify-center gap-2 text-base-content/70">
<Award class="w-5 h-5" />
<span class={`text-lg ${achievementColor}`}>{achievementLevel}</span>
</div>
{/if}
<!-- Quick Actions -->
<div class="flex gap-3 mt-6">
<button class="btn btn-primary gap-2">
<Share class="w-4 h-4" />
Share Profile
</button>
</div>
</div>
</div>
</div>
</div>
<div class="container mx-auto px-6 py-8">
<!-- Enhanced Stats Section -->
{#if stats}
<div class="content-section mb-16">
<div class="text-center mb-8">
<h2 class="text-3xl font-bold mb-2">Travel Statistics</h2>
<p class="text-base-content/60">Your adventure journey at a glance</p>
</div>
<!-- Primary Stats Grid -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
<!-- Adventures -->
<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>
<div class="text-primary/70 font-medium text-sm uppercase tracking-wide">
{$t('navbar.adventures')}
</div>
<div class="text-4xl font-bold text-primary">{stats.adventure_count}</div>
<div class="text-primary/60 mt-2 flex items-center gap-1">
<TrendingUp class="w-4 h-4" />
{achievementLevel}
</div>
</div>
<div class="p-4 bg-primary/20 rounded-2xl">
<Airplane class="w-8 h-8 text-primary" />
</div>
</div>
</div>
</div>
<!-- Collections -->
<div
class="stat-card card bg-gradient-to-br from-secondary/10 to-secondary/5 shadow-xl border border-secondary/20 hover:shadow-2xl transition-all duration-300"
>
<div class="card-body p-6">
<div class="flex items-center justify-between">
<div>
<div class="text-secondary/70 font-medium text-sm uppercase tracking-wide">
{$t('navbar.collections')}
</div>
<div class="text-4xl font-bold text-secondary">{stats.trips_count}</div>
<div class="text-secondary/60 mt-2">Curated trips</div>
</div>
<div class="p-4 bg-secondary/20 rounded-2xl">
<CollectionIcon class="w-8 h-8 text-secondary" />
</div>
</div>
</div>
</div>
<!-- Countries -->
<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 class="flex-1">
<div class="text-success/70 font-medium text-sm uppercase tracking-wide">
{$t('profile.visited_countries')}
</div>
<div class="text-4xl font-bold text-success">{stats.visited_country_count}</div>
<div class="text-success/60 mt-2">
<div class="flex items-center justify-between mb-1">
<span>{worldExplorationPercentage}% of world</span>
<span class="text-xs"
>{stats.visited_country_count}/{stats.total_countries}</span
>
</div>
<progress
class="progress progress-success w-full h-2"
value={stats.visited_country_count}
max={stats.total_countries}
></progress>
</div>
</div>
<div class="p-4 bg-success/20 rounded-2xl">
<FlagCheckered class="w-8 h-8 text-success" />
</div>
</div>
</div>
</div>
</div>
<!-- Secondary Stats -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- Regions -->
<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 class="flex-1">
<div class="text-info/70 font-medium text-sm uppercase tracking-wide">
{$t('profile.visited_regions')}
</div>
<div class="text-3xl font-bold text-info">{stats.visited_region_count}</div>
<div class="text-info/60 mt-2">
<div class="flex items-center justify-between mb-1">
<span>{regionExplorationPercentage}% explored</span>
<span class="text-xs">{stats.visited_region_count}/{stats.total_regions}</span
>
</div>
<progress
class="progress progress-info w-full h-2"
value={stats.visited_region_count}
max={stats.total_regions}
></progress>
</div>
</div>
<div class="p-4 bg-info/20 rounded-2xl">
<MapMarkerStar class="w-8 h-8 text-info" />
</div>
</div>
</div>
</div>
<!-- Cities -->
<div
class="stat-card card bg-gradient-to-br from-warning/10 to-warning/5 shadow-xl border border-warning/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="text-warning/70 font-medium text-sm uppercase tracking-wide">
{$t('profile.visited_cities')}
</div>
<div class="text-3xl font-bold text-warning">{stats.visited_city_count}</div>
<div class="text-warning/60 mt-2">
<div class="flex items-center justify-between mb-1">
<span>{cityExplorationPercentage}% discovered</span>
<span class="text-xs">{stats.visited_city_count}/{stats.total_cities}</span>
</div>
<progress
class="progress progress-warning w-full h-2"
value={stats.visited_city_count}
max={stats.total_cities}
></progress>
</div>
</div>
<div class="p-4 bg-warning/20 rounded-2xl">
<CityVariant class="w-8 h-8 text-warning" />
</div>
</div>
</div>
</div>
</div>
</div>
{/if}
<!-- Adventures Section -->
<div class="content-section mb-16">
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary/10 rounded-xl">
<Airplane class="w-6 h-6 text-primary" />
</div>
<div>
<h2 class="text-3xl font-bold">{$t('auth.user_adventures')}</h2>
<p class="text-base-content/60">Public adventure experiences</p>
</div>
</div>
{#if adventures && adventures.length > 0}
<div class="badge badge-primary badge-lg">
{adventures.length}
{adventures.length === 1 ? 'Adventure' : 'Adventures'}
</div>
{/if}
</div>
{#if adventures && adventures.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body text-center py-16">
<div class="p-6 bg-base-200/50 rounded-2xl w-fit mx-auto mb-6">
<Airplane class="w-16 h-16 text-base-content/30" />
</div>
<h3 class="text-xl font-bold text-base-content/70 mb-2">
{$t('auth.no_public_adventures')}
</h3>
<p class="text-base-content/50">This user hasn't shared any public adventures yet.</p>
</div>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{#each adventures as adventure}
<div class="adventure-card">
<AdventureCard {adventure} user={null} />
</div>
{/each}
</div>
{/if}
</div>
<!-- Collections Section -->
<div class="content-section">
<div class="flex items-center justify-between mb-8">
<div class="flex items-center gap-3">
<div class="p-2 bg-secondary/10 rounded-xl">
<CollectionIcon class="w-6 h-6 text-secondary" />
</div>
<div>
<h2 class="text-3xl font-bold">{$t('auth.user_collections')}</h2>
<p class="text-base-content/60">Curated travel collections</p>
</div>
</div>
{#if collections && collections.length > 0}
<div class="badge badge-secondary badge-lg">
{collections.length}
{collections.length === 1 ? 'Collection' : 'Collections'}
</div>
{/if}
</div>
{#if collections && collections.length === 0}
<div class="card bg-base-100 shadow-xl">
<div class="card-body text-center py-16">
<div class="p-6 bg-base-200/50 rounded-2xl w-fit mx-auto mb-6">
<CollectionIcon class="w-16 h-16 text-base-content/30" />
</div>
<h3 class="text-xl font-bold text-base-content/70 mb-2">
{$t('auth.no_public_collections')}
</h3>
<p class="text-base-content/50">This user hasn't shared any public collections yet.</p>
</div>
</div>
{:else}
<div class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-6">
{#each collections as collection}
<div class="collection-card">
<CollectionCard {collection} type={''} user={data.user} />
</div>
{/each}
</div>
{/if}
</div>
</div>
</div>