connect to database

This commit is contained in:
adeliptr 2025-06-06 00:25:21 +09:00
parent 143ac1d2ea
commit 9def1973a7
10 changed files with 595 additions and 71 deletions

View File

@ -63,15 +63,19 @@
if (place && place.name) { if (place && place.name) {
// If the place has photos, get the URL for the first photo // If the place has photos, get the URL for the first photo
if (place.photos && place.photos.length > 0) { if (place.photos && place.photos.length > 0) {
try {
const photoOptions = { const photoOptions = {
maxWidth: 400, maxWidth: 400,
maxHeight: 300 maxHeight: 300
}; };
// Get the photo URL place.photoUrl = place.photos[0].getUrl(photoOptions);
console.log(place.photos[0]); } catch (error) {
const photoUrl = place.photos[0].getUrl(photoOptions); console.error('Error getting photo URL:', error);
place.photoUrl = photoUrl; place.photoUrl = '/placeholder.jpeg';
console.log(place.photoUrl); }
}
else {
place.photoUrl = '/placeholder.jpeg';
} }
selectedPlace = place; selectedPlace = place;

View File

@ -16,7 +16,7 @@
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',
time: 'Add Time' time: 'Add Time'
}; };

View File

@ -3,16 +3,18 @@
import { goto } from '$app/navigation'; import { goto } from '$app/navigation';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { Loader } from '@googlemaps/js-api-loader'; import { Loader } from '@googlemaps/js-api-loader';
import { ref, child, get, set, onValue, push } from 'firebase/database';
import { db } from '../../firebase';
import Button from './Button.svelte'; import Button from './Button.svelte';
export let showPopup = false; export let showPopup = false;
export let fromPage = 'home';
let destination = ""; let destination = "";
let selectedPlace: any;
let lastSelectedPlaceName = ""; let lastSelectedPlaceName = "";
let startDate = ""; let startDate = "";
let endDate = ""; let endDate = "";
let friends: string[] = []; let tripmates: string[] = [];
let currentEmail = ""; let currentEmail = "";
let destinationError = false; let destinationError = false;
let startDateError = false; let startDateError = false;
@ -74,13 +76,14 @@
autocomplete = new google.maps.places.Autocomplete(input, { autocomplete = new google.maps.places.Autocomplete(input, {
types: ['(regions)'] types: ['(regions)']
}); });
autocomplete.setFields(['name', 'formatted_address']); autocomplete.setFields(['name', 'formatted_address', 'photos', 'place_id', 'geometry']);
autocomplete.addListener('place_changed', () => { autocomplete.addListener('place_changed', () => {
if (!autocomplete) return; if (!autocomplete) return;
const place = autocomplete.getPlace(); const place = autocomplete.getPlace();
if (place.name) { if (place.name) {
destination = place.name; destination = place.name;
selectedPlace = place;
lastSelectedPlaceName = input.value.trim(); lastSelectedPlaceName = input.value.trim();
destinationError = false; destinationError = false;
} }
@ -100,7 +103,7 @@
destinationError = false; destinationError = false;
destination = ""; destination = "";
} }
}, 200); }, 400);
}); });
} }
@ -114,15 +117,15 @@
event.preventDefault(); event.preventDefault();
const email = currentEmail.trim(); const email = currentEmail.trim();
if (email && isValidEmail(email) && !friends.includes(email)) { if (email && isValidEmail(email) && !tripmates.includes(email)) {
friends = [...friends, email]; tripmates = [...tripmates, email];
currentEmail = ""; currentEmail = "";
} }
} }
} }
function removeEmail(emailToRemove: string) { function removeEmail(emailToRemove: string) {
friends = friends.filter(email => email !== emailToRemove); tripmates = tripmates.filter(email => email !== emailToRemove);
} }
function handleCancel() { function handleCancel() {
@ -130,7 +133,7 @@
destination = ""; destination = "";
startDate = ""; startDate = "";
endDate = ""; endDate = "";
friends = []; tripmates = [];
currentEmail = ""; currentEmail = "";
destinationError = false; destinationError = false;
startDateError = false; startDateError = false;
@ -142,7 +145,7 @@
} }
} }
function handleStart() { async function handleStart() {
destinationError = !destination; destinationError = !destination;
startDateError = !startDate; startDateError = !startDate;
endDateError = !endDate; endDateError = !endDate;
@ -162,12 +165,41 @@
} }
if (destinationError || startDateError || endDateError) { if (destinationError || startDateError || endDateError) {
// alert('Please fill in all required fields: Destination, Start Date, End Date');
return; return;
} }
else { else {
goto(`/itinerary?from=${fromPage}`); const tid = crypto.randomUUID();
// Extract required place details
const placeDetails = {
name: selectedPlace.name,
formatted_address: selectedPlace.formatted_address,
photo: selectedPlace.photos?.[0]?.getUrl(),
location: {
lat: selectedPlace.geometry.location.lat(),
lng: selectedPlace.geometry.location.lng()
}
};
const tripData = {
tid,
destination: placeDetails,
startDate,
endDate,
tripmates,
created_at: new Date().toISOString()
};
try {
// Create a new reference for this specific trip using its ID
const tripRef = ref(db, `trips/${tid}`);
await set(tripRef, tripData);
console.log(`Trip saved to db with ID: ${tid}`);
goto(`/itinerary/${tid}`);
handleCancel(); handleCancel();
} catch (error) {
console.error('Error saving trip:', error);
}
} }
} }
@ -182,7 +214,7 @@
<h1>Start a New Plan</h1> <h1>Start a New Plan</h1>
<div class="input-form"> <div class="input-form">
<label for="destination-input">Destination</label> <label for="destination">Destination</label>
<div bind:this={destinationInput} class="destination-wrapper" id="destination"></div> <div bind:this={destinationInput} class="destination-wrapper" id="destination"></div>
{#if destinationError} {#if destinationError}
<p class="error-message">Please enter your destination</p> <p class="error-message">Please enter your destination</p>
@ -229,7 +261,7 @@
</span> </span>
</label> </label>
<div class="email-input-container"> <div class="email-input-container">
{#each friends as email} {#each tripmates as email}
<div class="email-tag"> <div class="email-tag">
<span>{email}</span> <span>{email}</span>
<button class="remove-email" onclick={() => removeEmail(email)}>×</button> <button class="remove-email" onclick={() => removeEmail(email)}>×</button>
@ -240,7 +272,7 @@
id="trip-friends" id="trip-friends"
bind:value={currentEmail} bind:value={currentEmail}
onkeydown={handleEmailInput} onkeydown={handleEmailInput}
placeholder={friends.length ? "" : "Enter email addresses"} placeholder={tripmates.length ? "" : "Enter email addresses"}
/> />
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
const defaultPlace: Omit<Place, 'desc'> = { const defaultPlace: Omit<Place, 'desc'> = {
name: 'PlaceName', name: 'PlaceName',
image: 'placeholder.jpeg', image: '/placeholder.jpeg',
time: 'Add Time' time: 'Add Time'
}; };

View File

@ -1,26 +1,39 @@
<script> <script lang="ts">
export let image = ''; export let friends = 1;
export let marginLeft = '0px'; export let images: string[] = [];
export let zIndex = '0';
</script> </script>
<div <div class="profile-pictures">
{#each Array(friends) as _, i}
<div
class="profile-picture" class="profile-picture"
style="margin-left: {marginLeft}; z-index: {zIndex}" style="z-index: {friends - i}; margin-left: {i === 0 ? '0' : '-20px'}"
> >
{#if image} {#if images[i]}
<img class="profile-img" src={image} alt="" /> <img class="profile-img" src={images[i]} alt="Profile" />
{:else} {:else}
<img class="profile-img" src='profile-pic.png' alt="" /> <div class="default-avatar">
<i class="fa-solid fa-user"></i>
</div>
{/if} {/if}
</div>
{/each}
</div> </div>
<style> <style>
.profile-pictures {
display: flex;
align-items: center;
}
.profile-picture { .profile-picture {
width: 2.5rem; width: 2.5rem;
height: 2.5rem; height: 2.5rem;
border-radius: 50%; border-radius: 50%;
border: 2px solid white; border: 2px solid white;
background-color: var(--gray-100);
overflow: hidden;
flex-shrink: 0;
} }
.profile-img { .profile-img {
@ -29,4 +42,14 @@
border-radius: 50%; border-radius: 50%;
object-fit: cover; object-fit: cover;
} }
.default-avatar {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: var(--gray-400);
font-size: 1.2rem;
}
</style> </style>

View File

@ -6,7 +6,7 @@
</script> </script>
<div class="trip-card"> <div class="trip-card">
<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}
<div class="placeholder"> <div class="placeholder">

View File

@ -29,7 +29,7 @@
<BottomBar onClick={() => goto('/trips')} /> <BottomBar onClick={() => goto('/trips')} />
<NewTripPopup bind:showPopup={showNewTripPopup} fromPage="home" /> <NewTripPopup bind:showPopup={showNewTripPopup} />
</main> </main>
<style> <style>

View File

@ -147,8 +147,7 @@
</div> </div>
<div class="tripmates"> <div class="tripmates">
<ProfilePicture zIndex=1/> <ProfilePicture friends={2} />
<ProfilePicture marginLeft=-15px zIndex=0/>
</div> </div>
</header> </header>

View File

@ -0,0 +1,420 @@
<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 { ref, onValue, update } from 'firebase/database';
import { db } from '../../../firebase';
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';
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);
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;
onMount(async () => {
if (!browser) return;
if (!GOOGLE_PLACES_API_KEY) {
console.error('Google Places API key is missing');
return;
}
// Get the trip ID from the URL
tid = page.params.tid;
const loader = new Loader({
apiKey: GOOGLE_PLACES_API_KEY,
version: "weekly",
libraries: ["places"],
language: 'en'
});
try {
const { Map } = await loader.importLibrary("maps");
// 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
const start = new Date(tripData.startDate);
const end = new Date(tripData.endDate);
const dates = [];
for (let date = new Date(start); date <= end; date.setDate(date.getDate() + 1)) {
dates.push(date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }));
}
tripDates = dates;
// Initialize expanded states for dates
expandedDates = Object.fromEntries(dates.map(date => [date, false]));
// Initialize placesToVisit from database or empty array
placesToVisit = tripData.placesToVisit || [];
// Initialize or update the map
if (mapContainer && tripData.destination?.location) {
if (!map) {
map = new Map(mapContainer, {
center: tripData.destination.location,
zoom: 8,
});
} else {
map.setCenter(tripData.destination.location);
}
}
}
});
} catch (error) {
console.error('Error loading Google Maps:', error);
}
});
let expandedSections = {
explore: true,
places_to_visit: true,
itinerary: true
};
let recommendedPlaces = [
{ name: "Place name" },
{ name: "Place name" },
{ name: "Place name" }
];
let placesToVisit: any[] = [];
async function handleDeletePlace(index: number) {
const newPlacesToVisit = placesToVisit.filter((_, i) => i !== index);
try {
// Update the database
await update(ref(db, `trips/${tid}`), {
placesToVisit: newPlacesToVisit
});
// Update local state
placesToVisit = newPlacesToVisit;
} catch (error) {
console.error('Error deleting place:', error);
}
}
function toggleSection(section: keyof typeof expandedSections) {
expandedSections[section] = !expandedSections[section];
}
function handleBack() {
goto('/trips');
}
function handlePastTrip() {
console.log(`see past trips to ${tripData?.destination?.name}`)
}
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'
};
const updatedPlaces = [...placesToVisit, newPlace];
try {
// Update the database
await update(ref(db, `trips/${tid}`), {
placesToVisit: updatedPlaces
});
// Update local state
placesToVisit = updatedPlaces;
} catch (error) {
console.error('Error adding place:', error);
}
}
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`);
}
</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 {tripData?.destination?.name || 'Loading...'}</h1>
<p class="date">{tripData?.startDate ? new Date(tripData.startDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }) : ''} - {tripData?.endDate ? new Date(tripData.endDate).toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: 'numeric' }) : ''}</p>
</div>
<div class="tripmates">
<ProfilePicture friends={tripData?.tripmates?.length || 0} />
</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>
{#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="Click to view all past trips to {tripData?.destination?.name}" 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

@ -4,29 +4,65 @@
import Button from '$lib/components/Button.svelte'; import Button from '$lib/components/Button.svelte';
import NewTripPopup from '$lib/components/NewTripPopup.svelte'; import NewTripPopup from '$lib/components/NewTripPopup.svelte';
import Nav from '$lib/components/Nav.svelte'; import Nav from '$lib/components/Nav.svelte';
import { onMount } from 'svelte';
import { ref, onValue } from 'firebase/database';
import { db } from '../../firebase';
interface Trip { interface Trip {
destination: string; tid: string;
destination: {
name: string;
photo: string;
formatted_address: string;
location: {
lat: number;
lng: number;
}
};
startDate: string; startDate: string;
endDate: string; endDate: string;
imageUrl: string; tripmates: string[];
created_at: string;
} }
let activeTab = "Ongoing Trips"; let activeTab = "Ongoing Trips";
let showNewTripPopup = false; let showNewTripPopup = false;
let contentContainer: HTMLElement; let contentContainer: HTMLElement;
// Sample data, replace with actual data later let ongoingTrips: Trip[] = [];
const sample_trip = { let pastTrips: Trip[] = [];
destination: "Taiwan",
startDate: "04.27.2025",
endDate: "04.30.2025",
imageUrl: ""
}
let ongoingTrips = Array(3).fill(sample_trip);
// let pastTrips: Trip[] = []; onMount(() => {
let pastTrips = Array(14).fill(sample_trip); // Reference to the trips node
const tripsRef = ref(db, 'trips');
// Listen for changes in the trips data
onValue(tripsRef, (snapshot) => {
const trips: Trip[] = [];
snapshot.forEach((childSnapshot) => {
trips.push({
tid: childSnapshot.key,
...childSnapshot.val()
});
});
console.log(trips);
// Get today's date at midnight for comparison
const today = new Date();
today.setHours(0, 0, 0, 0);
// Filter trips based on end date
ongoingTrips = trips.filter(trip => {
const endDate = new Date(trip.endDate);
return endDate >= today;
}).sort((a, b) => new Date(a.startDate).getTime() - new Date(b.startDate).getTime());
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
});
});
function handleNewTrip() { function handleNewTrip() {
showNewTripPopup = true; showNewTripPopup = true;
@ -71,7 +107,12 @@
{:else} {:else}
<div class="trips-grid"> <div class="trips-grid">
{#each ongoingTrips as trip} {#each ongoingTrips as trip}
<TripCard {...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}
/>
{/each} {/each}
</div> </div>
{/if} {/if}
@ -83,7 +124,12 @@
{:else} {:else}
<div class="trips-grid"> <div class="trips-grid">
{#each pastTrips as trip} {#each pastTrips as trip}
<TripCard {...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}
/>
{/each} {/each}
</div> </div>
{/if} {/if}
@ -95,7 +141,7 @@
</div> </div>
</div> </div>
<NewTripPopup bind:showPopup={showNewTripPopup} fromPage="trips" /> <NewTripPopup bind:showPopup={showNewTripPopup} />
</main> </main>
<style> <style>