AddPlaces with autocomplete
This commit is contained in:
parent
db6c421b47
commit
9ccf0a741a
191
src/lib/components/AddPlaces.svelte
Normal file
191
src/lib/components/AddPlaces.svelte
Normal file
|
@ -0,0 +1,191 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Loader } from '@googlemaps/js-api-loader';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
let inputContainer: HTMLDivElement;
|
||||||
|
let inputWrapper: HTMLDivElement;
|
||||||
|
let showAddButton = false;
|
||||||
|
let lastSelectedPlaceName = '';
|
||||||
|
let selectedPlace: google.maps.places.PlaceResult | null = null;
|
||||||
|
let inputElement: HTMLInputElement;
|
||||||
|
|
||||||
|
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!GOOGLE_PLACES_API_KEY) {
|
||||||
|
console.error('Google Maps API key is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = new Loader({
|
||||||
|
apiKey: GOOGLE_PLACES_API_KEY,
|
||||||
|
version: "weekly",
|
||||||
|
libraries: ["places"],
|
||||||
|
language: 'en'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loader.importLibrary("places");
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = id;
|
||||||
|
input.placeholder = placeholder;
|
||||||
|
|
||||||
|
inputWrapper.appendChild(input);
|
||||||
|
inputElement = input;
|
||||||
|
|
||||||
|
const autocompleteOptions: google.maps.places.AutocompleteOptions = {
|
||||||
|
types: placeTypes
|
||||||
|
};
|
||||||
|
|
||||||
|
if (countryRestriction) {
|
||||||
|
autocompleteOptions.componentRestrictions = { country: countryRestriction.toLowerCase() };
|
||||||
|
}
|
||||||
|
|
||||||
|
const autocomplete = new google.maps.places.Autocomplete(input, autocompleteOptions);
|
||||||
|
autocomplete.setFields(['name', 'formatted_address', 'photos', 'place_id']);
|
||||||
|
// TODO: how to get the photos?
|
||||||
|
|
||||||
|
autocomplete.addListener('place_changed', () => {
|
||||||
|
const place = autocomplete.getPlace();
|
||||||
|
if (place && place.name) {
|
||||||
|
selectedPlace = place;
|
||||||
|
lastSelectedPlaceName = input.value.trim();
|
||||||
|
showAddButton = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
const trimmed = input.value.trim();
|
||||||
|
if (trimmed === lastSelectedPlaceName) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
showAddButton = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading Places Autocomplete:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleAddPlace() {
|
||||||
|
if (selectedPlace && inputElement) {
|
||||||
|
onPlaceSelected(selectedPlace);
|
||||||
|
|
||||||
|
inputElement.value = ''; // reset the value
|
||||||
|
|
||||||
|
showAddButton = false;
|
||||||
|
lastSelectedPlaceName = '';
|
||||||
|
selectedPlace = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="add-places-wrapper">
|
||||||
|
<div class="input-container {showAddButton ? 'with-button' : ''}" bind:this={inputContainer}>
|
||||||
|
<div class="input-with-icon" bind:this={inputWrapper}>
|
||||||
|
<i class="fa-solid fa-location-dot location-icon"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{#if showAddButton}
|
||||||
|
<button class="add-button" onclick={handleAddPlace} aria-label="Add a new place">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.add-places-wrapper {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
transition: flex-basis 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container.with-button {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-width: calc(100% - 2rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-with-icon {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.location-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 1rem;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
color: var(--gray-400);
|
||||||
|
font-size: 1rem;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button {
|
||||||
|
padding: 0.55rem 0.75rem;
|
||||||
|
color: var(--gray-200);
|
||||||
|
background: none;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
border: none;
|
||||||
|
border-radius: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
align-self: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-button:hover {
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
color: var(--planner-400);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input#add-places) {
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 0.75rem 0.75rem 0.75rem 2.5rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
background: var(--gray-50);
|
||||||
|
border: 2px solid var(--gray-50);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input#add-places:hover) {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
border-color: var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input#add-places:focus) {
|
||||||
|
outline-color: var(--planner-400);
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.pac-container) {
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
border: 1px solid var(--gray-100);
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.pac-item) {
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.pac-item:hover) {
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,18 +1,31 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import PlaceCard from "./PlaceCard.svelte";
|
import PlaceCard from "./PlaceCard.svelte";
|
||||||
|
import AddPlaces from "./AddPlaces.svelte";
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import { quintOut } from 'svelte/easing';
|
import { quintOut } from 'svelte/easing';
|
||||||
|
|
||||||
export let date;
|
export let date;
|
||||||
export let isExpanded = true;
|
export let isExpanded = true;
|
||||||
/**
|
export let places: { name: string; desc?: string; img?: string; time?: string; }[] = [];
|
||||||
* @type {{ name: string, desc: string, img: string, time: string }[]}
|
|
||||||
*/
|
|
||||||
export let places = [];
|
|
||||||
// export let recommendedPlaces = [];
|
|
||||||
|
|
||||||
function toggleDate() {
|
function toggleDate() {
|
||||||
isExpanded = !isExpanded;
|
isExpanded = !isExpanded;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handlePlaceSelected(place: google.maps.places.PlaceResult) {
|
||||||
|
const newPlace = {
|
||||||
|
name: place.name || 'Unknown Place',
|
||||||
|
desc: place.formatted_address || '',
|
||||||
|
img: place.photos ? place.photos[0].getUrl() : 'placeholder.jpeg',
|
||||||
|
time: 'Add Time'
|
||||||
|
};
|
||||||
|
|
||||||
|
places = [...places, newPlace];
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeletePlace(index: number) {
|
||||||
|
places = places.filter((_, i) => i !== index);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="date-section">
|
<div class="date-section">
|
||||||
|
@ -28,14 +41,16 @@
|
||||||
class="date-content"
|
class="date-content"
|
||||||
transition:slide={{ duration: 400, easing: quintOut }}
|
transition:slide={{ duration: 400, easing: quintOut }}
|
||||||
>
|
>
|
||||||
{#each places as place}
|
{#each places as place, i}
|
||||||
<PlaceCard {place} />
|
<PlaceCard {place} onDelete={() => handleDeletePlace(i)} />
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
<button class="add-place-btn">
|
<div class="add-places-container">
|
||||||
<i class="fa-solid fa-location-dot"></i>
|
<AddPlaces
|
||||||
Add places
|
onPlaceSelected={handlePlaceSelected}
|
||||||
</button>
|
countryRestriction="tw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -65,14 +80,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.date-text h3 {
|
.date-text h3 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 1.1rem;
|
font-size: 1.1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.arrow-icon {
|
.arrow-icon {
|
||||||
transition: transform 0.3s ease;
|
transition: transform 0.3s ease;
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rotated {
|
.rotated {
|
||||||
|
@ -83,24 +98,7 @@
|
||||||
padding: 1rem 0 1rem 2rem;
|
padding: 1rem 0 1rem 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-place-btn {
|
.add-places-container {
|
||||||
width: 100%;
|
margin-top: 1rem;
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.75rem;
|
|
||||||
background: white;
|
|
||||||
border: 2px solid var(--gray-100);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
color: var(--gray-600);
|
|
||||||
cursor: pointer;
|
|
||||||
margin: 1rem 0;
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.add-place-btn:hover {
|
|
||||||
background: var(--gray-50);
|
|
||||||
border-color: var(--gray-50);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang>
|
<script>
|
||||||
import { Colors } from '$lib/constants/Colors';
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
let title = "Travel App";
|
let title = "Travel App";
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script>
|
<script lang="ts">
|
||||||
import { Colors } from '$lib/constants/Colors';
|
import { Colors } from '$lib/constants/Colors';
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { Loader } from '@googlemaps/js-api-loader';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
|
|
||||||
export let showPopup = false;
|
export let showPopup = false;
|
||||||
|
@ -10,6 +12,74 @@
|
||||||
let startDate = "";
|
let startDate = "";
|
||||||
let endDate = "";
|
let endDate = "";
|
||||||
let friends = "";
|
let friends = "";
|
||||||
|
let destinationError = false;
|
||||||
|
let startDateError = false;
|
||||||
|
let endDateError = false;
|
||||||
|
let destinationInput: HTMLDivElement;
|
||||||
|
|
||||||
|
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
if (!GOOGLE_PLACES_API_KEY) {
|
||||||
|
console.error('Google Maps API key is missing');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loader = new Loader({
|
||||||
|
apiKey: GOOGLE_PLACES_API_KEY,
|
||||||
|
version: "weekly",
|
||||||
|
libraries: ["places"],
|
||||||
|
language: 'en'
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await loader.importLibrary("places");
|
||||||
|
|
||||||
|
const waitForElement = () => new Promise<void>((resolve) => {
|
||||||
|
const check = () => {
|
||||||
|
if (destinationInput) return resolve();
|
||||||
|
requestAnimationFrame(check);
|
||||||
|
};
|
||||||
|
check();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitForElement();
|
||||||
|
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'text';
|
||||||
|
input.id = 'destination-input';
|
||||||
|
input.placeholder = 'Where do you want to go?';
|
||||||
|
|
||||||
|
destinationInput.appendChild(input);
|
||||||
|
|
||||||
|
const autocomplete = new google.maps.places.Autocomplete(input, {
|
||||||
|
types: ['(regions)']
|
||||||
|
});
|
||||||
|
autocomplete.setFields(['name', 'formatted_address']);
|
||||||
|
|
||||||
|
autocomplete.addListener('place_changed', () => {
|
||||||
|
const place = autocomplete.getPlace();
|
||||||
|
destination = place.name || "";
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
// ------ The Implementation below is the new one, but can't style it --------
|
||||||
|
// const placeAutocomplete = new google.maps.places.PlaceAutocompleteElement({
|
||||||
|
// types: ['(cities)'], // Restrict to cities only
|
||||||
|
// });
|
||||||
|
|
||||||
|
// destinationInput.appendChild(placeAutocomplete);
|
||||||
|
|
||||||
|
// //@ts-ignore
|
||||||
|
// placeAutocomplete.addEventListener('gmp-select', async ({ placePrediction }) => {
|
||||||
|
// const place = placePrediction.toPlace();
|
||||||
|
// await place.fetchFields({ fields: ['displayName', 'formattedAddress', 'location'] });
|
||||||
|
// destination = place.displayName;
|
||||||
|
// });
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading Places Autocomplete:', error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
function handleCancel() {
|
function handleCancel() {
|
||||||
showPopup = false;
|
showPopup = false;
|
||||||
|
@ -20,7 +90,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleStart() {
|
function handleStart() {
|
||||||
console.log(destination, startDate, endDate, friends);
|
destinationError = !destination;
|
||||||
|
startDateError = !startDate;
|
||||||
|
endDateError = !endDate;
|
||||||
|
|
||||||
|
if (destinationError || startDateError || endDateError) {
|
||||||
|
alert('Please fill in all required fields: Destination, Start Date, End Date');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
goto(`/itinerary?from=${fromPage}`);
|
goto(`/itinerary?from=${fromPage}`);
|
||||||
handleCancel();
|
handleCancel();
|
||||||
}
|
}
|
||||||
|
@ -32,31 +110,28 @@
|
||||||
<h1>Start a New Plan</h1>
|
<h1>Start a New Plan</h1>
|
||||||
|
|
||||||
<div class="input-form">
|
<div class="input-form">
|
||||||
<label for="destination">Destination</label>
|
<label for="destination-input" class:error={destinationError}>Destination</label>
|
||||||
<input
|
<div bind:this={destinationInput} class="destination-wrapper" id="destination"></div>
|
||||||
type="text"
|
|
||||||
id="destination"
|
|
||||||
bind:value={destination}
|
|
||||||
placeholder="Where do you want to go?"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="date-group">
|
<div class="date-group">
|
||||||
<div class="input-form">
|
<div class="input-form">
|
||||||
<label for="start-date">Start Date</label>
|
<label for="start-date" class:error={startDateError}>Start Date</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
id="start-date"
|
id="start-date"
|
||||||
bind:value={startDate}
|
bind:value={startDate}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="input-form">
|
<div class="input-form">
|
||||||
<label for="end-date">End Date</label>
|
<label for="end-date" class:error={endDateError}>End Date</label>
|
||||||
<input
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
id="end-date"
|
id="end-date"
|
||||||
bind:value={endDate}
|
bind:value={endDate}
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -124,6 +199,10 @@
|
||||||
color: var(--gray-800);
|
color: var(--gray-800);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.input-form label.error {
|
||||||
|
color: var(--memory-600) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.input-form input {
|
.input-form input {
|
||||||
width: 95.3%;
|
width: 95.3%;
|
||||||
padding: 0.75rem;
|
padding: 0.75rem;
|
||||||
|
@ -136,6 +215,18 @@
|
||||||
outline-color: var(--planner-600);
|
outline-color: var(--planner-600);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:global(input#destination-input) {
|
||||||
|
border: 1px solid var(--gray-200);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 0.75rem;
|
||||||
|
width: 95.3%;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(input#destination-input:focus) {
|
||||||
|
outline-color: var(--planner-600);
|
||||||
|
}
|
||||||
|
|
||||||
.date-group {
|
.date-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
type Place = {
|
type Place = {
|
||||||
name: string;
|
name: string;
|
||||||
desc?: string;
|
desc?: string;
|
||||||
image: string;
|
image: string;
|
||||||
time: string;
|
time?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultPlace: Omit<Place, 'desc'> = {
|
const defaultPlace: Omit<Place, 'desc'> = {
|
||||||
|
@ -13,44 +15,188 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
export let place: Partial<Place> = {};
|
export let place: Partial<Place> = {};
|
||||||
|
export let variant: 'simple' | 'detailed' = 'detailed';
|
||||||
|
export let onDelete: () => void = () => {};
|
||||||
|
|
||||||
|
let showDelete = false;
|
||||||
|
|
||||||
|
function toggleDelete(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
showDelete = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDelete(event: MouseEvent) {
|
||||||
|
event.stopPropagation();
|
||||||
|
showDelete = false;
|
||||||
|
onDelete();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleMouseLeave() {
|
||||||
|
showDelete = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close delete option when clicking outside
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
const target = event.target as HTMLElement;
|
||||||
|
if (!target.closest('.actions')) {
|
||||||
|
showDelete = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// merge user-provided values with defaults
|
// merge user-provided values with defaults
|
||||||
$: fullPlace = { ...defaultPlace, ...place };
|
$: fullPlace = { ...defaultPlace, ...place };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="place-card">
|
<svelte:window onclick={handleClickOutside}/>
|
||||||
<img class="place-image" src={fullPlace.image} alt=""/>
|
|
||||||
<div class="place-details">
|
{#if variant === 'simple'}
|
||||||
<div class="place-name">{fullPlace.name}</div>
|
<div class="place-card simple">
|
||||||
{#if fullPlace.desc}
|
<img class="place-image simple" src={fullPlace.image} alt=""/>
|
||||||
<p class="place-desc">{fullPlace.desc}</p>
|
<div class="place-name simple">{fullPlace.name}</div>
|
||||||
{/if}
|
<div class="actions" role="group" onmouseleave={handleMouseLeave}>
|
||||||
<div class="plan-time">
|
{#if showDelete}
|
||||||
<button class="edit-time">{fullPlace.time}</button>
|
<button
|
||||||
|
class="delete-btn"
|
||||||
|
onclick={handleDelete}
|
||||||
|
aria-label="Delete place"
|
||||||
|
transition:fade={{ duration: 100 }}
|
||||||
|
>
|
||||||
|
<span class="delete-text">Delete place</span>
|
||||||
|
<i class="fa-regular fa-trash-can"></i>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
<button class="more-btn" onclick={toggleDelete} aria-label="More options">
|
||||||
|
<i class="fa-solid fa-ellipsis-vertical"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{:else}
|
||||||
|
<div class="place-card detailed">
|
||||||
|
<img class="place-image detailed" src={fullPlace.image} alt=""/>
|
||||||
|
<div class="place-details" role="group">
|
||||||
|
<div class="actions-detailed">
|
||||||
|
<button class="close-btn" onclick={onDelete} aria-label="Delete place">
|
||||||
|
<i class="fa-solid fa-xmark"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="place-name">{fullPlace.name}</div>
|
||||||
|
{#if fullPlace.desc}
|
||||||
|
<p class="place-desc">{fullPlace.desc}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="plan-time">
|
||||||
|
<button class="edit-time">{fullPlace.time}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.place-card {
|
.place-card {
|
||||||
|
background-color: white;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-image {
|
||||||
|
object-fit: cover;
|
||||||
|
border: solid 1px var(--gray-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Simple variant styles */
|
||||||
|
.place-card.simple {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
padding: 0.5rem 0.5rem 0.5rem 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-image.simple {
|
||||||
|
width: 2.8rem;
|
||||||
|
height: 2.8rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-name.simple {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
color: var(--gray-400);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.more-btn:hover {
|
||||||
|
background-color: var(--gray-50);
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn {
|
||||||
|
background: rgba(240, 240, 240, 0.8);
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: var(--gray-400);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-btn:hover {
|
||||||
|
background-color: var(--gray-100);
|
||||||
|
color: var(--gray-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--gray-50);
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.85rem;
|
||||||
|
color: var(--gray-600);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 2rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-btn:hover {
|
||||||
|
background-color: var(--memory-50);
|
||||||
|
color: var(--memory-600);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delete-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-card.detailed {
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
gap: 3%;
|
gap: 3%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: white;
|
|
||||||
overflow: hidden;
|
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-image {
|
.place-image.detailed {
|
||||||
width: 30%;
|
width: 30%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
object-fit: cover;
|
|
||||||
border: solid 1px var(--gray-100)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.place-details {
|
.place-details {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--gray-50);
|
background-color: var(--gray-50);
|
||||||
|
@ -65,6 +211,18 @@
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.actions-detailed {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.place-details:hover .actions-detailed {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
.place-name {
|
.place-name {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
|
@ -77,7 +235,7 @@
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
.edit-time {
|
||||||
background-color: var(--gray-200);
|
background-color: var(--gray-200);
|
||||||
border: none;
|
border: none;
|
||||||
padding: 0.5rem 1rem;
|
padding: 0.5rem 1rem;
|
||||||
|
@ -85,10 +243,10 @@
|
||||||
color: var(--gray-800);
|
color: var(--gray-800);
|
||||||
font-size: 0.7rem;
|
font-size: 0.7rem;
|
||||||
margin-top: 0.5rem;
|
margin-top: 0.5rem;
|
||||||
}
|
|
||||||
|
|
||||||
button:hover {
|
|
||||||
opacity: 0.75;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.edit-time:hover {
|
||||||
|
opacity: 0.75;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
|
@ -6,12 +6,13 @@
|
||||||
import { page } from '$app/state';
|
import { page } from '$app/state';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { Loader } from '@googlemaps/js-api-loader';
|
import { Loader } from '@googlemaps/js-api-loader';
|
||||||
|
import { browser } from '$app/environment';
|
||||||
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
import ProfilePicture from '$lib/components/ProfilePicture.svelte';
|
||||||
import BottomBar from '$lib/components/BottomBar.svelte';
|
import BottomBar from '$lib/components/BottomBar.svelte';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import Button from '$lib/components/Button.svelte';
|
||||||
import ItineraryDate from '$lib/components/ItineraryDate.svelte';
|
import ItineraryDate from '$lib/components/ItineraryDate.svelte';
|
||||||
import { browser } from '$app/environment';
|
import AddPlaces from '$lib/components/AddPlaces.svelte';
|
||||||
|
import PlaceCard from '$lib/components/PlaceCard.svelte';
|
||||||
|
|
||||||
// Placeholder data obtained from the popup
|
// Placeholder data obtained from the popup
|
||||||
let destination = "Taiwan";
|
let destination = "Taiwan";
|
||||||
|
@ -53,6 +54,7 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
// Array of dates between startDate to endDate
|
// 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 tripDates = ["27/04/2025", "28/04/2025", "29/04/2025", "30/04/2025"];
|
||||||
let expandedSections = {
|
let expandedSections = {
|
||||||
explore: true,
|
explore: true,
|
||||||
|
@ -63,11 +65,21 @@
|
||||||
tripDates.forEach(date => expandedDates[date] = false);
|
tripDates.forEach(date => expandedDates[date] = false);
|
||||||
|
|
||||||
let recommendedPlaces = [
|
let recommendedPlaces = [
|
||||||
{ name: "Place name", image: "" },
|
{ name: "Place name" },
|
||||||
{ name: "Place name", image: "" },
|
{ name: "Place name" },
|
||||||
{ name: "Place name", image: "" }
|
{ 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) {
|
function toggleSection(section: keyof typeof expandedSections) {
|
||||||
expandedSections[section] = !expandedSections[section];
|
expandedSections[section] = !expandedSections[section];
|
||||||
}
|
}
|
||||||
|
@ -84,8 +96,14 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleAddPlace() {
|
function handlePlaceSelected(place: google.maps.places.PlaceResult) {
|
||||||
// TODO: Implement add place functionality
|
const newPlace = {
|
||||||
|
name: place.name || 'Unknown Place',
|
||||||
|
desc: place.formatted_address || '',
|
||||||
|
img: place.photos ? place.photos[0].getUrl() : 'placeholder.jpeg'
|
||||||
|
};
|
||||||
|
|
||||||
|
placesToVisit = [...placesToVisit, newPlace];
|
||||||
}
|
}
|
||||||
|
|
||||||
function handlePastTrip() {
|
function handlePastTrip() {
|
||||||
|
@ -127,17 +145,6 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<section class="explore-section">
|
|
||||||
<button class="section-header" onclick={() => toggleSection('explore')}>
|
|
||||||
<div class="section-text">
|
|
||||||
<i class="fa-solid fa-chevron-right arrow-icon" class:rotated={expandedSections.explore}></i>
|
|
||||||
<h2>Explore</h2>
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<!-- TODO: implement the content part -->
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="places-section">
|
<section class="places-section">
|
||||||
<button class="section-header" onclick={() => toggleSection('places_to_visit')}>
|
<button class="section-header" onclick={() => toggleSection('places_to_visit')}>
|
||||||
<div class="section-text">
|
<div class="section-text">
|
||||||
|
@ -147,6 +154,27 @@
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<!-- TODO: implement the content part -->
|
<!-- 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>
|
||||||
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="itinerary-section">
|
<section class="itinerary-section">
|
||||||
|
@ -166,7 +194,6 @@
|
||||||
<ItineraryDate
|
<ItineraryDate
|
||||||
{date}
|
{date}
|
||||||
isExpanded={expandedDates[date]}
|
isExpanded={expandedDates[date]}
|
||||||
places={places_placeholder}
|
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
@ -316,6 +343,14 @@
|
||||||
.section-content {
|
.section-content {
|
||||||
padding-left: 1.5rem;
|
padding-left: 1.5rem;
|
||||||
padding-top: 1rem;
|
padding-top: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content.places{
|
||||||
|
margin-left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button-group {
|
.button-group {
|
||||||
|
|
|
@ -32,7 +32,7 @@
|
||||||
<Button text="+ Add a new memory" type="memory" onClick={handleNewMemory} />
|
<Button text="+ Add a new memory" type="memory" onClick={handleNewMemory} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} fromPage="memory" />
|
<NewMemoryPopup bind:showPopup={showNewMemoryPopup} />
|
||||||
|
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user