more changes

This commit is contained in:
Mohamad 2025-01-30 21:17:29 +01:00
parent e7eaf2a592
commit 727da919a5
8 changed files with 576 additions and 7 deletions

View File

@ -8,14 +8,20 @@
"name": "frontend", "name": "frontend",
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
"@capacitor/cli": "^7.0.1", "@capacitor/cli": "^7.0.1",
"@capacitor/core": "^7.0.1", "@capacitor/core": "^7.0.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@inlang/paraglide-sveltekit": "^0.15.5", "@inlang/paraglide-sveltekit": "^0.15.5",
"@svelte-plugins/datepicker": "^1.0.11",
"@uppy/core": "^4.4.1", "@uppy/core": "^4.4.1",
"@uppy/dashboard": "^4.3.1", "@uppy/dashboard": "^4.3.1",
"@uppy/svelte": "^4.3.0", "@uppy/svelte": "^4.3.0",
"@uwdata/vgplot": "^0.12.2", "@uwdata/vgplot": "^0.12.2",
"canvas-confetti": "^1.9.3", "canvas-confetti": "^1.9.3",
"date-fns": "^4.1.0",
"globe.gl": "^2.39.2", "globe.gl": "^2.39.2",
"gsap": "^3.12.7", "gsap": "^3.12.7",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@ -24,6 +30,7 @@
"motion": "^12.0.6", "motion": "^12.0.6",
"phoenix": "^1.7.18", "phoenix": "^1.7.18",
"pikaday": "^1.8.2", "pikaday": "^1.8.2",
"sonner": "^1.7.3",
"svelte-motion": "^0.12.2", "svelte-motion": "^0.12.2",
"svelte-transitions": "^1.2.0", "svelte-transitions": "^1.2.0",
"three": "^0.172.0" "three": "^0.172.0"
@ -88,6 +95,17 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@atlaskit/pragmatic-drag-and-drop": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@atlaskit/pragmatic-drag-and-drop/-/pragmatic-drag-and-drop-1.4.0.tgz",
"integrity": "sha512-qRY3PTJIcxfl/QB8Gwswz+BRvlmgAC5pB+J2hL6dkIxgqAgVwOhAamMUKsrOcFU/axG2Q7RbNs1xfoLKDuhoPg==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.0.0",
"bind-event-listener": "^3.0.0",
"raf-schd": "^4.0.3"
}
},
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.26.7", "version": "7.26.7",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.7.tgz",
@ -141,6 +159,73 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/modifiers": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/modifiers/-/modifiers-9.0.0.tgz",
"integrity": "sha512-ybiLc66qRGuZoC20wdSSG6pDXFikui/dCNGthxv4Ndy8ylErY0N3KVxY2bgo7AWwIbxDmXDg3ylAFmnrjcbVvw==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@duckdb/duckdb-wasm": { "node_modules/@duckdb/duckdb-wasm": {
"version": "1.29.0", "version": "1.29.0",
"resolved": "https://registry.npmjs.org/@duckdb/duckdb-wasm/-/duckdb-wasm-1.29.0.tgz", "resolved": "https://registry.npmjs.org/@duckdb/duckdb-wasm/-/duckdb-wasm-1.29.0.tgz",
@ -1933,6 +2018,12 @@
"integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==", "integrity": "sha512-/s55Jujywdw/Jpan+vsy6JZs1z2ZTGxTmbZTPiuSL2wz9mfzA2gN1zzaqmvfi4pq+uOt7Du85fkiwv5ymW84aQ==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@svelte-plugins/datepicker": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@svelte-plugins/datepicker/-/datepicker-1.0.11.tgz",
"integrity": "sha512-Tqc07QLyRkCpc3Glg6oRLTUApLtCrOh52d6vJ7L32QI17HrwvcDDjaH3LF3X1SBm3CWdMrnqfJp3xjUZmB4wzw==",
"license": "MIT"
},
"node_modules/@sveltejs/adapter-auto": { "node_modules/@sveltejs/adapter-auto": {
"version": "3.3.1", "version": "3.3.1",
"resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz", "resolved": "https://registry.npmjs.org/@sveltejs/adapter-auto/-/adapter-auto-3.3.1.tgz",
@ -3333,6 +3424,12 @@
"integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==", "integrity": "sha512-H0ea4Fd3lS1+sTEB2TgcLoK21lLhwEJzlQv3IN47pJS976Gx4zoWe0ak3q+uYh60ppQxg9F16Ri4tS1sfD4+jA==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/bind-event-listener": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bind-event-listener/-/bind-event-listener-3.0.0.tgz",
"integrity": "sha512-PJvH288AWQhKs2v9zyfYdPzlPqf5bXbGMmhmUIY9x4dAUGIWgomO771oBQNwJnMQSnUIXhKu6sgzpBRXTlvb8Q==",
"license": "MIT"
},
"node_modules/bottleneck": { "node_modules/bottleneck": {
"version": "2.19.5", "version": "2.19.5",
"resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz",
@ -4286,6 +4383,16 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/date-fns": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.0", "version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
@ -7256,6 +7363,35 @@
"integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==", "integrity": "sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz",
"integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.0.0.tgz",
"integrity": "sha512-4GV5sHFG0e/0AD4X+ySy6UJd3jVl1iNsNHdpad0qhABJ11twS3TTBnseqsKurKcsNqCEFeGL3uLpVChpIO3QfQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.25.0"
},
"peerDependencies": {
"react": "^19.0.0"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
@ -7594,6 +7730,13 @@
"integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==", "integrity": "sha512-5f3k2PbGGp+YtKJjOItpg3P99IMD84E4HOvcfleTb5joCHNXYLsR9yWFPOYGgaeMPDubQILTCMdsFb2OMeOjtg==",
"license": "ISC" "license": "ISC"
}, },
"node_modules/scheduler": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.25.0.tgz",
"integrity": "sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==",
"license": "MIT",
"peer": true
},
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.3", "version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
@ -7723,6 +7866,16 @@
"csstype": "^3.1.0" "csstype": "^3.1.0"
} }
}, },
"node_modules/sonner": {
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.3.tgz",
"integrity": "sha512-KXLWQfyR6AHpYZuQk8eO8fCbZSJY3JOpgsu/tbGc++jgPjj8JsR1ZpO8vFhqR/OxvWMQCSAmnSShY0gr4FPqHg==",
"license": "MIT",
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc",
"react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc"
}
},
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",

