viewimage page test
This commit is contained in:
commit
7b0740fe0a
1047
package-lock.json
generated
1047
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -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
20
src/firebase.js
Normal 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);
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -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'
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,10 @@
|
|||
import { onMount } from 'svelte';
|
||||
|
||||
onMount(() => {
|
||||
/**
|
||||
*
|
||||
* @param e {any}
|
||||
*/
|
||||
const handleClickOutside = (e) => {
|
||||
if (!e.target.closest('.profile')) {
|
||||
showDropdown = false;
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}`);
|
||||
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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,26 +1,39 @@
|
|||
<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
|
||||
<div class="profile-pictures">
|
||||
{#each Array(friends) as _, i}
|
||||
<div
|
||||
class="profile-picture"
|
||||
style="margin-left: {marginLeft}; z-index: {zIndex}"
|
||||
>
|
||||
{#if image}
|
||||
<img class="profile-img" src={image} alt="" />
|
||||
style="z-index: {friends - i}; margin-left: {i === 0 ? '0' : '-20px'}"
|
||||
>
|
||||
{#if images[i]}
|
||||
<img class="profile-img" src={images[i]} alt="Profile" />
|
||||
{:else}
|
||||
<img class="profile-img" src='profile-pic.png' alt="" />
|
||||
<div class="default-avatar">
|
||||
<i class="fa-solid fa-user"></i>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.profile-pictures {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.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 {
|
||||
|
@ -29,4 +42,14 @@
|
|||
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>
|
|
@ -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">
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
|
||||
<BottomBar onClick={() => goto('/trips')} />
|
||||
|
||||
<NewTripPopup bind:showPopup={showNewTripPopup} fromPage="home" />
|
||||
<NewTripPopup bind:showPopup={showNewTripPopup} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -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;
|
||||
|
|
420
src/routes/itinerary/[tid]/+page.svelte
Normal file
420
src/routes/itinerary/[tid]/+page.svelte
Normal 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>
|
|
@ -56,7 +56,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} fromPage="memories" />
|
||||
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} />
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
|
|
@ -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 ongoingTrips: Trip[] = [];
|
||||
let pastTrips: Trip[] = [];
|
||||
|
||||
// let pastTrips: Trip[] = [];
|
||||
let pastTrips = Array(14).fill(sample_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>
|
||||
|
|
43
src/routes/viewimage/+page.svelte
Normal file
43
src/routes/viewimage/+page.svelte
Normal 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>
|
Loading…
Reference in New Issue
Block a user