adds new trail page
This commit is contained in:
56
web/src/lib/components/base/dropdown.svelte
Normal file
56
web/src/lib/components/base/dropdown.svelte
Normal file
@@ -0,0 +1,56 @@
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
export let items: { text: string; value: any }[] = [];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let isOpen = false;
|
||||
|
||||
function toggleMenu() {
|
||||
isOpen = !isOpen;
|
||||
}
|
||||
|
||||
function closeMenu() {
|
||||
isOpen = false;
|
||||
}
|
||||
|
||||
function handleItemClick(item: { text: string, value: any }) {
|
||||
dispatch("change", item);
|
||||
closeMenu();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:mouseup={() => (isOpen = false)} />
|
||||
|
||||
<div class="dropdown relative">
|
||||
<button
|
||||
class="hover:bg-gray-100 w-8 h-8 rounded-full"
|
||||
on:click={toggleMenu}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button>
|
||||
|
||||
{#if isOpen}
|
||||
<ul
|
||||
class="menu absolute bg-white border rounded-l-xl rounded-b-xl shadow-md z-10 right-0 overflow-hidden"
|
||||
class:none={isOpen}
|
||||
>
|
||||
{#each items as item}
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
|
||||
<li
|
||||
class="menu-item p-4 cursor-pointer hover:bg-gray-100 focus:bg-gray-200 transition-colors"
|
||||
tabindex="0"
|
||||
on:mouseup={() => handleItemClick(item)}
|
||||
on:keydown={() => handleItemClick(item)}
|
||||
>
|
||||
{item.text}
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
52
web/src/lib/components/base/modal.svelte
Normal file
52
web/src/lib/components/base/modal.svelte
Normal file
@@ -0,0 +1,52 @@
|
||||
<script lang="ts">
|
||||
export let id: string;
|
||||
export let title: string;
|
||||
export function openModal() {
|
||||
(document.getElementById(id) as HTMLDialogElement).showModal();
|
||||
}
|
||||
|
||||
export function closeModal() {
|
||||
(document.getElementById(id) as HTMLDialogElement).close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {openModal} />
|
||||
<dialog
|
||||
{id}
|
||||
tabindex="-1"
|
||||
aria-hidden="true"
|
||||
class="relative w-full max-w-2xl max-h-full rounded-xl"
|
||||
>
|
||||
<!-- Modal content -->
|
||||
<div class="relative bg-white shadow dark:bg-gray-700">
|
||||
<!-- Modal header -->
|
||||
<div
|
||||
class="flex items-center justify-between p-4 md:p-5 border-b rounded-t dark:border-gray-600"
|
||||
>
|
||||
<h3 class="text-xl font-semibold text-gray-900 dark:text-white">
|
||||
{title}
|
||||
</h3>
|
||||
<button
|
||||
type="button"
|
||||
class="text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
on:click={closeModal}
|
||||
>
|
||||
<i class="fa fa-close"></i>
|
||||
<span class="sr-only">Close modal</span>
|
||||
</button>
|
||||
</div>
|
||||
<!-- Modal body -->
|
||||
<div class="p-4 md:p-5 space-y-4">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
<!-- Modal footer -->
|
||||
<div
|
||||
class="p-4 md:p-5 border-t border-gray-200 rounded-b dark:border-gray-600"
|
||||
>
|
||||
<slot name="footer" {closeModal} />
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
21
web/src/lib/components/base/select.svelte
Normal file
21
web/src/lib/components/base/select.svelte
Normal file
@@ -0,0 +1,21 @@
|
||||
<script lang="ts">
|
||||
export let value: any;
|
||||
export let items: { text: string; value: any }[] = [];
|
||||
export let label: string = "";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label.length}
|
||||
<p class="text-sm font-medium pb-1">
|
||||
{label}
|
||||
</p>
|
||||
{/if}
|
||||
<select
|
||||
class="bg-gray-50 h-10 w-full px-4 border-r-8 border-transparent outline outline-1 outline-gray-200 rounded-md focus:outline-primary transition-colors"
|
||||
bind:value
|
||||
>
|
||||
{#each items as item}
|
||||
<option value={item.value}>{item.text}</option>
|
||||
{/each}
|
||||
</select>
|
||||
</div>
|
||||
33
web/src/lib/components/base/text_field.svelte
Normal file
33
web/src/lib/components/base/text_field.svelte
Normal file
@@ -0,0 +1,33 @@
|
||||
<script lang="ts">
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
export let icon: string = "";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label.length}
|
||||
<p class="text-sm font-medium pb-1">
|
||||
{label}
|
||||
</p>
|
||||
{/if}
|
||||
<div class="flex items-center gap-2">
|
||||
{#if icon.length > 0}
|
||||
<i class="fa fa-{icon}"></i>
|
||||
{/if}
|
||||
<input
|
||||
class="bg-gray-50 border rounded-md p-3 transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||
class:border-red-400={error.length > 0}
|
||||
class:bg-red-50={error.length > 0}
|
||||
type="text"
|
||||
bind:value
|
||||
on:change
|
||||
{placeholder}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
27
web/src/lib/components/base/textarea.svelte
Normal file
27
web/src/lib/components/base/textarea.svelte
Normal file
@@ -0,0 +1,27 @@
|
||||
<script lang="ts">
|
||||
export let value: string | number = "";
|
||||
export let placeholder: string = "";
|
||||
export let rows: number = 3;
|
||||
export let label: string = "";
|
||||
export let error: string = "";
|
||||
</script>
|
||||
|
||||
<div>
|
||||
{#if label.length}
|
||||
<p class="text-sm font-medium mb-1">
|
||||
{label}
|
||||
</p>
|
||||
{/if}
|
||||
<textarea
|
||||
class="bg-gray-50 border rounded-md p-3 resize-none transition-colors focus:border-primary focus:outline-none focus:ring-0 w-full"
|
||||
{rows}
|
||||
{placeholder}
|
||||
class:border-red-400={error.length > 0}
|
||||
class:bg-red-50={error.length > 0}
|
||||
bind:value
|
||||
on:change
|
||||
/>
|
||||
<span class="textfield-error text-xs text-red-400">
|
||||
{error}
|
||||
</span>
|
||||
</div>
|
||||
@@ -1,13 +1,24 @@
|
||||
<script lang="ts">
|
||||
import type { Category } from "$lib/models/category";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
|
||||
export let category: Category;
|
||||
</script>
|
||||
|
||||
<div class="category-card relative rounded-2xl shadow-md w-48 h-48 overflow-hidden cursor-pointer">
|
||||
<img class="w-full h-full" src={category.img} alt="" />
|
||||
<div class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"></div>
|
||||
<h5 class="absolute text-white font-bold bottom-4 left-4 text-xl">{category.name}</h5>
|
||||
<div
|
||||
class="category-card relative rounded-2xl shadow-md w-48 h-48 overflow-hidden cursor-pointer"
|
||||
>
|
||||
<img
|
||||
class="w-full h-full"
|
||||
src={getFileURL(category, category.img)}
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||
></div>
|
||||
<h5 class="absolute text-white font-bold bottom-4 left-4 text-xl">
|
||||
{category.name}
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -12,9 +12,7 @@
|
||||
<a class="font-semibold" href="">Favorites</a>
|
||||
</menu>
|
||||
<div class="flex gap-6 items-center">
|
||||
<button class="btn-primary"
|
||||
><i class="fa fa-plus mr-2"></i>New Trail</button
|
||||
>
|
||||
<a class="btn-primary btn-large" href="/new"><i class="fa fa-plus mr-2"></i>New Trail</a>
|
||||
<Avatar></Avatar>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
<script lang="ts">
|
||||
import type { Trail } from "$lib/models/trail";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
|
||||
export let trail: Trail;
|
||||
</script>
|
||||
@@ -7,15 +9,28 @@
|
||||
<div
|
||||
class="trail-card rounded-2xl shadow-md w-72 overflow-hidden cursor-pointer"
|
||||
>
|
||||
<img class="w-full h-48" src={trail.img} alt="" />
|
||||
<img class="w-full h-48" src={getFileURL(trail, trail.thumbnail)} alt="" />
|
||||
<div class="p-4">
|
||||
<div>
|
||||
<h4 class="font-semibold text-lg">{trail.name}</h4>
|
||||
<h5><i class="fa fa-location-dot mr-3"></i>{trail.location}</h5>
|
||||
</div>
|
||||
<div class="flex mt-2 gap-4">
|
||||
<span><i class="fa fa-left-right mr-2"></i>{trail.distance}m</span>
|
||||
<span><i class="fa fa-up-down mr-2"></i>{trail.height}m</span>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500">
|
||||
<span
|
||||
><i class="fa fa-left-right mr-2"></i>{formatMeters(
|
||||
trail.distance,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-up-down mr-2"></i>{formatMeters(
|
||||
trail.elevation_gain,
|
||||
)}</span
|
||||
>
|
||||
<span
|
||||
><i class="fa fa-clock mr-2"></i>{formatTimeHHMM(
|
||||
trail.duration,
|
||||
)}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
28
web/src/lib/components/waypoint/waypoint_card.svelte
Normal file
28
web/src/lib/components/waypoint/waypoint_card.svelte
Normal file
@@ -0,0 +1,28 @@
|
||||
<script lang="ts">
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
import Dropdown from "../base/dropdown.svelte";
|
||||
|
||||
export let waypoint: Waypoint;
|
||||
export let mode: "show" | "edit" = "show";
|
||||
|
||||
const dropdownItems = [
|
||||
{ text: "Edit", value: "edit" },
|
||||
{ text: "Delete", value: "delete" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="p-4 border rounded-md my-2 hover:bg-gray-50">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<h5>
|
||||
<i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}
|
||||
</h5>
|
||||
{#if mode == "edit"}
|
||||
<Dropdown items={dropdownItems} on:change></Dropdown>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if waypoint.description}
|
||||
<p>{waypoint.description}</p>
|
||||
{/if}
|
||||
<span class="text-sm text-gray-500">{waypoint.lat}, {waypoint.lon}</span>
|
||||
</div>
|
||||
87
web/src/lib/components/waypoint/waypoint_modal.svelte
Normal file
87
web/src/lib/components/waypoint/waypoint_modal.svelte
Normal file
@@ -0,0 +1,87 @@
|
||||
<script lang="ts">
|
||||
import { Waypoint, waypointSchema } from "$lib/models/waypoint";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import { createForm } from "svelte-forms-lib";
|
||||
import Modal from "../base/modal.svelte";
|
||||
import TextField from "../base/text_field.svelte";
|
||||
import Textarea from "../base/textarea.svelte";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
|
||||
export let openModal: (() => void) | undefined = undefined;
|
||||
export let closeModal: (() => void) | undefined = undefined;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<Waypoint>({
|
||||
initialValues: $waypoint,
|
||||
validationSchema: waypointSchema,
|
||||
onSubmit: async (submittedWaypoint) => {
|
||||
dispatch("save", submittedWaypoint);
|
||||
closeModal!();
|
||||
},
|
||||
});
|
||||
$: form.set($waypoint);
|
||||
</script>
|
||||
|
||||
<Modal
|
||||
id="waypoint-modal"
|
||||
title="Add Waypoint"
|
||||
let:openModal
|
||||
bind:openModal
|
||||
bind:closeModal
|
||||
>
|
||||
<slot {openModal} />
|
||||
<form
|
||||
id="waypoint-form"
|
||||
slot="content"
|
||||
class="modal-content space-y-4"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<div class="flex gap-4">
|
||||
<div class="basis-full">
|
||||
<TextField
|
||||
label="Name"
|
||||
bind:value={$form.name}
|
||||
error={$errors.name}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
</div>
|
||||
|
||||
<TextField
|
||||
label="Icon"
|
||||
bind:value={$form.icon}
|
||||
icon={$form.icon}
|
||||
error={$errors.icon}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
label="Description"
|
||||
bind:value={$form.description}
|
||||
error={$errors.description}
|
||||
on:change={handleChange}
|
||||
></Textarea>
|
||||
<div class="flex gap-4">
|
||||
<TextField
|
||||
label="Latitude"
|
||||
bind:value={$form.lat}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
<TextField
|
||||
label="Longitude"
|
||||
bind:value={$form.lon}
|
||||
error={$errors.lat}
|
||||
on:change={handleChange}
|
||||
></TextField>
|
||||
</div>
|
||||
</form>
|
||||
<div slot="footer" class="flex items-center gap-4">
|
||||
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
|
||||
<button class="btn-primary" type="submit" form="waypoint-form"
|
||||
>Save</button
|
||||
>
|
||||
</div>
|
||||
</Modal>
|
||||
@@ -1,11 +0,0 @@
|
||||
<script lang="ts">
|
||||
import type { Waypoint } from "$lib/models/waypoint";
|
||||
|
||||
export let waypoint: Waypoint;
|
||||
</script>
|
||||
|
||||
<div class="p-4 border rounded-md my-2 cursor-pointer hover:bg-gray-100">
|
||||
<h5 class="mb-2"><i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}</h5>
|
||||
<p>{waypoint.description}</p>
|
||||
<span class="text-sm text-gray-500">{waypoint.lat}, {waypoint.lon}</span>
|
||||
</div>
|
||||
Reference in New Issue
Block a user