adds komoot integration

This commit is contained in:
Christian Beutel
2025-02-04 21:23:54 +01:00
parent a0b6480854
commit 4d1d07922a
32 changed files with 1373 additions and 189 deletions

View File

@@ -72,7 +72,7 @@
</div>
{#if activity.photos.length}
<div
class="grid {activity.photos.length > 1
class="grid gap-[1px] {activity.photos.length > 1
? 'grid-cols-[8fr_5fr]'
: 'grid-cols-1'}"
>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import Toggle from "$lib/components/base/toggle.svelte";
import { _ } from "svelte-i18n";
interface Props {
onclick: () => void;
ontoggle: (value: boolean) => void;
active: boolean;
disabled: boolean;
img: string;
title: string;
description: string;
}
let {
onclick,
ontoggle,
active = $bindable(),
disabled,
img,
title,
description,
}: Props = $props();
</script>
<div class="border border-input-border rounded-lg p-4 space-y-4">
<img class="h-20" src={img} alt="integration logo"/>
<div>
<h5 class="text-xl font-semibold">{title}</h5>
<p class="text-sm text-gray-500">
{description}
</p>
</div>
<div class="flex items-center justify-between">
<button class="btn-secondary" {onclick}
><i class="fa fa-cogs mr-2"></i>{$_("settings")}</button
>
<Toggle bind:value={active} onchange={ontoggle} {disabled}></Toggle>
</div>
</div>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import { KomootSchema } from "$lib/models/api/integration_schema";
import type {
Integration,
KomootIntegration,
} from "$lib/models/integration";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
interface Props {
integration?: Integration;
onsave?: (komootIntegration: KomootIntegration) => void;
}
let { integration, onsave }: Props = $props();
let modal: Modal;
export function openModal() {
errors.set({})
modal.openModal();
}
const {
form,
errors,
data: d,
} = createForm({
initialValues: {
email: integration?.komoot?.email ?? "",
password: integration?.komoot?.password ?? "",
active: integration?.komoot?.active ?? false,
},
extend: validator({
schema: KomootSchema,
}),
onSubmit: async (form) => {
onsave?.(form);
modal.closeModal();
},
});
</script>
<Modal
id="komoot-settings-modal"
size="max-w-lg"
title={"komoot " + $_("settings")}
bind:this={modal}
>
{#snippet content()}
<form id="komoot-settings-form" class="space-y-2" use:form>
<TextField
label={$_("email")}
placeholder="user@example.com"
name="email"
error={$errors.email}
></TextField>
<TextField
label={$_("password")}
name="password"
type="password"
error={$errors.password}
></TextField>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button class="btn-secondary" onclick={() => modal.closeModal()}
>{$_("cancel")}</button
>
<button
class="btn-primary"
form="komoot-settings-form"
type="submit"
name="save">{$_("save")}</button
>
</div>
{/snippet}</Modal
>

View File

@@ -0,0 +1,93 @@
<script lang="ts">
import Modal from "$lib/components/base/modal.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
import { StravaSchema } from "$lib/models/api/integration_schema";
import type {
Integration,
StravaIntegration,
} from "$lib/models/integration";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
interface Props {
integration?: Integration;
onsave?: (stravaIntegration: StravaIntegration) => void;
}
let { integration, onsave }: Props = $props();
let modal: Modal;
export function openModal() {
errors.set({})
modal.openModal();
}
const {
form,
errors,
} = createForm({
initialValues: {
clientId: integration?.strava?.clientId ?? "",
clientSecret: integration?.strava?.clientSecret ?? "",
routes: integration?.strava?.routes ?? true,
activities: integration?.strava?.activities ?? true,
active: integration?.strava?.active ?? false,
},
extend: validator({
schema: StravaSchema,
}),
onSubmit: async (form) => {
onsave?.(form);
modal.closeModal();
},
});
</script>
<Modal
id="strava-settings-modal"
size="max-w-lg"
title={"strava " + $_("settings")}
bind:this={modal}
>
{#snippet content()}
<form id="strava-settings-form" class="space-y-2" use:form>
<TextField
label="Client-ID"
placeholder="000000"
name="clientId"
error={$errors.clientId}
></TextField>
<TextField
label="Client Secret"
placeholder="de8b3789bd7116d..."
name="clientSecret"
type="password"
error={$errors.clientSecret}
></TextField>
<div class="flex gap-x-4">
<Toggle name="routes" label={$_("route", { values: { n: 2 } })}
></Toggle>
<Toggle
name="activities"
label={$_("activity", { values: { n: 2 } })}
></Toggle>
</div>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button class="btn-secondary" onclick={() => modal.closeModal()}
>{$_("cancel")}</button
>
<button
class="btn-primary"
form="strava-settings-form"
type="submit"
name="save">{$_("save")}</button
>
</div>
{/snippet}</Modal
>

View File

@@ -55,7 +55,7 @@
<div
class="relative w-full min-h-40 max-h-48 overflow-hidden rounded-t-2xl"
>
<img width="100%" id="header-img" src={thumbnail} alt="" />
<img class="min-h-40" id="header-img" src={thumbnail} alt="" />
</div>
{#if (trail.public || trailIsShared) && pb.authStore.model}
<div

View File

@@ -44,13 +44,20 @@
import TrailTimeline from "./trail_timeline.svelte";
interface Props {
trail: Trail;
initTrail: Trail;
mode?: "overview" | "map" | "list";
markers?: M.Marker[];
activeTab?: number;
}
let { trail, mode = "map", markers = [], activeTab = 0 }: Props = $props();
let {
initTrail,
mode = "map",
markers = [],
activeTab = 0,
}: Props = $props();
let trail = $state(initTrail);
const tabs = [
$_("summit-book"),
@@ -362,7 +369,9 @@
{:else}
<EmptyStateDescription></EmptyStateDescription>
{/if}
<h4 class="text-2xl font-semibold mb-6 mt-12">{$_("route", { values: { n: 2 } })}</h4>
<h4 class="text-2xl font-semibold mb-6 mt-12">
{$_("route", { values: { n: 2 } })}
</h4>
{#if mode === "overview"}
<div
class="relative border border-input-border rounded-xl p-2 mb-6 text-xs"

View File

@@ -42,23 +42,26 @@
<div class="border border-input-border rounded-xl overflow-hidden">
{#if wp.photos.length}
<PhotoGallery photos={wp.photos.map((p) => getFileURL(wp, p))} bind:this={gallery[i]}
<PhotoGallery
photos={wp.photos.map((p) => getFileURL(wp, p))}
bind:this={gallery[i]}
></PhotoGallery>
<div
class="grid {wp.photos.length > 1
class="grid gap-[1px] {wp.photos.length > 1
? 'grid-cols-[8fr_5fr]'
: 'grid-cols-1'} cursor-pointer"
>
{#each wp.photos as photo, j}
<button onclick={() => gallery[i].openGallery(j)}>
<img
class="object-cover h-full max-h-80 w-full"
class:row-span-2={i == 0 &&
wp.photos.length > 2}
src={getFileURL(wp, photo)}
alt=""
/>
</button>
<img
onclick={() => gallery[i].openGallery(j)}
role="presentation"
class="w-full object-cover {j == 0 &&
wp.photos.length > 2
? 'row-span-2 h-80'
: 'h-[159.5px]'}"
src={getFileURL(wp, photo)}
alt=""
/>
{/each}
</div>
{/if}

View File

@@ -45,7 +45,7 @@
</script>
<div
class="flex gap-4 p-4 outline outline-1 outline-input-border rounded-md my-2 hover:outline-2"
class="flex gap-4 p-4 outline outline-1 outline-input-border rounded-md my-2 hover:outline-2 items-start"
>
{#if imgSrc.length}
{#if mode == "show"}
@@ -61,7 +61,7 @@
>
{#each imgSrc as img, i}
<img
class="absolute h-full rounded-xl object-cover"
class="absolute h-full rounded-xl object-cover aspect-square"
style="top: {6 * i}px; right: {6 *
i}px; transform: rotate(-{i * 5}deg)"
src={img}

View File

@@ -100,6 +100,7 @@
"error-during-login": "Fehler beim Login",
"error-during-password-reset": "Email zum Zurücksetzen des Passworts konnte nicht versandt werden",
"error-exporting-trail": "Fehler beim Exportieren des Trails",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Fehler beim Posten des Kommentars",
"error-printing-map": "Fehler beim Drucken der Karte",
"error-reading-file": "Fehler beim Lesen der Datei",
@@ -107,6 +108,7 @@
"error-saving-trail": "Fehler beim Speichern der Route",
"error-setting-up-strava-integration": "",
"error-updating-password": "Fehler beim Aktualisieren des Passworts",
"error-updating-strava-integration": "",
"est-duration": "Gesch. Dauer",
"explore": "Erkunden",
"explore-some-trails": "Erkunde einige Routen",
@@ -143,6 +145,8 @@
"import": "Importieren",
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
"include-description": "Beschreibung übernehmen",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Ungültiges Datum",
"invalid-username": "Ungültiger Nutzername",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Deine Listen sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Listen ändern.",
"settings-privacy-trails-private": "Deine Routen sind standardmäßig privat. \nNiemand außer Ihnen kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
"settings-privacy-trails-public": "Deine Trails sind standardmäßig öffentlich. \nJeder kann sie sehen. \nDu kannst diese Einstellung jederzeit für einzelne Routen ändern.",
"settings-saved": "",
"share": "Teilen",
"share-profile": "Profil teilen",
"share-this-list": "Diese Liste teilen",
@@ -295,4 +300,4 @@
"welcome_to": "Willkommen bei",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort",
"you-can": "Du kannst"
}
}

View File

@@ -100,6 +100,7 @@
"error-during-login": "Error during login",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
"error-logging-in-to-komoot": "Error logging in to komoot",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Error printing map",
"error-reading-file": "Error reading file",
@@ -107,6 +108,7 @@
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "Error setting up strava integration",
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "Error updating komoot integration",
"est-duration": "Est. duration",
"explore": "Explore",
"explore-some-trails": "Explore some trails",
@@ -143,6 +145,8 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description",
"integration-description-komoot": "Syncs your komoot tours with wanderer in regular intervals.",
"integration-description-strava": "Syncs your strava routes & activities with wanderer in regular intervals.",
"integrations": "Integrations",
"invalid-date": "Invalid Date",
"invalid-username": "Invalid username",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "Settings saved",
"share": "Share",
"share-profile": "Share profile",
"share-this-list": "Share this list",
@@ -295,4 +300,4 @@
"welcome_to": "Welcome to",
"wrong-username-or-password": "Wrong username or password",
"you-can": "You can"
}
}

View File

@@ -100,6 +100,7 @@
"error-during-login": "Error durante el acceso",
"error-during-password-reset": "Imposible enviar la contraseña de restablecimiento al correo electrónico",
"error-exporting-trail": "Error exportando la ruta",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error publicando el comentario",
"error-printing-map": "Error durante la impresión del mapa",
"error-reading-file": "Error leyendo el archivo",
@@ -107,6 +108,7 @@
"error-saving-trail": "Error guardando la ruta",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error actualizando la contraseña",
"error-updating-strava-integration": "",
"est-duration": "Duración estimada",
"explore": "Explora",
"explore-some-trails": "Explora alguna ruta",
@@ -143,6 +145,8 @@
"import": "Importar",
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
"include-description": "Incluir descripción",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Fecha no válida",
"invalid-username": "Usuario no válido",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Tus listas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para listas específicas.",
"settings-privacy-trails-private": "Tus rutas son privadas por defecto. Nadie excepto tú podrás verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
"settings-privacy-trails-public": "Tus rutas son públicas por defecto. Todos podrán verlas. Puedes cambiar esta configuración en cualquier momento para rutas específicas.",
"settings-saved": "",
"share": "Compartir",
"share-profile": "Compartir perfil",
"share-this-list": "Compartir esta lista",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Erreur durant la connexion",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Erreur lors de l'export de l'itinéraire",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Erreur d'impression de la carte",
"error-reading-file": "Erreur de lecture du fichier",
@@ -107,6 +108,7 @@
"error-saving-trail": "Erreur lors de l'enregistrement de l'itinéraire",
"error-setting-up-strava-integration": "",
"error-updating-password": "Erreur lors de la mise à jour du mot de passe",
"error-updating-strava-integration": "",
"est-duration": "Temps estimé",
"explore": "Explorer",
"explore-some-trails": "Explorer les itinéraires",
@@ -143,6 +145,8 @@
"import": "Importer",
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
"include-description": "Inclure la description",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Date invalide",
"invalid-username": "Nom d'utilisateur invalide",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "",
"share": "Partager",
"share-profile": "Share profile",
"share-this-list": "Partager cette liste",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Hiba bejelentkezés közben",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Error printing map",
"error-reading-file": "Hiba a fájl olvasása közben",
@@ -107,6 +108,7 @@
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "",
"est-duration": "Becsült időtartam",
"explore": "Felfedezés",
"explore-some-trails": "Fedezzen fel néhány ösvényt",
@@ -143,6 +145,8 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Érvénytelen dátum",
"invalid-username": "Érvénytelen felhasználó",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "",
"share": "Share",
"share-profile": "Share profile",
"share-this-list": "Share this list",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Errore durante il login",
"error-during-password-reset": "Impossibile inviare email per ripristinare la password",
"error-exporting-trail": "Errore durante l'esportazione del percorso",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Errore pubblicando il commento",
"error-printing-map": "Errore durante la stampa della mappa",
"error-reading-file": "Errore durante la lettura del file",
@@ -107,6 +108,7 @@
"error-saving-trail": "Errore nel salvataggio del percorso",
"error-setting-up-strava-integration": "",
"error-updating-password": "Errore nell'aggiornamento della password",
"error-updating-strava-integration": "",
"est-duration": "Durata stimata",
"explore": "Esplora",
"explore-some-trails": "Esplora alcuni percorsi",
@@ -143,6 +145,8 @@
"import": "Importa",
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
"include-description": "Adotta descrizione",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Data non valida",
"invalid-username": "Nome utente non valido",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Le tue liste sono pubbliche per impostazione predefinita. Tutto potrànno vederle. Puoi cambiare queste impostazione in qualsiasi momento per liste specifiche.",
"settings-privacy-trails-private": "Le tuoi percorsi sono privati per impostazione predefinita. Nessuno tranne tu potrà vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
"settings-privacy-trails-public": "Le tuoi percorsi sono pubblici per impostazione predefinita. Tutti potranno vederli. Puoi cambiare queste impostazione in qualsiasi momento per percorsi specifici.",
"settings-saved": "",
"share": "Condividi",
"share-profile": "Condividere profilo",
"share-this-list": "Condividi questa lista",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Het inloggen is mislukt",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Fout bij afdrukken van kaart",
"error-reading-file": "Het bestand kan niet worden ingelezen",
@@ -107,6 +108,7 @@
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error updating password",
"error-updating-strava-integration": "",
"est-duration": "Geschatte duur",
"explore": "Verkennen",
"explore-some-trails": "Verken enkele wandelroutes",
@@ -143,6 +145,8 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Inclusief beschrijving",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Ongeldige datum",
"invalid-username": "Ongeldige gebruikersnaam",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "",
"share": "Share",
"share-profile": "Share profile",
"share-this-list": "Share this list",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Błąd podczas logowania",
"error-during-password-reset": "Nie udało się wysłać e-maila z resetowaniem hasła",
"error-exporting-trail": "Błąd podczas eksportowania szlaku",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Błąd podczas drukowania mapy",
"error-reading-file": "Błąd wczytywania pliku",
@@ -107,6 +108,7 @@
"error-saving-trail": "Błąd podczas zapisywania szlaku",
"error-setting-up-strava-integration": "",
"error-updating-password": "Błąd podczas aktualizacji hasła",
"error-updating-strava-integration": "",
"est-duration": "Szacowany czas",
"explore": "Eksploruj",
"explore-some-trails": "Eksploruj różne szlaki",
@@ -143,6 +145,8 @@
"import": "Importuj",
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
"include-description": "Dołącz opis",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Nieprawidłowa data",
"invalid-username": "Błędna nazwa użytkownika",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Domyślnie, twoje listy są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdej listy.",
"settings-privacy-trails-private": "Domyślnie, twoje szlaki są prywatne. Nikt poza tobą nie będzie mógł ich zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
"settings-privacy-trails-public": "Domyślnie, twoje szlaki są publiczne. Każdy będzie mógł je zobaczyć. Zawsze możesz zmieniać to ustawienie, osobno dla każdego szlaku.",
"settings-saved": "",
"share": "Udostępnij",
"share-profile": "Udostępnij profil",
"share-this-list": "Udostępnij tę listę",

View File

@@ -100,6 +100,7 @@
"error-during-login": "Erro durante o ‘login’",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Erro na exportação do percurso",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "Erro na impressão do mapa",
"error-reading-file": "Erro ao ler o arquivo",
@@ -107,6 +108,7 @@
"error-saving-trail": "Erro ao gravar percurso",
"error-setting-up-strava-integration": "",
"error-updating-password": "Erro ao atualizar password",
"error-updating-strava-integration": "",
"est-duration": "Duração prevista",
"explore": "Explorar",
"explore-some-trails": "Explore algumas trilhas",
@@ -143,6 +145,8 @@
"import": "Importar",
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
"include-description": "Incluir descrição",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "Data inválida",
"invalid-username": "Nome de usuário inválido",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "",
"share": "Partilhar",
"share-profile": "Share profile",
"share-this-list": "Partilhar esta lista",

View File

@@ -100,6 +100,7 @@
"error-during-login": "登录错误",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "导出路线失败",
"error-logging-in-to-komoot": "",
"error-posting-comment": "Error posting comment",
"error-printing-map": "打印地图失败",
"error-reading-file": "读取文件错误",
@@ -107,6 +108,7 @@
"error-saving-trail": "保存路线失败",
"error-setting-up-strava-integration": "",
"error-updating-password": "更新密码失败",
"error-updating-strava-integration": "",
"est-duration": "预计时长",
"explore": "探索",
"explore-some-trails": "探索行程",
@@ -143,6 +145,8 @@
"import": "导入",
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
"include-description": "包含描述",
"integration-description-komoot": "",
"integration-description-strava": "",
"integrations": "",
"invalid-date": "无效日期",
"invalid-username": "无效用户名",
@@ -257,6 +261,7 @@
"settings-privacy-lists-public": "Your lists are public by default. Everyone will be able to see them. You can change this setting at any point for individual lists.",
"settings-privacy-trails-private": "Your trails are private by default. No one except you will be able to see them. You can change this setting at any point for individual trails.",
"settings-privacy-trails-public": "Your trails are public by default. Everyone will be able to see them. You can change this setting at any point for individual trails.",
"settings-saved": "",
"share": "分享",
"share-profile": "Share profile",
"share-this-list": "分享此列表",

View File

@@ -12,14 +12,22 @@ const StravaSchema = z.object({
active: z.boolean()
})
const KomootSchema = z.object({
email: z.string().email(),
password: z.string(),
active: z.boolean()
})
const IntegrationCreateSchema = z.object({
user: z.string().length(15),
strava: StravaSchema,
strava: StravaSchema.optional(),
komoot: KomootSchema.optional()
}) satisfies ZodType<Integration>
const IntegrationUpdateSchema = z.object({
strava: StravaSchema.optional(),
strava: StravaSchema.optional().nullable(),
komoot: KomootSchema.optional().nullable()
}) satisfies ZodType<Partial<Integration>>
export { StravaSchema, IntegrationCreateSchema, IntegrationUpdateSchema }
export { StravaSchema, IntegrationCreateSchema, IntegrationUpdateSchema, KomootSchema };

View File

@@ -1,9 +1,9 @@
interface BaseIntegration {
export interface BaseIntegration {
active: boolean
}
interface StravaIntegration extends BaseIntegration {
export interface StravaIntegration extends BaseIntegration {
clientId: string | number;
clientSecret: string;
routes: boolean;
@@ -13,13 +13,21 @@ interface StravaIntegration extends BaseIntegration {
expiresAt?: number;
}
export interface KomootIntegration extends BaseIntegration {
email: string,
password: string,
}
export class Integration {
id?: string;
user: string;
strava?: StravaIntegration;
strava?: StravaIntegration | null;
komoot?: KomootIntegration | null
constructor(user: string, strava?: StravaIntegration) {
constructor(user: string, strava?: StravaIntegration, komoot?: KomootIntegration) {
this.user = user;
this.strava = strava;
this.komoot = komoot;
}
}

View File

@@ -361,7 +361,7 @@
onclick={(data) => selectTrail(data.trail)}
></ListPanel>
{:else if selectedList && selectedTrail}
<TrailInfoPanel trail={selectedTrail} mode="list" {markers}
<TrailInfoPanel initTrail={selectedTrail} mode="list" {markers}
></TrailInfoPanel>
{/if}
</div>

View File

@@ -14,7 +14,7 @@
</svelte:head>
<main class="grid grid-cols-1 md:grid-cols-[458px_1fr] gap-x-1 gap-y-4">
<div id="panel" class="hidden md:block">
<TrailInfoPanel trail={$trail} {markers}></TrailInfoPanel>
<TrailInfoPanel initTrail={$trail} {markers}></TrailInfoPanel>
</div>
<div id="trail-details">
<MapWithElevationMaplibre

View File

@@ -1,18 +1,19 @@
<script lang="ts">
import { page } from "$app/state";
import Button from "$lib/components/base/button.svelte";
import Modal from "$lib/components/base/modal.svelte";
import TextField from "$lib/components/base/text_field.svelte";
import Toggle from "$lib/components/base/toggle.svelte";
import { StravaSchema } from "$lib/models/api/integration_schema.js";
import { Integration } from "$lib/models/integration.js";
import IntegrationCard from "$lib/components/settings/integrations/integration_card.svelte";
import KomootSettingsModal from "$lib/components/settings/integrations/komoot_settings_modal.svelte";
import StravaSettingsModal from "$lib/components/settings/integrations/strava_settings_modal.svelte";
import {
Integration,
type BaseIntegration,
type KomootIntegration,
type StravaIntegration,
} from "$lib/models/integration.js";
import {
integrations_create,
integrations_update,
} from "$lib/stores/integration_store.js";
import { show_toast } from "$lib/stores/toast_store.js";
import { validator } from "@felte/validator-zod";
import { createForm } from "felte";
import { _ } from "svelte-i18n";
let { data } = $props();
@@ -22,54 +23,44 @@
const scope = "read_all,activity:read_all";
const redirectUri = page.url.href + "/callback/strava";
let stravaSettingsModal: Modal;
let stravaSettingsModal: StravaSettingsModal;
let stravaToggleValue: boolean = $derived(
integration?.strava?.active || false,
);
const { form, errors, data: d } = createForm({
initialValues: {
clientId: data.integration?.strava?.clientId ?? "",
clientSecret: data.integration?.strava?.clientSecret ?? "",
routes: data.integration?.strava?.routes ?? true,
activities: data.integration?.strava?.activities ?? true,
active: data.integration?.strava?.active ?? false,
},
extend: validator({
schema: StravaSchema,
}),
onSubmit: async (form) => {
stravaSettingsModal.closeModal();
let komootSettingsModal: KomootSettingsModal;
let komootToggleValue: boolean = $state(data.integration?.komoot?.active ?? false);
try {
if (integration) {
integration.strava = {
clientId: form.clientId,
clientSecret: form.clientSecret,
routes: form.routes,
activities: form.activities,
active: integration.strava?.active ?? false,
};
integration = await integrations_update(integration);
} else {
const newIntegration = new Integration("", {
routes: form.routes,
activities: form.activities,
clientId: form.clientId,
clientSecret: form.clientSecret,
active: false,
});
integration = await integrations_create(newIntegration);
}
} catch (e) {
show_toast({
text: $_("error-setting-up-strava-integration"),
icon: "close",
type: "error",
});
async function onSettingsSave(
form: StravaIntegration | KomootIntegration,
key: "strava" | "komoot",
) {
try {
if (integration) {
integration[key] = form as any;
integration = await integrations_update(integration);
} else {
const newIntegration: Integration = {
user: "",
[key]: form,
};
integration = await integrations_create(newIntegration);
}
},
});
show_toast({
text: $_("settings-saved"),
icon: "check",
type: "success",
});
} catch (e) {
show_toast({
text: $_("error-setting-up-strava-integration"),
icon: "close",
type: "error",
});
}
}
async function onStravaToggle(value: boolean) {
if (!integration?.strava) {
return;
@@ -80,21 +71,13 @@
} else {
const deauthUrl = `https://www.strava.com/oauth/deauthorize`;
const r = await fetch(deauthUrl, {
await fetch(deauthUrl, {
method: "POST",
headers: {
Authorization: `Bearer ${integration.strava.accessToken}`,
},
});
if (!r.ok) {
show_toast({
text: $_("error-disabling-strava-integration"),
icon: "close",
type: "error",
});
return;
}
integration.strava = {
clientId: integration.strava.clientId,
clientSecret: integration.strava.clientSecret,
@@ -116,6 +99,41 @@
}
}
}
async function onKomootToggle(value: boolean) {
if (!integration?.komoot) {
return;
}
if (value) {
const authUrl = `https://api.komoot.de/v006/account/email/${integration.komoot.email}/`;
const r = await fetch(authUrl, {
method: "GET",
headers: {
Authorization: `Basic ${btoa(integration.komoot.email + ":" + integration.komoot.password)}`,
},
});
if (!r.ok) {
komootToggleValue = false;
show_toast({
text: $_("error-logging-in-to-komoot"),
icon: "close",
type: "error",
});
}
integration.komoot.active = true;
} else {
integration.komoot.active = false;
}
try {
integration = await integrations_update(integration);
} catch (e) {
show_toast({
text: $_("error-updating-strava-integration"),
icon: "close",
type: "error",
});
}
}
</script>
<svelte:head>
@@ -125,77 +143,35 @@
<h3 class="text-2xl font-semibold">{$_("integrations")}</h3>
<hr class="mt-4 mb-6 border-input-border" />
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
<div class="border border-input-border rounded-lg p-4 space-y-4">
<img
src="https://upload.wikimedia.org/wikipedia/commons/c/cb/Strava_Logo.svg"
alt="strava logo"
/>
<div>
<h5 class="text-xl font-semibold">strava</h5>
<p class="text-sm text-gray-500">
Syncs your strava routes with wanderer in regular intervals.
</p>
</div>
<div class="flex items-center justify-between">
<button
class="btn-secondary"
onclick={() => stravaSettingsModal.openModal()}
><i class="fa fa-cogs mr-2"></i>{$_("settings")}</button
>
<Toggle
value={stravaToggleValue}
onchange={onStravaToggle}
disabled={!integration?.strava}
></Toggle>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<IntegrationCard
img="https://upload.wikimedia.org/wikipedia/commons/c/cb/Strava_Logo.svg"
title="strava"
description={$_("integration-description-strava")}
disabled={!integration?.strava}
active={stravaToggleValue}
onclick={() => stravaSettingsModal.openModal()}
ontoggle={onStravaToggle}
></IntegrationCard>
<IntegrationCard
img="https://upload.wikimedia.org/wikipedia/commons/8/82/Komoot-logo-type.svg"
title="komoot"
description={$_("integration-description-komoot")}
disabled={!integration?.komoot}
bind:active={komootToggleValue}
onclick={() => komootSettingsModal.openModal()}
ontoggle={onKomootToggle}
></IntegrationCard>
</div>
<Modal
id="strava-settings-modal"
size="max-w-lg"
title={"strava " + $_("settings")}
<StravaSettingsModal
bind:this={stravaSettingsModal}
>
{#snippet content()}
<form id="strava-settings-form" class="space-y-2" use:form>
<TextField
label="Client-ID"
placeholder="000000"
name="clientId"
error={$errors.clientId}
></TextField>
<TextField
label="Client Secret"
placeholder="de8b3789bd7116d..."
name="clientSecret"
type="password"
error={$errors.clientSecret}
></TextField>
<div class="flex gap-x-4">
<Toggle name="routes" label={$_("route", { values: { n: 2 } })}
></Toggle>
<Toggle
name="activities"
label={$_("activity", { values: { n: 2 } })}
></Toggle>
</div>
</form>
{/snippet}
{#snippet footer()}
<div class="flex items-center gap-4">
<button
class="btn-secondary"
onclick={() => stravaSettingsModal.closeModal()}
>{$_("cancel")}</button
>
<button
class="btn-primary"
form="strava-settings-form"
type="submit"
name="save">{$_("save")}</button
>
</div>
{/snippet}</Modal
>
{integration}
onsave={(form) => onSettingsSave(form, "strava")}
></StravaSettingsModal>
<KomootSettingsModal
bind:this={komootSettingsModal}
{integration}
onsave={(form) => onSettingsSave(form, "komoot")}
></KomootSettingsModal>

View File

@@ -4,6 +4,10 @@ import { error, redirect, type RequestEvent, type ServerLoad } from "@sveltejs/k
export const load: ServerLoad = async ({ url, fetch }) => {
const oauthError = url.searchParams.get('error');
if (oauthError) {
// user cancelled
if(oauthError == "access_denied") {
return redirect(302, '/settings/integrations')
}
return error(400, {
message: oauthError
});

View File

@@ -10,7 +10,7 @@
{#if data.trail}
<TrailInfoPanel
activeTab={parseInt(page.url.searchParams.get("t") ?? "0")}
trail={data.trail}
initTrail={data.trail}
mode="overview"
></TrailInfoPanel>
{/if}