itinerary page: itinerary section
This commit is contained in:
parent
21ff12f6b8
commit
86c84b42e9
28
README.md
28
README.md
|
@ -1,28 +1,22 @@
|
|||
# sv
|
||||
# Travel App
|
||||
|
||||
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
|
||||
## How to Start
|
||||
|
||||
## Creating a project
|
||||
|
||||
If you're seeing this, you've probably already done this step. Congrats!
|
||||
|
||||
```bash
|
||||
# create a new project in the current directory
|
||||
npx sv create
|
||||
|
||||
# create a new project in my-app
|
||||
npx sv create my-app
|
||||
First, you need to clone this repository by clicking the triple dots button → `Open with VS Code` or using the command below:
|
||||
```
|
||||
git clone http://git.prototyping.id/20210782/travel-app.git
|
||||
```
|
||||
|
||||
Then, you need to download the project dependencies by typing this command:
|
||||
```
|
||||
npm install
|
||||
```
|
||||
## Developing
|
||||
|
||||
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
|
||||
Once you've created a project and installed dependencies, start a development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
# or start the server and open the app in a new browser tab
|
||||
npm run dev -- --open
|
||||
```
|
||||
|
||||
## Building
|
||||
|
@ -35,4 +29,4 @@ npm run build
|
|||
|
||||
You can preview the production build with `npm run preview`.
|
||||
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
||||
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.
|
38
package-lock.json
generated
38
package-lock.json
generated
|
@ -9,6 +9,7 @@
|
|||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"places-autocomplete-svelte": "^2.2.10",
|
||||
"topojson-client": "^3.1.0",
|
||||
"topojson-server": "^3.0.1"
|
||||
},
|
||||
|
@ -30,7 +31,6 @@
|
|||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz",
|
||||
"integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
|
@ -465,11 +465,16 @@
|
|||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@googlemaps/js-api-loader": {
|
||||
"version": "1.16.8",
|
||||
"resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.8.tgz",
|
||||
"integrity": "sha512-CROqqwfKotdO6EBjZO/gQGVTbeDps5V7Mt9+8+5Q+jTg5CRMi3Ii/L9PmV3USROrt2uWxtGzJHORmByxyo9pSQ==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.8",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
|
||||
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/set-array": "^1.2.1",
|
||||
|
@ -484,7 +489,6 @@
|
|||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
@ -494,7 +498,6 @@
|
|||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
|
||||
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
|
@ -504,14 +507,12 @@
|
|||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
|
||||
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.25",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
|
||||
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
|
@ -809,7 +810,6 @@
|
|||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.5.tgz",
|
||||
"integrity": "sha512-IwQk4yfwLdibDlrXVE04jTZYlLnwsTT2PIOQQGNLWfjavGifnk1JD1LcZjZaBTRcxZu2FfPfNLOE04DSu9lqtQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"acorn": "^8.9.0"
|
||||
|
@ -1192,7 +1192,6 @@
|
|||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
|
||||
"integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
|
@ -1238,7 +1237,6 @@
|
|||
"version": "8.14.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
|
@ -1278,7 +1276,6 @@
|
|||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
|
||||
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
@ -1288,7 +1285,6 @@
|
|||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
@ -1358,7 +1354,6 @@
|
|||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
|
||||
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
|
@ -1880,14 +1875,12 @@
|
|||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.2.tgz",
|
||||
"integrity": "sha512-Epxrv+Nr/CaL4ZcFGPJIYLWFom+YeV1DqMLHJoEd9SYRxNbaFruBwfEX/kkHUJf55j2+TUbmDcmuilbP1TmXHA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "1.4.6",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.4.6.tgz",
|
||||
"integrity": "sha512-F/D2mADJ9SHY3IwksD4DAXjTt7qt7GWUf3/8RhCNWmC/67tyb55dpimHmy7EplakFaflV0R/PC+fdSPqrRHAQw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.4.15"
|
||||
|
@ -2037,7 +2030,6 @@
|
|||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz",
|
||||
"integrity": "sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/estree": "^1.0.6"
|
||||
|
@ -2057,14 +2049,12 @@
|
|||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz",
|
||||
"integrity": "sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-string": {
|
||||
"version": "0.30.17",
|
||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
|
||||
"integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||
|
@ -2239,6 +2229,18 @@
|
|||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/places-autocomplete-svelte": {
|
||||
"version": "2.2.10",
|
||||
"resolved": "https://registry.npmjs.org/places-autocomplete-svelte/-/places-autocomplete-svelte-2.2.10.tgz",
|
||||
"integrity": "sha512-P/+4m0lF1nzqH+fUVmxobVPODNapIXzKxEBHS3gj+FAuDyBZdsCi9z2fy+vpbjwMRualklFFB59nET93r4hWDw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@googlemaps/js-api-loader": "^1.16.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"svelte": "^5.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.3",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||
|
@ -2435,7 +2437,6 @@
|
|||
"version": "5.33.2",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.33.2.tgz",
|
||||
"integrity": "sha512-uiyusx2rUa9NmVMaIcShnZyDhOfFXxgkn5eXOcgjDBL3RYQGR1+7TctPcI6AWNbu4gHWF5xZ/TlFM7nnw5H+JQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.3.0",
|
||||
|
@ -2688,7 +2689,6 @@
|
|||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
|
||||
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"d3": "^7.9.0",
|
||||
"places-autocomplete-svelte": "^2.2.10",
|
||||
"topojson-client": "^3.1.0",
|
||||
"topojson-server": "^3.0.1"
|
||||
}
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="stylesheet" href="./app.css" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel='stylesheet' href='https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.7.2/css/all.min.css' integrity='sha512-Evv84Mr4kqVGRNSgIGL/F/aIDqQb7xQ2vcrdIwxfjThSH8CSR7PBEakCr51Ck+w+/U6swU2Im1vVX0SVk9ABhg==' crossorigin='anonymous'/>
|
||||
%sveltekit.head%
|
||||
</head>
|
||||
<body data-sveltekit-preload-data="hover">
|
||||
<div style="display: contents">%sveltekit.body%</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
58
src/lib/components/BottomBar.svelte
Normal file
58
src/lib/components/BottomBar.svelte
Normal file
|
@ -0,0 +1,58 @@
|
|||
<script>
|
||||
import Button from "./Button.svelte";
|
||||
|
||||
export let title = 'Past Trips';
|
||||
export let desc = 'Click to view all past trips';
|
||||
export let onClick = () => {};
|
||||
|
||||
// change if there is a button
|
||||
export let buttonText = '+ Plan a new trip';
|
||||
export let onButtonClick = undefined;
|
||||
</script>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="text" onclick={onClick}>
|
||||
<h2>{title}</h2>
|
||||
<p class="hint">{desc}</p>
|
||||
</div>
|
||||
|
||||
{#if typeof onButtonClick === 'function'}
|
||||
<Button text={buttonText} type="single" onClick={onButtonClick} />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bottom-bar {
|
||||
display: flex;
|
||||
bottom: 0;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: var(--white);
|
||||
border-radius: 20px 20px 0 0;
|
||||
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.text {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.text:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.text h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0.2rem 0 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-400);
|
||||
}
|
||||
</style>
|
54
src/lib/components/Button.svelte
Normal file
54
src/lib/components/Button.svelte
Normal file
|
@ -0,0 +1,54 @@
|
|||
<script>
|
||||
// import '../../app.css';
|
||||
export let text = 'Button';
|
||||
export let type = 'single';
|
||||
export let onClick = () => {};
|
||||
</script>
|
||||
|
||||
<button class="{type}" onclick={onClick}>
|
||||
{text}
|
||||
</button>
|
||||
|
||||
<style>
|
||||
button {
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 50px;
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.single {
|
||||
background-color: var(--planner-300);
|
||||
color: var(--white);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.single:hover {
|
||||
opacity: 0.75;
|
||||
transform: scale(1.02);
|
||||
}
|
||||
|
||||
.blue {
|
||||
width: 50%;
|
||||
background-color: var(--planner-400);
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
.blue:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.gray {
|
||||
width: 50%;
|
||||
background: var(--gray-50);
|
||||
color: var(--gray-400);
|
||||
}
|
||||
|
||||
.gray:hover {
|
||||
background-color: var(--gray-100);
|
||||
color: var(--gray-600);
|
||||
}
|
||||
</style>
|
106
src/lib/components/ItineraryDate.svelte
Normal file
106
src/lib/components/ItineraryDate.svelte
Normal file
|
@ -0,0 +1,106 @@
|
|||
<script>
|
||||
import PlaceCard from "./PlaceCard.svelte";
|
||||
import { slide } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
export let date;
|
||||
export let isExpanded = true;
|
||||
/**
|
||||
* @type {{ name: string, desc: string, img: string, time: string }[]}
|
||||
*/
|
||||
export let places = [];
|
||||
// export let recommendedPlaces = [];
|
||||
|
||||
function toggleDate() {
|
||||
isExpanded = !isExpanded;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="date-section">
|
||||
<button class="date-header" onclick={toggleDate}>
|
||||
<div class="date-text">
|
||||
<i class="fa-solid fa-chevron-right arrow-icon" class:rotated={isExpanded}></i>
|
||||
<h3>{date}</h3>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{#if isExpanded}
|
||||
<div
|
||||
class="date-content"
|
||||
transition:slide={{ duration: 400, easing: quintOut }}
|
||||
>
|
||||
{#each places as place}
|
||||
<PlaceCard {place} />
|
||||
{/each}
|
||||
|
||||
<button class="add-place-btn">
|
||||
<i class="fa-solid fa-location-dot"></i>
|
||||
Add places
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.date-section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.date-header {
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem;
|
||||
cursor: pointer;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.date-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
margin: 0;
|
||||
font-size: 1rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.date-text h3 {
|
||||
margin: 0;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.arrow-icon {
|
||||
transition: transform 0.3s ease;
|
||||
transform-origin: center;
|
||||
}
|
||||
|
||||
.rotated {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.date-content {
|
||||
padding: 1rem 0 1rem 2rem;
|
||||
}
|
||||
|
||||
.add-place-btn {
|
||||
width: 100%;
|
||||
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>
|
94
src/lib/components/PlaceCard.svelte
Normal file
94
src/lib/components/PlaceCard.svelte
Normal file
|
@ -0,0 +1,94 @@
|
|||
<script lang="ts">
|
||||
type Place = {
|
||||
name: string;
|
||||
desc?: string;
|
||||
image: string;
|
||||
time: string;
|
||||
};
|
||||
|
||||
const defaultPlace: Omit<Place, 'desc'> = {
|
||||
name: 'PlaceName',
|
||||
image: 'placeholder.jpeg',
|
||||
time: 'Add Time'
|
||||
};
|
||||
|
||||
export let place: Partial<Place> = {};
|
||||
|
||||
// merge user-provided values with defaults
|
||||
$: fullPlace = { ...defaultPlace, ...place };
|
||||
</script>
|
||||
|
||||
<div class="place-card">
|
||||
<img class="place-image" src={fullPlace.image} alt=""/>
|
||||
<div class="place-details">
|
||||
<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>
|
||||
|
||||
<style>
|
||||
.place-card {
|
||||
display: flex;
|
||||
padding: 0.5rem 0;
|
||||
gap: 3%;
|
||||
width: 100%;
|
||||
background-color: white;
|
||||
overflow: hidden;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.place-image {
|
||||
width: 30%;
|
||||
height: 100%;
|
||||
border-radius: 20px;
|
||||
object-fit: cover;
|
||||
border: solid 1px var(--gray-100)
|
||||
}
|
||||
|
||||
.place-details {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--gray-50);
|
||||
flex: 1;
|
||||
border-radius: 20px;
|
||||
box-sizing: border-box;
|
||||
padding: 1.5rem;
|
||||
gap: 0.75rem;
|
||||
font-family: 'Inter', sans-serif;
|
||||
overflow-wrap: break-word;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.place-name {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.place-desc {
|
||||
margin: 0;
|
||||
font-weight: 400;
|
||||
color: var(--gray-400);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
button {
|
||||
background-color: var(--gray-200);
|
||||
border: none;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 12px;
|
||||
color: var(--gray-800);
|
||||
font-size: 0.7rem;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
opacity: 0.75;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
32
src/lib/components/ProfilePicture.svelte
Normal file
32
src/lib/components/ProfilePicture.svelte
Normal file
|
@ -0,0 +1,32 @@
|
|||
<script>
|
||||
export let image = '';
|
||||
export let marginLeft = '0px';
|
||||
export let zIndex = '0';
|
||||
</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>
|
||||
|
||||
<style>
|
||||
.profile-picture {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid white;
|
||||
}
|
||||
|
||||
.profile-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
</style>
|
|
@ -1,15 +1,42 @@
|
|||
<script>
|
||||
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import '../app.css';
|
||||
|
||||
let title = "Travel App";
|
||||
let activeTab = "Planner";
|
||||
import '../app.css';
|
||||
import { goto } from '$app/navigation';
|
||||
import { Colors } from '$lib/constants/Colors';
|
||||
import WorldMap from '$lib/components/WorldMap.svelte';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import BottomBar from '$lib/components/BottomBar.svelte';
|
||||
|
||||
let title = "Travel App";
|
||||
let activeTab = "Planner";
|
||||
let showNewTripPopup = false;
|
||||
let destination = "";
|
||||
let startDate = "";
|
||||
let endDate = "";
|
||||
let friends = "";
|
||||
|
||||
function handleNewTrip() {
|
||||
goto('/itinerary');
|
||||
}
|
||||
</script>
|
||||
const GOOGLE_PLACES_API_KEY = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
|
||||
|
||||
function handleNewTrip() {
|
||||
showNewTripPopup = true;
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
showNewTripPopup = false;
|
||||
destination = "";
|
||||
startDate = "";
|
||||
endDate = "";
|
||||
friends = "";
|
||||
}
|
||||
|
||||
function handleStart() {
|
||||
console.log(destination, startDate, endDate, friends);
|
||||
goto('/itinerary');
|
||||
}
|
||||
|
||||
function handlePastTrip() {
|
||||
console.log("let's see the past trip");
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<nav>
|
||||
|
@ -18,18 +45,18 @@
|
|||
<div class="menu">
|
||||
<button
|
||||
class:active={activeTab === "Planner"}
|
||||
on:click={() => activeTab = "Planner"}>
|
||||
onclick={() => activeTab = "Planner"}>
|
||||
Planner
|
||||
</button>
|
||||
<button
|
||||
class:active={activeTab === "Memory"}
|
||||
on:click={() => activeTab = "Memory"}>
|
||||
onclick={() => activeTab = "Memory"}>
|
||||
Memory
|
||||
</button>
|
||||
</div>
|
||||
<div class="profile">
|
||||
<button class="profile-btn">
|
||||
<img src="/user.png" alt="" class="profile-pic"/>
|
||||
<button class="profile-btn" aria-label="Open profile">
|
||||
<i class="fa-regular fa-user fa-xl" style="color: {Colors.black}"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -39,13 +66,67 @@
|
|||
<WorldMap />
|
||||
</div>
|
||||
|
||||
<div class="bottom-bar">
|
||||
<div class="past-trips">
|
||||
<h2>Past Trips</h2>
|
||||
<p class="hint">Click to view all past trips</p>
|
||||
<BottomBar onClick={handlePastTrip} onButtonClick={handleNewTrip} />
|
||||
|
||||
{#if showNewTripPopup}
|
||||
<!-- (optional) add onclick={handleCancel} -->
|
||||
<div class="overlay">
|
||||
<div class="popup">
|
||||
<h1>Start a New Plan</h1>
|
||||
|
||||
<div class="input-form">
|
||||
<label for="destination">Destination</label>
|
||||
<input
|
||||
type="text"
|
||||
id="destination"
|
||||
bind:value={destination}
|
||||
placeholder="Where do you want to go?"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="date-group">
|
||||
<div class="input-form">
|
||||
<label for="start-date">Start Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="start-date"
|
||||
bind:value={startDate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="input-form">
|
||||
<label for="end-date">End Date</label>
|
||||
<input
|
||||
type="date"
|
||||
id="end-date"
|
||||
bind:value={endDate}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="input-form">
|
||||
<label for="trip-friends">
|
||||
<span class="invite-label">
|
||||
+ Invite Friends
|
||||
<i class="fa-solid fa-user-group" style="color: {Colors.gray.dark800}"></i>
|
||||
</span>
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="trip-friends"
|
||||
bind:value={friends}
|
||||
placeholder="Enter email addresses"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<Button text="Cancel" type="gray" onClick={handleCancel} />
|
||||
<Button text="Start" type="blue" onClick={handleStart} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button class="new-trip-btn" on:click={handleNewTrip}>+ Plan a new trip</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
</main>
|
||||
|
||||
<style>
|
||||
|
@ -54,7 +135,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
background-color: var(--gray-50);
|
||||
font-family: 'Inter';
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
nav {
|
||||
|
@ -62,8 +143,8 @@
|
|||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 1rem 2rem;
|
||||
border-bottom: 1px solid #eee;
|
||||
background-color: white;
|
||||
border-bottom: 1px solid var(--gray-100);
|
||||
background-color: var(--white);
|
||||
}
|
||||
|
||||
.logo {
|
||||
|
@ -93,12 +174,12 @@
|
|||
}
|
||||
|
||||
.menu button.active {
|
||||
color: #000;
|
||||
color: var(--black);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.menu button:hover {
|
||||
color: #000;
|
||||
color: var(--black);
|
||||
}
|
||||
|
||||
.profile-btn {
|
||||
|
@ -117,10 +198,6 @@
|
|||
background-color: var(--gray-100);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.profile-pic {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.map-container {
|
||||
flex: 1;
|
||||
|
@ -128,53 +205,84 @@
|
|||
background-color: var(--gray-50);
|
||||
/* overflow: hidden; */
|
||||
}
|
||||
|
||||
.bottom-bar {
|
||||
|
||||
/* Popup Styling */
|
||||
.overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 1.5rem 2rem;
|
||||
background-color: white;
|
||||
border-radius: 20px 20px 0 0;
|
||||
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.past-trips {
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.popup {
|
||||
background: var(--white);
|
||||
padding: 2rem;
|
||||
border-radius: 20px;
|
||||
width: 80%;
|
||||
max-width: 560px;
|
||||
box-shadow: 0 0px 20px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.past-trips:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.past-trips h2 {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
|
||||
.popup h1 {
|
||||
margin: 0 0 2rem 0;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 600;
|
||||
/* color: var(--planner-600); */
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin: 0.2rem 0 0 0;
|
||||
font-size: 0.8rem;
|
||||
color: var(--gray-400);
|
||||
|
||||
.input-form {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.new-trip-btn {
|
||||
background-color: var(--planner-300);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.8rem 1.5rem;
|
||||
border-radius: 2rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
font-size: 0.8rem;
|
||||
transition: transform 0.2s ease, opacity 0.2s ease;
|
||||
|
||||
.input-form label {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: var(--gray-800);
|
||||
}
|
||||
|
||||
.new-trip-btn:hover {
|
||||
opacity: 0.9;
|
||||
transform: scale(1.02);
|
||||
|
||||
.input-form input {
|
||||
width: 95.3%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid var(--gray-200);
|
||||
border-radius: 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
input:focus {
|
||||
outline-color: var(--planner-600);
|
||||
}
|
||||
|
||||
.date-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.date-group .input-form {
|
||||
flex: 1;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.date-group .input-form input{
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.invite-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
282
src/routes/itinerary/+page.svelte
Normal file
282
src/routes/itinerary/+page.svelte
Normal file
|
@ -0,0 +1,282 @@
|
|||
<script lang="ts">
|
||||
import '../../app.css';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
import { Colors } from '$lib/constants/Colors';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { quintOut } from 'svelte/easing';
|
||||
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';
|
||||
|
||||
// Placeholder data obtained from the popup
|
||||
let destination = "Taiwan";
|
||||
let desc = `Click to view all past trips to ${destination}`;
|
||||
let startDate = "27/04/2025";
|
||||
let endDate = "30/04/2025";
|
||||
let places: string[] = [];
|
||||
const place_placeholder = { name: 'Somewhere', desc: 'desc of the place'}
|
||||
const places_placeholder = Array(3).fill(place_placeholder);
|
||||
|
||||
// Array of dates between startDate to endDate
|
||||
let tripDates = ["27/04/2025", "28/04/2025", "29/04/2025", "30/04/2025"];
|
||||
let expandedSections = {
|
||||
explore: true,
|
||||
places_to_visit: true,
|
||||
itinerary: true
|
||||
};
|
||||
let expandedDates: Record<string, boolean> = {};
|
||||
tripDates.forEach(date => expandedDates[date] = false);
|
||||
|
||||
let recommendedPlaces = [
|
||||
{ name: "Place name", image: "" },
|
||||
{ name: "Place name", image: "" },
|
||||
{ name: "Place name", image: "" }
|
||||
];
|
||||
|
||||
function toggleSection(section: keyof typeof expandedSections) {
|
||||
expandedSections[section] = !expandedSections[section];
|
||||
}
|
||||
|
||||
function handleBack() {
|
||||
goto('/');
|
||||
}
|
||||
|
||||
function handleAddPlace() {
|
||||
// TODO: Implement add place functionality
|
||||
}
|
||||
|
||||
function handlePastTrip() {
|
||||
console.log(`see past trips to ${destination}`)
|
||||
}
|
||||
|
||||
function handleCancel() {
|
||||
console.log('cancel update');
|
||||
}
|
||||
|
||||
function handleSave() {
|
||||
console.log('save update');
|
||||
}
|
||||
|
||||
function showPastTrips() {
|
||||
// TODO: Implement past trips view
|
||||
console.log('Show past trips');
|
||||
}
|
||||
</script>
|
||||
|
||||
<main>
|
||||
<div class="plan-section">
|
||||
<header>
|
||||
<div class="back-btn-wrapper">
|
||||
<button class="back-btn" onclick={handleBack} aria-label="Back">
|
||||
<i class="fa-solid fa-chevron-left"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="trip-info">
|
||||
<h1>Trip to {destination}</h1>
|
||||
<p class="date">{startDate} - {endDate}</p>
|
||||
</div>
|
||||
|
||||
<div class="tripmates">
|
||||
<ProfilePicture zIndex=1/>
|
||||
<ProfilePicture marginLeft=-15px zIndex=0/>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<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">
|
||||
<button class="section-header" onclick={() => toggleSection('places_to_visit')}>
|
||||
<div class="section-text">
|
||||
<i class="fa-solid fa-chevron-right arrow-icon" class:rotated={expandedSections.places_to_visit}></i>
|
||||
<h2>Places to Visit</h2>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- TODO: implement the content part -->
|
||||
</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]}
|
||||
places={places_placeholder}
|
||||
/>
|
||||
{/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"></div>
|
||||
<BottomBar desc={desc} onClick={handlePastTrip} />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
<style>
|
||||
main {
|
||||
height: 100vh;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
font-family: 'Inter', sans-serif;
|
||||
}
|
||||
|
||||
.plan-section {
|
||||
height: 100vh;
|
||||
box-sizing: border-box;
|
||||
padding: 0.5rem 0rem 0rem 0rem;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.map-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100vh;
|
||||
background-color: var(--gray-50);
|
||||
}
|
||||
|
||||
.map {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0 2rem 1.5rem 1rem;
|
||||
margin-bottom: 3rem;
|
||||
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: 0 1.5rem 0 1.5rem;
|
||||
}
|
||||
|
||||
.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-weight: 600;
|
||||
}
|
||||
|
||||
.section-content {
|
||||
padding-left: 1.5rem;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
position: sticky;
|
||||
background-color: var(--white);
|
||||
padding: 1.5rem 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
BIN
static/placeholder.jpeg
Normal file
BIN
static/placeholder.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.9 KiB |
BIN
static/profile-pic.png
Normal file
BIN
static/profile-pic.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
Loading…
Reference in New Issue
Block a user