Magic-link sign in UI and APIs
This commit is contained in:
@@ -1,19 +1,75 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import TopNav from "./TopNav.container";
|
||||
import type { TopNavProps } from "./TopNav.types";
|
||||
import { fetchAuthSession } from "../../../../lib/create/api";
|
||||
|
||||
export type TopNavWithPathnameProps = Omit<TopNavProps, "folderTop"> & {
|
||||
/** From Server Component (`getNavAuthSignedIn`); matches first HTML paint. */
|
||||
initialSignedIn?: boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* TopNav wrapper that automatically determines folderTop based on current pathname.
|
||||
* Use this in layout.tsx instead of ConditionalHeader.
|
||||
* TopNav wrapper: `folderTop` from pathname; Log in vs Profile from session.
|
||||
*
|
||||
* **SSR:** Parent passes `initialSignedIn` from `getSessionUser()` so the hydrated
|
||||
* header matches the cookie (Next.js pattern for HttpOnly session UI).
|
||||
*
|
||||
* **Client:** Refetch on pathname change (magic-link redirect, stale layout after
|
||||
* `router.refresh()`), **popstate** / **pageshow** `persisted` (bfcache / back).
|
||||
*/
|
||||
const TopNavWithPathname = memo<Omit<TopNavProps, "folderTop">>((props) => {
|
||||
const TopNavWithPathname = memo<TopNavWithPathnameProps>((props) => {
|
||||
const { initialSignedIn = false, ...topNavRest } = props;
|
||||
const pathname = usePathname();
|
||||
const isHomePage = pathname === "/";
|
||||
const [loggedIn, setLoggedIn] = useState(initialSignedIn);
|
||||
|
||||
return <TopNav {...props} folderTop={isHomePage} />;
|
||||
useEffect(() => {
|
||||
setLoggedIn(initialSignedIn);
|
||||
}, [initialSignedIn]);
|
||||
|
||||
const applySessionUser = useCallback(
|
||||
(user: { id: string; email: string } | null) => {
|
||||
setLoggedIn(Boolean(user));
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const syncSession = useCallback(() => {
|
||||
fetchAuthSession().then(({ user }) => {
|
||||
applySessionUser(user);
|
||||
});
|
||||
}, [applySessionUser]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
fetchAuthSession().then(({ user }) => {
|
||||
if (!cancelled) applySessionUser(user);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [pathname, applySessionUser]);
|
||||
|
||||
useEffect(() => {
|
||||
const onPageShow = (e: PageTransitionEvent) => {
|
||||
if (e.persisted) syncSession();
|
||||
};
|
||||
window.addEventListener("pageshow", onPageShow);
|
||||
return () => window.removeEventListener("pageshow", onPageShow);
|
||||
}, [syncSession]);
|
||||
|
||||
useEffect(() => {
|
||||
const onPopState = () => {
|
||||
queueMicrotask(syncSession);
|
||||
};
|
||||
window.addEventListener("popstate", onPopState);
|
||||
return () => window.removeEventListener("popstate", onPopState);
|
||||
}, [syncSession]);
|
||||
|
||||
return <TopNav {...topNavRest} folderTop={isHomePage} loggedIn={loggedIn} />;
|
||||
});
|
||||
|
||||
TopNavWithPathname.displayName = "TopNavWithPathname";
|
||||
|
||||
Reference in New Issue
Block a user