add user account page and associated functionaility to support it
Some checks are pending
check / check (push) Waiting to run

This commit is contained in:
KS Jannette
2026-03-05 08:25:24 -05:00
parent 8f1212813f
commit 4b77e7a4d4
9 changed files with 423 additions and 1 deletions

View File

@@ -0,0 +1,77 @@
.account-page h1 {
font-size: 2rem;
font-weight: 200;
}
.account__section-title {
font-size: 1.4rem;
font-weight: 400;
margin-bottom: 1.25rem;
color: var(--color-text);
border-bottom: 1px solid var(--color-border-light);
padding-bottom: 0.5rem;
}
.account__row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.6rem 0;
border-bottom: 1px solid var(--color-border-light);
}
.account__row--last {
border-bottom: none;
}
.account__label {
color: var(--color-text-muted);
font-size: 1.1rem;
}
.account__value {
color: var(--color-text);
font-size: 1.1rem;
text-align: right;
}
.account__uuid {
font-size: 0.85rem;
color: var(--color-text-dimmed);
word-break: break-all;
max-width: 60%;
}
.account__status {
font-weight: 600;
}
.account__status--active {
color: var(--color-success-text);
}
.account__status--canceled {
color: var(--color-danger);
}
.account__status--canceling {
color: var(--color-warning);
}
.account__status--past_due {
color: var(--color-error);
}
.account__portal-section {
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--color-border-light);
}
.account__portal-hint {
margin-top: 0.5rem;
}
.account__password-form {
max-width: 400px;
}

View File

@@ -0,0 +1,202 @@
import { useState, useEffect } from "react";
import { updatePassword } from "firebase/auth";
import { auth } from "../../firebase/config";
import { getAccount, createPortalSession } from "../../api/account";
import "./Account.css";
export default function Account() {
const [account, setAccount] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [newPassword, setNewPassword] = useState("");
const [confirmPassword, setConfirmPassword] = useState("");
const [passwordMsg, setPasswordMsg] = useState(null);
const [passwordErr, setPasswordErr] = useState(null);
const [changingPassword, setChangingPassword] = useState(false);
const [portalLoading, setPortalLoading] = useState(false);
useEffect(() => {
async function fetchAccount() {
try {
const data = await getAccount();
setAccount(data);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
fetchAccount();
}, []);
async function handlePasswordChange(e) {
e.preventDefault();
setPasswordMsg(null);
setPasswordErr(null);
if (newPassword.length < 6) {
setPasswordErr("Password must be at least 6 characters");
return;
}
if (newPassword !== confirmPassword) {
setPasswordErr("Passwords do not match");
return;
}
try {
setChangingPassword(true);
await updatePassword(auth.currentUser, newPassword);
setPasswordMsg("Password updated successfully");
setNewPassword("");
setConfirmPassword("");
} catch (err) {
setPasswordErr(err.message);
} finally {
setChangingPassword(false);
}
}
async function handleManageSubscription() {
try {
setPortalLoading(true);
const { url } = await createPortalSession();
window.location.href = url;
} catch (err) {
setError(err.message);
} finally {
setPortalLoading(false);
}
}
if (loading) {
return <div className="page">Loading...</div>;
}
if (error && !account) {
return <div className="page text-error">Error: {error}</div>;
}
const isCanceling = account.cancel_at_period_end;
const statusLabel = isCanceling
? "Canceling"
: account.subscription_status === "active"
? "Active"
: account.subscription_status.charAt(0).toUpperCase() +
account.subscription_status.slice(1);
return (
<div className="page account-page">
<h1 className="mb-lg">Account</h1>
{error && <div className="alert alert--error mb-lg">{error}</div>}
{/* ── Profile Section ──────────────────────── */}
<div className="section mb-lg">
<h2 className="account__section-title">Profile</h2>
<div className="account__row">
<span className="account__label">Email</span>
<span className="account__value">{account.email}</span>
</div>
<div className="account__row">
<span className="account__label">User Name</span>
<span className="account__value">{account.user_name}</span>
</div>
<div className="account__row account__row--last">
<span className="account__label">User ID</span>
<span className="account__value text-mono account__uuid">{account.user_id}</span>
</div>
</div>
{/* ── Subscription Section ─────────────────── */}
<div className="section mb-lg">
<h2 className="account__section-title">Subscription</h2>
<div className="account__row">
<span className="account__label">Plan</span>
<span className="account__value">{account.subscription_plan}</span>
</div>
<div className="account__row">
<span className="account__label">Status</span>
<span className={`account__value account__status account__status--${isCanceling ? "canceling" : account.subscription_status}`}>
{statusLabel}
</span>
</div>
{account.member_since && (
<div className="account__row">
<span className="account__label">Member Since</span>
<span className="account__value">{account.member_since}</span>
</div>
)}
{account.next_billing_date && !isCanceling && (
<div className="account__row">
<span className="account__label">Next Billing Date</span>
<span className="account__value">{account.next_billing_date}</span>
</div>
)}
{isCanceling && account.period_end_date && (
<div className="account__row">
<span className="account__label">Access Until</span>
<span className="account__value">{account.period_end_date}</span>
</div>
)}
{isCanceling && (
<div className="alert alert--warning mt-md">
Your subscription has been canceled and will not renew.
You have full access until {account.period_end_date}.
</div>
)}
<div className="account__portal-section">
<button
onClick={handleManageSubscription}
disabled={portalLoading}
className="btn btn--ghost"
>
{portalLoading ? "Redirecting..." : "Manage Subscription"}
</button>
<p className="text-dimmed text-sm account__portal-hint">
Cancel subscription, update payment method, or view invoices via Stripe.
</p>
</div>
</div>
{/* ── Change Password Section ──────────────── */}
<div className="section">
<h2 className="account__section-title">Change Password</h2>
{passwordMsg && <div className="alert alert--success mb-md">{passwordMsg}</div>}
{passwordErr && <div className="alert alert--error mb-md">{passwordErr}</div>}
<form onSubmit={handlePasswordChange} className="account__password-form">
<div className="form-field">
<label className="form-label">New Password</label>
<input
type="password"
className="form-control"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
disabled={changingPassword}
required
/>
</div>
<div className="form-field form-field--last">
<label className="form-label">Confirm Password</label>
<input
type="password"
className="form-control"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
disabled={changingPassword}
required
/>
</div>
<button type="submit" disabled={changingPassword} className="btn btn--primary">
{changingPassword ? "Updating..." : "Update Password"}
</button>
</form>
</div>
</div>
);
}