mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-24 15:29:36 +02:00
refactor: remove archived collections page and related components; enhance world travel pages with improved UI and filtering options
This commit is contained in:
parent
14eb4ca802
commit
a99553ba0d
9 changed files with 797 additions and 361 deletions
|
@ -129,12 +129,9 @@ class CategoryAdmin(admin.ModelAdmin):
|
||||||
search_fields = ('name', 'display_name')
|
search_fields = ('name', 'display_name')
|
||||||
|
|
||||||
class CollectionAdmin(admin.ModelAdmin):
|
class CollectionAdmin(admin.ModelAdmin):
|
||||||
def adventure_count(self, obj):
|
|
||||||
return obj.adventure_set.count()
|
|
||||||
|
|
||||||
adventure_count.short_description = 'Adventure Count'
|
list_display = ('name', 'user_id', 'is_public')
|
||||||
|
|
||||||
list_display = ('name', 'user_id', 'adventure_count', 'is_public')
|
|
||||||
|
|
||||||
admin.site.register(CustomUser, CustomUserAdmin)
|
admin.site.register(CustomUser, CustomUserAdmin)
|
||||||
|
|
||||||
|
|
|
@ -37,25 +37,34 @@
|
||||||
|
|
||||||
<div class="collapse collapse-plus mb-4">
|
<div class="collapse collapse-plus mb-4">
|
||||||
<input type="checkbox" />
|
<input type="checkbox" />
|
||||||
|
|
||||||
<div class="collapse-title text-xl bg-base-300 font-medium">
|
<div class="collapse-title text-xl bg-base-300 font-medium">
|
||||||
{$t('adventures.category_filter')}
|
{$t('adventures.category_filter')}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="collapse-content bg-base-300">
|
<div class="collapse-content bg-base-300">
|
||||||
<button class="btn btn-wide btn-neutral-300" on:click={clearTypes}
|
<button class="btn btn-sm btn-neutral-300 w-full mb-2" on:click={clearTypes}>
|
||||||
>{$t(`adventures.clear`)}</button
|
{$t('adventures.clear')}
|
||||||
>
|
</button>
|
||||||
{#each adventure_types as type}
|
|
||||||
<li>
|
<ul>
|
||||||
<label class="cursor-pointer">
|
{#each adventure_types as type}
|
||||||
<input
|
<li class="mb-1">
|
||||||
type="checkbox"
|
<label class="cursor-pointer flex items-center gap-2">
|
||||||
value={type.name}
|
<input
|
||||||
on:change={() => toggleSelect(type.name)}
|
type="checkbox"
|
||||||
checked={types.indexOf(type.name) > -1}
|
class="checkbox"
|
||||||
/>
|
value={type.name}
|
||||||
<span>{type.display_name + ' ' + type.icon + ` (${type.num_adventures})`}</span>
|
on:change={() => toggleSelect(type.name)}
|
||||||
</label>
|
checked={types.indexOf(type.name) > -1}
|
||||||
</li>
|
/>
|
||||||
{/each}
|
<span>
|
||||||
|
{type.display_name}
|
||||||
|
{type.icon} ({type.num_adventures})
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -44,10 +44,11 @@
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
if (is_archived) {
|
if (is_archived) {
|
||||||
addToast('info', $t('adventures.archived_collection_message'));
|
addToast('info', $t('adventures.archived_collection_message'));
|
||||||
|
dispatch('archive', collection.id);
|
||||||
} else {
|
} else {
|
||||||
addToast('info', $t('adventures.unarchived_collection_message'));
|
addToast('info', $t('adventures.unarchived_collection_message'));
|
||||||
|
dispatch('unarchive', collection.id);
|
||||||
}
|
}
|
||||||
dispatch('delete', collection.id);
|
|
||||||
} else {
|
} else {
|
||||||
console.log('Error archiving collection');
|
console.log('Error archiving collection');
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@
|
||||||
on:link={link}
|
on:link={link}
|
||||||
bind:linkedCollectionList
|
bind:linkedCollectionList
|
||||||
on:unlink={unlink}
|
on:unlink={unlink}
|
||||||
|
user={null}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
{#if collections.length === 0}
|
{#if collections.length === 0}
|
||||||
|
|
|
@ -63,28 +63,32 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reactive statements to update collections based on props
|
async function goToPage(pageNum: number) {
|
||||||
$: {
|
const url = new URL($page.url);
|
||||||
|
url.searchParams.set('page', pageNum.toString());
|
||||||
|
await goto(url.toString(), { invalidateAll: true, replaceState: true });
|
||||||
if (data.props.adventures) {
|
if (data.props.adventures) {
|
||||||
collections = data.props.adventures;
|
collections = data.props.adventures;
|
||||||
}
|
}
|
||||||
if (data.props.archivedCollections) {
|
if (data.props.archivedCollections) {
|
||||||
archivedCollections = data.props.archivedCollections;
|
archivedCollections = data.props.archivedCollections;
|
||||||
}
|
}
|
||||||
|
currentPage = pageNum;
|
||||||
}
|
}
|
||||||
|
|
||||||
function goToPage(pageNum: number) {
|
async function updateSort(by: string, direction: string) {
|
||||||
const url = new URL($page.url);
|
|
||||||
url.searchParams.set('page', pageNum.toString());
|
|
||||||
goto(url.toString(), { invalidateAll: true, replaceState: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSort(by: string, direction: string) {
|
|
||||||
const url = new URL($page.url);
|
const url = new URL($page.url);
|
||||||
url.searchParams.set('order_by', by);
|
url.searchParams.set('order_by', by);
|
||||||
url.searchParams.set('order_direction', direction);
|
url.searchParams.set('order_direction', direction);
|
||||||
url.searchParams.set('page', '1'); // Reset to first page when sorting changes
|
url.searchParams.set('page', '1'); // Reset to first page when sorting changes
|
||||||
goto(url.toString(), { invalidateAll: true, replaceState: true });
|
currentPage = 1;
|
||||||
|
await goto(url.toString(), { invalidateAll: true, replaceState: true });
|
||||||
|
if (data.props.adventures) {
|
||||||
|
collections = data.props.adventures;
|
||||||
|
}
|
||||||
|
if (data.props.archivedCollections) {
|
||||||
|
archivedCollections = data.props.archivedCollections;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteCollection(event: CustomEvent<string>) {
|
function deleteCollection(event: CustomEvent<string>) {
|
||||||
|
@ -98,6 +102,7 @@
|
||||||
|
|
||||||
function archiveCollection(event: CustomEvent<string>) {
|
function archiveCollection(event: CustomEvent<string>) {
|
||||||
const collectionId = event.detail;
|
const collectionId = event.detail;
|
||||||
|
console.log('Archiving collection with ID:', collectionId);
|
||||||
// Find the collection in owned collections
|
// Find the collection in owned collections
|
||||||
const collectionToArchive = collections.find((collection) => collection.id === collectionId);
|
const collectionToArchive = collections.find((collection) => collection.id === collectionId);
|
||||||
|
|
||||||
|
@ -106,9 +111,6 @@
|
||||||
collections = collections.filter((collection) => collection.id !== collectionId);
|
collections = collections.filter((collection) => collection.id !== collectionId);
|
||||||
// Add to archived collections
|
// Add to archived collections
|
||||||
archivedCollections = [...archivedCollections, { ...collectionToArchive, is_archived: true }];
|
archivedCollections = [...archivedCollections, { ...collectionToArchive, is_archived: true }];
|
||||||
|
|
||||||
// Automatically switch to archived tab to show the archived item
|
|
||||||
activeView = 'archived';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -126,9 +128,6 @@
|
||||||
);
|
);
|
||||||
// Add to owned collections
|
// Add to owned collections
|
||||||
collections = [...collections, { ...collectionToUnarchive, is_archived: false }];
|
collections = [...collections, { ...collectionToUnarchive, is_archived: false }];
|
||||||
|
|
||||||
// Automatically switch to owned tab to show the unarchived item
|
|
||||||
activeView = 'owned';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
import { redirect } from '@sveltejs/kit';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
|
||||||
import type { Adventure } from '$lib/types';
|
|
||||||
const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|
||||||
|
|
||||||
export const load = (async (event) => {
|
|
||||||
if (!event.locals.user) {
|
|
||||||
return redirect(302, '/login');
|
|
||||||
} else {
|
|
||||||
let sessionId = event.cookies.get('sessionid');
|
|
||||||
let adventures: Adventure[] = [];
|
|
||||||
let initialFetch = await fetch(`${serverEndpoint}/api/collections/archived/`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!initialFetch.ok) {
|
|
||||||
console.error('Failed to fetch visited adventures');
|
|
||||||
return redirect(302, '/login');
|
|
||||||
} else {
|
|
||||||
let res = await initialFetch.json();
|
|
||||||
let visited = res as Adventure[];
|
|
||||||
adventures = [...adventures, ...visited];
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
adventures
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}) satisfies PageServerLoad;
|
|
|
@ -1,37 +0,0 @@
|
||||||
<script lang="ts">
|
|
||||||
import CollectionCard from '$lib/components/CollectionCard.svelte';
|
|
||||||
import NotFound from '$lib/components/NotFound.svelte';
|
|
||||||
import type { Collection } from '$lib/types';
|
|
||||||
import { t } from 'svelte-i18n';
|
|
||||||
|
|
||||||
export let data: any;
|
|
||||||
console.log(data);
|
|
||||||
|
|
||||||
let collections: Collection[] = data.props.adventures || [];
|
|
||||||
|
|
||||||
function deleteCollection(event: CustomEvent<string>) {
|
|
||||||
collections = collections.filter((collection) => collection.id !== event.detail);
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="drawer lg:drawer-open">
|
|
||||||
<div class="drawer-content">
|
|
||||||
<!-- Page content -->
|
|
||||||
<h1 class="text-center font-bold text-4xl mb-6">{$t('adventures.archived_collections')}</h1>
|
|
||||||
{#if collections.length === 0}
|
|
||||||
<NotFound error={undefined} />
|
|
||||||
{/if}
|
|
||||||
<div class="p-4">
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 justify-center content-center">
|
|
||||||
{#each collections as collection}
|
|
||||||
<CollectionCard type="" {collection} on:delete={deleteCollection} />
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svelte:head>
|
|
||||||
<title>Collections</title>
|
|
||||||
<meta name="description" content="View your adventure collections." />
|
|
||||||
</svelte:head>
|
|
|
@ -6,33 +6,54 @@
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { MapLibre, Marker } from 'svelte-maplibre';
|
import { MapLibre, Marker } from 'svelte-maplibre';
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import Globe from '~icons/mdi/earth';
|
||||||
|
import Search from '~icons/mdi/magnify';
|
||||||
|
import Clear from '~icons/mdi/close';
|
||||||
|
import Filter from '~icons/mdi/filter-variant';
|
||||||
|
import Map from '~icons/mdi/map';
|
||||||
|
import Pin from '~icons/mdi/map-marker';
|
||||||
|
import Check from '~icons/mdi/check-circle';
|
||||||
|
import Progress from '~icons/mdi/progress-check';
|
||||||
|
import Cancel from '~icons/mdi/cancel';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
let searchQuery: string = '';
|
let searchQuery: string = '';
|
||||||
|
|
||||||
let filteredCountries: Country[] = [];
|
let filteredCountries: Country[] = [];
|
||||||
const allCountries: Country[] = data.props?.countries || [];
|
const allCountries: Country[] = data.props?.countries || [];
|
||||||
let worldSubregions: string[] = [];
|
let worldSubregions: string[] = [];
|
||||||
let showMap: boolean = false;
|
let showMap: boolean = false;
|
||||||
|
let sidebarOpen = false;
|
||||||
|
|
||||||
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
||||||
// remove blank subregions
|
|
||||||
worldSubregions = worldSubregions.filter((subregion) => subregion !== '');
|
worldSubregions = worldSubregions.filter((subregion) => subregion !== '');
|
||||||
console.log(worldSubregions);
|
console.log(worldSubregions);
|
||||||
|
|
||||||
let filterOption: string = 'all';
|
let filterOption: string = 'all';
|
||||||
let subRegionOption: string = '';
|
let subRegionOption: string = '';
|
||||||
|
|
||||||
|
// Statistics
|
||||||
|
$: totalCountries = allCountries.length;
|
||||||
|
$: visitedCountries = allCountries.filter((country) => country.num_visits > 0).length;
|
||||||
|
$: completeCountries = allCountries.filter(
|
||||||
|
(country) => country.num_visits === country.num_regions
|
||||||
|
).length;
|
||||||
|
$: partialCountries = allCountries.filter(
|
||||||
|
(country) => country.num_visits > 0 && country.num_visits < country.num_regions
|
||||||
|
).length;
|
||||||
|
$: notVisitedCountries = allCountries.filter((country) => country.num_visits === 0).length;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (searchQuery === '') {
|
if (searchQuery === '') {
|
||||||
filteredCountries = allCountries;
|
filteredCountries = allCountries;
|
||||||
} else {
|
} else {
|
||||||
// otherwise, filter countries by name
|
|
||||||
filteredCountries = allCountries.filter((country) =>
|
filteredCountries = allCountries.filter((country) =>
|
||||||
country.name.toLowerCase().includes(searchQuery.toLowerCase())
|
country.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterOption === 'partial') {
|
if (filterOption === 'partial') {
|
||||||
filteredCountries = filteredCountries.filter(
|
filteredCountries = filteredCountries.filter(
|
||||||
(country) => country.num_visits > 0 && country.num_visits < country.num_regions
|
(country) => country.num_visits > 0 && country.num_visits < country.num_regions
|
||||||
|
@ -43,8 +64,6 @@
|
||||||
);
|
);
|
||||||
} else if (filterOption === 'not') {
|
} else if (filterOption === 'not') {
|
||||||
filteredCountries = filteredCountries.filter((country) => country.num_visits === 0);
|
filteredCountries = filteredCountries.filter((country) => country.num_visits === 0);
|
||||||
} else {
|
|
||||||
filteredCountries = filteredCountries;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (subRegionOption !== '') {
|
if (subRegionOption !== '') {
|
||||||
|
@ -53,137 +72,360 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
sidebarOpen = !sidebarOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
searchQuery = '';
|
||||||
|
filterOption = 'all';
|
||||||
|
subRegionOption = '';
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-center font-bold text-4xl">{$t('worldtravel.country_list')}</h1>
|
|
||||||
<!-- result count -->
|
|
||||||
<p class="text-center mb-4">
|
|
||||||
{filteredCountries.length}
|
|
||||||
{$t('worldtravel.num_countries')}
|
|
||||||
</p>
|
|
||||||
<div class="flex items-center justify-center mb-4">
|
|
||||||
<div class="join">
|
|
||||||
<input
|
|
||||||
class="join-item btn"
|
|
||||||
type="radio"
|
|
||||||
name="filter"
|
|
||||||
aria-label={$t('worldtravel.all')}
|
|
||||||
checked
|
|
||||||
on:click={() => (filterOption = 'all')}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="join-item btn"
|
|
||||||
type="radio"
|
|
||||||
name="filter"
|
|
||||||
aria-label={$t('worldtravel.partially_visited')}
|
|
||||||
on:click={() => (filterOption = 'partial')}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="join-item btn"
|
|
||||||
type="radio"
|
|
||||||
name="filter"
|
|
||||||
aria-label={$t('worldtravel.completely_visited')}
|
|
||||||
on:click={() => (filterOption = 'complete')}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
class="join-item btn"
|
|
||||||
type="radio"
|
|
||||||
name="filter"
|
|
||||||
aria-label={$t('worldtravel.not_visited')}
|
|
||||||
on:click={() => (filterOption = 'not')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<select class="select select-bordered w-full max-w-xs ml-4" bind:value={subRegionOption}>
|
|
||||||
<option value="">{$t('worldtravel.all_subregions')}</option>
|
|
||||||
{#each worldSubregions as subregion}
|
|
||||||
<option value={subregion}>{subregion}</option>
|
|
||||||
{/each}
|
|
||||||
</select>
|
|
||||||
<!-- borderd checkbox -->
|
|
||||||
<div class="flex items-center justify-center ml-4">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
class="checkbox checkbox-bordered"
|
|
||||||
bind:checked={showMap}
|
|
||||||
aria-label={$t('adventures.show_map')}
|
|
||||||
/>
|
|
||||||
<span class="ml-2">{$t('adventures.show_map')}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center mb-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={$t('navbar.search')}
|
|
||||||
class="input input-bordered w-full max-w-xs"
|
|
||||||
bind:value={searchQuery}
|
|
||||||
/>
|
|
||||||
{#if searchQuery.length > 0}
|
|
||||||
<!-- clear button -->
|
|
||||||
<div class="flex items-center justify-center ml-4">
|
|
||||||
<button class="btn btn-neutral" on:click={() => (searchQuery = '')}>
|
|
||||||
{$t('worldtravel.clear_search')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if showMap}
|
|
||||||
<div class="mt-4 mb-4 flex justify-center">
|
|
||||||
<!-- checkbox to toggle marker -->
|
|
||||||
|
|
||||||
<MapLibre
|
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
|
||||||
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
|
||||||
standardControls
|
|
||||||
zoom={2}
|
|
||||||
>
|
|
||||||
{#each filteredCountries as country}
|
|
||||||
{#if country.latitude && country.longitude}
|
|
||||||
<Marker
|
|
||||||
lngLat={[country.longitude, country.latitude]}
|
|
||||||
class={`grid px-2 py-1 place-items-center rounded-full border border-gray-200 ${
|
|
||||||
country.num_visits === 0
|
|
||||||
? 'bg-red-200'
|
|
||||||
: country.num_visits === country.num_regions
|
|
||||||
? 'bg-green-200'
|
|
||||||
: 'bg-blue-200'
|
|
||||||
} text-black focus:outline-6 focus:outline-black`}
|
|
||||||
on:click={() => goto(`/worldtravel/${country.country_code}`)}
|
|
||||||
>
|
|
||||||
<span class="text-xs">
|
|
||||||
{country.name}
|
|
||||||
</span>
|
|
||||||
</Marker>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</MapLibre>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
|
||||||
{#each filteredCountries as country}
|
|
||||||
<CountryCard {country} />
|
|
||||||
<!-- <p>Name: {item.name}, Continent: {item.continent}</p> -->
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#if filteredCountries.length === 0}
|
|
||||||
<p class="text-center font-bold text-2xl mt-12">{$t('worldtravel.no_countries_found')}</p>
|
|
||||||
|
|
||||||
<!-- If there are no allCountries then likely the download country command has not run on the server -->
|
|
||||||
{#if allCountries.length === 0}
|
|
||||||
<div class="text-center mt-4">
|
|
||||||
<a
|
|
||||||
class="link link-primary"
|
|
||||||
href="https://adventurelog.app/docs/configuration/updating.html#updating-the-region-data"
|
|
||||||
target="_blank">{$t('settings.documentation_link')}</a
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Countries | World Travel</title>
|
<title>Countries | World Travel</title>
|
||||||
<meta name="description" content="Explore the world and add countries to your visited list!" />
|
<meta name="description" content="Explore the world and add countries to your visited list!" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
|
||||||
|
<div class="drawer lg:drawer-open">
|
||||||
|
<input id="travel-drawer" type="checkbox" class="drawer-toggle" bind:checked={sidebarOpen} />
|
||||||
|
|
||||||
|
<div class="drawer-content">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="sticky top-0 z-40 bg-base-100/80 backdrop-blur-lg border-b border-base-300">
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn btn-ghost btn-square lg:hidden" on:click={toggleSidebar}>
|
||||||
|
<Filter class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-xl">
|
||||||
|
<Globe class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
class="text-3xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
{$t('worldtravel.country_list')}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{filteredCountries.length} of {totalCountries} countries
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Stats -->
|
||||||
|
<div class="hidden md:flex items-center gap-2">
|
||||||
|
<div class="stats stats-horizontal bg-base-100 shadow-lg">
|
||||||
|
<div class="stat py-2 px-4">
|
||||||
|
<div class="stat-title text-xs">Visited</div>
|
||||||
|
<div class="stat-value text-lg text-success">{visitedCountries}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat py-2 px-4">
|
||||||
|
<div class="stat-title text-xs">Remaining</div>
|
||||||
|
<div class="stat-value text-lg text-error">{notVisitedCountries}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search Bar -->
|
||||||
|
<div class="mt-4 flex items-center gap-4">
|
||||||
|
<div class="relative flex-1 max-w-md">
|
||||||
|
<Search
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/40"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={$t('navbar.search')}
|
||||||
|
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
/>
|
||||||
|
{#if searchQuery.length > 0}
|
||||||
|
<button
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content"
|
||||||
|
on:click={() => (searchQuery = '')}
|
||||||
|
>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Toggle -->
|
||||||
|
<button
|
||||||
|
class="btn btn-outline gap-2 {showMap ? 'btn-active' : ''}"
|
||||||
|
on:click={() => (showMap = !showMap)}
|
||||||
|
>
|
||||||
|
{#if showMap}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">Hide Map</span>
|
||||||
|
{:else}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">Show Map</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Chips -->
|
||||||
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-base-content/60">Filter by:</span>
|
||||||
|
<div class="tabs tabs-boxed bg-base-200">
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'all' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'all')}
|
||||||
|
>
|
||||||
|
<Globe class="w-3 h-3" />
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'complete' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'complete')}
|
||||||
|
>
|
||||||
|
<Check class="w-3 h-3" />
|
||||||
|
Complete
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'partial' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'partial')}
|
||||||
|
>
|
||||||
|
<Progress class="w-3 h-3" />
|
||||||
|
Partial
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'not' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'not')}
|
||||||
|
>
|
||||||
|
<Cancel class="w-3 h-3" />
|
||||||
|
Not Visited
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if subRegionOption}
|
||||||
|
<div class="badge badge-primary gap-1">
|
||||||
|
{subRegionOption}
|
||||||
|
<button on:click={() => (subRegionOption = '')}>
|
||||||
|
<Clear class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if searchQuery || filterOption !== 'all' || subRegionOption}
|
||||||
|
<button class="btn btn-ghost btn-xs gap-1" on:click={clearFilters}>
|
||||||
|
<Clear class="w-3 h-3" />
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Section -->
|
||||||
|
{#if showMap}
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
|
class="aspect-[16/10] w-full rounded-lg"
|
||||||
|
standardControls
|
||||||
|
zoom={2}
|
||||||
|
>
|
||||||
|
{#each filteredCountries as country}
|
||||||
|
{#if country.latitude && country.longitude}
|
||||||
|
<Marker
|
||||||
|
lngLat={[country.longitude, country.latitude]}
|
||||||
|
class={`grid px-2 py-1 place-items-center rounded-full border border-gray-200 ${
|
||||||
|
country.num_visits === 0
|
||||||
|
? 'bg-red-200'
|
||||||
|
: country.num_visits === country.num_regions
|
||||||
|
? 'bg-green-200'
|
||||||
|
: 'bg-blue-200'
|
||||||
|
} text-black focus:outline-6 focus:outline-black hover:scale-110 transition-transform cursor-pointer`}
|
||||||
|
on:click={() => goto(`/worldtravel/${country.country_code}`)}
|
||||||
|
>
|
||||||
|
<span class="text-xs font-medium">
|
||||||
|
{country.name}
|
||||||
|
</span>
|
||||||
|
</Marker>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mx-auto px-6 py-8">
|
||||||
|
{#if filteredCountries.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
|
<div class="p-6 bg-base-200/50 rounded-2xl mb-6">
|
||||||
|
<Globe class="w-16 h-16 text-base-content/30" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||||
|
{$t('worldtravel.no_countries_found')}
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/50 text-center max-w-md mb-6">
|
||||||
|
Try adjusting your search terms or filters to find the countries you're looking for.
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary gap-2" on:click={clearFilters}>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{#if allCountries.length === 0}
|
||||||
|
<div class="mt-8 text-center">
|
||||||
|
<div class="alert alert-warning max-w-md">
|
||||||
|
<div>
|
||||||
|
<h4 class="font-bold">No country data available</h4>
|
||||||
|
<p class="text-sm">Please check the documentation for updating region data.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="link link-primary mt-4 inline-block"
|
||||||
|
href="https://adventurelog.app/docs/configuration/updating.html#updating-the-region-data"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{$t('settings.documentation_link')}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Countries Grid -->
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"
|
||||||
|
>
|
||||||
|
{#each filteredCountries as country}
|
||||||
|
<CountryCard {country} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="drawer-side z-50">
|
||||||
|
<label for="travel-drawer" class="drawer-overlay"></label>
|
||||||
|
<div class="w-80 min-h-full bg-base-100 shadow-2xl">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Sidebar Header -->
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Filter class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold">Filters & Stats</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Travel Statistics -->
|
||||||
|
<div class="card bg-base-200/50 p-4 mb-6">
|
||||||
|
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<Globe class="w-5 h-5" />
|
||||||
|
Travel Progress
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-sm">Total Countries</div>
|
||||||
|
<div class="stat-value text-2xl">{totalCountries}</div>
|
||||||
|
<div class="stat-desc">Available to explore</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Visited</div>
|
||||||
|
<div class="stat-value text-lg text-success">{visitedCountries}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Remaining</div>
|
||||||
|
<div class="stat-value text-lg text-error">{notVisitedCountries}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Complete</div>
|
||||||
|
<div class="stat-value text-sm text-success">{completeCountries}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Partial</div>
|
||||||
|
<div class="stat-value text-sm text-warning">{partialCountries}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>Progress</span>
|
||||||
|
<span>{Math.round((visitedCountries / totalCountries) * 100)}%</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary w-full"
|
||||||
|
value={visitedCountries}
|
||||||
|
max={totalCountries}
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Region Filter -->
|
||||||
|
<div class="card bg-base-200/50 p-4">
|
||||||
|
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<Pin class="w-5 h-5" />
|
||||||
|
Filter by Region
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-2">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="region"
|
||||||
|
class="radio radio-primary radio-sm"
|
||||||
|
checked={subRegionOption === ''}
|
||||||
|
on:change={() => (subRegionOption = '')}
|
||||||
|
/>
|
||||||
|
<span class="label-text">All Regions</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{#each worldSubregions as subregion}
|
||||||
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="region"
|
||||||
|
class="radio radio-primary radio-sm"
|
||||||
|
checked={subRegionOption === subregion}
|
||||||
|
on:change={() => (subRegionOption = subregion)}
|
||||||
|
/>
|
||||||
|
<span class="label-text text-sm">{subregion}</span>
|
||||||
|
</label>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="space-y-3 mt-6">
|
||||||
|
<button class="btn btn-outline w-full gap-2" on:click={() => (showMap = !showMap)}>
|
||||||
|
{#if showMap}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
Hide Map
|
||||||
|
{:else}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
Show Map
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-ghost w-full gap-2" on:click={clearFilters}>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
Clear All Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
|
@ -5,30 +5,78 @@
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
import { addToast } from '$lib/toasts';
|
import { addToast } from '$lib/toasts';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
// Icons
|
||||||
|
import MapMarker from '~icons/mdi/map-marker';
|
||||||
|
import Search from '~icons/mdi/magnify';
|
||||||
|
import Clear from '~icons/mdi/close';
|
||||||
|
import Filter from '~icons/mdi/filter-variant';
|
||||||
|
import Map from '~icons/mdi/map';
|
||||||
|
import Pin from '~icons/mdi/map-marker-outline';
|
||||||
|
import Check from '~icons/mdi/check-circle';
|
||||||
|
import Progress from '~icons/mdi/progress-check';
|
||||||
|
import Cancel from '~icons/mdi/cancel';
|
||||||
|
import Trophy from '~icons/mdi/trophy';
|
||||||
|
import Target from '~icons/mdi/target';
|
||||||
|
import Flag from '~icons/mdi/flag';
|
||||||
|
|
||||||
export let data: PageData;
|
export let data: PageData;
|
||||||
|
|
||||||
let regions: Region[] = data.props?.regions || [];
|
let regions: Region[] = data.props?.regions || [];
|
||||||
let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || [];
|
let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || [];
|
||||||
|
|
||||||
let filteredRegions: Region[] = [];
|
let filteredRegions: Region[] = [];
|
||||||
|
|
||||||
let searchQuery: string = '';
|
let searchQuery: string = '';
|
||||||
|
let showGeo: boolean = true;
|
||||||
$: {
|
let sidebarOpen = false;
|
||||||
if (searchQuery === '') {
|
let filterOption: string = 'all';
|
||||||
filteredRegions = regions;
|
|
||||||
} else {
|
|
||||||
// always filter from the original regions list
|
|
||||||
filteredRegions = regions.filter((region) =>
|
|
||||||
region.name.toLowerCase().includes(searchQuery.toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const country = data.props?.country || null;
|
const country = data.props?.country || null;
|
||||||
console.log(data);
|
console.log(data);
|
||||||
|
|
||||||
let showGeo: boolean = true;
|
// Statistics
|
||||||
|
let numRegions: number = country?.num_regions || 0;
|
||||||
|
let numVisitedRegions: number = country?.num_visits || 0;
|
||||||
|
|
||||||
|
$: visitedCount = visitedRegions.length;
|
||||||
|
$: notVisitedCount = regions.length - visitedCount;
|
||||||
|
$: completionPercentage =
|
||||||
|
regions.length > 0 ? Math.round((visitedCount / regions.length) * 100) : 0;
|
||||||
|
|
||||||
|
// Filter regions based on search and filter options
|
||||||
|
$: {
|
||||||
|
if (searchQuery === '') {
|
||||||
|
filteredRegions = regions;
|
||||||
|
} else {
|
||||||
|
filteredRegions = regions.filter((region) =>
|
||||||
|
region.name.toLowerCase().includes(searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (filterOption === 'visited') {
|
||||||
|
filteredRegions = filteredRegions.filter((region) =>
|
||||||
|
visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)
|
||||||
|
);
|
||||||
|
} else if (filterOption === 'not-visited') {
|
||||||
|
filteredRegions = filteredRegions.filter(
|
||||||
|
(region) => !visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove duplicates from visitedRegions
|
||||||
|
visitedRegions = visitedRegions.filter(
|
||||||
|
(visitedRegion, index, self) =>
|
||||||
|
index === self.findIndex((t) => t.region === visitedRegion.region)
|
||||||
|
);
|
||||||
|
|
||||||
|
function toggleSidebar() {
|
||||||
|
sidebarOpen = !sidebarOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearFilters() {
|
||||||
|
searchQuery = '';
|
||||||
|
filterOption = 'all';
|
||||||
|
}
|
||||||
|
|
||||||
function togleVisited(region: Region) {
|
function togleVisited(region: Region) {
|
||||||
return () => {
|
return () => {
|
||||||
|
@ -64,6 +112,7 @@
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function removeVisit(region: Region) {
|
async function removeVisit(region: Region) {
|
||||||
let res = await fetch(`/api/visitedregion/${region.id}`, {
|
let res = await fetch(`/api/visitedregion/${region.id}`, {
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
@ -78,114 +127,8 @@
|
||||||
addToast('info', `${$t('worldtravel.visit_to')} ${region.name} ${$t('worldtravel.removed')}`);
|
addToast('info', `${$t('worldtravel.visit_to')} ${region.name} ${$t('worldtravel.removed')}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let numRegions: number = country?.num_regions || 0;
|
|
||||||
let numVisitedRegions: number = country?.num_visits || 0;
|
|
||||||
|
|
||||||
visitedRegions = visitedRegions.filter(
|
|
||||||
(visitedRegion, index, self) =>
|
|
||||||
index === self.findIndex((t) => t.region === visitedRegion.region)
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<h1 class="text-center font-bold text-4xl mb-4">{$t('worldtravel.regions_in')} {country?.name}</h1>
|
|
||||||
<div class="flex items-center justify-center mb-4">
|
|
||||||
<div class="stats shadow bg-base-300">
|
|
||||||
<div class="stat">
|
|
||||||
<div class="stat-title">{$t('worldtravel.region_stats')}</div>
|
|
||||||
<div class="stat-value">{numVisitedRegions}/{numRegions} {$t('adventures.visited')}</div>
|
|
||||||
{#if numRegions === numVisitedRegions}
|
|
||||||
<div class="stat-desc">{$t('worldtravel.all_visited')} {country?.name} 🎉!</div>
|
|
||||||
{:else}
|
|
||||||
<div class="stat-desc">{$t('adventures.keep_exploring')}</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex items-center justify-center mb-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
placeholder={$t('navbar.search')}
|
|
||||||
class="input input-bordered w-full max-w-xs"
|
|
||||||
bind:value={searchQuery}
|
|
||||||
/>
|
|
||||||
{#if searchQuery.length > 0}
|
|
||||||
<!-- clear button -->
|
|
||||||
<div class="flex items-center justify-center ml-4">
|
|
||||||
<button class="btn btn-neutral" on:click={() => (searchQuery = '')}>
|
|
||||||
{$t('worldtravel.clear_search')}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-wrap gap-4 mr-4 ml-4 justify-center content-center">
|
|
||||||
{#each filteredRegions as region}
|
|
||||||
<RegionCard
|
|
||||||
{region}
|
|
||||||
visited={visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)}
|
|
||||||
on:visit={(e) => {
|
|
||||||
visitedRegions = [...visitedRegions, e.detail];
|
|
||||||
numVisitedRegions++;
|
|
||||||
}}
|
|
||||||
on:remove={() => {
|
|
||||||
visitedRegions = visitedRegions.filter(
|
|
||||||
(visitedRegion) => visitedRegion.region !== region.id
|
|
||||||
);
|
|
||||||
numVisitedRegions--;
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex justify-center border-neutral p-4 rounded-lg border-4 max-w-lg m-auto mt-4">
|
|
||||||
<label for="show-geo">{$t('adventures.show_region_labels')}</label>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
id="show-geo"
|
|
||||||
name="show-geo"
|
|
||||||
class="checkbox ml-2"
|
|
||||||
bind:checked={showGeo}
|
|
||||||
on:click={() => (showGeo = !showGeo)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mt-4 mb-4 flex justify-center">
|
|
||||||
<!-- checkbox to toggle marker -->
|
|
||||||
|
|
||||||
<MapLibre
|
|
||||||
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
|
||||||
class="aspect-[9/16] max-h-[70vh] sm:aspect-video sm:max-h-full w-10/12 rounded-lg"
|
|
||||||
standardControls
|
|
||||||
center={[regions[0]?.longitude || 0, regions[0]?.latitude || 0]}
|
|
||||||
zoom={2}
|
|
||||||
>
|
|
||||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
|
||||||
where you might not have access to the top-level `MapLibre` component. In this case
|
|
||||||
it would also work to just use on:click on the MapLibre component itself. -->
|
|
||||||
<!-- <MapEvents on:click={addMarker} /> -->
|
|
||||||
|
|
||||||
{#each regions as region}
|
|
||||||
{#if region.latitude && region.longitude && showGeo}
|
|
||||||
<Marker
|
|
||||||
lngLat={[region.longitude, region.latitude]}
|
|
||||||
class="grid px-2 py-1 place-items-center rounded-full border border-gray-200 {visitedRegions.some(
|
|
||||||
(visitedRegion) => visitedRegion.region === region.id
|
|
||||||
)
|
|
||||||
? 'bg-green-200'
|
|
||||||
: 'bg-red-200'} text-black focus:outline-6 focus:outline-black"
|
|
||||||
on:click={togleVisited(region)}
|
|
||||||
>
|
|
||||||
<span class="text-xs">
|
|
||||||
{region.name}
|
|
||||||
</span>
|
|
||||||
</Marker>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
</MapLibre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title
|
<title
|
||||||
>{data.props && data.props.country ? `Regions in ${data.props.country.name}` : 'Regions'}</title
|
>{data.props && data.props.country ? `Regions in ${data.props.country.name}` : 'Regions'}</title
|
||||||
|
@ -195,3 +138,317 @@
|
||||||
content="View the regions in countries and mark them visited to track your world travel."
|
content="View the regions in countries and mark them visited to track your world travel."
|
||||||
/>
|
/>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
|
<div class="min-h-screen bg-gradient-to-br from-base-200 via-base-100 to-base-200">
|
||||||
|
<div class="drawer lg:drawer-open">
|
||||||
|
<input id="regions-drawer" type="checkbox" class="drawer-toggle" bind:checked={sidebarOpen} />
|
||||||
|
|
||||||
|
<div class="drawer-content">
|
||||||
|
<!-- Header Section -->
|
||||||
|
<div class="sticky top-0 z-40 bg-base-100/80 backdrop-blur-lg border-b border-base-300">
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button class="btn btn-ghost btn-square lg:hidden" on:click={toggleSidebar}>
|
||||||
|
<Filter class="w-5 h-5" />
|
||||||
|
</button>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-xl">
|
||||||
|
<Flag class="w-8 h-8 text-primary" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1
|
||||||
|
class="text-3xl font-bold bg-gradient-to-r from-primary to-secondary bg-clip-text text-transparent"
|
||||||
|
>
|
||||||
|
{$t('worldtravel.regions_in')}
|
||||||
|
{country?.name}
|
||||||
|
</h1>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{filteredRegions.length} of {regions.length} regions
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Completion Badge -->
|
||||||
|
<div class="hidden md:flex items-center gap-2">
|
||||||
|
{#if completionPercentage === 100}
|
||||||
|
<div class="badge badge-success gap-2 p-3">
|
||||||
|
<Trophy class="w-4 h-4" />
|
||||||
|
Complete!
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="badge badge-primary gap-2 p-3">
|
||||||
|
<Target class="w-4 h-4" />
|
||||||
|
{completionPercentage}%
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Search and Filters -->
|
||||||
|
<div class="mt-4 flex items-center gap-4">
|
||||||
|
<div class="relative flex-1 max-w-md">
|
||||||
|
<Search
|
||||||
|
class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-base-content/40"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder={$t('navbar.search')}
|
||||||
|
class="input input-bordered w-full pl-10 pr-10 bg-base-100/80"
|
||||||
|
bind:value={searchQuery}
|
||||||
|
/>
|
||||||
|
{#if searchQuery.length > 0}
|
||||||
|
<button
|
||||||
|
class="absolute right-3 top-1/2 -translate-y-1/2 text-base-content/40 hover:text-base-content"
|
||||||
|
on:click={() => (searchQuery = '')}
|
||||||
|
>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Toggle -->
|
||||||
|
<button
|
||||||
|
class="btn btn-outline gap-2 {showGeo ? 'btn-active' : ''}"
|
||||||
|
on:click={() => (showGeo = !showGeo)}
|
||||||
|
>
|
||||||
|
{#if showGeo}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">Hide Labels</span>
|
||||||
|
{:else}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">Show Labels</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Filter Chips -->
|
||||||
|
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||||
|
<span class="text-sm font-medium text-base-content/60">Filter by:</span>
|
||||||
|
<div class="tabs tabs-boxed bg-base-200">
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'all' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'all')}
|
||||||
|
>
|
||||||
|
<MapMarker class="w-3 h-3" />
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'visited' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'visited')}
|
||||||
|
>
|
||||||
|
<Check class="w-3 h-3" />
|
||||||
|
Visited
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="tab tab-sm gap-2 {filterOption === 'not-visited' ? 'tab-active' : ''}"
|
||||||
|
on:click={() => (filterOption = 'not-visited')}
|
||||||
|
>
|
||||||
|
<Cancel class="w-3 h-3" />
|
||||||
|
Not Visited
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if searchQuery || filterOption !== 'all'}
|
||||||
|
<button class="btn btn-ghost btn-xs gap-1" on:click={clearFilters}>
|
||||||
|
<Clear class="w-3 h-3" />
|
||||||
|
Clear All
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Section -->
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div class="card bg-base-100 shadow-xl">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Map class="w-5 h-5 text-primary" />
|
||||||
|
<h2 class="text-lg font-semibold">Interactive Map</h2>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 text-sm text-base-content/60">
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 bg-green-200 rounded-full border"></div>
|
||||||
|
<span>Visited</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-1">
|
||||||
|
<div class="w-3 h-3 bg-red-200 rounded-full border"></div>
|
||||||
|
<span>Not Visited</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<MapLibre
|
||||||
|
style="https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json"
|
||||||
|
class="aspect-[16/10] w-full rounded-lg"
|
||||||
|
standardControls
|
||||||
|
center={[regions[0]?.longitude || 0, regions[0]?.latitude || 0]}
|
||||||
|
zoom={6}
|
||||||
|
>
|
||||||
|
{#each regions as region}
|
||||||
|
{#if region.latitude && region.longitude && showGeo}
|
||||||
|
<Marker
|
||||||
|
lngLat={[region.longitude, region.latitude]}
|
||||||
|
class="grid px-2 py-1 place-items-center rounded-full border border-gray-200 {visitedRegions.some(
|
||||||
|
(visitedRegion) => visitedRegion.region === region.id
|
||||||
|
)
|
||||||
|
? 'bg-green-200'
|
||||||
|
: 'bg-red-200'} text-black focus:outline-6 focus:outline-black"
|
||||||
|
on:click={togleVisited(region)}
|
||||||
|
>
|
||||||
|
<span class="text-xs">
|
||||||
|
{region.name}
|
||||||
|
</span>
|
||||||
|
</Marker>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</MapLibre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
|
<div class="container mx-auto px-6 py-8">
|
||||||
|
{#if filteredRegions.length === 0}
|
||||||
|
<div class="flex flex-col items-center justify-center py-16">
|
||||||
|
<div class="p-6 bg-base-200/50 rounded-2xl mb-6">
|
||||||
|
<MapMarker class="w-16 h-16 text-base-content/30" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-base-content/70 mb-2">No regions found</h3>
|
||||||
|
<p class="text-base-content/50 text-center max-w-md mb-6">
|
||||||
|
Try adjusting your search terms or filters to find the regions you're looking for.
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary gap-2" on:click={clearFilters}>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
Clear Filters
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- Regions Grid -->
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-6"
|
||||||
|
>
|
||||||
|
{#each filteredRegions as region}
|
||||||
|
<RegionCard
|
||||||
|
{region}
|
||||||
|
visited={visitedRegions.some((visitedRegion) => visitedRegion.region === region.id)}
|
||||||
|
on:visit={(e) => {
|
||||||
|
visitedRegions = [...visitedRegions, e.detail];
|
||||||
|
numVisitedRegions++;
|
||||||
|
}}
|
||||||
|
on:remove={() => {
|
||||||
|
visitedRegions = visitedRegions.filter(
|
||||||
|
(visitedRegion) => visitedRegion.region !== region.id
|
||||||
|
);
|
||||||
|
numVisitedRegions--;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sidebar -->
|
||||||
|
<div class="drawer-side z-50">
|
||||||
|
<label for="regions-drawer" class="drawer-overlay"></label>
|
||||||
|
<div class="w-80 min-h-full bg-base-100 shadow-2xl">
|
||||||
|
<div class="p-6">
|
||||||
|
<!-- Sidebar Header -->
|
||||||
|
<div class="flex items-center gap-3 mb-8">
|
||||||
|
<div class="p-2 bg-primary/10 rounded-lg">
|
||||||
|
<Filter class="w-6 h-6 text-primary" />
|
||||||
|
</div>
|
||||||
|
<h2 class="text-xl font-bold">Progress & Stats</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Country Progress -->
|
||||||
|
<div class="card bg-base-200/50 p-4 mb-6">
|
||||||
|
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<Flag class="w-5 h-5" />
|
||||||
|
{country?.name}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-sm">Total Regions</div>
|
||||||
|
<div class="stat-value text-2xl">{regions.length}</div>
|
||||||
|
<div class="stat-desc">Available to explore</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Visited</div>
|
||||||
|
<div class="stat-value text-lg text-success">{visitedCount}</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat p-0">
|
||||||
|
<div class="stat-title text-xs">Remaining</div>
|
||||||
|
<div class="stat-value text-lg text-error">{notVisitedCount}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Bar -->
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex justify-between text-sm">
|
||||||
|
<span>Progress</span>
|
||||||
|
<span>{completionPercentage}%</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary w-full"
|
||||||
|
value={visitedCount}
|
||||||
|
max={regions.length}
|
||||||
|
></progress>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if completionPercentage === 100}
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<Trophy class="w-4 h-4" />
|
||||||
|
<span class="text-sm">Country completed! 🎉</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Map Controls
|
||||||
|
<div class="card bg-base-200/50 p-4 mb-6">
|
||||||
|
<h3 class="font-semibold text-lg mb-4 flex items-center gap-2">
|
||||||
|
<Map class="w-5 h-5" />
|
||||||
|
Map Settings
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<div class="space-y-3">
|
||||||
|
<label class="label cursor-pointer justify-start gap-3">
|
||||||
|
<input type="checkbox" class="checkbox checkbox-primary" bind:checked={showGeo} />
|
||||||
|
<span class="label-text">{$t('adventures.show_region_labels')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div class="text-xs text-base-content/60">
|
||||||
|
Click markers on the map to toggle visited status
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- Quick Actions -->
|
||||||
|
<div class="space-y-3">
|
||||||
|
<button class="btn btn-outline w-full gap-2" on:click={() => (showGeo = !showGeo)}>
|
||||||
|
{#if showGeo}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
Hide Map Labels
|
||||||
|
{:else}
|
||||||
|
<Map class="w-4 h-4" />
|
||||||
|
Show Map Labels
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- <button class="btn btn-ghost w-full gap-2" on:click={clearFilters}>
|
||||||
|
<Clear class="w-4 h-4" />
|
||||||
|
Clear All Filters
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue