add settings feature: steam, gog providers, data management
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
src/features/settings/components/data-settings.tsx
Normal file
54
src/features/settings/components/data-settings.tsx
Normal file
@@ -0,0 +1,54 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { useRef, useState } from "react"
|
||||
import { useDataManagement } from "../hooks/use-data-management"
|
||||
|
||||
export function DataSettings() {
|
||||
const { exportData, importData, clearAll } = useDataManagement()
|
||||
const fileRef = useRef<HTMLInputElement>(null)
|
||||
const [status, setStatus] = useState<string | null>(null)
|
||||
|
||||
const handleImport = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
try {
|
||||
await importData(file)
|
||||
setStatus("Import complete")
|
||||
} catch {
|
||||
setStatus("Import failed")
|
||||
}
|
||||
}
|
||||
|
||||
const handleClear = async () => {
|
||||
if (!window.confirm(t("settings.data.clearConfirm"))) return
|
||||
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"
|
||||
/>
|
||||
<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>
|
||||
|
||||
{status && <p className="text-sm text-muted-foreground">{status}</p>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
97
src/features/settings/components/gog-settings.tsx
Normal file
97
src/features/settings/components/gog-settings.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { useConfig, useSaveConfig } from "@/shared/db/hooks"
|
||||
import { t } from "@/shared/i18n"
|
||||
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"
|
||||
|
||||
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 [code, setCode] = useState("")
|
||||
const isConnected = Boolean(gogConfig?.accessToken)
|
||||
|
||||
const handleConnect = async () => {
|
||||
const tokens = await connect(code)
|
||||
if (tokens) {
|
||||
setCode("")
|
||||
}
|
||||
}
|
||||
|
||||
const handleSync = () => {
|
||||
if (gogConfig) {
|
||||
syncGames(gogConfig.accessToken, gogConfig.refreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
await saveConfig("gog", null)
|
||||
}
|
||||
|
||||
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>
|
||||
<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")}
|
||||
</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>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm text-muted-foreground">Connected as user {gogConfig?.userId}</p>
|
||||
<div className="flex gap-2">
|
||||
<Button onClick={handleSync} disabled={syncing}>
|
||||
{syncing ? t("settings.syncing") : t("settings.steam.sync")}
|
||||
</Button>
|
||||
<Button onClick={handleDisconnect} variant="destructive">
|
||||
{t("settings.gog.disconnect")}
|
||||
</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>
|
||||
)}
|
||||
{lastSync && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.lastSync")}: {new Date(lastSync).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
52
src/features/settings/components/settings-list.tsx
Normal file
52
src/features/settings/components/settings-list.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Badge } from "@/shared/components/ui/badge"
|
||||
import { Card } from "@/shared/components/ui/card"
|
||||
import { useConfig } from "@/shared/db/hooks"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { Link } from "@tanstack/react-router"
|
||||
|
||||
const providers = [
|
||||
{ id: "steam", label: "Steam" },
|
||||
{ id: "gog", label: "GOG" },
|
||||
] as const
|
||||
|
||||
export function SettingsList() {
|
||||
const steamConfig = useConfig<{ apiKey: string; steamId: string }>("steam")
|
||||
const gogConfig = useConfig<{ accessToken: string }>("gog")
|
||||
|
||||
const isConnected = (id: string) => {
|
||||
if (id === "steam") return Boolean(steamConfig?.apiKey)
|
||||
if (id === "gog") return Boolean(gogConfig?.accessToken)
|
||||
return false
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<section>
|
||||
<h2 className="mb-3 text-sm font-medium text-muted-foreground">
|
||||
{t("settings.providers")}
|
||||
</h2>
|
||||
<div className="space-y-2">
|
||||
{providers.map((p) => (
|
||||
<Link key={p.id} to="/settings/$provider" params={{ provider: p.id }}>
|
||||
<Card className="flex items-center justify-between p-4">
|
||||
<span className="font-medium">{p.label}</span>
|
||||
<Badge variant={isConnected(p.id) ? "default" : "secondary"}>
|
||||
{isConnected(p.id) ? "Connected" : "Not configured"}
|
||||
</Badge>
|
||||
</Card>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2 className="mb-3 text-sm font-medium text-muted-foreground">{t("settings.data")}</h2>
|
||||
<Link to="/settings/$provider" params={{ provider: "data" }}>
|
||||
<Card className="p-4">
|
||||
<span className="font-medium">{t("settings.data")}</span>
|
||||
</Card>
|
||||
</Link>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
89
src/features/settings/components/steam-settings.tsx
Normal file
89
src/features/settings/components/steam-settings.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
import { Button } from "@/shared/components/ui/button"
|
||||
import { Input } from "@/shared/components/ui/input"
|
||||
import { useConfig, useSaveConfig } from "@/shared/db/hooks"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { useState } from "react"
|
||||
import { useSteamSync } from "../hooks/use-steam-sync"
|
||||
|
||||
export function SteamSettings() {
|
||||
const savedConfig = useConfig<{ apiKey: string; steamId: string }>("steam")
|
||||
const saveConfig = useSaveConfig()
|
||||
const { sync, syncing, error, lastCount } = useSteamSync()
|
||||
const lastSync = useConfig<string>("steam_last_sync")
|
||||
|
||||
const [apiKey, setApiKey] = useState("")
|
||||
const [steamId, setSteamId] = useState("")
|
||||
const [initialized, setInitialized] = useState(false)
|
||||
|
||||
if (savedConfig && !initialized) {
|
||||
setApiKey(savedConfig.apiKey || "")
|
||||
setSteamId(savedConfig.steamId || "")
|
||||
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 })
|
||||
}
|
||||
|
||||
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>
|
||||
|
||||
<div>
|
||||
<label htmlFor="steam-id" className="mb-1 block text-sm font-medium">
|
||||
{t("settings.steam.steamId")}
|
||||
</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>
|
||||
<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>
|
||||
)}
|
||||
{lastSync && (
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{t("settings.lastSync")}: {new Date(lastSync).toLocaleString()}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
99
src/features/settings/hooks/use-data-management.ts
Normal file
99
src/features/settings/hooks/use-data-management.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { getDb } from "@/shared/db/client"
|
||||
import { useCallback } from "react"
|
||||
|
||||
export function useDataManagement() {
|
||||
const exportData = useCallback(async () => {
|
||||
const db = await getDb()
|
||||
const games = await db.query("SELECT * FROM games")
|
||||
const playlists = await db.query("SELECT * FROM playlists")
|
||||
const playlistGames = await db.query("SELECT * FROM playlist_games")
|
||||
const config = await db.query("SELECT * FROM config")
|
||||
|
||||
const data = {
|
||||
version: "2026.03.01",
|
||||
exportedAt: new Date().toISOString(),
|
||||
games: games.rows,
|
||||
playlists: playlists.rows,
|
||||
playlistGames: playlistGames.rows,
|
||||
config: config.rows,
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: "application/json" })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement("a")
|
||||
a.href = url
|
||||
a.download = `whattoplay-export-${new Date().toISOString().slice(0, 10)}.json`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
}, [])
|
||||
|
||||
const importData = useCallback(async (file: File) => {
|
||||
const text = await file.text()
|
||||
const data = JSON.parse(text)
|
||||
const db = await getDb()
|
||||
|
||||
if (data.games) {
|
||||
for (const game of data.games) {
|
||||
await db.query(
|
||||
`INSERT INTO games (id, title, source, source_id, platform, last_played, playtime_hours, url, canonical_id, rating, game_state, is_favorite)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
title = $2, last_played = $6, playtime_hours = $7, url = $8, canonical_id = $9,
|
||||
rating = $10, game_state = $11, is_favorite = $12, updated_at = NOW()`,
|
||||
[
|
||||
game.id,
|
||||
game.title,
|
||||
game.source,
|
||||
game.source_id,
|
||||
game.platform,
|
||||
game.last_played,
|
||||
game.playtime_hours,
|
||||
game.url,
|
||||
game.canonical_id,
|
||||
game.rating ?? -1,
|
||||
game.game_state ?? "not_set",
|
||||
game.is_favorite ?? false,
|
||||
],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.playlists) {
|
||||
for (const pl of data.playlists) {
|
||||
await db.query(
|
||||
"INSERT INTO playlists (id, name, is_static) VALUES ($1, $2, $3) ON CONFLICT (id) DO NOTHING",
|
||||
[pl.id, pl.name, pl.is_static],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.playlistGames) {
|
||||
for (const pg of data.playlistGames) {
|
||||
await db.query(
|
||||
"INSERT INTO playlist_games (playlist_id, game_id) VALUES ($1, $2) ON CONFLICT DO NOTHING",
|
||||
[pg.playlist_id, pg.game_id],
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (data.config) {
|
||||
for (const cfg of data.config) {
|
||||
await db.query(
|
||||
`INSERT INTO config (key, value, updated_at) VALUES ($1, $2, NOW())
|
||||
ON CONFLICT (key) DO UPDATE SET value = $2, updated_at = NOW()`,
|
||||
[cfg.key, JSON.stringify(cfg.value)],
|
||||
)
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const clearAll = useCallback(async () => {
|
||||
const db = await getDb()
|
||||
await db.query("DELETE FROM playlist_games")
|
||||
await db.query("DELETE FROM games")
|
||||
await db.query("DELETE FROM playlists WHERE is_static = FALSE")
|
||||
await db.query("DELETE FROM config")
|
||||
}, [])
|
||||
|
||||
return { exportData, importData, clearAll }
|
||||
}
|
||||
81
src/features/settings/hooks/use-gog-sync.ts
Normal file
81
src/features/settings/hooks/use-gog-sync.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
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 }
|
||||
}
|
||||
49
src/features/settings/hooks/use-steam-sync.ts
Normal file
49
src/features/settings/hooks/use-steam-sync.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
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 }
|
||||
}
|
||||
14
src/features/settings/schema.ts
Normal file
14
src/features/settings/schema.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { z } from "zod"
|
||||
|
||||
export const steamConfigSchema = z.object({
|
||||
apiKey: z.string().min(1, "API key is required"),
|
||||
steamId: z.string().min(1, "Steam ID is required"),
|
||||
})
|
||||
export type SteamConfig = z.infer<typeof steamConfigSchema>
|
||||
|
||||
export const gogConfigSchema = z.object({
|
||||
accessToken: z.string(),
|
||||
refreshToken: z.string(),
|
||||
userId: z.string(),
|
||||
})
|
||||
export type GogConfig = z.infer<typeof gogConfigSchema>
|
||||
@@ -1,5 +1,32 @@
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
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: () => <div>Provider Settings</div>,
|
||||
component: ProviderSettingsPage,
|
||||
})
|
||||
|
||||
const titles: Record<string, string> = {
|
||||
steam: "Steam",
|
||||
gog: "GOG",
|
||||
data: "Data",
|
||||
}
|
||||
|
||||
function ProviderSettingsPage() {
|
||||
const { provider } = Route.useParams()
|
||||
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,16 @@
|
||||
import { SettingsList } from "@/features/settings/components/settings-list"
|
||||
import { t } from "@/shared/i18n"
|
||||
import { createFileRoute } from "@tanstack/react-router"
|
||||
|
||||
export const Route = createFileRoute("/settings/")({
|
||||
component: () => <div>Settings</div>,
|
||||
component: SettingsPage,
|
||||
})
|
||||
|
||||
function SettingsPage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-lg p-4">
|
||||
<h1 className="mb-6 text-2xl font-bold">{t("settings.title")}</h1>
|
||||
<SettingsList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user