World Travel Improvements (#925)
* Security Patch Django 5.2.8 * Fix Menus on Safari Browser * Enhance touch support and event handling for emoji picker and dropdown * Add touch and pointer event handling to category selection for better mobile support * Add PWA support for iOS/Safari with touch icons * Refactor event listener for dropdown to use non-capturing 'click' for improved compatibility on Safari * Enhance country and region description fetching from Wikipedia - Refactor `generate_description_view.py` to improve candidate page selection and description retrieval. - Update `CategoryDropdown.svelte` to simplify emoji selection handling and improve dropdown behavior. - Add new translation keys in `en.json` for UI elements related to country descriptions. - Modify `+page.svelte` and `+page.server.ts` in world travel routes to fetch and display country and region descriptions. - Implement a toggle for showing full descriptions in the UI. * Update Unraid installation documentation with improved variable formatting and additional resources * Implement cache invalidation for visited regions and cities to ensure updated visit lists * Add ClusterMap component for enhanced geographical data visualization
This commit is contained in:
@@ -57,11 +57,36 @@ export const load = (async (event) => {
|
||||
country = (await res.json()) as Country;
|
||||
}
|
||||
|
||||
// Attempt to fetch a short description (Wikipedia/Wikidata generated) for the country
|
||||
let description: string | null = null;
|
||||
try {
|
||||
const descRes = await fetch(
|
||||
`${endpoint}/api/generate/desc/?name=${encodeURIComponent(country.name)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (descRes.ok) {
|
||||
const descJson = await descRes.json();
|
||||
if (descJson && typeof descJson.extract === 'string') {
|
||||
description = descJson.extract;
|
||||
}
|
||||
} else {
|
||||
console.debug('No description available for', country.name);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Failed to fetch description:', e);
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
regions,
|
||||
visitedRegions,
|
||||
country
|
||||
country,
|
||||
description
|
||||
}
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -12,9 +12,8 @@
|
||||
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 Info from '~icons/mdi/information-outline';
|
||||
import Cancel from '~icons/mdi/cancel';
|
||||
import Trophy from '~icons/mdi/trophy';
|
||||
import Target from '~icons/mdi/target';
|
||||
@@ -25,6 +24,8 @@
|
||||
|
||||
let regions: Region[] = data.props?.regions || [];
|
||||
let visitedRegions: VisitedRegion[] = data.props?.visitedRegions || [];
|
||||
let description: string = data.props?.description || '';
|
||||
let showFullDesc = false;
|
||||
let filteredRegions: Region[] = [];
|
||||
let searchQuery: string = '';
|
||||
let showGeo: boolean = true;
|
||||
@@ -186,7 +187,8 @@
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="mt-4 flex items-center gap-4">
|
||||
<div class="mt-4 flex flex-col lg:flex-row lg:items-center gap-4">
|
||||
<!-- Search Bar -->
|
||||
<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"
|
||||
@@ -206,47 +208,80 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Chips -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content/60"
|
||||
>{$t('worldtravel.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')}
|
||||
<!-- Filter Chips -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content/60 hidden sm:inline"
|
||||
>{$t('worldtravel.filter_by')}:</span
|
||||
>
|
||||
<MapMarker class="w-3 h-3" />
|
||||
{$t('adventures.all')}
|
||||
</button>
|
||||
<button
|
||||
class="tab tab-sm gap-2 {filterOption === 'visited' ? 'tab-active' : ''}"
|
||||
on:click={() => (filterOption = 'visited')}
|
||||
>
|
||||
<Check class="w-3 h-3" />
|
||||
{$t('adventures.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" />
|
||||
{$t('adventures.not_visited')}
|
||||
</button>
|
||||
<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" />
|
||||
{$t('adventures.all')}
|
||||
</button>
|
||||
<button
|
||||
class="tab tab-sm gap-2 {filterOption === 'visited' ? 'tab-active' : ''}"
|
||||
on:click={() => (filterOption = 'visited')}
|
||||
>
|
||||
<Check class="w-3 h-3" />
|
||||
{$t('adventures.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" />
|
||||
{$t('adventures.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" />
|
||||
{$t('worldtravel.clear_all')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if searchQuery || filterOption !== 'all'}
|
||||
<button class="btn btn-ghost btn-xs gap-1" on:click={clearFilters}>
|
||||
<Clear class="w-3 h-3" />
|
||||
{$t('worldtravel.clear_all')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Section -->
|
||||
{#if description}
|
||||
<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 gap-2 mb-4">
|
||||
<Info class="w-5 h-5 text-primary" />
|
||||
<h2 class="text-lg font-semibold">{$t('worldtravel.about_country')}</h2>
|
||||
</div>
|
||||
<p
|
||||
class="text-base-content/70 leading-relaxed"
|
||||
class:overflow-hidden={!showFullDesc}
|
||||
style={!showFullDesc && description.length > 400
|
||||
? 'max-height:8rem;overflow:hidden;'
|
||||
: ''}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{#if description.length > 400}
|
||||
<button
|
||||
class="btn btn-ghost btn-sm mt-3"
|
||||
on:click={() => (showFullDesc = !showFullDesc)}
|
||||
>
|
||||
{#if showFullDesc}{$t('worldtravel.show_less')}{:else}{$t(
|
||||
'worldtravel.show_more'
|
||||
)}{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Map Section -->
|
||||
{#if regions.some((region) => region.latitude && region.longitude)}
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
|
||||
@@ -44,6 +44,59 @@ export const load = (async (event) => {
|
||||
region = (await res.json()) as Region;
|
||||
}
|
||||
|
||||
// Fetch country details (if available) to improve description search
|
||||
let country: Country | null = null;
|
||||
if (region?.country) {
|
||||
res = await fetch(`${endpoint}/api/countries/${region.country}/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
country = (await res.json()) as Country;
|
||||
} else {
|
||||
console.debug('Failed to fetch country for region description');
|
||||
}
|
||||
}
|
||||
|
||||
// Attempt to fetch a short description (Wikipedia/Wikidata generated) for the region.
|
||||
// Try multiple candidate queries to improve the chance of a match: region name, "region, country", then country name.
|
||||
let description: string | null = null;
|
||||
try {
|
||||
const candidates: string[] = [];
|
||||
if (region?.name) candidates.push(region.name);
|
||||
if (region?.name && country?.name) candidates.push(`${region.name}, ${country.name}`);
|
||||
if (country?.name) candidates.push(country.name);
|
||||
|
||||
for (const name of candidates) {
|
||||
try {
|
||||
const descRes = await fetch(
|
||||
`${endpoint}/api/generate/desc/?name=${encodeURIComponent(name)}`,
|
||||
{
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Cookie: `sessionid=${sessionId}`
|
||||
}
|
||||
}
|
||||
);
|
||||
if (descRes.ok) {
|
||||
const descJson = await descRes.json();
|
||||
if (descJson && typeof descJson.extract === 'string') {
|
||||
description = descJson.extract;
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
console.debug('No description available for', name);
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Failed to fetch description for', name, e);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Description generation attempt failed', e);
|
||||
}
|
||||
|
||||
res = await fetch(`${endpoint}/api/regions/${region.id}/cities/visits/`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
@@ -61,7 +114,8 @@ export const load = (async (event) => {
|
||||
props: {
|
||||
cities,
|
||||
region,
|
||||
visitedCities
|
||||
visitedCities,
|
||||
description
|
||||
}
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
import Cancel from '~icons/mdi/cancel';
|
||||
import Trophy from '~icons/mdi/trophy';
|
||||
import Target from '~icons/mdi/target';
|
||||
import Info from '~icons/mdi/information-outline';
|
||||
import CityIcon from '~icons/mdi/city';
|
||||
|
||||
export let data: PageData;
|
||||
@@ -30,6 +31,8 @@
|
||||
const allCities: City[] = data.props?.cities || [];
|
||||
let visitedCities: VisitedCity[] = data.props?.visitedCities || [];
|
||||
const region = data.props?.region || null;
|
||||
let description: string = data.props?.description || '';
|
||||
let showFullDesc = false;
|
||||
|
||||
console.log(data);
|
||||
|
||||
@@ -181,7 +184,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Search and Filters -->
|
||||
<div class="mt-4 flex items-center gap-4">
|
||||
<div class="mt-4 flex flex-col lg:flex-row items-start lg: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"
|
||||
@@ -201,47 +204,80 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filter Chips -->
|
||||
<div class="mt-4 flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content/60"
|
||||
>{$t('worldtravel.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')}
|
||||
<!-- Filter Chips -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="text-sm font-medium text-base-content/60"
|
||||
>{$t('worldtravel.filter_by')}:</span
|
||||
>
|
||||
<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" />
|
||||
{$t('adventures.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" />
|
||||
{$t('adventures.not_visited')}
|
||||
</button>
|
||||
<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" />
|
||||
{$t('adventures.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" />
|
||||
{$t('adventures.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" />
|
||||
{$t('worldtravel.clear_all')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if searchQuery || filterOption !== 'all'}
|
||||
<button class="btn btn-ghost btn-xs gap-1" on:click={clearFilters}>
|
||||
<Clear class="w-3 h-3" />
|
||||
{$t('worldtravel.clear_all')}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description Section -->
|
||||
{#if description}
|
||||
<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 gap-2 mb-4">
|
||||
<Info class="w-5 h-5 text-primary" />
|
||||
<h2 class="text-lg font-semibold">{$t('worldtravel.about_country')}</h2>
|
||||
</div>
|
||||
<p
|
||||
class="text-base-content/70 leading-relaxed"
|
||||
class:overflow-hidden={!showFullDesc}
|
||||
style={!showFullDesc && description.length > 400
|
||||
? 'max-height:8rem;overflow:hidden;'
|
||||
: ''}
|
||||
>
|
||||
{description}
|
||||
</p>
|
||||
{#if description.length > 400}
|
||||
<button
|
||||
class="btn btn-ghost btn-sm mt-3"
|
||||
on:click={() => (showFullDesc = !showFullDesc)}
|
||||
>
|
||||
{#if showFullDesc}{$t('worldtravel.show_less')}{:else}{$t(
|
||||
'worldtravel.show_more'
|
||||
)}{/if}
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Map Section -->
|
||||
{#if allCities.length > 0}
|
||||
<div class="container mx-auto px-6 py-4">
|
||||
|
||||
Reference in New Issue
Block a user