diff --git a/src/lib/components/AddPlaces.svelte b/src/lib/components/AddPlaces.svelte index e04553d..a2adb31 100644 --- a/src/lib/components/AddPlaces.svelte +++ b/src/lib/components/AddPlaces.svelte @@ -210,4 +210,8 @@ :global(.pac-item:hover) { background-color: var(--gray-50); } + + :global(.pac-container:after) { + display: none; + } \ No newline at end of file diff --git a/src/lib/components/DeleteConfirmationPopup.svelte b/src/lib/components/DeleteConfirmationPopup.svelte new file mode 100644 index 0000000..a6d81b5 --- /dev/null +++ b/src/lib/components/DeleteConfirmationPopup.svelte @@ -0,0 +1,91 @@ + + +{#if showPopup} + +
+ +
+{/if} + + \ No newline at end of file diff --git a/src/lib/components/ItineraryDate.svelte b/src/lib/components/ItineraryDate.svelte index 622e425..253c85a 100644 --- a/src/lib/components/ItineraryDate.svelte +++ b/src/lib/components/ItineraryDate.svelte @@ -6,7 +6,21 @@ export let date; export let isExpanded = true; - export let places: { name: string; desc?: string; image?: string; time?: string; }[] = []; + export let countryCode = 'tw'; // Default to Taiwan if not provided + + interface Place { + name: string; + desc?: string; + image?: string; + time?: string; + geometry?: { + lat: number; + lng: number; + }; + } + + export let places: Place[] = []; + export let onPlacesUpdate: (places: Place[]) => void; function toggleDate() { isExpanded = !isExpanded; @@ -17,14 +31,22 @@ name: place.name || 'Unknown Place', desc: place.formatted_address || '', image: (place as any).photoUrl || '/placeholder.jpeg', - time: 'Add Time' + time: 'Add Time', + geometry: place.geometry?.location ? { + lat: place.geometry.location.lat(), + lng: place.geometry.location.lng() + } : undefined }; - places = [...places, newPlace]; + const updatedPlaces = [...places, newPlace]; + places = updatedPlaces; + onPlacesUpdate(updatedPlaces); } function handleDeletePlace(index: number) { - places = places.filter((_, i) => i !== index); + const updatedPlaces = places.filter((_, i) => i !== index); + places = updatedPlaces; + onPlacesUpdate(updatedPlaces); } @@ -48,7 +70,7 @@
diff --git a/src/lib/components/PastTripsPanel.svelte b/src/lib/components/PastTripsPanel.svelte new file mode 100644 index 0000000..44fb141 --- /dev/null +++ b/src/lib/components/PastTripsPanel.svelte @@ -0,0 +1,258 @@ + + +{#if showPanel} +
+
+

Past Trips to {destination}

+ +
+ +
+ {#if loading} +
Loading...
+ {:else if pastTrips.length === 0} +
This is your first trip to {destination}!
+ {:else} +
+ {#if showLeftButton} + + {/if} + +
+ {#each pastTrips as trip} + + {/each} +
+ + {#if showRightButton} + + {/if} +
+ {/if} +
+
+{/if} + + \ No newline at end of file diff --git a/src/lib/components/TripCard.svelte b/src/lib/components/TripCard.svelte index 55d8918..dbd28cb 100644 --- a/src/lib/components/TripCard.svelte +++ b/src/lib/components/TripCard.svelte @@ -1,11 +1,60 @@ - -
+ +
{#if !image} @@ -13,6 +62,16 @@
{/if} + {#if showDelete} + + {/if}

{destination}

@@ -20,6 +79,13 @@
+ + \ No newline at end of file diff --git a/src/lib/constants/CountryMappings.ts b/src/lib/constants/CountryMappings.ts new file mode 100644 index 0000000..51457c8 --- /dev/null +++ b/src/lib/constants/CountryMappings.ts @@ -0,0 +1,250 @@ +export const countryMappings: Record = { + 'Afghanistan': 'AF', + 'Albania': 'AL', + 'Algeria': 'DZ', + 'American Samoa': 'AS', + 'Andorra': 'AD', + 'Angola': 'AO', + 'Anguilla': 'AI', + 'Antarctica': 'AQ', + 'Antigua and Barbuda': 'AG', + 'Argentina': 'AR', + 'Armenia': 'AM', + 'Aruba': 'AW', + 'Australia': 'AU', + 'Austria': 'AT', + 'Azerbaijan': 'AZ', + 'Bahamas': 'BS', + 'Bahrain': 'BH', + 'Bangladesh': 'BD', + 'Barbados': 'BB', + 'Belarus': 'BY', + 'Belgium': 'BE', + 'Belize': 'BZ', + 'Benin': 'BJ', + 'Bermuda': 'BM', + 'Bhutan': 'BT', + 'Bolivia': 'BO', + 'Bonaire, Sint Eustatius and Saba': 'BQ', + 'Bosnia and Herzegovina': 'BA', + 'Botswana': 'BW', + 'Bouvet Island': 'BV', + 'Brazil': 'BR', + 'British Indian Ocean Territory': 'IO', + 'Brunei Darussalam': 'BN', + 'Bulgaria': 'BG', + 'Burkina Faso': 'BF', + 'Burundi': 'BI', + 'Cambodia': 'KH', + 'Cameroon': 'CM', + 'Canada': 'CA', + 'Cape Verde': 'CV', + 'Cayman Islands': 'KY', + 'Central African Republic': 'CF', + 'Chad': 'TD', + 'Chile': 'CL', + 'China': 'CN', + 'Christmas Island': 'CX', + 'Cocos (Keeling) Islands': 'CC', + 'Colombia': 'CO', + 'Comoros': 'KM', + 'Congo': 'CG', + 'Congo, the Democratic Republic of the': 'CD', + 'Cook Islands': 'CK', + 'Costa Rica': 'CR', + 'Croatia': 'HR', + 'Cuba': 'CU', + 'Curaçao': 'CW', + 'Cyprus': 'CY', + 'Czech Republic': 'CZ', + "Côte d'Ivoire": 'CI', + 'Denmark': 'DK', + 'Djibouti': 'DJ', + 'Dominica': 'DM', + 'Dominican Republic': 'DO', + 'Ecuador': 'EC', + 'Egypt': 'EG', + 'El Salvador': 'SV', + 'Equatorial Guinea': 'GQ', + 'Eritrea': 'ER', + 'Estonia': 'EE', + 'Ethiopia': 'ET', + 'Falkland Islands (Malvinas)': 'FK', + 'Faroe Islands': 'FO', + 'Fiji': 'FJ', + 'Finland': 'FI', + 'France': 'FR', + 'French Guiana': 'GF', + 'French Polynesia': 'PF', + 'French Southern Territories': 'TF', + 'Gabon': 'GA', + 'Gambia': 'GM', + 'Georgia': 'GE', + 'Germany': 'DE', + 'Ghana': 'GH', + 'Gibraltar': 'GI', + 'Greece': 'GR', + 'Greenland': 'GL', + 'Grenada': 'GD', + 'Guadeloupe': 'GP', + 'Guam': 'GU', + 'Guatemala': 'GT', + 'Guernsey': 'GG', + 'Guinea': 'GN', + 'Guinea-Bissau': 'GW', + 'Guyana': 'GY', + 'Haiti': 'HT', + 'Heard Island and McDonald Islands': 'HM', + 'Holy See (Vatican City State)': 'VA', + 'Honduras': 'HN', + 'Hong Kong': 'HK', + 'Hungary': 'HU', + 'Iceland': 'IS', + 'India': 'IN', + 'Indonesia': 'ID', + 'Iran': 'IR', + 'Iraq': 'IQ', + 'Ireland': 'IE', + 'Isle of Man': 'IM', + 'Italy': 'IT', + 'Jamaica': 'JM', + 'Japan': 'JP', + 'Jersey': 'JE', + 'Jordan': 'JO', + 'Kazakhstan': 'KZ', + 'Kenya': 'KE', + 'Kiribati': 'KI', + 'North Korea': 'KP', + 'South Korea': 'KR', + 'Kuwait': 'KW', + 'Kyrgyzstan': 'KG', + "Laos": 'LA', + 'Latvia': 'LV', + 'Lebanon': 'LB', + 'Lesotho': 'LS', + 'Liberia': 'LR', + 'Libya': 'LY', + 'Liechtenstein': 'LI', + 'Lithuania': 'LT', + 'Luxembourg': 'LU', + 'Macao': 'MO', + 'Macedonia, the former Yugoslav Republic of': 'MK', + 'Madagascar': 'MG', + 'Malawi': 'MW', + 'Malaysia': 'MY', + 'Maldives': 'MV', + 'Mali': 'ML', + 'Malta': 'MT', + 'Marshall Islands': 'MH', + 'Martinique': 'MQ', + 'Mauritania': 'MR', + 'Mauritius': 'MU', + 'Mayotte': 'YT', + 'Mexico': 'MX', + 'Micronesia': 'FM', + 'Moldova, Republic of': 'MD', + 'Monaco': 'MC', + 'Mongolia': 'MN', + 'Montenegro': 'ME', + 'Montserrat': 'MS', + 'Morocco': 'MA', + 'Mozambique': 'MZ', + 'Myanmar': 'MM', + 'Namibia': 'NA', + 'Nauru': 'NR', + 'Nepal': 'NP', + 'Netherlands': 'NL', + 'New Caledonia': 'NC', + 'New Zealand': 'NZ', + 'Nicaragua': 'NI', + 'Niger': 'NE', + 'Nigeria': 'NG', + 'Niue': 'NU', + 'Norfolk Island': 'NF', + 'Northern Mariana Islands': 'MP', + 'Norway': 'NO', + 'Oman': 'OM', + 'Pakistan': 'PK', + 'Palau': 'PW', + 'Palestine, State of': 'PS', + 'Panama': 'PA', + 'Papua New Guinea': 'PG', + 'Paraguay': 'PY', + 'Peru': 'PE', + 'Philippines': 'PH', + 'Pitcairn': 'PN', + 'Poland': 'PL', + 'Portugal': 'PT', + 'Puerto Rico': 'PR', + 'Qatar': 'QA', + 'Romania': 'RO', + 'Russian Federation': 'RU', + 'Rwanda': 'RW', + 'Réunion': 'RE', + 'Saint Barthélemy': 'BL', + 'Saint Helena, Ascension and Tristan da Cunha': 'SH', + 'Saint Kitts and Nevis': 'KN', + 'Saint Lucia': 'LC', + 'Saint Martin (French part)': 'MF', + 'Saint Pierre and Miquelon': 'PM', + 'Saint Vincent and the Grenadines': 'VC', + 'Samoa': 'WS', + 'San Marino': 'SM', + 'Sao Tome and Principe': 'ST', + 'Saudi Arabia': 'SA', + 'Senegal': 'SN', + 'Serbia': 'RS', + 'Seychelles': 'SC', + 'Sierra Leone': 'SL', + 'Singapore': 'SG', + 'Sint Maarten (Dutch part)': 'SX', + 'Slovakia': 'SK', + 'Slovenia': 'SI', + 'Solomon Islands': 'SB', + 'Somalia': 'SO', + 'South Africa': 'ZA', + 'South Georgia and the South Sandwich Islands': 'GS', + 'South Sudan': 'SS', + 'Spain': 'ES', + 'Sri Lanka': 'LK', + 'Sudan': 'SD', + 'Suriname': 'SR', + 'Svalbard and Jan Mayen': 'SJ', + 'Swaziland': 'SZ', + 'Sweden': 'SE', + 'Switzerland': 'CH', + 'Syrian Arab Republic': 'SY', + 'Taiwan': 'TW', + 'Tajikistan': 'TJ', + 'Tanzania, United Republic of': 'TZ', + 'Thailand': 'TH', + 'Timor-Leste': 'TL', + 'Togo': 'TG', + 'Tokelau': 'TK', + 'Tonga': 'TO', + 'Trinidad and Tobago': 'TT', + 'Tunisia': 'TN', + 'Turkey': 'TR', + 'Turkmenistan': 'TM', + 'Turks and Caicos Islands': 'TC', + 'Tuvalu': 'TV', + 'Uganda': 'UG', + 'Ukraine': 'UA', + 'United Arab Emirates': 'AE', + 'United Kingdom': 'GB', + 'United States': 'US', + 'United States Minor Outlying Islands': 'UM', + 'Uruguay': 'UY', + 'Uzbekistan': 'UZ', + 'Vanuatu': 'VU', + 'Venezuela': 'VE', + 'Vietnam': 'VN', + 'Virgin Islands, British': 'VG', + 'Virgin Islands, U.S.': 'VI', + 'Wallis and Futuna': 'WF', + 'Western Sahara': 'EH', + 'Yemen': 'YE', + 'Zambia': 'ZM', + 'Zimbabwe': 'ZW', + 'Åland Islands': 'AX' +} \ No newline at end of file diff --git a/src/routes/itinerary/+page.svelte b/src/routes/itinerary/+page.svelte deleted file mode 100644 index 9f32c0c..0000000 --- a/src/routes/itinerary/+page.svelte +++ /dev/null @@ -1,383 +0,0 @@ - - -
-
-
-
- -
- -
-

Trip to {destination}

-

{startDate} - {endDate}

-
- -
- -
-
- -
-
- - - - {#if expandedSections.places_to_visit} -
-
- {#each placesToVisit as place, i} - handleDeletePlace(i)} - /> - {/each} -
- - - -
-
-
- {/if} -
- -
- - - {#if expandedSections.itinerary} -
- {#each tripDates as date} - - {/each} -
- {/if} -
- -
-
-
-
- -
-
- -
-
- - - \ No newline at end of file diff --git a/src/routes/itinerary/[tid]/+page.svelte b/src/routes/itinerary/[tid]/+page.svelte index 9c746cf..0ff0b94 100644 --- a/src/routes/itinerary/[tid]/+page.svelte +++ b/src/routes/itinerary/[tid]/+page.svelte @@ -15,18 +15,134 @@ import ItineraryDate from '$lib/components/ItineraryDate.svelte'; import AddPlaces from '$lib/components/AddPlaces.svelte'; import PlaceCard from '$lib/components/PlaceCard.svelte'; + import PastTripsPanel from '$lib/components/PastTripsPanel.svelte'; + import { countryMappings } from '$lib/constants/CountryMappings'; + import { Colors } from '$lib/constants/Colors'; let tripData: any = null; let tripDates: string[] = []; - let places: string[] = []; let tid: string; - const place_placeholder = { name: 'Somewhere'} - const places_placeholder = Array(3).fill(place_placeholder); + let countryCode = 'tw'; // country code to restrict the autocomplete search + + // the place data structure saved in the database + interface Place { + name: string; + desc?: string; + image?: string; + time?: string; + geometry?: { + lat: number; + lng: number; + }; + } + + interface DatePlaces { + placesPlanned: Place[]; + } + + let placesPlanned: Record = {}; + + /** + * Convert date format from DD/MM/YYYY to DD-MM-YYYY + * + * @param date the date to be converted + */ + function convertDateFormat(date: string): string { + return date.replace(/\//g, '-'); + } const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY; let mapContainer: HTMLDivElement; let expandedDates: Record = {}; let map: google.maps.Map | null = null; + let markers: google.maps.marker.AdvancedMarkerElement[] = []; + let showPastTrips = false; + + /** + * Get the ISO 3166-1 alpha-2 country code from /constants/CountryMappings.ts + * + * @param formattedAddress the address of the destination + * @returns the country code of the input + */ + function getCountryCode(formattedAddress: string): string { + // get the country from the last part of formatted address + const parts = formattedAddress.split(','); + const country = parts[parts.length - 1].trim(); + + // check if the mapping is available in /constants/CountryMappings.ts + if (countryMappings[country]) { + return countryMappings[country]; + } + + // if no mapping found, convert to lowercase and take first two letters + // might not always be correct + return country.toLowerCase().slice(0, 2); + } + + function clearMarkers() { + if (markers.length > 0) { + markers.forEach(marker => marker.map = null); + markers = []; + } + } + + async function updateMapMarkers() { + if (!map) return; + + // clear existing markers + clearMarkers(); + + const { AdvancedMarkerElement, PinElement } = await google.maps.importLibrary("marker") as google.maps.MarkerLibrary; + + // add markers for placesToVisit (red color) + placesToVisit.forEach(place => { + if (place.geometry?.lat && place.geometry?.lng) { + const pin = new PinElement({ + background: Colors.memory.med400, + borderColor: Colors.memory.dark700, + glyphColor: Colors.white, + }); + + const marker = new AdvancedMarkerElement({ + map, + position: { lat: place.geometry.lat, lng: place.geometry.lng }, + title: place.name, + content: pin.element + }); + markers.push(marker); + } + }); + + // add markers for placesPlanned (blue color) + Object.values(placesPlanned).forEach(dateData => { + if (!dateData?.placesPlanned) return; + + dateData.placesPlanned.forEach(place => { + if (place.geometry?.lat && place.geometry?.lng) { + const pin = new PinElement({ + background: Colors.planner.med400, + borderColor: Colors.planner.dark700, + glyphColor: Colors.white, + }); + + const marker = new AdvancedMarkerElement({ + map, + position: { lat: place.geometry.lat, lng: place.geometry.lng }, + title: place.name, + content: pin.element + }); + markers.push(marker); + } + }); + }); + } + + // Update markers whenever places change + $: { + if (placesToVisit || placesPlanned) { + updateMapMarkers(); + } + } onMount(async () => { if (!browser) return; @@ -49,13 +165,18 @@ try { const { Map } = await loader.importLibrary("maps"); - // Fetch trip data and initialize map when data is ready + // fetch trip data and initialize map when data is ready const tripRef = ref(db, `trips/${tid}`); onValue(tripRef, (snapshot) => { tripData = snapshot.val(); if (tripData) { - // Generate array of dates between start and end date + // update country code based on destination + if (tripData.destination?.formatted_address) { + countryCode = getCountryCode(tripData.destination.formatted_address); + } + + // generate array of dates between start and end date const start = new Date(tripData.startDate); const end = new Date(tripData.endDate); const dates = []; @@ -64,18 +185,29 @@ } tripDates = dates; - // Initialize expanded states for dates + // initialize expanded states for dates expandedDates = Object.fromEntries(dates.map(date => [date, false])); - // Initialize placesToVisit from database or empty array + // initialize placesToVisit from database or empty array placesToVisit = tripData.placesToVisit || []; + + // initialize placesPlanned from database or empty object + placesPlanned = {}; + if (tripData.itineraryDate) { + // convert keys from DD/MM/YYYY to DD-MM-YYYY if needed + Object.entries(tripData.itineraryDate).forEach(([key, value]) => { + const formattedKey = key.includes('/') ? convertDateFormat(key) : key; + placesPlanned[formattedKey] = value as DatePlaces; + }); + } - // Initialize or update the map + // initialize or update the map if (mapContainer && tripData.destination?.location) { if (!map) { map = new Map(mapContainer, { center: tripData.destination.location, - zoom: 8, + zoom: 10, + mapId: 'ITINERARY_MAP_ID' }); } else { map.setCenter(tripData.destination.location); @@ -127,25 +259,29 @@ } function handlePastTrip() { - console.log(`see past trips to ${tripData?.destination?.name}`) + showPastTrips = !showPastTrips; } async function handlePlaceSelected(place: google.maps.places.PlaceResult) { const newPlace = { name: place.name || 'Unknown Place', desc: place.formatted_address || '', - image: (place as any).photoUrl || '/placeholder.jpeg' + image: (place as any).photoUrl || '/placeholder.jpeg', + geometry: place.geometry?.location ? { + lat: place.geometry.location.lat(), + lng: place.geometry.location.lng() + } : undefined }; const updatedPlaces = [...placesToVisit, newPlace]; try { - // Update the database + // update the database await update(ref(db, `trips/${tid}`), { placesToVisit: updatedPlaces }); - // Update local state + // update local state placesToVisit = updatedPlaces; } catch (error) { console.error('Error adding place:', error); @@ -168,6 +304,26 @@ console.log(`please turn this into itinerary`); } + async function handlePlacePlanned(date: string, places: Place[]) { + const formattedDate = convertDateFormat(date); + try { + // Update the database + await update(ref(db, `trips/${tid}/itineraryDate/${formattedDate}`), { + placesPlanned: places + }); + + // Update local state + placesPlanned = { + ...placesPlanned, + [formattedDate]: { + placesPlanned: places + } + }; + } catch (error) { + console.error('Error updating places planned:', error); + } + } +
@@ -215,7 +371,7 @@
@@ -241,8 +397,11 @@ > {#each tripDates as date} handlePlacePlanned(date, places)} + countryCode={countryCode} /> {/each}
@@ -260,6 +419,11 @@
+ showPastTrips = false} + />
@@ -285,6 +449,8 @@ flex-direction: column; height: 100vh; background-color: #84D7EB; + position: relative; + overflow: hidden; } .map-container { @@ -296,8 +462,8 @@ display: flex; flex-shrink: 0; align-items: center; - gap: 1rem; - padding: 0 2rem 1.5rem 1rem; + gap: 0.75rem; + padding: 0 2rem 1.5rem 0.75rem; border-bottom: 1px solid var(--gray-100); } @@ -311,10 +477,10 @@ border: none; font-size: 1.2rem; cursor: pointer; - padding: 0.5rem; + padding: 0.5rem 0.75rem; color: var(--gray-400); border-radius: 50%; - transition: background-color 0.2s; + transition: all 0.2s ease; } .back-btn:hover { diff --git a/src/routes/trips/+page.svelte b/src/routes/trips/+page.svelte index 4411cfd..acd270e 100644 --- a/src/routes/trips/+page.svelte +++ b/src/routes/trips/+page.svelte @@ -33,10 +33,10 @@ let pastTrips: Trip[] = []; onMount(() => { - // Reference to the trips node + // reference to the trips node const tripsRef = ref(db, 'trips'); - // Listen for changes in the trips data + // listen for changes in the trips data onValue(tripsRef, (snapshot) => { const trips: Trip[] = []; snapshot.forEach((childSnapshot) => { @@ -46,12 +46,12 @@ }); }); - console.log(trips); - // Get today's date at midnight for comparison + // get today's date at midnight for comparison const today = new Date(); today.setHours(0, 0, 0, 0); - // Filter trips based on end date + // filter trips based on end date + // end date > today = pastTrips ongoingTrips = trips.filter(trip => { const endDate = new Date(trip.endDate); return endDate >= today; @@ -60,7 +60,7 @@ pastTrips = trips.filter(trip => { const endDate = new Date(trip.endDate); return endDate < today; - }).sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime()); // Sort past trips by most recent first + }).sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime()); // sort past trips by most recent first }); }); @@ -112,6 +112,7 @@ startDate={new Date(trip.startDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })} endDate={new Date(trip.endDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })} image={trip.destination.photo} + tid={trip.tid} /> {/each} @@ -129,6 +130,7 @@ startDate={new Date(trip.startDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })} endDate={new Date(trip.endDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })} image={trip.destination.photo} + tid={trip.tid} /> {/each}