This commit is contained in:
Chaebean Yang 2025-06-06 19:26:52 +09:00
commit 13f93623a5
9 changed files with 915 additions and 415 deletions

View File

@ -210,4 +210,8 @@
:global(.pac-item:hover) { :global(.pac-item:hover) {
background-color: var(--gray-50); background-color: var(--gray-50);
} }
:global(.pac-container:after) {
display: none;
}
</style> </style>

View File

@ -0,0 +1,91 @@
<script lang="ts">
export let showPopup = false;
export let destination = '';
export let onConfirm: () => void;
export let onCancel: () => void;
function handlePopupClick(event: MouseEvent) {
event.stopPropagation();
}
</script>
{#if showPopup}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="overlay" role="button" tabindex="0">
<div class="popup" role="button" tabindex="0" onclick={handlePopupClick}>
<h2>Delete Trip</h2>
<p>Are you sure you want to delete Trip to {destination}?</p>
<div class="button-group">
<button class="cancel-btn" onclick={onCancel}>Cancel</button>
<button class="delete-btn" onclick={onConfirm}>Yes, delete</button>
</div>
</div>
</div>
{/if}
<style>
.overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.3);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
.popup {
background: var(--white);
padding: 2rem;
border-radius: 20px;
width: 90%;
max-width: 400px;
box-shadow: 0 0px 20px rgba(0, 0, 0, 0.1);
}
.popup h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
color: var(--memory-600);
text-align: center;
margin-bottom: 1.5rem;
}
.popup p {
margin: 1rem 0 2rem 0;
color: var(--gray-600);
text-align: center;
}
.button-group {
display: flex;
gap: 1rem;
justify-content: center;
}
button {
padding: 0.75rem 1.5rem;
border-radius: 20px;
font-size: 0.9rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
background: var(--gray-100);
border: none;
color: var(--gray-600);
width: 50%;
}
.cancel-btn:hover {
opacity: 0.75;
}
.delete-btn:hover {
background: var(--memory-400);
color: white;
}
</style>

View File

