diff --git a/backend/server/adventures/admin.py b/backend/server/adventures/admin.py index a893834..e23fa15 100644 --- a/backend/server/adventures/admin.py +++ b/backend/server/adventures/admin.py @@ -129,12 +129,9 @@ class CategoryAdmin(admin.ModelAdmin): search_fields = ('name', 'display_name') 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', 'adventure_count', 'is_public') + list_display = ('name', 'user_id', 'is_public') admin.site.register(CustomUser, CustomUserAdmin) diff --git a/frontend/src/lib/components/CategoryFilterDropdown.svelte b/frontend/src/lib/components/CategoryFilterDropdown.svelte index d9069be..37e4db0 100644 --- a/frontend/src/lib/components/CategoryFilterDropdown.svelte +++ b/frontend/src/lib/components/CategoryFilterDropdown.svelte @@ -37,25 +37,34 @@
+
{$t('adventures.category_filter')}
+
- - {#each adventure_types as type} -
  • - -
  • - {/each} + + +
    diff --git a/frontend/src/lib/components/CollectionCard.svelte b/frontend/src/lib/components/CollectionCard.svelte index 2363fc6..884fa9c 100644 --- a/frontend/src/lib/components/CollectionCard.svelte +++ b/frontend/src/lib/components/CollectionCard.svelte @@ -44,10 +44,11 @@ if (res.ok) { if (is_archived) { addToast('info', $t('adventures.archived_collection_message')); + dispatch('archive', collection.id); } else { addToast('info', $t('adventures.unarchived_collection_message')); + dispatch('unarchive', collection.id); } - dispatch('delete', collection.id); } else { console.log('Error archiving collection'); } diff --git a/frontend/src/lib/components/CollectionLink.svelte b/frontend/src/lib/components/CollectionLink.svelte index dd41b7c..75546a8 100644 --- a/frontend/src/lib/components/CollectionLink.svelte +++ b/frontend/src/lib/components/CollectionLink.svelte @@ -60,6 +60,7 @@ on:link={link} bind:linkedCollectionList on:unlink={unlink} + user={null} /> {/each} {#if collections.length === 0} diff --git a/frontend/src/routes/collections/+page.svelte b/frontend/src/routes/collections/+page.svelte index 1d1680c..605ebfe 100644 --- a/frontend/src/routes/collections/+page.svelte +++ b/frontend/src/routes/collections/+page.svelte @@ -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) { collections = data.props.adventures; } if (data.props.archivedCollections) { archivedCollections = data.props.archivedCollections; } + currentPage = pageNum; } - function goToPage(pageNum: number) { - 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) { + async function updateSort(by: string, direction: string) { const url = new URL($page.url); url.searchParams.set('order_by', by); url.searchParams.set('order_direction', direction); 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) { @@ -98,6 +102,7 @@ function archiveCollection(event: CustomEvent) { const collectionId = event.detail; + console.log('Archiving collection with ID:', collectionId); // Find the collection in owned collections const collectionToArchive = collections.find((collection) => collection.id === collectionId); @@ -106,9 +111,6 @@ collections = collections.filter((collection) => collection.id !== collectionId); // Add to archived collections 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 collections = [...collections, { ...collectionToUnarchive, is_archived: false }]; - - // Automatically switch to owned tab to show the unarchived item - activeView = 'owned'; } } diff --git a/frontend/src/routes/collections/archived/+page.server.ts b/frontend/src/routes/collections/archived/+page.server.ts deleted file mode 100644 index 7b0f8c5..0000000 --- a/frontend/src/routes/collections/archived/+page.server.ts +++ /dev/null @@ -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; diff --git a/frontend/src/routes/collections/archived/+page.svelte b/frontend/src/routes/collections/archived/+page.svelte deleted file mode 100644 index 9f0222c..0000000 --- a/frontend/src/routes/collections/archived/+page.svelte +++ /dev/null @@ -1,37 +0,0 @@ - - -
    -
    - -

    {$t('adventures.archived_collections')}

    - {#if collections.length === 0} - - {/if} -
    -
    - {#each collections as collection} - - {/each} -
    -
    -
    -
    - - - Collections - - diff --git a/frontend/src/routes/worldtravel/+page.svelte b/frontend/src/routes/worldtravel/+page.svelte index 8359fa3..c47831b 100644 --- a/frontend/src/routes/worldtravel/+page.svelte +++ b/frontend/src/routes/worldtravel/+page.svelte @@ -6,33 +6,54 @@ import { t } from 'svelte-i18n'; 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; console.log(data); let searchQuery: string = ''; - let filteredCountries: Country[] = []; const allCountries: Country[] = data.props?.countries || []; let worldSubregions: string[] = []; let showMap: boolean = false; + let sidebarOpen = false; worldSubregions = [...new Set(allCountries.map((country) => country.subregion))]; - // remove blank subregions worldSubregions = worldSubregions.filter((subregion) => subregion !== ''); console.log(worldSubregions); let filterOption: string = 'all'; 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 === '') { filteredCountries = allCountries; } else { - // otherwise, filter countries by name filteredCountries = allCountries.filter((country) => country.name.toLowerCase().includes(searchQuery.toLowerCase()) ); } + if (filterOption === 'partial') { filteredCountries = filteredCountries.filter( (country) => country.num_visits > 0 && country.num_visits < country.num_regions @@ -43,8 +64,6 @@ ); } else if (filterOption === 'not') { filteredCountries = filteredCountries.filter((country) => country.num_visits === 0); - } else { - filteredCountries = filteredCountries; } if (subRegionOption !== '') { @@ -53,137 +72,360 @@ ); } } + + function toggleSidebar() { + sidebarOpen = !sidebarOpen; + } + + function clearFilters() { + searchQuery = ''; + filterOption = 'all'; + subRegionOption = ''; + } -

    {$t('worldtravel.country_list')}

    - -

    - {filteredCountries.length} - {$t('worldtravel.num_countries')} -

    -
    -
    - (filterOption = 'all')} - /> - (filterOption = 'partial')} - /> - (filterOption = 'complete')} - /> - (filterOption = 'not')} - /> -
    - - -
    - - {$t('adventures.show_map')} -
    -
    - -
    - - {#if searchQuery.length > 0} - -
    - -
    - {/if} -
    - -{#if showMap} -
    - - - - {#each filteredCountries as country} - {#if country.latitude && country.longitude} - goto(`/worldtravel/${country.country_code}`)} - > - - {country.name} - - - {/if} - {/each} - -
    -{/if} - -
    - {#each filteredCountries as country} - - - {/each} -
    - -{#if filteredCountries.length === 0} -

    {$t('worldtravel.no_countries_found')}

    - - - {#if allCountries.length === 0} - - {/if} -{/if} - Countries | World Travel + +
    +
    + + +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +

    + {$t('worldtravel.country_list')} +

    +

    + {filteredCountries.length} of {totalCountries} countries +

    +
    +
    +
    + + + +
    + + +
    +
    + + + {#if searchQuery.length > 0} + + {/if} +
    + + + +
    + + +
    + Filter by: +
    + + + + +
    + + {#if subRegionOption} +
    + {subRegionOption} + +
    + {/if} + + {#if searchQuery || filterOption !== 'all' || subRegionOption} + + {/if} +
    +
    +
    + + + {#if showMap} +
    +
    +
    + + {#each filteredCountries as country} + {#if country.latitude && country.longitude} + goto(`/worldtravel/${country.country_code}`)} + > + + {country.name} + + + {/if} + {/each} + +
    +
    +
    + {/if} + + +
    + {#if filteredCountries.length === 0} +
    +
    + +
    +

    + {$t('worldtravel.no_countries_found')} +

    +

    + Try adjusting your search terms or filters to find the countries you're looking for. +

    + + + {#if allCountries.length === 0} +
    +
    +
    +

    No country data available

    +

    Please check the documentation for updating region data.

    +
    +
    + + {$t('settings.documentation_link')} + +
    + {/if} +
    + {:else} + +
    + {#each filteredCountries as country} + + {/each} +
    + {/if} +
    +
    + + +
    + +
    +
    + +
    +
    + +
    +

    Filters & Stats

    +
    + + +
    +

    + + Travel Progress +

    + +
    +
    +
    Total Countries
    +
    {totalCountries}
    +
    Available to explore
    +
    + +
    +
    +
    Visited
    +
    {visitedCountries}
    +
    +
    +
    Remaining
    +
    {notVisitedCountries}
    +
    +
    + +
    +
    +
    Complete
    +
    {completeCountries}
    +
    +
    +
    Partial
    +
    {partialCountries}
    +
    +
    + + +
    +
    + Progress + {Math.round((visitedCountries / totalCountries) * 100)}% +
    + +
    +
    +
    + + +
    +

    + + Filter by Region +

    + +
    + + + {#each worldSubregions as subregion} + + {/each} +
    +
    + + +
    + + + +
    +
    +
    +
    +
    +
    diff --git a/frontend/src/routes/worldtravel/[id]/+page.svelte b/frontend/src/routes/worldtravel/[id]/+page.svelte index 8d3de77..958079d 100644 --- a/frontend/src/routes/worldtravel/[id]/+page.svelte +++ b/frontend/src/routes/worldtravel/[id]/+page.svelte @@ -5,30 +5,78 @@ import type { PageData } from './$types'; import { addToast } from '$lib/toasts'; 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; let regions: Region[] = data.props?.regions || []; let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || []; - let filteredRegions: Region[] = []; - let searchQuery: string = ''; - - $: { - if (searchQuery === '') { - filteredRegions = regions; - } else { - // always filter from the original regions list - filteredRegions = regions.filter((region) => - region.name.toLowerCase().includes(searchQuery.toLowerCase()) - ); - } - } + let showGeo: boolean = true; + let sidebarOpen = false; + let filterOption: string = 'all'; const country = data.props?.country || null; 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) { return () => { @@ -64,6 +112,7 @@ ); } } + async function removeVisit(region: Region) { let res = await fetch(`/api/visitedregion/${region.id}`, { headers: { 'Content-Type': 'application/json' }, @@ -78,114 +127,8 @@ 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) - ); -

    {$t('worldtravel.regions_in')} {country?.name}

    -
    -
    -
    -
    {$t('worldtravel.region_stats')}
    -
    {numVisitedRegions}/{numRegions} {$t('adventures.visited')}
    - {#if numRegions === numVisitedRegions} -
    {$t('worldtravel.all_visited')} {country?.name} 🎉!
    - {:else} -
    {$t('adventures.keep_exploring')}
    - {/if} -
    -
    -
    - -
    - - {#if searchQuery.length > 0} - -
    - -
    - {/if} -
    - -
    - {#each filteredRegions as region} - visitedRegion.region === region.id)} - on:visit={(e) => { - visitedRegions = [...visitedRegions, e.detail]; - numVisitedRegions++; - }} - on:remove={() => { - visitedRegions = visitedRegions.filter( - (visitedRegion) => visitedRegion.region !== region.id - ); - numVisitedRegions--; - }} - /> - {/each} -
    - -
    - - (showGeo = !showGeo)} - /> -
    - -
    - - - - - - - {#each regions as region} - {#if region.latitude && region.longitude && showGeo} - - - {region.name} - - - {/if} - {/each} - -
    - {data.props && data.props.country ? `Regions in ${data.props.country.name}` : 'Regions'} + +
    +
    + + +
    + +
    +
    +
    +
    + +
    +
    + +
    +
    +

    + {$t('worldtravel.regions_in')} + {country?.name} +

    +

    + {filteredRegions.length} of {regions.length} regions +

    +
    +
    +
    + + + +
    + + +
    +
    + + + {#if searchQuery.length > 0} + + {/if} +
    + + + +
    + + +
    + Filter by: +
    + + + +
    + + {#if searchQuery || filterOption !== 'all'} + + {/if} +
    +
    +
    + + +
    +
    +
    +
    +
    + +

    Interactive Map

    +
    +
    +
    +
    + Visited +
    +
    +
    + Not Visited +
    +
    +
    + + {#each regions as region} + {#if region.latitude && region.longitude && showGeo} + + + {region.name} + + + {/if} + {/each} + +
    +
    +
    + + +
    + {#if filteredRegions.length === 0} +
    +
    + +
    +

    No regions found

    +

    + Try adjusting your search terms or filters to find the regions you're looking for. +

    + +
    + {:else} + +
    + {#each filteredRegions as region} + visitedRegion.region === region.id)} + on:visit={(e) => { + visitedRegions = [...visitedRegions, e.detail]; + numVisitedRegions++; + }} + on:remove={() => { + visitedRegions = visitedRegions.filter( + (visitedRegion) => visitedRegion.region !== region.id + ); + numVisitedRegions--; + }} + /> + {/each} +
    + {/if} +
    +
    + + +
    + +
    +
    + +
    +
    + +
    +

    Progress & Stats

    +
    + + +
    +

    + + {country?.name} +

    + +
    +
    +
    Total Regions
    +
    {regions.length}
    +
    Available to explore
    +
    + +
    +
    +
    Visited
    +
    {visitedCount}
    +
    +
    +
    Remaining
    +
    {notVisitedCount}
    +
    +
    + + +
    +
    + Progress + {completionPercentage}% +
    + +
    + + {#if completionPercentage === 100} +
    + + Country completed! 🎉 +
    + {/if} +
    +
    + + + + +
    + + + +
    +
    +
    +
    +
    +