adds strava integration

This commit is contained in:
Christian Beutel
2025-02-04 11:43:48 +01:00
parent 3ab591177a
commit a0b6480854
32 changed files with 1737 additions and 57 deletions

View File

@@ -1,11 +1,11 @@
<script lang="ts">
interface Props {
name?: string;
value?: boolean;
label?: string;
error?: string;
onchange?: (value: boolean) => void
disabled?: boolean;
onchange?: (value: boolean) => void;
}
let {
@@ -13,30 +13,35 @@
value = $bindable(false),
label = "",
error = "",
onchange
disabled = false,
onchange,
}: Props = $props();
function handleToggleChange() {
onchange?.(value);
}
</script>
<label class="relative my-2 inline-flex items-center cursor-pointer">
<input
{name}
bind:checked={value}
type="checkbox"
class="sr-only peer"
value="1"
onchange={handleToggleChange}
/>
<div
class="w-11 h-6 bg-input-background border border-input-border peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-input-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"
></div>
<span class="ms-3 text-sm font-medium">{label}</span>
</label>
<div>
<label class="relative my-2 inline-flex items-center" class:cursor-pointer={!disabled}>
<input
{name}
bind:checked={value}
type="checkbox"
class="sr-only peer"
value="1"
{disabled}
onchange={handleToggleChange}
/>
<div
class="w-11 h-6 bg-input-background border border-input-border peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-input-ring rounded-full peer peer-checked:after:translate-x-full rtl:peer-checked:after:-translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-primary"
></div>
{#if label}
<span class="ms-3 text-sm font-medium">{label}</span>
{/if}
</label>
<span class="toggle-error text-xs text-red-400">
{error}
</span>
<p class="toggle-error text-xs text-red-400">
{error}
</p>
</div>

View File

@@ -362,7 +362,7 @@
{:else}
<EmptyStateDescription></EmptyStateDescription>
{/if}
<h4 class="text-2xl font-semibold mb-6 mt-12">{$_("route")}</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

@@ -48,6 +48,7 @@
"confirm-deletion": "Löschen bestätigen",
"confirm-publish": "Veröffentlichen bestätigen",
"confirm-share": "Teilen bestätigen",
"connect": "",
"contribute": "Mitwirken",
"copy-link": "Link kopieren",
"create-new-list": "Neue Liste erstellen",
@@ -95,6 +96,7 @@
"english": "Englisch",
"entry": "Eintrag",
"error-creating-user": "Fehler beim Erstellen des Nutzers",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Fehler beim Lesen der Datei",
"error-saving-list": "Fehler beim Speichern der Liste",
"error-saving-trail": "Fehler beim Speichern der Route",
"error-setting-up-strava-integration": "",
"error-updating-password": "Fehler beim Aktualisieren des Passworts",
"est-duration": "Gesch. Dauer",
"explore": "Erkunden",
@@ -140,6 +143,7 @@
"import": "Importieren",
"import-hint": "GPX, FIT, KML oder TCX Dateien auswählen oder hierher ziehen...",
"include-description": "Beschreibung übernehmen",
"integrations": "",
"invalid-date": "Ungültiges Datum",
"invalid-username": "Ungültiger Nutzername",
"italian": "Italienisch",
@@ -229,7 +233,7 @@
"removed-trail-from": "Route entfernt aus",
"required": "Pflichtfeld",
"reset-password": "Passwort zurücksetzen",
"route": "",
"route": "{n, plural, =1 {Route} other {Routen}}",
"route-point": "Routenpunkt",
"save": "Speichern",
"save-list": "Liste speichern",
@@ -291,4 +295,4 @@
"welcome_to": "Willkommen bei",
"wrong-username-or-password": "Falscher Nutzername oder falsches Passwort",
"you-can": "Du kannst"
}
}

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirm Deletion",
"confirm-publish": "Confirm publishing",
"confirm-share": "Confirm share",
"connect": "Connect",
"contribute": "Contribute",
"copy-link": "Copy Link",
"create-new-list": "Create new list",
@@ -95,6 +96,7 @@
"english": "English",
"entry": "Entry",
"error-creating-user": "Error creating user",
"error-disabling-strava-integration": "Error disabling strava integration",
"error-during-login": "Error during login",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
@@ -103,6 +105,7 @@
"error-reading-file": "Error reading file",
"error-saving-list": "Error saving list",
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "Error setting up strava integration",
"error-updating-password": "Error updating password",
"est-duration": "Est. duration",
"explore": "Explore",
@@ -140,6 +143,7 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description",
"integrations": "Integrations",
"invalid-date": "Invalid Date",
"invalid-username": "Invalid username",
"italian": "Italian",
@@ -229,7 +233,7 @@
"removed-trail-from": "Removed trail from",
"required": "Required",
"reset-password": "Reset Password",
"route": "Route",
"route": "{n, plural, =1 {Route} other {Routes}}",
"route-point": "Route Point",
"save": "Save",
"save-list": "Save List",
@@ -291,4 +295,4 @@
"welcome_to": "Welcome to",
"wrong-username-or-password": "Wrong username or password",
"you-can": "You can"
}
}

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirmar Borrado",
"confirm-publish": "Confirmar publicación",
"confirm-share": "Confirmar compartir",
"connect": "",
"contribute": "Contribuir",
"copy-link": "Copiar Enlace",
"create-new-list": "Crear una nueva lista",
@@ -95,6 +96,7 @@
"english": "Inglés",
"entry": "Entrada",
"error-creating-user": "Error creando el usuario",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Error leyendo el archivo",
"error-saving-list": "Error guardando la lista",
"error-saving-trail": "Error guardando la ruta",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error actualizando la contraseña",
"est-duration": "Duración estimada",
"explore": "Explora",
@@ -140,6 +143,7 @@
"import": "Importar",
"import-hint": "Selecciona o arrastra aquí archivos GPX, FIT, KML o TCX...",
"include-description": "Incluir descripción",
"integrations": "",
"invalid-date": "Fecha no válida",
"invalid-username": "Usuario no válido",
"italian": "Italiano",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirmer la suppression",
"confirm-publish": "Confirmer la publication",
"confirm-share": "Confirmer le partage",
"connect": "",
"contribute": "Contribuer",
"copy-link": "Copier le lien",
"create-new-list": "Créer une nouvelle liste",
@@ -95,6 +96,7 @@
"english": "Anglais",
"entry": "Entrée",
"error-creating-user": "Erreur durant la création de l'utilisateur",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Erreur de lecture du fichier",
"error-saving-list": "Error saving list",
"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",
"est-duration": "Temps estimé",
"explore": "Explorer",
@@ -140,6 +143,7 @@
"import": "Importer",
"import-hint": "Sélectionnez ou glissez des fichiers GPX, FIT, KML ou TCX ici...",
"include-description": "Inclure la description",
"integrations": "",
"invalid-date": "Date invalide",
"invalid-username": "Nom d'utilisateur invalide",
"italian": "Italien",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirm Deletion",
"confirm-publish": "Confirm publishing",
"confirm-share": "Confirm share",
"connect": "",
"contribute": "Hozzájárulás",
"copy-link": "Copy Link",
"create-new-list": "Új lista létrehozása",
@@ -95,6 +96,7 @@
"english": "Angol",
"entry": "Bejegyzés",
"error-creating-user": "Hiba felhasználó hozzáadása közben",
"error-disabling-strava-integration": "",
"error-during-login": "Hiba bejelentkezés közben",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
@@ -103,6 +105,7 @@
"error-reading-file": "Hiba a fájl olvasása közben",
"error-saving-list": "Error saving list",
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error updating password",
"est-duration": "Becsült időtartam",
"explore": "Felfedezés",
@@ -140,6 +143,7 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Include description",
"integrations": "",
"invalid-date": "Érvénytelen dátum",
"invalid-username": "Érvénytelen felhasználó",
"italian": "Olasz",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Conferma eliminazione",
"confirm-publish": "Conferma pubblicazione",
"confirm-share": "Conferma condivisione",
"connect": "",
"contribute": "Contribuisci",
"copy-link": "Copia link",
"create-new-list": "Crea nuova lista",
@@ -95,6 +96,7 @@
"english": "Inglese",
"entry": "Voce",
"error-creating-user": "Errore nella creazione dell'utente",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Errore durante la lettura del file",
"error-saving-list": "Errore salvando la lista",
"error-saving-trail": "Errore nel salvataggio del percorso",
"error-setting-up-strava-integration": "",
"error-updating-password": "Errore nell'aggiornamento della password",
"est-duration": "Durata stimata",
"explore": "Esplora",
@@ -140,6 +143,7 @@
"import": "Importa",
"import-hint": "Seleziona o trascina qui i file GPX, FIT, KML o TCX...",
"include-description": "Adotta descrizione",
"integrations": "",
"invalid-date": "Data non valida",
"invalid-username": "Nome utente non valido",
"italian": "Italiano",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirm Deletion",
"confirm-publish": "Confirm publishing",
"confirm-share": "Confirm share",
"connect": "",
"contribute": "Bijdragen",
"copy-link": "Copy Link",
"create-new-list": "Nieuwe lijst",
@@ -95,6 +96,7 @@
"english": "Engels",
"entry": "Item",
"error-creating-user": "Het account kan niet worden aangemaakt",
"error-disabling-strava-integration": "",
"error-during-login": "Het inloggen is mislukt",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "Error exporting trail",
@@ -103,6 +105,7 @@
"error-reading-file": "Het bestand kan niet worden ingelezen",
"error-saving-list": "Error saving list",
"error-saving-trail": "Error saving trail",
"error-setting-up-strava-integration": "",
"error-updating-password": "Error updating password",
"est-duration": "Geschatte duur",
"explore": "Verkennen",
@@ -140,6 +143,7 @@
"import": "Import",
"import-hint": "Select or drag GPX, FIT, KML or TCX files here...",
"include-description": "Inclusief beschrijving",
"integrations": "",
"invalid-date": "Ongeldige datum",
"invalid-username": "Ongeldige gebruikersnaam",
"italian": "Italiaans",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Potwierdź usunięcie",
"confirm-publish": "Potwierdź publikację",
"confirm-share": "Potwierdź udostępnienie",
"connect": "",
"contribute": "Kontrybuuj",
"copy-link": "Kopiuj link",
"create-new-list": "Stwórz nową listę",
@@ -95,6 +96,7 @@
"english": "Angielski",
"entry": "Pozycja",
"error-creating-user": "Błąd tworzenia użytkownika",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Błąd wczytywania pliku",
"error-saving-list": "Błąd przy zapisywaniu listy",
"error-saving-trail": "Błąd podczas zapisywania szlaku",
"error-setting-up-strava-integration": "",
"error-updating-password": "Błąd podczas aktualizacji hasła",
"est-duration": "Szacowany czas",
"explore": "Eksploruj",
@@ -140,6 +143,7 @@
"import": "Importuj",
"import-hint": "Wybierz lub przeciągnij tutaj plik GPX, FIT, KML lub TCX...",
"include-description": "Dołącz opis",
"integrations": "",
"invalid-date": "Nieprawidłowa data",
"invalid-username": "Błędna nazwa użytkownika",
"italian": "Włoski",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "Confirmar eliminação",
"confirm-publish": "Confirm publishing",
"confirm-share": "Confirmar partilha",
"connect": "",
"contribute": "Contribuir",
"copy-link": "Copiar ligação",
"create-new-list": "Criar nova lista",
@@ -95,6 +96,7 @@
"english": "Inglês",
"entry": "Entrada",
"error-creating-user": "Erro ao criar utilizador",
"error-disabling-strava-integration": "",
"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",
@@ -103,6 +105,7 @@
"error-reading-file": "Erro ao ler o arquivo",
"error-saving-list": "Erro ao gravar lista",
"error-saving-trail": "Erro ao gravar percurso",
"error-setting-up-strava-integration": "",
"error-updating-password": "Erro ao atualizar password",
"est-duration": "Duração prevista",
"explore": "Explorar",
@@ -140,6 +143,7 @@
"import": "Importar",
"import-hint": "Selecionar ou arrastar ficheiros GPX, FIT, KML ou TCX para aqui...",
"include-description": "Incluir descrição",
"integrations": "",
"invalid-date": "Data inválida",
"invalid-username": "Nome de usuário inválido",
"italian": "Italiano",

