init
This commit is contained in:
28
app/settings/actions.ts
Normal file
28
app/settings/actions.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { saveGarminCredentials } from "@/lib/models/garmin-auth";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
|
||||
export type SaveGarminCredentialsState = { error: string } | { success: true } | null;
|
||||
|
||||
export async function saveGarminCredentialsAction(
|
||||
_prevState: SaveGarminCredentialsState,
|
||||
formData: FormData
|
||||
): Promise<SaveGarminCredentialsState> {
|
||||
const email = formData.get("email");
|
||||
const password = formData.get("password");
|
||||
|
||||
if (typeof email !== "string" || !email.includes("@")) {
|
||||
return { error: "Podaj prawidłowy adres e-mail." };
|
||||
}
|
||||
if (typeof password !== "string" || password.length < 1) {
|
||||
return { error: "Podaj hasło." };
|
||||
}
|
||||
|
||||
const userId = await getCurrentUserId();
|
||||
await saveGarminCredentials(userId, email.trim(), password);
|
||||
|
||||
revalidatePath("/settings");
|
||||
return { success: true };
|
||||
}
|
||||
58
app/settings/garmin-credentials-form.tsx
Normal file
58
app/settings/garmin-credentials-form.tsx
Normal file
@@ -0,0 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { useActionState } from "react";
|
||||
import { saveGarminCredentialsAction, type SaveGarminCredentialsState } from "./actions";
|
||||
|
||||
export function GarminCredentialsForm({ savedEmail }: { savedEmail: string | null }) {
|
||||
const [state, action, pending] = useActionState<SaveGarminCredentialsState, FormData>(
|
||||
saveGarminCredentialsAction,
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<form action={action} className="flex flex-col gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="garmin-email" className="text-sm text-fg/70">
|
||||
E-mail Garmin Connect
|
||||
</label>
|
||||
<input
|
||||
id="garmin-email"
|
||||
name="email"
|
||||
type="email"
|
||||
defaultValue={savedEmail ?? ""}
|
||||
placeholder="twoj@email.com"
|
||||
required
|
||||
className="rounded-md border border-muted/40 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label htmlFor="garmin-password" className="text-sm text-fg/70">
|
||||
Hasło Garmin Connect
|
||||
</label>
|
||||
<input
|
||||
id="garmin-password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
required
|
||||
className="rounded-md border border-muted/40 bg-bg px-3 py-2 text-sm text-fg placeholder:text-fg/30 focus:border-accent/60 focus:outline-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state && "error" in state && (
|
||||
<p className="text-sm text-red-400">{state.error}</p>
|
||||
)}
|
||||
{state && "success" in state && (
|
||||
<p className="text-sm text-accent">Zapisano dane logowania.</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={pending}
|
||||
className="self-start rounded-md bg-accent px-4 py-2 text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
|
||||
>
|
||||
{pending ? "Zapisywanie…" : "Zapisz"}
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -2,22 +2,30 @@ import { CheckCircle2, XCircle } from "lucide-react";
|
||||
import { SyncButton } from "@/components/sync-button";
|
||||
import { formatDate } from "@/lib/format";
|
||||
import { getLastSyncAt } from "@/lib/models/running";
|
||||
import { getGarminCredentials, getSavedOauth1Token } from "@/lib/models/garmin-auth";
|
||||
import { getCurrentUserId } from "@/lib/session";
|
||||
import { GarminCredentialsForm } from "./garmin-credentials-form";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
function ConfigRow({ label, configured }: { label: string; configured: boolean }) {
|
||||
function StatusRow({ label, ok, okLabel = "Skonfigurowano", failLabel = "Brak konfiguracji" }: {
|
||||
label: string;
|
||||
ok: boolean;
|
||||
okLabel?: string;
|
||||
failLabel?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<span className="text-fg">{label}</span>
|
||||
{configured ? (
|
||||
{ok ? (
|
||||
<span className="flex items-center gap-1.5 text-sm text-fg/70">
|
||||
<CheckCircle2 size={16} className="text-accent" />
|
||||
Skonfigurowano
|
||||
{okLabel}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1.5 text-sm text-fg/50">
|
||||
<XCircle size={16} />
|
||||
Brak w .env.local
|
||||
{failLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -25,31 +33,52 @@ function ConfigRow({ label, configured }: { label: string; configured: boolean }
|
||||
}
|
||||
|
||||
export default async function SettingsPage() {
|
||||
const lastSyncAt = await getLastSyncAt();
|
||||
|
||||
const mongoConfigured = Boolean(process.env.MONGODB_URI);
|
||||
const garminConfigured = Boolean(process.env.GARMIN_EMAIL && process.env.GARMIN_PASSWORD);
|
||||
const claudeConfigured = Boolean(process.env.ANTHROPIC_API_KEY);
|
||||
const userId = await getCurrentUserId();
|
||||
const [lastSyncAt, garminCreds, garminToken] = await Promise.all([
|
||||
getLastSyncAt(userId),
|
||||
getGarminCredentials(userId),
|
||||
getSavedOauth1Token(userId),
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-fg">Ustawienia</h1>
|
||||
<p className="mt-1 text-sm text-fg/60">Status konfiguracji i synchronizacja Garmin.</p>
|
||||
<p className="mt-1 text-sm text-fg/60">Konfiguracja konta i synchronizacja Garmin.</p>
|
||||
</div>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold text-fg">Konfiguracja</h2>
|
||||
<ConfigRow label="MongoDB" configured={mongoConfigured} />
|
||||
<ConfigRow label="Garmin Connect" configured={garminConfigured} />
|
||||
<ConfigRow label="Claude API" configured={claudeConfigured} />
|
||||
<h2 className="text-lg font-semibold text-fg">Status</h2>
|
||||
<StatusRow label="MongoDB" ok={Boolean(process.env.MONGODB_URI)} />
|
||||
<StatusRow
|
||||
label="Garmin Connect — token sesji"
|
||||
ok={Boolean(garminToken)}
|
||||
okLabel="Aktywny (synchronizacja działa)"
|
||||
failLabel="Brak tokenu — wymagane logowanie"
|
||||
/>
|
||||
<StatusRow
|
||||
label="Garmin Connect — dane logowania"
|
||||
ok={Boolean(garminCreds)}
|
||||
okLabel={`Zapisano (${garminCreds?.email})`}
|
||||
failLabel="Brak — potrzebne gdy token wygaśnie"
|
||||
/>
|
||||
<StatusRow label="Claude API" ok={Boolean(process.env.ANTHROPIC_API_KEY)} />
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold text-fg">Konto Garmin Connect</h2>
|
||||
<div className="rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<GarminCredentialsForm savedEmail={garminCreds?.email ?? null} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="flex flex-col gap-3">
|
||||
<h2 className="text-lg font-semibold text-fg">Synchronizacja z Garmin</h2>
|
||||
<div className="flex items-center justify-between rounded-lg border border-muted/40 bg-surface p-4">
|
||||
<span className="text-sm text-fg/70">
|
||||
{lastSyncAt ? `Ostatnia synchronizacja: ${formatDate(lastSyncAt)}` : "Jeszcze nie zsynchronizowano"}
|
||||
{lastSyncAt
|
||||
? `Ostatnia synchronizacja: ${formatDate(lastSyncAt)}`
|
||||
: "Jeszcze nie zsynchronizowano"}
|
||||
</span>
|
||||
<SyncButton />
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user