@ -6,7 +6,21 @@
export let date; export let date;
export let isExpanded = true; 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() { function toggleDate() {
isExpanded = !isExpanded; isExpanded = !isExpanded;
@ -17,14 +31,22 @@
name: place.name || 'Unknown Place', name: place.name || 'Unknown Place',
desc: place.formatted_address || '', desc: place.formatted_address || '',
image: (place as any).photoUrl || '/placeholder.jpeg', 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) { function handleDeletePlace(index: number) {
places = places.filter((_, i) => i !== index); const updatedPlaces = places.filter((_, i) => i !== index);
places = updatedPlaces;
onPlacesUpdate(updatedPlaces);
} }
</script> </script>
@ -48,7 +70,7 @@
<div class="add-places-container"> <div class="add-places-container">
<AddPlaces <AddPlaces
onPlaceSelected={handlePlaceSelected} onPlaceSelected={handlePlaceSelected}
countryRestriction="tw" countryRestriction={countryCode}
/> />
</div> </div>
</div> </div>

View File

@ -0,0 +1,258 @@
<script lang="ts">
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import TripCard from './TripCard.svelte';
import { ref, onValue } from 'firebase/database';
import { db } from '../../firebase';
export let showPanel = false;
export let destination = '';
export let onClose = () => {};
let pastTrips: any[] = [];
let loading = true;
let tripsContainer: HTMLElement;
let showLeftButton = false;
let showRightButton = true;
function handleScroll() {
if (!tripsContainer) return;
// Show/hide left button based on scroll position
showLeftButton = tripsContainer.scrollLeft > 0;
// Show/hide right button based on whether we can scroll more
const maxScroll = tripsContainer.scrollWidth - tripsContainer.clientWidth;
showRightButton = Math.ceil(tripsContainer.scrollLeft) < maxScroll;
}
function scrollLeft() {
if (!tripsContainer) return;
const scrollAmount = tripsContainer.clientWidth * 0.8; // Scroll 80% of container width
tripsContainer.scrollBy({
left: -scrollAmount,
behavior: 'smooth'
});
}
function scrollRight() {
if (!tripsContainer) return;
const scrollAmount = tripsContainer.clientWidth * 0.8; // Scroll 80% of container width
tripsContainer.scrollBy({
left: scrollAmount,
behavior: 'smooth'
});
}
$: if (showPanel && destination) {
// Fetch past trips for this destination
const tripsRef = ref(db, 'trips');
onValue(tripsRef, (snapshot) => {
const allTrips: any[] = [];
snapshot.forEach((childSnapshot) => {
const trip = {
tid: childSnapshot.key,
...childSnapshot.val()
};
allTrips.push(trip);
});
// Get today's date at midnight for comparison
const today = new Date();
today.setHours(0, 0, 0, 0);
// Filter past trips for this destination
pastTrips = allTrips
.filter(trip => {
const endDate = new Date(trip.endDate);
return endDate < today && trip.destination.name === destination;
})
.sort((a, b) => new Date(b.endDate).getTime() - new Date(a.endDate).getTime()); // Most recent first
loading = false;
});
}
</script>
{#if showPanel}
<div
class="panel"
transition:slide={{ duration: 400, easing: quintOut }}
>
<div class="header">
<h2>Past Trips to {destination}</h2>
<button class="close-btn" onclick={onClose} aria-label="Close panel">
<i class="fa-solid fa-xmark"></i>
</button>
</div>
<div class="content">
{#if loading}
<div class="message">Loading...</div>
{:else if pastTrips.length === 0}
<div class="message">This is your first trip to {destination}!</div>
{:else}
<div class="trips-scroll-container">
{#if showLeftButton}
<button
class="scroll-btn left"
onclick={scrollLeft}
aria-label="Scroll left"
transition:slide={{ duration: 200, easing: quintOut }}
>
<i class="fa-solid fa-chevron-left"></i>
</button>
{/if}
<div
class="trips-container"
bind:this={tripsContainer}
onscroll={handleScroll}
>
{#each pastTrips as trip}
<TripCard
destination={trip.destination.name}
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}
</div>
{#if showRightButton}
<button
class="scroll-btn right"
onclick={scrollRight}
aria-label="Scroll right"
transition:slide={{ duration: 200, easing: quintOut }}
>
<i class="fa-solid fa-chevron-right"></i>
</button>
{/if}
</div>
{/if}
</div>
</div>
{/if}
<style>
.panel {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50vh;
background: var(--white);
border-radius: 20px 20px 0 0;
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
z-index: 10;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 2rem;
border-bottom: 1px solid var(--gray-100);
}
.header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 600;
}
.close-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
color: var(--gray-400);
border-radius: 50%;
transition: all 0.2s ease;
width: 2.5rem;
height: 2.5rem;
display: flex;
align-items: center;
justify-content: center;
}
.close-btn:hover {
background-color: var(--gray-100);
}
.content {
flex: 1;
padding: 1.5rem 2rem;
overflow: hidden;
}
.message {
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-400);
font-size: 1.1rem;
}
.trips-scroll-container {
position: relative;
height: 100%;
}
.trips-container {
display: flex;
gap: 1.5rem;
overflow-x: auto;
padding-bottom: 1rem;
scroll-behavior: smooth;
scrollbar-width: none;
-ms-overflow-style: none;
}
.trips-container::-webkit-scrollbar {
display: none;
}
.trips-container :global(.trip-card) {
min-width: 280px;
width: 280px;
}
.scroll-btn {
position: absolute;
top: 50%;
transform: translateY(-50%);
background: var(--white);
border: none;
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
color: var(--gray-600);
transition: all 0.2s ease;
z-index: 1;
}
.scroll-btn:hover {
background-color: var(--gray-50);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.scroll-btn.left {
left: -1.25rem;
}
.scroll-btn.right {
right: -1.25rem;
}
</style>

View File

@ -1,11 +1,60 @@
<script> <script lang="ts">
import { goto } from '$app/navigation';
import { fade } from 'svelte/transition';
import DeleteConfirmationPopup from './DeleteConfirmationPopup.svelte';
import { ref, remove } from 'firebase/database';
import { db } from '../../firebase';
export let destination = ''; export let destination = '';
export let startDate = ''; export let startDate = '';
export let endDate = ''; export let endDate = '';
export let image = ''; export let image = '';
export let tid = '';
let showDelete = false;
let showDeleteConfirmation = false;
function handleClick() {
goto(`/itinerary/${tid}`);
}
function handleMouseEnter() {
showDelete = true;
}
function handleMouseLeave() {
showDelete = false;
}
function handleDeleteClick(event: MouseEvent) {
event.stopPropagation();
showDeleteConfirmation = true;
}
async function handleConfirmDelete() {
try {
const tripRef = ref(db, `trips/${tid}`);
await remove(tripRef);
showDeleteConfirmation = false;
} catch (error) {
console.error('Error deleting trip:', error);
}
}
function handleCancelDelete() {
showDeleteConfirmation = false;
}
</script> </script>
<div class="trip-card"> <!-- svelte-ignore a11y_click_events_have_key_events -->
<div
class="trip-card"
role="button"
tabindex="0"
onmouseenter={handleMouseEnter}
onmouseleave={handleMouseLeave}
onclick={handleClick}
>
<div class="image" style="background-image: url({image || ''})"> <div class="image" style="background-image: url({image || ''})">
<!-- Image placeholder if no image provided --> <!-- Image placeholder if no image provided -->
{#if !image} {#if !image}
@ -13,6 +62,16 @@
<i class="fa-solid fa-image" style="color: var(--gray-400)"></i> <i class="fa-solid fa-image" style="color: var(--gray-400)"></i>
</div> </div>
{/if} {/if}
{#if showDelete}
<button
class="delete-btn"
onclick={handleDeleteClick}
transition:fade={{ duration: 100 }}
aria-label="Delete trip"
>
<i class="fa-solid fa-xmark"></i>
</button>
{/if}
</div> </div>
<div class="info"> <div class="info">
<h3>{destination}</h3> <h3>{destination}</h3>
@ -20,6 +79,13 @@
</div> </div>
</div> </div>
<DeleteConfirmationPopup
showPopup={showDeleteConfirmation}
{destination}
onConfirm={handleConfirmDelete}
onCancel={handleCancelDelete}
/>
<style> <style>
.trip-card { .trip-card {
background: var(--white); background: var(--white);
@ -29,6 +95,7 @@
transition: transform 0.2s ease, box-shadow 0.2s ease; transition: transform 0.2s ease, box-shadow 0.2s ease;
cursor: pointer; cursor: pointer;
font-family: 'Inter', sans-serif; font-family: 'Inter', sans-serif;
position: relative;
} }
.trip-card:hover { .trip-card:hover {
@ -41,6 +108,7 @@
background-size: cover; background-size: cover;
background-position: center; background-position: center;
background-color: var(--gray-100); background-color: var(--gray-100);
position: relative;
} }
.placeholder { .placeholder {
@ -66,4 +134,26 @@
font-size: 0.8rem; font-size: 0.8rem;
color: var(--gray-400); color: var(--gray-400);
} }
.delete-btn {
position: absolute;
top: 0.75rem;
right: 0.75rem;
background: rgba(240, 240, 240, 0.8);
border: none;
width: 2rem;
height: 2rem;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: var(--gray-600);
transition: all 0.2s ease;
}
.delete-btn:hover {
background-color: var(--memory-50);
color: var(--memory-600);
}
</style> </style>

View File

@ -0,0 +1,250 @@
export const countryMappings: Record<string, string> = {
'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'
}

View File

@ -1,383 +0,0 @@
<script lang="ts">
import '../../app.css';
import { goto } from '$app/navigation';
import { slide } from 'svelte/transition';
import { quintOut } from 'svelte/easing';
import { page } from '$app/state';
import { onMount } from 'svelte';
import { Loader } from '@googlemaps/js-api-loader';
import { browser } from '$app/environment';
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
import BottomBar from '$lib/components/BottomBar.svelte';
import Button from '$lib/components/Button.svelte';
import ItineraryDate from '$lib/components/ItineraryDate.svelte';
import AddPlaces from '$lib/components/AddPlaces.svelte';
import PlaceCard from '$lib/components/PlaceCard.svelte';
// Placeholder data obtained from the popup
let destination = "Taiwan";
let desc = `Click to view all past trips to ${destination}`;
let startDate = "27/04/2025";
let endDate = "30/04/2025";
let places: string[] = [];
const place_placeholder = { name: 'Somewhere'}
const places_placeholder = Array(3).fill(place_placeholder);
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
let mapContainer: HTMLDivElement;
onMount(async () => {
if (!browser) return;
if (!GOOGLE_PLACES_API_KEY) {
console.error('Google Places API key is missing');
return;
}
const loader = new Loader({
apiKey: GOOGLE_PLACES_API_KEY,
version: "weekly",
libraries: ["places"],
language: 'en'
});
try {
await loader.importLibrary("maps");
const map = new google.maps.Map(mapContainer, {
center: { lat: 23.5, lng: 121 }, // Taiwan's coordinates
zoom: 8,
});
} catch (error) {
console.error('Error loading Google Maps:', error);
}
});
// Array of dates between startDate to endDate
// TODO: implement generateTripDates(startDate, endDate)
let tripDates = ["27/04/2025", "28/04/2025", "29/04/2025", "30/04/2025"];
let expandedSections = {
explore: true,
places_to_visit: true,
itinerary: true
};
let expandedDates: Record<string, boolean> = {};
tripDates.forEach(date => expandedDates[date] = false);
let recommendedPlaces = [
{ name: "Place name" },
{ name: "Place name" },
{ name: "Place name" }
];
let placesToVisit = [
{ name: "Place name"},
{ name: "Place name"},
{ name: "Place name"}
];
function handleDeletePlace(index: number) {
placesToVisit = placesToVisit.filter((_, i) => i !== index);
}
function toggleSection(section: keyof typeof expandedSections) {
expandedSections[section] = !expandedSections[section];
}
function handleBack() {
// Get the 'from' parameter from the URL
const fromPage = page.url.searchParams.get('from');
console.log(`fromPage = ${fromPage}`);
if (fromPage === 'trips') {
goto('/trips');
} else {
goto('/');
}
}
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'
};
placesToVisit = [...placesToVisit, newPlace];
}
function handlePastTrip() {
console.log(`see past trips to ${destination}`)
}
function handleCancel() {
console.log('cancel update');
}
function handleSave() {
console.log('save update');
}
function handleRecommendPlaces() {
console.log(`will give recommendation using OpenAI`);
}
function handleTurnIntoItinerary() {
console.log(`please turn this into itinerary`);
}
function showPastTrips() {
// TODO: Implement past trips view
console.log('Show past trips');
}
</script>
<main>
<div class="plan-section">
<header>
<div class="back-btn-wrapper">
<button class="back-btn" onclick={handleBack} aria-label="Back">
<i class="fa-solid fa-chevron-left"></i>
</button>
</div>
<div class="trip-info">
<h1>Trip to {destination}</h1>
<p class="date">{startDate} - {endDate}</p>
</div>
<div class="tripmates">
<ProfilePicture friends={2} />
</div>
</header>
<div class="content">
<section class="places-section">
<button class="section-header" onclick={() => toggleSection('places_to_visit')}>
<div class="section-text">
<i class="fa-solid fa-chevron-right arrow-icon" class:rotated={expandedSections.places_to_visit}></i>
<h2>Places to Visit</h2>
</div>
</button>
<!-- TODO: implement the content part -->
{#if expandedSections.places_to_visit}
<div
class="section-content places"
transition:slide={{ duration: 400, easing: quintOut }}
>
<div class="added-places">
{#each placesToVisit as place, i}
<PlaceCard
variant="simple"
place={place}
onDelete={() => handleDeletePlace(i)}
/>
{/each}
</div>
<AddPlaces
onPlaceSelected={handlePlaceSelected}
countryRestriction="tw"
/>
<div class="places-buttons">
<Button text="Recommend Places" type="blue" onClick={handleRecommendPlaces} />
<Button text="Turn into Itinerary" type="blue" onClick={handleTurnIntoItinerary} />
</div>
</div>
{/if}
</section>
<section class="itinerary-section">
<button class="section-header" onclick={() => toggleSection('itinerary')}>
<div class="section-text">
<i class="fa-solid fa-chevron-right arrow-icon" class:rotated={expandedSections.itinerary}></i>
<h2>Itinerary</h2>
</div>
</button>
{#if expandedSections.itinerary}
<div
class="section-content"
transition:slide={{ duration: 400, easing: quintOut }}
>
{#each tripDates as date}
<ItineraryDate
{date}
isExpanded={expandedDates[date]}
/>
{/each}
</div>
{/if}
</section>
<div class="button-group">
<Button text="Cancel" type="gray" onClick={handleCancel}/>
<!-- later edit this so button turns blue only when there is changes to the plan -->
<Button text="Save" type="blue" onClick={handleSave} />
</div>
</div>
</div>
<div class="map-section">
<div class="map-container" bind:this={mapContainer}></div>
<BottomBar title="Past Trips" desc={desc} onClick={handlePastTrip} />
</div>
</main>
<style>
main {
height: 100vh;
display: grid;
grid-template-columns: 50% 50%;
font-family: 'Inter', sans-serif;
}
.plan-section {
height: 100vh;
box-sizing: border-box;
padding: 0.5rem 0rem 0rem 0rem;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.map-section {
display: flex;
flex-direction: column;
height: 100vh;
background-color: #84D7EB;
}
.map-container {
flex: 1;
position: relative;
}
header {
display: flex;
flex-shrink: 0;
align-items: center;
gap: 1rem;
padding: 0 2rem 1.5rem 1rem;
border-bottom: 1px solid var(--gray-100);
}
.back-btn-wrapper {
align-self: flex-start;
margin-top: 1.75rem;
}
.back-btn {
background: none;
border: none;
font-size: 1.2rem;
cursor: pointer;
padding: 0.5rem;
color: var(--gray-400);
border-radius: 50%;
transition: background-color 0.2s;
}
.back-btn:hover {
background-color: var(--gray-100);
}
.trip-info {
flex: 1;
margin-bottom: 0.5rem;
}
.trip-info h1 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 0;
}
.date {
color: var(--gray-400);
margin-top: 0.5rem;
margin-bottom: 0;
font-size: 0.9rem;
}
.tripmates {
margin-top: 1rem;
display: flex;
align-items: center;
}
.content {
padding: 1rem 1.5rem 0 1.5rem;
flex: 1;
display: flex;
flex-direction: column;
overflow-y: auto;
}
.section-header {
width: 100%;
background: none;
border: none;
border-bottom: 1px solid var(--gray-100);
box-sizing: border-box;
padding: 0.75rem 0.5rem;
cursor: pointer;
text-align: left;
}
.section-text {
display: flex;
align-items: center;
gap: 1rem;
margin: 0;
font-size: 1rem;
font-family: 'Inter', sans-serif;
}
.arrow-icon {
transition: transform 0.3s ease;
transform-origin: center;
}
.rotated {
transform: rotate(90deg);
}
.section-text h2 {
margin: 0;
font-size: 1.2rem;
font-weight: 600;
}
.section-content {
padding-left: 1.5rem;
padding-top: 1rem;
display: flex;
flex-direction: column;
margin-bottom: 1rem;
gap: 0.5rem;
}
.section-content.places{
margin-left: 0.5rem;
}
.places-buttons {
margin-top: 0.5rem;
display: flex;
gap: 0.5rem;
}
.button-group {
position: sticky;
flex-shrink: 0;
background-color: var(--white);
padding: 1.5rem 0;
bottom: 0;
display: flex;
gap: 1rem;
margin-top: auto;
}
</style>

View File

@ -15,18 +15,134 @@
import ItineraryDate from '$lib/components/ItineraryDate.svelte'; import ItineraryDate from '$lib/components/ItineraryDate.svelte';
import AddPlaces from '$lib/components/AddPlaces.svelte'; import AddPlaces from '$lib/components/AddPlaces.svelte';
import PlaceCard from '$lib/components/PlaceCard.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 tripData: any = null;
let tripDates: string[] = []; let tripDates: string[] = [];
let places: string[] = [];
let tid: string; let tid: string;
const place_placeholder = { name: 'Somewhere'} let countryCode = 'tw'; // country code to restrict the autocomplete search
const places_placeholder = Array(3).fill(place_placeholder);
// 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<string, DatePlaces> = {};
/**
* 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; const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
let mapContainer: HTMLDivElement; let mapContainer: HTMLDivElement;
let expandedDates: Record<string, boolean> = {}; let expandedDates: Record<string, boolean> = {};
let map: google.maps.Map | null = null; 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 () => { onMount(async () => {
if (!browser) return; if (!browser) return;
@ -49,13 +165,18 @@
try { try {
const { Map } = await loader.importLibrary("maps"); 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}`); const tripRef = ref(db, `trips/${tid}`);
onValue(tripRef, (snapshot) => { onValue(tripRef, (snapshot) => {
tripData = snapshot.val(); tripData = snapshot.val();
if (tripData) { 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 start = new Date(tripData.startDate);
const end = new Date(tripData.endDate); const end = new Date(tripData.endDate);
const dates = []; const dates = [];
@ -64,18 +185,29 @@
} }
tripDates = dates; tripDates = dates;
// Initialize expanded states for dates // initialize expanded states for dates
expandedDates = Object.fromEntries(dates.map(date => [date, false])); 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 || []; 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 (mapContainer && tripData.destination?.location) {
if (!map) { if (!map) {
map = new Map(mapContainer, { map = new Map(mapContainer, {
center: tripData.destination.location, center: tripData.destination.location,
zoom: 8, zoom: 10,
mapId: 'ITINERARY_MAP_ID'
}); });
} else { } else {
map.setCenter(tripData.destination.location); map.setCenter(tripData.destination.location);
@ -127,25 +259,29 @@
} }
function handlePastTrip() { function handlePastTrip() {
console.log(`see past trips to ${tripData?.destination?.name}`) showPastTrips = !showPastTrips;
} }
async function handlePlaceSelected(place: google.maps.places.PlaceResult) { async function handlePlaceSelected(place: google.maps.places.PlaceResult) {
const newPlace = { const newPlace = {
name: place.name || 'Unknown Place', name: place.name || 'Unknown Place',
desc: place.formatted_address || '', 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]; const updatedPlaces = [...placesToVisit, newPlace];
try { try {
// Update the database // update the database
await update(ref(db, `trips/${tid}`), { await update(ref(db, `trips/${tid}`), {
placesToVisit: updatedPlaces placesToVisit: updatedPlaces
}); });
// Update local state // update local state
placesToVisit = updatedPlaces; placesToVisit = updatedPlaces;
} catch (error) { } catch (error) {
console.error('Error adding place:', error); console.error('Error adding place:', error);
@ -168,6 +304,26 @@
console.log(`please turn this into itinerary`); 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);
}
}
</script> </script>
<main> <main>
@ -215,7 +371,7 @@
<AddPlaces <AddPlaces
onPlaceSelected={handlePlaceSelected} onPlaceSelected={handlePlaceSelected}
countryRestriction="tw" countryRestriction={countryCode}
/> />
<div class="places-buttons"> <div class="places-buttons">
@ -241,8 +397,11 @@
> >
{#each tripDates as date} {#each tripDates as date}
<ItineraryDate <ItineraryDate
{date} date={date}
isExpanded={expandedDates[date]} isExpanded={expandedDates[date]}
places={placesPlanned[convertDateFormat(date)]?.placesPlanned || []}
onPlacesUpdate={(places) => handlePlacePlanned(date, places)}
countryCode={countryCode}
/> />
{/each} {/each}
</div> </div>
@ -260,6 +419,11 @@
<div class="map-section"> <div class="map-section">
<div class="map-container" bind:this={mapContainer}></div> <div class="map-container" bind:this={mapContainer}></div>
<BottomBar title="Past Trips" desc="Click to view all past trips to {tripData?.destination?.name}" onClick={handlePastTrip} /> <BottomBar title="Past Trips" desc="Click to view all past trips to {tripData?.destination?.name}" onClick={handlePastTrip} />
<PastTripsPanel
showPanel={showPastTrips}
destination={tripData?.destination?.name || ''}
onClose={() => showPastTrips = false}
/>
</div> </div>
</main> </main>
@ -285,6 +449,8 @@
flex-direction: column; flex-direction: column;
height: 100vh; height: 100vh;
background-color: #84D7EB; background-color: #84D7EB;
position: relative;
overflow: hidden;
} }
.map-container { .map-container {
@ -296,8 +462,8 @@
display: flex; display: flex;
flex-shrink: 0; flex-shrink: 0;
align-items: center; align-items: center;
gap: 1rem; gap: 0.75rem;
padding: 0 2rem 1.5rem 1rem; padding: 0 2rem 1.5rem 0.75rem;
border-bottom: 1px solid var(--gray-100); border-bottom: 1px solid var(--gray-100);
} }
@ -311,10 +477,10 @@
border: none; border: none;
font-size: 1.2rem; font-size: 1.2rem;
cursor: pointer; cursor: pointer;
padding: 0.5rem; padding: 0.5rem 0.75rem;
color: var(--gray-400); color: var(--gray-400);
border-radius: 50%; border-radius: 50%;
transition: background-color 0.2s; transition: all 0.2s ease;
} }
.back-btn:hover { .back-btn:hover {

View File

@ -33,10 +33,10 @@
let pastTrips: Trip[] = []; let pastTrips: Trip[] = [];
onMount(() => { onMount(() => {
// Reference to the trips node // reference to the trips node
const tripsRef = ref(db, 'trips'); const tripsRef = ref(db, 'trips');
// Listen for changes in the trips data // listen for changes in the trips data
onValue(tripsRef, (snapshot) => { onValue(tripsRef, (snapshot) => {
const trips: Trip[] = []; const trips: Trip[] = [];
snapshot.forEach((childSnapshot) => { 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(); const today = new Date();
today.setHours(0, 0, 0, 0); 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 => { ongoingTrips = trips.filter(trip => {
const endDate = new Date(trip.endDate); const endDate = new Date(trip.endDate);
return endDate >= today; return endDate >= today;
@ -60,7 +60,7 @@
pastTrips = trips.filter(trip => { pastTrips = trips.filter(trip => {
const endDate = new Date(trip.endDate); const endDate = new Date(trip.endDate);
return endDate < today; 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' })} 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' })} endDate={new Date(trip.endDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
image={trip.destination.photo} image={trip.destination.photo}
tid={trip.tid}
/> />
{/each} {/each}
</div> </div>
@ -129,6 +130,7 @@
startDate={new Date(trip.startDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })} 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' })} endDate={new Date(trip.endDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' })}
image={trip.destination.photo} image={trip.destination.photo}
tid={trip.tid}
/> />
{/each} {/each}
</div> </div>