View File

@ -50,14 +50,20 @@
"vitest": "^3.0.0" "vitest": "^3.0.0"
}, },
"dependencies": { "dependencies": {
"@atlaskit/pragmatic-drag-and-drop": "^1.4.0",
"@capacitor/cli": "^7.0.1", "@capacitor/cli": "^7.0.1",
"@capacitor/core": "^7.0.1", "@capacitor/core": "^7.0.1",
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/modifiers": "^9.0.0",
"@dnd-kit/sortable": "^10.0.0",
"@inlang/paraglide-sveltekit": "^0.15.5", "@inlang/paraglide-sveltekit": "^0.15.5",
"@svelte-plugins/datepicker": "^1.0.11",
"@uppy/core": "^4.4.1", "@uppy/core": "^4.4.1",
"@uppy/dashboard": "^4.3.1", "@uppy/dashboard": "^4.3.1",
"@uppy/svelte": "^4.3.0", "@uppy/svelte": "^4.3.0",
"@uwdata/vgplot": "^0.12.2", "@uwdata/vgplot": "^0.12.2",
"canvas-confetti": "^1.9.3", "canvas-confetti": "^1.9.3",
"date-fns": "^4.1.0",
"globe.gl": "^2.39.2", "globe.gl": "^2.39.2",
"gsap": "^3.12.7", "gsap": "^3.12.7",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
@ -66,6 +72,7 @@
"motion": "^12.0.6", "motion": "^12.0.6",
"phoenix": "^1.7.18", "phoenix": "^1.7.18",
"pikaday": "^1.8.2", "pikaday": "^1.8.2",
"sonner": "^1.7.3",
"svelte-motion": "^0.12.2", "svelte-motion": "^0.12.2",
"svelte-transitions": "^1.2.0", "svelte-transitions": "^1.2.0",
"three": "^0.172.0" "three": "^0.172.0"

View File

@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import Globe from 'globe.gl';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import * as THREE from 'three';
// Types // Types
interface Traveler { interface Traveler {

View File

@ -6,10 +6,10 @@
// Fleets data // Fleets data
const fleets = [ const fleets = [
{ user: 'Swipe', handle: '@user1', avatar: '😊' }, { user: 'Swipe', handle: '@user1', avatar: '😊' , url: '/home/swipe'},
{ user: 'feature', handle: '@user2', avatar: '😎' }, { user: 'Dinner', handle: '@user2', avatar: '😎', url: '/home/dinner'},
{ user: 'feature', handle: '@user3', avatar: '🤩' }, { user: 'Chat', handle: '@user3', avatar: '🤩', url: '/home/chat'},
{ user: 'feature', handle: '@user4', avatar: '🥳' }, { user: 'Plan', handle: '@user4', avatar: '🥳', url: '/home/plan'},
]; ];
// Tweets data // Tweets data
@ -41,7 +41,7 @@
{#each fleets as fleet} {#each fleets as fleet}
<div class="flex flex-col items-center space-y-2 flex-shrink-0"> <div class="flex flex-col items-center space-y-2 flex-shrink-0">
<div class="p-1 rounded-full bg-gradient-to-tr from-primary to-secondary hover:from-secondary hover:to-primary"> <div class="p-1 rounded-full bg-gradient-to-tr from-primary to-secondary hover:from-secondary hover:to-primary">
<button on:click={goto("/swipe")} class="avatar bg-base-200 rounded-full w-16 h-16 flex items-center justify-center text-2xl hover:opacity-90"> <button on:click={goto(fleet.url)} class="avatar bg-base-200 rounded-full w-16 h-16 flex items-center justify-center text-2xl hover:opacity-90">
{fleet.avatar} {fleet.avatar}
</button> </button>
</div> </div>

View File

@ -0,0 +1,78 @@
<script lang="ts">
let messages: any = [];
let newMessage = '';
let selectedLanguage = 'en';
const languages = [
{ value: 'en', label: 'English' },
{ value: 'es', label: 'Español' },
{ value: 'fr', label: 'Français' },
{ value: 'de', label: 'Deutsch' },
{ value: 'ja', label: '日本語' }
];
function addMessage() {
if (!newMessage.trim()) return;
// Add user message
messages = [...messages, {
id: Date.now(),
content: newMessage,
isUser: true,
timestamp: new Date().toLocaleTimeString()
}];
// Simulate bot response
setTimeout(() => {
messages = [...messages, {
id: Date.now() + 1,
content: `Translated to ${selectedLanguage}: [${newMessage}]`,
isUser: false,
timestamp: new Date().toLocaleTimeString()
}];
}, 1000);
newMessage = '';
}
</script>
<div class="flex flex-col h-full overflow-scroll bg-base-200">
<!-- Language Selector -->
<div class="flex justify-end p-4 bg-base-100 shadow-sm">
<select
bind:value={selectedLanguage}
class="select select-primary select-sm w-full max-w-xs"
>
{#each languages as lang}
<option value={lang.value}>{lang.label}</option>
{/each}
</select>
</div>
<!-- Chat Messages -->
<div class="flex-1 overflow-y-auto p-4 space-y-4">
{#each messages as message}
<div class="chat {message.isUser ? 'chat-end' : 'chat-start'}">
<div class="chat-bubble {message.isUser ? 'chat-bubble-primary' : 'chat-bubble-secondary'}">
{message.content}
<div class="text-xs opacity-50 mt-1">{message.timestamp}</div>
</div>
</div>
{/each}
</div>
<!-- Message Input -->
<div class="p-4 bg-base-100 border-t">
<form on:submit|preventDefault={addMessage} class="flex gap-2">
<input
type="text"
bind:value={newMessage}
placeholder="Type a message..."
class="input input-bordered flex-1"
/>
<button type="submit" class="btn btn-primary">
Send
</button>
</form>
</div>
</div>

View File

@ -0,0 +1,113 @@
<script>
let currentCity = "Lisbon"; // Dynamic location
const expeditions = [
{
name: "Tram 28 Tascas Crawl",
seats: 3,
cuisine: "Petiscos",
highlights: ["Sardines", "Green Wine", "Local Artists"],
meeting: "Praça Luís de Camões, 7pm"
},
{
name: "Alfama Family Feast",
seats: 5,
cuisine: "Seafood",
highlights: ["Cataplana Stew", "Fado Music", "Viewpoint"],
meeting: "Miradouro Santa Luzia, 7:30pm"
}
];
</script>
<div class="h-full overflow-scroll bg-base-100 font-[Inter]">
<!-- Header -->
<!-- Local Context Bar -->
<div class="bg-primary/10 py-3 px-4 flex items-center gap-4">
<div class="flex-1">
<div class="text-sm opacity-75">Currently Exploring</div>
<div class="text-xl font-bold text-primary">{currentCity}</div>
</div>
<div class="text-4xl">🍴</div>
</div>
<!-- Expedition Cards -->
<main class="p-4 space-y-6">
{#each expeditions as expedition}
<div class="card bg-base-100 shadow-sm hover:shadow-md transition-shadow">
<div class="card-body">
<!-- Expedition Header -->
<div class="flex justify-between items-start">
<div>
<h2 class="card-title text-lg">{expedition.name}</h2>
<div class="badge badge-outline badge-sm mt-1">
{expedition.cuisine}
</div>
</div>
<div class="text-2xl">
{#if expedition.seats > 3}
🍽️
{:else}
🔥
{/if}
</div>
</div>
<!-- Cultural Highlights -->
<div class="my-4 space-y-2">
{#each expedition.highlights as item}
<div class="flex items-center gap-2 text-sm">
<div class="text-primary"></div>
<span>{item}</span>
</div>
{/each}
</div>
<!-- Group Status -->
<div class="flex items-center justify-between border-t pt-4">
<div class="flex -space-x-3">
{#each Array(expedition.seats) as _, i}
<div class="avatar placeholder">
<div class="w-8 h-8 bg-neutral text-neutral-content">
{i === 0 ? '👤' : '?'}
</div>
</div>
{/each}
</div>
<button class="btn btn-primary btn-sm">
Claim Seat ({expedition.seats} left)
</button>
</div>
<!-- Meeting Details -->
<div class="mt-4 text-xs opacity-75 flex items-center gap-2">
<span class="flex-1">📍 {expedition.meeting}</span>
<span class="badge badge-ghost badge-sm">Local Guide: Maria</span>
</div>
</div>
</div>
{/each}
</main>
<!-- Cultural Footprint -->
<div class="p-4 bg-base-200 mt-8">
<div class="max-w-md mx-auto text-center space-y-4">
<div class="text-3xl">🌐</div>
<h3 class="font-bold">Your Lisbon Journey</h3>
<div class="flex justify-center gap-4 text-sm">
<div class="space-y-1">
<div class="text-primary font-bold">5</div>
<div class="opacity-75">New Allies</div>
</div>
<div class="space-y-1">
<div class="text-primary font-bold">9</div>
<div class="opacity-75">Local Dishes</div>
</div>
<div class="space-y-1">
<div class="text-primary font-bold">3</div>
<div class="opacity-75">Neighborhoods</div>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,220 @@
<script lang="ts">
import { fade } from 'svelte/transition';
let currentTab = 'pack';
let xp = 420;
let streak = 8;
let packedItems: any = [];
let newItem = '';
let destinations: any = [];
let newDestination = '';
let expenses: any = [];
let newExpense = '';
// Duolingo-style achievement system
const achievements = [
{ name: 'Packing Pro', earned: true },
{ name: 'Globetrotter', earned: false },
{ name: 'Budget Master', earned: false }
];
function addItem() {
if (newItem) {
packedItems = [...packedItems, {
name: newItem,
packed: false,
xp: 10,
category: ['🧳 Clothing', '📱 Tech', '🧼 Toiletry'][Math.floor(Math.random() * 3)]
}];
newItem = '';
}
}
function togglePacked(item: any) {
packedItems = packedItems.map((i: { packed: any; }) =>
i === item ? {...i, packed: !i.packed} : i
);
if (!item.packed) xp += item.xp;
}
function addDestination() {
destinations = [...destinations, {
name: newDestination,
date: new Date().toISOString().split('T')[0],
emoji: '🌍'
}];
newDestination = '';
}
function addExpense() {
expenses = [...expenses, {
description: newExpense,
amount: Math.floor(Math.random() * 100),
paidBy: 'You',
splitWith: ['Travel Buddy 1', 'Travel Buddy 2']
}];
newExpense = '';
}
</script>
<svelte:head>
<link href="https://cdn.jsdelivr.net/npm/daisyui@3.9.2/dist/full.css" rel="stylesheet">
</svelte:head>
<div class="min-h-screen bg-base-200 p-8 font-[Poppins]">
<!-- XP & Streak Header -->
<div class="flex justify-between items-center mb-8">
<div class="stats shadow bg-primary text-primary-content">
<div class="stat">
<div class="stat-title">Travel XP</div>
<div class="stat-value">{xp}</div>
<div class="stat-desc">Level {Math.floor(xp/100)}</div>
</div>
</div>
<div class="indicator">
<span class="indicator-item badge badge-secondary">{streak}🔥</span>
<div class="grid w-32 h-20 bg-base-100 place-items-center rounded-box">
Streak!
</div>
</div>
</div>
<!-- Navigation Tabs -->
<div class="tabs tabs-boxed bg-accent mb-8">
<button
class:tab-active={currentTab === 'pack'}
on:click={() => currentTab = 'pack'}
class="tab text-lg"
>
🎒 Pack
</button>
<button
class:tab-active={currentTab === 'plan'}
on:click={() => currentTab = 'plan'}
class="tab text-lg"
>
🗺 Plan
</button>
<button
class:tab-active={currentTab === 'split'}
on:click={() => currentTab = 'split'}
class="tab text-lg"
>
💸 Split
</button>
</div>
<!-- Packing List Section -->
{#if currentTab === 'pack'}
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newItem}
class="join-item input input-bordered w-full"
placeholder="Add item (e.g. passport)"
/>
<button on:click={addItem} class="join-item btn btn-primary">
<span class="text-2xl">+</span>
</button>
</div>
{#each packedItems as item (item.name)}
<div transition:fade class="card bg-base-100 shadow-xl">
<div class="card-body p-4">
<div class="flex items-center gap-4">
<input
type="checkbox"
checked={item.packed}
on:change={() => togglePacked(item)}
class="checkbox checkbox-primary checkbox-lg"
/>
<div class="flex-1">
<h3 class="text-xl font-bold">{item.name}</h3>
<div class="badge badge-outline">{item.category}</div>
</div>
<div class="text-success">+{item.xp}XP</div>
</div>
</div>
</div>
{/each}
</div>
{:else if currentTab === 'plan'}
<!-- Trip Planning Section -->
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newDestination}
class="join-item input input-bordered w-full"
placeholder="Add destination (e.g. Paris)"
/>
<button on:click={addDestination} class="join-item btn btn-secondary">
<span class="text-2xl">+</span>
</button>
</div>
<div class="carousel rounded-box gap-4">
{#each destinations as destination (destination.name)}
<div class="carousel-item">
<div class="card bg-base-100 shadow-xl w-64">
<div class="card-body items-center text-center">
<div class="text-6xl mb-2">{destination.emoji}</div>
<h2 class="card-title">{destination.name}</h2>
<p>{destination.date}</p>
</div>
</div>
</div>
{/each}
</div>
</div>
{:else}
<!-- Expense Splitting Section -->
<div class="grid gap-4">
<div class="join w-full">
<input
bind:value={newExpense}
class="join-item input input-bordered w-full"
placeholder="Add expense (e.g. Hotel)"
/>
<button on:click={addExpense} class="join-item btn btn-accent">
<span class="text-2xl">+</span>
</button>
</div>
{#each expenses as expense (expense.description)}
<div class="collapse collapse-arrow bg-base-100">
<input type="radio" name="expenses" />
<div class="collapse-title text-xl font-medium">
💰 {expense.description} - ${expense.amount}
</div>
<div class="collapse-content">
<p>Paid by: {expense.paidBy}</p>
<div class="divider"></div>
<div class="flex gap-2 flex-wrap">
{#each expense.splitWith as person}
<div class="badge badge-lg badge-outline">
👤 {person}
</div>
{/each}
</div>
</div>
</div>
{/each}
</div>
{/if}
<!-- Achievements Floating Button -->
<div class="fixed bottom-4 right-4">
<div class="dropdown dropdown-top dropdown-end">
<div tabindex="0" role="button" class="btn btn-circle btn-primary text-2xl">
🏆
</div>
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-64">
{#each achievements as achievement}
<li class={achievement.earned ? 'text-success' : 'text-base-content/50'}>
<a>{achievement.name}</a>
</li>
{/each}
</ul>
</div>
</div>
</div>