Adds option to mark trail as completed (#920)

* initial commit

* add completed for komoot tours

* mark hammerhead activities as completed

* fix review suggestions

* adds completed modal to trail edit page

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
This commit is contained in:
Flomp
2026-04-27 20:37:11 +02:00
committed by GitHub
parent 7e84e39cda
commit 7bdf0d3151
25 changed files with 244 additions and 18 deletions

View File

@@ -543,6 +543,7 @@ func createTrailFromActivity(app core.App, detailedTour *HammerheadActivity, gpx
"id": trailid,
"name": detailedTour.ActivityData.Name,
"public": false,
"completed": true,
"distance": detailedTour.ActivityData.ActivityInfo[idDistance].Value.Value,
"elevation_gain": detailedTour.ActivityData.ActivityInfo[idElevationGain].Value.Value,
"elevation_loss": detailedTour.ActivityData.ActivityInfo[idElevationLoss].Value.Value,

View File

@@ -292,6 +292,7 @@ func createTrailFromTour(app core.App, k *KomootApi, detailedTour *DetailedKomoo
"id": trailid,
"name": detailedTour.Name,
"public": public,
"completed": detailedTour.Type == "tour_recorded",
"distance": detailedTour.Distance,
"elevation_gain": detailedTour.ElevationUp,
"elevation_loss": detailedTour.ElevationDown,

View File

@@ -0,0 +1,62 @@
package migrations
import (
"github.com/pocketbase/pocketbase/core"
m "github.com/pocketbase/pocketbase/migrations"
)
func init() {
m.Register(func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// add field
if err := collection.Fields.AddMarshaledJSONAt(5, []byte(`{
"hidden": false,
"id": "bool989355118",
"name": "completed",
"presentable": false,
"required": false,
"system": false,
"type": "bool"
}`)); err != nil {
return err
}
err = app.Save(collection)
if err != nil {
return err
}
logs, err := app.FindAllRecords("summit_logs")
if err != nil {
return err
}
for _, l := range logs {
trail, err := app.FindRecordById("trails", l.GetString("trail"))
if err != nil {
continue
}
trail.Set("completed", true)
err = app.Save(trail)
if err != nil {
return err
}
}
return nil
}, func(app core.App) error {
collection, err := app.FindCollectionByNameOrId("e864strfxo14pm4")
if err != nil {
return err
}
// remove field
collection.Fields.RemoveById("bool989355118")
return app.Save(collection)
})
}

View File

@@ -12,7 +12,6 @@ import (
"time"
"github.com/meilisearch/meilisearch-go"
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/tkrajina/gpxgo/gpx"
"github.com/twpayne/go-polyline"
@@ -52,11 +51,6 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
domain = author.GetString("domain")
}
logCount, err := app.CountRecords("summit_logs", dbx.NewExp("trail={:id}", dbx.Params{"id": r.Id}))
if err != nil {
return nil, err
}
document := map[string]any{
"id": r.Id,
"author": author.Id,
@@ -71,7 +65,7 @@ func documentFromTrailRecord(app core.App, r *core.Record, author *core.Record,
"duration": r.GetFloat("duration"),
"difficulty": difficultyToNumber(r.GetString("difficulty")),
"category": category,
"completed": logCount > 0,
"completed": r.GetBool("completed"),
"date": r.GetDateTime("date").Time().Unix(),
"created": r.GetDateTime("created").Time().Unix(),
"public": r.GetBool("public"),

View File

@@ -150,7 +150,9 @@
{/if}
<div class="p-4">
<div>
<h4 class="font-semibold text-lg line-clamp-2 wrap-anywhere">{trail.name}</h4>
<h4 class="font-semibold text-lg line-clamp-2 wrap-anywhere">
{trail.name}
</h4>
{#if trail.date}
<p class="text-xs text-gray-500 mb-3">
{new Date(trail.date).toLocaleDateString(undefined, {
@@ -200,7 +202,11 @@
<div class="flex gap-x-4 gap-y-1 text-base flex-wrap">
{#if trail.expand?.category?.name || trail.category}
<p>
<i class="fa fa-shapes mr-3"> </i>{$_(trail.expand?.category?.name ?? trail.category ?? "-")}
<i class="fa fa-shapes mr-3"> </i>{$_(
trail.expand?.category?.name ??
trail.category ??
"-",
)}
</p>
{/if}
{#if trail.location}

View File

@@ -56,6 +56,7 @@
import { handleFromRecordWithIRI } from "$lib/util/activitypub_util";
import LikeButton from "./like_button.svelte";
import Editor from "../base/editor.svelte";
import { trails_update } from "$lib/stores/trail_store";
interface Props {
initTrail: Trail;
@@ -75,6 +76,7 @@
let summitLogModal: SummitLogModal;
let confirmModal: ConfirmModal;
let markTrailAsCompletedModal: ConfirmModal;
let trail = $state(untrack(() => initTrail));
@@ -243,6 +245,13 @@
log.trail = trail.id;
const newLog = await summit_logs_create(log);
summitLogs.set([...$summitLogs, newLog]);
if (
$summitLogs.length == 1 &&
trail.author == $currentUser?.actor &&
!trail.completed
) {
markTrailAsCompletedModal.openModal();
}
}
summitLogCreateLoading = false;
}
@@ -259,6 +268,12 @@
);
summitLogs.set(newSummitLogList);
}
async function markTrailAsCompleted() {
trail.completed = true;
const updatedTrail: Trail = { ...trail };
await trails_update(trail, updatedTrail);
}
</script>
<div
@@ -280,7 +295,7 @@
</button>
{/if}
<div
class="grid gap-[1px] {headerPhotos.length > 1
class="grid gap-px {headerPhotos.length > 1
? 'grid-cols-[8fr_5fr]'
: 'grid-cols-1'} h-80 rounded-t-3xl overflow-hidden cursor-pointer"
>
@@ -391,10 +406,20 @@
{trail.location}
</h3>
{/if}
<h3 class="text-lg">
<h3>
<i class="fa fa-gauge mr-2"></i>
{$_(trail.difficulty ?? "?")}
</h3>
<h3>
<i
class="fa {trail.completed
? 'fa-flag-checkered'
: 'fa-compass-drafting'} mr-2"
></i>
{$_(
trail.completed ? "completed" : "not-completed",
)}
</h3>
</div>
</div>
<div class="flex flex-col items-center gap-y-2">
@@ -675,6 +700,16 @@
<SummitLogModal bind:this={summitLogModal} onsave={(log) => saveSummitLog(log)}
></SummitLogModal>
<ConfirmModal
id="mark-trail-as-completed-modal"
title={$_("mark-trail-as-completed")}
text={$_("mark-trail-as-completed-modal-text")}
action={$_("yes")}
deny={$_("no")}
bind:this={markTrailAsCompletedModal}
onconfirm={markTrailAsCompleted}
></ConfirmModal>
<ConfirmModal
id="confirm-summit-log-delete-modal"
text={$_("delete-summit-log-confirm")}

View File

@@ -133,7 +133,8 @@
{#if trail.tags.length && trail.expand?.tags}
<div class="flex flex-wrap gap-1 mb-3 items-center">
{#each expandedTags ? trail.expand.tags : trail.expand.tags.slice(0, 2) as tag}
<Chip text={tag.name} closable={false} primary={false}></Chip>
<Chip text={tag.name} closable={false} primary={false}
></Chip>
{/each}
{#if trail.expand.tags.length > 2}
@@ -154,7 +155,9 @@
<div class="flex flex-wrap gap-x-8 gap-y-1">
{#if trail.expand?.category?.name || trail.category}
<p>
<i class="fa fa-shapes mr-3"> </i>{$_(trail.expand?.category?.name ?? trail.category ?? "-")}
<i class="fa fa-shapes mr-3"> </i>{$_(
trail.expand?.category?.name ?? trail.category ?? "-",
)}
</p>
{/if}
{#if trail.location}
@@ -165,6 +168,14 @@
<p class="whitespace-nowrap">
<i class="fa fa-gauge mr-3"></i>{$_(trail.difficulty ?? "?")}
</p>
<p class="whitespace-nowrap">
<i
class="fa {trail.completed
? 'fa-flag-checkered'
: 'fa-compass-drafting'} mr-2"
></i>
{$_(trail.completed ? "completed" : "not-completed")}
</p>
</div>
<div class="flex flex-wrap mt-1 gap-x-4 gap-y-2 text-sm text-gray-500">

View File

@@ -3,6 +3,7 @@
"Canoeing": "Kanoistika",
"Climbing": "Horolezectví",
"Hiking": "Turistika",
"Skiing": "",
"Walking": "Chůze",
"about": "O aplikaci",
"account-delete-confirm": "Chystáte se odstranit svůj účet. Všechny vaše záznamy budou odstraněny též. Chcete pokračovat?",
@@ -264,6 +265,8 @@
"make-thumbnail": "Vytvořit náhled",
"map": "Mapa",
"map-style": "Styl mapy",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Max. obtížnost túry",
"metric": "Metrický",
"moderate": "Středně náročné",
@@ -291,6 +294,7 @@
"new-password-text": "Nastavit nové heslo",
"new-token-generated": "",
"new-trail": "Nová trasa",
"no": "",
"no-account": "Ještě nemáte účet?",
"no-api-tokens": "",
"no-comments-so-far": "Zatím žádné komentáře",
@@ -467,5 +471,6 @@
"welcome_to": "Vítejte v",
"width": "Šířka",
"wrong-username-or-password": "Nesprávné uživatelské jméno nebo heslo",
"yes": "",
"you-can": "Můžete"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Kanufahren",
"Climbing": "Klettern",
"Hiking": "Wandern",
"Skiing": "Skifahren",
"Walking": "Laufen",
"about": "Über",
"account-delete-confirm": "Du bist dabei, dein Konto zu löschen. Alle deine Routen werden ebenfalls gelöscht. Möchtest du fortfahren?",
@@ -264,6 +265,8 @@
"make-thumbnail": "Thumbnail festlegen",
"map": "Karte",
"map-style": "Kartenstil",
"mark-trail-as-completed": "Route als abgeschlossen markieren",
"mark-trail-as-completed-modal-text": "Möchtest du diese Route als abgeschlossen markieren? Du kannst diesen Status jederzeit wieder ändern.",
"max-hiking-difficulty": "Max. Schwierigkeit der Wanderung",
"metric": "Metrisch",
"moderate": "Mittel",
@@ -291,6 +294,7 @@
"new-password-text": "Wähle ein neues Passwort",
"new-token-generated": "",
"new-trail": "Neue Route",
"no": "Nein",
"no-account": "Du hast noch kein Konto?",
"no-api-tokens": "",
"no-comments-so-far": "Bisher keine Kommentare",
@@ -467,5 +471,6 @@
"welcome_to": "Willkommen bei",
"width": "Breite",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort",
"yes": "Ja",
"you-can": "Du kannst"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Canoeing",
"Climbing": "Climbing",
"Hiking": "Hiking",
"Skiing": "",
"Walking": "Walking",
"about": "About",
"account-delete-confirm": "You are about to delete your account. All your trails will also be deleted. Do you want to proceed?",
@@ -21,6 +22,7 @@
"already-account": "Already have an account?",
"altitude": "Altitude",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "API Documentation",
"api-tokens": "API Tokens",
"api-tokens-hint": "API Tokens can be used to grant 3rd party applications access to your wanderer account.",
@@ -263,6 +265,8 @@
"make-thumbnail": "Make thumbnail",
"map": "Map",
"map-style": "Map style",
"mark-trail-as-completed": "Mark trail as completed",
"mark-trail-as-completed-modal-text": "Would you like to mark this trail as completed? You can change this status again at any time.",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Metric",
"moderate": "Moderate",
@@ -290,6 +294,7 @@
"new-password-text": "Set a new password",
"new-token-generated": "New API Token generated",
"new-trail": "New Trail",
"no": "No",
"no-account": "Don't have an account?",
"no-api-tokens": "You have no API Tokens",
"no-comments-so-far": "No comments so far",
@@ -466,5 +471,6 @@
"welcome_to": "Welcome to",
"width": "Width",
"wrong-username-or-password": "Wrong username or password",
"yes": "Yes",
"you-can": "You can"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Remo",
"Climbing": "Escalada",
"Hiking": "Senderismo",
"Skiing": "",
"Walking": "Paseo",
"about": "Sobre",
"account-delete-confirm": "Estás al punto de borrar tu cuenta. Todas tus rutas también serán borradas. ¿Quieres proceder?",
@@ -264,6 +265,8 @@
"make-thumbnail": "Generar miniaturas",
"map": "Mapa",
"map-style": "Estilo de mapa",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Dificultad máxima",
"metric": "Métrica",
"moderate": "Medio",
@@ -291,6 +294,7 @@
"new-password-text": "Configura una nueva contraseña",
"new-token-generated": "",
"new-trail": "Nueva Ruta",
"no": "",
"no-account": "¿No tienes una cuenta?",
"no-api-tokens": "",
"no-comments-so-far": "Ningún comentario todavía",
@@ -467,5 +471,6 @@
"welcome_to": "Bienvenid@ a",
"width": "Anchura",
"wrong-username-or-password": "Usuario o contraseña no correctas",
"yes": "",
"you-can": "Puedes"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Kanoa",
"Climbing": "Eskalada",
"Hiking": "Mendi-ibilaldia",
"Skiing": "",
"Walking": "Oinez",
"about": "Honi buruz",
"account-delete-confirm": "Zure kontua ezabatzera zoaz. Zure ibilbide guztiak ere ezabatu egingo dira. Jarraitu nahi duzu?",
@@ -21,6 +22,7 @@
"already-account": "Baduzu kontua lehendik?",
"altitude": "Altuera",
"amenity": "Altimetria",
"ammenity": "",
"api-documentation": "API dokumentazioa",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Egin iruditxoa",
"map": "Mapa",
"map-style": "Maparen estiloa",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Mendi ibilaldiaren gehienezko zailtasuna",
"metric": "Metrikoa",
"moderate": "Erdi-bidekoa",
@@ -290,6 +294,7 @@
"new-password-text": "Pasahitz berria sortu",
"new-token-generated": "",
"new-trail": "Ibilaldi berria",
"no": "",
"no-account": "Ez duzu konturik?",
"no-api-tokens": "",
"no-comments-so-far": "Ez dago iruzkinik",
@@ -466,5 +471,6 @@
"welcome_to": "Ongi etorri",
"width": "Zabalera",
"wrong-username-or-password": "Erabiltzaile izena edo pasahitza okerrak dira",
"yes": "",
"you-can": "Hau egin dezakezu"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Canoë",
"Climbing": "Escalade",
"Hiking": "Randonnée",
"Skiing": "",
"Walking": "Marche",
"about": "Informations",
"account-delete-confirm": "Vous êtes sur le point de supprimer votre compte. Tous vos itinéraires seront également supprimés. Voulez-vous continuer ?",
@@ -264,6 +265,8 @@
"make-thumbnail": "Créer une miniature",
"map": "Carte",
"map-style": "Style de carte",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Difficulté maximale de marche",
"metric": "Métrique",
"moderate": "Moyenne",
@@ -291,6 +294,7 @@
"new-password-text": "Définir un nouveau mot de passe",
"new-token-generated": "",
"new-trail": "Nouvel itinéraire",
"no": "",
"no-account": "Pas encore de compte ?",
"no-api-tokens": "",
"no-comments-so-far": "Aucun commentaire pour l'instant",
@@ -467,5 +471,6 @@
"welcome_to": "Bienvenue sur",
"width": "Largeur",
"wrong-username-or-password": "Nom d'utilisateur ou mot de passe incorrect",
"yes": "",
"you-can": "Vous pouvez"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Canoeing",
"Climbing": "Climbing",
"Hiking": "Hiking",
"Skiing": "",
"Walking": "Walking",
"about": "A programról",
"account-delete-confirm": "Ön most a profilját készül törölni. Minden nyomvonala törlődik. Szeretné folytatni?",
@@ -21,6 +22,7 @@
"already-account": "Már rendelkezik fiókkal?",
"altitude": "Magasság",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "API Dokumentáció",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Készítsen miniatűrképet",
"map": "Térkép",
"map-style": "Map style",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Metrikus",
"moderate": "Mérsékelt",
@@ -290,6 +294,7 @@
"new-password-text": "Set a new password",
"new-token-generated": "",
"new-trail": "Új útvonal",
"no": "",
"no-account": "Nincs még fiókja?",
"no-api-tokens": "",
"no-comments-so-far": "No comments so far",
@@ -466,5 +471,6 @@
"welcome_to": "Üdvözöljük a",
"width": "Width",
"wrong-username-or-password": "Helytelen felhasználónév vagy jelszó",
"yes": "",
"you-can": "You can"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Canoa",
"Climbing": "Arrampicata",
"Hiking": "Escursionismo",
"Skiing": "",
"Walking": "Camminare",
"about": "Su di noi",
"account-delete-confirm": "Stai per eliminare il tuo account. Tutti i tuoi percorsi saranno cancellati. Vuoi procedere?",
@@ -21,6 +22,7 @@
"already-account": "Hai già un account?",
"altitude": "Altitudine",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "Documentazione API",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Imposta miniatura",
"map": "Mappa",
"map-style": "Map style",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Metrico",
"moderate": "Moderato",
@@ -290,6 +294,7 @@
"new-password-text": "Definire una nuova password",
"new-token-generated": "",
"new-trail": "Nuovo percorso",
"no": "",
"no-account": "Non hai ancora un account?",
"no-api-tokens": "",
"no-comments-so-far": "Nessun commento per il momento",
@@ -466,5 +471,6 @@
"welcome_to": "Benvenuti a",
"width": "Width",
"wrong-username-or-password": "Nome utente o password errati",
"yes": "",
"you-can": "Puoi"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Kanoën",
"Climbing": "Klimmen",
"Hiking": "Hiken",
"Skiing": "",
"Walking": "Wandelen",
"about": "Over",
"account-delete-confirm": "Je staat op het punt je account te verwijderen. Al je routes worden hierdoor eveneens verwijderd. Wil je doorgaan?",
@@ -264,6 +265,8 @@
"make-thumbnail": "Miniatuur maken",
"map": "Kaart",
"map-style": "Kaartstijl",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Max. Hiking Moeilijkheid",
"metric": "Metrisch",
"moderate": "Gemiddeld",
@@ -291,6 +294,7 @@
"new-password-text": "Stel nieuw wachtwoord in",
"new-token-generated": "",
"new-trail": "Nieuwe Route",
"no": "",
"no-account": "Heb je nog geen account?",
"no-api-tokens": "",
"no-comments-so-far": "Tot nu toe geen opmerkingen",
@@ -467,5 +471,6 @@
"welcome_to": "Welkom bij",
"width": "Breedte",
"wrong-username-or-password": "Onjuiste gebruikersnaam of wachtwoord",
"yes": "",
"you-can": "Jij kunt"
}

View File

@@ -265,6 +265,8 @@
"make-thumbnail": "Lag miniatyrbilde",
"map": "Kart",
"map-style": "Kartstil",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Maks. vanskelighetsgrad",
"metric": "Metrisk",
"moderate": "Moderat",
@@ -292,6 +294,7 @@
"new-password-text": "Sett et nytt passord",
"new-token-generated": "Nytt API-token generert",
"new-trail": "Ny sti",
"no": "",
"no-account": "Har du ikke en konto?",
"no-api-tokens": "Du har ingen API-tokens",
"no-comments-so-far": "Ingen kommentarer ennå",
@@ -468,5 +471,6 @@
"welcome_to": "Velkommen til",
"width": "Bredde",
"wrong-username-or-password": "Feil brukernavn eller passord",
"yes": "",
"you-can": "Du kan"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Kajak",
"Climbing": "Wspinaczka",
"Hiking": "Wędrówka",
"Skiing": "",
"Walking": "Spacer",
"about": "Na temat",
"account-delete-confirm": "Za chwilę usuniesz swoje konto. Wszystkie twoje szlaki zostaną usunięte. Czy chcesz kontynuować?",
@@ -21,6 +22,7 @@
"already-account": "Czy masz już konto?",
"altitude": "Wysokość",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "Dokumentacja API",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Zrób miniaturkę",
"map": "Mapa",
"map-style": "Map style",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Maksymalny poziom trudności wędrówki",
"metric": "Metryczne",
"moderate": "Średni",
@@ -290,6 +294,7 @@
"new-password-text": "Ustaw nowe hasło",
"new-token-generated": "",
"new-trail": "Nowy szlak",
"no": "",
"no-account": "Nie masz konta?",
"no-api-tokens": "",
"no-comments-so-far": "Nie ma jeszcze komentarzy",
@@ -466,5 +471,6 @@
"welcome_to": "Witaj w",
"width": "Szerokość",
"wrong-username-or-password": "Zła nazwa użytkownika lub hasło",
"yes": "",
"you-can": "Możesz"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Canoagem",
"Climbing": "Escalada",
"Hiking": "Montanhismo",
"Skiing": "",
"Walking": "Caminhada",
"about": "Sobre",
"account-delete-confirm": "Está prestes a excluir a sua conta. Todos os seus percursos também serão excluídos. Quer continuar?",
@@ -21,6 +22,7 @@
"already-account": "Já tem uma conta?",
"altitude": "Altitude",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "Documentação da API",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Faça miniatura",
"map": "Mapa",
"map-style": "Map style",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Max. Hiking Difficulty",
"metric": "Métrica",
"moderate": "Moderado",
@@ -290,6 +294,7 @@
"new-password-text": "Set a new password",
"new-token-generated": "",
"new-trail": "Nova trilha",
"no": "",
"no-account": "Não tem uma conta?",
"no-api-tokens": "",
"no-comments-so-far": "No comments so far",
@@ -466,5 +471,6 @@
"welcome_to": "Bem-vindo ao",
"width": "Width",
"wrong-username-or-password": "Nome de utilizador ou palavra-passe errados",
"yes": "",
"you-can": "Podes"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "Каякинг",
"Climbing": "Скалолазание",
"Hiking": "Пеший туризм",
"Skiing": "",
"Walking": "Прогулка",
"about": "О",
"account-delete-confirm": "Вы собираетесь удалить аккаунт. Все ваши треки также будут удалены. Продолжить?",
@@ -21,6 +22,7 @@
"already-account": "Уже есть аккаунт?",
"altitude": "Высота",
"amenity": "Amenity",
"ammenity": "",
"api-documentation": "Документация API",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "Сделать миниатюру",
"map": "Карта",
"map-style": "Стиль карты",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "Макс. сложность",
"metric": "Метрическая",
"moderate": "Средний",
@@ -290,6 +294,7 @@
"new-password-text": "Установить новый пароль",
"new-token-generated": "",
"new-trail": "Новый трек",
"no": "",
"no-account": "Нет аккаунта?",
"no-api-tokens": "",
"no-comments-so-far": "Пока нет комментариев",
@@ -466,5 +471,6 @@
"welcome_to": "Добро пожаловать в",
"width": "Ширина",
"wrong-username-or-password": "Неверный логин или пароль",
"yes": "",
"you-can": "Вы можете"
}

View File

@@ -3,6 +3,7 @@
"Canoeing": "划艇",
"Climbing": "攀岩",
"Hiking": "徒步",
"Skiing": "",
"Walking": "步行",
"about": "关于",
"account-delete-confirm": "您现在要删除当前账户,所有的路线都会删除无法恢复,确认继续操作吗?",
@@ -21,6 +22,7 @@
"already-account": "已注册账户?",
"altitude": "海拔",
"amenity": "友好性",
"ammenity": "",
"api-documentation": "API 文档",
"api-tokens": "",
"api-tokens-hint": "",
@@ -263,6 +265,8 @@
"make-thumbnail": "生成缩略图",
"map": "地图",
"map-style": "地图样式",
"mark-trail-as-completed": "",
"mark-trail-as-completed-modal-text": "",
"max-hiking-difficulty": "最大徒步难度",
"metric": "公制",
"moderate": "中等",
@@ -290,6 +294,7 @@
"new-password-text": "设置新密码",
"new-token-generated": "",
"new-trail": "创建新路线",
"no": "",
"no-account": "还未注册?",
"no-api-tokens": "",
"no-comments-so-far": "到目前为止没有评论",
@@ -466,5 +471,6 @@
"welcome_to": "欢迎",
"width": "宽度",
"wrong-username-or-password": "用户名或密码无效",
"yes": "",
"you-can": "你可以"
}

View File

@@ -9,6 +9,7 @@ const TrailCreateSchema = z.object({
location: z.string().optional(),
date: z.string().optional().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date"),
public: z.boolean(),
completed: z.boolean(),
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
lat: z.number().min(-90).max(90).optional(),
lon: z.number().min(-180).max(180).optional(),
@@ -32,6 +33,7 @@ const TrailUpdateSchema = z.object({
location: z.string().optional(),
date: z.string().optional().refine((val) => !val || !isNaN(Date.parse(val)), "invalid-date"),
public: z.boolean().optional(),
completed: z.boolean().optional(),
difficulty: z.enum(["easy", "moderate", "difficult"]).optional(),
lat: z.number().min(-90).max(90).optional(),
lon: z.number().min(-180).max(180).optional(),

View File

@@ -15,6 +15,7 @@ class Trail {
location?: string;
date?: string;
public: boolean;
completed: boolean;
distance?: number;
elevation_gain?: number;
elevation_loss?: number;
@@ -55,6 +56,7 @@ class Trail {
location?: string,
date?: string,
public?: boolean,
completed?: boolean,
distance?: number,
elevation_gain?: number,
elevation_loss?: number,
@@ -82,6 +84,7 @@ class Trail {
this.location = params?.location;
this.date = params?.date ?? new Date().toISOString().split('T')[0];
this.public = params?.public ?? false
this.completed = params?.completed ?? false,
this.distance = params?.distance ?? 0;
this.elevation_gain = params?.elevation_gain ?? 0;
this.elevation_loss = params?.elevation_loss ?? 0;

View File

@@ -441,6 +441,7 @@ export async function searchResultToTrailList(hits: Hits<TrailSearchResult>): Pr
name: h.name,
photos: h.thumbnail ? [h.thumbnail] : [],
public: h.public,
completed: h.completed,
summit_logs: [],
waypoints: [],
tags: h.tags ?? [],

View File

@@ -101,6 +101,7 @@
import { z } from "zod";
import Track from "$lib/models/gpx/track.js";
import TrackSegment from "$lib/models/gpx/track-segment.js";
import ConfirmModal from "$lib/components/confirm_modal.svelte";
let { data } = $props();
@@ -112,6 +113,7 @@
let waypointModal: WaypointModal;
let summitLogModal: SummitLogModal;
let listSelectModal: ListSearchModal;
let markTrailAsCompletedModal: ConfirmModal;
let loading = $state(false);
@@ -452,7 +454,8 @@
if (!$formData.expand!.waypoints_via_trail?.length) {
$formData.expand!.waypoints_via_trail = [];
}
$formData.expand!.waypoints_via_trail = $formData.expand!.waypoints_via_trail;
$formData.expand!.waypoints_via_trail =
$formData.expand!.waypoints_via_trail;
// updateTrailOnMap();
}
@@ -464,7 +467,8 @@
) ?? -1;
if (editedWaypointIndex >= 0) {
$formData.expand!.waypoints_via_trail![editedWaypointIndex] = savedWaypoint;
$formData.expand!.waypoints_via_trail![editedWaypointIndex] =
savedWaypoint;
} else {
savedWaypoint.id = cryptoRandomString({ length: 15 });
$formData.expand!.waypoints_via_trail = [
@@ -479,7 +483,9 @@
function moveMarker(marker: M.Marker, wpId?: string) {
const position = marker.getLngLat();
const editableWaypointIndex =
$formData.expand!.waypoints_via_trail?.findIndex((w) => w.id == wpId) ?? -1;
$formData.expand!.waypoints_via_trail?.findIndex(
(w) => w.id == wpId,
) ?? -1;
const editableWaypoint =
$formData.expand!.waypoints_via_trail![editableWaypointIndex];
if (!editableWaypoint) {
@@ -487,7 +493,9 @@
}
editableWaypoint.lat = position.lat;
editableWaypoint.lon = position.lng;
$formData.expand!.waypoints_via_trail = [...($formData.expand!.waypoints_via_trail ?? [])];
$formData.expand!.waypoints_via_trail = [
...($formData.expand!.waypoints_via_trail ?? []),
];
// updateTrailOnMap();
}
@@ -515,6 +523,13 @@
log,
];
}
if (
$formData.expand?.summit_logs_via_trail?.length == 1 &&
!$formData.completed
) {
markTrailAsCompletedModal.openModal();
}
}
function handleSummitLogMenuClick(
@@ -1167,6 +1182,10 @@
initRouteAnchors(valhallaStore.route, true);
updateTrailWithRouteData();
}
function markTrailAsCompleted() {
setFields("completed", true);
}
</script>
<svelte:head>
@@ -1180,7 +1199,7 @@
<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"
class="overflow-y-auto overflow-x-hidden flex flex-col gap-4 px-8 order-1 md:order-0 mt-8 md:mt-0"
use:form
>
<Search
@@ -1360,6 +1379,11 @@
></Select>
</div>
<Toggle
name="completed"
label={$formData.completed ? $_("completed") : $_("not-completed")}
icon={$formData.completed ? "flag-checkered" : "compass-drafting"}
></Toggle>
<Toggle
name="public"
label={$formData.public ? $_("public") : $_("private")}
@@ -1525,6 +1549,15 @@
bind:this={listSelectModal}
onchange={(e) => handleListSelection(e)}
></ListSearchModal>
<ConfirmModal
id="mark-trail-as-completed-modal"
title={$_("mark-trail-as-completed")}
text={$_("mark-trail-as-completed-modal-text")}
action={$_("yes")}
deny={$_("no")}
bind:this={markTrailAsCompletedModal}
onconfirm={markTrailAsCompleted}
></ConfirmModal>
<style>
#trail-map {