View File

@@ -48,6 +48,7 @@
"confirm-deletion": "确认删除",
"confirm-publish": "Confirm publishing",
"confirm-share": "确认分享",
"connect": "",
"contribute": "贡献",
"copy-link": "复制链接",
"create-new-list": "创建新列表",
@@ -95,6 +96,7 @@
"english": "英语",
"entry": "日程",
"error-creating-user": "创建用户错误",
"error-disabling-strava-integration": "",
"error-during-login": "登录错误",
"error-during-password-reset": "Unable to send password reset email",
"error-exporting-trail": "导出路线失败",
@@ -103,6 +105,7 @@
"error-reading-file": "读取文件错误",
"error-saving-list": "保存列表失败",
"error-saving-trail": "保存路线失败",
"error-setting-up-strava-integration": "",
"error-updating-password": "更新密码失败",
"est-duration": "预计时长",
"explore": "探索",
@@ -140,6 +143,7 @@
"import": "导入",
"import-hint": "在此选择或拖拽GPX、FIT、KML或TCX文件...",
"include-description": "包含描述",
"integrations": "",
"invalid-date": "无效日期",
"invalid-username": "无效用户名",
"italian": "意大利语",

View File

@@ -0,0 +1,25 @@
import { z, ZodType } from "zod";
import type { Integration } from "../integration";
const StravaSchema = z.object({
clientId: z.number({ coerce: true }).int().positive(),
clientSecret: z.string().length(40),
routes: z.boolean(),
activities: z.boolean(),
accessToken: z.string().length(40).optional(),
refreshToken: z.string().length(40).optional(),
expiresAt: z.number().int().positive().optional(),
active: z.boolean()
})
const IntegrationCreateSchema = z.object({
user: z.string().length(15),
strava: StravaSchema,
}) satisfies ZodType<Integration>
const IntegrationUpdateSchema = z.object({
strava: StravaSchema.optional(),
}) satisfies ZodType<Partial<Integration>>
export { StravaSchema, IntegrationCreateSchema, IntegrationUpdateSchema }

