mirror of
https://github.com/seanmorley15/AdventureLog.git
synced 2025-07-25 15:59:38 +02:00
migration to new backend
This commit is contained in:
parent
28a5d423c2
commit
9abe9fb315
309 changed files with 21476 additions and 24132 deletions
64
frontend/src/lib/components/AboutModal.svelte
Normal file
64
frontend/src/lib/components/AboutModal.svelte
Normal file
|
@ -0,0 +1,64 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { onMount } from 'svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
import { appVersion, copyrightYear, versionChangelog } from '$lib/config';
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
dispatch('close');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">About AdventureLog</h3>
|
||||
<p class="py-1">
|
||||
AdventureLog <a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-500 underline"
|
||||
href={versionChangelog}>{appVersion}</a
|
||||
>
|
||||
</p>
|
||||
<p class="py-1">
|
||||
© {copyrightYear}
|
||||
<a
|
||||
href="https://github.com/seanmorley15"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-500 underline">Sean Morley</a
|
||||
>
|
||||
</p>
|
||||
<p class="py-1">Liscensed under the GPL-3.0 License.</p>
|
||||
<p class="py-1">
|
||||
<a
|
||||
href="https://github.com/seanmorley15/AdventureLog"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="text-primary-500 underline">Source Code</a
|
||||
>
|
||||
</p>
|
||||
<p class="py-1">Made with ❤️ in the United States.</p>
|
||||
<div
|
||||
class="modal-action items-center"
|
||||
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||
></div>
|
||||
<button class="btn btn-primary" on:click={close}>Close</button>
|
||||
</div>
|
||||
</dialog>
|
116
frontend/src/lib/components/AdventureCard.svelte
Normal file
116
frontend/src/lib/components/AdventureCard.svelte
Normal file
|
@ -0,0 +1,116 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { Adventure } from '$lib/types';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
import Launch from '~icons/mdi/launch';
|
||||
import FileDocumentEdit from '~icons/mdi/file-document-edit';
|
||||
import TrashCan from '~icons/mdi/trash-can-outline';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
import MapMarker from '~icons/mdi/map-marker';
|
||||
import { addToast } from '$lib/toasts';
|
||||
|
||||
export let type: string;
|
||||
|
||||
export let adventure: Adventure;
|
||||
|
||||
async function deleteAdventure() {
|
||||
let res = await fetch(`/adventures/${adventure.id}?/delete`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
});
|
||||
if (res.ok) {
|
||||
console.log('Adventure deleted');
|
||||
addToast('info', 'Adventure deleted successfully!');
|
||||
dispatch('delete', adventure.id);
|
||||
} else {
|
||||
console.log('Error deleting adventure');
|
||||
}
|
||||
}
|
||||
|
||||
function editAdventure() {
|
||||
dispatch('edit', adventure);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
||||
>
|
||||
<figure>
|
||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||
{#if adventure.image && adventure.image !== ''}
|
||||
<img
|
||||
src={adventure.image}
|
||||
alt="Adventure Image"
|
||||
class="w-full h-48 object-cover"
|
||||
crossorigin="anonymous"
|
||||
/>
|
||||
{:else}
|
||||
<img
|
||||
src={'https://placehold.co/300?text=No%20Image%20Found&font=roboto'}
|
||||
alt="No image available"
|
||||
class="w-full h-48 object-cover"
|
||||
/>
|
||||
{/if}
|
||||
</figure>
|
||||
|
||||
<div class="card-body">
|
||||
<h2 class="card-title break-words text-wrap">
|
||||
{adventure.name}
|
||||
</h2>
|
||||
{#if adventure.location && adventure.location !== ''}
|
||||
<div class="inline-flex items-center">
|
||||
<MapMarker class="w-5 h-5 mr-1" />
|
||||
<p class="ml-.5">{adventure.location}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if adventure.date && adventure.date !== ''}
|
||||
<div class="inline-flex items-center">
|
||||
<Calendar class="w-5 h-5 mr-1" />
|
||||
<p>{new Date(adventure.date).toLocaleDateString()}</p>
|
||||
</div>
|
||||
{/if}
|
||||
{#if adventure.activity_types && adventure.activity_types.length > 0}
|
||||
<ul class="flex flex-wrap">
|
||||
{#each adventure.activity_types as activity}
|
||||
<div class="badge badge-primary mr-1 text-md font-semibold pb-2 pt-1 mb-1">
|
||||
{activity}
|
||||
</div>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
<div class="card-actions justify-end mt-2">
|
||||
{#if type == 'visited'}
|
||||
<button class="btn btn-primary" on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" /></button
|
||||
>
|
||||
<button class="btn btn-primary" on:click={editAdventure}>
|
||||
<FileDocumentEdit class="w-6 h-6" />
|
||||
</button>
|
||||
<button class="btn btn-secondary" on:click={deleteAdventure}
|
||||
><TrashCan class="w-6 h-6" /></button
|
||||
>
|
||||
{/if}
|
||||
{#if type == 'planned'}
|
||||
<button class="btn btn-primary" on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" /></button
|
||||
>
|
||||
<button class="btn btn-primary" on:click={editAdventure}>
|
||||
<FileDocumentEdit class="w-6 h-6" />
|
||||
</button>
|
||||
<button class="btn btn-secondary" on:click={deleteAdventure}
|
||||
><TrashCan class="w-6 h-6" /></button
|
||||
>
|
||||
{/if}
|
||||
{#if type == 'featured'}
|
||||
<!-- TODO: option to add to visited or featured -->
|
||||
<button class="btn btn-primary" on:click={() => goto(`/adventures/${adventure.id}`)}
|
||||
><Launch class="w-6 h-6" /></button
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
39
frontend/src/lib/components/Avatar.svelte
Normal file
39
frontend/src/lib/components/Avatar.svelte
Normal file
|
@ -0,0 +1,39 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
export let user: any;
|
||||
|
||||
let letter: string = user.first_name[0];
|
||||
|
||||
if (user && !user.first_name && user.username) {
|
||||
letter = user.username[0];
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="dropdown dropdown-bottom dropdown-end" tabindex="0" role="button">
|
||||
<div class="avatar placeholder">
|
||||
<div class="bg-neutral text-neutral-content rounded-full w-10 ml-4">
|
||||
{#if user.profile_pic}
|
||||
<img src={user.profile_pic} alt="" />
|
||||
{:else}
|
||||
<span class="text-2xl -mt-1">{letter}</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="dropdown-content z-[1] menu p-2 shadow bg-primary-content mt-2 rounded-box w-52"
|
||||
>
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<!-- svelte-ignore a11y-missing-attribute -->
|
||||
<p class="text-lg ml-4 font-bold">Hi, {user.first_name} {user.last_name}</p>
|
||||
<li><button on:click={() => goto('/profile')}>Profile</button></li>
|
||||
<li><button on:click={() => goto('/visited')}>My Log</button></li>
|
||||
<li><button on:click={() => goto('/settings')}>User Settings</button></li>
|
||||
<form method="post">
|
||||
<li><button formaction="/?/logout">Logout</button></li>
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
34
frontend/src/lib/components/CountryCard.svelte
Normal file
34
frontend/src/lib/components/CountryCard.svelte
Normal file
|
@ -0,0 +1,34 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { getFlag } from '$lib';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let countryName: string | undefined = undefined;
|
||||
export let countryCode: string;
|
||||
|
||||
async function nav() {
|
||||
goto(`/worldtravel/${countryCode}`);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
||||
>
|
||||
<figure>
|
||||
<!-- svelte-ignore a11y-img-redundant-alt -->
|
||||
<img
|
||||
src={getFlag(240, countryCode) ||
|
||||
'https://placehold.co/300?text=No%20Image%20Found&font=roboto'}
|
||||
alt="No image available"
|
||||
class="w-full h-48 object-cover"
|
||||
/>
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title overflow-ellipsis">{countryName}</h2>
|
||||
<div class="card-actions justify-end">
|
||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
||||
<button class="btn btn-primary" on:click={nav}>Open</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
256
frontend/src/lib/components/EditAdventure.svelte
Normal file
256
frontend/src/lib/components/EditAdventure.svelte
Normal file
|
@ -0,0 +1,256 @@
|
|||
<script lang="ts">
|
||||
export let adventureToEdit: Adventure;
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Adventure } from '$lib/types';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { onMount } from 'svelte';
|
||||
import { addToast } from '$lib/toasts';
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
console.log(adventureToEdit.id);
|
||||
|
||||
let originalName = adventureToEdit.name;
|
||||
|
||||
let isPointModalOpen: boolean = false;
|
||||
|
||||
import MapMarker from '~icons/mdi/map-marker';
|
||||
import Calendar from '~icons/mdi/calendar';
|
||||
import Notebook from '~icons/mdi/notebook';
|
||||
import ClipboardList from '~icons/mdi/clipboard-list';
|
||||
import Image from '~icons/mdi/image';
|
||||
import Star from '~icons/mdi/star';
|
||||
import Attachment from '~icons/mdi/attachment';
|
||||
import PointSelectionModal from './PointSelectionModal.svelte';
|
||||
|
||||
onMount(async () => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
function submit() {}
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const response = await fetch(form.action, {
|
||||
method: form.method,
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const data = JSON.parse(result.data);
|
||||
console.log(data);
|
||||
|
||||
if (data) {
|
||||
if (typeof adventureToEdit.activity_types === 'string') {
|
||||
adventureToEdit.activity_types = (adventureToEdit.activity_types as string)
|
||||
.split(',')
|
||||
.map((activity_type) => activity_type.trim())
|
||||
.filter((activity_type) => activity_type !== '' && activity_type !== ',');
|
||||
|
||||
// Remove duplicates
|
||||
adventureToEdit.activity_types = Array.from(new Set(adventureToEdit.activity_types));
|
||||
}
|
||||
|
||||
adventureToEdit.image = data[1];
|
||||
adventureToEdit.link = data[2];
|
||||
addToast('success', 'Adventure edited successfully!');
|
||||
dispatch('saveEdit', adventureToEdit);
|
||||
close();
|
||||
} else {
|
||||
addToast('warning', 'Error editing adventure');
|
||||
console.log('Error editing adventure');
|
||||
}
|
||||
}
|
||||
}
|
||||
function setLongLat(event: CustomEvent<[number, number]>) {
|
||||
console.log(event.detail);
|
||||
adventureToEdit.latitude = event.detail[1];
|
||||
adventureToEdit.longitude = event.detail[0];
|
||||
isPointModalOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isPointModalOpen}
|
||||
<PointSelectionModal
|
||||
longitude={adventureToEdit.longitude}
|
||||
latitude={adventureToEdit.latitude}
|
||||
on:close={() => (isPointModalOpen = false)}
|
||||
on:submit={setLongLat}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">Edit Adventure: {originalName}</h3>
|
||||
<div
|
||||
class="modal-action items-center"
|
||||
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||
>
|
||||
<form method="post" style="width: 100%;" on:submit={handleSubmit} action="/adventures?/edit">
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="adventureId"
|
||||
name="adventureId"
|
||||
hidden
|
||||
readonly
|
||||
bind:value={adventureToEdit.id}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="type"
|
||||
name="type"
|
||||
hidden
|
||||
readonly
|
||||
bind:value={adventureToEdit.type}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<label for="name">Name</label><br />
|
||||
<input
|
||||
type="text"
|
||||
name="name"
|
||||
id="name"
|
||||
bind:value={adventureToEdit.name}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="location">Location<MapMarker class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
bind:value={adventureToEdit.location}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="date">Date <Calendar class="inline-block mb-1 w-6 h-6" /></label><br />
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={adventureToEdit.date}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="date">Description <Notebook class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={adventureToEdit.description}
|
||||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||
/>
|
||||
<!-- <button
|
||||
class="btn btn-neutral ml-2"
|
||||
type="button"
|
||||
on:click={generate}
|
||||
><iconify-icon icon="mdi:wikipedia" class="text-xl -mb-1"
|
||||
></iconify-icon>Generate Description</button
|
||||
> -->
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="activityTypes"
|
||||
>Activity Types <ClipboardList class="inline-block -mt-1 mb-1 w-6 h-6" /></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="activity_types"
|
||||
name="activity_types"
|
||||
bind:value={adventureToEdit.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="image">Image <Image class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br />
|
||||
<input
|
||||
type="file"
|
||||
id="image"
|
||||
name="image"
|
||||
bind:value={adventureToEdit.image}
|
||||
class="file-input file-input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="link">Link <Attachment class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br
|
||||
/>
|
||||
<input
|
||||
type="url"
|
||||
id="link"
|
||||
name="link"
|
||||
bind:value={adventureToEdit.link}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="rating">Rating <Star class="inline-block -mt-1 mb-1 w-6 h-6" /></label><br />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
name="rating"
|
||||
id="rating"
|
||||
bind:value={adventureToEdit.rating}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
id="latitude"
|
||||
hidden
|
||||
name="latitude"
|
||||
bind:value={adventureToEdit.latitude}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
id="longitude"
|
||||
hidden
|
||||
name="longitude"
|
||||
bind:value={adventureToEdit.longitude}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<div class="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
on:click={() => (isPointModalOpen = true)}
|
||||
>
|
||||
{adventureToEdit.latitude && adventureToEdit.longitude ? 'Change' : 'Select'}
|
||||
Location</button
|
||||
>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary mr-4 mt-4" on:click={submit}>Edit</button>
|
||||
<!-- if there is a button in form, it will close the modal -->
|
||||
<button class="btn mt-4" on:click={close}>Close</button>
|
||||
</form>
|
||||
<div class="flex items-center justify-center flex-wrap gap-4 mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
147
frontend/src/lib/components/Navbar.svelte
Normal file
147
frontend/src/lib/components/Navbar.svelte
Normal file
|
@ -0,0 +1,147 @@
|
|||
<script lang="ts">
|
||||
import { enhance } from '$app/forms';
|
||||
import { goto } from '$app/navigation';
|
||||
export let data: any;
|
||||
import type { SubmitFunction } from '@sveltejs/kit';
|
||||
|
||||
import DotsHorizontal from '~icons/mdi/dots-horizontal';
|
||||
import WeatherSunny from '~icons/mdi/weather-sunny';
|
||||
import WeatherNight from '~icons/mdi/weather-night';
|
||||
import Forest from '~icons/mdi/forest';
|
||||
import Flower from '~icons/mdi/flower';
|
||||
import Water from '~icons/mdi/water';
|
||||
import AboutModal from './AboutModal.svelte';
|
||||
import Avatar from './Avatar.svelte';
|
||||
|
||||
let isAboutModalOpen: boolean = false;
|
||||
|
||||
const submitUpdateTheme: SubmitFunction = ({ action }) => {
|
||||
const theme = action.searchParams.get('theme');
|
||||
console.log('theme', theme);
|
||||
if (theme) {
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
{#if isAboutModalOpen}
|
||||
<AboutModal on:close={() => (isAboutModalOpen = false)} />
|
||||
{/if}
|
||||
|
||||
<div class="navbar bg-base-100">
|
||||
<div class="navbar-start">
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost lg:hidden">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-5 w-5"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
><path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 6h16M4 12h8m-8 6h16"
|
||||
/></svg
|
||||
>
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul
|
||||
tabindex="0"
|
||||
class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 gap-2"
|
||||
>
|
||||
{#if data.user}
|
||||
<li>
|
||||
<button on:click={() => goto('/visited')}>Visited</button>
|
||||
</li>
|
||||
<li>
|
||||
<button on:click={() => goto('/planner')}>Planner</button>
|
||||
</li>
|
||||
<li>
|
||||
<button on:click={() => goto('/worldtravel')}>World Travel</button>
|
||||
</li>
|
||||
<li>
|
||||
<button on:click={() => goto('/featured')}>Featured</button>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{#if !data.user}
|
||||
<li>
|
||||
<button class="btn btn-primary" on:click={() => goto('/login')}>Login</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-primary" on:click={() => goto('/signup')}>Signup</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<a class="btn btn-ghost text-xl" href="/"
|
||||
>AdventureLog <img src="/favicon.png" alt="Map Logo" class="w-8" /></a
|
||||
>
|
||||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1 gap-2">
|
||||
{#if data.user}
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/visited')}>Visited</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/planner')}>Planner</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/worldtravel')}>World Travel</button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-neutral" on:click={() => goto('/featured')}>Featured</button>
|
||||
</li>
|
||||
{/if}
|
||||
|
||||
{#if !data.user}
|
||||
<li>
|
||||
<button class="btn btn-primary" on:click={() => goto('/login')}>Login</button>
|
||||
</li>
|
||||
<li>
|
||||
<button class="btn btn-primary" on:click={() => goto('/signup')}>Signup</button>
|
||||
</li>
|
||||
{/if}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
{#if data.user}
|
||||
<Avatar user={data.user} />
|
||||
{/if}
|
||||
<div class="dropdown dropdown-bottom dropdown-end">
|
||||
<div tabindex="0" role="button" class="btn m-1 ml-4">
|
||||
<DotsHorizontal class="w-6 h-6" />
|
||||
</div>
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<button class="btn" on:click={() => (isAboutModalOpen = true)}>About AdventureLog</button>
|
||||
<p class="font-bold m-4 text-lg">Theme Selection</p>
|
||||
<form method="POST" use:enhance={submitUpdateTheme}>
|
||||
<li>
|
||||
<button formaction="/?/setTheme&theme=light"
|
||||
>Light<WeatherSunny class="w-6 h-6" />
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<button formaction="/?/setTheme&theme=dark">Dark<WeatherNight class="w-6 h-6" /></button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<button formaction="/?/setTheme&theme=night"
|
||||
>Night<WeatherNight class="w-6 h-6" /></button
|
||||
>
|
||||
</li>
|
||||
<li>
|
||||
<button formaction="/?/setTheme&theme=forest">Forest<Forest class="w-6 h-6" /></button>
|
||||
<button formaction="/?/setTheme&theme=garden">Garden<Flower class="w-6 h-6" /></button>
|
||||
<button formaction="/?/setTheme&theme=aqua">Aqua<Water class="w-6 h-6" /></button>
|
||||
</li>
|
||||
</form>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
267
frontend/src/lib/components/NewAdventure.svelte
Normal file
267
frontend/src/lib/components/NewAdventure.svelte
Normal file
|
@ -0,0 +1,267 @@
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
import type { Adventure, Point } from '$lib/types';
|
||||
import { onMount } from 'svelte';
|
||||
import { enhance } from '$app/forms';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import PointSelectionModal from './PointSelectionModal.svelte';
|
||||
|
||||
export let type: string = 'visited';
|
||||
|
||||
let newAdventure: Adventure = {
|
||||
id: NaN,
|
||||
type: type,
|
||||
name: '',
|
||||
location: '',
|
||||
date: '',
|
||||
description: '',
|
||||
activity_types: [],
|
||||
rating: NaN,
|
||||
link: '',
|
||||
image: '',
|
||||
user_id: NaN,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
is_public: false
|
||||
};
|
||||
|
||||
let image: File;
|
||||
|
||||
let isPointModalOpen: boolean = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
onMount(async () => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
close();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSubmit(event: Event) {
|
||||
event.preventDefault();
|
||||
const form = event.target as HTMLFormElement;
|
||||
const formData = new FormData(form);
|
||||
|
||||
const response = await fetch(form.action, {
|
||||
method: form.method,
|
||||
body: formData
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
const data = JSON.parse(result.data); // Parsing the JSON string in the data field
|
||||
|
||||
if (data[1] !== undefined) {
|
||||
// these two lines here are wierd, because the data[1] is the id of the new adventure and data[2] is the user_id of the new adventure
|
||||
console.log(data);
|
||||
let id = data[1];
|
||||
let user_id = data[2];
|
||||
let image_url = data[3];
|
||||
let link = data[4];
|
||||
newAdventure.image = image_url;
|
||||
newAdventure.id = id;
|
||||
newAdventure.user_id = user_id;
|
||||
newAdventure.link = link;
|
||||
// turn the activity_types string into an array by splitting it at the commas
|
||||
if (typeof newAdventure.activity_types === 'string') {
|
||||
newAdventure.activity_types = (newAdventure.activity_types as string)
|
||||
.split(',')
|
||||
.map((activity_type) => activity_type.trim())
|
||||
.filter((activity_type) => activity_type !== '' && activity_type !== ',');
|
||||
|
||||
// Remove duplicates
|
||||
newAdventure.activity_types = Array.from(new Set(newAdventure.activity_types));
|
||||
}
|
||||
console.log(newAdventure);
|
||||
dispatch('create', newAdventure);
|
||||
addToast('success', 'Adventure created successfully!');
|
||||
close();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setLongLat(event: CustomEvent<[number, number]>) {
|
||||
console.log(event.detail);
|
||||
newAdventure.latitude = event.detail[1];
|
||||
newAdventure.longitude = event.detail[0];
|
||||
isPointModalOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isPointModalOpen}
|
||||
<PointSelectionModal on:close={() => (isPointModalOpen = false)} on:submit={setLongLat} />
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg">New {type} Adventure</h3>
|
||||
<div
|
||||
class="modal-action items-center"
|
||||
style="display: flex; flex-direction: column; align-items: center; width: 100%;"
|
||||
>
|
||||
<form
|
||||
method="post"
|
||||
style="width: 100%;"
|
||||
on:submit={handleSubmit}
|
||||
action="/adventures?/create"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
name="type"
|
||||
id="type"
|
||||
value={type}
|
||||
hidden
|
||||
readonly
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<div class="mb-2">
|
||||
<label for="name">Name</label><br />
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
bind:value={newAdventure.name}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="location"
|
||||
>Location<iconify-icon icon="mdi:map-marker" class="text-lg ml-0.5 -mb-0.5"
|
||||
></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="location"
|
||||
name="location"
|
||||
bind:value={newAdventure.location}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="date"
|
||||
>Date<iconify-icon icon="mdi:calendar" class="text-lg ml-1 -mb-0.5"
|
||||
></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="date"
|
||||
id="date"
|
||||
name="date"
|
||||
bind:value={newAdventure.date}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="description"
|
||||
>Description<iconify-icon icon="mdi:notebook" class="text-lg ml-1 -mb-0.5"
|
||||
></iconify-icon></label
|
||||
><br />
|
||||
<div class="flex">
|
||||
<input
|
||||
type="text"
|
||||
id="description"
|
||||
name="description"
|
||||
bind:value={newAdventure.description}
|
||||
class="input input-bordered w-full max-w-xs mt-1 mb-2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="activity_types"
|
||||
>Activity Types <iconify-icon icon="mdi:clipboard-list" class="text-xl -mb-1"
|
||||
></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
name="activity_types"
|
||||
id="activity_types"
|
||||
bind:value={newAdventure.activity_types}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="rating"
|
||||
>Rating <iconify-icon icon="mdi:star" class="text-xl -mb-1"></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="5"
|
||||
bind:value={newAdventure.rating}
|
||||
id="rating"
|
||||
name="rating"
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="link"
|
||||
>Link <iconify-icon icon="mdi:link" class="text-xl -mb-1"></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="text"
|
||||
id="link"
|
||||
name="link"
|
||||
bind:value={newAdventure.link}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<label for="image"
|
||||
>Image <iconify-icon icon="mdi:image" class="text-xl -mb-1"></iconify-icon></label
|
||||
><br />
|
||||
<input
|
||||
type="file"
|
||||
id="image"
|
||||
name="image"
|
||||
bind:value={image}
|
||||
class="file-input file-input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="latitude"
|
||||
hidden
|
||||
name="latitude"
|
||||
bind:value={newAdventure.latitude}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div class="mb-2">
|
||||
<input
|
||||
type="text"
|
||||
id="longitude"
|
||||
name="longitude"
|
||||
hidden
|
||||
bind:value={newAdventure.longitude}
|
||||
class="input input-bordered w-full max-w-xs mt-1"
|
||||
/>
|
||||
<div class="mb-2">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary"
|
||||
on:click={() => (isPointModalOpen = true)}>Define Location</button
|
||||
>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary mr-4 mt-4">Create</button>
|
||||
<button type="button" class="btn mt-4" on:click={close}>Close</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
77
frontend/src/lib/components/PointSelectionModal.svelte
Normal file
77
frontend/src/lib/components/PointSelectionModal.svelte
Normal file
|
@ -0,0 +1,77 @@
|
|||
<script lang="ts">
|
||||
// @ts-nocheck
|
||||
import type { Point } from '$lib/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
import { onMount } from 'svelte';
|
||||
let modal: HTMLDialogElement;
|
||||
|
||||
import { DefaultMarker, MapEvents, MapLibre, Popup } from 'svelte-maplibre';
|
||||
|
||||
let markers: Point[] = [];
|
||||
|
||||
export let longitude: number | null = null;
|
||||
export let latitude: number | null = null;
|
||||
|
||||
function addMarker(e: CustomEvent<MouseEvent>) {
|
||||
markers = [];
|
||||
markers = [...markers, { lngLat: e.detail.lngLat, name: 'Marker 1' }];
|
||||
console.log(markers);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
modal = document.getElementById('my_modal_1') as HTMLDialogElement;
|
||||
if (modal) {
|
||||
modal.showModal();
|
||||
}
|
||||
if (longitude && latitude) {
|
||||
markers = [{ lngLat: { lng: longitude, lat: latitude }, name: 'Marker 1' }];
|
||||
}
|
||||
});
|
||||
|
||||
function close() {
|
||||
dispatch('close');
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent) {
|
||||
if (event.key === 'Escape') {
|
||||
dispatch('close');
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (markers.length === 0) {
|
||||
alert('Please select a point on the map');
|
||||
return;
|
||||
}
|
||||
let coordArray: [number, number] = [markers[0].lngLat.lng, markers[0].lngLat.lat];
|
||||
console.log(coordArray);
|
||||
dispatch('submit', coordArray);
|
||||
}
|
||||
</script>
|
||||
|
||||
<dialog id="my_modal_1" class="modal">
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<div class="modal-box" role="dialog" on:keydown={handleKeydown} tabindex="0">
|
||||
<h3 class="font-bold text-lg mb-4">Choose a Point</h3>
|
||||
<MapLibre
|
||||
style="https://basemaps.cartocdn.com/gl/positron-gl-style/style.json"
|
||||
class="relative aspect-[9/16] max-h-[70vh] w-full sm:aspect-video sm:max-h-full"
|
||||
standardControls
|
||||
>
|
||||
<!-- MapEvents gives you access to map events even from other components inside the map,
|
||||
where you might not have access to the top-level `MapLibre` component. In this case
|
||||
it would also work to just use on:click on the MapLibre component itself. -->
|
||||
<MapEvents on:click={addMarker} />
|
||||
|
||||
{#each markers as marker}
|
||||
<DefaultMarker lngLat={marker.lngLat} />
|
||||
{/each}
|
||||
</MapLibre>
|
||||
|
||||
<div class="mb-4 mt-4"></div>
|
||||
<button class="btn btn-primary" on:click={submit}>Submit</button>
|
||||
<button class="btn btn-neutral" on:click={close}>Close</button>
|
||||
</div>
|
||||
</dialog>
|
72
frontend/src/lib/components/RegionCard.svelte
Normal file
72
frontend/src/lib/components/RegionCard.svelte
Normal file
|
@ -0,0 +1,72 @@
|
|||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import { addToast } from '$lib/toasts';
|
||||
import type { Region, VisitedRegion } from '$lib/types';
|
||||
import { createEventDispatcher } from 'svelte';
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let region: Region;
|
||||
export let visited: boolean;
|
||||
|
||||
export let visit_id: number | undefined | null;
|
||||
|
||||
console.log(visit_id);
|
||||
|
||||
async function markVisited() {
|
||||
let res = await fetch(`/worldtravel?/markVisited`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ regionId: region.id })
|
||||
});
|
||||
if (res.ok) {
|
||||
// visited = true;
|
||||
const result = await res.json();
|
||||
const data = JSON.parse(result.data);
|
||||
if (data[1] !== undefined) {
|
||||
console.log('New adventure created with id:', data[3]);
|
||||
let visit_id = data[3];
|
||||
let region_id = data[5];
|
||||
let user_id = data[4];
|
||||
|
||||
let newVisit: VisitedRegion = {
|
||||
id: visit_id,
|
||||
region: region_id,
|
||||
user_id: user_id
|
||||
};
|
||||
addToast('success', `Visit to ${region.name} marked`);
|
||||
dispatch('visit', newVisit);
|
||||
}
|
||||
} else {
|
||||
console.error('Failed to mark region as visited');
|
||||
}
|
||||
}
|
||||
async function removeVisit() {
|
||||
let res = await fetch(`/worldtravel?/removeVisited`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ visitId: visit_id })
|
||||
});
|
||||
if (res.ok) {
|
||||
visited = false;
|
||||
addToast('info', `Visit to ${region.name} removed`);
|
||||
} else {
|
||||
console.error('Failed to remove visit');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="card w-full max-w-xs sm:max-w-sm md:max-w-md lg:max-w-md xl:max-w-md bg-primary-content shadow-xl overflow-hidden text-base-content"
|
||||
>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title overflow-ellipsis">{region.name}</h2>
|
||||
<p>{region.id}</p>
|
||||
<div class="card-actions justify-end">
|
||||
<!-- <button class="btn btn-info" on:click={moreInfo}>More Info</button> -->
|
||||
{#if !visited}
|
||||
<button class="btn btn-primary" on:click={markVisited}>Mark Visited</button>
|
||||
{/if}
|
||||
{#if visited}
|
||||
<button class="btn btn-warning" on:click={removeVisit}>Remove</button>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
18
frontend/src/lib/components/Toast.svelte
Normal file
18
frontend/src/lib/components/Toast.svelte
Normal file
|
@ -0,0 +1,18 @@
|
|||
<script lang="ts">
|
||||
import { toasts } from '$lib/toasts';
|
||||
|
||||
let toastList: any[] = [];
|
||||
|
||||
toasts.subscribe((value) => {
|
||||
toastList = value;
|
||||
console.log(toastList);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="toast toast-top toast-end z-50 min-w-20">
|
||||
{#each toastList as { type, message, id, duration }}
|
||||
<div class="alert alert-{type}">
|
||||
<span>{message}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
Loading…
Add table
Add a link
Reference in a new issue