Profile page UI and functionality implemented
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user