View File

@@ -0,0 +1,25 @@
interface BaseIntegration {
active: boolean
}
interface StravaIntegration extends BaseIntegration {
clientId: string | number;
clientSecret: string;
routes: boolean;
activities: boolean;
accessToken?: string;
refreshToken?: string;
expiresAt?: number;
}
export class Integration {
id?: string;
user: string;
strava?: StravaIntegration;
constructor(user: string, strava?: StravaIntegration) {
this.user = user;
this.strava = strava;
}
}

View File

@@ -0,0 +1,74 @@
import { Integration } from "$lib/models/integration";
import { pb } from "$lib/pocketbase";
import { APIError } from "$lib/util/api_util";
import { type ListResult } from "pocketbase";
import { writable, type Writable } from "svelte/store";
export const integrations: Writable<Integration[]> = writable([])
export async function integrations_index(f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
let r = await f('/api/v1/integration' + new URLSearchParams({
}), {
method: 'GET',
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
const fetchedIntegrations: ListResult<Integration> = await r.json();
integrations.set(fetchedIntegrations.items);
return fetchedIntegrations.items;
}
export async function integrations_create(integration: Integration) {
if (!pb.authStore.model) {
throw new Error("Unauthenticated");
}
integration.user = pb.authStore.model!.id;
let r = await fetch('/api/v1/integration', {
method: 'PUT',
body: JSON.stringify(integration),
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
const model: Integration = await r.json();
return model;
}
export async function integrations_update(integration: Integration, f: (url: RequestInfo | URL, config?: RequestInit) => Promise<Response> = fetch) {
let r = await f('/api/v1/integration/' + integration.id, {
method: 'POST',
body: JSON.stringify(integration),
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
const model: Integration = await r.json();
return model;
}
export async function integrations_delete(integration: Integration) {
const r = await fetch('/api/v1/integration/' + integration.id, {
method: 'DELETE',
})
if (!r.ok) {
const response = await r.json();
throw new APIError(r.status, response.message, response.detail)
}
}

View File

@@ -23,6 +23,7 @@ export enum Collection {
categories = "categories",
comments = "comments",
follows = "follows",
integrations = "integrations",
list_share = "list_share",
lists = "lists",
notifications = "notifications",