import { useCallback, useEffect, useRef, useState } from "react"; import { data, redirect, useFetcher } from "react-router"; import { useTranslation } from "react-i18next"; import type { Route } from "./+types/sync.import.$provider"; import { getSessionUser } from "~/lib/auth.server"; import { getProvider } from "~/lib/sync/registry"; import { getConnection, updateTokens } from "~/lib/sync/connections.server"; import { getImportedIds, recordImport } from "~/lib/sync/imports.server"; import { createActivity } from "~/lib/activities.server"; import { ClientDate } from "~/components/ClientDate"; export async function loader({ params, request }: Route.LoaderArgs) { const user = await getSessionUser(request); if (!user) return redirect("/auth/login"); const provider = getProvider(params.provider); if (!provider) throw data({ error: "Unknown provider" }, { status: 404 }); const connection = await getConnection(user.id, provider.id); if (!connection) return redirect("/settings"); const url = new URL(request.url); const page = parseInt(url.searchParams.get("page") ?? "1"); // Refresh token if needed let tokens = { accessToken: connection.accessToken, refreshToken: connection.refreshToken, expiresAt: connection.expiresAt, }; if (new Date() >= tokens.expiresAt) { tokens = await provider.refreshToken(tokens.refreshToken); await updateTokens(connection.id, tokens); } const workoutList = await provider.listWorkouts(tokens, page); const importedIds = await getImportedIds( user.id, provider.id, workoutList.workouts.map((w) => w.id), ); return data({ provider: { id: provider.id, name: provider.name }, workouts: workoutList.workouts.map((w) => ({ ...w, imported: importedIds.has(w.id), })), page: workoutList.page, totalPages: Math.ceil(workoutList.total / workoutList.perPage), }); } export async function action({ params, request }: Route.ActionArgs) { const user = await getSessionUser(request); if (!user) return redirect("/auth/login"); const provider = getProvider(params.provider); if (!provider) throw data({ error: "Unknown provider" }, { status: 404 }); const connection = await getConnection(user.id, provider.id); if (!connection) return redirect("/settings"); const formData = await request.formData(); const workoutId = formData.get("workoutId") as string; const workoutName = formData.get("workoutName") as string; const fileUrl = formData.get("fileUrl") as string; const startedAt = formData.get("startedAt") as string; const distance = formData.get("distance") as string; const duration = formData.get("duration") as string; // Refresh token if needed let tokens = { accessToken: connection.accessToken, refreshToken: connection.refreshToken, expiresAt: connection.expiresAt, }; if (new Date() >= tokens.expiresAt) { tokens = await provider.refreshToken(tokens.refreshToken); await updateTokens(connection.id, tokens); } let gpx: string | null = null; if (fileUrl) { const workout = { id: workoutId, name: workoutName, type: "", startedAt: "", duration: null, distance: null, fileUrl }; const fileBuffer = await provider.downloadFile(tokens, workout); gpx = await provider.convertToGpx(fileBuffer); } const activityId = await createActivity(user.id, { name: workoutName || `${provider.name} workout`, gpx: gpx ?? undefined, distance: distance ? parseFloat(distance) : null, duration: duration ? parseInt(duration) : null, startedAt: startedAt ? new Date(startedAt) : null, }); await recordImport(user.id, provider.id, workoutId, activityId); return data({ imported: workoutId }); } export function meta({ data: loaderData }: Route.MetaArgs) { const name = (loaderData as { provider: { name: string } })?.provider?.name ?? "Import"; return [{ title: `Import from ${name} — trails.cool` }]; } function WorkoutRow({ workout, provider, alreadyImported, }: { workout: { id: string; name: string; fileUrl?: string; startedAt: string; distance: number | null; duration: number | null }; provider: { id: string; name: string }; alreadyImported: boolean; }) { const { t } = useTranslation("journal"); const fetcher = useFetcher<{ imported?: string }>(); const isImporting = fetcher.state !== "idle"; const isImported = alreadyImported || fetcher.data?.imported === workout.id; return (
{workout.name}
{!workout.fileUrl && ( {t("sync.noGps")} )}{t("sync.noWorkouts")}
) : (