adds new default state images

This commit is contained in:
Christian Beutel
2024-03-11 00:08:02 +01:00
parent a2819f3b45
commit 02c82d3e46
25 changed files with 191 additions and 98 deletions

View File

@@ -46,7 +46,7 @@ services:
build: ./web build: ./web
environment: environment:
<<: *cenv <<: *cenv
MEILI_API_TOKEN: MEILI_API_TOKEN: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhcGlLZXlVaWQiOiIwZDA0YTAzYy1iNzBjLTQzZTctOGUzMC00NjNiODFhM2UwY2YiLCJzZWFyY2hSdWxlcyI6eyJ0cmFpbHMiOnsiZmlsdGVyIjoicHVibGljPXRydWUifSwiY2l0aWVzNTAwIjp7fX0sImV4cCI6bnVsbH0.XhHwyRqtKjij2-oPvcYAL3uIFmB4lxUBUgVzhQqL1To
ORIGIN: http://localhost:8080 ORIGIN: http://localhost:8080
PUBLIC_POCKETBASE_URL: http://db:8090 PUBLIC_POCKETBASE_URL: http://db:8090
ports: ports:

View File

@@ -43,7 +43,7 @@ def init_indices():
def generate_public_token(): def generate_public_token():
search_key = client.get_keys().results[0] search_key = next(filter(lambda r: r.name == "Default Search API Key", client.get_keys().results))
search_rules = { search_rules = {
'trails': { 'trails': {
@@ -60,6 +60,6 @@ init_indices()
print("Indices initialized!") print("Indices initialized!")
token = generate_public_token() token = generate_public_token()
print("Generating public token...") print("Generating public token...")
print(f"PUBLIC_MEILISEARCH_API_TOKEN:{token}") print(f"MEILI_API_TOKEN: {token}")
print("Bootstrapping complete!") print("Bootstrapping complete!")

View File

@@ -12,7 +12,7 @@ export const handle: Handle = async ({ event, resolve }) => {
// validate the user existence and if the path is acceesible // validate the user existence and if the path is acceesible
if (!pb.authStore.model && isRouteProtected(url.pathname)) { if (!pb.authStore.model && isRouteProtected(url.pathname)) {
throw redirect(302, '/login'); throw redirect(302, '/login?r='+url.pathname);
} else if (pb.authStore.model && url.pathname === "/login") { } else if (pb.authStore.model && url.pathname === "/login") {
throw redirect(302, '/'); throw redirect(302, '/');
} }

View File

@@ -7,7 +7,7 @@
<img <img
style="width: {width}px" style="width: {width}px"
class="rounded-full aspect-square" class="rounded-full aspect-square"
src="/imgs/empty_state.webp" src="/imgs/empty_state.png"
alt="Empty State showing a wanderer going into the distance" alt="Empty State showing a wanderer going into the distance"
/> />

View File

@@ -78,7 +78,7 @@
function handleDropdownClick(item: { text: string; value: any }) { function handleDropdownClick(item: { text: string; value: any }) {
if (item.value == "logout") { if (item.value == "logout") {
logout(); logout();
goto("/"); window.location.href = "/";
} else if (item.value == "profile") { } else if (item.value == "profile") {
goto("/profile"); goto("/profile");
} }
@@ -117,7 +117,7 @@
<hr class="my-6 border-input-border" /> <hr class="my-6 border-input-border" />
<div class="flex flex-col basis-full"> <div class="flex flex-col basis-full">
<a class="btn-primary btn-large text-center mx-4" href="/trail/edit/new" <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} {#if $currentUser}
<div class="basis-full"></div> <div class="basis-full"></div>
@@ -180,7 +180,7 @@
on:click={() => toggleTheme()} on:click={() => toggleTheme()}
></button> ></button>
<a class="btn-primary btn-large" href="/trail/edit/new" <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 <Dropdown
items={dropdownItems} items={dropdownItems}
@@ -208,7 +208,7 @@
: 'moon'}" : 'moon'}"
on:click={() => toggleTheme()} on:click={() => toggleTheme()}
></button> ></button>
<a class="btn-primary btn-large" href="/login">{$_('login')}</a> <a class="btn-primary btn-large" href="/login">{$_("login")}</a>
</div> </div>
{/if} {/if}
<button <button

View File

@@ -8,6 +8,10 @@
} from "$lib/util/format_util"; } from "$lib/util/format_util";
export let trail: Trail; export let trail: Trail;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp";
</script> </script>
<div <div
@@ -17,7 +21,7 @@
role="listitem" role="listitem"
> >
<div class="w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"> <div class="w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl">
<img src={getFileURL(trail, trail.photos[trail.thumbnail])} alt="" /> <img src={thumbnail} alt="" />
</div> </div>
<div class="p-4"> <div class="p-4">
<div> <div>

View File

@@ -97,7 +97,7 @@
<div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full"> <div id="trails" class="flex items-start flex-wrap gap-8 py-8 max-w-full">
{#if trails.length == 0} {#if trails.length == 0}
<div class="flex flex-col basis-full items-center"> <div class="flex flex-col basis-full items-center">
<EmptyStateSearch></EmptyStateSearch> <EmptyStateSearch width={356}></EmptyStateSearch>
</div> </div>
{/if} {/if}
{#each trails as trail} {#each trails as trail}

View File

@@ -1,20 +1,24 @@
<script lang="ts"> <script lang="ts">
import type { Trail } from "$lib/models/trail"; import type { Trail } from "$lib/models/trail";
import { getFileURL } from "$lib/util/file_util"; import { getFileURL } from "$lib/util/file_util";
import { formatDistance, formatElevation, formatTimeHHMM } from "$lib/util/format_util"; import {
formatDistance,
formatElevation,
formatTimeHHMM,
} from "$lib/util/format_util";
export let trail: Trail; export let trail: Trail;
$: thumbnail = trail.photos.length
? getFileURL(trail, trail.photos[trail.thumbnail])
: "/imgs/default_thumbnail.webp";
</script> </script>
<li <li
class="flex gap-8 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors" class="flex gap-8 p-4 rounded-xl border border-input-border cursor-pointer hover:bg-secondary-hover transition-colors"
> >
<div class="shrink-0"> <div class="shrink-0">
<img <img class="h-28 w-28 object-cover rounded-xl" src={thumbnail} alt="" />
class="h-28 w-28 object-cover rounded-xl"
src={getFileURL(trail, trail.photos[trail.thumbnail])}
alt=""
/>
</div> </div>
<div class="min-w-0 basis-full"> <div class="min-w-0 basis-full">
<h4 class="font-semibold text-lg">{trail.name}</h4> <h4 class="font-semibold text-lg">{trail.name}</h4>

View File

@@ -28,7 +28,7 @@
"delete-trail-confirm": "Möchtest du diese Route wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.", "delete-trail-confirm": "Möchtest du diese Route wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.",
"describe-your-trail": "Beschreibe deine Route", "describe-your-trail": "Beschreibe deine Route",
"description": "Beschreibung", "description": "Beschreibung",
"difficult": "Schwierig", "difficult": "Schwer",
"difficulty": "Schwierigkeit", "difficulty": "Schwierigkeit",
"directions": "Wegbeschreibung", "directions": "Wegbeschreibung",
"display-as": "Anzeigen als", "display-as": "Anzeigen als",
@@ -42,16 +42,21 @@
"elevation-gain": "Höhenunterschied", "elevation-gain": "Höhenunterschied",
"email": "Email", "email": "Email",
"english": "Englisch", "english": "Englisch",
"error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-during-login": "Fehler beim Login",
"est-duration": "Gesch. Dauer", "est-duration": "Gesch. Dauer",
"explore": "Erkunden", "explore": "Erkunden",
"explore-some-trails": "Erkunden Sie einige Routen", "explore-some-trails": "Erkunden Sie einige Routen",
"features": "Feature", "features": "Feature",
"german": "Deutsch", "german": "Deutsch",
"hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!", "hero_section_0_text": "Entdecke spannende Routen, speichere deine Favoriten und erlebe die Schönheit der Natur. Finde dein nächstes Abenteuer!",
"hero_section_1_heading": "Hier gibt es noch keine Routen",
"hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.", "hero_section_1_text": "Hier sind einige Routen, die dir gefallen könnten. Oder du wirfst einen Blick auf die vollständige Liste.",
"hero_section_1_text_alternative": "Speichere dein letztes Abenteuer, um loszulegen.",
"hero_section_2_text": "Wusstest du schon? Du kannst nicht nur deine Routen speichern. Es gibt viele Kategorien für alle deine Abenteuer.", "hero_section_2_text": "Wusstest du schon? Du kannst nicht nur deine Routen speichern. Es gibt viele Kategorien für alle deine Abenteuer.",
"icon": "Icon", "icon": "Icon",
"imperial": "Amerikanisch", "imperial": "Amerikanisch",
"invalid-username": "Ungültiger Nutzername",
"language": "Sprache", "language": "Sprache",
"latitude": "Breitengrad", "latitude": "Breitengrad",
"license": "Lizenz", "license": "Lizenz",
@@ -65,6 +70,7 @@
"map": "Karte", "map": "Karte",
"metric": "Metrisch", "metric": "Metrisch",
"moderate": "Mittel", "moderate": "Mittel",
"must-be-at-least-8-characters-long": "Muss mindestens 8 Zeichen lang sein",
"name": "Name", "name": "Name",
"near": "Nahe", "near": "Nahe",
"new-list": "Neue Liste", "new-list": "Neue Liste",
@@ -72,6 +78,7 @@
"no-account": "Du hast noch kein Konto?", "no-account": "Du hast noch kein Konto?",
"no-preference": "Keine Präferenz", "no-preference": "Keine Präferenz",
"no-results": "Keine Ergebnisse gefunden", "no-results": "Keine Ergebnisse gefunden",
"not-a-valid-email-address": "Keine gültige Email-Adresse",
"not-completed": "Nicht abgeschlossen", "not-completed": "Nicht abgeschlossen",
"password": "Passwort", "password": "Passwort",
"photos": "Fotos", "photos": "Fotos",
@@ -81,6 +88,7 @@
"radius": "Radius", "radius": "Radius",
"register": "Registrieren", "register": "Registrieren",
"removed-trail-from": "Route entfernt aus", "removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",
"save": "Speichern", "save": "Speichern",
"save-trail": "Route speichern", "save-trail": "Route speichern",
"search-cities": "Städte suchen", "search-cities": "Städte suchen",
@@ -100,5 +108,6 @@
"upload-gpx": "GPX hochladen", "upload-gpx": "GPX hochladen",
"username": "Nutzername", "username": "Nutzername",
"waypoints": "Wegpunkte", "waypoints": "Wegpunkte",
"welcome_to": "Willkommen bei" "welcome_to": "Willkommen bei",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort"
} }

View File

@@ -42,16 +42,21 @@
"elevation-gain": "Elevation Gain", "elevation-gain": "Elevation Gain",
"email": "Email", "email": "Email",
"english": "English", "english": "English",
"error-creating-user": "Error creating user",
"error-during-login": "Error during login",
"est-duration": "Est. duration", "est-duration": "Est. duration",
"explore": "Explore", "explore": "Explore",
"explore-some-trails": "Explore some trails", "explore-some-trails": "Explore some trails",
"features": "Features", "features": "Features",
"german": "German", "german": "German",
"hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!", "hero_section_0_text": "Explore exciting trails, save your favorites, and experience the beauty of nature. Find your next adventure!",
"hero_section_1_heading": "It seems there are no trails here yet.",
"hero_section_1_text": "Here are some trails you might like. Or you can just go to the full list right now.", "hero_section_1_text": "Here are some trails you might like. Or you can just go to the full list right now.",
"hero_section_1_text_alternative": "Get started by saving your latest adventure.",
"hero_section_2_text": "Did you know? You cannot only save you hiking trails. There are many categories for all your adventures.", "hero_section_2_text": "Did you know? You cannot only save you hiking trails. There are many categories for all your adventures.",
"icon": "Icon", "icon": "Icon",
"imperial": "Imperial", "imperial": "Imperial",
"invalid-username": "Invalid username",
"language": "Language", "language": "Language",
"latitude": "Latitude", "latitude": "Latitude",
"license": "License", "license": "License",
@@ -65,6 +70,7 @@
"map": "Map", "map": "Map",
"metric": "Metric", "metric": "Metric",
"moderate": "Moderate", "moderate": "Moderate",
"must-be-at-least-8-characters-long": "Must be at least 8 characters long",
"name": "Name", "name": "Name",
"near": "Near", "near": "Near",
"new-list": "New List", "new-list": "New List",
@@ -72,6 +78,7 @@
"no-account": "Don't have an account?", "no-account": "Don't have an account?",
"no-preference": "No preference", "no-preference": "No preference",
"no-results": "No results found", "no-results": "No results found",
"not-a-valid-email-address": "Not a valid email address",
"not-completed": "Not completed", "not-completed": "Not completed",
"password": "Password", "password": "Password",
"photos": "Photos", "photos": "Photos",
@@ -81,6 +88,7 @@
"radius": "Radius", "radius": "Radius",
"register": "Register", "register": "Register",
"removed-trail-from": "Removed trail from", "removed-trail-from": "Removed trail from",
"required": "Required",
"save": "Save", "save": "Save",
"save-trail": "Save Trail", "save-trail": "Save Trail",
"search-cities": "Search cities", "search-cities": "Search cities",
@@ -100,5 +108,6 @@
"upload-gpx": "Upload GPX", "upload-gpx": "Upload GPX",
"username": "Username", "username": "Username",
"waypoints": "Waypoints", "waypoints": "Waypoints",
"welcome_to": "Welcome to" "welcome_to": "Welcome to",
"wrong-username-or-password": "Wrong username or password"
} }

View File

@@ -1,4 +1,3 @@
import { array, number, object, string } from "yup";
import type { Category } from "./category"; import type { Category } from "./category";
import type { SummitLog } from "./summit_log"; import type { SummitLog } from "./summit_log";
import type { Waypoint } from "./waypoint"; import type { Waypoint } from "./waypoint";
@@ -11,7 +10,7 @@ class Trail {
distance?: number; distance?: number;
elevation_gain?: number; elevation_gain?: number;
duration?: number; duration?: number;
difficulty?: "easy"|"moderate"|"difficult" difficulty?: "easy" | "moderate" | "difficult"
lat?: number; lat?: number;
lon?: number; lon?: number;
thumbnail: number; thumbnail: number;
@@ -39,8 +38,8 @@ class Trail {
distance?: number, distance?: number,
elevation_gain?: number, elevation_gain?: number,
duration?: number, duration?: number,
difficulty?: "easy"|"moderate"|"difficult", difficulty?: "easy" | "moderate" | "difficult",
lat?:number, lat?: number,
lon?: number, lon?: number,
thumbnail?: number, thumbnail?: number,
photos?: string[], photos?: string[],
@@ -66,7 +65,7 @@ class Trail {
this.lon = params?.lon; this.lon = params?.lon;
this.thumbnail = params?.thumbnail ?? 0; this.thumbnail = params?.thumbnail ?? 0;
this.photos = params?.photos ?? []; this.photos = params?.photos ?? [];
this.waypoints = []; this.waypoints = [];
this.summit_logs = []; this.summit_logs = [];
this.gpx = params?.gpx; this.gpx = params?.gpx;
this.expand = { this.expand = {
@@ -80,19 +79,6 @@ class Trail {
} }
} }
const trailSchema = object<Trail>({
id: string().optional(),
name: string().required("Required"),
location: string().optional(),
distance: number().optional(),
elevation_gain: number().optional(),
duration: number().optional(),
thumbnail: string().optional(),
photos: array(string()).optional(),
gpx: string().optional(),
description: string().optional()
});
interface TrailFilter { interface TrailFilter {
q: string, q: string,
category: string[], category: string[],
@@ -111,7 +97,6 @@ interface TrailFilter {
sortOrder: "+" | "-" sortOrder: "+" | "-"
} }
export { Trail, trailSchema }; export { Trail };
export type { TrailFilter }; export type { TrailFilter };

View File

@@ -92,6 +92,8 @@ export async function trails_search_bounding_box(northEast: LatLng, southWest: L
if (filter) { if (filter) {
filterText += `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`; filterText += `distance >= ${filter.distanceMin} AND distance <= ${filter.distanceMax} AND elevation_gain >= ${filter.elevationGainMin} AND elevation_gain <= ${filter.elevationGainMax}`;
filterText += ` AND difficulty IN [${filter.difficulty.join(",")}]`
if (filter.category.length > 0) { if (filter.category.length > 0) {
filterText += ` AND category IN [${filter.category.join(",")}]`; filterText += ` AND category IN [${filter.category.join(",")}]`;
} }

View File

@@ -1,6 +1,7 @@
const privateRoutes = [ const privateRoutes = [
"/profile", "/profile",
"/lists" "/lists",
"/trail",
] ]
export function isRouteProtected(path: string) { export function isRouteProtected(path: string) {

View File

@@ -13,7 +13,7 @@
beforeNavigate((n) => { beforeNavigate((n) => {
if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) { if (!$currentUser && isRouteProtected(n.to?.url?.pathname ?? "")) {
n.cancel(); n.cancel();
goto("/login"); goto("/login?r="+n.to?.url?.pathname);
} }
}); });
</script> </script>

View File

@@ -108,6 +108,16 @@
id="trails" id="trails"
class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none" class="flex flex-wrap justify-items-center gap-8 py-8 order-1 md:order-none"
> >
{#if $trails.length == 0}
<div>
<img
style="max-width: 450px"
class="rounded-full aspect-square"
src="/imgs/default_thumbnail.webp"
alt="Empty State showing a wanderer going into the distance"
/>
</div>
{/if}
{#each { length: Math.min($trails.length, 4) } as _, i} {#each { length: Math.min($trails.length, 4) } as _, i}
<a href="/trail/view/{$trails[i].id}"> <a href="/trail/view/{$trails[i].id}">
<TrailCard trail={$trails[i]}></TrailCard></a <TrailCard trail={$trails[i]}></TrailCard></a
@@ -115,18 +125,33 @@
{/each} {/each}
</div> </div>
<div class="max-w-md md:mx-auto space-y-8"> <div class="max-w-md md:mx-auto space-y-8">
<h2 class="text-4xl md:text-5xl font-bold"> {#if $trails.length == 0}
{$currentUser ? $_("trails-for-you") : $_("explore-some-trails")} <h2 class="text-4xl md:text-5xl font-bold">
</h2> {$_("hero_section_1_heading")}
<h5> </h2>
{$_("hero_section_1_text")} <h5>{$_("hero_section_1_text_alternative")}</h5>
</h5> <a
<a class="inline-block btn-primary btn-large"
class="inline-block btn-primary btn-large" href="/trail/edit/new"
href="/trails" data-sveltekit-preload-data="off"
data-sveltekit-preload-data="off" role="button">{$_("new-trail")}</a
role="button">{$_("explore")}</a >
> {:else}
<h2 class="text-4xl md:text-5xl font-bold">
{$currentUser
? $_("trails-for-you")
: $_("explore-some-trails")}
</h2>
<h5>
{$_("hero_section_1_text")}
</h5>
<a
class="inline-block btn-primary btn-large"
href="/trails"
data-sveltekit-preload-data="off"
role="button">{$_("explore")}</a
>
{/if}
</div> </div>
</section> </section>
<section <section

View File

@@ -11,6 +11,7 @@
import { object, string } from "yup"; import { object, string } from "yup";
import { theme } from "$lib/stores/theme_store"; import { theme } from "$lib/stores/theme_store";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { page } from "$app/stores";
let loading: boolean = false; let loading: boolean = false;
const { form, errors, handleChange, handleSubmit } = createForm<User>({ const { form, errors, handleChange, handleSubmit } = createForm<User>({
@@ -20,14 +21,14 @@
password: "", password: "",
}, },
validationSchema: object<User>({ validationSchema: object<User>({
username: string().required("Required"), username: string().required($_("required")),
password: string().required("Required"), password: string().required($_("required")),
}), }),
onSubmit: async (newUser) => { onSubmit: async (newUser) => {
loading = true; loading = true;
try { try {
await login(newUser); await login(newUser);
goto(`/`); goto($page.url.searchParams.get("r") ?? "/");
} catch (e) { } catch (e) {
if ( if (
e instanceof ClientResponseError && e instanceof ClientResponseError &&
@@ -36,13 +37,13 @@
show_toast({ show_toast({
icon: "close", icon: "close",
type: "error", type: "error",
text: "Wrong username or password.", text: $_('wrong-username-or-password'),
}); });
} else { } else {
show_toast({ show_toast({
icon: "close", icon: "close",
type: "error", type: "error",
text: "Error during login.", text: $_('error-during-login'),
}); });
} }
} finally { } finally {
@@ -90,9 +91,9 @@
> >
</div> </div>
<span <span
>{$_('no-account')} <a >{$_("no-account")}
class="text-blue-500 underline" <a class="text-blue-500 underline" href="/register"
href="/register">{$_('make-one')}</a >{$_("make-one")}</a
></span ></span
> >
</form> </form>

View File

@@ -47,7 +47,7 @@
let showFilter: boolean = false; let showFilter: boolean = false;
let showMap: boolean = true; let showMap: boolean = true;
let filter: TrailFilter; const filter: TrailFilter = $page.data.filter;
onMount(async () => { onMount(async () => {
L = (await import("leaflet")).default; L = (await import("leaflet")).default;
@@ -130,20 +130,26 @@
const response = await r.json(); const response = await r.json();
const trailItems = response.results[0].hits.map((t: Record<string, any>) => ({ const trailItems = response.results[0].hits.map(
text: t.name, (t: Record<string, any>) => ({
description: `Trail | ${t.location}`, text: t.name,
value: t, description: `Trail | ${t.location}`,
icon: "route", value: t,
})); icon: "route",
const cityItems = response.results[1].hits.map((c: Record<string, any>) => ({ }),
text: c.name, );
description: `City | ${ const cityItems = response.results[1].hits.map(
country_codes[c["country code"] as keyof typeof country_codes] (c: Record<string, any>) => ({
}`, text: c.name,
value: c, description: `City | ${
icon: "city", country_codes[
})); c["country code"] as keyof typeof country_codes
]
}`,
value: c,
icon: "city",
}),
);
searchDropdownItems = [...trailItems, ...cityItems]; searchDropdownItems = [...trailItems, ...cityItems];
} }
@@ -291,7 +297,7 @@
categories={$categories} categories={$categories}
showTrailSearch={false} showTrailSearch={false}
showCitySearch={false} showCitySearch={false}
bind:filter {filter}
on:update={(e) => handleFilterUpdate(e.detail)} on:update={(e) => handleFilterUpdate(e.detail)}
></TrailFilterPanel> ></TrailFilterPanel>
</div> </div>

View File

@@ -1,9 +1,27 @@
import type { TrailFilter } from "$lib/models/trail";
import { categories_index } from "$lib/stores/category_store"; import { categories_index } from "$lib/stores/category_store";
import { trails } from "$lib/stores/trail_store"; import { trails } from "$lib/stores/trail_store";
import type { ServerLoad } from "@sveltejs/kit"; import type { ServerLoad } from "@sveltejs/kit";
export const load: ServerLoad = async ({ params, locals, fetch }) => { export const load: ServerLoad = async ({ params, locals, fetch }) => {
const filter: TrailFilter = {
q: "",
category: [],
difficulty: ["easy", "moderate", "difficult"],
near: {
radius: 2000,
},
distanceMin: 0,
distanceMax: 20000,
elevationGainMin: 0,
elevationGainMax: 4000,
sort: "created",
sortOrder: "+",
};
await categories_index(fetch) await categories_index(fetch)
trails.set([]) trails.set([])
return { filter: filter }
}; };

View File

@@ -35,6 +35,10 @@
]; ];
let activeTab = 0; let activeTab = 0;
const thumbnail = $trail.photos.length
? getFileURL(trail, $trail.photos[$trail.thumbnail])
: "/imgs/default_thumbnail.webp";
onMount(() => { onMount(() => {
lightboxDataSource = $trail.photos.map((p) => ({ lightboxDataSource = $trail.photos.map((p) => ({
src: getFileURL($trail, p), src: getFileURL($trail, p),
@@ -83,7 +87,7 @@
<section class="relative h-80"> <section class="relative h-80">
<img <img
class="w-full h-80 object-cover" class="w-full h-80 object-cover"
src={getFileURL($trail, $trail.photos[$trail.thumbnail])} src={thumbnail}
alt="" alt=""
/> />
<div <div

View File

@@ -6,11 +6,10 @@
import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte"; import LogoTextTwoLineLight from "$lib/components/logo/logo_text_two_line_light.svelte";
import { theme } from "$lib/stores/theme_store"; import { theme } from "$lib/stores/theme_store";
import { show_toast } from "$lib/stores/toast_store"; import { show_toast } from "$lib/stores/toast_store";
import { users_create, type User, login } from "$lib/stores/user_store"; import { login, users_create, type User } from "$lib/stores/user_store";
import { createForm } from "$lib/vendor/svelte-form-lib"; import { createForm } from "$lib/vendor/svelte-form-lib";
import { object, string } from "yup";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { object, string } from "yup";
let loading: boolean = false; let loading: boolean = false;
const { form, errors, handleChange, handleSubmit } = createForm<User>({ const { form, errors, handleChange, handleSubmit } = createForm<User>({
initialValues: { initialValues: {
@@ -20,11 +19,15 @@
password: "", password: "",
}, },
validationSchema: object<User>({ validationSchema: object<User>({
username: string().required("Required"), username: string()
email: string().email().required("Required"), .required($_("required"))
.matches(/^[\w][\w\.]*$/, { message: $_("invalid-username") }),
email: string()
.email($_("not-a-valid-email-address"))
.required($_("required")),
password: string() password: string()
.min(8, "Must be at least 8 characters long") .min(8, $_("must-be-at-least-8-characters-long"))
.required("Required"), .required($_("required")),
}), }),
onSubmit: async (newUser) => { onSubmit: async (newUser) => {
loading = true; loading = true;
@@ -34,8 +37,9 @@
show_toast({ show_toast({
icon: "close", icon: "close",
type: "error", type: "error",
text: "Error creating user.", text: $_("error-creating-user"),
}); });
return;
} finally { } finally {
loading = false; loading = false;
} }
@@ -46,7 +50,7 @@
show_toast({ show_toast({
icon: "close", icon: "close",
type: "error", type: "error",
text: "Error during login.", text: $_("error-during-login"),
}); });
} finally { } finally {
loading = false; loading = false;
@@ -56,7 +60,7 @@
</script> </script>
<svelte:head> <svelte:head>
<title>{$_('register')} | wanderer</title> <title>{$_("register")} | wanderer</title>
</svelte:head> </svelte:head>
<main class="flex justify-center"> <main class="flex justify-center">
<form <form

View File

@@ -10,7 +10,7 @@
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte"; import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte"; import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
import { SummitLog } from "$lib/models/summit_log"; import { SummitLog } from "$lib/models/summit_log";
import { Trail, trailSchema } from "$lib/models/trail"; import { Trail } from "$lib/models/trail";
import { Waypoint } from "$lib/models/waypoint"; import { Waypoint } from "$lib/models/waypoint";
import { categories } from "$lib/stores/category_store"; import { categories } from "$lib/stores/category_store";
import { summitLog } from "$lib/stores/summit_log_store"; import { summitLog } from "$lib/stores/summit_log_store";
@@ -37,6 +37,7 @@
import "leaflet/dist/leaflet.css"; import "leaflet/dist/leaflet.css";
import { onMount } from "svelte"; import { onMount } from "svelte";
import { _ } from "svelte-i18n"; import { _ } from "svelte-i18n";
import { array, number, object, string } from "yup";
export let data: { trail: Trail }; export let data: { trail: Trail };
@@ -55,6 +56,22 @@
let gpxFile: File | null = null; let gpxFile: File | null = null;
const trailSchema = object<Trail>({
id: string().optional(),
name: string().required($_("required")),
location: string().optional(),
distance: number().optional(),
difficulty: string()
.oneOf(["easy", "moderate", "difficult"])
.optional(),
elevation_gain: number().optional(),
duration: number().optional(),
thumbnail: string().optional(),
photos: array(string()).optional(),
gpx: string().optional(),
description: string().optional(),
});
onMount(async () => { onMount(async () => {
L = (await import("leaflet")).default; L = (await import("leaflet")).default;
await import("leaflet-gpx"); await import("leaflet-gpx");
@@ -457,9 +474,9 @@
label={$_("difficulty")} label={$_("difficulty")}
bind:value={$form.difficulty} bind:value={$form.difficulty}
items={[ items={[
{ text: $_('easy'), value: "easy" }, { text: $_("easy"), value: "easy" },
{ text: $_('moderate'), value: "moderate" }, { text: $_("moderate"), value: "moderate" },
{ text: $_('difficult'), value: "difficult" }, { text: $_("difficult"), value: "difficult" },
]} ]}
></Select> ></Select>
{#if $form.expand.category} {#if $form.expand.category}

View File

@@ -39,6 +39,10 @@
let lightbox: PhotoSwipeLightbox; let lightbox: PhotoSwipeLightbox;
let lightboxDataSource: DataSource; let lightboxDataSource: DataSource;
const thumbnail = $trail.photos.length
? getFileURL(trail, $trail.photos[$trail.thumbnail])
: "/imgs/default_thumbnail.webp";
onMount(async () => { onMount(async () => {
const L = (await import("leaflet")).default; const L = (await import("leaflet")).default;
await import("leaflet-gpx"); await import("leaflet-gpx");
@@ -134,7 +138,7 @@
<section class="relative h-80"> <section class="relative h-80">
<img <img
class="w-full h-80" class="w-full h-80"
src={getFileURL($trail, $trail.photos[$trail.thumbnail])} src={thumbnail}
alt="" alt=""
/> />
<div <div

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 284 KiB