setting, session and profile
This commit is contained in:
parent
48eea5d201
commit
4a563f4f34
31 changed files with 1797 additions and 159 deletions
132
frontend/src/components/AccountProfile.jsx
Normal file
132
frontend/src/components/AccountProfile.jsx
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
import { Camera, UserRound } from "lucide-react";
|
||||
import { useRef, useState } from "react";
|
||||
import { updateAccountProfile } from "../api";
|
||||
|
||||
const MAX_AVATAR_BYTES = 512 * 1024;
|
||||
const ALLOWED_TYPES = new Set(["image/jpeg", "image/png", "image/webp"]);
|
||||
|
||||
function accountInitials(account) {
|
||||
return `${account?.first_name?.[0] || ""}${account?.last_name?.[0] || ""}`.toUpperCase() || "AG";
|
||||
}
|
||||
|
||||
export default function AccountProfile({ account, onUpdated }) {
|
||||
const token = sessionStorage.getItem("agmail_access_token");
|
||||
const inputRef = useRef(null);
|
||||
const [status, setStatus] = useState({ loading: false, error: "", notice: "" });
|
||||
|
||||
async function handleFileChange(event) {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
|
||||
if (!ALLOWED_TYPES.has(file.type)) {
|
||||
setStatus({ loading: false, error: "Use a JPG, PNG, or WebP image.", notice: "" });
|
||||
return;
|
||||
}
|
||||
if (file.size > MAX_AVATAR_BYTES) {
|
||||
setStatus({ loading: false, error: "Profile picture must be 512 KB or smaller.", notice: "" });
|
||||
return;
|
||||
}
|
||||
|
||||
setStatus({ loading: true, error: "", notice: "" });
|
||||
try {
|
||||
const dataUrl = await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result || ""));
|
||||
reader.onerror = () => reject(new Error("Could not read that image."));
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
const updated = await updateAccountProfile({ profile_picture_url: dataUrl }, token);
|
||||
sessionStorage.setItem("agmail_account", JSON.stringify(updated));
|
||||
onUpdated?.(updated);
|
||||
setStatus({ loading: false, error: "", notice: "Profile picture updated." });
|
||||
} catch (error) {
|
||||
setStatus({ loading: false, error: error.message, notice: "" });
|
||||
}
|
||||
}
|
||||
|
||||
async function removePhoto() {
|
||||
setStatus({ loading: true, error: "", notice: "" });
|
||||
try {
|
||||
const updated = await updateAccountProfile({ profile_picture_url: null }, token);
|
||||
sessionStorage.setItem("agmail_account", JSON.stringify(updated));
|
||||
onUpdated?.(updated);
|
||||
setStatus({ loading: false, error: "", notice: "Profile picture removed." });
|
||||
} catch (error) {
|
||||
setStatus({ loading: false, error: error.message, notice: "" });
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="account-profile-page">
|
||||
<header className="account-profile-head">
|
||||
<div>
|
||||
<p className="account-profile-eyebrow">Account</p>
|
||||
<h1>Profile</h1>
|
||||
<p>Update how you appear across Gentic.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div className="account-profile-card">
|
||||
<div className="account-profile-avatar-block">
|
||||
<div className="account-profile-avatar">
|
||||
{account?.profile_picture_url ? (
|
||||
<img src={account.profile_picture_url} alt="" />
|
||||
) : (
|
||||
<span>{accountInitials(account)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="account-profile-avatar-actions">
|
||||
<button
|
||||
type="button"
|
||||
className="account-profile-btn"
|
||||
disabled={status.loading}
|
||||
onClick={() => inputRef.current?.click()}
|
||||
>
|
||||
<Camera size={16} />
|
||||
{account?.profile_picture_url ? "Change photo" : "Upload photo"}
|
||||
</button>
|
||||
{account?.profile_picture_url && (
|
||||
<button
|
||||
type="button"
|
||||
className="account-profile-btn subtle"
|
||||
disabled={status.loading}
|
||||
onClick={removePhoto}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
)}
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="file"
|
||||
accept="image/jpeg,image/png,image/webp"
|
||||
hidden
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="account-profile-fields">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<div className="account-profile-readonly">
|
||||
<UserRound size={16} />
|
||||
{account?.first_name} {account?.last_name}
|
||||
</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>Email</span>
|
||||
<div className="account-profile-readonly">{account?.email}</div>
|
||||
</label>
|
||||
<label>
|
||||
<span>Phone</span>
|
||||
<div className="account-profile-readonly">{account?.phone_number || "Not set"}</div>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{status.notice && <p className="account-profile-notice" role="status">{status.notice}</p>}
|
||||
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue