diff --git a/backend/server/adventures/serializers.py b/backend/server/adventures/serializers.py index df0f9ac..5771c7c 100644 --- a/backend/server/adventures/serializers.py +++ b/backend/server/adventures/serializers.py @@ -203,6 +203,17 @@ class TransportationSerializer(CustomModelSerializer): ] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] +class HotelSerializer(CustomModelSerializer): + + class Meta: + model = Hotel + fields = [ + 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', + 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', + 'collection', 'created_at', 'updated_at' + ] + read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] + class NoteSerializer(CustomModelSerializer): class Meta: @@ -289,10 +300,11 @@ class CollectionSerializer(CustomModelSerializer): transportations = TransportationSerializer(many=True, read_only=True, source='transportation_set') notes = NoteSerializer(many=True, read_only=True, source='note_set') checklists = ChecklistSerializer(many=True, read_only=True, source='checklist_set') + hotels = HotelSerializer(many=True, read_only=True, source='hotel_set') class Meta: model = Collection - fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link'] + fields = ['id', 'description', 'user_id', 'name', 'is_public', 'adventures', 'created_at', 'start_date', 'end_date', 'transportations', 'notes', 'updated_at', 'checklists', 'is_archived', 'shared_with', 'link', 'hotels'] read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] def to_representation(self, instance): @@ -302,15 +314,4 @@ class CollectionSerializer(CustomModelSerializer): for user in instance.shared_with.all(): shared_uuids.append(str(user.uuid)) representation['shared_with'] = shared_uuids - return representation - -class HotelSerializer(CustomModelSerializer): - - class Meta: - model = Hotel - fields = [ - 'id', 'user_id', 'name', 'description', 'rating', 'link', 'check_in', 'check_out', - 'reservation_number', 'price', 'latitude', 'longitude', 'location', 'is_public', - 'collection', 'created_at', 'updated_at' - ] - read_only_fields = ['id', 'created_at', 'updated_at', 'user_id'] \ No newline at end of file + return representation \ No newline at end of file diff --git a/backend/server/worldtravel/management/commands/download-countries.py b/backend/server/worldtravel/management/commands/download-countries.py index 3a1cd11..9930b8e 100644 --- a/backend/server/worldtravel/management/commands/download-countries.py +++ b/backend/server/worldtravel/management/commands/download-countries.py @@ -45,8 +45,9 @@ class Command(BaseCommand): force = options['force'] batch_size = 100 current_version_json = os.path.join(settings.MEDIA_ROOT, 'data_version.json') - cdn_version_json = requests.get(f'{ADVENTURELOG_CDN_URL}/data/version.json') - if cdn_version_json.status_code == 200: + try: + cdn_version_json = requests.get(f'{ADVENTURELOG_CDN_URL}/data/version.json') + cdn_version_json.raise_for_status() cdn_version = cdn_version_json.json().get('version') if os.path.exists(current_version_json): with open(current_version_json, 'r') as f: @@ -62,8 +63,8 @@ class Command(BaseCommand): else: self.stdout.write(self.style.SUCCESS('Data is already up-to-date. Run with --force to re-download')) return - else: - self.stdout.write(self.style.ERROR('Error downloading version.json')) + except requests.RequestException as e: + self.stdout.write(self.style.ERROR(f'Error fetching version from the CDN: {e}, skipping data import. Try restarting the container once CDN connection has been restored.')) return self.stdout.write(self.style.SUCCESS('Fetching latest data from the AdventureLog CDN located at: ' + ADVENTURELOG_CDN_URL)) @@ -90,27 +91,6 @@ class Command(BaseCommand): self.stdout.write(self.style.ERROR('Error downloading countries_states_cities.json')) return - - # if not os.path.exists(version_json) or force: - # res = requests.get(f'https://raw.githubusercontent.com/dr5hn/countries-states-cities-database/{COUNTRY_REGION_JSON_VERSION}/json/countries%2Bstates%2Bcities.json') - # if res.status_code == 200: - # with open(countries_json_path, 'w') as f: - # f.write(res.text) - # self.stdout.write(self.style.SUCCESS('countries+regions+states.json downloaded successfully')) - # else: - # self.stdout.write(self.style.ERROR('Error downloading countries+regions+states.json')) - # return - # elif not os.path.isfile(countries_json_path): - # self.stdout.write(self.style.ERROR('countries+regions+states.json is not a file')) - # return - # elif os.path.getsize(countries_json_path) == 0: - # self.stdout.write(self.style.ERROR('countries+regions+states.json is empty')) - # elif Country.objects.count() == 0 or Region.objects.count() == 0 or City.objects.count() == 0: - # self.stdout.write(self.style.WARNING('Some region data is missing. Re-importing all data.')) - # else: - # self.stdout.write(self.style.SUCCESS('Latest country, region, and state data already downloaded.')) - # return - with open(countries_json_path, 'r') as f: f = open(countries_json_path, 'rb') parser = ijson.items(f, 'item') diff --git a/frontend/src/lib/components/AdventureModal.svelte b/frontend/src/lib/components/AdventureModal.svelte index 28cd752..3a5afc9 100644 --- a/frontend/src/lib/components/AdventureModal.svelte +++ b/frontend/src/lib/components/AdventureModal.svelte @@ -1,51 +1,35 @@ @@ -810,150 +597,7 @@ -
- -
- {$t('adventures.location_information')} -
-
- -
-
-
- - {#if is_custom_location} - - {/if} -
-
- -
-
- - - -
-
- {#if places.length > 0} -
-

{$t('adventures.search_results')}

- -
- {#each places as place} - - {/each} -
-
- {:else if noPlaces} -

{$t('adventures.no_results')}

- {/if} - -
- - - - - {#each markers as marker} - - {/each} - - {#if reverseGeocodePlace} -
-

- {reverseGeocodePlace.city - ? reverseGeocodePlace.city + ', ' - : ''}{reverseGeocodePlace.region}, - {reverseGeocodePlace.country} -

-

- {reverseGeocodePlace.region}: - {reverseGeocodePlace.region_visited - ? $t('adventures.visited') - : $t('adventures.not_visited')} -

- {#if reverseGeocodePlace.city} -

- {reverseGeocodePlace.city}: - {reverseGeocodePlace.city_visited - ? $t('adventures.visited') - : $t('adventures.not_visited')} -

- {/if} -
- {#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)} - - {/if} - {#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)} - - {/if} - {/if} -
-
-
+
diff --git a/frontend/src/lib/components/HotelModal.svelte b/frontend/src/lib/components/HotelModal.svelte index e44d1f8..241be48 100644 --- a/frontend/src/lib/components/HotelModal.svelte +++ b/frontend/src/lib/components/HotelModal.svelte @@ -6,6 +6,7 @@ import { appVersion } from '$lib/config'; import { DefaultMarker, MapEvents, MapLibre } from 'svelte-maplibre'; import type { Collection, Hotel, ReverseGeocode, OpenStreetMapPlace, Point } from '$lib/types'; + import LocationDropdown from './LocationDropdown.svelte'; const dispatch = createEventDispatcher(); @@ -17,7 +18,7 @@ let hotel: Hotel = { ...initializeHotel(hotelToEdit) }; let fullStartDate: string = ''; let fullEndDate: string = ''; - let reverseGeocodePlace: ReverseGeocode | null = null; + let reverseGeocodePlace: any | null = null; let query: string = ''; let places: OpenStreetMapPlace[] = []; let noPlaces: boolean = false; @@ -83,34 +84,6 @@ if (event.key === 'Escape') close(); } - // Geocode location search - async function geocode(e: Event | null) { - if (e) e.preventDefault(); - if (!query) { - alert($t('adventures.no_location')); - return; - } - const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${query}&format=jsonv2`, { - headers: { 'User-Agent': `AdventureLog / ${appVersion}` } - }); - const data = (await res.json()) as OpenStreetMapPlace[]; - places = data; - noPlaces = data.length === 0; - } - - // Set custom location flag based on hotel location - $: is_custom_location = hotel.location !== (reverseGeocodePlace?.display_name || ''); - - // Add marker to map - async function addMarker(e: CustomEvent) { - markers = [{ lngLat: e.detail.lngLat, name: '', location: '', activity_type: '' }]; - } - - // Clear all markers from the map - function clearMap() { - markers = []; - } - // Handle form submission (save hotel) async function handleSubmit(event: Event) { event.preventDefault(); @@ -325,106 +298,7 @@
-
- -
- {$t('adventures.location_information')} -
-
- -
-
-
- - {#if is_custom_location} - - {/if} -
-
- -
-
- - - -
-
- {#if places.length > 0} -
-

{$t('adventures.search_results')}

- -
- {#each places as place} - - {/each} -
-
- {:else if noPlaces} -

{$t('adventures.no_results')}

- {/if} - -
- - - - - {#each markers as marker} - - {/each} - -
-
-
- -
- -
- {$t('adventures.location_information')} -
- -
-
+
diff --git a/frontend/src/lib/components/LocationDropdown.svelte b/frontend/src/lib/components/LocationDropdown.svelte new file mode 100644 index 0000000..42328f4 --- /dev/null +++ b/frontend/src/lib/components/LocationDropdown.svelte @@ -0,0 +1,336 @@ + + +
+ +
+ {$t('adventures.location_information')} +
+
+ +
+
+
+ + {#if is_custom_location} + + {/if} +
+
+ +
+
+ + + +
+
+ {#if places.length > 0} +
+

{$t('adventures.search_results')}

+ +
+ {#each places as place} + + {/each} +
+
+ {:else if noPlaces} +

{$t('adventures.no_results')}

+ {/if} + +
+ + + + + {#each markers as marker} + + {/each} + + {#if reverseGeocodePlace} +
+

+ {reverseGeocodePlace.city + ? reverseGeocodePlace.city + ', ' + : ''}{reverseGeocodePlace.region}, + {reverseGeocodePlace.country} +

+

+ {reverseGeocodePlace.region}: + {reverseGeocodePlace.region_visited + ? $t('adventures.visited') + : $t('adventures.not_visited')} +

+ {#if reverseGeocodePlace.city} +

+ {reverseGeocodePlace.city}: + {reverseGeocodePlace.city_visited + ? $t('adventures.visited') + : $t('adventures.not_visited')} +

+ {/if} +
+ {#if !reverseGeocodePlace.region_visited || (!reverseGeocodePlace.city_visited && !willBeMarkedVisited)} + + {/if} + {#if (willBeMarkedVisited && !reverseGeocodePlace.region_visited && reverseGeocodePlace.region_id) || (!reverseGeocodePlace.city_visited && willBeMarkedVisited && reverseGeocodePlace.city_id)} + + {/if} + {/if} +
+
+
diff --git a/frontend/src/lib/types.ts b/frontend/src/lib/types.ts index e339ee0..30f5b27 100644 --- a/frontend/src/lib/types.ts +++ b/frontend/src/lib/types.ts @@ -113,6 +113,7 @@ export type Collection = { end_date: string | null; transportations?: Transportation[]; notes?: Note[]; + hotels?: Hotel[]; checklists?: Checklist[]; is_archived?: boolean; shared_with: string[] | undefined; diff --git a/frontend/src/locales/en.json b/frontend/src/locales/en.json index 4c4e645..011dfbf 100644 --- a/frontend/src/locales/en.json +++ b/frontend/src/locales/en.json @@ -191,6 +191,7 @@ "no_description_found": "No description found", "adventure_created": "Adventure created", "adventure_create_error": "Failed to create adventure", + "hotel": "Hotel", "create_adventure": "Create Adventure", "adventure_updated": "Adventure updated", "adventure_update_error": "Failed to update adventure", diff --git a/frontend/src/routes/collections/[id]/+page.svelte b/frontend/src/routes/collections/[id]/+page.svelte index 6f49525..f19f7c7 100644 --- a/frontend/src/routes/collections/[id]/+page.svelte +++ b/frontend/src/routes/collections/[id]/+page.svelte @@ -1,5 +1,5 @@ {#if isShowingLinkModal} @@ -376,6 +404,15 @@ /> {/if} +{#if isShowingHotelModal} + (isShowingHotelModal = false)} + on:save={saveOrCreateHotel} + {collection} + /> +{/if} + {#if isAdventureModalOpen} {$t('adventures.checklist')} +