151 lines
5.3 KiB
JavaScript
151 lines
5.3 KiB
JavaScript
import { useRef, useState } from "react";
|
|
import { updateAccountProfile } from "../api";
|
|
import { getAccessToken, saveStoredAccount } from "../auth";
|
|
|
|
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";
|
|
}
|
|
|
|
function displayPhone(phone) {
|
|
if (!phone) return "Not set";
|
|
return phone.replace(/^\+/, "");
|
|
}
|
|
|
|
export default function AccountProfile({ account, onUpdated }) {
|
|
const token = getAccessToken();
|
|
const inputRef = useRef(null);
|
|
const [status, setStatus] = useState({ loading: false, error: "", notice: "" });
|
|
const fullName = `${account?.first_name || ""} ${account?.last_name || ""}`.trim();
|
|
const isCompany = account?.account_type === "company";
|
|
const hasPhoto = Boolean(account?.profile_picture_url);
|
|
|
|
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);
|
|
saveStoredAccount(updated);
|
|
onUpdated?.(updated);
|
|
setStatus({ loading: false, error: "", notice: "Saved" });
|
|
} 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);
|
|
saveStoredAccount(updated);
|
|
onUpdated?.(updated);
|
|
setStatus({ loading: false, error: "", notice: "Saved" });
|
|
} catch (error) {
|
|
setStatus({ loading: false, error: error.message, notice: "" });
|
|
}
|
|
}
|
|
|
|
return (
|
|
<section className="account-profile-page">
|
|
<header className="account-profile-head">
|
|
<p className="account-profile-eyebrow">{isCompany ? "Company workspace" : "Personal workspace"}</p>
|
|
<h1>Profile</h1>
|
|
<p>Manage your personal information and how you appear across Gentic.</p>
|
|
<p className="account-profile-meta">
|
|
{isCompany ? "Company account" : "Personal account"} · {isCompany ? "Company" : "Individual"}
|
|
</p>
|
|
</header>
|
|
|
|
<div className="account-profile-section">
|
|
<div className="account-profile-identity">
|
|
<div className="account-profile-avatar">
|
|
{hasPhoto ? (
|
|
<img src={account.profile_picture_url} alt="" />
|
|
) : (
|
|
<span>{accountInitials(account)}</span>
|
|
)}
|
|
</div>
|
|
<div className="account-profile-identity-copy">
|
|
<strong>{fullName}</strong>
|
|
<div className="account-profile-identity-actions">
|
|
<button
|
|
type="button"
|
|
className="account-profile-link"
|
|
disabled={status.loading}
|
|
onClick={() => inputRef.current?.click()}
|
|
>
|
|
{hasPhoto ? "Change photo" : "Upload photo"}
|
|
</button>
|
|
{hasPhoto && (
|
|
<>
|
|
<span aria-hidden="true">·</span>
|
|
<button
|
|
type="button"
|
|
className="account-profile-link subtle"
|
|
disabled={status.loading}
|
|
onClick={removePhoto}
|
|
>
|
|
Remove
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
<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>
|
|
<input value={fullName} readOnly disabled />
|
|
</label>
|
|
<label>
|
|
<span>Email</span>
|
|
<input value={account?.email || ""} readOnly disabled />
|
|
<small>Your Gentic email address</small>
|
|
</label>
|
|
<label>
|
|
<span>Phone</span>
|
|
<input value={displayPhone(account?.phone_number)} readOnly disabled />
|
|
</label>
|
|
</div>
|
|
|
|
<div className="account-profile-footer">
|
|
{status.error && <p className="form-error" role="alert">{status.error}</p>}
|
|
{status.notice && !status.error && (
|
|
<p className="account-profile-saved" role="status">{status.notice}</p>
|
|
)}
|
|
{status.loading && !status.notice && (
|
|
<p className="account-profile-saved" role="status">Saving…</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|