Move auth to client state
This commit is contained in:
parent
2f11e8b0cf
commit
ef1c8fc79d
@ -9,6 +9,7 @@ import { isPathFavs, isPhotoFav } from '@/tag';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { BiTrash } from 'react-icons/bi';
|
||||
import MoreMenu from '@/components/MoreMenu';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function AdminPhotoMenuClient({
|
||||
photo,
|
||||
@ -16,14 +17,16 @@ export default function AdminPhotoMenuClient({
|
||||
}: Omit<ComponentProps<typeof MoreMenu>, 'items'> & {
|
||||
photo: Photo
|
||||
}) {
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const isFav = isPhotoFav(photo);
|
||||
const path = usePathname();
|
||||
const shouldRedirectFav = isPathFavs(path) && isFav;
|
||||
const shouldRedirectDelete = pathForPhoto(photo.id) === path;
|
||||
|
||||
return (
|
||||
<>
|
||||
<MoreMenu {...{
|
||||
isUserSignedIn
|
||||
? <MoreMenu {...{
|
||||
items: [
|
||||
{
|
||||
label: 'Edit',
|
||||
@ -63,6 +66,6 @@ export default function AdminPhotoMenuClient({
|
||||
],
|
||||
...props,
|
||||
}}/>
|
||||
</>
|
||||
: null
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import AdminNav from '@/admin/AdminNav';
|
||||
import AdminNavClient from '@/admin/AdminNavClient';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export default async function AdminLayout({
|
||||
children,
|
||||
@ -9,13 +7,7 @@ export default async function AdminLayout({
|
||||
}) {
|
||||
return (
|
||||
<div className="mt-4 space-y-5">
|
||||
<Suspense fallback={<AdminNavClient items={[{
|
||||
label: 'Photos',
|
||||
count: 0,
|
||||
href: '/admin/photos',
|
||||
}]} />}>
|
||||
<AdminNav />
|
||||
</Suspense>
|
||||
<AdminNav />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -11,7 +11,6 @@ import { Metadata } from 'next/types';
|
||||
import PhotoGridSidebar from '@/photo/PhotoGridSidebar';
|
||||
import { getPhotoSidebarDataCached } from '@/photo/data';
|
||||
import { MorePhotosGrid } from '@/photo/MorePhotosGrid';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export const revalidate = 3600;
|
||||
|
||||
@ -37,13 +36,11 @@ export default async function GridPage() {
|
||||
? <SiteGrid
|
||||
contentMain={<div className="space-y-0.5 sm:space-y-1">
|
||||
<PhotoGrid {...{ photos, photoPriority: true }} />
|
||||
<Suspense>
|
||||
<MorePhotosGrid
|
||||
initialOffset={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
itemsPerRequest={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
totalPhotosCount={photosCount}
|
||||
/>
|
||||
</Suspense>
|
||||
<MorePhotosGrid
|
||||
initialOffset={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
itemsPerRequest={INFINITE_SCROLL_MULTIPLE_GRID}
|
||||
totalPhotosCount={photosCount}
|
||||
/>
|
||||
</div>}
|
||||
contentSide={<div className="sticky top-4 space-y-4 mt-[-4px]">
|
||||
<PhotoGridSidebar {...{
|
||||
|
||||
@ -9,12 +9,12 @@ import PhotoEscapeHandler from '@/photo/PhotoEscapeHandler';
|
||||
import { Metadata } from 'next/types';
|
||||
import MoreComponentsProvider from '@/state/MoreComponentsProvider';
|
||||
import { ThemeProvider } from 'next-themes';
|
||||
import Nav from '@/site/Nav';
|
||||
import Footer from '@/site/Footer';
|
||||
import CommandK from '@/site/CommandK';
|
||||
|
||||
import '../site/globals.css';
|
||||
import '../site/sonner.css';
|
||||
import NavClient from '@/site/NavClient';
|
||||
import CommandKClient from '@/components/CommandKClient';
|
||||
import FooterClient from '@/site/FooterClient';
|
||||
|
||||
const ibmPlexMono = IBM_Plex_Mono({
|
||||
subsets: ['latin'],
|
||||
@ -78,16 +78,16 @@ export default function RootLayout({
|
||||
'mx-3 mb-3',
|
||||
'lg:mx-6 lg:mb-6',
|
||||
)}>
|
||||
<NavClient />
|
||||
<Nav />
|
||||
<div className={clsx(
|
||||
'min-h-[16rem] sm:min-h-[30rem]',
|
||||
'mb-12',
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
<FooterClient />
|
||||
<Footer />
|
||||
</main>
|
||||
<CommandKClient />
|
||||
<CommandK />
|
||||
</ThemeProvider>
|
||||
</MoreComponentsProvider>
|
||||
<Analytics debug={false} />
|
||||
|
||||
@ -3,16 +3,19 @@
|
||||
import FieldSetWithStatus from '@/components/FieldSetWithStatus';
|
||||
import InfoBlock from '@/components/InfoBlock';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { useLayoutEffect, useRef, useState } from 'react';
|
||||
import { signInAction } from './actions';
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { getCurrentUser, signInAction } from './actions';
|
||||
import { useFormState } from 'react-dom';
|
||||
import ErrorNote from '@/components/ErrorNote';
|
||||
import { KEY_CALLBACK_URL, KEY_CREDENTIALS_SIGN_IN_ERROR } from '.';
|
||||
import { useSearchParams } from 'next/navigation';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function SignInForm() {
|
||||
const params = useSearchParams();
|
||||
|
||||
const { setUserEmail } = useAppState();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [response, action] = useFormState(signInAction, undefined);
|
||||
@ -22,6 +25,13 @@ export default function SignInForm() {
|
||||
emailRef.current?.focus();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Capture user email before unmounting
|
||||
getCurrentUser().then(user => setUserEmail?.(user?.email ?? undefined));
|
||||
};
|
||||
}, [setUserEmail]);
|
||||
|
||||
const isFormValid =
|
||||
email.length > 0 &&
|
||||
password.length > 0;
|
||||
|
||||
@ -4,10 +4,11 @@ import {
|
||||
KEY_CALLBACK_URL,
|
||||
KEY_CREDENTIALS_SIGN_IN_ERROR,
|
||||
KEY_CREDENTIALS_SIGN_IN_ERROR_URL,
|
||||
auth,
|
||||
signIn,
|
||||
signOut,
|
||||
} from '@/auth';
|
||||
import { PATH_ADMIN_PHOTOS } from '@/site/paths';
|
||||
import { PATH_ADMIN_PHOTOS, PATH_ROOT } from '@/site/paths';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const signInAction = async (
|
||||
@ -35,6 +36,7 @@ export const signInAction = async (
|
||||
redirect(formData.get(KEY_CALLBACK_URL) as string || PATH_ADMIN_PHOTOS);
|
||||
};
|
||||
|
||||
export const signOutAction = async () => {
|
||||
await signOut();
|
||||
};
|
||||
export const signOutAndRedirectAction = async () =>
|
||||
signOut({ redirectTo: PATH_ROOT });
|
||||
|
||||
export const getCurrentUser = async () => (await auth())?.user;
|
||||
|
||||
@ -9,6 +9,14 @@ import {
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react';
|
||||
import {
|
||||
PATH_ADMIN_BASELINE,
|
||||
PATH_ADMIN_CONFIGURATION,
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_ADMIN_TAGS,
|
||||
PATH_ADMIN_UPLOADS,
|
||||
PATH_SIGN_IN,
|
||||
} from '../site/paths';
|
||||
import Modal from './Modal';
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { useDebounce } from 'use-debounce';
|
||||
@ -20,22 +28,27 @@ import { IoInvertModeSharp } from 'react-icons/io5';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
import { getPhotoItemsAction } 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';
|
||||
|
||||
const LISTENER_KEYDOWN = 'keydown';
|
||||
const MINIMUM_QUERY_LENGTH = 2;
|
||||
|
||||
type CommandKItem = {
|
||||
label: string
|
||||
keywords?: string[]
|
||||
annotation?: ReactNode
|
||||
annotationAria?: string
|
||||
accessory?: ReactNode
|
||||
path?: string
|
||||
action?: () => void | Promise<void>
|
||||
}
|
||||
|
||||
export type CommandKSection = {
|
||||
heading: string
|
||||
accessory?: ReactNode
|
||||
items: {
|
||||
label: string
|
||||
keywords?: string[]
|
||||
annotation?: ReactNode
|
||||
annotationAria?: string
|
||||
accessory?: ReactNode
|
||||
path?: string
|
||||
action?: () => void
|
||||
}[]
|
||||
items: CommandKItem[]
|
||||
}
|
||||
|
||||
export default function CommandKClient({
|
||||
@ -48,6 +61,8 @@ export default function CommandKClient({
|
||||
footer?: string
|
||||
}) {
|
||||
const {
|
||||
isUserSignedIn,
|
||||
setUserEmail,
|
||||
isCommandKOpen: isOpen,
|
||||
setIsCommandKOpen: setIsOpen,
|
||||
setShouldRespondToKeyboardCommands,
|
||||
@ -167,6 +182,57 @@ export default function CommandKClient({
|
||||
});
|
||||
}
|
||||
|
||||
const sectionPages: CommandKSection = {
|
||||
heading: 'Pages',
|
||||
accessory: <HiDocumentText size={15} className="translate-x-[-1px]" />,
|
||||
items: ([{
|
||||
label: 'Home',
|
||||
path: '/',
|
||||
}, {
|
||||
label: 'Grid',
|
||||
path:'/grid',
|
||||
}]),
|
||||
};
|
||||
|
||||
const adminSection: CommandKSection = {
|
||||
heading: 'Admin',
|
||||
accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />,
|
||||
items: isUserSignedIn
|
||||
? ([{
|
||||
label: 'Manage Photos',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_PHOTOS,
|
||||
}, {
|
||||
label: 'Manage Uploads',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_UPLOADS,
|
||||
}, {
|
||||
label: 'Manage Tags',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_TAGS,
|
||||
}, {
|
||||
label: 'App Config',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_CONFIGURATION,
|
||||
}] as CommandKItem[])
|
||||
.concat(showDebugTools
|
||||
? [{
|
||||
label: 'Baseline Overview',
|
||||
path: PATH_ADMIN_BASELINE,
|
||||
}]
|
||||
: [])
|
||||
.concat({
|
||||
label: 'Sign Out',
|
||||
action: () => {
|
||||
signOutAndRedirectAction().then(() => setUserEmail?.(undefined));
|
||||
},
|
||||
})
|
||||
: [{
|
||||
label: 'Sign In',
|
||||
path: PATH_SIGN_IN,
|
||||
}],
|
||||
};
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
open={isOpen}
|
||||
@ -219,6 +285,8 @@ export default function CommandKClient({
|
||||
</Command.Empty>
|
||||
{queriedSections
|
||||
.concat(serverSections)
|
||||
.concat(sectionPages)
|
||||
.concat(adminSection)
|
||||
.concat(clientSections)
|
||||
.filter(({ items }) => items.length > 0)
|
||||
.map(({ heading, accessory, items }) =>
|
||||
|
||||
@ -144,9 +144,9 @@ export default function FieldSetWithStatus({
|
||||
Boolean(error) && 'error',
|
||||
)}
|
||||
/>}
|
||||
<div>
|
||||
{accessory && <div>
|
||||
{accessory}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -6,12 +6,6 @@ import {
|
||||
getUniqueTagsCached,
|
||||
} from '@/photo/cache';
|
||||
import {
|
||||
PATH_ADMIN_BASELINE,
|
||||
PATH_ADMIN_CONFIGURATION,
|
||||
PATH_ADMIN_PHOTOS,
|
||||
PATH_ADMIN_TAGS,
|
||||
PATH_ADMIN_UPLOADS,
|
||||
PATH_SIGN_IN,
|
||||
pathForCamera,
|
||||
pathForFilmSimulation,
|
||||
pathForTag,
|
||||
@ -20,13 +14,10 @@ import { formatCameraText } from '@/camera';
|
||||
import { authCachedSafe } from '@/auth/cache';
|
||||
import { photoQuantityText } from '@/photo';
|
||||
import { formatCount, formatCountDescriptive } from '@/utility/string';
|
||||
import { BiLockAlt, BiSolidUser } from 'react-icons/bi';
|
||||
import { sortTagsObject } from '@/tag';
|
||||
import PhotoFilmSimulationIcon from '@/simulation/PhotoFilmSimulationIcon';
|
||||
import { FaTag } from 'react-icons/fa';
|
||||
import { IoMdCamera } from 'react-icons/io';
|
||||
import { HiDocumentText } from 'react-icons/hi';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import { ADMIN_DEBUG_TOOLS_ENABLED } from './config';
|
||||
|
||||
export default async function CommandK() {
|
||||
@ -84,62 +75,11 @@ export default async function CommandK() {
|
||||
})),
|
||||
};
|
||||
|
||||
const SECTION_PAGES: CommandKSection = {
|
||||
heading: 'Pages',
|
||||
accessory: <HiDocumentText size={15} className="translate-x-[-1px]" />,
|
||||
items: ([{
|
||||
label: 'Home',
|
||||
path: '/',
|
||||
}, {
|
||||
label: 'Grid',
|
||||
path:'/grid',
|
||||
}]),
|
||||
};
|
||||
|
||||
const SECTION_ADMIN: CommandKSection = {
|
||||
heading: 'Admin',
|
||||
accessory: <BiSolidUser size={15} className="translate-x-[-1px]" />,
|
||||
items: isAdminLoggedIn
|
||||
? [{
|
||||
label: 'Manage Photos',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_PHOTOS,
|
||||
}, {
|
||||
label: 'Manage Uploads',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_UPLOADS,
|
||||
}, {
|
||||
label: 'Manage Tags',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_TAGS,
|
||||
}, {
|
||||
label: 'App Config',
|
||||
annotation: <BiLockAlt />,
|
||||
path: PATH_ADMIN_CONFIGURATION,
|
||||
}, {
|
||||
label: 'Sign Out',
|
||||
action: signOutAction,
|
||||
}]
|
||||
: [{
|
||||
label: 'Sign In',
|
||||
path: PATH_SIGN_IN,
|
||||
}],
|
||||
};
|
||||
|
||||
if (isAdminLoggedIn && ADMIN_DEBUG_TOOLS_ENABLED) {
|
||||
SECTION_ADMIN.items.push({
|
||||
label: 'Baseline Overview',
|
||||
path: PATH_ADMIN_BASELINE,
|
||||
});
|
||||
}
|
||||
|
||||
return <CommandKClient
|
||||
serverSections={[
|
||||
SECTION_TAGS,
|
||||
SECTION_CAMERAS,
|
||||
SECTION_FILM,
|
||||
SECTION_PAGES,
|
||||
SECTION_ADMIN,
|
||||
]}
|
||||
showDebugTools={isAdminLoggedIn && ADMIN_DEBUG_TOOLS_ENABLED}
|
||||
footer={photoQuantityText(count, false)}
|
||||
|
||||
@ -1,9 +1,75 @@
|
||||
import { authCachedSafe } from '@/auth/cache';
|
||||
import FooterClient from './FooterClient';
|
||||
'use client';
|
||||
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SiteGrid from '../components/SiteGrid';
|
||||
import ThemeSwitcher from '@/site/ThemeSwitcher';
|
||||
import Link from 'next/link';
|
||||
import { SHOW_REPO_LINK } from '@/site/config';
|
||||
import RepoLink from '../components/RepoLink';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { isPathAdmin, isPathSignIn, pathForAdminPhotos } from './paths';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { signOutAndRedirectAction } from '@/auth/actions';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function Footer() {
|
||||
const pathname = usePathname();
|
||||
|
||||
const { userEmail, setUserEmail } = useAppState();
|
||||
|
||||
const showFooter = !isPathSignIn(pathname);
|
||||
|
||||
const shouldAnimate = !isPathAdmin(pathname);
|
||||
|
||||
export default async function Footer() {
|
||||
const session = await authCachedSafe();
|
||||
return (
|
||||
<FooterClient userEmail={session?.user?.email} />
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
type={!shouldAnimate ? 'none' : 'bottom'}
|
||||
distanceOffset={10}
|
||||
items={showFooter
|
||||
? [<div
|
||||
key="footer"
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
'text-dim min-h-10',
|
||||
)}>
|
||||
<div className="flex gap-x-4 gap-y-0.5 flex-grow flex-wrap">
|
||||
{isPathAdmin(pathname)
|
||||
? <>
|
||||
{userEmail === undefined &&
|
||||
<Spinner />}
|
||||
{userEmail && <>
|
||||
<div className={clsx(
|
||||
'truncate max-w-full',
|
||||
)}>
|
||||
{userEmail}
|
||||
</div>
|
||||
<form action={() => signOutAndRedirectAction()
|
||||
.then(() => setUserEmail?.(undefined))}>
|
||||
<SubmitButtonWithStatus styleAsLink>
|
||||
Sign out
|
||||
</SubmitButtonWithStatus>
|
||||
</form>
|
||||
</>}
|
||||
</>
|
||||
: <>
|
||||
<Link href={pathForAdminPhotos()}>
|
||||
Admin
|
||||
</Link>
|
||||
{SHOW_REPO_LINK &&
|
||||
<RepoLink />}
|
||||
</>}
|
||||
</div>
|
||||
<div className="flex items-center h-10">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>]
|
||||
: []}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { clsx } from 'clsx/lite';
|
||||
import SiteGrid from '../components/SiteGrid';
|
||||
import ThemeSwitcher from '@/site/ThemeSwitcher';
|
||||
import Link from 'next/link';
|
||||
import { SHOW_REPO_LINK } from '@/site/config';
|
||||
import RepoLink from '../components/RepoLink';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import { isPathAdmin, isPathSignIn, pathForAdminPhotos } from './paths';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { signOutAction } from '@/auth/actions';
|
||||
import Spinner from '@/components/Spinner';
|
||||
import AnimateItems from '@/components/AnimateItems';
|
||||
|
||||
export default function FooterClient({
|
||||
userEmail,
|
||||
}: {
|
||||
userEmail?: string | null | undefined
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const showFooter = !isPathSignIn(pathname);
|
||||
|
||||
const shouldAnimate = !isPathAdmin(pathname);
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
type={!shouldAnimate ? 'none' : 'bottom'}
|
||||
distanceOffset={10}
|
||||
items={showFooter
|
||||
? [<div
|
||||
key="footer"
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
'text-dim min-h-10',
|
||||
)}>
|
||||
<div className="flex gap-x-4 gap-y-0.5 flex-grow flex-wrap">
|
||||
{isPathAdmin(pathname)
|
||||
? <>
|
||||
{userEmail === undefined &&
|
||||
<Spinner />}
|
||||
{userEmail && <>
|
||||
<div className={clsx(
|
||||
'truncate max-w-full',
|
||||
)}>
|
||||
{userEmail}
|
||||
</div>
|
||||
<form action={signOutAction}>
|
||||
<SubmitButtonWithStatus styleAsLink>
|
||||
Sign out
|
||||
</SubmitButtonWithStatus>
|
||||
</form>
|
||||
</>}
|
||||
</>
|
||||
: <>
|
||||
<Link href={pathForAdminPhotos()}>
|
||||
Admin
|
||||
</Link>
|
||||
{SHOW_REPO_LINK &&
|
||||
<RepoLink />}
|
||||
</>}
|
||||
</div>
|
||||
<div className="flex items-center h-10">
|
||||
<ThemeSwitcher />
|
||||
</div>
|
||||
</div>]
|
||||
: []}
|
||||
/>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1,16 +1,76 @@
|
||||
import { authCachedSafe } from '@/auth/cache';
|
||||
import NavClient from './NavClient';
|
||||
'use client';
|
||||
|
||||
export default async function Nav({
|
||||
animate,
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import SiteGrid from '../components/SiteGrid';
|
||||
import { SITE_DOMAIN_OR_TITLE } from '@/site/config';
|
||||
import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
isPathGrid,
|
||||
isPathProtected,
|
||||
isPathSignIn,
|
||||
} from '@/site/paths';
|
||||
import AnimateItems from '../components/AnimateItems';
|
||||
import { useAppState } from '@/state/AppState';
|
||||
|
||||
export default function Nav({
|
||||
animate = true,
|
||||
}: {
|
||||
animate?: boolean
|
||||
animate?: boolean,
|
||||
}) {
|
||||
const session = await authCachedSafe();
|
||||
const pathname = usePathname();
|
||||
|
||||
const { isUserSignedIn } = useAppState();
|
||||
|
||||
const showNav = !isPathSignIn(pathname);
|
||||
|
||||
const renderLink = (
|
||||
text: string,
|
||||
linkOrAction: string | (() => void),
|
||||
) =>
|
||||
typeof linkOrAction === 'string'
|
||||
? <Link href={linkOrAction}>{text}</Link>
|
||||
: <button onClick={linkOrAction}>{text}</button>;
|
||||
|
||||
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
|
||||
if (pathname === PATH_ROOT) {
|
||||
return 'full-frame';
|
||||
} else if (isPathGrid(pathname)) {
|
||||
return 'grid';
|
||||
} else if (isPathProtected(pathname)) {
|
||||
return 'admin';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavClient
|
||||
showAdmin={Boolean(session?.user?.email)}
|
||||
animate={animate}
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
type={animate ? 'bottom' : 'none'}
|
||||
distanceOffset={10}
|
||||
items={showNav
|
||||
? [<div
|
||||
key="nav"
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
'w-full min-h-[4rem]',
|
||||
)}>
|
||||
<div className="flex-grow">
|
||||
<ViewSwitcher
|
||||
currentSelection={switcherSelectionForPath()}
|
||||
showAdmin={isUserSignedIn}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden xs:block text-right text-balance">
|
||||
{renderLink(SITE_DOMAIN_OR_TITLE, PATH_ROOT)}
|
||||
</div>
|
||||
</div>]
|
||||
: []}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,75 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import { clsx } from 'clsx/lite';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Link from 'next/link';
|
||||
import SiteGrid from '../components/SiteGrid';
|
||||
import { SITE_DOMAIN_OR_TITLE } from '@/site/config';
|
||||
import ViewSwitcher, { SwitcherSelection } from '@/site/ViewSwitcher';
|
||||
import {
|
||||
PATH_ROOT,
|
||||
isPathGrid,
|
||||
isPathProtected,
|
||||
isPathSignIn,
|
||||
} from '@/site/paths';
|
||||
import AnimateItems from '../components/AnimateItems';
|
||||
|
||||
export default function NavClient({
|
||||
showAdmin,
|
||||
animate = true,
|
||||
}: {
|
||||
showAdmin?: boolean,
|
||||
animate?: boolean,
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
|
||||
const showNav = !isPathSignIn(pathname);
|
||||
|
||||
const renderLink = (
|
||||
text: string,
|
||||
linkOrAction: string | (() => void),
|
||||
) =>
|
||||
typeof linkOrAction === 'string'
|
||||
? <Link href={linkOrAction}>{text}</Link>
|
||||
: <button onClick={linkOrAction}>{text}</button>;
|
||||
|
||||
const switcherSelectionForPath = (): SwitcherSelection | undefined => {
|
||||
if (pathname === PATH_ROOT) {
|
||||
return 'full-frame';
|
||||
} else if (isPathGrid(pathname)) {
|
||||
return 'grid';
|
||||
} else if (isPathProtected(pathname)) {
|
||||
return 'admin';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<AnimateItems
|
||||
animateOnFirstLoadOnly
|
||||
type={animate ? 'bottom' : 'none'}
|
||||
distanceOffset={10}
|
||||
items={showNav
|
||||
? [<div
|
||||
key="nav"
|
||||
className={clsx(
|
||||
'flex items-center',
|
||||
'w-full min-h-[4rem]',
|
||||
)}>
|
||||
<div className="flex-grow">
|
||||
<ViewSwitcher
|
||||
currentSelection={switcherSelectionForPath()}
|
||||
showAdmin={showAdmin}
|
||||
/>
|
||||
</div>
|
||||
<div className="hidden xs:block text-right text-balance">
|
||||
{renderLink(SITE_DOMAIN_OR_TITLE, PATH_ROOT)}
|
||||
</div>
|
||||
</div>]
|
||||
: []}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -4,6 +4,9 @@ import { AnimationConfig } from '@/components/AnimateItems';
|
||||
export interface AppStateContext {
|
||||
previousPathname?: string
|
||||
hasLoaded?: boolean
|
||||
userEmail?: string
|
||||
setUserEmail?: Dispatch<SetStateAction<string | undefined>>
|
||||
isUserSignedIn?: boolean
|
||||
setHasLoaded?: Dispatch<SetStateAction<boolean>>
|
||||
nextPhotoAnimation?: AnimationConfig
|
||||
setNextPhotoAnimation?: Dispatch<SetStateAction<AnimationConfig | undefined>>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useState, useEffect, ReactNode } from 'react';
|
||||
import { AppStateContext } from './AppState';
|
||||
import { AnimationConfig } from '@/components/AnimateItems';
|
||||
import usePathnames from '@/utility/usePathnames';
|
||||
import { getCurrentUser } from '@/auth/actions';
|
||||
|
||||
export default function AppStateProvider({
|
||||
children,
|
||||
@ -14,6 +15,8 @@ export default function AppStateProvider({
|
||||
|
||||
const [hasLoaded, setHasLoaded] =
|
||||
useState(false);
|
||||
const [userEmail, setUserEmail] =
|
||||
useState<string>();
|
||||
const [nextPhotoAnimation, setNextPhotoAnimation] =
|
||||
useState<AnimationConfig>();
|
||||
const [shouldRespondToKeyboardCommands, setShouldRespondToKeyboardCommands] =
|
||||
@ -25,6 +28,7 @@ export default function AppStateProvider({
|
||||
|
||||
useEffect(() => {
|
||||
setHasLoaded?.(true);
|
||||
getCurrentUser().then(user => setUserEmail?.(user?.email ?? undefined));
|
||||
}, [setHasLoaded]);
|
||||
|
||||
return (
|
||||
@ -33,6 +37,9 @@ export default function AppStateProvider({
|
||||
previousPathname,
|
||||
hasLoaded,
|
||||
setHasLoaded,
|
||||
isUserSignedIn: userEmail !== undefined,
|
||||
userEmail,
|
||||
setUserEmail,
|
||||
nextPhotoAnimation,
|
||||
setNextPhotoAnimation,
|
||||
shouldRespondToKeyboardCommands,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user