Profile page UI and functionality implemented

This commit is contained in:
adilallo
2026-04-25 17:57:58 -06:00
parent 7dd2562bae
commit 68517796a9
103 changed files with 4439 additions and 1476 deletions
+237 -36
View File
@@ -1,55 +1,256 @@
"use client";
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { useAuthModal } from "../../contexts/AuthModalContext";
import { useTranslation } from "../../contexts/MessagesContext";
import Button from "../../components/buttons/Button";
import { fetchAuthSession, logout } from "../../../lib/create/api";
import {
deleteAccount,
deletePublishedRule,
deleteServerDraft,
duplicatePublishedRule,
fetchAuthSession,
fetchMyPublishedRules,
fetchServerDraftForProfile,
logout,
type MyPublishedRule,
} from "../../../lib/create/api";
import {
FIRST_STEP,
isValidStep,
} from "../create/utils/flowSteps";
import type { CreateFlowStep } from "../create/types";
import { clearAnonymousCreateFlowStorage } from "../create/utils/anonymousDraftStorage";
import { useMediaQuery } from "../../hooks/useMediaQuery";
import {
ProfilePageSignedOutView,
ProfilePageView,
} from "./_components/ProfilePage.view";
function resolveContinueStepState(
state: { currentStep?: CreateFlowStep } & Record<string, unknown>,
): CreateFlowStep {
const s = state.currentStep;
if (s && isValidStep(s)) return s;
return FIRST_STEP;
}
export default function ProfilePageClient() {
const t = useTranslation("pages.profile");
const router = useRouter();
const { openLogin } = useAuthModal();
const [sessionLoaded, setSessionLoaded] = useState(false);
const [user, setUser] = useState<{ id: string; email: string } | null>(null);
const [loaded, setLoaded] = useState(false);
const [rules, setRules] = useState<MyPublishedRule[]>([]);
const [rulesError, setRulesError] = useState(false);
const [draft, setDraft] = useState<
Awaited<ReturnType<typeof fetchServerDraftForProfile>>
>(null);
const [ruleDeleteTargetId, setRuleDeleteTargetId] = useState<string | null>(
null,
);
const [ruleDeleteBusy, setRuleDeleteBusy] = useState(false);
const [draftDeleteOpen, setDraftDeleteOpen] = useState(false);
const [draftDeleteBusy, setDraftDeleteBusy] = useState(false);
const [accountDeleteOpen, setAccountDeleteOpen] = useState(false);
const [accountDeleteBusy, setAccountDeleteBusy] = useState(false);
const [actionError, setActionError] = useState<string | null>(null);
const load = useCallback(async () => {
setActionError(null);
const { user: u } = await fetchAuthSession();
setUser(u);
setSessionLoaded(true);
if (!u) {
setRules([]);
setRulesError(false);
setDraft(null);
return;
}
const [r, d] = await Promise.all([
fetchMyPublishedRules(),
fetchServerDraftForProfile(),
]);
if (r === null) {
setRules([]);
setRulesError(true);
} else {
setRules(r);
setRulesError(false);
}
setDraft(d);
}, []);
useEffect(() => {
let cancelled = false;
void fetchAuthSession().then(({ user: u }) => {
if (!cancelled) {
setUser(u);
setLoaded(true);
}
});
return () => {
cancelled = true;
};
}, []);
void load();
}, [load]);
const handleSignOut = useCallback(async () => {
setActionError(null);
await logout();
setUser(null);
setRules([]);
setDraft(null);
router.refresh();
}, [router]);
const handleRequestDeleteRule = useCallback((id: string) => {
setActionError(null);
setRuleDeleteTargetId(id);
}, []);
const handleCloseDeleteRuleDialog = useCallback(() => {
if (ruleDeleteBusy) return;
setRuleDeleteTargetId(null);
}, [ruleDeleteBusy]);
const handleConfirmDeleteRule = useCallback(async () => {
const id = ruleDeleteTargetId;
if (!id || ruleDeleteBusy) return;
setActionError(null);
setRuleDeleteBusy(true);
const res = await deletePublishedRule(id);
setRuleDeleteBusy(false);
if (res.ok === true) {
setRuleDeleteTargetId(null);
void load();
return;
}
if (res.status === 404) {
setActionError(t("notFound"));
setRuleDeleteTargetId(null);
} else if (res.status === 403) {
setActionError(t("forbidden"));
setRuleDeleteTargetId(null);
} else {
setActionError(t("actionError"));
}
}, [load, ruleDeleteBusy, ruleDeleteTargetId, t]);
const handleDuplicateRule = useCallback(
async (id: string) => {
setActionError(null);
const res = await duplicatePublishedRule(id);
if (res.ok === true) {
void load();
} else {
if (res.status === 404) {
setActionError(t("notFound"));
} else if (res.status === 403) {
setActionError(t("forbidden"));
} else {
setActionError(t("actionError"));
}
}
},
[load, t],
);
const handleContinueDraft = useCallback(() => {
if (draft == null || !draft.hasDraft) return;
const step = resolveContinueStepState(draft.state);
router.push(`/create/${step}`);
}, [draft, router]);
const handleRequestDeleteDraft = useCallback(() => {
setActionError(null);
setDraftDeleteOpen(true);
}, []);
const handleCloseDeleteDraftDialog = useCallback(() => {
if (draftDeleteBusy) return;
setDraftDeleteOpen(false);
}, [draftDeleteBusy]);
const handleConfirmDeleteDraft = useCallback(async () => {
if (draftDeleteBusy) return;
setActionError(null);
setDraftDeleteBusy(true);
clearAnonymousCreateFlowStorage();
await deleteServerDraft();
setDraftDeleteBusy(false);
setDraftDeleteOpen(false);
void load();
}, [draftDeleteBusy, load]);
const handleConfirmDeleteAccount = useCallback(async () => {
setActionError(null);
setAccountDeleteBusy(true);
const res = await deleteAccount();
setAccountDeleteBusy(false);
if (res.ok) {
setAccountDeleteOpen(false);
setUser(null);
setRules([]);
setDraft(null);
router.push("/");
router.refresh();
return;
}
setActionError(t("actionError"));
}, [router, t]);
/** `lg`+ layout; matches `--breakpoint-lg` in `app/tailwind.css`. */
const isProfileLgUp = useMediaQuery("(min-width: 1024px)");
/** `List` L + Bricolage section titles — Figma `22143:900247`; matches `--breakpoint-xl` (1440px). */
const isProfileXlUp = useMediaQuery("(min-width: 1440px)");
if (!sessionLoaded) {
return (
<div className="mx-auto max-w-3xl px-4 py-16">
<p className="font-inter text-sm text-[var(--color-content-default-secondary)]">
{t("loading")}
</p>
</div>
);
}
if (!user) {
return (
<ProfilePageSignedOutView
profileLgUp={isProfileLgUp}
onSignIn={() => openLogin({ nextPath: "/profile" })}
/>
);
}
const showDraftCard = Boolean(
draft && draft.hasDraft,
);
return (
<div className="mx-auto max-w-2xl px-4 py-16 md:py-24">
<h1 className="font-bricolage text-3xl font-extrabold text-[var(--color-content-default-primary)] md:text-4xl">
{t("placeholderTitle")}
</h1>
<p className="mt-4 font-inter text-lg leading-relaxed text-[var(--color-content-default-secondary)]">
{t("placeholderBody")}
</p>
{loaded && user ? (
<div className="mt-8">
<Button
buttonType="outline"
palette="default"
size="small"
type="button"
onClick={() => void handleSignOut()}
ariaLabel={t("signOut")}
>
{t("signOut")}
</Button>
</div>
) : null}
</div>
<ProfilePageView
userEmail={user.email}
ruleCardSize={isProfileLgUp ? "L" : "M"}
profileLgUp={isProfileLgUp}
profileListSize={isProfileXlUp ? "l" : "m"}
rules={rules}
rulesError={rulesError}
draft={draft}
showDraftCard={showDraftCard}
ruleDeleteOpen={ruleDeleteTargetId !== null}
ruleDeleteBusy={ruleDeleteBusy}
draftDeleteOpen={draftDeleteOpen}
draftDeleteBusy={draftDeleteBusy}
accountDeleteOpen={accountDeleteOpen}
accountDeleteBusy={accountDeleteBusy}
actionError={actionError}
onSignOut={handleSignOut}
onDeleteRule={handleRequestDeleteRule}
onCloseDeleteRule={handleCloseDeleteRuleDialog}
onConfirmDeleteRule={handleConfirmDeleteRule}
onDuplicateRule={handleDuplicateRule}
onContinueDraft={handleContinueDraft}
onDeleteDraft={handleRequestDeleteDraft}
onCloseDeleteDraft={handleCloseDeleteDraftDialog}
onConfirmDeleteDraft={handleConfirmDeleteDraft}
onOpenDeleteAccount={() => {
setActionError(null);
setAccountDeleteOpen(true);
}}
onCloseDeleteAccount={() => setAccountDeleteOpen(false)}
onConfirmDeleteAccount={handleConfirmDeleteAccount}
/>
);
}