First Commit
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { Avatar, Box, Button, Paper, TextField, Typography } from "@mui/material";
|
||||
|
||||
import { api } from "../api";
|
||||
import { useToast } from "../toast";
|
||||
|
||||
type MeResponse = {
|
||||
provider?: "local" | "google" | "external";
|
||||
id?: string;
|
||||
email?: string;
|
||||
userName?: string;
|
||||
roles?: string[];
|
||||
};
|
||||
|
||||
function initialsFrom(s?: string) {
|
||||
const v = (s ?? "").trim();
|
||||
if (!v) return "?";
|
||||
const parts = v.split(/[\s@._-]+/).filter(Boolean);
|
||||
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
||||
return (parts[0][0] + parts[1][0]).toUpperCase();
|
||||
}
|
||||
|
||||
export default function ProfilePage() {
|
||||
const { toast } = useToast();
|
||||
const [me, setMe] = useState<MeResponse | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const [email, setEmail] = useState("");
|
||||
const [userName, setUserName] = useState("");
|
||||
|
||||
const [currentPassword, setCurrentPassword] = useState("");
|
||||
const [newPassword, setNewPassword] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<MeResponse>("/auth/me")
|
||||
.then((r) => {
|
||||
setMe(r.data);
|
||||
setEmail(r.data?.email ?? "");
|
||||
setUserName(r.data?.userName ?? "");
|
||||
})
|
||||
.catch(() => setMe(null));
|
||||
}, []);
|
||||
|
||||
const initials = useMemo(() => initialsFrom(me?.userName || me?.email), [me]);
|
||||
const isLocal = me?.provider === "local";
|
||||
|
||||
return (
|
||||
<Paper sx={{ mt: 0, p: 2 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center", gap: 2, flexWrap: "wrap" }}>
|
||||
<Avatar sx={{ width: 44, height: 44, fontWeight: 900 }}>{initials}</Avatar>
|
||||
<Box>
|
||||
<Typography variant="h5" sx={{ fontWeight: 900 }}>
|
||||
Profile
|
||||
</Typography>
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
{me?.email ? me.email : "â€â€"} {me?.provider ? `(${me.provider})` : ""}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ mt: 3, display: "grid", gridTemplateColumns: "1fr 1fr", gap: 2 }}>
|
||||
<Box sx={{ gridColumn: "1 / -1" }}>
|
||||
<Typography variant="h6">Account</Typography>
|
||||
{!isLocal ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
This account is authenticated via Google; profile updates are read-only in this build.
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={!isLocal}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="Username"
|
||||
value={userName}
|
||||
onChange={(e) => setUserName(e.target.value)}
|
||||
disabled={!isLocal}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
variant="contained"
|
||||
disabled={!isLocal || loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.put("/auth/profile", { email, userName });
|
||||
toast("Profile updated.", "success");
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Failed to update profile.";
|
||||
toast(String(msg), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save changes
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", mt: 1 }}>
|
||||
<Typography variant="h6">Change password</Typography>
|
||||
{!isLocal ? (
|
||||
<Typography sx={{ color: "text.secondary" }}>
|
||||
Password changes are only available for local accounts.
|
||||
</Typography>
|
||||
) : null}
|
||||
</Box>
|
||||
|
||||
<TextField
|
||||
label="Current password"
|
||||
type="password"
|
||||
value={currentPassword}
|
||||
onChange={(e) => setCurrentPassword(e.target.value)}
|
||||
disabled={!isLocal}
|
||||
fullWidth
|
||||
/>
|
||||
<TextField
|
||||
label="New password"
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
disabled={!isLocal}
|
||||
fullWidth
|
||||
/>
|
||||
|
||||
<Box sx={{ gridColumn: "1 / -1", display: "flex", justifyContent: "flex-end" }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
disabled={!isLocal || loading}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
await api.post("/auth/change-password", {
|
||||
currentPassword,
|
||||
newPassword,
|
||||
});
|
||||
setCurrentPassword("");
|
||||
setNewPassword("");
|
||||
toast("Password updated.", "success");
|
||||
} catch (e: any) {
|
||||
const msg = e?.response?.data || e?.message || "Failed to change password.";
|
||||
toast(String(msg), "error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Update password
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Paper>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user