adds i18n
This commit is contained in:
@@ -1,16 +1,23 @@
|
||||
<script>
|
||||
import { beforeNavigate, goto } from "$app/navigation";
|
||||
import Toast from "$lib/components/base/toast.svelte";
|
||||
import Footer from "$lib/components/footer.svelte";
|
||||
import NavBar from "$lib/components/nav_bar.svelte";
|
||||
import { isRouteProtected } from "$lib/util/authorization_util";
|
||||
import "@fortawesome/fontawesome-free/css/all.min.css";
|
||||
import "../css/app.css";
|
||||
import "../css/components.css";
|
||||
import "../css/theme.css";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
|
||||
beforeNavigate((n) => {
|
||||
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
|
||||
n.cancel();
|
||||
goto("/login");
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>| wanderer</title>
|
||||
</svelte:head>
|
||||
<NavBar></NavBar>
|
||||
<Toast></Toast>
|
||||
<slot />
|
||||
|
||||
8
web/src/routes/+layout.ts
Normal file
8
web/src/routes/+layout.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
// +layout.ts
|
||||
import '$lib/i18n'
|
||||
import { waitLocale } from 'svelte-i18n'
|
||||
import type { LayoutLoad } from './$types'
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
await waitLocale()
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import { Canvas } from "@threlte/core";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
|
||||
@@ -59,6 +60,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Home | wanderer</title>
|
||||
</svelte:head>
|
||||
|
||||
<section
|
||||
class="hero grid grid-cols-1 md:grid-cols-2 md:px-8 md:gap-8"
|
||||
style="height: calc(100vh - 112px)"
|
||||
@@ -67,17 +72,16 @@
|
||||
class="flex flex-col justify-center gap-8 max-w-md mx-8 md:mx-auto lg:-mt-24"
|
||||
>
|
||||
<h2 class="text-7xl font-bold">
|
||||
Welcome to <span class="-tracking-[0.075em]">wanderer</span>
|
||||
{$_("welcome_to")} <span class="-tracking-[0.075em]">wanderer</span>
|
||||
</h2>
|
||||
<h5>
|
||||
Explore exciting trails, save your favorites, and experience the
|
||||
beauty of nature. Find your next adventure!
|
||||
{$_("hero_section_0_text")}
|
||||
</h5>
|
||||
<Search
|
||||
on:update={(e) => search(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
large={true}
|
||||
placeholder="Search for trails, places..."
|
||||
placeholder="{$_("search-for-trails-places")}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
</div>
|
||||
@@ -105,13 +109,12 @@
|
||||
{$currentUser ? "Trails for you" : "Explore some trails"}
|
||||
</h2>
|
||||
<h5>
|
||||
Here are some trails you might like. Or you can just go to the full
|
||||
list right now.
|
||||
{$_("hero_section_1_text")}
|
||||
</h5>
|
||||
<a
|
||||
class="inline-block btn-primary btn-large"
|
||||
href="/trails"
|
||||
role="button">Explore</a
|
||||
role="button">{$_("explore")}</a
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
@@ -119,10 +122,9 @@
|
||||
class="max-w-7xl mx-auto mt-8 px-8 xl:px-0 grid grid-cols-1 md:grid-cols-2 items-center"
|
||||
>
|
||||
<div class="max-w-md md:mx-auto space-y-8">
|
||||
<h2 class="text-4xl md:text-5xl font-bold">Categories</h2>
|
||||
<h2 class="text-4xl md:text-5xl font-bold">{$_("categories")}</h2>
|
||||
<h5>
|
||||
Did you know? You cannot only save you hiking trails. There are many
|
||||
categories for all your adventures.
|
||||
{$_("hero_section_2_text")}
|
||||
</h5>
|
||||
</div>
|
||||
<div
|
||||
@@ -130,7 +132,10 @@
|
||||
class="grid grid-cols-1 lg:grid-cols-2 justify-items-center gap-8 py-8"
|
||||
>
|
||||
{#each $categories as category}
|
||||
<a href="/trails?category={category.id}" data-sveltekit-preload-data="off">
|
||||
<a
|
||||
href="/trails?category={category.id}"
|
||||
data-sveltekit-preload-data="off"
|
||||
>
|
||||
<CategoryCard {category}></CategoryCard>
|
||||
</a>
|
||||
{/each}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { page } from "$app/stores";
|
||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import ListCard from "$lib/components/list/list_card.svelte";
|
||||
import ListModal from "$lib/components/list/list_modal.svelte";
|
||||
import TrailList from "$lib/components/trail/trail_list.svelte";
|
||||
@@ -14,11 +15,15 @@
|
||||
lists_index,
|
||||
lists_update,
|
||||
} from "$lib/stores/list_store";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let openListModal: () => void;
|
||||
let openConfirmModal: () => void;
|
||||
|
||||
let filter: TrailFilter = $page.data.filter;
|
||||
|
||||
let listToBeDeleted: List | null = null;
|
||||
|
||||
function beforeListModalOpen() {
|
||||
list.set(new List("", []));
|
||||
openListModal();
|
||||
@@ -47,12 +52,24 @@
|
||||
list.set(currentList);
|
||||
openListModal();
|
||||
} else if (item.value == "delete") {
|
||||
await lists_delete(currentList);
|
||||
await lists_index();
|
||||
openConfirmModal();
|
||||
listToBeDeleted = currentList;
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteList() {
|
||||
if (!listToBeDeleted) {
|
||||
return;
|
||||
}
|
||||
await lists_delete(listToBeDeleted);
|
||||
await lists_index();
|
||||
listToBeDeleted = null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("list", { values: { n: 2 } })} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[400px_1fr] gap-8 max-w-7xl mx-4 md:mx-auto"
|
||||
style="min-height: calc(100vh - 124px)"
|
||||
@@ -65,7 +82,7 @@
|
||||
on:click={beforeListModalOpen}
|
||||
>
|
||||
<i class="fa fa-plus text-xl aspect-square"></i>
|
||||
<h5 class="text-xl font-semibold">Create new list</h5>
|
||||
<h5 class="text-xl font-semibold">{$_("create-new-list")}</h5>
|
||||
</button>
|
||||
<hr class="border-separator my-2" />
|
||||
<div class="max-h-[512px] md:max-h-none overflow-y-auto">
|
||||
@@ -89,4 +106,9 @@
|
||||
on:update={async () => await lists_index()}
|
||||
></TrailList>
|
||||
<ListModal bind:openModal={openListModal} on:save={saveList}></ListModal>
|
||||
<ConfirmModal
|
||||
text={$_("delete-list-confirm")}
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteList}
|
||||
></ConfirmModal>
|
||||
</main>
|
||||
|
||||
@@ -10,11 +10,12 @@
|
||||
import { ClientResponseError } from "pocketbase";
|
||||
import { object, string } from "yup";
|
||||
import { theme } from "$lib/stores/theme_store";
|
||||
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let loading: boolean = false;
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
password: "",
|
||||
},
|
||||
@@ -51,6 +52,9 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("login")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
@@ -61,18 +65,18 @@
|
||||
{:else}
|
||||
<LogoTextTwoLineLight></LogoTextTwoLineLight>
|
||||
{/if}
|
||||
<h4 class="text-xl font-semibold">Save your adventures!</h4>
|
||||
<h4 class="text-xl font-semibold">{$_("slogan")}</h4>
|
||||
<div class="space-y-6 w-80">
|
||||
<TextField
|
||||
name="username"
|
||||
label="Username/Email"
|
||||
label="{$_('username')}/{$_('email')}"
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label="Password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
@@ -86,9 +90,9 @@
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
>Don't have an account? <a
|
||||
>{$_('no-account')} <a
|
||||
class="text-blue-500 underline"
|
||||
href="/register">Make one!</a
|
||||
href="/register">{$_('make-one')}</a
|
||||
></span
|
||||
>
|
||||
</form>
|
||||
|
||||
@@ -15,9 +15,14 @@
|
||||
trails,
|
||||
trails_search_bounding_box,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type {
|
||||
GPX,
|
||||
@@ -31,8 +36,8 @@
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
import { slide } from "svelte/transition";
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
let gpxLayers: Record<string, GPX> = {};
|
||||
@@ -85,6 +90,11 @@
|
||||
];
|
||||
|
||||
map.fitBounds(boundingBox);
|
||||
} else if ($currentUser && $currentUser.location) {
|
||||
map.setView(
|
||||
[$currentUser.location.lat, $currentUser.location.lon],
|
||||
12,
|
||||
);
|
||||
} else {
|
||||
navigator.geolocation.getCurrentPosition(
|
||||
(position) => {
|
||||
@@ -202,9 +212,9 @@
|
||||
<h4 class="font-semibold text-lg">${trail.name}</h4>
|
||||
<h5><i class="fa fa-location-dot mr-3"></i>${trail.location}</h5>
|
||||
<div class="flex mt-2 gap-4 text-sm text-gray-500"><span class="shrink-0"><i
|
||||
class="fa fa-left-right mr-2"></i>${formatMeters(
|
||||
class="fa fa-left-right mr-2"></i>${formatDistance(
|
||||
trail.distance,
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatMeters(
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-up-down mr-2"></i>${formatElevation(
|
||||
trail.elevation_gain,
|
||||
)}</span> <span class="shrink-0"><i class="fa fa-clock mr-2"></i>${formatTimeHHMM(
|
||||
trail.duration,
|
||||
@@ -239,6 +249,9 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("map")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
||||
<div
|
||||
id="trail-list"
|
||||
@@ -250,7 +263,7 @@
|
||||
extraClasses="w-full"
|
||||
on:update={(e) => search(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
placeholder="Search for trails, places..."
|
||||
placeholder="{$_("search-for-trails-places")}..."
|
||||
items={searchDropdownItems}
|
||||
></Search>
|
||||
<button
|
||||
|
||||
@@ -1,95 +1,81 @@
|
||||
<script lang="ts">
|
||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||
import Tabs from "$lib/components/tabs.svelte";
|
||||
import MapWithElevation from "$lib/components/trail/map_with_elevation.svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type { Map, Marker } from "leaflet";
|
||||
import type { Marker } from "leaflet";
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import type { DataSource } from "photoswipe/lightbox";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import "photoswipe/style.css";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let L: any;
|
||||
let map: Map;
|
||||
let markers: Marker[] = [];
|
||||
let markers: Marker[];
|
||||
|
||||
const tabs = ["Description", "Waypoints", "Photos", "Summit book"];
|
||||
let lightbox: PhotoSwipeLightbox;
|
||||
let lightboxDataSource: DataSource;
|
||||
|
||||
const tabs = [
|
||||
$_("description"),
|
||||
$_("waypoints"),
|
||||
$_("photos"),
|
||||
$_("summit-book"),
|
||||
];
|
||||
let activeTab = 0;
|
||||
|
||||
onMount(async () => {
|
||||
L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
await import("leaflet.awesome-markers");
|
||||
//@ts-ignore
|
||||
await import("$lib/vendor/leaflet-elevation/src/index.js");
|
||||
onMount(() => {
|
||||
lightboxDataSource = $trail.photos.map((p) => ({
|
||||
src: p,
|
||||
}));
|
||||
lightbox = new PhotoSwipeLightbox({
|
||||
dataSource: lightboxDataSource,
|
||||
pswpModule: async () => await import("photoswipe"),
|
||||
});
|
||||
lightbox.init();
|
||||
|
||||
map = L.map("map").setView([$trail.lat, $trail.lon], 14);
|
||||
|
||||
L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
attribution: "© OpenStreetMap contributors",
|
||||
}).addTo(map);
|
||||
|
||||
const elevation_options = {
|
||||
height: 200,
|
||||
|
||||
theme: "lightblue-theme",
|
||||
detached: true,
|
||||
elevationDiv: "#elevation",
|
||||
closeBtn: false,
|
||||
followMarker: true,
|
||||
autofitBounds: true,
|
||||
imperial: false,
|
||||
reverseCoords: false,
|
||||
acceleration: false,
|
||||
slope: true,
|
||||
speed: false,
|
||||
altitude: true,
|
||||
time: true,
|
||||
distance: true,
|
||||
// Summary track info style: "inline" || "multiline" || false
|
||||
summary: false,
|
||||
downloadLink: false,
|
||||
ruler: false,
|
||||
legend: true,
|
||||
// Toggle "leaflet-almostover" integration
|
||||
almostOver: true,
|
||||
// Toggle "leaflet-distance-markers" integration
|
||||
distanceMarkers: false,
|
||||
// Toggle "leaflet-edgescale" integration
|
||||
edgeScale: false,
|
||||
// Toggle "leaflet-hotline" integration
|
||||
hotline: true,
|
||||
// Display track datetimes: true || false
|
||||
timestamps: false,
|
||||
waypoints: false,
|
||||
wptIcons: false,
|
||||
wptLabels: false,
|
||||
preferCanvas: true,
|
||||
};
|
||||
|
||||
const controlElevation = L.control
|
||||
.elevation(elevation_options)
|
||||
.addTo(map);
|
||||
controlElevation.load(getFileURL($trail, $trail.gpx));
|
||||
|
||||
// addGPXLayer($trail);
|
||||
|
||||
for (const waypoint of $trail.expand.waypoints) {
|
||||
const marker = createMarkerFromWaypoint(L, waypoint);
|
||||
marker.addTo(map);
|
||||
markers.push(marker);
|
||||
}
|
||||
lightbox.on("beforeOpen", () => {
|
||||
const pswp = lightbox.pswp;
|
||||
const ds = pswp?.options?.dataSource;
|
||||
if (Array.isArray(ds)) {
|
||||
for (let idx = 0, len = ds.length; idx < len; idx++) {
|
||||
const item = ds[idx];
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
item.width = img.naturalWidth;
|
||||
item.height = img.naturalHeight;
|
||||
pswp?.refreshSlideContent(idx);
|
||||
};
|
||||
img.src = item.src as string;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function openMarkerPopup(i: number) {
|
||||
markers[i].openPopup();
|
||||
}
|
||||
|
||||
function openGallery(idx: number) {
|
||||
lightbox?.loadAndOpen(idx, lightboxDataSource);
|
||||
}
|
||||
</script>
|
||||
|
||||
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1">
|
||||
<svelte:head>
|
||||
<title>{$trail.name} | {$_("map")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="grid grid-cols-1 md:grid-cols-[452px_1fr] gap-x-1 gap-y-4">
|
||||
<div
|
||||
id="trail-details"
|
||||
class="md:overflow-y-auto md:overflow-x-hidden flex flex-col items-stretch gap-4 rounded-xl"
|
||||
@@ -104,7 +90,7 @@
|
||||
class="absolute bottom-0 w-full h-1/2 bg-gradient-to-b from-transparent to-black opacity-50"
|
||||
></div>
|
||||
<div
|
||||
class="flex absolute flex-wrap justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
|
||||
class="flex absolute justify-between items-end w-full bottom-8 left-0 px-8 gap-y-4"
|
||||
>
|
||||
<div class="text-white">
|
||||
<h4 class="text-4xl font-bold">
|
||||
@@ -115,30 +101,33 @@
|
||||
{$trail.location}
|
||||
</h3>
|
||||
</div>
|
||||
{#if $currentUser && $currentUser.id == $trail.author}
|
||||
<TrailDropdown trail={$trail} mode="map"></TrailDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
<section class="grid grid-cols-2 sm:grid-cols-4 gap-y-4 justify-around">
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Distance</span>
|
||||
<span class="text-gray-500">{$_('distance')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatMeters($trail.distance)}</span
|
||||
>{formatDistance($trail.distance)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Elevation gain</span>
|
||||
<span class="text-gray-500">{$_('elevation-gain')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatMeters($trail.elevation_gain)}</span
|
||||
>{formatElevation($trail.elevation_gain)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Est. duration</span>
|
||||
<span class="text-gray-500">{$_('est-duration')}</span>
|
||||
<span class="font-semibold"
|
||||
>{formatTimeHHMM($trail.duration)}</span
|
||||
>
|
||||
</div>
|
||||
{#if $trail.expand.category}
|
||||
<div class="flex flex-col items-center text-sm">
|
||||
<span class="text-gray-500">Category</span>
|
||||
<span class="text-gray-500">{$_('category')}</span>
|
||||
<span class="font-semibold"
|
||||
>{$trail.expand.category.name}</span
|
||||
>
|
||||
@@ -163,10 +152,13 @@
|
||||
</ul>
|
||||
{/if}
|
||||
{#if activeTab == 2}
|
||||
<div id="photo-gallery" class="">
|
||||
<div id="photo-gallery" class="space-y-4">
|
||||
{#each $trail.photos ?? [] as photo, i}
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
|
||||
<img
|
||||
class="rounded-xl cursor-pointer hover:scale-105 transition-transform"
|
||||
on:click={() => openGallery(i)}
|
||||
src={photo}
|
||||
alt=""
|
||||
/>
|
||||
@@ -182,18 +174,11 @@
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
<div id="map-container" class="flex flex-col">
|
||||
<div id="map" class="rounded-xl z-0 basis-full"></div>
|
||||
<div id="elevation"></div>
|
||||
</div>
|
||||
<MapWithElevation trail={$trail} bind:markers></MapWithElevation>
|
||||
</main>
|
||||
|
||||
<style>
|
||||
#map-container {
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
@media only screen and (min-width: 768px) {
|
||||
#map-container,
|
||||
#trail-details {
|
||||
height: calc(100vh - 124px);
|
||||
}
|
||||
|
||||
@@ -1,26 +1,35 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import RadioGroup, {
|
||||
type RadioItem,
|
||||
} from "$lib/components/base/radio_group.svelte";
|
||||
import Search, {
|
||||
type SearchItem,
|
||||
} from "$lib/components/base/search.svelte";
|
||||
import Select, {
|
||||
type SelectItem,
|
||||
} from "$lib/components/base/select.svelte";
|
||||
import TextField from "$lib/components/base/text_field.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import { ms } from "$lib/meilisearch";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import {
|
||||
currentUser,
|
||||
logout,
|
||||
users_delete,
|
||||
users_update,
|
||||
} from "$lib/stores/user_store";
|
||||
import { country_codes } from "$lib/util/country_code_util";
|
||||
import RadioGroup, {
|
||||
type RadioItem,
|
||||
} from "$lib/components/base/radio_group.svelte";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { onMount } from "svelte";
|
||||
import { _, locale } from "svelte-i18n";
|
||||
|
||||
const languages: SelectItem[] = [
|
||||
{ text: "English", value: "en" },
|
||||
{ text: "German", value: "de" },
|
||||
{ text: $_("english"), value: "en" },
|
||||
{ text: $_("german"), value: "de" },
|
||||
];
|
||||
|
||||
const units: RadioItem[] = [
|
||||
{ text: "Metric", value: "metric" },
|
||||
{ text: "Imperial", value: "imperial" },
|
||||
{ text: $_("metric"), value: "metric" },
|
||||
{ text: $_("imperial"), value: "imperial" },
|
||||
];
|
||||
|
||||
let selectedLanguage = "en";
|
||||
@@ -28,6 +37,13 @@
|
||||
let searchDropdownItems: SearchItem[] = [];
|
||||
let citySearchQuery: string = "";
|
||||
|
||||
let openConfirmModal: () => void;
|
||||
|
||||
onMount(() => {
|
||||
citySearchQuery = $currentUser?.location?.name ?? "";
|
||||
selectedLanguage = $currentUser?.language || "en";
|
||||
});
|
||||
|
||||
async function searchCities(q: string) {
|
||||
const result = await ms.index("cities500").search(q, { limit: 5 });
|
||||
searchDropdownItems = result.hits.map((h) => ({
|
||||
@@ -39,9 +55,58 @@
|
||||
}));
|
||||
}
|
||||
|
||||
async function handleSearchClick(item: SearchItem) {}
|
||||
async function handleSearchClick(item: SearchItem) {
|
||||
await users_update($currentUser!.id, {
|
||||
location: {
|
||||
name: item.value.name,
|
||||
lat: item.value.lat,
|
||||
lon: item.value.lon,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function handleAvatarSelection() {
|
||||
const files = (
|
||||
document.getElementById("avatarInput") as HTMLInputElement
|
||||
).files;
|
||||
|
||||
if (!files) {
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("avatar", files[0]);
|
||||
|
||||
await users_update($currentUser!.id, formData);
|
||||
}
|
||||
|
||||
async function handleUnitSelection(e: RadioItem) {
|
||||
await users_update($currentUser!.id, {
|
||||
unit: e.value as "imperial" | "metric",
|
||||
});
|
||||
}
|
||||
|
||||
async function handleLanguageSelection(value: "en" | "de") {
|
||||
locale.set(value);
|
||||
await users_update($currentUser!.id, {
|
||||
language: value,
|
||||
});
|
||||
}
|
||||
|
||||
async function deleteAccount() {
|
||||
await users_delete($currentUser!);
|
||||
logout();
|
||||
goto("/");
|
||||
}
|
||||
|
||||
function openFileBrowser() {
|
||||
document.getElementById("avatarInput")!.click();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("profile")} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[356px_1fr] max-w-4xl mx-4 md:mx-auto gap-x-8 items-start"
|
||||
>
|
||||
@@ -49,54 +114,75 @@
|
||||
<div
|
||||
class="flex flex-col items-center rounded-xl border border-input-border p-4"
|
||||
>
|
||||
<img
|
||||
class="rounded-full w-32 aspect-square"
|
||||
src={$currentUser.avatar ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt=""
|
||||
/>
|
||||
<div
|
||||
class="rounded-full w-32 aspect-square overflow-hidden relative group"
|
||||
>
|
||||
<img
|
||||
src={getFileURL($currentUser, $currentUser.avatar) ||
|
||||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
|
||||
alt="avatar"
|
||||
/>
|
||||
<button
|
||||
class="absolute top-0 w-32 aspect-square opacity-0 group-hover:opacity-100 flex justify-center items-center bg-black/50 focus:bg-black/60 text-white cursor-pointer transition-opacity"
|
||||
on:click={openFileBrowser}
|
||||
>
|
||||
<i class="fa fa-pen"></i>
|
||||
</button>
|
||||
<input
|
||||
type="file"
|
||||
name="avatar"
|
||||
id="avatarInput"
|
||||
accept="image/*"
|
||||
style="display: none;"
|
||||
on:change={handleAvatarSelection}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h3 class="text-2xl mt-4 font-semibold">{$currentUser.username}</h3>
|
||||
<h5 class="font-medium">{$currentUser.email}</h5>
|
||||
</div>
|
||||
<div class="space-y-6 rounded-xl p-4">
|
||||
<h3 class="text-2xl font-semibold">Settings</h3>
|
||||
<h3 class="text-2xl font-semibold">{$_("settings")}</h3>
|
||||
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Default Location</h5>
|
||||
<h5 class="font-medium mb-1">{$_("default-location")}</h5>
|
||||
<Search
|
||||
items={searchDropdownItems}
|
||||
placeholder="Search cities..."
|
||||
placeholder="{$_('search-cities')}..."
|
||||
bind:value={citySearchQuery}
|
||||
on:update={(e) => searchCities(e.detail)}
|
||||
on:click={(e) => handleSearchClick(e.detail)}
|
||||
></Search>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Language</h5>
|
||||
<Select items={languages} bind:value={selectedLanguage}
|
||||
<h5 class="font-medium mb-1">{$_("language")}</h5>
|
||||
<Select
|
||||
items={languages}
|
||||
bind:value={selectedLanguage}
|
||||
on:change={(e) => handleLanguageSelection(e.detail)}
|
||||
></Select>
|
||||
</div>
|
||||
<div>
|
||||
<h5 class="font-medium mb-1">Units</h5>
|
||||
<h5 class="font-medium mb-1">{$_("units")}</h5>
|
||||
<RadioGroup
|
||||
name="unit"
|
||||
items={units}
|
||||
selected={0}
|
||||
on:change={() => {}}
|
||||
selected={$currentUser.unit == "metric" ? 0 : 1}
|
||||
on:change={(e) => handleUnitSelection(e.detail)}
|
||||
></RadioGroup>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<h5 class="font-medium mb-1">Set new password</h5>
|
||||
<TextField placeholder="New password" label="" type="password"
|
||||
></TextField>
|
||||
<TextField placeholder="Repeat password" type="password"
|
||||
></TextField>
|
||||
</div>
|
||||
<hr class="border-input-border" />
|
||||
<div class="space-y-4">
|
||||
<h4 class="text-xl text-red-400">Danger zone</h4>
|
||||
<button class="btn-danger">Delete Account</button>
|
||||
<h4 class="text-xl text-red-400">{$_("danger-zone")}</h4>
|
||||
<button class="btn-danger" on:click={openConfirmModal}
|
||||
>{$_("delete-account")}</button
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</main>
|
||||
<ConfirmModal
|
||||
text={$_("account-delete-confirm")}
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteAccount}
|
||||
></ConfirmModal>
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
import { users_create, type User, login } from "$lib/stores/user_store";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
import { object, string } from "yup";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let loading: boolean = false;
|
||||
const { form, errors, handleChange, handleSubmit } = createForm<User>({
|
||||
initialValues: {
|
||||
id: "",
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
@@ -53,6 +55,9 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_('register')} | wanderer</title>
|
||||
</svelte:head>
|
||||
<main class="flex justify-center">
|
||||
<form
|
||||
class="login-panel max-w-md border border-input-border rounded-xl p-8 flex flex-col justify-center items-center gap-8 w-[28rem] mt-8"
|
||||
@@ -63,25 +68,25 @@
|
||||
{:else}
|
||||
<LogoTextTwoLineLight></LogoTextTwoLineLight>
|
||||
{/if}
|
||||
<h4 class="text-xl font-semibold">Save your adventures!</h4>
|
||||
<h4 class="text-xl font-semibold">{$_("slogan")}</h4>
|
||||
<div class="space-y-6 w-80">
|
||||
<TextField
|
||||
name="username"
|
||||
label="Username"
|
||||
label={$_("username")}
|
||||
bind:value={$form.username}
|
||||
on:change={handleChange}
|
||||
error={$errors.username}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="email"
|
||||
label="Email"
|
||||
label={$_("email")}
|
||||
bind:value={$form.email}
|
||||
on:change={handleChange}
|
||||
error={$errors.email}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="password"
|
||||
label="Password"
|
||||
label={$_("password")}
|
||||
type="password"
|
||||
bind:value={$form.password}
|
||||
on:change={handleChange}
|
||||
@@ -91,13 +96,12 @@
|
||||
primary={true}
|
||||
extraClasses={"min-w-full"}
|
||||
type="submit"
|
||||
{loading}>Register</Button
|
||||
{loading}>{$_("register")}</Button
|
||||
>
|
||||
</div>
|
||||
<span
|
||||
>Already have an account? <a
|
||||
class="text-blue-500 underline"
|
||||
href="/login">Login!</a
|
||||
>{$_("already-account")}
|
||||
<a class="text-blue-500 underline" href="/login">{$_("login")}!</a
|
||||
></span
|
||||
>
|
||||
</form>
|
||||
|
||||
@@ -22,7 +22,11 @@
|
||||
trails_update,
|
||||
} from "$lib/stores/trail_store";
|
||||
import { waypoint } from "$lib/stores/waypoint_store";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import { createForm } from "$lib/vendor/svelte-form-lib";
|
||||
@@ -32,6 +36,7 @@
|
||||
import "leaflet.awesome-markers/dist/leaflet.awesome-markers.css";
|
||||
import "leaflet/dist/leaflet.css";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
export let data: { trail: Trail };
|
||||
|
||||
@@ -350,15 +355,21 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title
|
||||
>{$form.id ? `${$form.name} | ${$_("edit")}` : $_("new-trail")} | wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<main class="grid grid-cols-1 md:grid-cols-[400px_1fr]">
|
||||
<form
|
||||
id="trail-form"
|
||||
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-none mt-8 md:mt-0"
|
||||
on:submit={handleSubmit}
|
||||
>
|
||||
<h3 class="text-xl font-semibold">Pick a trail</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("pick-a-trail")}</h3>
|
||||
<button class="btn-primary" type="button" on:click={openFileBrowser}
|
||||
>Upload GPX</button
|
||||
>{$_("upload-gpx")}</button
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
@@ -369,17 +380,18 @@
|
||||
on:change={handleFileSelection}
|
||||
/>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Basic Info</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("basic-info")}</h3>
|
||||
<div class="flex gap-4 justify-around">
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Distance</span>
|
||||
<span class="font-medium">{formatMeters($form.distance)}</span>
|
||||
<span>{$_('distance')}</span>
|
||||
<span class="font-medium">{formatDistance($form.distance)}</span
|
||||
>
|
||||
<input type="hidden" name="distance" value={$form.distance} />
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<span>{$_("elevation-gain")}</span>
|
||||
<span class="font-medium"
|
||||
>{formatMeters($form.elevation_gain)}</span
|
||||
>{formatElevation($form.elevation_gain)}</span
|
||||
>
|
||||
<input
|
||||
type="hidden"
|
||||
@@ -388,7 +400,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span>{$_("est-duration")}</span>
|
||||
<span class="font-medium">{formatTimeHHMM($form.duration)}</span
|
||||
>
|
||||
<input type="hidden" name="duration" value={$form.duration} />
|
||||
@@ -398,33 +410,34 @@
|
||||
</div>
|
||||
<TextField
|
||||
name="name"
|
||||
label="Name"
|
||||
label={$_("name")}
|
||||
on:change={handleChange}
|
||||
error={$errors.name}
|
||||
bind:value={$form.name}
|
||||
></TextField>
|
||||
<TextField
|
||||
name="location"
|
||||
label="Location"
|
||||
label={$_("location")}
|
||||
error={$errors.location}
|
||||
bind:value={$form.location}
|
||||
></TextField>
|
||||
<Textarea
|
||||
name="description"
|
||||
label="Describe your trail"
|
||||
label={$_("describe-your-trail")}
|
||||
bind:value={$form.description}
|
||||
></Textarea>
|
||||
{#if $form.expand.category}
|
||||
<Select
|
||||
name="category"
|
||||
label="Category"
|
||||
label={$_("category")}
|
||||
bind:value={$form.expand.category.id}
|
||||
items={$categories.map((c) => ({ text: c.name, value: c.id }))}
|
||||
></Select>
|
||||
{/if}
|
||||
<Toggle name="public" label="Public" bind:value={$form.public}></Toggle>
|
||||
<Toggle name="public" label={$_("public")} bind:value={$form.public}
|
||||
></Toggle>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Waypoints</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("waypoints")}</h3>
|
||||
<ul>
|
||||
{#each $form.expand.waypoints ?? [] as waypoint, i}
|
||||
<li on:mouseenter={() => openMarkerPopup(waypoint)}>
|
||||
@@ -441,10 +454,10 @@
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeWaypointModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Waypoint</button
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-waypoint")}</button
|
||||
>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Photos</h3>
|
||||
<h3 class="text-xl font-semibold">{$_("photos")}</h3>
|
||||
<div class="flex gap-4 max-w-full overflow-x-auto shrink-0">
|
||||
<button
|
||||
class="btn-secondary h-32 w-32 m-2 shrink-0 grow-0 basis-auto"
|
||||
@@ -471,7 +484,7 @@
|
||||
{/each}
|
||||
</div>
|
||||
<hr class="border-separator" />
|
||||
<h3 class="text-xl font-semibold">Summit Book</h3>
|
||||
<h3 class="text-xl font-semibold">{$_('summit-book')}</h3>
|
||||
<ul>
|
||||
{#each $form.expand.summit_logs ?? [] as log, i}
|
||||
<li>
|
||||
@@ -487,7 +500,7 @@
|
||||
class="btn-secondary"
|
||||
type="button"
|
||||
on:click={beforeSummitLogModalOpen}
|
||||
><i class="fa fa-plus mr-2"></i>Add Entry</button
|
||||
><i class="fa fa-plus mr-2"></i>{$_("add-entry")}</button
|
||||
>
|
||||
<hr class="border-separator" />
|
||||
<Button
|
||||
@@ -495,7 +508,7 @@
|
||||
large={true}
|
||||
type="submit"
|
||||
extraClasses="mb-2"
|
||||
{loading}>Save Trail</Button
|
||||
{loading}>{$_("save-trail")}</Button
|
||||
>
|
||||
</form>
|
||||
<div class="rounded-xl" id="map"></div>
|
||||
|
||||
@@ -1,23 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { goto } from "$app/navigation";
|
||||
import type { DropdownItem } from "$lib/components/base/dropdown.svelte";
|
||||
import Dropdown from "$lib/components/base/dropdown.svelte";
|
||||
import ConfirmModal from "$lib/components/confirm_modal.svelte";
|
||||
import ListSelectModal from "$lib/components/list/list_select_modal.svelte";
|
||||
import SummitLogCard from "$lib/components/summit_log/summit_log_card.svelte";
|
||||
import Tabs from "$lib/components/tabs.svelte";
|
||||
import TrailDropdown from "$lib/components/trail/trail_dropdown.svelte";
|
||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||
import type { List } from "$lib/models/list";
|
||||
import {
|
||||
lists_add_trail,
|
||||
lists_index,
|
||||
lists_remove_trail,
|
||||
} from "$lib/stores/list_store";
|
||||
import { show_toast } from "$lib/stores/toast_store";
|
||||
import { trail, trails_delete } from "$lib/stores/trail_store";
|
||||
import { trail } from "$lib/stores/trail_store";
|
||||
import { currentUser } from "$lib/stores/user_store";
|
||||
import { getFileURL } from "$lib/util/file_util";
|
||||
import { formatMeters, formatTimeHHMM } from "$lib/util/format_util";
|
||||
import {
|
||||
formatDistance,
|
||||
formatElevation,
|
||||
formatTimeHHMM,
|
||||
} from "$lib/util/format_util";
|
||||
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
|
||||
import "$lib/vendor/leaflet-elevation/src/index.css";
|
||||
import type { Icon, Map, Marker } from "leaflet";
|
||||
@@ -26,9 +20,15 @@
|
||||
import type { DataSource } from "photoswipe";
|
||||
import PhotoSwipeLightbox from "photoswipe/lightbox";
|
||||
import "photoswipe/style.css";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
const tabs = ["Description", "Waypoints", "Photos", "Summit book"];
|
||||
const tabs = [
|
||||
$_("description"),
|
||||
$_("waypoints"),
|
||||
$_("photos"),
|
||||
$_("summit-book"),
|
||||
];
|
||||
|
||||
let map: Map;
|
||||
|
||||
@@ -39,20 +39,6 @@
|
||||
let lightbox: PhotoSwipeLightbox;
|
||||
let lightboxDataSource: DataSource;
|
||||
|
||||
let openConfirmModal: () => void;
|
||||
let openListSelectModal: () => void;
|
||||
|
||||
const dropdownItems: DropdownItem[] = [
|
||||
{ text: "Show on map", value: "map", icon: "map" },
|
||||
{ text: "Directions", value: "direction", icon: "car" },
|
||||
...($trail.gpx
|
||||
? [{ text: "Download GPX", value: "download", icon: "download" }]
|
||||
: []),
|
||||
{ text: "Add to list", value: "list", icon: "bookmark" },
|
||||
{ text: "Edit", value: "edit", icon: "pen" },
|
||||
{ text: "Delete", value: "delete", icon: "trash" },
|
||||
];
|
||||
|
||||
onMount(async () => {
|
||||
const L = (await import("leaflet")).default;
|
||||
await import("leaflet-gpx");
|
||||
@@ -132,74 +118,16 @@
|
||||
lightbox?.loadAndOpen(idx, lightboxDataSource);
|
||||
}
|
||||
|
||||
async function handleDropdownClick(item: { text: string; value: any }) {
|
||||
if (item.value == "map") {
|
||||
goto(`/map/trail/${$trail.id!}`);
|
||||
} else if (item.value == "list") {
|
||||
openListSelectModal();
|
||||
} else if (item.value == "direction") {
|
||||
window
|
||||
.open(
|
||||
`https://www.google.com/maps/dir/Current+Location/${$trail.lat},${$trail.lon}`,
|
||||
"_blank",
|
||||
)
|
||||
?.focus();
|
||||
} else if (item.value == "download") {
|
||||
downloadURI(getFileURL($trail, $trail.gpx), $trail.gpx!);
|
||||
} else if (item.value == "edit") {
|
||||
goto(`/trail/edit/${$trail.id}`);
|
||||
} else if (item.value == "delete") {
|
||||
openConfirmModal();
|
||||
}
|
||||
}
|
||||
|
||||
function downloadURI(uri: string, name: string) {
|
||||
var link = document.createElement("a");
|
||||
link.setAttribute("download", name);
|
||||
link.href = uri;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
link.remove();
|
||||
}
|
||||
|
||||
async function deleteTrail() {
|
||||
trails_delete($trail).then(() => history.back());
|
||||
}
|
||||
|
||||
async function toggleMapFullScreen() {
|
||||
goto(`/map/trail/${$trail.id!}`);
|
||||
}
|
||||
|
||||
async function handleListSelection(list: List) {
|
||||
try {
|
||||
if (list.trails?.includes($trail.id!)) {
|
||||
await lists_remove_trail(list, $trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Removed trail from "${list.name}"`,
|
||||
});
|
||||
} else {
|
||||
await lists_add_trail(list, $trail);
|
||||
show_toast({
|
||||
type: "success",
|
||||
icon: "check",
|
||||
text: `Added trail to "${list.name}"`,
|
||||
});
|
||||
}
|
||||
await lists_index();
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
show_toast({
|
||||
type: "error",
|
||||
icon: "close",
|
||||
text: "Error adding trail to list.",
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$trail.name} | {$_("trail", { values: { n: 1 } })} | wanderer</title
|
||||
>
|
||||
</svelte:head>
|
||||
|
||||
<div
|
||||
class="trail-panel max-w-5xl mx-auto border border-input-border rounded-3xl overflow-hidden"
|
||||
>
|
||||
@@ -225,17 +153,7 @@
|
||||
</h3>
|
||||
</div>
|
||||
{#if $currentUser && $currentUser.id == $trail.author}
|
||||
<Dropdown
|
||||
items={dropdownItems}
|
||||
on:change={(e) => handleDropdownClick(e.detail)}
|
||||
let:toggleMenu={openDropdown}
|
||||
><button
|
||||
class="rounded-full bg-white text-black hover:bg-gray-200 focus:ring-4 ring-gray-100/50 transition-colors h-12 w-12"
|
||||
on:click={openDropdown}
|
||||
>
|
||||
<i class="fa fa-ellipsis-vertical"></i>
|
||||
</button></Dropdown
|
||||
>
|
||||
<TrailDropdown trail={$trail} mode="overview"></TrailDropdown>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
@@ -243,26 +161,26 @@
|
||||
class="grid grid-cols-2 sm:grid-cols-4 gap-y-4 justify-around py-8 border-b border-input-border"
|
||||
>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Distance</span>
|
||||
<span>{$_("distance")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatMeters($trail.distance)}</span
|
||||
>{formatDistance($trail.distance)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Elevation gain</span>
|
||||
<span>{$_("elevation-gain")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatMeters($trail.elevation_gain)}</span
|
||||
>{formatElevation($trail.elevation_gain)}</span
|
||||
>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Est. duration</span>
|
||||
<span>{$_("est-duration")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{formatTimeHHMM($trail.duration)}</span
|
||||
>
|
||||
</div>
|
||||
{#if $trail.expand.category}
|
||||
<div class="flex flex-col items-center">
|
||||
<span>Category</span>
|
||||
<span>{$_("category")}</span>
|
||||
<span class="font-semibold text-lg"
|
||||
>{$trail.expand.category.name}</span
|
||||
>
|
||||
@@ -334,15 +252,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<ConfirmModal
|
||||
text="Do you really want to delete this trail? This action cannot be undone."
|
||||
bind:openModal={openConfirmModal}
|
||||
on:confirm={deleteTrail}
|
||||
></ConfirmModal>
|
||||
<ListSelectModal
|
||||
bind:openModal={openListSelectModal}
|
||||
on:change={(e) => handleListSelection(e.detail)}
|
||||
></ListSelectModal>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import { categories } from "$lib/stores/category_store";
|
||||
import { trails, trails_search_filter } from "$lib/stores/trail_store";
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import { _ } from "svelte-i18n";
|
||||
|
||||
let filterExpanded: boolean = true;
|
||||
|
||||
@@ -23,6 +23,10 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>{$_("trail", { values: { n: 2 } })} | wanderer</title>
|
||||
</svelte:head>
|
||||
|
||||
<main
|
||||
class="grid grid-cols-1 md:grid-cols-[300px_1fr] gap-8 max-w-7xl mx-6 md:mx-auto"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user