adds i18n

This commit is contained in:
Christian Beutel
2024-02-29 23:26:18 +01:00
parent 528b443aa7
commit 5e19a4e8bb
45 changed files with 1520 additions and 638 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts">
export let id: string;
export let title: string;
export let size: string = "2xl";
export let size: string = "max-w-2xl";
export function openModal() {
document.body.style.position = "fixed";
document.body.style.width = "100%";
@@ -25,7 +25,7 @@
{id}
tabindex="-1"
aria-hidden="true"
class="relative w-full max-w-{size} max-h-full rounded-xl text-content"
class="relative w-full {size} max-h-full rounded-xl text-content"
>
<!-- Modal content -->
<div class="relative bg-background shadow">

View File

@@ -1,14 +1,15 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
export let title: string = "Confirm Deletion";
export let text: string;
const dispatch = createEventDispatcher()
const dispatch = createEventDispatcher();
function confirm() {
dispatch("confirm");
@@ -19,11 +20,11 @@
<Modal id="confirm-modal" {title} bind:openModal bind:closeModal>
<p slot="content">{text}</p>
<div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
<button
class="btn-danger"
type="button"
on:click={confirm}>Delete</button
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-danger" type="button" on:click={confirm}
>{$_("delete")}</button
>
</div></Modal
>

View File

@@ -1,4 +1,5 @@
<script lang="ts">
import { _ } from "svelte-i18n";
export let width: number = 256;
</script>
@@ -10,5 +11,5 @@
alt="Empty State showing a wanderer going into the distance"
/>
<h3 class="text-2xl font-semibold text-center">No results found</h3>
<h3 class="text-2xl font-semibold text-center">{$_("no-results")}</h3>
</div>

View File

@@ -1,5 +1,7 @@
<script>
import LogoTextLight from "./logo/logo_text_light.svelte";
import { _ } from "svelte-i18n";
</script>
<footer class="bg-footer-background text-white w-full grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-y-8 p-12 mt-12 rounded-t-3xl">
@@ -11,18 +13,18 @@
<div>
<h5 class="font-semibold">wanderer</h5>
<ul class="mt-4 text-sm">
<li>About</li>
<li>Features</li>
<li>Changelog</li>
<li>License</li>
<li>{$_('about')}</li>
<li>{$_('features')}</li>
<li>{$_('changelog')}</li>
<li><a href="https://github.com/Flomp/wanderer/blob/main/LICENSE">{$_('license')}</a></li>
</ul>
</div>
<div>
<h5 class="font-semibold">Community</h5>
<ul class="mt-4 text-sm">
<li>GitHub</li>
<li>Issues</li>
<li>Contribute</li>
<li><a href="https://github.com/Flomp/wanderer">GitHub</a></li>
<li><a href="https://github.com/Flomp/wanderer/issues">Issues</a></li>
<li><a href="https://github.com/Flomp/wanderer/pulls">{$_('contribute')}</a></li>
</ul>
</div>
</footer>

View File

@@ -1,12 +1,13 @@
<script lang="ts">
import type { List } from "$lib/models/list";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import { _ } from "svelte-i18n";
export let list: List;
export let active: boolean = false;
const dropdownItems: DropdownItem[] = [
{ text: "Edit", value: "edit" },
{ text: "Delete", value: "delete" },
{ text: $_("edit"), value: "edit" },
{ text: $_("delete"), value: "delete" },
];
</script>
@@ -21,7 +22,9 @@
alt="avatar"
/>
{:else}
<div class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center">
<div
class="flex w-16 md:w-24 aspect-square shrink-0 items-center justify-center"
>
<i class="fa fa-table-list text-5xl"></i>
</div>
{/if}

View File

@@ -6,10 +6,10 @@
import { list } from "$lib/stores/list_store";
import { createForm } from "$lib/vendor/svelte-form-lib/index";
import { util } from "$lib/vendor/svelte-form-lib/util";
import { _ } from "svelte-i18n";
import Modal from "../base/modal.svelte";
import TextField from "../base/text_field.svelte";
import Textarea from "../base/textarea.svelte";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
@@ -37,9 +37,8 @@
}
function handleAvatarSelection() {
const files = (
document.getElementById("avatar") as HTMLInputElement
).files;
const files = (document.getElementById("avatar") as HTMLInputElement)
.files;
if (!files) {
return;
@@ -55,7 +54,7 @@
<Modal
id="list-modal"
title="New List"
title={$form.id ? $_("edit-list") : $_("new-list")}
let:openModal
bind:openModal
bind:closeModal
@@ -67,7 +66,7 @@
class="modal-content space-y-4"
on:submit={handleSubmit}
>
<label for="avatar" class="text-sm font-medium block"> Avatar </label>
<label for="avatar" class="text-sm font-medium block"> {$_('avatar')} </label>
<input
name="avatar"
type="file"
@@ -87,13 +86,13 @@
<button
class="btn-secondary"
type="button"
on:click={openAvatarBrowser}>Change...</button
on:click={openAvatarBrowser}>{$_('change')}...</button
>
</div>
<TextField
name="name"
label="Name"
label={$_("name")}
bind:value={$form.name}
error={$errors.name}
on:change={handleChange}
@@ -101,14 +100,18 @@
<Textarea
name="description"
label="Description"
label={$_("description")}
bind:value={$form.description}
error={$errors.description}
on:change={handleChange}
></Textarea>
</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="list-form">Save</button>
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="list-form"
>{$_("save")}</button
>
</div>
</Modal>

View File

@@ -1,10 +1,11 @@
<script lang="ts">
import { createEventDispatcher, onMount } from "svelte";
import { createEventDispatcher } from "svelte";
import { lists, lists_index } from "$lib/stores/list_store";
import Modal from "../base/modal.svelte";
import type { List } from "$lib/models/list";
import { lists } from "$lib/stores/list_store";
import { trail } from "$lib/stores/trail_store";
import { _ } from "svelte-i18n";
import Modal from "../base/modal.svelte";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
@@ -23,8 +24,8 @@
<Modal
id="list-modal"
title="Select List"
size="md"
title={$_("select-list")}
size="max-w-sm"
let:openModal
bind:openModal
bind:closeModal

View File

@@ -3,22 +3,23 @@
import LogoText from "$lib/components/logo/logo_text.svelte";
import { theme, toggleTheme } from "$lib/stores/theme_store";
import { currentUser, logout } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { backInOut, cubicOut } from "svelte/easing";
import { tweened } from "svelte/motion";
import Drawer from "./base/drawer.svelte";
import Dropdown from "./base/dropdown.svelte";
import LogoTextLight from "./logo/logo_text_light.svelte";
import Drawer from "./base/drawer.svelte";
import { _, format } from "svelte-i18n";
const navBarItems = [
let navBarItems = [
{ text: "Home", value: "/" },
{ text: "Trails", value: "/trails" },
{ text: "Map", value: "/map" },
{ text: "Lists", value: "/lists" },
{ text: $_("trail", { values: { n: 2 } }), value: "/trails" },
{ text: $_("map"), value: "/map" },
];
const dropdownItems = [
{ text: "Profile", value: "profile", icon: "user" },
{ text: "Logout", value: "logout", icon: "right-from-bracket" },
{ text: $_("profile"), value: "profile", icon: "user" },
{ text: $_("logout"), value: "logout", icon: "right-from-bracket" },
];
const indicatorPosition = tweened(0, {
@@ -104,11 +105,19 @@
data-sveltekit-preload-data="off">{item.text}</a
>
{/each}
{#if $currentUser}
<a
class="font-semibold text-xl"
href="/lists"
data-sveltekit-preload-data="off"
>{$_("list", { values: { n: 2 } })}</a
>
{/if}
</div>
<hr class="my-6 border-input-border" />
<div class="flex flex-col basis-full">
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>New Trail</a
><i class="fa fa-plus mr-2"></i>{$_('new-trail')}</a
>
{#if $currentUser}
<div class="basis-full"></div>
@@ -116,9 +125,9 @@
<div class="flex gap-4 items-center m-4">
<img
class="rounded-full w-8 aspect-square"
src={$currentUser.avatar ||
src={getFileURL($currentUser, $currentUser.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
alt=""
alt="avatar"
/>
<div>
<p class="text-sm">{$currentUser.username}</p>
@@ -153,6 +162,14 @@
data-sveltekit-preload-data="off">{item.text}</a
>
{/each}
{#if $currentUser}
<a
class="font-semibold z-10"
href="/lists"
data-sveltekit-preload-data="off"
>{$_("list", { values: { n: 2 } })}</a
>
{/if}
</menu>
{#if $currentUser}
<div class="hidden md:flex gap-6 items-center">
@@ -163,7 +180,7 @@
on:click={() => toggleTheme()}
></button>
<a class="btn-primary btn-large" href="/trail/edit/new"
><i class="fa fa-plus mr-2"></i>New Trail</a
><i class="fa fa-plus mr-2"></i>{$_('new-trail')}</a
>
<Dropdown
items={dropdownItems}
@@ -176,8 +193,9 @@
>
<img
class="rounded-full"
src="https://api.dicebear.com/7.x/initials/svg?seed={$currentUser.username}&backgroundType=gradientLinear"
alt=""
src={getFileURL($currentUser, $currentUser.avatar) ||
`https://api.dicebear.com/7.x/initials/svg?seed=${$currentUser.username}&backgroundType=gradientLinear`}
alt="avatar"
/>
</button>
</Dropdown>
@@ -190,7 +208,7 @@
: 'moon'}"
on:click={() => toggleTheme()}
></button>
<a class="btn-primary btn-large" href="/login">Login</a>
<a class="btn-primary btn-large" href="/login">{$_('login')}</a>
</div>
{/if}
<button

View File

@@ -1,5 +1,6 @@
<script lang="ts">
import { createEventDispatcher } from "svelte";
import { _ } from "svelte-i18n";
export let src: string;
export let isThumbnail: boolean = false;
@@ -30,13 +31,13 @@
<button
type="button"
class="tooltip"
data-title="Make thumbnail"
data-title={$_("make-thumbnail")}
on:click={handleThumbnailClick}
><i class="fa fa-file-image text-primary"></i></button
>
<button
class="tooltip"
data-title="Delete"
data-title={$_("delete")}
on:click={handleDeleteClick}
type="button"><i class="fa fa-trash text-red-500"></i></button
>

View File

@@ -1,20 +1,21 @@
<script lang="ts">
import type { SummitLog } from "$lib/models/summit_log";
import { format, parse } from "date-fns";
import Dropdown from "../base/dropdown.svelte";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import { _ } from "svelte-i18n";
export let log: SummitLog;
export let mode: "show" | "edit" = "show";
const dropdownItems = [
{ text: "Edit", value: "edit" },
{ text: "Delete", value: "delete" },
const dropdownItems: DropdownItem[] = [
{ text: $_("edit"), value: "edit" },
{ text: $_("delete"), value: "delete" },
];
</script>
<div class="p-4 my-2 border border-input-border rounded-xl">
<div class="flex justify-between items-center mb-2">
<h5 class="font-medium mr-2">{format(log.date, 'dd.MM.yyyy')}</h5>
<h5 class="font-medium mr-2">{format(log.date, "dd.MM.yyyy")}</h5>
{#if mode == "edit"}
<Dropdown items={dropdownItems} on:change></Dropdown>

View File

@@ -4,11 +4,11 @@
import { summitLogSchema, type SummitLog } from "$lib/models/summit_log";
import { summitLog } from "$lib/stores/summit_log_store";
import { createForm } from "$lib/vendor/svelte-form-lib/index";
import { util } from "$lib/vendor/svelte-form-lib/util";
import { _ } from "svelte-i18n";
import Datepicker from "../base/datepicker.svelte";
import Modal from "../base/modal.svelte";
import TextField from "../base/text_field.svelte";
import { util } from "$lib/vendor/svelte-form-lib/util";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
@@ -17,17 +17,17 @@
const { form, errors, handleChange, handleSubmit } = createForm<SummitLog>({
initialValues: $summitLog,
validationSchema: summitLogSchema,
onSubmit: async (submittedValues) => {
onSubmit: async (submittedValues) => {
dispatch("save", submittedValues);
closeModal!();
},
});
$: form.set(util.cloneDeep($summitLog));
$: form.set(util.cloneDeep($summitLog));
</script>
<Modal
id="summit-log-modal"
title="Add Entry"
title={$form.id ? $_("edit-entry") : $_("add-entry")}
let:openModal
bind:openModal
bind:closeModal
@@ -42,7 +42,7 @@
<div class="flex gap-4">
<Datepicker
name="date"
label="Date"
label={$_("date")}
bind:value={$form.date}
error={$errors.date}
on:change={handleChange}
@@ -50,7 +50,7 @@
<div class="basis-full">
<TextField
name="text"
label="Text"
label={$_("text")}
bind:value={$form.text}
error={$errors.text}
on:change={handleChange}
@@ -59,9 +59,11 @@
</div>
</form>
<div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="summit-log-form"
>Save</button
>{$_("save")}</button
>
</div>
</Modal>

View File

@@ -0,0 +1,111 @@
<script lang="ts">
import type { Trail } from "$lib/models/trail";
import { currentUser } from "$lib/stores/user_store";
import { getFileURL } from "$lib/util/file_util";
import { createMarkerFromWaypoint } from "$lib/util/leaflet_util";
import type { Map, Marker } from "leaflet";
import { onMount } from "svelte";
export let trail: Trail;
export let markers: Marker[] = [];
let L: any;
let map: Map;
let controlElevation: any;
$: if (trail.expand.gpx_data && controlElevation) {
controlElevation.load(trail.expand.gpx_data);
}
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");
map = L.map("map").setView([trail.lat ?? 0, trail.lon ?? 0], 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: $currentUser?.unit == "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,
trkStart: {
icon: L.AwesomeMarkers.icon({
icon: "circle-half-stroke",
prefix: "fa",
markerColor: "cadetblue",
iconColor: "white",
}),
},
trkEnd: {
icon: L.AwesomeMarkers.icon({
icon: "flag-checkered",
prefix: "fa",
markerColor: "cadetblue",
iconColor: "white",
}),
},
};
controlElevation = L.control.elevation(elevation_options).addTo(map);
for (const waypoint of trail.expand.waypoints) {
const marker = createMarkerFromWaypoint(L, waypoint);
marker.addTo(map);
markers.push(marker);
}
});
</script>
<div id="map-container" class="flex flex-col">
<div id="map" class="rounded-xl z-0 basis-full"></div>
<div id="elevation"></div>
</div>
<style>
#map-container {
height: calc(100vh - 180px);
}
@media only screen and (min-width: 768px) {
#map-container {
height: calc(100vh - 124px);
}
}
</style>

View File

@@ -1,7 +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";
import { formatDistance, formatElevation, formatTimeHHMM } from "$lib/util/format_util";
export let trail: Trail;
@@ -23,12 +23,12 @@
</div>
<div class="flex mt-2 gap-4 text-sm text-gray-500 whitespace-nowrap">
<span
><i class="fa fa-left-right mr-2"></i>{formatMeters(
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatMeters(
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain,
)}</span
>

View File

@@ -0,0 +1,136 @@
<script lang="ts">
import { goto } from "$app/navigation";
import type { List } from "$lib/models/list";
import type { Trail } from "$lib/models/trail";
import {
lists_add_trail,
lists_index,
lists_remove_trail,
} from "$lib/stores/list_store";
import { show_toast } from "$lib/stores/toast_store";
import { trails_delete } from "$lib/stores/trail_store";
import { getFileURL } from "$lib/util/file_util";
import Dropdown, { type DropdownItem } from "../base/dropdown.svelte";
import ConfirmModal from "../confirm_modal.svelte";
import ListSelectModal from "../list/list_select_modal.svelte";
import { _ } from "svelte-i18n";
export let trail: Trail;
export let mode: "overview" | "map";
let openConfirmModal: () => void;
let openListSelectModal: () => void;
const dropdownItems: DropdownItem[] = [
mode == "overview"
? { text: $_("show-on-map"), value: "show", icon: "map" }
: {
text: $_("show-in-overview"),
value: "show",
icon: "table-columns",
},
{ 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" },
];
async function handleDropdownClick(item: { text: string; value: any }) {
if (item.value == "show") {
goto(
mode == "overview"
? `/map/trail/${trail.id!}`
: `/trail/view/${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 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>
<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
>
<ConfirmModal
text={$_("delete-trail-confirm")}
bind:openModal={openConfirmModal}
on:confirm={deleteTrail}
></ConfirmModal>
<ListSelectModal
bind:openModal={openListSelectModal}
on:change={(e) => handleListSelection(e.detail)}
></ListSelectModal>

View File

@@ -3,7 +3,7 @@
import type { Category } from "$lib/models/category";
import type { TrailFilter } from "$lib/models/trail";
import { country_codes } from "$lib/util/country_code_util";
import { formatMeters } from "$lib/util/format_util";
import { formatDistance, formatElevation } from "$lib/util/format_util";
import { createEventDispatcher } from "svelte";
import DoubleSlider from "../base/double_slider.svelte";
import type { RadioItem } from "../base/radio_group.svelte";
@@ -11,6 +11,7 @@
import Search, { type SearchItem } from "../base/search.svelte";
import Slider from "../base/slider.svelte";
import { slide } from "svelte/transition";
import { _ } from "svelte-i18n";
export let categories: Category[];
export let filterExpanded: boolean = true;
@@ -33,9 +34,9 @@
const dispatch = createEventDispatcher();
const radioGroupItems: RadioItem[] = [
{ text: "Completed", value: "completed" },
{ text: "Not completed", value: "not_completed" },
{ text: "No preference", value: "no_preference" },
{ text: $_("completed"), value: "completed" },
{ text: $_("not-completed"), value: "not_completed" },
{ text: $_("no-preference"), value: "no_preference" },
];
let searchDropdownItems: SearchItem[] = [];
@@ -112,7 +113,7 @@
<Search
bind:value={filter.q}
on:update={update}
placeholder="Search trails..."
placeholder="{$_('search-trails')}..."
></Search>
</div>
<button
@@ -128,7 +129,7 @@
{#if showTrailSearch}
<hr class="my-4 border-separator" />
{/if}
<p class="text-sm font-medium pb-4">Category</p>
<p class="text-sm font-medium pb-4">{$_("category")}</p>
{#each categories as category, i}
<div class="flex items-center mb-4">
<input
@@ -146,11 +147,11 @@
{/each}
<hr class="my-4 border-separator" />
{#if showCitySearch}
<p class="text-sm font-medium pb-4">Near</p>
<p class="text-sm font-medium pb-4">{$_("near")}</p>
<div class="mb-8">
<Search
items={searchDropdownItems}
placeholder="Search cities..."
placeholder="{$_('search-cities')}..."
bind:value={citySearchQuery}
on:update={(e) => searchCities(e.detail)}
on:click={(e) => handleSearchClick(e.detail)}
@@ -162,12 +163,12 @@
on:set={() => update()}
></Slider>
<p>
<span class="text-gray-500 text-sm">Radius:</span>
{formatMeters(filter.near.radius)}
<span class="text-gray-500 text-sm">{$_("radius")}:</span>
{formatDistance(filter.near.radius)}
</p>
<hr class="my-4 border-separator" />
{/if}
<p class="text-sm font-medium pb-4">Distance</p>
<p class="text-sm font-medium pb-4">{$_("distance")}</p>
<DoubleSlider
minValue={filter.distanceMin}
maxValue={filter.distanceMax}
@@ -176,11 +177,11 @@
on:set={() => update()}
></DoubleSlider>
<div class="flex justify-between">
<span>{formatMeters(filter.distanceMin)}</span>
<span>{formatMeters(filter.distanceMax)}</span>
<span>{formatDistance(filter.distanceMin)}</span>
<span>{formatDistance(filter.distanceMax)}</span>
</div>
<hr class="my-4 border-separator" />
<p class="text-sm font-medium pb-4">Elevation Gain</p>
<p class="text-sm font-medium pb-4">{$_("elevation-gain")}</p>
<DoubleSlider
minValue={filter.elevationGainMin}
maxValue={filter.elevationGainMax}
@@ -189,11 +190,11 @@
on:set={() => update()}
></DoubleSlider>
<div class="flex justify-between">
<span>{formatMeters(filter.elevationGainMin)}</span>
<span>{formatMeters(filter.elevationGainMax)}</span>
<span>{formatElevation(filter.elevationGainMin)}</span>
<span>{formatElevation(filter.elevationGainMax)}</span>
</div>
<hr class="my-4 border-separator" />
<p class="text-sm font-medium pb-4">Completed</p>
<p class="text-sm font-medium pb-4">{$_("completed")}</p>
<RadioGroup
name="completed"
items={radioGroupItems}

View File

@@ -5,13 +5,14 @@
import EmptyStateSearch from "../empty_states/empty_state_search.svelte";
import TrailCard from "./trail_card.svelte";
import TrailListItem from "./trail_list_item.svelte";
import { _ } from "svelte-i18n";
export let filter: TrailFilter;
export let trails: Trail[];
const displayOptions: SelectItem[] = [
{ text: "Cards", value: "cards" },
{ text: "List", value: "list" },
{ text: $_('card', {values: {n: 2}}), value: "cards" },
{ text: $_('list', {values: {n: 1}}), value: "list" },
];
let selectedDisplayOption = displayOptions[0].value;
@@ -19,10 +20,10 @@
let dispatch = createEventDispatcher();
const sortOptions: SelectItem[] = [
{ text: "Alphabetical", value: "name" },
{ text: "Creation date", value: "created" },
{ text: "Distance", value: "distance" },
{ text: "Elevation gain", value: "elevation_gain" },
{ text: $_('alphabetical'), value: "name" },
{ text: $_('creation-date'), value: "created" },
{ text: $_('distance'), value: "distance" },
{ text: $_('elevation-gain'), value: "elevation_gain" },
];
onMount(() => {
@@ -53,7 +54,7 @@
<div class="min-w-0">
<div class="flex items-start gap-8 justify-end mx-4">
<div>
<p class="text-sm text-gray-500 pb-2">Sort</p>
<p class="text-sm text-gray-500 pb-2">{$_('sort')}</p>
<div class="flex items-center gap-2">
<Select
bind:value={filter.sort}
@@ -70,7 +71,7 @@
</div>
</div>
<div>
<p class="text-sm text-gray-500 pb-2">Display as</p>
<p class="text-sm text-gray-500 pb-2">{$_('display-as')}</p>
<Select
bind:value={selectedDisplayOption}
@@ -91,6 +92,7 @@
class="max-w-full"
class:basis-full={selectedDisplayOption === "list"}
href="/trail/view/{trail.id}"
data-sveltekit-preload-data="off"
>
{#if selectedDisplayOption === "cards"}
<TrailCard {trail}></TrailCard>

View File

@@ -1,7 +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";
import { formatDistance, formatElevation, formatTimeHHMM } from "$lib/util/format_util";
export let trail: Trail;
</script>
@@ -21,12 +21,12 @@
<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
><i class="fa fa-left-right mr-2"></i>{formatMeters(
><i class="fa fa-left-right mr-2"></i>{formatDistance(
trail.distance,
)}</span
>
<span
><i class="fa fa-up-down mr-2"></i>{formatMeters(
><i class="fa fa-up-down mr-2"></i>{formatElevation(
trail.elevation_gain,
)}</span
>

View File

@@ -1,17 +1,20 @@
<script lang="ts">
import type { Waypoint } from "$lib/models/waypoint";
import { _ } from "svelte-i18n";
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" },
{ text: $_("edit"), value: "edit" },
{ text: $_("delete"), value: "delete" },
];
</script>
<div class="p-4 border border-input-border rounded-md my-2 hover:bg-menu-item-background-hover">
<div
class="p-4 border border-input-border rounded-md my-2 hover:bg-menu-item-background-hover"
>
<div class="flex justify-between items-center mb-2">
<h5>
<i class="fa fa-{waypoint.icon} mr-2"></i>{waypoint.name}

View File

@@ -5,10 +5,10 @@
import { waypoint } from "$lib/stores/waypoint_store";
import { createForm } from "$lib/vendor/svelte-form-lib/index";
import { util } from "$lib/vendor/svelte-form-lib/util";
import { _ } from "svelte-i18n";
import Modal from "../base/modal.svelte";
import TextField from "../base/text_field.svelte";
import Textarea from "../base/textarea.svelte";
export let openModal: (() => void) | undefined = undefined;
export let closeModal: (() => void) | undefined = undefined;
@@ -27,7 +27,7 @@
<Modal
id="waypoint-modal"
title="Add Waypoint"
title={$form.id ? $_("edit-waypoint") : $_("add-waypoint")}
let:openModal
bind:openModal
bind:closeModal
@@ -43,7 +43,7 @@
<div class="basis-full">
<TextField
name="name"
label="Name"
label={$_("name")}
bind:value={$form.name}
error={$errors.name}
on:change={handleChange}
@@ -52,7 +52,7 @@
<TextField
name="icon"
label="Icon"
label={$_("icon")}
bind:value={$form.icon}
icon={$form.icon}
error={$errors.icon}
@@ -62,7 +62,7 @@
<Textarea
name="description"
label="Description"
label={$_("description")}
bind:value={$form.description}
error={$errors.description}
on:change={handleChange}
@@ -70,14 +70,14 @@
<div class="flex gap-4">
<TextField
name="lat"
label="Latitude"
label={$_("latitude")}
bind:value={$form.lat}
error={$errors.lat}
on:change={handleChange}
></TextField>
<TextField
name="lon"
label="Longitude"
label={$_("longitude")}
bind:value={$form.lon}
error={$errors.lat}
on:change={handleChange}
@@ -85,9 +85,11 @@
</div>
</form>
<div slot="footer" class="flex items-center gap-4">
<button class="btn-secondary" on:click={closeModal}>Cancel</button>
<button class="btn-secondary" on:click={closeModal}
>{$_("cancel")}</button
>
<button class="btn-primary" type="submit" form="waypoint-form"
>Save</button
>{$_("save")}</button
>
</div>
</Modal>