2026-01-04 21:58:59 -05:00
|
|
|
"use client";
|
|
|
|
|
|
2026-01-06 09:46:07 -05:00
|
|
|
import { useState, useEffect } from "react";
|
2026-01-09 05:29:11 -05:00
|
|
|
import { Card, Button, Badge, Toggle, Input } from "@/shared/components";
|
2026-01-04 21:58:59 -05:00
|
|
|
import { useTheme } from "@/shared/hooks/useTheme";
|
|
|
|
|
import { APP_CONFIG } from "@/shared/constants/config";
|
|
|
|
|
|
|
|
|
|
export default function ProfilePage() {
|
|
|
|
|
const { theme, setTheme, isDark } = useTheme();
|
2026-01-06 09:46:07 -05:00
|
|
|
const [settings, setSettings] = useState({ fallbackStrategy: "fill-first" });
|
|
|
|
|
const [loading, setLoading] = useState(true);
|
2026-01-09 05:29:11 -05:00
|
|
|
const [passwords, setPasswords] = useState({ current: "", new: "", confirm: "" });
|
|
|
|
|
const [passStatus, setPassStatus] = useState({ type: "", message: "" });
|
|
|
|
|
const [passLoading, setPassLoading] = useState(false);
|
2026-01-06 09:46:07 -05:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetch("/api/settings")
|
|
|
|
|
.then((res) => res.json())
|
|
|
|
|
.then((data) => {
|
|
|
|
|
setSettings(data);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
})
|
|
|
|
|
.catch((err) => {
|
|
|
|
|
console.error("Failed to fetch settings:", err);
|
|
|
|
|
setLoading(false);
|
|
|
|
|
});
|
|
|
|
|
}, []);
|
|
|
|
|
|
2026-01-09 05:29:11 -05:00
|
|
|
const handlePasswordChange = async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
if (passwords.new !== passwords.confirm) {
|
|
|
|
|
setPassStatus({ type: "error", message: "Passwords do not match" });
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPassLoading(true);
|
|
|
|
|
setPassStatus({ type: "", message: "" });
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/settings", {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
currentPassword: passwords.current,
|
|
|
|
|
newPassword: passwords.new,
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
setPassStatus({ type: "success", message: "Password updated successfully" });
|
|
|
|
|
setPasswords({ current: "", new: "", confirm: "" });
|
|
|
|
|
} else {
|
|
|
|
|
setPassStatus({ type: "error", message: data.error || "Failed to update password" });
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setPassStatus({ type: "error", message: "An error occurred" });
|
|
|
|
|
} finally {
|
|
|
|
|
setPassLoading(false);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-06 09:46:07 -05:00
|
|
|
const updateFallbackStrategy = async (strategy) => {
|
|
|
|
|
try {
|
|
|
|
|
const res = await fetch("/api/settings", {
|
|
|
|
|
method: "PATCH",
|
|
|
|
|
headers: { "Content-Type": "application/json" },
|
|
|
|
|
body: JSON.stringify({ fallbackStrategy: strategy }),
|
|
|
|
|
});
|
|
|
|
|
if (res.ok) {
|
|
|
|
|
setSettings(prev => ({ ...prev, fallbackStrategy: strategy }));
|
|
|
|
|
}
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error("Failed to update settings:", err);
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-04 21:58:59 -05:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="max-w-2xl mx-auto">
|
|
|
|
|
<div className="flex flex-col gap-6">
|
|
|
|
|
{/* Local Mode Info */}
|
|
|
|
|
<Card>
|
|
|
|
|
<div className="flex items-center gap-4 mb-4">
|
|
|
|
|
<div className="size-12 rounded-lg bg-green-500/10 text-green-500 flex items-center justify-center">
|
|
|
|
|
<span className="material-symbols-outlined text-2xl">computer</span>
|
|
|
|
|
</div>
|
|
|
|
|
<div>
|
|
|
|
|
<h2 className="text-xl font-semibold">Local Mode</h2>
|
|
|
|
|
<p className="text-text-muted">Running on your machine</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="pt-4 border-t border-border">
|
|
|
|
|
<p className="text-sm text-text-muted">
|
|
|
|
|
All data is stored locally in the <code className="bg-sidebar px-1 rounded">data/db.json</code> file.
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-01-09 05:29:11 -05:00
|
|
|
{/* Routing Preferences */}
|
|
|
|
|
<Card>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Security</h3>
|
|
|
|
|
<form onSubmit={handlePasswordChange} className="flex flex-col gap-4">
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<label className="text-sm font-medium">Current Password</label>
|
|
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="Enter current password"
|
|
|
|
|
value={passwords.current}
|
|
|
|
|
onChange={(e) => setPasswords({ ...passwords, current: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<label className="text-sm font-medium">New Password</label>
|
|
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="Enter new password"
|
|
|
|
|
value={passwords.new}
|
|
|
|
|
onChange={(e) => setPasswords({ ...passwords, new: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex flex-col gap-2">
|
|
|
|
|
<label className="text-sm font-medium">Confirm New Password</label>
|
|
|
|
|
<Input
|
|
|
|
|
type="password"
|
|
|
|
|
placeholder="Confirm new password"
|
|
|
|
|
value={passwords.confirm}
|
|
|
|
|
onChange={(e) => setPasswords({ ...passwords, confirm: e.target.value })}
|
|
|
|
|
required
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{passStatus.message && (
|
|
|
|
|
<p className={`text-sm ${passStatus.type === "error" ? "text-red-500" : "text-green-500"}`}>
|
|
|
|
|
{passStatus.message}
|
|
|
|
|
</p>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
<div className="pt-2">
|
|
|
|
|
<Button type="submit" variant="primary" isLoading={passLoading}>
|
|
|
|
|
Update Password
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-01-06 09:46:07 -05:00
|
|
|
{/* Routing Preferences */}
|
|
|
|
|
<Card>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Routing Strategy</h3>
|
|
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">Round Robin</p>
|
|
|
|
|
<p className="text-sm text-text-muted">
|
|
|
|
|
Cycle through accounts to distribute load
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Toggle
|
|
|
|
|
checked={settings.fallbackStrategy === "round-robin"}
|
|
|
|
|
onChange={() => updateFallbackStrategy(settings.fallbackStrategy === "round-robin" ? "fill-first" : "round-robin")}
|
|
|
|
|
disabled={loading}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<p className="text-xs text-text-muted italic pt-2 border-t border-border/50">
|
|
|
|
|
{settings.fallbackStrategy === "round-robin"
|
|
|
|
|
? "Currently distributing requests across all available accounts."
|
|
|
|
|
: "Currently using accounts in priority order (Fill First)."}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
2026-01-04 21:58:59 -05:00
|
|
|
{/* Theme Preferences */}
|
|
|
|
|
<Card>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Appearance</h3>
|
|
|
|
|
<div className="flex flex-col gap-4">
|
|
|
|
|
<div className="flex items-center justify-between">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">Dark Mode</p>
|
|
|
|
|
<p className="text-sm text-text-muted">
|
|
|
|
|
Switch between light and dark themes
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
<Toggle
|
|
|
|
|
checked={isDark}
|
|
|
|
|
onChange={() => setTheme(isDark ? "light" : "dark")}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Theme Options */}
|
|
|
|
|
<div className="flex gap-3 pt-4 border-t border-border">
|
|
|
|
|
{["light", "dark", "system"].map((option) => (
|
|
|
|
|
<button
|
|
|
|
|
key={option}
|
|
|
|
|
onClick={() => setTheme(option)}
|
|
|
|
|
className={`flex-1 flex flex-col items-center gap-2 p-4 rounded-lg border transition-all ${
|
|
|
|
|
theme === option
|
|
|
|
|
? "border-primary bg-primary/5"
|
|
|
|
|
: "border-border hover:border-primary/50"
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<span className="material-symbols-outlined text-2xl">
|
|
|
|
|
{option === "light"
|
|
|
|
|
? "light_mode"
|
|
|
|
|
: option === "dark"
|
|
|
|
|
? "dark_mode"
|
|
|
|
|
: "contrast"}
|
|
|
|
|
</span>
|
|
|
|
|
<span className="text-sm font-medium capitalize">{option}</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* Data Management */}
|
|
|
|
|
<Card>
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Data</h3>
|
|
|
|
|
<div className="flex flex-col gap-3">
|
|
|
|
|
<div className="flex items-center justify-between p-4 rounded-lg bg-bg border border-border">
|
|
|
|
|
<div>
|
|
|
|
|
<p className="font-medium">Database Location</p>
|
|
|
|
|
<p className="text-sm text-text-muted font-mono">~/9router/data/db.json</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* App Info */}
|
|
|
|
|
<div className="text-center text-sm text-text-muted py-4">
|
|
|
|
|
<p>{APP_CONFIG.name} v{APP_CONFIG.version}</p>
|
|
|
|
|
<p className="mt-1">Local Mode - All data stored on your machine</p>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|