Paul-CICD/packages/client/src/pages/Settings.tsx

160 lines
6.1 KiB
TypeScript

import { useState, useEffect } from "react";
import { KeyRound, UserPen } from "lucide-react";
export default function Settings() {
const [username, setUsername] = useState("");
const [newUsername, setNewUsername] = useState("");
const [usernameError, setUsernameError] = useState("");
const [usernameSuccess, setUsernameSuccess] = useState(false);
const [usernameLoading, setUsernameLoading] = useState(false);
const [pwForm, setPwForm] = useState({
current_password: "",
new_password: "",
confirm_password: "",
});
const [pwError, setPwError] = useState("");
const [pwSuccess, setPwSuccess] = useState(false);
const [pwLoading, setPwLoading] = useState(false);
useEffect(() => {
fetch("/api/auth/me")
.then((r) => r.json())
.then((d) => {
setUsername(d.username);
setNewUsername(d.username);
});
}, []);
const saveUsername = async (e: React.FormEvent) => {
e.preventDefault();
setUsernameError("");
setUsernameSuccess(false);
if (newUsername.trim() === username) return;
setUsernameLoading(true);
const res = await fetch("/api/auth/change-username", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ new_username: newUsername.trim() }),
});
setUsernameLoading(false);
if (res.ok) {
setUsername(newUsername.trim());
setUsernameSuccess(true);
} else {
const data = await res.json();
setUsernameError(data.error || "Failed to update username");
}
};
const savePassword = async (e: React.FormEvent) => {
e.preventDefault();
setPwError("");
setPwSuccess(false);
if (pwForm.new_password !== pwForm.confirm_password) {
setPwError("New passwords do not match");
return;
}
setPwLoading(true);
const res = await fetch("/api/auth/change-password", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
current_password: pwForm.current_password,
new_password: pwForm.new_password,
}),
});
setPwLoading(false);
if (res.ok) {
setPwSuccess(true);
setPwForm({ current_password: "", new_password: "", confirm_password: "" });
} else {
const data = await res.json();
setPwError(data.error || "Failed to change password");
}
};
return (
<div className="space-y-6">
<h1 className="text-xl font-semibold">Settings</h1>
{/* Username */}
<section className="bg-gray-900 border border-gray-800 rounded-lg p-5 space-y-4 max-w-md">
<div className="flex items-center gap-2">
<UserPen className="w-4 h-4 text-gray-400" />
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-wider">
Change Username
</h2>
</div>
<form onSubmit={saveUsername} className="space-y-3">
<div className="space-y-1">
<label className="text-xs text-gray-400">Username</label>
<input
value={newUsername}
onChange={(e) => setNewUsername(e.target.value)}
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
/>
</div>
{usernameError && <p className="text-xs text-red-400">{usernameError}</p>}
{usernameSuccess && <p className="text-xs text-green-400">Username updated successfully.</p>}
<button
type="submit"
disabled={usernameLoading || newUsername.trim() === username}
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm px-4 py-2 rounded-md transition-colors"
>
{usernameLoading ? "Saving..." : "Save"}
</button>
</form>
</section>
{/* Password */}
<section className="bg-gray-900 border border-gray-800 rounded-lg p-5 space-y-4 max-w-md">
<div className="flex items-center gap-2">
<KeyRound className="w-4 h-4 text-gray-400" />
<h2 className="text-sm font-medium text-gray-400 uppercase tracking-wider">
Change Password
</h2>
</div>
<form onSubmit={savePassword} className="space-y-3">
<div className="space-y-1">
<label className="text-xs text-gray-400">Current Password</label>
<input
type="password"
value={pwForm.current_password}
onChange={(e) => setPwForm((f) => ({ ...f, current_password: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-gray-400">New Password</label>
<input
type="password"
value={pwForm.new_password}
onChange={(e) => setPwForm((f) => ({ ...f, new_password: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
/>
</div>
<div className="space-y-1">
<label className="text-xs text-gray-400">Confirm New Password</label>
<input
type="password"
value={pwForm.confirm_password}
onChange={(e) => setPwForm((f) => ({ ...f, confirm_password: e.target.value }))}
className="w-full bg-gray-800 border border-gray-700 rounded-md px-3 py-2 text-sm outline-none focus:border-blue-500"
/>
</div>
{pwError && <p className="text-xs text-red-400">{pwError}</p>}
{pwSuccess && <p className="text-xs text-green-400">Password changed successfully.</p>}
<button
type="submit"
disabled={pwLoading}
className="bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white text-sm px-4 py-2 rounded-md transition-colors"
>
{pwLoading ? "Saving..." : "Save"}
</button>
</form>
</section>
</div>
);
}