adds i18n
This commit is contained in:
111
web/src/lib/components/trail/map_with_elevation.svelte
Normal file
111
web/src/lib/components/trail/map_with_elevation.svelte
Normal 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>
|
||||
@@ -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
|
||||
>
|
||||
|
||||
136
web/src/lib/components/trail/trail_dropdown.svelte
Normal file
136
web/src/lib/components/trail/trail_dropdown.svelte
Normal 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>
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user