test
This commit is contained in:
commit
13f93623a5
|
@ -210,4 +210,8 @@
|
|||
:global(.pac-item:hover) {
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
:global(.pac-container:after) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
91
src/lib/components/DeleteConfirmationPopup.svelte
Normal file
91
src/lib/components/DeleteConfirmationPopup.svelte
Normal 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>
|
|
@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -48,7 +70,7 @@
|
|||
<div class="add-places-container">
|
||||
<AddPlaces
|
||||
onPlaceSelected={handlePlaceSelected}
|
||||
countryRestriction="tw"
|
||||
countryRestriction={countryCode}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
258
src/lib/components/PastTripsPanel.svelte
Normal file
258
src/lib/components/PastTripsPanel.svelte
Normal 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>
|
|
@ -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 startDate = '';
|
||||
export let endDate = '';
|
||||
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>
|
||||
|
||||
<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 || ''})">
|
||||
<!-- Image placeholder if no image provided -->
|
||||
{#if !image}
|
||||
|
@ -13,6 +62,16 @@
|
|||
<i class="fa-solid fa-image" style="color: var(--gray-400)"></i>
|
||||
</div>
|
||||
{/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 class="info">
|
||||
<h3>{destination}</h3>
|
||||
|
@ -20,6 +79,13 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<DeleteConfirmationPopup
|
||||
showPopup={showDeleteConfirmation}
|
||||
{destination}
|
||||
onConfirm={handleConfirmDelete}
|
||||
onCancel={handleCancelDelete}
|
||||
/>
|
||||
|
||||
<style>
|
||||
.trip-card {
|
||||
background: var(--white);
|
||||
|
@ -29,6 +95,7 @@
|
|||
transition: transform 0.2s ease, box-shadow 0.2s ease;
|
||||
cursor: pointer;
|
||||
font-family: 'Inter', sans-serif;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.trip-card:hover {
|
||||
|
@ -41,6 +108,7 @@
|
|||
background-size: cover;
|
||||
background-position: center;
|
||||
background-color: var(--gray-100);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
|
@ -66,4 +134,26 @@
|
|||
font-size: 0.8rem;
|
||||
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>
|
250
src/lib/constants/CountryMappings.ts
Normal file
250
src/lib/constants/CountryMappings.ts
Normal 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'
|
||||
}
|
|
@ -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>
|
|
@ -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<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;
|
||||
let mapContainer: HTMLDivElement;
|
||||
let expandedDates: Record<string, boolean> = {};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
|
||||
<main>
|
||||
|
@ -215,7 +371,7 @@
|
|||
|
||||
<AddPlaces
|
||||
onPlaceSelected={handlePlaceSelected}
|
||||
countryRestriction="tw"
|
||||
countryRestriction={countryCode}
|
||||
/>
|
||||
|
||||
<div class="places-buttons">
|
||||
|
@ -241,8 +397,11 @@
|
|||
>
|
||||
{#each tripDates as date}
|
||||
<ItineraryDate
|
||||
{date}
|
||||
date={date}
|
||||
isExpanded={expandedDates[date]}
|
||||
places={placesPlanned[convertDateFormat(date)]?.placesPlanned || []}
|
||||
onPlacesUpdate={(places) => handlePlacePlanned(date, places)}
|
||||
countryCode={countryCode}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
@ -260,6 +419,11 @@
|
|||
<div class="map-section">
|
||||
<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} />
|
||||
<PastTripsPanel
|
||||
showPanel={showPastTrips}
|
||||
destination={tripData?.destination?.name || ''}
|
||||
onClose={() => showPastTrips = false}
|
||||
/>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
@ -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 {
|
||||
|
|
|
@ -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}
|
||||
</div>
|
||||
|
@ -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}
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user