viewimage page test

This commit is contained in:
Chaebean Yang 2025-06-06 15:40:18 +09:00
commit 7b0740fe0a
18 changed files with 1805 additions and 87 deletions

1047
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -28,6 +28,7 @@
"dependencies": {
"@googlemaps/js-api-loader": "^1.16.8",
"d3": "^7.9.0",
"firebase": "^11.8.1",
"topojson-client": "^3.1.0",
"topojson-server": "^3.0.1"
}

20
src/firebase.js Normal file
View File

@ -0,0 +1,20 @@
// Import the functions you need from the SDKs you need
import { initializeApp } from "firebase/app";
import { ref, child, get, set, getDatabase, onValue, push } from 'firebase/database';
// TODO: Add SDKs for Firebase products that you want to use
// https://firebase.google.com/docs/web/setup#available-libraries
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
appId: import.meta.env.VITE_FIREBASE_APP_ID
};
// Initialize Firebase
const app = initializeApp(firebaseConfig);
export const db = getDatabase(app);

View File

@ -2,17 +2,22 @@
import { onMount } from 'svelte';
import { Loader } from '@googlemaps/js-api-loader';
// Extend the PlaceResult type to include our custom photoUrl
interface ExtendedPlaceResult extends google.maps.places.PlaceResult {
photoUrl?: string;
}
export let onPlaceSelected: (place: google.maps.places.PlaceResult) => void;
export let countryRestriction: string | undefined = undefined;
export let placeTypes: string[] = ['establishment'];
export let placeholder = 'Add a place';
export let id = 'add-places';
export let id = crypto.randomUUID(); // Generate unique ID for each instance
let inputContainer: HTMLDivElement;
let inputWrapper: HTMLDivElement;
let showAddButton = false;
let lastSelectedPlaceName = '';
let selectedPlace: google.maps.places.PlaceResult | null = null;
let selectedPlace: ExtendedPlaceResult | null = null;
let inputElement: HTMLInputElement;
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
@ -35,7 +40,8 @@
const input = document.createElement('input');
input.type = 'text';
input.id = id;
input.id = `places-input-${id}`;
input.className = 'places-input';
input.placeholder = placeholder;
inputWrapper.appendChild(input);
@ -50,12 +56,28 @@
}
const autocomplete = new google.maps.places.Autocomplete(input, autocompleteOptions);
autocomplete.setFields(['name', 'formatted_address', 'photos', 'place_id']);
// TODO: how to get the photos?
autocomplete.setFields(['name', 'formatted_address', 'photos', 'place_id', 'geometry']);
autocomplete.addListener('place_changed', () => {
const place = autocomplete.getPlace();
const place = autocomplete.getPlace() as ExtendedPlaceResult;
if (place && place.name) {
// If the place has photos, get the URL for the first photo
if (place.photos && place.photos.length > 0) {
try {
const photoOptions = {
maxWidth: 400,
maxHeight: 300
};
place.photoUrl = place.photos[0].getUrl(photoOptions);
} catch (error) {
console.error('Error getting photo URL:', error);
place.photoUrl = '/placeholder.jpeg';
}
}
else {
place.photoUrl = '/placeholder.jpeg';
}
selectedPlace = place;
lastSelectedPlaceName = input.value.trim();
showAddButton = true;
@ -150,7 +172,7 @@
color: var(--planner-400);
}
:global(input#add-places) {
:global(.places-input) {
width: 100%;
box-sizing: border-box;
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
@ -163,12 +185,12 @@
transition: all 0.2s ease;
}
:global(input#add-places:hover) {
:global(.places-input:hover) {
background-color: var(--gray-100);
border-color: var(--gray-100);
}
:global(input#add-places:focus) {
:global(.places-input:focus) {
outline-color: var(--planner-400);
background-color: white;
}

View File

@ -3,13 +3,14 @@
export let text = 'Button';
export let type = 'single';
export let onClick = () => {};
</script>
export let disabled = false;
</script>
<button class="{type}" onclick={onClick}>
<button class="{type}" on:click={onClick} {disabled}>
{text}
</button>
</button>
<style>
<style>
button {
border: none;
padding: 0.8rem 1.5rem;
@ -20,6 +21,17 @@
transition: transform 0.2s ease, opacity 0.2s ease;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
transform: none;
}
button:disabled:hover {
opacity: 0.5;
transform: none;
}
.single {
background-color: var(--planner-300);
color: var(--white);
@ -73,4 +85,4 @@
.orange:hover {
background-color: var(--memory-400);
}
</style>
</style>

View File

@ -6,7 +6,7 @@
export let date;
export let isExpanded = true;
export let places: { name: string; desc?: string; img?: string; time?: string; }[] = [];
export let places: { name: string; desc?: string; image?: string; time?: string; }[] = [];
function toggleDate() {
isExpanded = !isExpanded;
@ -16,7 +16,7 @@
const newPlace = {
name: place.name || 'Unknown Place',
desc: place.formatted_address || '',
img: place.photos ? place.photos[0].getUrl() : 'placeholder.jpeg',
image: (place as any).photoUrl || '/placeholder.jpeg',
time: 'Add Time'
};

View File

@ -3,6 +3,10 @@
import { onMount } from 'svelte';
onMount(() => {
/**
*
* @param e {any}
*/
const handleClickOutside = (e) => {
if (!e.target.closest('.profile')) {
showDropdown = false;

View File

@ -6,7 +6,12 @@
import { goto } from '$app/navigation';
export let showPopup = false;
<<<<<<< HEAD
export let onAddMemory = () => {};
=======
export let locations: any[] = [];
export let onAddMemory = (p0?: { location: string; images: any[]; startDate: string; endDate: string; }) => {};
>>>>>>> 9def1973a7b39a01052b5d81f6a5327e2524e7e1
export let onCancel = () => {};
let startDate = "";
@ -15,8 +20,13 @@
let dragActive = false;
let selectedLocation = '';
let customLocation = '';
<<<<<<< HEAD
let customLocationInput: HTMLInputElement;
let images = [];
=======
let images: any[] = [];
let dragActive = false;
>>>>>>> 9def1973a7b39a01052b5d81f6a5327e2524e7e1
let showLocationError = false;
let showImageError = false;
@ -60,7 +70,7 @@
});
}
function handleFiles(files) {
function handleFiles(files: any) {
for (const file of files) {
if (file.type.startsWith('image/')) {
images = [...images, file];
@ -68,23 +78,23 @@
}
}
function handleDrop(event) {
function handleDrop(event: any) {
event.preventDefault();
dragActive = false;
handleFiles(event.dataTransfer.files);
}
function handleDragOver(event) {
function handleDragOver(event: any) {
event.preventDefault();
dragActive = true;
}
function handleDragLeave(event) {
function handleDragLeave(event: any) {
event.preventDefault();
dragActive = false;
}
function handleInputChange(event) {
function handleInputChange(event: any) {
if (event.target.files) {
handleFiles(event.target.files);
}
@ -200,8 +210,11 @@
{/if}
<div class="input-form">
<!-- svelte-ignore a11y_label_has_associated_control -->
<label>Upload images</label>
<div class="drop-area {dragActive ? 'active' : ''}"
role="button"
tabindex="0"
on:drop={handleDrop}
on:dragover={handleDragOver}
on:dragleave={handleDragLeave}
@ -214,7 +227,8 @@
style="display: none;"
id="fileInput"
/>
<div class="drop-label" on:click={() => document.getElementById('fileInput')?.click()}>
<!-- svelte-ignore a11y_click_events_have_key_events -->
<div class="drop-label" role="button" tabindex="0" on:click={() => document.getElementById('fileInput')?.click()}>
{#if images.length === 0}
<span>Drop image here</span>
{:else}

View File

@ -3,16 +3,18 @@
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
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';
export let showPopup = false;
export let fromPage = 'home';
let destination = "";
let selectedPlace: any;
let lastSelectedPlaceName = "";
let startDate = "";
let endDate = "";
let friends: string[] = [];
let tripmates: string[] = [];
let currentEmail = "";
let destinationError = false;
let startDateError = false;
@ -24,6 +26,22 @@
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
// Add reactive statements to clear errors when valid input is provided
$: if (destination) destinationError = false;
$: if (startDate) startDateError = false;
$: if (endDate) endDateError = false;
// Clear date order error when either date changes
$: if (startDate && endDate) {
const startDateTime = new Date(startDate).getTime();
const endDateTime = new Date(endDate).getTime();
if (endDateTime >= startDateTime) {
dateOrderError = false;
startDateError = false;
endDateError = false;
}
}
onMount(async () => {
if (!GOOGLE_PLACES_API_KEY) {
console.error('Google Maps API key is missing');
@ -58,13 +76,14 @@
autocomplete = new google.maps.places.Autocomplete(input, {
types: ['(regions)']
});
autocomplete.setFields(['name', 'formatted_address']);
autocomplete.setFields(['name', 'formatted_address', 'photos', 'place_id', 'geometry']);
autocomplete.addListener('place_changed', () => {
if (!autocomplete) return;
const place = autocomplete.getPlace();
if (place.name) {
destination = place.name;
selectedPlace = place;
lastSelectedPlaceName = input.value.trim();
destinationError = false;
}
@ -84,7 +103,7 @@
destinationError = false;
destination = "";
}
}, 200);
}, 400);
});
}
@ -98,15 +117,15 @@
event.preventDefault();
const email = currentEmail.trim();
if (email && isValidEmail(email) && !friends.includes(email)) {
friends = [...friends, email];
if (email && isValidEmail(email) && !tripmates.includes(email)) {
tripmates = [...tripmates, email];
currentEmail = "";
}
}
}
function removeEmail(emailToRemove: string) {
friends = friends.filter(email => email !== emailToRemove);
tripmates = tripmates.filter(email => email !== emailToRemove);
}
function handleCancel() {
@ -114,7 +133,7 @@
destination = "";
startDate = "";
endDate = "";
friends = [];
tripmates = [];
currentEmail = "";
destinationError = false;
startDateError = false;
@ -126,7 +145,7 @@
}
}
function handleStart() {
async function handleStart() {
destinationError = !destination;
startDateError = !startDate;
endDateError = !endDate;
@ -146,12 +165,41 @@
}
if (destinationError || startDateError || endDateError) {
// alert('Please fill in all required fields: Destination, Start Date, End Date');
return;
}
else {
goto(`/itinerary?from=${fromPage}`);
handleCancel();
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();
} catch (error) {
console.error('Error saving trip:', error);
}
}
}
@ -166,7 +214,7 @@
<h1>Start a New Plan</h1>
<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>
{#if destinationError}
<p class="error-message">Please enter your destination</p>
@ -213,7 +261,7 @@
</span>
</label>
<div class="email-input-container">
{#each friends as email}
{#each tripmates as email}
<div class="email-tag">
<span>{email}</span>
<button class="remove-email" onclick={() => removeEmail(email)}>×</button>
@ -224,14 +272,14 @@
id="trip-friends"
bind:value={currentEmail}
onkeydown={handleEmailInput}
placeholder={friends.length ? "" : "Enter email addresses"}
placeholder={tripmates.length ? "" : "Enter email addresses"}
/>
</div>
</div>
<div class="button-group">
<Button text="Cancel" type="gray" onClick={handleCancel} />
<Button text="Start" type="blue" onClick={handleStart} />
<Button text="Start" type="blue" onClick={handleStart} disabled={destinationError || startDateError || endDateError}/>
</div>
</div>
</div>

View File

@ -10,7 +10,7 @@
const defaultPlace: Omit<Place, 'desc'> = {
name: 'PlaceName',
image: 'placeholder.jpeg',
image: '/placeholder.jpeg',
time: 'Add Time'
};
@ -191,7 +191,8 @@
.place-image.detailed {
width: 30%;
height: 100%;
aspect-ratio: 16 / 9;
height: auto;
border-radius: 20px;
}

View File

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

View File

@ -6,7 +6,7 @@
</script>
<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 -->
{#if !image}
<div class="placeholder">

View File

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

View File

@ -100,7 +100,7 @@
const newPlace = {
name: place.name || 'Unknown Place',
desc: place.formatted_address || '',
img: place.photos ? place.photos[0].getUrl() : 'placeholder.jpeg'
image: (place as any).photoUrl || 'placeholder.jpeg'
};
placesToVisit = [...placesToVisit, newPlace];
@ -118,6 +118,14 @@
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');
@ -139,8 +147,7 @@
</div>
<div class="tripmates">
<ProfilePicture zIndex=1/>
<ProfilePicture marginLeft=-15px zIndex=0/>
<ProfilePicture friends={2} />
</div>
</header>
@ -173,6 +180,11 @@
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>
@ -353,6 +365,11 @@
margin-left: 0.5rem;
}
.places-buttons {
margin-top: 0.5rem;
display: flex;
gap: 0.5rem;
}
.button-group {
position: sticky;
flex-shrink: 0;

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

@ -56,7 +56,7 @@
</div>
</div>
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} fromPage="memories" />
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} />
</main>
<style>

View File

@ -4,29 +4,65 @@
import Button from '$lib/components/Button.svelte';
import NewTripPopup from '$lib/components/NewTripPopup.svelte';
import Nav from '$lib/components/Nav.svelte';
import { onMount } from 'svelte';
import { ref, onValue } from 'firebase/database';
import { db } from '../../firebase';
interface Trip {
destination: string;
tid: string;
destination: {
name: string;
photo: string;
formatted_address: string;
location: {
lat: number;
lng: number;
}
};
startDate: string;
endDate: string;
imageUrl: string;
tripmates: string[];
created_at: string;
}
let activeTab = "Ongoing Trips";
let showNewTripPopup = false;
let contentContainer: HTMLElement;
// Sample data, replace with actual data later
const sample_trip = {
destination: "Taiwan",
startDate: "04.27.2025",
endDate: "04.30.2025",
imageUrl: ""
}
let ongoingTrips = Array(3).fill(sample_trip);
// let pastTrips: Trip[] = [];
let pastTrips = Array(14).fill(sample_trip);
let ongoingTrips: Trip[] = [];
let pastTrips: Trip[] = [];
onMount(() => {
// 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() {
showNewTripPopup = true;
@ -71,7 +107,12 @@
{:else}
<div class="trips-grid">
{#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}
</div>
{/if}
@ -83,7 +124,12 @@
{:else}
<div class="trips-grid">
{#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}
</div>
{/if}
@ -95,7 +141,7 @@
</div>
</div>
<NewTripPopup bind:showPopup={showNewTripPopup} fromPage="trips" />
<NewTripPopup bind:showPopup={showNewTripPopup} />
</main>
<style>

View File

@ -0,0 +1,43 @@
<script>
import { page } from '$app/stores';
import { onMount } from 'svelte';
let location = '';
let startDate = '';
let endDate = '';
$: {
const q = $page.url.searchParams;
location = q.get('location') ?? '';
startDate = q.get('startDate') ?? '';
endDate = q.get('endDate') ?? '';
}
let gradientColors = ['#e74c3c', '#f1c40f', '#2ecc71', '#3498db', '#9b59b6'];
$: gradientStyle = `conic-gradient(${[...gradientColors, gradientColors[0]].join(', ')})`;
</script>
<div class="memory-view">
<h2>{location}</h2>
<p>{startDate} - {endDate}</p>
<div class="gradient-wheel" style="background-image: {gradientStyle};"></div>
</div>
<style>
.memory-view {
padding: 2rem;
font-family: sans-serif;
text-align: center;
}
.gradient-wheel {
width: 300px;
height: 300px;
margin: 2rem auto;
border-radius: 50%;
background: var(--gradient);
box-shadow: 0 0 20px rgba(0, 0, 0, 0.3);
}
</style>