overhaul settings UI, move sync logic to sync store
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,13 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/components/ui/dialog"
|
||||
import { ListItem } from "@/shared/components/ui/list-item"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { useRef, useState } from "react"
|
||||
import { useDataManagement } from "../hooks/use-data-management"
|
||||
@@ -7,6 +16,7 @@ export function DataSettings() {
|
||||
const { exportData, importData, clearAll } = useDataManagement()
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
const [status, setStatus] = useState<string | null>(null)
|
||||
const [confirmOpen, setConfirmOpen] = useState(false)
|
||||
|
||||
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
@@ -20,35 +30,72 @@ export function DataSettings() {
|
||||
}
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!window.confirm(t("settings.data.clearConfirm"))) return
|
||||
setConfirmOpen(false)
|
||||
await clearAll()
|
||||
setStatus("All data cleared")
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Button onClick={exportData} variant="secondary" className="w-full">
|
||||
{t("settings.data.export")}
|
||||
</Button>
|
||||
|
||||
<div>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".json"
|
||||
onChange={handleImport}
|
||||
className="hidden"
|
||||
<>
|
||||
<div className="divide-y rounded-lg border bg-card">
|
||||
<ListItem
|
||||
title={t("settings.data.export")}
|
||||
after={
|
||||
<Button size="sm" variant="outline" onClick={exportData}>
|
||||
{t("settings.data.export")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<ListItem
|
||||
title={t("settings.data.import")}
|
||||
after={
|
||||
<Button size="sm" variant="outline" onClick={() => fileRef.current?.click()}>
|
||||
{t("settings.data.import")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button onClick={() => fileRef.current?.click()} variant="secondary" className="w-full">
|
||||
{t("settings.data.import")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Button onClick={handleClear} variant="destructive" className="w-full">
|
||||
{t("settings.data.clear")}
|
||||
</Button>
|
||||
<input ref={fileRef} type="file" accept=".json" onChange={handleImport} className="hidden" />
|
||||
|
||||
{status && <p className="text-sm text-muted-foreground">{status}</p>}
|
||||
</div>
|
||||
<div className="mt-4 divide-y rounded-lg border bg-card">
|
||||
<ListItem
|
||||
title={t("settings.data.clear")}
|
||||
after={
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
className="text-red-500"
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
>
|
||||
{t("settings.data.clear")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{status && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-muted-foreground">{status}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Dialog open={confirmOpen} onOpenChange={setConfirmOpen}>
|
||||
<DialogContent showCloseButton={false}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t("settings.data.clear")}</DialogTitle>
|
||||
<DialogDescription>{t("settings.data.clearConfirm")}</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setConfirmOpen(false)}>
|
||||
{t("general.cancel")}
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={handleClear}>
|
||||
{t("general.confirm")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { ListItem } from "@/shared/components/ui/list-item"
|
||||
import { useConfig, useSaveConfig } from "@/shared/db/hooks"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { useSyncStore } from "@/shared/stores/sync-store"
|
||||
import { useState } from "react"
|
||||
import { useGogSync } from "../hooks/use-gog-sync"
|
||||
|
||||
const GOG_AUTH_URL =
|
||||
"https://auth.gog.com/auth?client_id=46899977096215655&redirect_uri=https%3A%2F%2Fembed.gog.com%2Fon_login_success%3Forigin%3Dclient&response_type=code&layout=client2"
|
||||
@@ -12,13 +13,15 @@ export function GogSettings() {
|
||||
const gogConfig = useConfig<{ accessToken: string; refreshToken: string; userId: string }>("gog")
|
||||
const saveConfig = useSaveConfig()
|
||||
const lastSync = useConfig<string>("gog_last_sync")
|
||||
const { connect, syncGames, syncing, error, lastCount } = useGogSync()
|
||||
const { syncing, error, lastCount } = useSyncStore((s) => s.gog)
|
||||
const connectGog = useSyncStore((s) => s.connectGog)
|
||||
const syncGogGames = useSyncStore((s) => s.syncGogGames)
|
||||
|
||||
const [code, setCode] = useState("")
|
||||
const isConnected = Boolean(gogConfig?.accessToken)
|
||||
|
||||
const handleConnect = async () => {
|
||||
const tokens = await connect(code)
|
||||
const tokens = await connectGog(code)
|
||||
if (tokens) {
|
||||
setCode("")
|
||||
}
|
||||
@@ -26,7 +29,7 @@ export function GogSettings() {
|
||||
|
||||
const handleSync = () => {
|
||||
if (gogConfig) {
|
||||
syncGames(gogConfig.accessToken, gogConfig.refreshToken)
|
||||
syncGogGames(gogConfig.accessToken, gogConfig.refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,63 +38,79 @@ export function GogSettings() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<>
|
||||
{!isConnected ? (
|
||||
<>
|
||||
<div className="space-y-2 text-sm text-muted-foreground">
|
||||
<p>1. Open the GOG login page below</p>
|
||||
<p>2. Log in with your GOG account</p>
|
||||
<p>3. Copy the authorization code from the URL</p>
|
||||
<p>4. Paste the code below</p>
|
||||
<div className="space-y-2">
|
||||
<ol className="list-inside list-decimal space-y-1 text-sm text-muted-foreground">
|
||||
<li>Open the GOG login page below</li>
|
||||
<li>Log in with your GOG account</li>
|
||||
<li>Copy the authorization code from the URL</li>
|
||||
<li>Paste the code below</li>
|
||||
</ol>
|
||||
<a
|
||||
href={GOG_AUTH_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-blue-500 underline"
|
||||
>
|
||||
Open GOG Login →
|
||||
</a>
|
||||
</div>
|
||||
<a
|
||||
href={GOG_AUTH_URL}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-primary underline"
|
||||
>
|
||||
Open GOG Login →
|
||||
</a>
|
||||
<div>
|
||||
<label htmlFor="gog-code" className="mb-1 block text-sm font-medium">
|
||||
{t("settings.gog.code")}
|
||||
<div className="mt-4">
|
||||
{/* biome-ignore lint/a11y/noLabelWithoutControl: Input is inside the label */}
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium">{t("settings.gog.code")}</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="Paste authorization code"
|
||||
/>
|
||||
</label>
|
||||
<Input
|
||||
id="gog-code"
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
placeholder="Paste authorization code"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={handleConnect} disabled={syncing || !code}>
|
||||
{syncing ? t("settings.syncing") : t("settings.gog.connect")}
|
||||
</Button>
|
||||
<div className="mt-4">
|
||||
<Button onClick={handleConnect} disabled={syncing || !code}>
|
||||
{syncing ? t("settings.syncing") : t("settings.gog.connect")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">Connected as user {gogConfig?.userId}</p>
|
||||
<div className="flex gap-2">
|
||||
<div className="divide-y rounded-lg border bg-card">
|
||||
<ListItem title="Account" after={gogConfig?.userId} />
|
||||
</div>
|
||||
{lastSync && (
|
||||
<div className="mt-4 divide-y rounded-lg border bg-card">
|
||||
<ListItem
|
||||
title={t("settings.lastSync")}
|
||||
after={new Date(lastSync).toLocaleString()}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-4 flex gap-2">
|
||||
<Button onClick={handleSync} disabled={syncing}>
|
||||
{syncing ? t("settings.syncing") : t("settings.steam.sync")}
|
||||
</Button>
|
||||
<Button onClick={handleDisconnect} variant="destructive">
|
||||
<Button variant="ghost" className="text-red-500" onClick={handleDisconnect}>
|
||||
{t("settings.gog.disconnect")}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{error && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{lastCount !== null && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.syncSuccess", { count: lastCount })}
|
||||
</p>
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.syncSuccess", { count: lastCount })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{lastSync && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.lastSync")}: {new Date(lastSync).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +1,49 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { useConfig, useSaveConfig } from "@/shared/db/hooks"
|
||||
import { ListItem } from "@/shared/components/ui/list-item"
|
||||
import { useConfig } from "@/shared/db/hooks"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { useSyncStore } from "@/shared/stores/sync-store"
|
||||
import { Loader2 } from "lucide-react"
|
||||
import { useState } from "react"
|
||||
import { useSteamSync } from "../hooks/use-steam-sync"
|
||||
|
||||
function SyncProgress({ progress }: { progress: string | null }) {
|
||||
if (!progress) return null
|
||||
|
||||
if (progress === "fetching") {
|
||||
return (
|
||||
<div className="mt-4 flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t("settings.syncFetching")}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (progress.startsWith("saving:")) {
|
||||
const [, current, total] = progress.split(":")
|
||||
return (
|
||||
<div className="mt-4 space-y-1.5">
|
||||
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
{t("settings.syncSaving", { current, total })}
|
||||
</div>
|
||||
<div className="h-2 overflow-hidden rounded-full bg-muted">
|
||||
<div
|
||||
className="h-full rounded-full bg-primary transition-all"
|
||||
style={{ width: `${(Number(current) / Number(total)) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export function SteamSettings() {
|
||||
const savedConfig = useConfig<{ apiKey: string; steamId: string }>("steam")
|
||||
const saveConfig = useSaveConfig()
|
||||
const { sync, syncing, error, lastCount } = useSteamSync()
|
||||
const { syncing, error, lastCount, progress } = useSyncStore((s) => s.steam)
|
||||
const syncSteam = useSyncStore((s) => s.syncSteam)
|
||||
const lastSync = useConfig<string>("steam_last_sync")
|
||||
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
@@ -21,69 +56,73 @@ export function SteamSettings() {
|
||||
setInitialized(true)
|
||||
}
|
||||
|
||||
const extractSteamId = (input: string) => {
|
||||
const match = input.match(/\/profiles\/(\d+)/) || input.match(/\/id\/([^/]+)/)
|
||||
return match ? match[1] : input
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
const id = extractSteamId(steamId)
|
||||
await saveConfig("steam", { apiKey, steamId: id })
|
||||
setSteamId(id)
|
||||
}
|
||||
|
||||
const handleSync = () => {
|
||||
const id = extractSteamId(steamId)
|
||||
sync({ apiKey, steamId: id })
|
||||
syncSteam({ apiKey, steamId: steamId.trim() })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="steam-api-key" className="mb-1 block text-sm font-medium">
|
||||
{t("settings.steam.apiKey")}
|
||||
</label>
|
||||
<Input
|
||||
id="steam-api-key"
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Your Steam Web API Key"
|
||||
/>
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-muted-foreground">{t("settings.steam.instructions")}</p>
|
||||
<a
|
||||
href="https://steamcommunity.com/dev/apikey"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-block text-sm text-blue-500 underline"
|
||||
>
|
||||
steamcommunity.com/dev/apikey →
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label htmlFor="steam-id" className="mb-1 block text-sm font-medium">
|
||||
{t("settings.steam.steamId")}
|
||||
<div className="mt-4 space-y-3">
|
||||
{/* biome-ignore lint/a11y/noLabelWithoutControl: Input is inside the label */}
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium">{t("settings.steam.steamId")}</span>
|
||||
<Input
|
||||
type="text"
|
||||
value={steamId}
|
||||
onChange={(e) => setSteamId(e.target.value)}
|
||||
placeholder="Steam ID or profile URL"
|
||||
/>
|
||||
</label>
|
||||
{/* biome-ignore lint/a11y/noLabelWithoutControl: Input is inside the label */}
|
||||
<label className="space-y-1">
|
||||
<span className="text-sm font-medium">{t("settings.steam.apiKey")}</span>
|
||||
<Input
|
||||
type="password"
|
||||
value={apiKey}
|
||||
onChange={(e) => setApiKey(e.target.value)}
|
||||
placeholder="Your Steam Web API Key"
|
||||
/>
|
||||
</label>
|
||||
<Input
|
||||
id="steam-id"
|
||||
value={steamId}
|
||||
onChange={(e) => setSteamId(e.target.value)}
|
||||
placeholder="Steam ID or profile URL"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSave} variant="secondary">
|
||||
{t("general.save")}
|
||||
</Button>
|
||||
<div className="mt-4">
|
||||
<Button onClick={handleSync} disabled={syncing || !apiKey || !steamId}>
|
||||
{syncing ? t("settings.syncing") : t("settings.steam.sync")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
{lastCount !== null && (
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.syncSuccess", { count: lastCount })}
|
||||
</p>
|
||||
<SyncProgress progress={progress} />
|
||||
|
||||
{error && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-red-500">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
{!syncing && lastCount !== null && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("settings.syncSuccess", { count: lastCount })}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{lastSync && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.lastSync")}: {new Date(lastSync).toLocaleString()}
|
||||
</p>
|
||||
<div className="mt-4 divide-y rounded-lg border bg-card">
|
||||
<ListItem title={t("settings.lastSync")} after={new Date(lastSync).toLocaleString()} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import { useSaveConfig, useSaveGamesBySource } from "@/shared/db/hooks"
|
||||
import { api } from "@/shared/lib/api"
|
||||
import { useCallback, useState } from "react"
|
||||
|
||||
export function useGogSync() {
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastCount, setLastCount] = useState<number | null>(null)
|
||||
const saveConfig = useSaveConfig()
|
||||
const saveGames = useSaveGamesBySource()
|
||||
|
||||
const connect = useCallback(
|
||||
async (code: string) => {
|
||||
setSyncing(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await api.api.gog.auth.$post({ json: { code } })
|
||||
if (!res.ok) throw new Error(`GOG auth failed: ${res.status}`)
|
||||
const tokens = await res.json()
|
||||
|
||||
await saveConfig("gog", {
|
||||
accessToken: tokens.access_token,
|
||||
refreshToken: tokens.refresh_token,
|
||||
userId: tokens.user_id,
|
||||
})
|
||||
|
||||
return tokens
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
return null
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
},
|
||||
[saveConfig],
|
||||
)
|
||||
|
||||
const syncGames = useCallback(
|
||||
async (accessToken: string, refreshToken: string) => {
|
||||
setSyncing(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await api.api.gog.games.$post({
|
||||
json: { accessToken, refreshToken },
|
||||
})
|
||||
if (!res.ok) throw new Error(`GOG sync failed: ${res.status}`)
|
||||
const data = await res.json()
|
||||
|
||||
if (data.newAccessToken && data.newRefreshToken) {
|
||||
await saveConfig("gog", {
|
||||
accessToken: data.newAccessToken,
|
||||
refreshToken: data.newRefreshToken,
|
||||
})
|
||||
}
|
||||
|
||||
const dbGames = data.games.map((g) => ({
|
||||
id: g.id,
|
||||
title: g.title,
|
||||
source: g.source,
|
||||
source_id: g.sourceId,
|
||||
platform: g.platform,
|
||||
last_played: null,
|
||||
playtime_hours: 0,
|
||||
url: g.url ?? null,
|
||||
canonical_id: null,
|
||||
}))
|
||||
|
||||
await saveGames("gog", dbGames)
|
||||
await saveConfig("gog_last_sync", new Date().toISOString())
|
||||
setLastCount(data.count)
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
},
|
||||
[saveConfig, saveGames],
|
||||
)
|
||||
|
||||
return { connect, syncGames, syncing, error, lastCount }
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import { useSaveConfig, useSaveGamesBySource } from "@/shared/db/hooks"
|
||||
import { api } from "@/shared/lib/api"
|
||||
import { useCallback, useState } from "react"
|
||||
import type { SteamConfig } from "../schema"
|
||||
|
||||
export function useSteamSync() {
|
||||
const [syncing, setSyncing] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [lastCount, setLastCount] = useState<number | null>(null)
|
||||
const saveConfig = useSaveConfig()
|
||||
const saveGames = useSaveGamesBySource()
|
||||
|
||||
const sync = useCallback(
|
||||
async (config: SteamConfig) => {
|
||||
setSyncing(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await api.api.steam.games.$post({
|
||||
json: { apiKey: config.apiKey, steamId: config.steamId },
|
||||
})
|
||||
if (!res.ok) throw new Error(`Steam sync failed: ${res.status}`)
|
||||
const data = await res.json()
|
||||
|
||||
const dbGames = data.games.map((g) => ({
|
||||
id: g.id,
|
||||
title: g.title,
|
||||
source: g.source,
|
||||
source_id: g.sourceId,
|
||||
platform: g.platform,
|
||||
last_played: g.lastPlayed,
|
||||
playtime_hours: g.playtimeHours,
|
||||
url: g.url,
|
||||
canonical_id: null,
|
||||
}))
|
||||
|
||||
await saveGames("steam", dbGames)
|
||||
await saveConfig("steam_last_sync", new Date().toISOString())
|
||||
setLastCount(data.count)
|
||||
} catch (err) {
|
||||
setError((err as Error).message)
|
||||
} finally {
|
||||
setSyncing(false)
|
||||
}
|
||||
},
|
||||
[saveConfig, saveGames],
|
||||
)
|
||||
|
||||
return { sync, syncing, error, lastCount }
|
||||
}
|
||||
@@ -1,12 +1,9 @@
|
||||
import { DataSettings } from "@/features/settings/components/data-settings"
|
||||
import { GogSettings } from "@/features/settings/components/gog-settings"
|
||||
import { SteamSettings } from "@/features/settings/components/steam-settings"
|
||||
import { Link, createFileRoute } from "@tanstack/react-router"
|
||||
import { ArrowLeft } from "lucide-react"
|
||||
|
||||
export const Route = createFileRoute("/settings/$provider")({
|
||||
component: ProviderSettingsPage,
|
||||
})
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { createFileRoute, useNavigate } from "@tanstack/react-router"
|
||||
import { ChevronLeft } from "lucide-react"
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
steam: "Steam",
|
||||
@@ -14,19 +11,27 @@ const titles: Record<string, string> = {
|
||||
data: "Data",
|
||||
}
|
||||
|
||||
export const Route = createFileRoute("/settings/$provider")({
|
||||
component: ProviderSettingsPage,
|
||||
})
|
||||
|
||||
function ProviderSettingsPage() {
|
||||
const { provider } = Route.useParams()
|
||||
const navigate = useNavigate()
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-lg p-4">
|
||||
<Link to="/settings" className="mb-4 flex items-center gap-1 text-sm text-muted-foreground">
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Back
|
||||
</Link>
|
||||
<h1 className="mb-6 text-2xl font-bold">{titles[provider] ?? provider}</h1>
|
||||
{provider === "steam" && <SteamSettings />}
|
||||
{provider === "gog" && <GogSettings />}
|
||||
{provider === "data" && <DataSettings />}
|
||||
<div>
|
||||
<header className="flex items-center gap-2 px-2 pt-4 pb-2">
|
||||
<Button variant="ghost" size="icon-sm" onClick={() => navigate({ to: "/settings" })}>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Button>
|
||||
<h1 className="text-xl font-bold">{titles[provider] ?? provider}</h1>
|
||||
</header>
|
||||
<div className="mx-auto max-w-lg p-4">
|
||||
{provider === "steam" && <SteamSettings />}
|
||||
{provider === "gog" && <GogSettings />}
|
||||
{provider === "data" && <DataSettings />}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,8 +8,10 @@ export const Route = createFileRoute("/settings/")({
|
||||
|
||||
function SettingsPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-lg p-4">
|
||||
<h1 className="mb-6 text-2xl font-bold">{t("settings.title")}</h1>
|
||||
<div>
|
||||
<header className="px-4 pt-4 pb-2">
|
||||
<h1 className="text-2xl font-bold">{t("settings.title")}</h1>
|
||||
</header>
|
||||
<SettingsList />
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user