Eagerly load admin nav with client-side cookie strategy
This commit is contained in:
parent
ac19ed2215
commit
2a0e898ba6
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@ -6,6 +6,7 @@
|
||||
"ARROWLEFT",
|
||||
"ARROWRIGHT",
|
||||
"Astia",
|
||||
"authjs",
|
||||
"camelcase",
|
||||
"cloudflarestorage",
|
||||
"cmdk",
|
||||
|
||||
@ -17,7 +17,7 @@ import { FiTag } from 'react-icons/fi';
|
||||
import { BiLockAlt } from 'react-icons/bi';
|
||||
import AdminAppInfoIcon from './AdminAppInfoIcon';
|
||||
import { PiSignOutBold } from 'react-icons/pi';
|
||||
import { signOutAndRedirectAction } from '@/auth/actions';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import { ComponentProps } from 'react';
|
||||
import { FaRegFolderOpen } from 'react-icons/fa';
|
||||
|
||||
@ -34,6 +34,7 @@ export default function AdminAppMenu({
|
||||
tagsCount,
|
||||
selectedPhotoIds,
|
||||
setSelectedPhotoIds,
|
||||
clearAuthStateAndRedirect,
|
||||
} = useAppState();
|
||||
|
||||
const isSelecting = selectedPhotoIds !== undefined;
|
||||
@ -107,7 +108,7 @@ export default function AdminAppMenu({
|
||||
}, {
|
||||
label: 'Sign Out',
|
||||
icon: <PiSignOutBold size={15} />,
|
||||
action: signOutAndRedirectAction,
|
||||
action: () => signOutAction().then(clearAuthStateAndRedirect),
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@ -9,7 +9,7 @@ import RepoLink from '../components/RepoLink';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { PATH_ADMIN_PHOTOS, isPathAdmin, isPathSignIn } from './paths';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { signOutAndRedirectAction } from '@/auth/actions';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
@ -17,7 +17,7 @@ import { useAppState } from '@/state/AppState';
|
||||
export default function Footer() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { userEmail, setUserEmail } = useAppState();
|
||||
const { userEmail, clearAuthStateAndRedirect } = useAppState();
|
||||
|
||||
const showFooter = !isPathSignIn(pathname);
|
||||
|
||||
@ -48,8 +48,8 @@ export default function Footer() {
|
||||
)}>
|
||||
{userEmail}
|
||||
</div>
|
||||
<form action={() => signOutAndRedirectAction()
|
||||
.then(() => setUserEmail?.(undefined))}>
|
||||
<form action={() => signOutAction()
|
||||
.then(clearAuthStateAndRedirect)}>
|
||||
<SubmitButtonWithStatus styleAs="link">
|
||||
Sign out
|
||||
</SubmitButtonWithStatus>
|
||||
|
||||
@ -11,6 +11,7 @@ import { useAppState } from '@/state/AppState';
|
||||
import { GRID_HOMEPAGE_ENABLED } from './config';
|
||||
import AdminAppMenu from '@/admin/AdminAppMenu';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import Spinner from '@/components/Spinner';
|
||||
|
||||
export type SwitcherSelection = 'feed' | 'grid' | 'admin';
|
||||
|
||||
@ -19,9 +20,13 @@ export default function ViewSwitcher({
|
||||
}: {
|
||||
currentSelection?: SwitcherSelection
|
||||
}) {
|
||||
const { setIsCommandKOpen, isUserSignedIn } = useAppState();
|
||||
const {
|
||||
isUserSignedIn,
|
||||
isUserSignedInEager,
|
||||
setIsCommandKOpen,
|
||||
} = useAppState();
|
||||
|
||||
const renderItemFeed = () =>
|
||||
const renderItemFeed =
|
||||
<SwitcherItem
|
||||
icon={<IconFeed />}
|
||||
href={PATH_FEED_INFERRED}
|
||||
@ -29,7 +34,7 @@ export default function ViewSwitcher({
|
||||
noPadding
|
||||
/>;
|
||||
|
||||
const renderItemGrid = () =>
|
||||
const renderItemGrid =
|
||||
<SwitcherItem
|
||||
icon={<IconGrid />}
|
||||
href={PATH_GRID_INFERRED}
|
||||
@ -40,20 +45,30 @@ export default function ViewSwitcher({
|
||||
return (
|
||||
<div className="flex gap-1 sm:gap-2">
|
||||
<Switcher>
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid() : renderItemFeed()}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed() : renderItemGrid()}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemGrid : renderItemFeed}
|
||||
{GRID_HOMEPAGE_ENABLED ? renderItemFeed : renderItemGrid}
|
||||
{/* Show spinner if admin is suspected to be logged in */}
|
||||
{(isUserSignedInEager && !isUserSignedIn) &&
|
||||
<SwitcherItem
|
||||
icon={<Spinner />}
|
||||
isInteractive={false}
|
||||
noPadding
|
||||
/>}
|
||||
{isUserSignedIn &&
|
||||
<AdminAppMenu
|
||||
className="mt-3 ml-[-84px]"
|
||||
<SwitcherItem
|
||||
icon={<AdminAppMenu
|
||||
className="mt-3 ml-[-94px]"
|
||||
buttonClassName={clsx(
|
||||
'w-[40px] h-[28px]',
|
||||
'flex items-center justify-center',
|
||||
'active:bg-transparent',
|
||||
'bg-transparent dark:bg-transparent',
|
||||
'hover:bg-transparent dark:hover:bg-transparent',
|
||||
'active:bg-transparent dark:active:bg-transparent',
|
||||
currentSelection === 'admin'
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-gray-400 dark:text-gray-600',
|
||||
)}
|
||||
/>}
|
||||
noPadding
|
||||
/>}
|
||||
</Switcher>
|
||||
<Switcher type="borderless">
|
||||
<SwitcherItem
|
||||
|
||||
@ -41,6 +41,9 @@ export const signInAction = async (
|
||||
redirect(formData.get(KEY_CALLBACK_URL) as string || PATH_ADMIN_PHOTOS);
|
||||
};
|
||||
|
||||
export const signOutAction = async () =>
|
||||
signOut({ redirect: false });
|
||||
|
||||
export const signOutAndRedirectAction = async (redirectTo = PATH_SIGN_IN) =>
|
||||
signOut({ redirectTo });
|
||||
|
||||
|
||||
12
src/auth/client.ts
Normal file
12
src/auth/client.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { deleteCookie, getCookie, storeCookie } from '@/utility/cookie';
|
||||
|
||||
const KEY_AUTH_EMAIL = 'authjs.email';
|
||||
|
||||
export const storeAuthEmailCookie = (email: string) =>
|
||||
storeCookie(KEY_AUTH_EMAIL, email);
|
||||
|
||||
export const clearAuthEmailCookie = () =>
|
||||
deleteCookie(KEY_AUTH_EMAIL);
|
||||
|
||||
export const hasAuthEmailCookie = () =>
|
||||
Boolean(getCookie(KEY_AUTH_EMAIL));
|
||||
@ -11,6 +11,7 @@ export default function SwitcherItem({
|
||||
className: classNameProp,
|
||||
onClick,
|
||||
active,
|
||||
isInteractive = true,
|
||||
noPadding,
|
||||
prefetch = SHOULD_PREFETCH_ALL_LINKS,
|
||||
}: {
|
||||
@ -20,21 +21,24 @@ export default function SwitcherItem({
|
||||
className?: string
|
||||
onClick?: () => void
|
||||
active?: boolean
|
||||
isInteractive?: boolean
|
||||
noPadding?: boolean
|
||||
prefetch?: boolean
|
||||
}) {
|
||||
const className = clsx(
|
||||
classNameProp,
|
||||
'flex items-center justify-center',
|
||||
'w-[42px] h-full',
|
||||
'py-0.5 px-1.5',
|
||||
'cursor-pointer',
|
||||
'hover:bg-gray-100/60 active:bg-gray-100',
|
||||
'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
|
||||
isInteractive && 'cursor-pointer',
|
||||
isInteractive && 'hover:bg-gray-100/60 active:bg-gray-100',
|
||||
isInteractive && 'dark:hover:bg-gray-900/75 dark:active:bg-gray-900',
|
||||
active
|
||||
? 'text-black dark:text-white'
|
||||
: 'text-gray-400 dark:text-gray-600',
|
||||
active
|
||||
? 'hover:text-black dark:hover:text-white'
|
||||
: 'hover:text-gray-700 dark:hover:text-gray-400',
|
||||
classNameProp,
|
||||
);
|
||||
|
||||
const renderIcon = () => noPadding
|
||||
@ -54,6 +58,8 @@ export default function SwitcherItem({
|
||||
}}>
|
||||
{renderIcon()}
|
||||
</LinkWithLoader>
|
||||
: <div {...{ title, onClick, className }}>{renderIcon()}</div>
|
||||
: <div {...{ title, onClick, className }}>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -39,7 +39,7 @@ import { searchPhotosAction } from '@/photo/actions';
|
||||
import { RiToolsFill } from 'react-icons/ri';
|
||||
import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
|
||||
import { HiDocumentText } from 'react-icons/hi';
|
||||
import { signOutAndRedirectAction } from '@/auth/actions';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import { TbPhoto } from 'react-icons/tb';
|
||||
import { getKeywordsForPhoto, titleForPhoto } from '@/photo';
|
||||
import PhotoDate from '@/photo/PhotoDate';
|
||||
@ -101,7 +101,7 @@ export default function CommandKClient({
|
||||
|
||||
const {
|
||||
isUserSignedIn,
|
||||
setUserEmail,
|
||||
clearAuthStateAndRedirect,
|
||||
isCommandKOpen: isOpen,
|
||||
photosCountHidden,
|
||||
uploadsCount,
|
||||
@ -400,9 +400,7 @@ export default function CommandKClient({
|
||||
}
|
||||
adminSection.items.push({
|
||||
label: 'Sign Out',
|
||||
action: () => {
|
||||
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
|
||||
},
|
||||
action: () => signOutAction().then(clearAuthStateAndRedirect),
|
||||
});
|
||||
} else {
|
||||
adminSection.items.push({
|
||||
|
||||
@ -197,7 +197,6 @@ export const formHasTextContent = ({
|
||||
// CREATE FORM DATA: FROM PHOTO
|
||||
|
||||
export const convertPhotoToFormData = (photo: Photo): PhotoFormData => {
|
||||
console.log('convertPhotoToFormData', photo);
|
||||
const valueForKey = (key: keyof Photo, value: any) => {
|
||||
switch (key) {
|
||||
case 'tags':
|
||||
|
||||
@ -133,8 +133,6 @@ export const getGitHubPublicFork = async (): Promise<RepoParams> => {
|
||||
};
|
||||
|
||||
export const getGitHubMeta = async (params: RepoParams) => {
|
||||
console.log('getGitHubMeta', params);
|
||||
|
||||
const urlOwner = getGitHubUrlOwner(params);
|
||||
const urlRepo = getGitHubUrlRepo(params);
|
||||
const urlBranch = getGitHubUrlBranch(params);
|
||||
|
||||
@ -20,10 +20,13 @@ export interface AppStateContext {
|
||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||
shareModalProps?: ShareModalProps
|
||||
setShareModalProps?: Dispatch<SetStateAction<ShareModalProps | undefined>>
|
||||
// ADMIN
|
||||
// AUTH
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
isUserSignedInEager?: boolean
|
||||
clearAuthStateAndRedirect?: () => void
|
||||
// ADMIN
|
||||
adminUpdateTimes?: Date[]
|
||||
registerAdminUpdate?: () => void
|
||||
photosCount?: number
|
||||
|
||||
@ -16,6 +16,13 @@ import { ShareModalProps } from '@/share';
|
||||
import { storeTimezoneCookie } from '@/utility/timezone';
|
||||
import { InsightIndicatorStatus } from '@/admin/insights';
|
||||
import { getAdminDataAction } from '@/admin/actions';
|
||||
import {
|
||||
storeAuthEmailCookie,
|
||||
clearAuthEmailCookie,
|
||||
hasAuthEmailCookie,
|
||||
} from '@/auth/client';
|
||||
import { useRouter } from 'next/navigation';
|
||||
import { PATH_SIGN_IN } from '@/app/paths';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -24,6 +31,8 @@ export default function AppStateProvider({
|
||||
}) {
|
||||
const { previousPathname } = usePathnames();
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
// CORE
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
@ -41,6 +50,9 @@ export default function AppStateProvider({
|
||||
// ADMIN
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [isUserSignedInEager, setIsUserSignedInEager] =
|
||||
useState(false);
|
||||
// ADMIN
|
||||
const [adminUpdateTimes, setAdminUpdateTimes] =
|
||||
useState<Date[]>([]);
|
||||
const [photosCount, setPhotosCount] =
|
||||
@ -77,6 +89,7 @@ export default function AppStateProvider({
|
||||
|
||||
const { data: auth, error: authError } = useSWR('getAuth', getAuthAction);
|
||||
useEffect(() => {
|
||||
setIsUserSignedInEager(hasAuthEmailCookie());
|
||||
if (!authError) {
|
||||
setUserEmail(auth?.user?.email ?? undefined);
|
||||
}
|
||||
@ -91,7 +104,8 @@ export default function AppStateProvider({
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isUserSignedIn) {
|
||||
if (userEmail) {
|
||||
storeAuthEmailCookie(userEmail);
|
||||
if (adminData) {
|
||||
const timeout = setTimeout(() => {
|
||||
setPhotosCount(adminData.countPhotos);
|
||||
@ -105,7 +119,7 @@ export default function AppStateProvider({
|
||||
} else {
|
||||
setPhotosCountHidden(0);
|
||||
}
|
||||
}, [adminData, adminError, isUserSignedIn]);
|
||||
}, [adminData, adminError, userEmail]);
|
||||
|
||||
const registerAdminUpdate = useCallback(() =>
|
||||
setAdminUpdateTimes(updates => [...updates, new Date()])
|
||||
@ -116,6 +130,12 @@ export default function AppStateProvider({
|
||||
storeTimezoneCookie();
|
||||
}, []);
|
||||
|
||||
const clearAuthStateAndRedirect = useCallback((shouldRedirect = true) => {
|
||||
setUserEmail(undefined);
|
||||
clearAuthEmailCookie();
|
||||
if (shouldRedirect) { router.push(PATH_SIGN_IN); }
|
||||
}, [router]);
|
||||
|
||||
return (
|
||||
<AppStateContext.Provider
|
||||
value={{
|
||||
@ -135,10 +155,13 @@ export default function AppStateProvider({
|
||||
setIsCommandKOpen,
|
||||
shareModalProps,
|
||||
setShareModalProps,
|
||||
// ADMIN
|
||||
// AUTH
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
isUserSignedIn,
|
||||
isUserSignedInEager,
|
||||
clearAuthStateAndRedirect,
|
||||
// ADMIN
|
||||
adminUpdateTimes,
|
||||
registerAdminUpdate,
|
||||
photosCount,
|
||||
|
||||
@ -5,19 +5,25 @@ export const storeCookie = (
|
||||
maxAge = 63158400,
|
||||
sameSite = 'Lax',
|
||||
) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie =
|
||||
`${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`;
|
||||
}
|
||||
};
|
||||
|
||||
export const getCookie = (name: string) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
const cookie: Record<string, string> = {};
|
||||
document.cookie.split(';').forEach(function(el) {
|
||||
const split = el.split('=');
|
||||
cookie[split[0].trim()] = split.slice(1).join('=');
|
||||
});
|
||||
return cookie[name];
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteCookie = (name: string) => {
|
||||
if (typeof document !== 'undefined') {
|
||||
document.cookie = `${name}=;Max-Age=0`;
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
Reference in New Issue
Block a user