Bug Fixes + Speed Improvements (#871)
* Show timezone information on visits list (#865)
* Initial plan
* Initial investigation: Show timezone on visits list - planning implementation
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>
* Show timezone on visits list - add timezone badge display
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>
---------
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: seanmorley15 <98704938+seanmorley15@users.noreply.github.com>
* fix: add additional timezones (#852)
* [BUG] Transportation node does not accept complex links in link parameter
Fixes #856
* Squashed commit of the following:
commit 59d5128cc642d133b0c166fbaf2d41a88c237d92
Merge: 0f9d31f4 7b8961e0
Author: Hosted Weblate <hosted@weblate.org>
Date: Fri Sep 19 04:22:36 2025 +0200
Merge branch 'origin/development' into Weblate.
commit 7b8961e02430b9b6fab7b22a7a8c1f7b06ff950b
Author: Orhun <orhunavcu@gmail.com>
Date: Fri Sep 19 00:30:25 2025 +0200
Translated using Weblate (Turkish)
Currently translated at 16.7% (160 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/tr/
commit a8134bdbda318d00175c984785d150e38a1e24bf
Author: Orhun <orhunavcu@gmail.com>
Date: Thu Sep 18 22:52:56 2025 +0200
Added translation using Weblate (Turkish)
commit ac8a8ee8c9fc55da2d4ded1c4beac04a1ea66bb8
Merge: 2527e345 3fca3872
Author: Hosted Weblate <hosted@weblate.org>
Date: Thu Sep 18 14:11:58 2025 +0200
Merge branch 'origin/development' into Weblate.
commit 3fca387272d52dfcb634751a74e4a4b4fcf7ac6b
Merge: 4907ba87 85d8b45c
Author: Hosted Weblate <hosted@weblate.org>
Date: Thu Sep 18 05:13:16 2025 +0200
Merge branch 'origin/development' into Weblate.
commit 85d8b45c4eff77bcc9d5708f7fd8a4fcea91890b
Merge: 9f5cc9cc ae07c440
Author: Sean Morley <mail@seanmorley.com>
Date: Wed Sep 17 19:25:35 2025 -0400
Merge branch 'development' of github.com:seanmorley15/AdventureLog into development
commit 9f5cc9ccb8809cd5ae0c01e979dae271e9197987
Author: Sean Morley <mail@seanmorley.com>
Date: Wed Sep 17 19:23:47 2025 -0400
Remove duplicate comment for syncing development branch with main in .env.example
commit 77c1f516266b2db5e1c97c5cbc8a3fb9e08d40cd
Author: Sean Morley <mail@seanmorley.com>
Date: Wed Sep 17 19:22:36 2025 -0400
Squashed commit of the following:
commit 9d4f1b8f534a3cdfb22812f2a25ababd7a236a87
Author: Jacob <jacob.aulin@proton.me>
Date: Sat Sep 13 15:17:22 2025 +0200
Translated using Weblate (Swedish)
Currently translated at 99.8% (957 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sv/
commit 8fac40cfde425c989521c891b3ba9c75ab32e57e
Author: Christian S <schuld.christian@gmail.com>
Date: Sat Sep 13 12:54:52 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 8e8c42396ec77b763983155e8b1e89cabf38ce17
Author: Patricio Carrau <duckycb@proton.me>
Date: Tue Sep 9 21:59:48 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit be818ab408d00c5c26dfb3b25632604a415d3570
Author: pplulee <hi@pplulee.me>
Date: Mon Sep 8 04:06:54 2025 +0100
fix(i18n): update Chinese translations for location-related terms (#829)
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 9e40dcf6a1dc194d4694a114b3c7e88135121016
Merge: af2f2809 733eefce
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:03:57 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit af2f28090b9242fb7ab263fa5bbb95a5bcc1b27f
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:00:33 2025 -0400
[BUG] Location Visit End Date not affected by Location Timezone
Fixes #843
commit 733eefceddbdad01726364e5d4523605f095fde2
Author: Alex <div@alexe.at>
Date: Sun Sep 7 23:28:20 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 6c750d1c8f95b42418893e15ad46c3d4ed86d053
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 09:17:16 2025 +0200
Translated using Weblate (Slovak)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sk/
commit f733b3b96bbddc71d426f2e60320a5ad2f6755af
Merge: 769ea6ad af4e541c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:35 2025 -0400
Merge branch 'development' of github.com:seanmorley15/AdventureLog into development
commit 769ea6ad710890e931aabace2c00dc37436f869f
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:33 2025 -0400
Implement code changes to enhance functionality and improve performance
commit af4e541c1c9e7309857102287199279aec339387
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 03:36:23 2025 +0200
Added Slovak translations (#815)
* Created sk.json
* Update Navbar.svelte
* Update +layout.svelte
---------
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 904474d757577229b47441d1378a6fd6788fbe40
Merge: d4709434 f87a5fe3
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:31:58 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit d47094346c0b63ea753294a0786414e5e070ae7f
Merge: 4a5f59bf 6366a3eb
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:29:39 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit f87a5fe3bcc2fe28cfc206fb5cba517bbffa8df6
Author: Sergio <garcia.sergio@me.com>
Date: Sun Sep 7 01:12:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 4a5f59bfd24e32fdf3558b009a8f636636cb3663
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:06:17 2025 -0400
Fixes #654
commit c1302bb54ab272c2a98c53ce0d508b7d39e9674b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 20:52:05 2025 -0400
[BUG] Single day Collections will think location visits are out of date range
Fixes #827
commit 773f2d65bbfb2a9591b31fabfd6844612b840f1a
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:52:28 2025 -0400
[BUG] Server Error (500) when trying to access the API docs
Fixes #712
commit 4228db249ed5e3261931a1cdb3895d0ddd3ac4ac
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:44:00 2025 -0400
[BUG]Ordered Itinerary includes visits that are outside itinerary date range
Fixes #746
commit 26f36cabb0a860f10d7ba62b5279ddd1e282c78e
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 18:36:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 3bfd2dd5615afdbd04e3451c2ef728f1d7caf466
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 12:33:23 2025 -0400
Remove empty English (United States) locale file
commit 6366a3eba6ab72090e52be474212a663799dfe19
Author: Nikolai Eidsheim <nikolai.eidsheim@gmail.com>
Date: Sat Sep 6 18:10:15 2025 +0200
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/nb_NO/
commit 671cd3701fc5a601f2f1bad9aef93106f91eec0b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:58:04 2025 +0200
Added translation using Weblate (English (United States))
commit bdbbe5f4978f041f620f0503da69fa870cb1997c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:54:43 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit f310771702291be93e1c6b3bdb5a76e02fb8cae8
Author: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
Date: Mon Sep 8 22:38:08 2025 -0400
Update issue templates for bug reports, deployment issues, and feature requests (#849)
commit 02ed89fa46fa22f8c5d96b0e7f0948204dec9306
Author: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
Date: Sun Sep 7 23:16:12 2025 -0400
Timezone fixes, Translations, and Misc Fixes (#844)
* Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
* Added translation using Weblate (English (United States))
* Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/nb_NO/
* Remove empty English (United States) locale file
* Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
* [BUG]Ordered Itinerary includes visits that are outside itinerary date range
Fixes #746
* [BUG] Server Error (500) when trying to access the API docs
Fixes #712
* [BUG] Single day Collections will think location visits are out of date range
Fixes #827
* Fixes #654
* Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
* Added Slovak translations (#815)
* Created sk.json
* Update Navbar.svelte
* Update +layout.svelte
---------
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
* Implement code changes to enhance functionality and improve performance
* Translated using Weblate (Slovak)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sk/
* Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
* [BUG] Location Visit End Date not affected by Location Timezone
Fixes #843
* fix(i18n): update Chinese translations for location-related terms (#829)
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
---------
Co-authored-by: Nikolai Eidsheim <nikolai.eidsheim@gmail.com>
Co-authored-by: Sergio <garcia.sergio@me.com>
Co-authored-by: fantastron27 <fantastron27@gmail.com>
Co-authored-by: Alex <div@alexe.at>
Co-authored-by: pplulee <hi@pplulee.me>
commit ae07c44030ff597be80988b63460595e8bea132f
Author: Sean Morley <mail@seanmorley.com>
Date: Wed Sep 17 19:23:47 2025 -0400
Remove duplicate comment for syncing development branch with main in .env.example
commit 94964f1fb16583f1ff63ddf4aee2469a532b0c6c
Author: Sean Morley <mail@seanmorley.com>
Date: Wed Sep 17 19:22:36 2025 -0400
Squashed commit of the following:
commit 9d4f1b8f534a3cdfb22812f2a25ababd7a236a87
Author: Jacob <jacob.aulin@proton.me>
Date: Sat Sep 13 15:17:22 2025 +0200
Translated using Weblate (Swedish)
Currently translated at 99.8% (957 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sv/
commit 8fac40cfde425c989521c891b3ba9c75ab32e57e
Author: Christian S <schuld.christian@gmail.com>
Date: Sat Sep 13 12:54:52 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 8e8c42396ec77b763983155e8b1e89cabf38ce17
Author: Patricio Carrau <duckycb@proton.me>
Date: Tue Sep 9 21:59:48 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit be818ab408d00c5c26dfb3b25632604a415d3570
Author: pplulee <hi@pplulee.me>
Date: Mon Sep 8 04:06:54 2025 +0100
fix(i18n): update Chinese translations for location-related terms (#829)
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 9e40dcf6a1dc194d4694a114b3c7e88135121016
Merge: af2f2809 733eefce
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:03:57 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit af2f28090b9242fb7ab263fa5bbb95a5bcc1b27f
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:00:33 2025 -0400
[BUG] Location Visit End Date not affected by Location Timezone
Fixes #843
commit 733eefceddbdad01726364e5d4523605f095fde2
Author: Alex <div@alexe.at>
Date: Sun Sep 7 23:28:20 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 6c750d1c8f95b42418893e15ad46c3d4ed86d053
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 09:17:16 2025 +0200
Translated using Weblate (Slovak)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sk/
commit f733b3b96bbddc71d426f2e60320a5ad2f6755af
Merge: 769ea6ad af4e541c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:35 2025 -0400
Merge branch 'development' of github.com:seanmorley15/AdventureLog into development
commit 769ea6ad710890e931aabace2c00dc37436f869f
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:33 2025 -0400
Implement code changes to enhance functionality and improve performance
commit af4e541c1c9e7309857102287199279aec339387
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 03:36:23 2025 +0200
Added Slovak translations (#815)
* Created sk.json
* Update Navbar.svelte
* Update +layout.svelte
---------
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 904474d757577229b47441d1378a6fd6788fbe40
Merge: d4709434 f87a5fe3
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:31:58 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit d47094346c0b63ea753294a0786414e5e070ae7f
Merge: 4a5f59bf 6366a3eb
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:29:39 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit f87a5fe3bcc2fe28cfc206fb5cba517bbffa8df6
Author: Sergio <garcia.sergio@me.com>
Date: Sun Sep 7 01:12:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 4a5f59bfd24e32fdf3558b009a8f636636cb3663
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:06:17 2025 -0400
Fixes #654
commit c1302bb54ab272c2a98c53ce0d508b7d39e9674b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 20:52:05 2025 -0400
[BUG] Single day Collections will think location visits are out of date range
Fixes #827
commit 773f2d65bbfb2a9591b31fabfd6844612b840f1a
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:52:28 2025 -0400
[BUG] Server Error (500) when trying to access the API docs
Fixes #712
commit 4228db249ed5e3261931a1cdb3895d0ddd3ac4ac
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:44:00 2025 -0400
[BUG]Ordered Itinerary includes visits that are outside itinerary date range
Fixes #746
commit 26f36cabb0a860f10d7ba62b5279ddd1e282c78e
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 18:36:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 3bfd2dd5615afdbd04e3451c2ef728f1d7caf466
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 12:33:23 2025 -0400
Remove empty English (United States) locale file
commit 6366a3eba6ab72090e52be474212a663799dfe19
Author: Nikolai Eidsheim <nikolai.eidsheim@gmail.com>
Date: Sat Sep 6 18:10:15 2025 +0200
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/nb_NO/
commit 671cd3701fc5a601f2f1bad9aef93106f91eec0b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:58:04 2025 +0200
Added translation using Weblate (English (United States))
commit bdbbe5f4978f041f620f0503da69fa870cb1997c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:54:43 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 9d4f1b8f534a3cdfb22812f2a25ababd7a236a87
Author: Jacob <jacob.aulin@proton.me>
Date: Sat Sep 13 15:17:22 2025 +0200
Translated using Weblate (Swedish)
Currently translated at 99.8% (957 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sv/
commit 8fac40cfde425c989521c891b3ba9c75ab32e57e
Author: Christian S <schuld.christian@gmail.com>
Date: Sat Sep 13 12:54:52 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 8e8c42396ec77b763983155e8b1e89cabf38ce17
Author: Patricio Carrau <duckycb@proton.me>
Date: Tue Sep 9 21:59:48 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit be818ab408d00c5c26dfb3b25632604a415d3570
Author: pplulee <hi@pplulee.me>
Date: Mon Sep 8 04:06:54 2025 +0100
fix(i18n): update Chinese translations for location-related terms (#829)
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 9e40dcf6a1dc194d4694a114b3c7e88135121016
Merge: af2f2809 733eefce
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:03:57 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit af2f28090b9242fb7ab263fa5bbb95a5bcc1b27f
Author: Sean Morley <mail@seanmorley.com>
Date: Sun Sep 7 23:00:33 2025 -0400
[BUG] Location Visit End Date not affected by Location Timezone
Fixes #843
commit 733eefceddbdad01726364e5d4523605f095fde2
Author: Alex <div@alexe.at>
Date: Sun Sep 7 23:28:20 2025 +0200
Translated using Weblate (German)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/de/
commit 6c750d1c8f95b42418893e15ad46c3d4ed86d053
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 09:17:16 2025 +0200
Translated using Weblate (Slovak)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/sk/
commit f733b3b96bbddc71d426f2e60320a5ad2f6755af
Merge: 769ea6ad af4e541c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:35 2025 -0400
Merge branch 'development' of github.com:seanmorley15/AdventureLog into development
commit 769ea6ad710890e931aabace2c00dc37436f869f
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:36:33 2025 -0400
Implement code changes to enhance functionality and improve performance
commit af4e541c1c9e7309857102287199279aec339387
Author: fantastron27 <fantastron27@gmail.com>
Date: Sun Sep 7 03:36:23 2025 +0200
Added Slovak translations (#815)
* Created sk.json
* Update Navbar.svelte
* Update +layout.svelte
---------
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
commit 904474d757577229b47441d1378a6fd6788fbe40
Merge: d4709434 f87a5fe3
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:31:58 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit d47094346c0b63ea753294a0786414e5e070ae7f
Merge: 4a5f59bf 6366a3eb
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:29:39 2025 -0400
Merge remote-tracking branch 'weblate/development' into development
commit f87a5fe3bcc2fe28cfc206fb5cba517bbffa8df6
Author: Sergio <garcia.sergio@me.com>
Date: Sun Sep 7 01:12:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 4a5f59bfd24e32fdf3558b009a8f636636cb3663
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 21:06:17 2025 -0400
Fixes #654
commit c1302bb54ab272c2a98c53ce0d508b7d39e9674b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 20:52:05 2025 -0400
[BUG] Single day Collections will think location visits are out of date range
Fixes #827
commit 773f2d65bbfb2a9591b31fabfd6844612b840f1a
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:52:28 2025 -0400
[BUG] Server Error (500) when trying to access the API docs
Fixes #712
commit 4228db249ed5e3261931a1cdb3895d0ddd3ac4ac
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 19:44:00 2025 -0400
[BUG]Ordered Itinerary includes visits that are outside itinerary date range
Fixes #746
commit 26f36cabb0a860f10d7ba62b5279ddd1e282c78e
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 18:36:50 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
commit 3bfd2dd5615afdbd04e3451c2ef728f1d7caf466
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 12:33:23 2025 -0400
Remove empty English (United States) locale file
commit 6366a3eba6ab72090e52be474212a663799dfe19
Author: Nikolai Eidsheim <nikolai.eidsheim@gmail.com>
Date: Sat Sep 6 18:10:15 2025 +0200
Translated using Weblate (Norwegian Bokmål)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/nb_NO/
commit 671cd3701fc5a601f2f1bad9aef93106f91eec0b
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:58:04 2025 +0200
Added translation using Weblate (English (United States))
commit bdbbe5f4978f041f620f0503da69fa870cb1997c
Author: Sean Morley <mail@seanmorley.com>
Date: Sat Sep 6 16:54:43 2025 +0200
Translated using Weblate (Spanish)
Currently translated at 100.0% (956 of 956 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/es/
* Translated using Weblate (French)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/fr/
* Translated using Weblate (Turkish)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/tr/
* feat: enhance serializers and views for nested representations and lightweight data fetching
* feat: implement caching for visits by country and region endpoints
* feat: update LocationSerializer to include debug logging and adjust nested context handling in CollectionSerializer
* feat: enhance CollectionSerializer and CollectionViewSet to support nested context handling
* feat(worldtravel): add globe spin feature with loading and result display
- Introduced a toggle button for the globe spin feature.
- Implemented fetch logic for globe spin data from the API.
- Added loading state with a spinning globe animation.
- Displayed country information, including flag, subregion, and visit statistics.
- Enhanced UI with animations for loading and displaying results.
- Included error handling for API fetch failures.
* Add Turkish language support to the application
* feat(map): enhance pin styling with gradient backgrounds and improved hover effects
* Squashed commit of the following:
commit 990b0645059421c4a293fbf64830a6d864ceb40e
Author: Henrique Fonseca Veloso <henriquefv@tutamail.com>
Date: Sun Sep 21 00:27:59 2025 +0200
Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/pt_BR/
commit 03a4b9235faa849fb817348a2774e834a6851dc3
Author: Orhun <orhunavcu@gmail.com>
Date: Fri Sep 19 19:38:12 2025 +0200
Translated using Weblate (Turkish)
Currently translated at 100.0% (958 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/tr/
commit 1ccdc678627eb5915b56a8bfb3465928c80a524f
Author: Henrique Fonseca Veloso <henriquefv@tutamail.com>
Date: Sat Sep 20 20:01:36 2025 +0200
Translated using Weblate (Portuguese (Brazil))
Currently translated at 97.0% (930 of 958 strings)
Translation: AdventureLog/Web App
Translate-URL: https://hosted.weblate.org/projects/adventurelog/web-app/pt_BR/
* fix(config): update appVersion to v0.11.0-main-09212025
* fix duration calculation on ordered collection view (#867)
Co-authored-by: Sean Morley <98704938+seanmorley15@users.noreply.github.com>
* Update frontend/src/routes/worldtravel/+page.svelte
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update backend/server/worldtravel/views.py
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* Update frontend/src/routes/map/+page.svelte
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---------
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Rohan <Alchez@users.noreply.github.com>
Co-authored-by: Orhun <orhunavcu@gmail.com>
Co-authored-by: vorbeiei <vorbeiei@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -305,7 +305,7 @@ class Transportation(models.Model):
|
|||||||
name = models.CharField(max_length=200)
|
name = models.CharField(max_length=200)
|
||||||
description = models.TextField(blank=True, null=True)
|
description = models.TextField(blank=True, null=True)
|
||||||
rating = models.FloatField(blank=True, null=True)
|
rating = models.FloatField(blank=True, null=True)
|
||||||
link = models.URLField(blank=True, null=True)
|
link = models.URLField(blank=True, null=True, max_length=2083)
|
||||||
date = models.DateTimeField(blank=True, null=True)
|
date = models.DateTimeField(blank=True, null=True)
|
||||||
end_date = models.DateTimeField(blank=True, null=True)
|
end_date = models.DateTimeField(blank=True, null=True)
|
||||||
start_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
start_timezone = models.CharField(max_length=50, choices=[(tz, tz) for tz in TIMEZONES], null=True, blank=True)
|
||||||
|
|||||||
@@ -234,9 +234,26 @@ class LocationSerializer(CustomModelSerializer):
|
|||||||
# Makes it so the whole user object is returned in the serializer instead of just the user uuid
|
# Makes it so the whole user object is returned in the serializer instead of just the user uuid
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
representation = super().to_representation(instance)
|
representation = super().to_representation(instance)
|
||||||
|
is_nested = self.context.get('nested', False)
|
||||||
|
allowed_nested_fields = set(self.context.get('allowed_nested_fields', []))
|
||||||
|
|
||||||
|
if not is_nested:
|
||||||
|
# Full representation for standalone locations
|
||||||
representation['user'] = CustomUserDetailsSerializer(instance.user, context=self.context).data
|
representation['user'] = CustomUserDetailsSerializer(instance.user, context=self.context).data
|
||||||
|
else:
|
||||||
|
# Slim representation for nested contexts, but keep allowed fields
|
||||||
|
fields_to_remove = [
|
||||||
|
'visits', 'attachments', 'trails', 'collections',
|
||||||
|
'user', 'city', 'country', 'region'
|
||||||
|
]
|
||||||
|
for field in fields_to_remove:
|
||||||
|
# Keep field if explicitly allowed for nested mode
|
||||||
|
if field not in allowed_nested_fields:
|
||||||
|
representation.pop(field, None)
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
|
|
||||||
def get_images(self, obj):
|
def get_images(self, obj):
|
||||||
serializer = ContentImageSerializer(obj.images.all(), many=True, context=self.context)
|
serializer = ContentImageSerializer(obj.images.all(), many=True, context=self.context)
|
||||||
# Filter out None values from the serialized data
|
# Filter out None values from the serialized data
|
||||||
@@ -349,7 +366,6 @@ class LocationSerializer(CustomModelSerializer):
|
|||||||
category_data = validated_data.pop('category', None)
|
category_data = validated_data.pop('category', None)
|
||||||
collections_data = validated_data.pop('collections', [])
|
collections_data = validated_data.pop('collections', [])
|
||||||
|
|
||||||
print(category_data)
|
|
||||||
location = Location.objects.create(**validated_data)
|
location = Location.objects.create(**validated_data)
|
||||||
|
|
||||||
# Handle category
|
# Handle category
|
||||||
@@ -392,6 +408,18 @@ class LocationSerializer(CustomModelSerializer):
|
|||||||
|
|
||||||
return instance
|
return instance
|
||||||
|
|
||||||
|
class MapPinSerializer(serializers.ModelSerializer):
|
||||||
|
is_visited = serializers.SerializerMethodField()
|
||||||
|
category = CategorySerializer(read_only=True, required=False)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
model = Location
|
||||||
|
fields = ['id', 'name', 'latitude', 'longitude', 'is_visited', 'category']
|
||||||
|
read_only_fields = ['id', 'name', 'latitude', 'longitude', 'is_visited', 'category']
|
||||||
|
|
||||||
|
def get_is_visited(self, obj):
|
||||||
|
return obj.is_visited_status()
|
||||||
|
|
||||||
class TransportationSerializer(CustomModelSerializer):
|
class TransportationSerializer(CustomModelSerializer):
|
||||||
distance = serializers.SerializerMethodField()
|
distance = serializers.SerializerMethodField()
|
||||||
images = serializers.SerializerMethodField()
|
images = serializers.SerializerMethodField()
|
||||||
@@ -555,24 +583,67 @@ class ChecklistSerializer(CustomModelSerializer):
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
class CollectionSerializer(CustomModelSerializer):
|
class CollectionSerializer(CustomModelSerializer):
|
||||||
locations = LocationSerializer(many=True, read_only=True)
|
locations = serializers.SerializerMethodField()
|
||||||
transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set')
|
transportations = serializers.SerializerMethodField()
|
||||||
notes = NoteSerializer(many=True, read_only=True, source='note_set')
|
notes = serializers.SerializerMethodField()
|
||||||
checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set')
|
checklists = serializers.SerializerMethodField()
|
||||||
lodging = LodgingSerializer(many=True, read_only=True, source='lodging_set')
|
lodging = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = Collection
|
model = Collection
|
||||||
fields = ['id', 'description', 'user', 'name', 'is_public', 'locations', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
|
fields = ['id', 'description', 'user', 'name', 'is_public', 'locations', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'lodging']
|
||||||
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'shared_with']
|
read_only_fields = ['id', 'created_at', 'updated_at', 'user', 'shared_with']
|
||||||
|
|
||||||
|
def get_locations(self, obj):
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
allowed_nested_fields = set(self.context.get('allowed_nested_fields', []))
|
||||||
|
return LocationSerializer(
|
||||||
|
obj.locations.all(),
|
||||||
|
many=True,
|
||||||
|
context={**self.context, 'nested': True, 'allowed_nested_fields': allowed_nested_fields}
|
||||||
|
).data
|
||||||
|
|
||||||
|
return LocationSerializer(obj.locations.all(), many=True, context=self.context).data
|
||||||
|
|
||||||
|
def get_transportations(self, obj):
|
||||||
|
# Only include transportations if not in nested context
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
return []
|
||||||
|
return TransportationSerializer(obj.transportation_set.all(), many=True, context=self.context).data
|
||||||
|
|
||||||
|
def get_notes(self, obj):
|
||||||
|
# Only include notes if not in nested context
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
return []
|
||||||
|
return NoteSerializer(obj.note_set.all(), many=True, context=self.context).data
|
||||||
|
|
||||||
|
def get_checklists(self, obj):
|
||||||
|
# Only include checklists if not in nested context
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
return []
|
||||||
|
return ChecklistSerializer(obj.checklist_set.all(), many=True, context=self.context).data
|
||||||
|
|
||||||
|
def get_lodging(self, obj):
|
||||||
|
# Only include lodging if not in nested context
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
return []
|
||||||
|
return LodgingSerializer(obj.lodging_set.all(), many=True, context=self.context).data
|
||||||
|
|
||||||
def to_representation(self, instance):
|
def to_representation(self, instance):
|
||||||
representation = super().to_representation(instance)
|
representation = super().to_representation(instance)
|
||||||
|
|
||||||
# Make it display the user uuid for the shared users instead of the PK
|
# Make it display the user uuid for the shared users instead of the PK
|
||||||
shared_uuids = []
|
shared_uuids = []
|
||||||
for user in instance.shared_with.all():
|
for user in instance.shared_with.all():
|
||||||
shared_uuids.append(str(user.uuid))
|
shared_uuids.append(str(user.uuid))
|
||||||
representation['shared_with'] = shared_uuids
|
representation['shared_with'] = shared_uuids
|
||||||
|
|
||||||
|
# If nested, remove the heavy fields entirely from the response
|
||||||
|
if self.context.get('nested', False):
|
||||||
|
fields_to_remove = ['transportations', 'notes', 'checklists', 'lodging']
|
||||||
|
for field in fields_to_remove:
|
||||||
|
representation.pop(field, None)
|
||||||
|
|
||||||
return representation
|
return representation
|
||||||
|
|
||||||
class CollectionInviteSerializer(serializers.ModelSerializer):
|
class CollectionInviteSerializer(serializers.ModelSerializer):
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ TIMEZONES = [
|
|||||||
"Africa/Accra",
|
"Africa/Accra",
|
||||||
"Africa/Addis_Ababa",
|
"Africa/Addis_Ababa",
|
||||||
"Africa/Algiers",
|
"Africa/Algiers",
|
||||||
|
"Africa/Asmara",
|
||||||
"Africa/Asmera",
|
"Africa/Asmera",
|
||||||
"Africa/Bamako",
|
"Africa/Bamako",
|
||||||
"Africa/Bangui",
|
"Africa/Bangui",
|
||||||
@@ -56,7 +57,12 @@ TIMEZONES = [
|
|||||||
"America/Anguilla",
|
"America/Anguilla",
|
||||||
"America/Antigua",
|
"America/Antigua",
|
||||||
"America/Araguaina",
|
"America/Araguaina",
|
||||||
|
"America/Argentina/Buenos_Aires",
|
||||||
|
"America/Argentina/Catamarca",
|
||||||
|
"America/Argentina/Cordoba",
|
||||||
|
"America/Argentina/Jujuy",
|
||||||
"America/Argentina/La_Rioja",
|
"America/Argentina/La_Rioja",
|
||||||
|
"America/Argentina/Mendoza",
|
||||||
"America/Argentina/Rio_Gallegos",
|
"America/Argentina/Rio_Gallegos",
|
||||||
"America/Argentina/Salta",
|
"America/Argentina/Salta",
|
||||||
"America/Argentina/San_Juan",
|
"America/Argentina/San_Juan",
|
||||||
@@ -65,6 +71,7 @@ TIMEZONES = [
|
|||||||
"America/Argentina/Ushuaia",
|
"America/Argentina/Ushuaia",
|
||||||
"America/Aruba",
|
"America/Aruba",
|
||||||
"America/Asuncion",
|
"America/Asuncion",
|
||||||
|
"America/Atikokan",
|
||||||
"America/Bahia",
|
"America/Bahia",
|
||||||
"America/Bahia_Banderas",
|
"America/Bahia_Banderas",
|
||||||
"America/Barbados",
|
"America/Barbados",
|
||||||
@@ -88,6 +95,7 @@ TIMEZONES = [
|
|||||||
"America/Coral_Harbour",
|
"America/Coral_Harbour",
|
||||||
"America/Cordoba",
|
"America/Cordoba",
|
||||||
"America/Costa_Rica",
|
"America/Costa_Rica",
|
||||||
|
"America/Coyhaique",
|
||||||
"America/Creston",
|
"America/Creston",
|
||||||
"America/Cuiaba",
|
"America/Cuiaba",
|
||||||
"America/Curacao",
|
"America/Curacao",
|
||||||
@@ -114,6 +122,7 @@ TIMEZONES = [
|
|||||||
"America/Halifax",
|
"America/Halifax",
|
||||||
"America/Havana",
|
"America/Havana",
|
||||||
"America/Hermosillo",
|
"America/Hermosillo",
|
||||||
|
"America/Indiana/Indianapolis",
|
||||||
"America/Indiana/Knox",
|
"America/Indiana/Knox",
|
||||||
"America/Indiana/Marengo",
|
"America/Indiana/Marengo",
|
||||||
"America/Indiana/Petersburg",
|
"America/Indiana/Petersburg",
|
||||||
@@ -127,6 +136,7 @@ TIMEZONES = [
|
|||||||
"America/Jamaica",
|
"America/Jamaica",
|
||||||
"America/Jujuy",
|
"America/Jujuy",
|
||||||
"America/Juneau",
|
"America/Juneau",
|
||||||
|
"America/Kentucky/Louisville",
|
||||||
"America/Kentucky/Monticello",
|
"America/Kentucky/Monticello",
|
||||||
"America/Kralendijk",
|
"America/Kralendijk",
|
||||||
"America/La_Paz",
|
"America/La_Paz",
|
||||||
@@ -158,6 +168,7 @@ TIMEZONES = [
|
|||||||
"America/North_Dakota/Beulah",
|
"America/North_Dakota/Beulah",
|
||||||
"America/North_Dakota/Center",
|
"America/North_Dakota/Center",
|
||||||
"America/North_Dakota/New_Salem",
|
"America/North_Dakota/New_Salem",
|
||||||
|
"America/Nuuk",
|
||||||
"America/Ojinaga",
|
"America/Ojinaga",
|
||||||
"America/Panama",
|
"America/Panama",
|
||||||
"America/Paramaribo",
|
"America/Paramaribo",
|
||||||
@@ -233,6 +244,7 @@ TIMEZONES = [
|
|||||||
"Asia/Famagusta",
|
"Asia/Famagusta",
|
||||||
"Asia/Gaza",
|
"Asia/Gaza",
|
||||||
"Asia/Hebron",
|
"Asia/Hebron",
|
||||||
|
"Asia/Ho_Chi_Minh",
|
||||||
"Asia/Hong_Kong",
|
"Asia/Hong_Kong",
|
||||||
"Asia/Hovd",
|
"Asia/Hovd",
|
||||||
"Asia/Irkutsk",
|
"Asia/Irkutsk",
|
||||||
@@ -243,7 +255,9 @@ TIMEZONES = [
|
|||||||
"Asia/Kamchatka",
|
"Asia/Kamchatka",
|
||||||
"Asia/Karachi",
|
"Asia/Karachi",
|
||||||
"Asia/Katmandu",
|
"Asia/Katmandu",
|
||||||
|
"Asia/Kathmandu",
|
||||||
"Asia/Khandyga",
|
"Asia/Khandyga",
|
||||||
|
"Asia/Kolkata",
|
||||||
"Asia/Krasnoyarsk",
|
"Asia/Krasnoyarsk",
|
||||||
"Asia/Kuala_Lumpur",
|
"Asia/Kuala_Lumpur",
|
||||||
"Asia/Kuching",
|
"Asia/Kuching",
|
||||||
@@ -286,6 +300,7 @@ TIMEZONES = [
|
|||||||
"Asia/Vientiane",
|
"Asia/Vientiane",
|
||||||
"Asia/Vladivostok",
|
"Asia/Vladivostok",
|
||||||
"Asia/Yakutsk",
|
"Asia/Yakutsk",
|
||||||
|
"Asia/Yangon",
|
||||||
"Asia/Yekaterinburg",
|
"Asia/Yekaterinburg",
|
||||||
"Asia/Yerevan",
|
"Asia/Yerevan",
|
||||||
"Atlantic/Azores",
|
"Atlantic/Azores",
|
||||||
@@ -309,6 +324,32 @@ TIMEZONES = [
|
|||||||
"Australia/Melbourne",
|
"Australia/Melbourne",
|
||||||
"Australia/Perth",
|
"Australia/Perth",
|
||||||
"Australia/Sydney",
|
"Australia/Sydney",
|
||||||
|
"Etc/GMT+1",
|
||||||
|
"Etc/GMT+10",
|
||||||
|
"Etc/GMT+11",
|
||||||
|
"Etc/GMT+12",
|
||||||
|
"Etc/GMT+2",
|
||||||
|
"Etc/GMT+3",
|
||||||
|
"Etc/GMT+4",
|
||||||
|
"Etc/GMT+5",
|
||||||
|
"Etc/GMT+6",
|
||||||
|
"Etc/GMT+7",
|
||||||
|
"Etc/GMT+8",
|
||||||
|
"Etc/GMT+9",
|
||||||
|
"Etc/GMT-1",
|
||||||
|
"Etc/GMT-10",
|
||||||
|
"Etc/GMT-11",
|
||||||
|
"Etc/GMT-12",
|
||||||
|
"Etc/GMT-13",
|
||||||
|
"Etc/GMT-14",
|
||||||
|
"Etc/GMT-2",
|
||||||
|
"Etc/GMT-3",
|
||||||
|
"Etc/GMT-4",
|
||||||
|
"Etc/GMT-5",
|
||||||
|
"Etc/GMT-6",
|
||||||
|
"Etc/GMT-7",
|
||||||
|
"Etc/GMT-8",
|
||||||
|
"Etc/GMT-9",
|
||||||
"Europe/Amsterdam",
|
"Europe/Amsterdam",
|
||||||
"Europe/Andorra",
|
"Europe/Andorra",
|
||||||
"Europe/Astrakhan",
|
"Europe/Astrakhan",
|
||||||
@@ -332,6 +373,7 @@ TIMEZONES = [
|
|||||||
"Europe/Kaliningrad",
|
"Europe/Kaliningrad",
|
||||||
"Europe/Kiev",
|
"Europe/Kiev",
|
||||||
"Europe/Kirov",
|
"Europe/Kirov",
|
||||||
|
"Europe/Kyiv",
|
||||||
"Europe/Lisbon",
|
"Europe/Lisbon",
|
||||||
"Europe/Ljubljana",
|
"Europe/Ljubljana",
|
||||||
"Europe/London",
|
"Europe/London",
|
||||||
@@ -382,6 +424,7 @@ TIMEZONES = [
|
|||||||
"Pacific/Auckland",
|
"Pacific/Auckland",
|
||||||
"Pacific/Bougainville",
|
"Pacific/Bougainville",
|
||||||
"Pacific/Chatham",
|
"Pacific/Chatham",
|
||||||
|
"Pacific/Chuuk",
|
||||||
"Pacific/Easter",
|
"Pacific/Easter",
|
||||||
"Pacific/Efate",
|
"Pacific/Efate",
|
||||||
"Pacific/Enderbury",
|
"Pacific/Enderbury",
|
||||||
@@ -393,6 +436,7 @@ TIMEZONES = [
|
|||||||
"Pacific/Guadalcanal",
|
"Pacific/Guadalcanal",
|
||||||
"Pacific/Guam",
|
"Pacific/Guam",
|
||||||
"Pacific/Honolulu",
|
"Pacific/Honolulu",
|
||||||
|
"Pacific/Kanton",
|
||||||
"Pacific/Kiritimati",
|
"Pacific/Kiritimati",
|
||||||
"Pacific/Kosrae",
|
"Pacific/Kosrae",
|
||||||
"Pacific/Kwajalein",
|
"Pacific/Kwajalein",
|
||||||
@@ -407,6 +451,7 @@ TIMEZONES = [
|
|||||||
"Pacific/Palau",
|
"Pacific/Palau",
|
||||||
"Pacific/Pitcairn",
|
"Pacific/Pitcairn",
|
||||||
"Pacific/Ponape",
|
"Pacific/Ponape",
|
||||||
|
"Pacific/Pohnpei",
|
||||||
"Pacific/Port_Moresby",
|
"Pacific/Port_Moresby",
|
||||||
"Pacific/Rarotonga",
|
"Pacific/Rarotonga",
|
||||||
"Pacific/Saipan",
|
"Pacific/Saipan",
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||||||
permission_classes = [CollectionShared]
|
permission_classes = [CollectionShared]
|
||||||
pagination_class = pagination.StandardResultsSetPagination
|
pagination_class = pagination.StandardResultsSetPagination
|
||||||
|
|
||||||
|
|
||||||
def apply_sorting(self, queryset):
|
def apply_sorting(self, queryset):
|
||||||
order_by = self.request.query_params.get('order_by', 'name')
|
order_by = self.request.query_params.get('order_by', 'name')
|
||||||
order_direction = self.request.query_params.get('order_direction', 'asc')
|
order_direction = self.request.query_params.get('order_direction', 'asc')
|
||||||
@@ -48,14 +47,36 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return queryset.order_by(ordering)
|
return queryset.order_by(ordering)
|
||||||
|
|
||||||
def list(self, request, *args, **kwargs):
|
def get_serializer_context(self):
|
||||||
|
"""Override to add nested and exclusion contexts based on query parameters"""
|
||||||
|
context = super().get_serializer_context()
|
||||||
|
|
||||||
|
# Handle nested parameter
|
||||||
|
is_nested = self.request.query_params.get('nested', 'false').lower() == 'true'
|
||||||
|
if is_nested:
|
||||||
|
context['nested'] = True
|
||||||
|
|
||||||
|
# Handle individual exclusion parameters (if using granular approach)
|
||||||
|
exclude_params = [
|
||||||
|
'exclude_transportations',
|
||||||
|
'exclude_notes',
|
||||||
|
'exclude_checklists',
|
||||||
|
'exclude_lodging'
|
||||||
|
]
|
||||||
|
|
||||||
|
for param in exclude_params:
|
||||||
|
if self.request.query_params.get(param, 'false').lower() == 'true':
|
||||||
|
context[param] = True
|
||||||
|
|
||||||
|
return context
|
||||||
|
|
||||||
|
def list(self, request):
|
||||||
# make sure the user is authenticated
|
# make sure the user is authenticated
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return Response({"error": "User is not authenticated"}, status=400)
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
queryset = Collection.objects.filter(user=request.user, is_archived=False)
|
queryset = Collection.objects.filter(user=request.user, is_archived=False)
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
collections = self.paginate_and_respond(queryset, request)
|
return self.paginate_and_respond(queryset, request)
|
||||||
return collections
|
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def all(self, request):
|
def all(self, request):
|
||||||
@@ -415,3 +436,12 @@ class CollectionViewSet(viewsets.ModelViewSet):
|
|||||||
return paginator.get_paginated_response(serializer.data)
|
return paginator.get_paginated_response(serializer.data)
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
def get_serializer(self, *args, **kwargs):
|
||||||
|
# Add nested=True to serializer context for GET list requests
|
||||||
|
context = self.get_serializer_context()
|
||||||
|
# If this is a list action, make sure nested=True in context
|
||||||
|
if self.action == 'list':
|
||||||
|
context['nested'] = True
|
||||||
|
kwargs['context'] = context
|
||||||
|
return super().get_serializer(*args, **kwargs)
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ class IcsCalendarGeneratorViewSet(viewsets.ViewSet):
|
|||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def generate(self, request):
|
def generate(self, request):
|
||||||
locations = Location.objects.filter(user=request.user)
|
locations = Location.objects.filter(user=request.user)
|
||||||
serializer = LocationSerializer(locations, many=True)
|
context={'nested': True, 'allowed_nested_fields': ['visits']}
|
||||||
|
serializer = LocationSerializer(locations, many=True, context=context)
|
||||||
user = request.user
|
user = request.user
|
||||||
name = f"{user.first_name} {user.last_name}"
|
name = f"{user.first_name} {user.last_name}"
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from rest_framework.response import Response
|
|||||||
import requests
|
import requests
|
||||||
from adventures.models import Location, Category
|
from adventures.models import Location, Category
|
||||||
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
from adventures.permissions import IsOwnerOrSharedWithFullAccess
|
||||||
from adventures.serializers import LocationSerializer
|
from adventures.serializers import LocationSerializer, MapPinSerializer
|
||||||
from adventures.utils import pagination
|
from adventures.utils import pagination
|
||||||
|
|
||||||
class LocationViewSet(viewsets.ModelViewSet):
|
class LocationViewSet(viewsets.ModelViewSet):
|
||||||
@@ -193,6 +193,8 @@ class LocationViewSet(viewsets.ModelViewSet):
|
|||||||
return Response({"error": "User is not authenticated"}, status=400)
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
|
|
||||||
include_collections = request.query_params.get('include_collections', 'false') == 'true'
|
include_collections = request.query_params.get('include_collections', 'false') == 'true'
|
||||||
|
nested = request.query_params.get('nested', 'false') == 'true'
|
||||||
|
allowedNestedFields = request.query_params.get('allowed_nested_fields', '').split(',')
|
||||||
|
|
||||||
# Build queryset with collection filtering
|
# Build queryset with collection filtering
|
||||||
base_filter = Q(user=request.user.id)
|
base_filter = Q(user=request.user.id)
|
||||||
@@ -203,7 +205,7 @@ class LocationViewSet(viewsets.ModelViewSet):
|
|||||||
queryset = Location.objects.filter(base_filter, collections__isnull=True)
|
queryset = Location.objects.filter(base_filter, collections__isnull=True)
|
||||||
|
|
||||||
queryset = self.apply_sorting(queryset)
|
queryset = self.apply_sorting(queryset)
|
||||||
serializer = self.get_serializer(queryset, many=True)
|
serializer = self.get_serializer(queryset, many=True, context={'nested': nested, 'allowed_nested_fields': allowedNestedFields})
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
@action(detail=True, methods=['get'], url_path='additional-info')
|
@action(detail=True, methods=['get'], url_path='additional-info')
|
||||||
@@ -228,6 +230,17 @@ class LocationViewSet(viewsets.ModelViewSet):
|
|||||||
|
|
||||||
return Response(response_data)
|
return Response(response_data)
|
||||||
|
|
||||||
|
# view to return location name and lat/lon for all locations a user owns for the golobal map
|
||||||
|
@action(detail=False, methods=['get'], url_path='pins')
|
||||||
|
def map_locations(self, request):
|
||||||
|
"""Get all locations with name and lat/lon for map display."""
|
||||||
|
if not request.user.is_authenticated:
|
||||||
|
return Response({"error": "User is not authenticated"}, status=400)
|
||||||
|
|
||||||
|
locations = Location.objects.filter(user=request.user)
|
||||||
|
serializer = MapPinSerializer(locations, many=True)
|
||||||
|
return Response(serializer.data)
|
||||||
|
|
||||||
# ==================== HELPER METHODS ====================
|
# ==================== HELPER METHODS ====================
|
||||||
|
|
||||||
def _validate_collection_update_permissions(self, instance, new_collections):
|
def _validate_collection_update_permissions(self, instance, new_collections):
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
from django.urls import include, path
|
from django.urls import include, path
|
||||||
from rest_framework.routers import DefaultRouter
|
from rest_framework.routers import DefaultRouter
|
||||||
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, cities_by_region, VisitedCityViewSet, visits_by_region
|
from .views import CountryViewSet, RegionViewSet, VisitedRegionViewSet, regions_by_country, visits_by_country, cities_by_region, VisitedCityViewSet, visits_by_region, globespin
|
||||||
router = DefaultRouter()
|
router = DefaultRouter()
|
||||||
router.register(r'countries', CountryViewSet, basename='countries')
|
router.register(r'countries', CountryViewSet, basename='countries')
|
||||||
router.register(r'regions', RegionViewSet, basename='regions')
|
router.register(r'regions', RegionViewSet, basename='regions')
|
||||||
@@ -15,4 +15,5 @@ urlpatterns = [
|
|||||||
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
|
path('<str:country_code>/visits/', visits_by_country, name='visits-by-country'),
|
||||||
path('regions/<str:region_id>/cities/', cities_by_region, name='cities-by-region'),
|
path('regions/<str:region_id>/cities/', cities_by_region, name='cities-by-region'),
|
||||||
path('regions/<str:region_id>/cities/visits/', visits_by_region, name='visits-by-region'),
|
path('regions/<str:region_id>/cities/visits/', visits_by_region, name='visits-by-region'),
|
||||||
|
path('globespin/', globespin, name='globespin'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,24 +1,23 @@
|
|||||||
from django.shortcuts import render
|
from django.shortcuts import render, get_object_or_404
|
||||||
from .models import Country, Region, VisitedRegion, City, VisitedCity
|
from .models import Country, Region, VisitedRegion, City, VisitedCity
|
||||||
from .serializers import CitySerializer, CountrySerializer, RegionSerializer, VisitedRegionSerializer, VisitedCitySerializer
|
from .serializers import CitySerializer, CountrySerializer, RegionSerializer, VisitedRegionSerializer, VisitedCitySerializer
|
||||||
from rest_framework import viewsets, status
|
from rest_framework import viewsets, status
|
||||||
from rest_framework.permissions import IsAuthenticated
|
from rest_framework.permissions import IsAuthenticated
|
||||||
from django.shortcuts import get_object_or_404
|
|
||||||
from rest_framework.response import Response
|
from rest_framework.response import Response
|
||||||
from rest_framework.decorators import api_view, permission_classes
|
from rest_framework.decorators import api_view, permission_classes, action
|
||||||
import os
|
|
||||||
import json
|
|
||||||
from django.http import JsonResponse
|
|
||||||
from django.contrib.gis.geos import Point
|
from django.contrib.gis.geos import Point
|
||||||
from django.conf import settings
|
from django.core.cache import cache
|
||||||
from rest_framework.decorators import action
|
from django.views.decorators.cache import cache_page
|
||||||
from django.contrib.staticfiles import finders
|
from django.utils.decorators import method_decorator
|
||||||
from adventures.models import Location
|
from adventures.models import Location
|
||||||
|
|
||||||
|
# Cache TTL
|
||||||
|
CACHE_TTL = 60 * 60 * 24 # 1 day
|
||||||
|
|
||||||
|
@cache_page(CACHE_TTL)
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def regions_by_country(request, country_code):
|
def regions_by_country(request, country_code):
|
||||||
# require authentication
|
|
||||||
country = get_object_or_404(Country, country_code=country_code)
|
country = get_object_or_404(Country, country_code=country_code)
|
||||||
regions = Region.objects.filter(country=country).order_by('name')
|
regions = Region.objects.filter(country=country).order_by('name')
|
||||||
serializer = RegionSerializer(regions, many=True)
|
serializer = RegionSerializer(regions, many=True)
|
||||||
@@ -27,12 +26,17 @@ def regions_by_country(request, country_code):
|
|||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def visits_by_country(request, country_code):
|
def visits_by_country(request, country_code):
|
||||||
|
cache_key = f"visits_by_country_{country_code}_{request.user.id}"
|
||||||
|
data = cache.get(cache_key)
|
||||||
|
if data is not None:
|
||||||
|
return Response(data)
|
||||||
country = get_object_or_404(Country, country_code=country_code)
|
country = get_object_or_404(Country, country_code=country_code)
|
||||||
visits = VisitedRegion.objects.filter(region__country=country, user=request.user.id)
|
visits = VisitedRegion.objects.filter(region__country=country, user=request.user.id)
|
||||||
|
|
||||||
serializer = VisitedRegionSerializer(visits, many=True)
|
serializer = VisitedRegionSerializer(visits, many=True)
|
||||||
|
cache.set(cache_key, serializer.data, CACHE_TTL)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
@cache_page(CACHE_TTL)
|
||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def cities_by_region(request, region_id):
|
def cities_by_region(request, region_id):
|
||||||
@@ -44,12 +48,38 @@ def cities_by_region(request, region_id):
|
|||||||
@api_view(['GET'])
|
@api_view(['GET'])
|
||||||
@permission_classes([IsAuthenticated])
|
@permission_classes([IsAuthenticated])
|
||||||
def visits_by_region(request, region_id):
|
def visits_by_region(request, region_id):
|
||||||
|
cache_key = f"visits_by_region_{region_id}_{request.user.id}"
|
||||||
|
data = cache.get(cache_key)
|
||||||
|
if data is not None:
|
||||||
|
return Response(data)
|
||||||
region = get_object_or_404(Region, id=region_id)
|
region = get_object_or_404(Region, id=region_id)
|
||||||
visits = VisitedCity.objects.filter(city__region=region, user=request.user.id)
|
visits = VisitedCity.objects.filter(city__region=region, user=request.user.id)
|
||||||
|
|
||||||
serializer = VisitedCitySerializer(visits, many=True)
|
serializer = VisitedCitySerializer(visits, many=True)
|
||||||
|
cache.set(cache_key, serializer.data, CACHE_TTL)
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
# view called spin the globe that return a random country, a random region in that country and a random city in that region
|
||||||
|
@api_view(['GET'])
|
||||||
|
@permission_classes([IsAuthenticated])
|
||||||
|
def globespin(request):
|
||||||
|
country = Country.objects.order_by('?').first()
|
||||||
|
data = {
|
||||||
|
"country": CountrySerializer(country).data,
|
||||||
|
}
|
||||||
|
|
||||||
|
regions = Region.objects.filter(country=country)
|
||||||
|
if regions.exists():
|
||||||
|
region = regions.order_by('?').first()
|
||||||
|
data["region"] = RegionSerializer(region).data
|
||||||
|
|
||||||
|
cities = City.objects.filter(region=region)
|
||||||
|
if cities.exists():
|
||||||
|
city = cities.order_by('?').first()
|
||||||
|
data["city"] = CitySerializer(city).data
|
||||||
|
|
||||||
|
return Response(data)
|
||||||
|
|
||||||
|
@method_decorator(cache_page(CACHE_TTL), name='list')
|
||||||
class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = Country.objects.all().order_by('name')
|
queryset = Country.objects.all().order_by('name')
|
||||||
serializer_class = CountrySerializer
|
serializer_class = CountrySerializer
|
||||||
@@ -60,15 +90,12 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
lat = float(request.query_params.get('lat'))
|
lat = float(request.query_params.get('lat'))
|
||||||
lon = float(request.query_params.get('lon'))
|
lon = float(request.query_params.get('lon'))
|
||||||
point = Point(lon, lat, srid=4326)
|
point = Point(lon, lat, srid=4326)
|
||||||
|
|
||||||
region = Region.objects.filter(geometry__contains=point).first()
|
region = Region.objects.filter(geometry__contains=point).first()
|
||||||
|
|
||||||
if region:
|
if region:
|
||||||
return Response({'in_region': True, 'region_name': region.name, 'region_id': region.id})
|
return Response({'in_region': True, 'region_name': region.name, 'region_id': region.id})
|
||||||
else:
|
else:
|
||||||
return Response({'in_region': False})
|
return Response({'in_region': False})
|
||||||
|
|
||||||
# make a post action that will get all of the users adventures and check if the point is in any of the regions if so make a visited region object for that user if it does not already exist
|
|
||||||
@action(detail=False, methods=['post'])
|
@action(detail=False, methods=['post'])
|
||||||
def region_check_all_adventures(self, request):
|
def region_check_all_adventures(self, request):
|
||||||
adventures = Location.objects.filter(user=request.user.id, type='visited')
|
adventures = Location.objects.filter(user=request.user.id, type='visited')
|
||||||
@@ -87,6 +114,7 @@ class CountryViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
continue
|
continue
|
||||||
return Response({'regions_visited': count})
|
return Response({'regions_visited': count})
|
||||||
|
|
||||||
|
@method_decorator(cache_page(CACHE_TTL), name='list')
|
||||||
class RegionViewSet(viewsets.ReadOnlyModelViewSet):
|
class RegionViewSet(viewsets.ReadOnlyModelViewSet):
|
||||||
queryset = Region.objects.all()
|
queryset = Region.objects.all()
|
||||||
serializer_class = RegionSerializer
|
serializer_class = RegionSerializer
|
||||||
@@ -113,7 +141,6 @@ class VisitedRegionViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
def destroy(self, request, **kwargs):
|
def destroy(self, request, **kwargs):
|
||||||
# delete by region id
|
|
||||||
region = get_object_or_404(Region, id=kwargs['pk'])
|
region = get_object_or_404(Region, id=kwargs['pk'])
|
||||||
visited_region = VisitedRegion.objects.filter(user=request.user.id, region=region)
|
visited_region = VisitedRegion.objects.filter(user=request.user.id, region=region)
|
||||||
if visited_region.exists():
|
if visited_region.exists():
|
||||||
@@ -137,7 +164,6 @@ class VisitedCityViewSet(viewsets.ModelViewSet):
|
|||||||
serializer = self.get_serializer(data=request.data)
|
serializer = self.get_serializer(data=request.data)
|
||||||
serializer.is_valid(raise_exception=True)
|
serializer.is_valid(raise_exception=True)
|
||||||
self.perform_create(serializer)
|
self.perform_create(serializer)
|
||||||
# if the region is not visited, visit it
|
|
||||||
region = serializer.validated_data['city'].region
|
region = serializer.validated_data['city'].region
|
||||||
if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists():
|
if not VisitedRegion.objects.filter(user=request.user.id, region=region).exists():
|
||||||
VisitedRegion.objects.create(user=request.user, region=region)
|
VisitedRegion.objects.create(user=request.user, region=region)
|
||||||
@@ -145,7 +171,6 @@ class VisitedCityViewSet(viewsets.ModelViewSet):
|
|||||||
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
|
||||||
|
|
||||||
def destroy(self, request, **kwargs):
|
def destroy(self, request, **kwargs):
|
||||||
# delete by city id
|
|
||||||
city = get_object_or_404(City, id=kwargs['pk'])
|
city = get_object_or_404(City, id=kwargs['pk'])
|
||||||
visited_city = VisitedCity.objects.filter(user=request.user.id, city=city)
|
visited_city = VisitedCity.objects.filter(user=request.user.id, city=city)
|
||||||
if visited_city.exists():
|
if visited_city.exists():
|
||||||
|
|||||||
5551
frontend/package-lock.json
generated
Normal file
5551
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {}
|
||||||
},
|
}
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -43,8 +43,8 @@
|
|||||||
try {
|
try {
|
||||||
// Fetch both own collections and shared collections
|
// Fetch both own collections and shared collections
|
||||||
const [ownRes, sharedRes] = await Promise.all([
|
const [ownRes, sharedRes] = await Promise.all([
|
||||||
fetch(`/api/collections/all/`, { method: 'GET' }),
|
fetch(`/api/collections/all?nested=true`, { method: 'GET' }),
|
||||||
fetch(`/api/collections/shared`, { method: 'GET' })
|
fetch(`/api/collections/shared?nested=true`, { method: 'GET' })
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const ownResult = await ownRes.json();
|
const ownResult = await ownRes.json();
|
||||||
|
|||||||
@@ -52,10 +52,12 @@
|
|||||||
let outsideCollectionRange: boolean = false;
|
let outsideCollectionRange: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
if (collection) {
|
if (collection && collection.start_date && collection.end_date) {
|
||||||
outsideCollectionRange = adventure.visits.every((visit) =>
|
outsideCollectionRange = adventure.visits.every((visit) =>
|
||||||
isEntityOutsideCollectionDateRange(visit, collection)
|
isEntityOutsideCollectionDateRange(visit, collection)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
outsideCollectionRange = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,7 +64,8 @@
|
|||||||
ja: '日本語',
|
ja: '日本語',
|
||||||
ar: 'العربية',
|
ar: 'العربية',
|
||||||
'pt-br': 'Português (Brasil)',
|
'pt-br': 'Português (Brasil)',
|
||||||
'sk': 'Slovenský'
|
sk: 'Slovenský',
|
||||||
|
tr: 'Türkçe'
|
||||||
};
|
};
|
||||||
|
|
||||||
const submitLocaleChange = (event: Event) => {
|
const submitLocaleChange = (event: Event) => {
|
||||||
|
|||||||
@@ -899,6 +899,9 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<ClockIcon class="w-3 h-3 text-base-content/50" />
|
<ClockIcon class="w-3 h-3 text-base-content/50" />
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if visit.timezone && !isAllDay(visit.start_date)}
|
||||||
|
<span class="badge badge-outline badge-sm">{visit.timezone}</span>
|
||||||
|
{/if}
|
||||||
<div class="text-sm font-medium truncate">
|
<div class="text-sm font-medium truncate">
|
||||||
{#if isAllDay(visit.start_date)}
|
{#if isAllDay(visit.start_date)}
|
||||||
{visit.start_date && typeof visit.start_date === 'string'
|
{visit.start_date && typeof visit.start_date === 'string'
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export let appVersion = 'v0.11.0-main-09172025';
|
export let appVersion = 'v0.11.0-main-09212025';
|
||||||
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.11.0';
|
export let versionChangelog = 'https://github.com/seanmorley15/AdventureLog/releases/tag/v0.11.0';
|
||||||
export let appTitle = 'AdventureLog';
|
export let appTitle = 'AdventureLog';
|
||||||
export let copyrightYear = '2023-2025';
|
export let copyrightYear = '2023-2025';
|
||||||
|
|||||||
@@ -465,3 +465,12 @@ export type WandererTrail = {
|
|||||||
updated: string; // ISO 8601 date string
|
updated: string; // ISO 8601 date string
|
||||||
waypoints: string[];
|
waypoints: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type Pin = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
latitude: string;
|
||||||
|
longitude: string;
|
||||||
|
is_visited?: boolean;
|
||||||
|
category: Category | null;
|
||||||
|
};
|
||||||
|
|||||||
@@ -902,7 +902,13 @@
|
|||||||
"shared": "مشترك",
|
"shared": "مشترك",
|
||||||
"shared_with": "مشترك مع",
|
"shared_with": "مشترك مع",
|
||||||
"unshared": "غير مشترك",
|
"unshared": "غير مشترك",
|
||||||
"with": "مع"
|
"with": "مع",
|
||||||
|
"available_users": "المستخدمين المتاحين",
|
||||||
|
"invite_failed": "فشل دعوة",
|
||||||
|
"invite_revoked": "دعوة إلغاء",
|
||||||
|
"invite_sent": "دعوة إرسال",
|
||||||
|
"revoke_failed": "فشل الإلغاء",
|
||||||
|
"unshare_failed": "فشل Unshare"
|
||||||
},
|
},
|
||||||
"strava": {
|
"strava": {
|
||||||
"account_connected": "حساب متصل",
|
"account_connected": "حساب متصل",
|
||||||
@@ -1013,6 +1019,21 @@
|
|||||||
"visit_remove_failed": "فشل في إزالة الزيارة",
|
"visit_remove_failed": "فشل في إزالة الزيارة",
|
||||||
"visit_to": "زيارة",
|
"visit_to": "زيارة",
|
||||||
"visited_first": "زار أولاً",
|
"visited_first": "زار أولاً",
|
||||||
"getting_location_details": "الحصول على تفاصيل الموقع"
|
"getting_location_details": "الحصول على تفاصيل الموقع",
|
||||||
|
"cities_available": "المدن المتاحة",
|
||||||
|
"destination_revealed": "كشفت الوجهة!",
|
||||||
|
"dive_deeper": "الغوص أعمق",
|
||||||
|
"exploration_progress": "تقدم الاستكشاف",
|
||||||
|
"explore_country": "استكشف البلد",
|
||||||
|
"globe_spin_error_desc": "خطأ جلب بيانات الدوران العالمي",
|
||||||
|
"hide_globe_spin": "إخفاء الدوران العالمي",
|
||||||
|
"in": "في",
|
||||||
|
"loading_globe_spin": "تحميل الكرة الأرضية",
|
||||||
|
"no_globe_spin_data": "لا توجد بيانات تدور حول العالم",
|
||||||
|
"show_globe_spin": "عرض Globe Spin",
|
||||||
|
"spin_again": "تدور مرة أخرى",
|
||||||
|
"spinning_globe": "كرة الغزل",
|
||||||
|
"try_again": "حاول ثانية",
|
||||||
|
"your_random_adventure_awaits": "مغامرتك العشوائية تنتظر!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -563,7 +563,22 @@
|
|||||||
"unvisited_first": "Nicht-besucht zuerst",
|
"unvisited_first": "Nicht-besucht zuerst",
|
||||||
"visited_first": "Besucht zuerst",
|
"visited_first": "Besucht zuerst",
|
||||||
"total_items": "Artikel gesamt",
|
"total_items": "Artikel gesamt",
|
||||||
"getting_location_details": "Erhalten von Standortdetails"
|
"getting_location_details": "Erhalten von Standortdetails",
|
||||||
|
"cities_available": "Städte verfügbar",
|
||||||
|
"destination_revealed": "Ziel enthüllt!",
|
||||||
|
"dive_deeper": "Tauchen tiefer",
|
||||||
|
"exploration_progress": "Explorationsfortschritt",
|
||||||
|
"explore_country": "Land erkunden",
|
||||||
|
"globe_spin_error_desc": "Fehler beim Abholen von Globus Spin -Daten",
|
||||||
|
"hide_globe_spin": "Globusspin verstecken",
|
||||||
|
"in": "In",
|
||||||
|
"loading_globe_spin": "Globusspin laden",
|
||||||
|
"no_globe_spin_data": "Keine Globus -Spin -Daten",
|
||||||
|
"show_globe_spin": "SHOW GLOBE Spin",
|
||||||
|
"spin_again": "Wieder drehen",
|
||||||
|
"spinning_globe": "Spinning Globe",
|
||||||
|
"try_again": "Versuchen Sie es erneut",
|
||||||
|
"your_random_adventure_awaits": "Ihr zufälliges Abenteuer wartet!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Benutzerkontoeinstellungen",
|
"account_settings": "Benutzerkontoeinstellungen",
|
||||||
@@ -851,7 +866,13 @@
|
|||||||
"available": "Verfügbar",
|
"available": "Verfügbar",
|
||||||
"pending": "Ausstehend",
|
"pending": "Ausstehend",
|
||||||
"revoke_invite": "Einladung zurückziehen",
|
"revoke_invite": "Einladung zurückziehen",
|
||||||
"send_invite": "Einladung senden"
|
"send_invite": "Einladung senden",
|
||||||
|
"available_users": "Verfügbare Benutzer",
|
||||||
|
"invite_failed": "Einladung fehlgeschlagen",
|
||||||
|
"invite_revoked": "Einladung widerrufen",
|
||||||
|
"invite_sent": "Einladung gesendet",
|
||||||
|
"revoke_failed": "Revoke fehlgeschlagen",
|
||||||
|
"unshare_failed": "Unshare scheiterte"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Mitglied seit",
|
"member_since": "Mitglied seit",
|
||||||
|
|||||||
@@ -535,7 +535,22 @@
|
|||||||
"oldest_first": "Oldest First",
|
"oldest_first": "Oldest First",
|
||||||
"visited_first": "Visited First",
|
"visited_first": "Visited First",
|
||||||
"unvisited_first": "Unvisited First",
|
"unvisited_first": "Unvisited First",
|
||||||
"getting_location_details": "Getting location details"
|
"getting_location_details": "Getting location details",
|
||||||
|
"hide_globe_spin": "Hide Globe Spin",
|
||||||
|
"show_globe_spin": "Show Globe Spin",
|
||||||
|
"loading_globe_spin": "Loading Globe Spin",
|
||||||
|
"spinning_globe": "Spinning Globe",
|
||||||
|
"destination_revealed": "Destination Revealed!",
|
||||||
|
"your_random_adventure_awaits": "Your Random Adventure Awaits!",
|
||||||
|
"exploration_progress": "Exploration Progress",
|
||||||
|
"dive_deeper": "Dive Deeper",
|
||||||
|
"cities_available": "Cities Available",
|
||||||
|
"in": "in",
|
||||||
|
"explore_country": "Explore Country",
|
||||||
|
"spin_again": "Spin Again",
|
||||||
|
"globe_spin_error_desc": "Error fetching globe spin data",
|
||||||
|
"try_again": "Try Again",
|
||||||
|
"no_globe_spin_data": "No Globe Spin Data"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
@@ -874,7 +889,13 @@
|
|||||||
"revoke_invite": "Revoke Invite",
|
"revoke_invite": "Revoke Invite",
|
||||||
"send_invite": "Send Invite",
|
"send_invite": "Send Invite",
|
||||||
"available": "Available",
|
"available": "Available",
|
||||||
"pending": "Pending"
|
"pending": "Pending",
|
||||||
|
"available_users": "Available Users",
|
||||||
|
"revoke_failed": "Revoke Failed",
|
||||||
|
"invite_revoked": "Invite Revoked",
|
||||||
|
"unshare_failed": "Unshare Failed",
|
||||||
|
"invite_failed": "Invite Failed",
|
||||||
|
"invite_sent": "Invite Sent"
|
||||||
},
|
},
|
||||||
"languages": {},
|
"languages": {},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
|||||||
@@ -534,7 +534,22 @@
|
|||||||
"unvisited_first": "Primero no visitado",
|
"unvisited_first": "Primero no visitado",
|
||||||
"visited_first": "Visitado primero",
|
"visited_first": "Visitado primero",
|
||||||
"total_items": "Total de artículos",
|
"total_items": "Total de artículos",
|
||||||
"getting_location_details": "Obtener detalles de ubicación"
|
"getting_location_details": "Obtener detalles de ubicación",
|
||||||
|
"cities_available": "Ciudades disponibles",
|
||||||
|
"destination_revealed": "¡Destino revelado!",
|
||||||
|
"dive_deeper": "Sumergirse",
|
||||||
|
"exploration_progress": "Progreso de exploración",
|
||||||
|
"explore_country": "Explorar el país",
|
||||||
|
"globe_spin_error_desc": "Error al obtener datos de giro global",
|
||||||
|
"hide_globe_spin": "Ocultar giro global",
|
||||||
|
"in": "en",
|
||||||
|
"loading_globe_spin": "Cargando giro global",
|
||||||
|
"no_globe_spin_data": "Sin datos de giro de globo",
|
||||||
|
"show_globe_spin": "Show Globe Spin",
|
||||||
|
"spin_again": "Girar de nuevo",
|
||||||
|
"spinning_globe": "Globo hilado",
|
||||||
|
"try_again": "Intentar otra vez",
|
||||||
|
"your_random_adventure_awaits": "¡Tu aventura aleatoria te espera!"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"forgot_password": "¿Has olvidado tu contraseña?",
|
"forgot_password": "¿Has olvidado tu contraseña?",
|
||||||
@@ -851,7 +866,13 @@
|
|||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"pending": "Pendiente",
|
"pending": "Pendiente",
|
||||||
"revoke_invite": "Revocar la invitación",
|
"revoke_invite": "Revocar la invitación",
|
||||||
"send_invite": "Enviar invitación"
|
"send_invite": "Enviar invitación",
|
||||||
|
"available_users": "Usuarios disponibles",
|
||||||
|
"invite_failed": "Invitación falló",
|
||||||
|
"invite_revoked": "Invitar revocado",
|
||||||
|
"invite_sent": "Invitación enviada",
|
||||||
|
"revoke_failed": "Revocar falló",
|
||||||
|
"unshare_failed": "Falló el desastre"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Miembro desde",
|
"member_since": "Miembro desde",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"view_license": "Afficher la licence"
|
"view_license": "Afficher la licence"
|
||||||
},
|
},
|
||||||
"adventures": {
|
"adventures": {
|
||||||
"activities": {},
|
|
||||||
"add_to_collection": "Ajouter à la collection",
|
"add_to_collection": "Ajouter à la collection",
|
||||||
"delete": "Supprimer",
|
"delete": "Supprimer",
|
||||||
"edit_adventure": "Modifier l'aventure",
|
"edit_adventure": "Modifier l'aventure",
|
||||||
@@ -189,7 +188,7 @@
|
|||||||
"display_name": "Nom d'affichage",
|
"display_name": "Nom d'affichage",
|
||||||
"location_details": "Détails du lieu",
|
"location_details": "Détails du lieu",
|
||||||
"lodging": "Hébergement",
|
"lodging": "Hébergement",
|
||||||
"lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet hébergement? \nCette action ne peut pas être annulée.",
|
"lodging_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet hébergement? Cette action ne peut pas être annulée.",
|
||||||
"lodging_information": "Informations sur l'hébergement",
|
"lodging_information": "Informations sur l'hébergement",
|
||||||
"price": "Prix",
|
"price": "Prix",
|
||||||
"region": "Région",
|
"region": "Région",
|
||||||
@@ -244,7 +243,7 @@
|
|||||||
"edit_location": "Modifier l'emplacement",
|
"edit_location": "Modifier l'emplacement",
|
||||||
"location_create_error": "Échec de la création de l'emplacement",
|
"location_create_error": "Échec de la création de l'emplacement",
|
||||||
"location_created": "Emplacement créé",
|
"location_created": "Emplacement créé",
|
||||||
"location_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement? \nCette action ne peut pas être annulée.",
|
"location_delete_confirm": "Êtes-vous sûr de vouloir supprimer cet emplacement? Cette action ne peut pas être annulée.",
|
||||||
"location_delete_success": "Emplacement supprimé avec succès!",
|
"location_delete_success": "Emplacement supprimé avec succès!",
|
||||||
"location_not_found": "Emplacement introuvable",
|
"location_not_found": "Emplacement introuvable",
|
||||||
"location_not_found_desc": "L'emplacement que vous recherchiez n'a pas pu être trouvé. \nVeuillez essayer un autre emplacement ou revenir plus tard.",
|
"location_not_found_desc": "L'emplacement que vous recherchiez n'a pas pu être trouvé. \nVeuillez essayer un autre emplacement ou revenir plus tard.",
|
||||||
@@ -564,7 +563,22 @@
|
|||||||
"unvisited_first": "Sans visité d'abord",
|
"unvisited_first": "Sans visité d'abord",
|
||||||
"visited_first": "Visité en premier",
|
"visited_first": "Visité en premier",
|
||||||
"total_items": "Total des articles",
|
"total_items": "Total des articles",
|
||||||
"getting_location_details": "Obtenir les détails de l'emplacement"
|
"getting_location_details": "Obtenir les détails de l'emplacement",
|
||||||
|
"cities_available": "Villes disponibles",
|
||||||
|
"destination_revealed": "Destination révélée!",
|
||||||
|
"dive_deeper": "Plonger plus profondément",
|
||||||
|
"exploration_progress": "Progrès de l'exploration",
|
||||||
|
"explore_country": "Explorer le pays",
|
||||||
|
"globe_spin_error_desc": "Erreur pour récupérer les données de spin globe",
|
||||||
|
"hide_globe_spin": "Hide Globe Spin",
|
||||||
|
"in": "dans",
|
||||||
|
"loading_globe_spin": "Chargement du globe Spin",
|
||||||
|
"no_globe_spin_data": "Pas de données de spin globe",
|
||||||
|
"show_globe_spin": "Montrer le spin au globe",
|
||||||
|
"spin_again": "Remonter",
|
||||||
|
"spinning_globe": "Globe de rotation",
|
||||||
|
"try_again": "Essayer à nouveau",
|
||||||
|
"your_random_adventure_awaits": "Votre aventure aléatoire vous attend!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Paramètres du compte utilisateur",
|
"account_settings": "Paramètres du compte utilisateur",
|
||||||
@@ -838,7 +852,6 @@
|
|||||||
"show_activities": "Montrer les activités",
|
"show_activities": "Montrer les activités",
|
||||||
"show_visited_cities": "Villes visites"
|
"show_visited_cities": "Villes visites"
|
||||||
},
|
},
|
||||||
"languages": {},
|
|
||||||
"share": {
|
"share": {
|
||||||
"no_users_shared": "Aucun utilisateur",
|
"no_users_shared": "Aucun utilisateur",
|
||||||
"not_shared_with": "Pas encore partagé avec",
|
"not_shared_with": "Pas encore partagé avec",
|
||||||
@@ -853,7 +866,13 @@
|
|||||||
"available": "Disponible",
|
"available": "Disponible",
|
||||||
"pending": "En attente",
|
"pending": "En attente",
|
||||||
"revoke_invite": "Revoke Inviter",
|
"revoke_invite": "Revoke Inviter",
|
||||||
"send_invite": "Envoyer l'invitation"
|
"send_invite": "Envoyer l'invitation",
|
||||||
|
"available_users": "Utilisateurs disponibles",
|
||||||
|
"invite_failed": "L'invitation a échoué",
|
||||||
|
"invite_revoked": "Inviter révoqué",
|
||||||
|
"invite_sent": "Inviter envoyé",
|
||||||
|
"revoke_failed": "Revoke a échoué",
|
||||||
|
"unshare_failed": "Sans partage a échoué"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Membre depuis",
|
"member_since": "Membre depuis",
|
||||||
|
|||||||
@@ -564,7 +564,22 @@
|
|||||||
"unvisited_first": "Non visitato per primo",
|
"unvisited_first": "Non visitato per primo",
|
||||||
"visited_first": "Visitato per primo",
|
"visited_first": "Visitato per primo",
|
||||||
"total_items": "Articoli totali",
|
"total_items": "Articoli totali",
|
||||||
"getting_location_details": "Ottenere dettagli sulla posizione"
|
"getting_location_details": "Ottenere dettagli sulla posizione",
|
||||||
|
"cities_available": "Città disponibili",
|
||||||
|
"destination_revealed": "Destinazione rivelata!",
|
||||||
|
"dive_deeper": "Immergersi più in profondità",
|
||||||
|
"exploration_progress": "Progressi di esplorazione",
|
||||||
|
"explore_country": "Esplora il paese",
|
||||||
|
"globe_spin_error_desc": "Errore che recupera i dati di spin Globe",
|
||||||
|
"hide_globe_spin": "Nascondi lo spin di globo",
|
||||||
|
"in": "In",
|
||||||
|
"loading_globe_spin": "Caricamento di rotazione del globo",
|
||||||
|
"no_globe_spin_data": "Nessun dati di spin Globe",
|
||||||
|
"show_globe_spin": "Mostra lo spin globo",
|
||||||
|
"spin_again": "Girare di nuovo",
|
||||||
|
"spinning_globe": "Globe rotante",
|
||||||
|
"try_again": "Riprova",
|
||||||
|
"your_random_adventure_awaits": "La tua avventura casuale ti aspetta!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Impostazioni dell'account utente",
|
"account_settings": "Impostazioni dell'account utente",
|
||||||
@@ -853,7 +868,13 @@
|
|||||||
"available": "Disponibile",
|
"available": "Disponibile",
|
||||||
"pending": "In attesa di",
|
"pending": "In attesa di",
|
||||||
"revoke_invite": "Revoca invito",
|
"revoke_invite": "Revoca invito",
|
||||||
"send_invite": "Invia invito"
|
"send_invite": "Invia invito",
|
||||||
|
"available_users": "Utenti disponibili",
|
||||||
|
"invite_failed": "Invito fallito",
|
||||||
|
"invite_revoked": "Invita revocato",
|
||||||
|
"invite_sent": "Invito inviato",
|
||||||
|
"revoke_failed": "Revoca fallita",
|
||||||
|
"unshare_failed": "Unshare non è riuscito"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Membro da",
|
"member_since": "Membro da",
|
||||||
|
|||||||
@@ -902,7 +902,13 @@
|
|||||||
"shared": "共有",
|
"shared": "共有",
|
||||||
"shared_with": "共有",
|
"shared_with": "共有",
|
||||||
"unshared": "非共有",
|
"unshared": "非共有",
|
||||||
"with": "と"
|
"with": "と",
|
||||||
|
"available_users": "利用可能なユーザー",
|
||||||
|
"invite_failed": "招待は失敗しました",
|
||||||
|
"invite_revoked": "招待された招待",
|
||||||
|
"invite_sent": "送信招待",
|
||||||
|
"revoke_failed": "取り消しは失敗しました",
|
||||||
|
"unshare_failed": "UNSHAREは失敗しました"
|
||||||
},
|
},
|
||||||
"strava": {
|
"strava": {
|
||||||
"account_connected": "接続されたアカウント",
|
"account_connected": "接続されたアカウント",
|
||||||
@@ -1013,6 +1019,21 @@
|
|||||||
"visit_remove_failed": "訪問を削除できませんでした",
|
"visit_remove_failed": "訪問を削除できませんでした",
|
||||||
"visit_to": "訪問",
|
"visit_to": "訪問",
|
||||||
"visited_first": "最初に訪問しました",
|
"visited_first": "最初に訪問しました",
|
||||||
"getting_location_details": "場所の詳細を取得します"
|
"getting_location_details": "場所の詳細を取得します",
|
||||||
|
"cities_available": "利用可能な都市",
|
||||||
|
"destination_revealed": "目的地が明らかに!",
|
||||||
|
"dive_deeper": "より深く潜ります",
|
||||||
|
"exploration_progress": "探索の進行",
|
||||||
|
"explore_country": "国を探索します",
|
||||||
|
"globe_spin_error_desc": "グローブスピンデータの取得エラー",
|
||||||
|
"hide_globe_spin": "グローブスピンを隠します",
|
||||||
|
"in": "で",
|
||||||
|
"loading_globe_spin": "グローブスピンのロード",
|
||||||
|
"no_globe_spin_data": "グローブスピンデータはありません",
|
||||||
|
"show_globe_spin": "グローブスピンを表示します",
|
||||||
|
"spin_again": "もう一度スピンします",
|
||||||
|
"spinning_globe": "スピニンググローブ",
|
||||||
|
"try_again": "もう一度やり直してください",
|
||||||
|
"your_random_adventure_awaits": "あなたのランダムな冒険が待っています!"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -846,7 +846,13 @@
|
|||||||
"available": "사용 가능",
|
"available": "사용 가능",
|
||||||
"pending": "보류 중",
|
"pending": "보류 중",
|
||||||
"revoke_invite": "취소 초대",
|
"revoke_invite": "취소 초대",
|
||||||
"send_invite": "초대를 보내십시오"
|
"send_invite": "초대를 보내십시오",
|
||||||
|
"available_users": "사용 가능한 사용자",
|
||||||
|
"invite_failed": "초대 실패",
|
||||||
|
"invite_revoked": "취소 된 초대",
|
||||||
|
"invite_sent": "초대장",
|
||||||
|
"revoke_failed": "취소가 실패했습니다",
|
||||||
|
"unshare_failed": "공해를 실패했습니다"
|
||||||
},
|
},
|
||||||
"transportation": {
|
"transportation": {
|
||||||
"edit": "편집",
|
"edit": "편집",
|
||||||
@@ -938,7 +944,22 @@
|
|||||||
"unvisited_first": "먼저 방문하지 않습니다",
|
"unvisited_first": "먼저 방문하지 않습니다",
|
||||||
"visited_first": "먼저 방문했습니다",
|
"visited_first": "먼저 방문했습니다",
|
||||||
"total_items": "총 항목",
|
"total_items": "총 항목",
|
||||||
"getting_location_details": "위치 세부 정보 얻기"
|
"getting_location_details": "위치 세부 정보 얻기",
|
||||||
|
"dive_deeper": "더 깊이 다이빙하십시오",
|
||||||
|
"exploration_progress": "탐사 진행",
|
||||||
|
"explore_country": "국가를 탐험하십시오",
|
||||||
|
"globe_spin_error_desc": "오류 페치 글로브 스핀 데이터",
|
||||||
|
"hide_globe_spin": "글로브 스핀을 숨기십시오",
|
||||||
|
"in": "~에",
|
||||||
|
"loading_globe_spin": "로드 글로브 스핀",
|
||||||
|
"no_globe_spin_data": "글로브 스핀 데이터가 없습니다",
|
||||||
|
"show_globe_spin": "글로브 스핀을 보여주십시오",
|
||||||
|
"spin_again": "다시 회전하십시오",
|
||||||
|
"spinning_globe": "회전하는 글로브",
|
||||||
|
"try_again": "다시 시도하십시오",
|
||||||
|
"your_random_adventure_awaits": "당신의 임의의 모험이 기다리고 있습니다!",
|
||||||
|
"cities_available": "이용 가능",
|
||||||
|
"destination_revealed": "목적지 공개!"
|
||||||
},
|
},
|
||||||
"lodging": {
|
"lodging": {
|
||||||
"apartment": "아파트",
|
"apartment": "아파트",
|
||||||
|
|||||||
@@ -564,7 +564,22 @@
|
|||||||
"unvisited_first": "Eerst niet bezocht",
|
"unvisited_first": "Eerst niet bezocht",
|
||||||
"visited_first": "Eerst bezocht",
|
"visited_first": "Eerst bezocht",
|
||||||
"total_items": "Totale items",
|
"total_items": "Totale items",
|
||||||
"getting_location_details": "Locatiegegevens krijgen"
|
"getting_location_details": "Locatiegegevens krijgen",
|
||||||
|
"cities_available": "Steden beschikbaar",
|
||||||
|
"destination_revealed": "Bestemming onthuld!",
|
||||||
|
"dive_deeper": "Duik dieper",
|
||||||
|
"exploration_progress": "Verkennings voortgang",
|
||||||
|
"explore_country": "Verken het land",
|
||||||
|
"globe_spin_error_desc": "Fout bij het ophalen van globe spin -gegevens",
|
||||||
|
"hide_globe_spin": "Globe spin verbergen",
|
||||||
|
"in": "in",
|
||||||
|
"loading_globe_spin": "Globe spin laden",
|
||||||
|
"no_globe_spin_data": "Geen Globe spin -gegevens",
|
||||||
|
"show_globe_spin": "Toon Globe Spin",
|
||||||
|
"spin_again": "Weer spinnen",
|
||||||
|
"spinning_globe": "Spinnende bol",
|
||||||
|
"try_again": "Probeer het opnieuw",
|
||||||
|
"your_random_adventure_awaits": "Je willekeurige avontuur wacht!"
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"account_settings": "Gebruikersaccount instellingen",
|
"account_settings": "Gebruikersaccount instellingen",
|
||||||
@@ -853,7 +868,13 @@
|
|||||||
"available": "Beschikbaar",
|
"available": "Beschikbaar",
|
||||||
"pending": "In behandeling",
|
"pending": "In behandeling",
|
||||||
"revoke_invite": "Revoke uitnodigen",
|
"revoke_invite": "Revoke uitnodigen",
|
||||||
"send_invite": "Stuur uitnodiging"
|
"send_invite": "Stuur uitnodiging",
|
||||||
|
"available_users": "Beschikbare gebruikers",
|
||||||
|
"invite_failed": "Uitnodigen mislukt",
|
||||||
|
"invite_revoked": "Uitnodigen ingetrokken",
|
||||||
|
"invite_sent": "Uitnodigen verzonden",
|
||||||
|
"revoke_failed": "Revoke mislukt",
|
||||||
|
"unshare_failed": "Onverschuiving mislukt"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Lid sinds",
|
"member_since": "Lid sinds",
|
||||||
|
|||||||
@@ -534,7 +534,22 @@
|
|||||||
"unvisited_first": "Uvisitert først",
|
"unvisited_first": "Uvisitert først",
|
||||||
"visited_first": "Besøkte først",
|
"visited_first": "Besøkte først",
|
||||||
"total_items": "Totalt gjenstander",
|
"total_items": "Totalt gjenstander",
|
||||||
"getting_location_details": "Få stedsdetaljer"
|
"getting_location_details": "Få stedsdetaljer",
|
||||||
|
"cities_available": "Byer tilgjengelig",
|
||||||
|
"destination_revealed": "Destinasjon avslørt!",
|
||||||
|
"dive_deeper": "Dykk dypere",
|
||||||
|
"exploration_progress": "Utforskningsfremgang",
|
||||||
|
"explore_country": "Utforsk landet",
|
||||||
|
"globe_spin_error_desc": "Feilhåndtering av klode -spinndata",
|
||||||
|
"hide_globe_spin": "Skjul klode spinn",
|
||||||
|
"in": "i",
|
||||||
|
"loading_globe_spin": "Laster klode spinn",
|
||||||
|
"no_globe_spin_data": "Ingen klode spinndata",
|
||||||
|
"show_globe_spin": "Vis Globe Spin",
|
||||||
|
"spin_again": "Spinn igjen",
|
||||||
|
"spinning_globe": "Spinnende klode",
|
||||||
|
"try_again": "Prøv igjen",
|
||||||
|
"your_random_adventure_awaits": "Ditt tilfeldige eventyr venter!"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Brukernavn",
|
"username": "Brukernavn",
|
||||||
@@ -873,7 +888,13 @@
|
|||||||
"available": "Tilgjengelig",
|
"available": "Tilgjengelig",
|
||||||
"pending": "I påvente av",
|
"pending": "I påvente av",
|
||||||
"revoke_invite": "Revoke Inviter",
|
"revoke_invite": "Revoke Inviter",
|
||||||
"send_invite": "Send invitasjon"
|
"send_invite": "Send invitasjon",
|
||||||
|
"available_users": "Tilgjengelige brukere",
|
||||||
|
"invite_failed": "Inviter mislyktes",
|
||||||
|
"invite_revoked": "Inviter tilbakekalt",
|
||||||
|
"invite_sent": "Inviter sendt",
|
||||||
|
"revoke_failed": "Revoke mislyktes",
|
||||||
|
"unshare_failed": "Unshare mislyktes"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Medlem siden",
|
"member_since": "Medlem siden",
|
||||||
|
|||||||
@@ -535,7 +535,22 @@
|
|||||||
"unvisited_first": "Najpierw niewidziane",
|
"unvisited_first": "Najpierw niewidziane",
|
||||||
"visited_first": "Odwiedziłem pierwszy",
|
"visited_first": "Odwiedziłem pierwszy",
|
||||||
"total_items": "Całkowite przedmioty",
|
"total_items": "Całkowite przedmioty",
|
||||||
"getting_location_details": "Uzyskanie szczegółów lokalizacji"
|
"getting_location_details": "Uzyskanie szczegółów lokalizacji",
|
||||||
|
"cities_available": "Dostępne miasta",
|
||||||
|
"destination_revealed": "Ujawnione miejsce docelowe!",
|
||||||
|
"dive_deeper": "Nurkuj głębiej",
|
||||||
|
"exploration_progress": "Postęp eksploracyjny",
|
||||||
|
"explore_country": "Poznaj kraj",
|
||||||
|
"globe_spin_error_desc": "Błąd przyciąganie danych spinowych globe",
|
||||||
|
"hide_globe_spin": "Ukryj globe spin",
|
||||||
|
"in": "W",
|
||||||
|
"loading_globe_spin": "Ładowanie globowego spinu",
|
||||||
|
"no_globe_spin_data": "Brak danych spinowych globe",
|
||||||
|
"show_globe_spin": "Pokaż globe spin",
|
||||||
|
"spin_again": "Obrócić ponownie",
|
||||||
|
"spinning_globe": "Spinning Globe",
|
||||||
|
"try_again": "Spróbuj ponownie",
|
||||||
|
"your_random_adventure_awaits": "Twoja przypadkowa przygoda czeka!"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Nazwa użytkownika",
|
"username": "Nazwa użytkownika",
|
||||||
@@ -852,7 +867,13 @@
|
|||||||
"available": "Dostępny",
|
"available": "Dostępny",
|
||||||
"pending": "Aż do",
|
"pending": "Aż do",
|
||||||
"revoke_invite": "Cofnij zaproszenie",
|
"revoke_invite": "Cofnij zaproszenie",
|
||||||
"send_invite": "Wyślij zaproszenie"
|
"send_invite": "Wyślij zaproszenie",
|
||||||
|
"available_users": "Dostępni użytkownicy",
|
||||||
|
"invite_failed": "Zaproś nieudane",
|
||||||
|
"invite_revoked": "Zaproś cofnięte",
|
||||||
|
"invite_sent": "Zaproś wysłane",
|
||||||
|
"revoke_failed": "Revoke nie powiodło się",
|
||||||
|
"unshare_failed": "Unhare nie powiodło się"
|
||||||
},
|
},
|
||||||
"languages": {},
|
"languages": {},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -535,7 +535,22 @@
|
|||||||
"unvisited_first": "Сначала не посещенные",
|
"unvisited_first": "Сначала не посещенные",
|
||||||
"visited_first": "Сначала посещенные",
|
"visited_first": "Сначала посещенные",
|
||||||
"total_items": "Общие предметы",
|
"total_items": "Общие предметы",
|
||||||
"getting_location_details": "Получение деталей локации"
|
"getting_location_details": "Получение деталей локации",
|
||||||
|
"cities_available": "Города доступны",
|
||||||
|
"destination_revealed": "Открыто место!",
|
||||||
|
"dive_deeper": "Погрузитесь глубже",
|
||||||
|
"exploration_progress": "Прогресс исследования",
|
||||||
|
"explore_country": "Исследуйте страну",
|
||||||
|
"globe_spin_error_desc": "Ошибка извлечения данных спиновых глобусов",
|
||||||
|
"hide_globe_spin": "Скрыть глобус спин",
|
||||||
|
"in": "в",
|
||||||
|
"loading_globe_spin": "Загрузка глобуса спина",
|
||||||
|
"no_globe_spin_data": "Нет данных о вращении Globe",
|
||||||
|
"show_globe_spin": "Показать Globe Spin",
|
||||||
|
"spin_again": "Снова спите",
|
||||||
|
"spinning_globe": "Вращающийся глобус",
|
||||||
|
"try_again": "Попробуйте еще раз",
|
||||||
|
"your_random_adventure_awaits": "Ваше случайное приключение ждет!"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Имя пользователя",
|
"username": "Имя пользователя",
|
||||||
@@ -874,7 +889,13 @@
|
|||||||
"available": "Доступный",
|
"available": "Доступный",
|
||||||
"pending": "В ожидании",
|
"pending": "В ожидании",
|
||||||
"revoke_invite": "Отменить приглашение",
|
"revoke_invite": "Отменить приглашение",
|
||||||
"send_invite": "Отправить приглашение"
|
"send_invite": "Отправить приглашение",
|
||||||
|
"available_users": "Доступные пользователи",
|
||||||
|
"invite_failed": "Приглашение не удалось",
|
||||||
|
"invite_revoked": "Пригласить отменен",
|
||||||
|
"invite_sent": "Пригласить отправлено",
|
||||||
|
"revoke_failed": "Отмена не удалась",
|
||||||
|
"unshare_failed": "UNSHARE не удалось"
|
||||||
},
|
},
|
||||||
"languages": {},
|
"languages": {},
|
||||||
"profile": {
|
"profile": {
|
||||||
|
|||||||
@@ -534,7 +534,22 @@
|
|||||||
"oldest_first": "Najstaršie najprv",
|
"oldest_first": "Najstaršie najprv",
|
||||||
"visited_first": "Navštívené najprv",
|
"visited_first": "Navštívené najprv",
|
||||||
"unvisited_first": "Nenavštívené najprv",
|
"unvisited_first": "Nenavštívené najprv",
|
||||||
"getting_location_details": "Získavajú sa detaily miesta"
|
"getting_location_details": "Získavajú sa detaily miesta",
|
||||||
|
"cities_available": "K dispozícii sú mestá",
|
||||||
|
"destination_revealed": "Destinácia odhalená!",
|
||||||
|
"dive_deeper": "Prehlbovať sa",
|
||||||
|
"exploration_progress": "Pokrok v prieskume",
|
||||||
|
"explore_country": "Preskúmať krajinu",
|
||||||
|
"globe_spin_error_desc": "Chyba načítava dáta spinov Globe",
|
||||||
|
"loading_globe_spin": "Nakladanie zemegule",
|
||||||
|
"no_globe_spin_data": "Žiadne údaje o spinte Globe",
|
||||||
|
"show_globe_spin": "Zobraziť globe rotáciu",
|
||||||
|
"spin_again": "Znova",
|
||||||
|
"spinning_globe": "Glóbus",
|
||||||
|
"try_again": "Skúste to znova",
|
||||||
|
"your_random_adventure_awaits": "Čaká vaše náhodné dobrodružstvo!",
|
||||||
|
"hide_globe_spin": "Skryť zemeguľu",
|
||||||
|
"in": "v"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"username": "Používateľské meno",
|
"username": "Používateľské meno",
|
||||||
@@ -873,7 +888,13 @@
|
|||||||
"revoke_invite": "Zrušiť pozvánku",
|
"revoke_invite": "Zrušiť pozvánku",
|
||||||
"send_invite": "Odoslať pozvánku",
|
"send_invite": "Odoslať pozvánku",
|
||||||
"available": "Dostupné",
|
"available": "Dostupné",
|
||||||
"pending": "Čakajúce"
|
"pending": "Čakajúce",
|
||||||
|
"available_users": "Dostupní používatelia",
|
||||||
|
"invite_failed": "Pozvite zlyhalo",
|
||||||
|
"invite_revoked": "Pozvať odvolané",
|
||||||
|
"invite_sent": "Pozvať odoslané",
|
||||||
|
"revoke_failed": "Odbočka zlyhala",
|
||||||
|
"unshare_failed": "Unshare zlyhala"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Člen od",
|
"member_since": "Člen od",
|
||||||
|
|||||||
@@ -534,7 +534,22 @@
|
|||||||
"unvisited_first": "Oöverträffad först",
|
"unvisited_first": "Oöverträffad först",
|
||||||
"visited_first": "Besökt först",
|
"visited_first": "Besökt först",
|
||||||
"total_items": "Totala artiklar",
|
"total_items": "Totala artiklar",
|
||||||
"getting_location_details": "Få platsinformation"
|
"getting_location_details": "Få platsinformation",
|
||||||
|
"cities_available": "Städer tillgängliga",
|
||||||
|
"destination_revealed": "Destination avslöjad!",
|
||||||
|
"dive_deeper": "Dyk djupare",
|
||||||
|
"exploration_progress": "Undersökningens framsteg",
|
||||||
|
"explore_country": "Utforska land",
|
||||||
|
"globe_spin_error_desc": "Fel som hämtar Globe Spin Data",
|
||||||
|
"hide_globe_spin": "Dölj jordklot",
|
||||||
|
"in": "i",
|
||||||
|
"loading_globe_spin": "Loading Globe Spin",
|
||||||
|
"no_globe_spin_data": "Inga Globe Spin -data",
|
||||||
|
"show_globe_spin": "Show Globe Spin",
|
||||||
|
"spin_again": "Snurra igen",
|
||||||
|
"spinning_globe": "Snurrande jordklot",
|
||||||
|
"try_again": "Försök igen",
|
||||||
|
"your_random_adventure_awaits": "Ditt slumpmässiga äventyr väntar!"
|
||||||
},
|
},
|
||||||
"auth": {
|
"auth": {
|
||||||
"confirm_password": "Bekräfta lösenord",
|
"confirm_password": "Bekräfta lösenord",
|
||||||
@@ -851,7 +866,13 @@
|
|||||||
"available": "Tillgänglig",
|
"available": "Tillgänglig",
|
||||||
"pending": "I avvaktan på",
|
"pending": "I avvaktan på",
|
||||||
"revoke_invite": "Återkalla inbjudan",
|
"revoke_invite": "Återkalla inbjudan",
|
||||||
"send_invite": "Skicka inbjudan"
|
"send_invite": "Skicka inbjudan",
|
||||||
|
"available_users": "Tillgängliga användare",
|
||||||
|
"invite_failed": "Inbjudan misslyckades",
|
||||||
|
"invite_revoked": "Bjuda återkallad",
|
||||||
|
"invite_sent": "Inbjudan skickad",
|
||||||
|
"revoke_failed": "Återkallad misslyckades",
|
||||||
|
"unshare_failed": "Unshare misslyckades"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "Medlem sedan",
|
"member_since": "Medlem sedan",
|
||||||
|
|||||||
1018
frontend/src/locales/tr.json
Normal file
1018
frontend/src/locales/tr.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -561,7 +561,22 @@
|
|||||||
"unvisited_first": "未访问优先",
|
"unvisited_first": "未访问优先",
|
||||||
"visited_first": "已访问优先",
|
"visited_first": "已访问优先",
|
||||||
"total_items": "总项目",
|
"total_items": "总项目",
|
||||||
"getting_location_details": "获取地点详细信息"
|
"getting_location_details": "获取地点详细信息",
|
||||||
|
"cities_available": "可用的城市",
|
||||||
|
"destination_revealed": "目的地揭示了!",
|
||||||
|
"dive_deeper": "深入潜水",
|
||||||
|
"exploration_progress": "勘探进度",
|
||||||
|
"explore_country": "探索国家",
|
||||||
|
"globe_spin_error_desc": "错误获取地球旋转数据",
|
||||||
|
"hide_globe_spin": "隐藏环球旋转",
|
||||||
|
"in": "在",
|
||||||
|
"loading_globe_spin": "加载地球旋转",
|
||||||
|
"no_globe_spin_data": "没有地球旋转数据",
|
||||||
|
"show_globe_spin": "显示环球旋转",
|
||||||
|
"spin_again": "再次旋转",
|
||||||
|
"spinning_globe": "旋转地球",
|
||||||
|
"try_again": "再试一次",
|
||||||
|
"your_random_adventure_awaits": "您的随机冒险在等待!"
|
||||||
},
|
},
|
||||||
"users": {
|
"users": {
|
||||||
"no_users_found": "未找到已公开的用户。"
|
"no_users_found": "未找到已公开的用户。"
|
||||||
@@ -854,8 +869,12 @@
|
|||||||
"pending": "待办的",
|
"pending": "待办的",
|
||||||
"revoke_invite": "撤销邀请",
|
"revoke_invite": "撤销邀请",
|
||||||
"send_invite": "发送邀请",
|
"send_invite": "发送邀请",
|
||||||
"available_users": "可邀请的用户",
|
"available_users": "可用用户",
|
||||||
"no_available_users": "没有可邀请的用户"
|
"invite_failed": "邀请失败",
|
||||||
|
"invite_revoked": "邀请被撤销",
|
||||||
|
"invite_sent": "邀请发送",
|
||||||
|
"revoke_failed": "撤销失败",
|
||||||
|
"unshare_failed": "没有共享失败"
|
||||||
},
|
},
|
||||||
"profile": {
|
"profile": {
|
||||||
"member_since": "会员自",
|
"member_since": "会员自",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
register('ar', () => import('../locales/ar.json'));
|
register('ar', () => import('../locales/ar.json'));
|
||||||
register('pt-br', () => import('../locales/pt-br.json'));
|
register('pt-br', () => import('../locales/pt-br.json'));
|
||||||
register('sk', () => import('../locales/sk.json'));
|
register('sk', () => import('../locales/sk.json'));
|
||||||
|
register('tr', () => import('../locales/tr.json'));
|
||||||
|
|
||||||
let locales = [
|
let locales = [
|
||||||
'en',
|
'en',
|
||||||
@@ -38,7 +39,8 @@
|
|||||||
'ja',
|
'ja',
|
||||||
'ar',
|
'ar',
|
||||||
'pt-br',
|
'pt-br',
|
||||||
'sk'
|
'sk',
|
||||||
|
'tr'
|
||||||
];
|
];
|
||||||
|
|
||||||
if (browser) {
|
if (browser) {
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
let sessionId = event.cookies.get('sessionid');
|
let sessionId = event.cookies.get('sessionid');
|
||||||
let visitedFetch = await fetch(`${endpoint}/api/locations/all/?include_collections=true`, {
|
let visitedFetch = await fetch(
|
||||||
|
`${endpoint}/api/locations/all?include_collections=true&nested=true&allowed_nested_fields=visits`,
|
||||||
|
{
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${sessionId}`
|
Cookie: `sessionid=${sessionId}`
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
);
|
||||||
let adventures = (await visitedFetch.json()) as Location[];
|
let adventures = (await visitedFetch.json()) as Location[];
|
||||||
|
|
||||||
// Get user's local timezone as fallback
|
// Get user's local timezone as fallback
|
||||||
|
|||||||
@@ -12,88 +12,66 @@ const serverEndpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
|||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
if (!event.locals.user) {
|
if (!event.locals.user) {
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else {
|
}
|
||||||
let next = null;
|
|
||||||
let previous = null;
|
const sessionId = event.cookies.get('sessionid');
|
||||||
let count = 0;
|
if (!sessionId) {
|
||||||
let collections: Location[] = [];
|
return redirect(302, '/login');
|
||||||
let sessionId = event.cookies.get('sessionid');
|
}
|
||||||
|
|
||||||
// Get sorting parameters from URL
|
// Get sorting parameters from URL
|
||||||
const order_by = event.url.searchParams.get('order_by') || 'updated_at';
|
const order_by = event.url.searchParams.get('order_by') || 'updated_at';
|
||||||
const order_direction = event.url.searchParams.get('order_direction') || 'desc';
|
const order_direction = event.url.searchParams.get('order_direction') || 'desc';
|
||||||
const page = event.url.searchParams.get('page') || '1';
|
const page = event.url.searchParams.get('page') || '1';
|
||||||
|
|
||||||
// Build API URL with parameters
|
|
||||||
let apiUrl = `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}&page=${page}`;
|
|
||||||
|
|
||||||
let initialFetch = await fetch(apiUrl, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
},
|
|
||||||
credentials: 'include'
|
|
||||||
});
|
|
||||||
if (!initialFetch.ok) {
|
|
||||||
console.error('Failed to fetch collections');
|
|
||||||
return redirect(302, '/login');
|
|
||||||
} else {
|
|
||||||
let res = await initialFetch.json();
|
|
||||||
let visited = res.results as Location[];
|
|
||||||
next = res.next;
|
|
||||||
previous = res.previous;
|
|
||||||
count = res.count;
|
|
||||||
collections = [...collections, ...visited];
|
|
||||||
}
|
|
||||||
|
|
||||||
let sharedRes = await fetch(`${serverEndpoint}/api/collections/shared/`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!sharedRes.ok) {
|
|
||||||
console.error('Failed to fetch shared collections');
|
|
||||||
return redirect(302, '/login');
|
|
||||||
}
|
|
||||||
let sharedCollections = (await sharedRes.json()) as Collection[];
|
|
||||||
|
|
||||||
let archivedRes = await fetch(`${serverEndpoint}/api/collections/archived/`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!archivedRes.ok) {
|
|
||||||
console.error('Failed to fetch archived collections');
|
|
||||||
return redirect(302, '/login');
|
|
||||||
}
|
|
||||||
let archivedCollections = (await archivedRes.json()) as Collection[];
|
|
||||||
|
|
||||||
let inviteRes = await fetch(`${serverEndpoint}/api/collections/invites/`, {
|
|
||||||
headers: {
|
|
||||||
Cookie: `sessionid=${sessionId}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!inviteRes.ok) {
|
|
||||||
console.error('Failed to fetch invites');
|
|
||||||
return redirect(302, '/login');
|
|
||||||
}
|
|
||||||
let invites = await inviteRes.json();
|
|
||||||
|
|
||||||
// Calculate current page from URL
|
|
||||||
const currentPage = parseInt(page);
|
const currentPage = parseInt(page);
|
||||||
|
|
||||||
|
// Common headers for all requests
|
||||||
|
const headers = {
|
||||||
|
Cookie: `sessionid=${sessionId}`
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build API URL with nested=true for lighter payload
|
||||||
|
const apiUrl = `${serverEndpoint}/api/collections/?order_by=${order_by}&order_direction=${order_direction}&page=${page}&nested=true`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Execute all API calls in parallel
|
||||||
|
const [collectionsRes, sharedRes, archivedRes, invitesRes] = await Promise.all([
|
||||||
|
fetch(apiUrl, { headers, credentials: 'include' }),
|
||||||
|
fetch(`${serverEndpoint}/api/collections/shared/?nested=true`, { headers }),
|
||||||
|
fetch(`${serverEndpoint}/api/collections/archived/?nested=true`, { headers }),
|
||||||
|
fetch(`${serverEndpoint}/api/collections/invites/`, { headers })
|
||||||
|
]);
|
||||||
|
|
||||||
|
// Check if main collections request failed (most critical)
|
||||||
|
if (!collectionsRes.ok) {
|
||||||
|
console.error('Failed to fetch collections:', collectionsRes.status);
|
||||||
|
return redirect(302, '/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse responses in parallel
|
||||||
|
const [collectionsData, sharedData, archivedData, invitesData] = await Promise.all([
|
||||||
|
collectionsRes.json(),
|
||||||
|
sharedRes.ok ? sharedRes.json() : [],
|
||||||
|
archivedRes.ok ? archivedRes.json() : [],
|
||||||
|
invitesRes.ok ? invitesRes.json() : []
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
adventures: collections,
|
adventures: collectionsData.results as Location[],
|
||||||
next,
|
next: collectionsData.next,
|
||||||
previous,
|
previous: collectionsData.previous,
|
||||||
count,
|
count: collectionsData.count,
|
||||||
sharedCollections,
|
sharedCollections: sharedData as Collection[],
|
||||||
currentPage,
|
currentPage,
|
||||||
order_by,
|
order_by,
|
||||||
order_direction,
|
order_direction,
|
||||||
archivedCollections,
|
archivedCollections: archivedData as Collection[],
|
||||||
invites
|
invites: invitesData
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching data:', error);
|
||||||
|
return redirect(302, '/login');
|
||||||
}
|
}
|
||||||
}) satisfies PageServerLoad;
|
}) satisfies PageServerLoad;
|
||||||
|
|||||||
@@ -33,7 +33,8 @@
|
|||||||
groupLodgingByDate,
|
groupLodgingByDate,
|
||||||
LODGING_TYPES_ICONS,
|
LODGING_TYPES_ICONS,
|
||||||
getBasemapUrl,
|
getBasemapUrl,
|
||||||
isAllDay
|
isAllDay,
|
||||||
|
getActivityColor
|
||||||
} from '$lib';
|
} from '$lib';
|
||||||
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
|
import { formatDateInTimezone, formatAllDayDate } from '$lib/dateUtils';
|
||||||
|
|
||||||
@@ -1230,7 +1231,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<!-- Duration -->
|
<!-- Duration -->
|
||||||
{Math.round(
|
{Math.floor(
|
||||||
(new Date(orderedItem.end).getTime() -
|
(new Date(orderedItem.end).getTime() -
|
||||||
new Date(orderedItem.start).getTime()) /
|
new Date(orderedItem.start).getTime()) /
|
||||||
1000 /
|
1000 /
|
||||||
@@ -1375,6 +1376,39 @@
|
|||||||
</Marker>
|
</Marker>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<!-- Shows activity GPX on the map -->
|
||||||
|
{#each adventures as adventure}
|
||||||
|
{#each adventure.visits as visit}
|
||||||
|
{#each visit.activities as activity}
|
||||||
|
{#if activity.geojson}
|
||||||
|
<GeoJSON data={activity.geojson}>
|
||||||
|
<LineLayer
|
||||||
|
paint={{
|
||||||
|
'line-color': getActivityColor(activity.sport_type),
|
||||||
|
'line-width': 3,
|
||||||
|
'line-opacity': 0.8
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeoJSON>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
{#each adventure.attachments as attachment}
|
||||||
|
{#if attachment.geojson}
|
||||||
|
<GeoJSON data={attachment.geojson}>
|
||||||
|
<LineLayer
|
||||||
|
paint={{
|
||||||
|
'line-color': '#00FF00',
|
||||||
|
'line-width': 2,
|
||||||
|
'line-opacity': 0.6
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</GeoJSON>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
{/each}
|
||||||
|
|
||||||
{#if lineData && collection.start_date && collection.end_date}
|
{#if lineData && collection.start_date && collection.end_date}
|
||||||
<GeoJSON data={lineData}>
|
<GeoJSON data={lineData}>
|
||||||
<LineLayer
|
<LineLayer
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageServerLoad } from './$types';
|
import type { PageServerLoad } from './$types';
|
||||||
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
const PUBLIC_SERVER_URL = process.env['PUBLIC_SERVER_URL'];
|
||||||
import type { Location, VisitedRegion } from '$lib/types';
|
import type { Location, Pin, VisitedRegion } from '$lib/types';
|
||||||
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
const endpoint = PUBLIC_SERVER_URL || 'http://localhost:8000';
|
||||||
|
|
||||||
export const load = (async (event) => {
|
export const load = (async (event) => {
|
||||||
@@ -9,7 +9,7 @@ export const load = (async (event) => {
|
|||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else {
|
} else {
|
||||||
let sessionId = event.cookies.get('sessionid');
|
let sessionId = event.cookies.get('sessionid');
|
||||||
let visitedFetch = await fetch(`${endpoint}/api/locations/all/?include_collections=true`, {
|
let pinFetch = await fetch(`${endpoint}/api/locations/pins/`, {
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `sessionid=${sessionId}`
|
Cookie: `sessionid=${sessionId}`
|
||||||
}
|
}
|
||||||
@@ -22,19 +22,19 @@ export const load = (async (event) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[];
|
let visitedRegions = (await visitedRegionsFetch.json()) as VisitedRegion[];
|
||||||
let adventures = (await visitedFetch.json()) as Location[];
|
let pins = (await pinFetch.json()) as Pin[];
|
||||||
|
|
||||||
if (!visitedRegionsFetch.ok) {
|
if (!visitedRegionsFetch.ok) {
|
||||||
console.error('Failed to fetch visited regions');
|
console.error('Failed to fetch visited regions');
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else if (!visitedFetch.ok) {
|
} else if (!pinFetch.ok) {
|
||||||
console.error('Failed to fetch visited adventures');
|
console.error('Failed to fetch location pins');
|
||||||
return redirect(302, '/login');
|
return redirect(302, '/login');
|
||||||
} else {
|
} else {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
visitedRegions,
|
visitedRegions,
|
||||||
adventures
|
pins
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
LineLayer
|
LineLayer
|
||||||
} from 'svelte-maplibre';
|
} from 'svelte-maplibre';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { Activity, Location, VisitedCity, VisitedRegion } from '$lib/types.js';
|
import type { Activity, Location, VisitedCity, VisitedRegion, Pin } from '$lib/types.js';
|
||||||
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
import CardCarousel from '$lib/components/CardCarousel.svelte';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { basemapOptions, getActivityColor, getBasemapLabel, getBasemapUrl } from '$lib';
|
import { basemapOptions, getActivityColor, getBasemapLabel, getBasemapUrl } from '$lib';
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
import Plus from '~icons/mdi/plus';
|
import Plus from '~icons/mdi/plus';
|
||||||
import Clear from '~icons/mdi/close';
|
import Clear from '~icons/mdi/close';
|
||||||
import Eye from '~icons/mdi/eye';
|
import Eye from '~icons/mdi/eye';
|
||||||
import Pin from '~icons/mdi/map-marker';
|
import PinIcon from '~icons/mdi/map-marker';
|
||||||
import Calendar from '~icons/mdi/calendar';
|
import Calendar from '~icons/mdi/calendar';
|
||||||
import LocationIcon from '~icons/mdi/crosshairs-gps';
|
import LocationIcon from '~icons/mdi/crosshairs-gps';
|
||||||
import NewLocationModal from '$lib/components/NewLocationModal.svelte';
|
import NewLocationModal from '$lib/components/NewLocationModal.svelte';
|
||||||
@@ -35,17 +35,16 @@
|
|||||||
let showCities: boolean = false;
|
let showCities: boolean = false;
|
||||||
let sidebarOpen: boolean = false;
|
let sidebarOpen: boolean = false;
|
||||||
|
|
||||||
let basemapType: string = 'default'; // default
|
let basemapType: string = 'default';
|
||||||
|
|
||||||
export let initialLatLng: { lat: number; lng: number } | null = null;
|
export let initialLatLng: { lat: number; lng: number } | null = null;
|
||||||
|
|
||||||
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
let visitedRegions: VisitedRegion[] = data.props.visitedRegions;
|
||||||
let visitedCities: VisitedCity[] = [];
|
let visitedCities: VisitedCity[] = [];
|
||||||
let adventures: Location[] = data.props.adventures;
|
let pins: Pin[] = data.props.pins; // Lightweight pin objects
|
||||||
|
|
||||||
let activities: Activity[] = [];
|
let activities: Activity[] = [];
|
||||||
|
|
||||||
let filteredAdventures = adventures;
|
let filteredPins = pins;
|
||||||
|
|
||||||
let showVisited: boolean = true;
|
let showVisited: boolean = true;
|
||||||
let showPlanned: boolean = true;
|
let showPlanned: boolean = true;
|
||||||
@@ -54,23 +53,25 @@
|
|||||||
let newLongitude: number | null = null;
|
let newLongitude: number | null = null;
|
||||||
let newLatitude: number | null = null;
|
let newLatitude: number | null = null;
|
||||||
|
|
||||||
let isPopupOpen = false;
|
// Cache for full location data
|
||||||
|
let locationCache: Map<string, Location> = new Map();
|
||||||
|
let loadingLocations: Set<string> = new Set();
|
||||||
|
|
||||||
|
let locationBeingUpdated: Location | undefined = undefined;
|
||||||
|
|
||||||
// Statistics
|
// Statistics
|
||||||
$: totalAdventures = adventures.length;
|
$: totalAdventures = pins.length;
|
||||||
$: visitedAdventures = adventures.filter((adventure) => adventure.is_visited).length;
|
$: visitedAdventures = pins.filter((pin) => pin.is_visited).length;
|
||||||
$: plannedAdventures = adventures.filter((adventure) => !adventure.is_visited).length;
|
$: plannedAdventures = pins.filter((pin) => !pin.is_visited).length;
|
||||||
$: totalRegions = visitedRegions.length;
|
$: totalRegions = visitedRegions.length;
|
||||||
|
|
||||||
// Get unique categories for filtering
|
// Get unique categories for filtering
|
||||||
$: categories = [
|
$: categories = [...new Set(pins.map((pin) => pin.category?.display_name).filter(Boolean))];
|
||||||
...new Set(adventures.map((adventure) => adventure.category?.display_name).filter(Boolean))
|
|
||||||
];
|
|
||||||
|
|
||||||
// Updates the filtered adventures based on the checkboxes
|
// Updates the filtered pins based on the checkboxes
|
||||||
$: {
|
$: {
|
||||||
filteredAdventures = adventures.filter(
|
filteredPins = pins.filter(
|
||||||
(adventure) => (showVisited && adventure.is_visited) || (showPlanned && !adventure.is_visited)
|
(pin) => (showVisited && pin.is_visited === true) || (showPlanned && pin.is_visited !== true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,22 +83,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let locationBeingUpdated: Location | undefined = undefined;
|
// Sync the locationBeingUpdated with the pins array
|
||||||
|
|
||||||
// Sync the locationBeingUpdated with the adventures array
|
|
||||||
$: {
|
$: {
|
||||||
if (locationBeingUpdated && locationBeingUpdated.id) {
|
if (locationBeingUpdated && locationBeingUpdated.id) {
|
||||||
const index = adventures.findIndex((adventure) => adventure.id === locationBeingUpdated?.id);
|
const index = pins.findIndex((pin) => pin.id === locationBeingUpdated?.id);
|
||||||
|
|
||||||
if (index !== -1) {
|
if (index !== -1) {
|
||||||
adventures[index] = { ...locationBeingUpdated };
|
// Update existing pin with new data
|
||||||
adventures = adventures; // Trigger reactivity
|
pins[index] = {
|
||||||
|
id: locationBeingUpdated.id,
|
||||||
|
name: locationBeingUpdated.name,
|
||||||
|
latitude: locationBeingUpdated.latitude?.toString() || '',
|
||||||
|
longitude: locationBeingUpdated.longitude?.toString() || '',
|
||||||
|
is_visited: locationBeingUpdated.is_visited,
|
||||||
|
category: locationBeingUpdated.category
|
||||||
|
};
|
||||||
|
pins = pins; // Trigger reactivity
|
||||||
} else {
|
} else {
|
||||||
adventures = [{ ...locationBeingUpdated }, ...adventures];
|
// Add new pin
|
||||||
if (data.props.adventures) {
|
const newPin: Pin = {
|
||||||
data.props.adventures = adventures; // Update data.props.adventure.locations as well
|
id: locationBeingUpdated.id,
|
||||||
}
|
name: locationBeingUpdated.name,
|
||||||
|
latitude: locationBeingUpdated.latitude?.toString() || '',
|
||||||
|
longitude: locationBeingUpdated.longitude?.toString() || '',
|
||||||
|
is_visited: locationBeingUpdated.is_visited,
|
||||||
|
category: locationBeingUpdated.category
|
||||||
|
};
|
||||||
|
pins = [newPin, ...pins];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also update the cache
|
||||||
|
locationCache.set(locationBeingUpdated.id, locationBeingUpdated);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +140,36 @@
|
|||||||
visitedCities = await response.json();
|
visitedCities = await response.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function fetchLocationDetails(locationId: string): Promise<Location | null> {
|
||||||
|
// Check cache first
|
||||||
|
if (locationCache.has(locationId)) {
|
||||||
|
return locationCache.get(locationId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent duplicate requests
|
||||||
|
if (loadingLocations.has(locationId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
loadingLocations.add(locationId);
|
||||||
|
const response = await fetch(`/api/locations/${locationId}`);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch location: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const location: Location = await response.json();
|
||||||
|
locationCache.set(locationId, location);
|
||||||
|
return location;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching location details:', error);
|
||||||
|
return null;
|
||||||
|
} finally {
|
||||||
|
loadingLocations.delete(locationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addMarker(e: { detail: { lngLat: { lng: any; lat: any } } }) {
|
function addMarker(e: { detail: { lngLat: { lng: any; lat: any } } }) {
|
||||||
newMarker = null;
|
newMarker = null;
|
||||||
newMarker = { lngLat: e.detail.lngLat };
|
newMarker = { lngLat: e.detail.lngLat };
|
||||||
@@ -137,22 +183,43 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function createNewAdventure(event: CustomEvent) {
|
function createNewAdventure(event: CustomEvent) {
|
||||||
adventures = [...adventures, event.detail];
|
const location: Location = event.detail;
|
||||||
|
|
||||||
|
// Add to pins array
|
||||||
|
const newPin: Pin = {
|
||||||
|
id: location.id,
|
||||||
|
name: location.name,
|
||||||
|
latitude: location.latitude?.toString() || '',
|
||||||
|
longitude: location.longitude?.toString() || '',
|
||||||
|
is_visited: location.is_visited,
|
||||||
|
category: location.category
|
||||||
|
};
|
||||||
|
|
||||||
|
pins = [...pins, newPin];
|
||||||
|
|
||||||
|
// Add to cache
|
||||||
|
locationCache.set(location.id, location);
|
||||||
|
|
||||||
newMarker = null;
|
newMarker = null;
|
||||||
createModalOpen = false;
|
createModalOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function togglePopup() {
|
|
||||||
isPopupOpen = !isPopupOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleSidebar() {
|
|
||||||
sidebarOpen = !sidebarOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearMarker() {
|
function clearMarker() {
|
||||||
newMarker = null;
|
newMarker = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Function to handle popup opening - only fetch when actually needed
|
||||||
|
let openPopups = new Set<string>();
|
||||||
|
|
||||||
|
function handlePopupOpen(pinId: string) {
|
||||||
|
openPopups.add(pinId);
|
||||||
|
openPopups = openPopups; // Trigger reactivity
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePopupClose(pinId: string) {
|
||||||
|
openPopups.delete(pinId);
|
||||||
|
openPopups = openPopups; // Trigger reactivity
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -170,7 +237,10 @@
|
|||||||
<div class="container mx-auto px-6 py-4">
|
<div class="container mx-auto px-6 py-4">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-4">
|
<div class="flex items-center gap-4">
|
||||||
<button class="btn btn-ghost btn-square lg:hidden" on:click={toggleSidebar}>
|
<button
|
||||||
|
class="btn btn-ghost btn-square lg:hidden"
|
||||||
|
on:click={() => (sidebarOpen = !sidebarOpen)}
|
||||||
|
>
|
||||||
<Filter class="w-5 h-5" />
|
<Filter class="w-5 h-5" />
|
||||||
</button>
|
</button>
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
@@ -182,7 +252,7 @@
|
|||||||
{$t('map.location_map')}
|
{$t('map.location_map')}
|
||||||
</h1>
|
</h1>
|
||||||
<p class="text-sm text-base-content/60">
|
<p class="text-sm text-base-content/60">
|
||||||
{filteredAdventures.length}
|
{filteredPins.length}
|
||||||
{$t('worldtravel.of')}
|
{$t('worldtravel.of')}
|
||||||
{totalAdventures}
|
{totalAdventures}
|
||||||
{$t('map.locations_shown')}
|
{$t('map.locations_shown')}
|
||||||
@@ -252,71 +322,85 @@
|
|||||||
class="w-full h-full min-h-[70vh] rounded-lg"
|
class="w-full h-full min-h-[70vh] rounded-lg"
|
||||||
standardControls
|
standardControls
|
||||||
>
|
>
|
||||||
{#each filteredAdventures as adventure}
|
{#each filteredPins as pin}
|
||||||
{#if adventure.latitude && adventure.longitude}
|
{#if pin.latitude && pin.longitude}
|
||||||
<Marker
|
<Marker
|
||||||
lngLat={[adventure.longitude, adventure.latitude]}
|
lngLat={[parseFloat(pin.longitude), parseFloat(pin.latitude)]}
|
||||||
class="grid h-8 w-8 place-items-center rounded-full border border-gray-200 shadow-lg cursor-pointer hover:scale-110 transition-transform {adventure.is_visited
|
class="grid h-8 w-8 place-items-center rounded-full border-2 border-white shadow-lg cursor-pointer hover:scale-110 transition-all duration-200 {pin.is_visited
|
||||||
? 'bg-red-300 hover:bg-red-400'
|
? 'bg-gradient-to-br from-emerald-400 to-emerald-600 hover:from-emerald-500 hover:to-emerald-700'
|
||||||
: 'bg-blue-300 hover:bg-blue-400'} text-black focus:outline-6 focus:outline-black"
|
: 'bg-gradient-to-br from-blue-400 to-blue-600 hover:from-blue-500 hover:to-blue-700'} text-white focus:outline-4 focus:outline-primary/50"
|
||||||
on:click={togglePopup}
|
|
||||||
>
|
>
|
||||||
<span class="text-xl">
|
<span class="text-xl">
|
||||||
{adventure.category?.icon || '📍'}
|
{pin.category?.icon || '📍'}
|
||||||
</span>
|
</span>
|
||||||
{#if isPopupOpen}
|
|
||||||
<Popup
|
<Popup
|
||||||
openOn="click"
|
openOn="click"
|
||||||
offset={[0, -10]}
|
offset={[0, -10]}
|
||||||
on:close={() => (isPopupOpen = false)}
|
on:open={() => handlePopupOpen(pin.id)}
|
||||||
|
on:close={() => handlePopupClose(pin.id)}
|
||||||
>
|
>
|
||||||
<div class="min-w-64 max-w-sm">
|
<div class="min-w-64 max-w-sm">
|
||||||
{#if adventure.images && adventure.images.length > 0}
|
{#if openPopups.has(pin.id)}
|
||||||
|
{#await fetchLocationDetails(pin.id)}
|
||||||
|
<div class="flex items-center justify-center p-4">
|
||||||
|
<span class="loading loading-spinner loading-sm"></span>
|
||||||
|
<span class="ml-2 text-sm">Loading details...</span>
|
||||||
|
</div>
|
||||||
|
{:then location}
|
||||||
|
{#if location}
|
||||||
|
{#if location.images && location.images.length > 0}
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<CardCarousel
|
<CardCarousel
|
||||||
images={adventure.images}
|
images={location.images}
|
||||||
name={adventure.name}
|
name={location.name}
|
||||||
icon={adventure?.category?.icon}
|
icon={location?.category?.icon}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="text-lg text-black font-bold">{adventure.name}</div>
|
<div class="text-lg text-black font-bold">{location.name}</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
class="badge {adventure.is_visited
|
class="badge {location.is_visited
|
||||||
? 'badge-success'
|
? 'badge-success'
|
||||||
: 'badge-info'} badge-sm"
|
: 'badge-info'} badge-sm"
|
||||||
>
|
>
|
||||||
{adventure.is_visited
|
{location.is_visited
|
||||||
? $t('adventures.visited')
|
? $t('adventures.visited')
|
||||||
: $t('adventures.planned')}
|
: $t('adventures.planned')}
|
||||||
</span>
|
</span>
|
||||||
{#if adventure.category}
|
{#if location.category}
|
||||||
<span class="badge badge-outline badge-sm">
|
<span class="badge badge-outline badge-sm">
|
||||||
{adventure.category.display_name}
|
{location.category.display_name}
|
||||||
{adventure.category.icon}
|
{location.category.icon}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if adventure.visits && adventure.visits.length > 0}
|
{#if location.visits && location.visits.length > 0}
|
||||||
<div class="text-black text-sm space-y-1">
|
<div class="text-black text-sm space-y-1">
|
||||||
{#each adventure.visits as visit}
|
{#each location.visits as visit}
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<Calendar class="w-3 h-3" />
|
<Calendar class="w-3 h-3" />
|
||||||
<span>
|
<span>
|
||||||
{visit.start_date
|
{visit.start_date
|
||||||
? new Date(visit.start_date).toLocaleDateString(undefined, {
|
? new Date(visit.start_date).toLocaleDateString(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
})
|
}
|
||||||
|
)
|
||||||
: ''}
|
: ''}
|
||||||
{visit.end_date &&
|
{visit.end_date &&
|
||||||
visit.end_date !== '' &&
|
visit.end_date !== '' &&
|
||||||
visit.end_date !== visit.start_date
|
visit.end_date !== visit.start_date
|
||||||
? ' - ' +
|
? ' - ' +
|
||||||
new Date(visit.end_date).toLocaleDateString(undefined, {
|
new Date(visit.end_date).toLocaleDateString(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
timeZone: 'UTC'
|
timeZone: 'UTC'
|
||||||
})
|
}
|
||||||
|
)
|
||||||
: ''}
|
: ''}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -324,10 +408,10 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="flex flex-col gap-2 pt-2">
|
<div class="flex flex-col gap-2 pt-2">
|
||||||
{#if adventure.longitude && adventure.latitude}
|
{#if location.longitude && location.latitude}
|
||||||
<a
|
<a
|
||||||
class="btn btn-outline btn-sm gap-2"
|
class="btn btn-outline btn-sm gap-2"
|
||||||
href={`https://maps.apple.com/?q=${adventure.latitude},${adventure.longitude}`}
|
href={`https://maps.apple.com/?q=${location.latitude},${location.longitude}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
>
|
>
|
||||||
@@ -337,16 +421,47 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary btn-sm gap-2"
|
class="btn btn-primary btn-sm gap-2"
|
||||||
on:click={() => goto(`/locations/${adventure.id}`)}
|
on:click={() => goto(`/locations/${location.id}`)}
|
||||||
>
|
>
|
||||||
<Eye class="w-4 h-4" />
|
<Eye class="w-4 h-4" />
|
||||||
{$t('map.view_details')}
|
{$t('map.view_details')}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="p-4 text-center">
|
||||||
|
<div class="text-lg text-black font-bold">{pin.name}</div>
|
||||||
|
<div class="text-sm text-gray-600">Failed to load details</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm gap-2 mt-2"
|
||||||
|
on:click={() => goto(`/locations/${pin.id}`)}
|
||||||
|
>
|
||||||
|
<Eye class="w-4 h-4" />
|
||||||
|
{$t('map.view_details')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{:catch error}
|
||||||
|
<div class="p-4 text-center">
|
||||||
|
<div class="text-lg text-black font-bold">{pin.name}</div>
|
||||||
|
<div class="text-sm text-red-600">Error loading details</div>
|
||||||
|
<button
|
||||||
|
class="btn btn-primary btn-sm gap-2 mt-2"
|
||||||
|
on:click={() => goto(`/locations/${pin.id}`)}
|
||||||
|
>
|
||||||
|
<Eye class="w-4 h-4" />
|
||||||
|
{$t('map.view_details')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/await}
|
||||||
|
{:else}
|
||||||
|
<div class="p-4 text-center">
|
||||||
|
<div class="text-lg text-black font-bold">{pin.name}</div>
|
||||||
|
<div class="text-sm text-gray-600">Click to load details...</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</Popup>
|
</Popup>
|
||||||
{/if}
|
|
||||||
</Marker>
|
</Marker>
|
||||||
{/if}
|
{/if}
|
||||||
{/each}
|
{/each}
|
||||||
@@ -551,7 +666,7 @@
|
|||||||
{#if newMarker}
|
{#if newMarker}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
<div class="alert alert-info">
|
<div class="alert alert-info">
|
||||||
<Pin class="w-4 h-4" />
|
<PinIcon class="w-4 h-4" />
|
||||||
<span class="text-sm">{$t('map.marker_placed_on_map')}</span>
|
<span class="text-sm">{$t('map.marker_placed_on_map')}</span>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="btn btn-primary w-full gap-2" on:click={newAdventure}>
|
<button type="button" class="btn btn-primary w-full gap-2" on:click={newAdventure}>
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
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 showGlobeSpin: boolean = false;
|
||||||
let sidebarOpen = false;
|
let sidebarOpen = false;
|
||||||
|
|
||||||
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
worldSubregions = [...new Set(allCountries.map((country) => country.subregion))];
|
||||||
@@ -74,6 +75,39 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// when isGlobeSpin is enabled, fetch /api/globespin/
|
||||||
|
type GlobeSpinData = {
|
||||||
|
country: {
|
||||||
|
flag_url: string;
|
||||||
|
name: string;
|
||||||
|
country_code: string;
|
||||||
|
num_visits: number;
|
||||||
|
subregion: string;
|
||||||
|
capital: string;
|
||||||
|
num_regions: number;
|
||||||
|
};
|
||||||
|
region: { name: string; num_cities: number };
|
||||||
|
city: { name: string; region_name: string };
|
||||||
|
};
|
||||||
|
let globeSpinData: GlobeSpinData | null = null;
|
||||||
|
let isLoadingGlobeSpin = false;
|
||||||
|
|
||||||
|
async function fetchGlobeSpin() {
|
||||||
|
isLoadingGlobeSpin = true;
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/globespin/');
|
||||||
|
if (response.ok) {
|
||||||
|
globeSpinData = await response.json();
|
||||||
|
} else {
|
||||||
|
console.error('Failed to fetch globe spin data');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching globe spin data:', error);
|
||||||
|
} finally {
|
||||||
|
isLoadingGlobeSpin = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function toggleSidebar() {
|
function toggleSidebar() {
|
||||||
sidebarOpen = !sidebarOpen;
|
sidebarOpen = !sidebarOpen;
|
||||||
}
|
}
|
||||||
@@ -171,6 +205,24 @@
|
|||||||
<span class="hidden sm:inline">{$t('worldtravel.show_map')}</span>
|
<span class="hidden sm:inline">{$t('worldtravel.show_map')}</span>
|
||||||
{/if}
|
{/if}
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Globe Spin Toggle -->
|
||||||
|
<button
|
||||||
|
class="btn btn-outline gap-2 {showGlobeSpin ? 'btn-active' : ''}"
|
||||||
|
on:click={() => {
|
||||||
|
showGlobeSpin = !showGlobeSpin;
|
||||||
|
if (showGlobeSpin) {
|
||||||
|
fetchGlobeSpin();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{#if showGlobeSpin}
|
||||||
|
<Globe class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">{$t('worldtravel.hide_globe_spin')}</span>
|
||||||
|
{:else}
|
||||||
|
<Globe class="w-4 h-4" />
|
||||||
|
<span class="hidden sm:inline">{$t('worldtravel.show_globe_spin')}</span>
|
||||||
|
{/if}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filter Chips -->
|
<!-- Filter Chips -->
|
||||||
@@ -264,6 +316,257 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Globe Spin Section -->
|
||||||
|
{#if showGlobeSpin}
|
||||||
|
<div class="container mx-auto px-6 py-4">
|
||||||
|
<div class="card bg-base-100 shadow-xl overflow-hidden">
|
||||||
|
<div class="card-body p-6">
|
||||||
|
{#if isLoadingGlobeSpin}
|
||||||
|
<!-- Loading State with Spinning Globe -->
|
||||||
|
<div class="flex flex-col items-center py-12">
|
||||||
|
<div class="relative">
|
||||||
|
<!-- Spinning globe with pulse effect -->
|
||||||
|
<div class="relative animate-spin" style="animation-duration: 3s;">
|
||||||
|
<div
|
||||||
|
class="w-24 h-24 rounded-full bg-gradient-to-br from-primary/20 to-accent/30 flex items-center justify-center border-4 border-primary/30"
|
||||||
|
>
|
||||||
|
<Globe class="w-12 h-12 text-primary" />
|
||||||
|
</div>
|
||||||
|
<!-- Orbit rings -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 rounded-full border-2 border-dashed border-primary/20 animate-pulse"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute -inset-2 rounded-full border border-dashed border-accent/20 animate-pulse"
|
||||||
|
style="animation-delay: 0.5s;"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- Sparkle effects -->
|
||||||
|
<div
|
||||||
|
class="absolute -top-2 -right-2 w-3 h-3 bg-yellow-400 rounded-full animate-ping"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute -bottom-3 -left-3 w-2 h-2 bg-blue-400 rounded-full animate-ping"
|
||||||
|
style="animation-delay: 1s;"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="absolute top-1/2 -right-4 w-1.5 h-1.5 bg-green-400 rounded-full animate-ping"
|
||||||
|
style="animation-delay: 2s;"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 text-center">
|
||||||
|
<h3 class="text-xl font-bold text-primary mb-2">
|
||||||
|
{$t('worldtravel.spinning_globe') + '...'}
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/70 animate-pulse">
|
||||||
|
{$t('worldtravel.loading_globe_spin')}
|
||||||
|
</p>
|
||||||
|
<div class="flex items-center justify-center gap-1 mt-3">
|
||||||
|
<div class="w-2 h-2 bg-primary rounded-full animate-bounce"></div>
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||||
|
style="animation-delay: 0.2s;"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="w-2 h-2 bg-primary rounded-full animate-bounce"
|
||||||
|
style="animation-delay: 0.4s;"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else if globeSpinData}
|
||||||
|
<!-- Result Display with Amazing Animations -->
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-6">
|
||||||
|
<h3
|
||||||
|
class="text-2xl font-bold text-primary mb-2 flex items-center justify-center gap-3"
|
||||||
|
>
|
||||||
|
<Globe class="w-8 h-8 animate-spin" style="animation-duration: 4s;" />
|
||||||
|
{$t('worldtravel.destination_revealed')}
|
||||||
|
<Globe
|
||||||
|
class="w-8 h-8 animate-spin"
|
||||||
|
style="animation-duration: 4s; animation-direction: reverse;"
|
||||||
|
/>
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/60">
|
||||||
|
{$t('worldtravel.your_random_adventure_awaits')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Country Card with Entrance Animation -->
|
||||||
|
<div class="animate-slideInUp" style="animation-duration: 0.8s;">
|
||||||
|
<!-- Flag with Reveal Effect -->
|
||||||
|
<div class="relative mb-6 mx-auto w-fit">
|
||||||
|
<div
|
||||||
|
class="relative overflow-hidden rounded-2xl shadow-2xl border-4 border-primary/20 hover:border-primary/40 transition-colors duration-300"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={globeSpinData.country.flag_url}
|
||||||
|
alt="{globeSpinData.country.name} flag"
|
||||||
|
class="w-64 h-40 object-cover hover:scale-105 transition-transform duration-500"
|
||||||
|
/>
|
||||||
|
<!-- Shimmer overlay -->
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-shimmer"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<!-- Floating badges -->
|
||||||
|
<div
|
||||||
|
class="absolute -top-3 -right-3 badge badge-primary badge-lg animate-bounce shadow-lg"
|
||||||
|
>
|
||||||
|
{globeSpinData.country.country_code}
|
||||||
|
</div>
|
||||||
|
{#if globeSpinData.country.num_visits > 0}
|
||||||
|
<div
|
||||||
|
class="absolute -top-3 -left-3 badge badge-success badge-lg animate-pulse shadow-lg"
|
||||||
|
>
|
||||||
|
<Check class="w-4 h-4 mr-1" />
|
||||||
|
{$t('adventures.visited')}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Country Info -->
|
||||||
|
<div class="space-y-4 animate-fadeInUp" style="animation-delay: 0.2s;">
|
||||||
|
<h2
|
||||||
|
class="text-4xl font-bold text-primary bg-gradient-to-r from-primary to-accent bg-clip-text text-transparent pb-2"
|
||||||
|
>
|
||||||
|
{globeSpinData.country.name}
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div class="flex flex-wrap justify-center gap-4">
|
||||||
|
<div class="badge badge-lg badge-outline gap-2">
|
||||||
|
<Pin class="w-4 h-4" />
|
||||||
|
{globeSpinData.country.subregion}
|
||||||
|
</div>
|
||||||
|
{#if globeSpinData.country.capital}
|
||||||
|
<div class="badge badge-lg badge-outline gap-2">
|
||||||
|
<Globe class="w-4 h-4" />
|
||||||
|
{globeSpinData.country.capital}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress Info -->
|
||||||
|
<div
|
||||||
|
class="card bg-gradient-to-br from-base-200/50 to-base-300/30 p-4 max-w-md mx-auto"
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<span class="text-sm font-medium"
|
||||||
|
>{$t('worldtravel.exploration_progress')}</span
|
||||||
|
>
|
||||||
|
<span class="text-lg font-bold text-primary">
|
||||||
|
{globeSpinData.country.num_visits}/{globeSpinData.country.num_regions}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<progress
|
||||||
|
class="progress progress-primary w-full"
|
||||||
|
value={globeSpinData.country.num_visits}
|
||||||
|
max={globeSpinData.country.num_regions}
|
||||||
|
></progress>
|
||||||
|
<div class="text-xs text-base-content/60 mt-1">
|
||||||
|
{Math.round(
|
||||||
|
(globeSpinData.country.num_visits / globeSpinData.country.num_regions) *
|
||||||
|
100
|
||||||
|
)}% explored
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Region & City Info (if available) -->
|
||||||
|
{#if globeSpinData.region || globeSpinData.city}
|
||||||
|
<div class="mt-8 space-y-4 animate-fadeInUp" style="animation-delay: 0.4s;">
|
||||||
|
<div class="divider">
|
||||||
|
<span class="text-primary font-semibold"
|
||||||
|
>{$t('worldtravel.dive_deeper')}</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid md:grid-cols-2 gap-4 max-w-2xl mx-auto">
|
||||||
|
{#if globeSpinData.region}
|
||||||
|
<div
|
||||||
|
class="card bg-gradient-to-br from-accent/10 to-secondary/10 border border-accent/20"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h4 class="font-bold text-accent flex items-center gap-2">
|
||||||
|
<Pin class="w-5 h-5" />
|
||||||
|
{$t('adventures.region')}
|
||||||
|
</h4>
|
||||||
|
<p class="text-lg font-semibold">{globeSpinData.region.name}</p>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{globeSpinData.region.num_cities}
|
||||||
|
{$t('worldtravel.cities_available')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if globeSpinData.city}
|
||||||
|
<div
|
||||||
|
class="card bg-gradient-to-br from-success/10 to-info/10 border border-success/20"
|
||||||
|
>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<h4 class="font-bold text-success flex items-center gap-2">
|
||||||
|
<Map class="w-5 h-5" />
|
||||||
|
{$t('adventures.city')}
|
||||||
|
</h4>
|
||||||
|
<p class="text-lg font-semibold">{globeSpinData.city.name}</p>
|
||||||
|
<p class="text-sm text-base-content/60">
|
||||||
|
{$t('worldtravel.in')}
|
||||||
|
{globeSpinData.city.region_name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div
|
||||||
|
class="mt-8 flex flex-wrap justify-center gap-4 animate-fadeInUp"
|
||||||
|
style="animation-delay: 0.6s;"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
href="/worldtravel/{globeSpinData.country.country_code}"
|
||||||
|
class="btn btn-primary btn-lg gap-2 shadow-lg hover:shadow-xl transition-all duration-300 hover:scale-105"
|
||||||
|
>
|
||||||
|
<Globe class="w-5 h-5" />
|
||||||
|
{$t('worldtravel.explore_country')}
|
||||||
|
</a>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline btn-lg gap-2 hover:scale-105 transition-all duration-300"
|
||||||
|
on:click={fetchGlobeSpin}
|
||||||
|
>
|
||||||
|
<Globe class="w-5 h-5 animate-spin" style="animation-duration: 2s;" />
|
||||||
|
{$t('worldtravel.spin_again')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<!-- No Data State -->
|
||||||
|
<div class="flex flex-col items-center py-12">
|
||||||
|
<div class="p-6 bg-error/10 rounded-2xl mb-6">
|
||||||
|
<Cancel class="w-16 h-16 text-error/50" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-xl font-semibold text-base-content/70 mb-2">
|
||||||
|
{$t('worldtravel.no_globe_spin_data')}
|
||||||
|
</h3>
|
||||||
|
<p class="text-base-content/50 text-center max-w-md mb-6">
|
||||||
|
{$t('worldtravel.globe_spin_error_desc')}
|
||||||
|
</p>
|
||||||
|
<button class="btn btn-primary gap-2" on:click={fetchGlobeSpin}>
|
||||||
|
<Globe class="w-4 h-4" />
|
||||||
|
{$t('worldtravel.try_again')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<div class="container mx-auto px-6 py-8">
|
<div class="container mx-auto px-6 py-8">
|
||||||
{#if filteredCountries.length === 0}
|
{#if filteredCountries.length === 0}
|
||||||
@@ -433,3 +736,48 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes slideInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fadeInUp {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-slideInUp {
|
||||||
|
animation: slideInUp ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-fadeInUp {
|
||||||
|
animation: fadeInUp ease-out forwards;
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-shimmer {
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user