diff --git a/.vscode/settings.json b/.vscode/settings.json index a707b354..df4f6cbe 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -6,6 +6,7 @@ "ARROWLEFT", "ARROWRIGHT", "Astia", + "authjs", "camelcase", "cloudflarestorage", "cmdk", diff --git a/src/admin/AdminAppMenu.tsx b/src/admin/AdminAppMenu.tsx index a0b62c91..5147eecc 100644 --- a/src/admin/AdminAppMenu.tsx +++ b/src/admin/AdminAppMenu.tsx @@ -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: , - action: signOutAndRedirectAction, + action: () => signOutAction().then(clearAuthStateAndRedirect), }); return ( diff --git a/src/app/Footer.tsx b/src/app/Footer.tsx index 3579b55c..d639b014 100644 --- a/src/app/Footer.tsx +++ b/src/app/Footer.tsx @@ -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} -
signOutAndRedirectAction() - .then(() => setUserEmail?.(undefined))}> + signOutAction() + .then(clearAuthStateAndRedirect)}> Sign out diff --git a/src/app/ViewSwitcher.tsx b/src/app/ViewSwitcher.tsx index 407ade46..d0cf9222 100644 --- a/src/app/ViewSwitcher.tsx +++ b/src/app/ViewSwitcher.tsx @@ -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 = } href={PATH_FEED_INFERRED} @@ -29,7 +34,7 @@ export default function ViewSwitcher({ noPadding />; - const renderItemGrid = () => + const renderItemGrid = } href={PATH_GRID_INFERRED} @@ -40,19 +45,29 @@ export default function ViewSwitcher({ return (
- {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) && + } + isInteractive={false} + noPadding + />} {isUserSignedIn && - } + noPadding />} diff --git a/src/auth/actions.ts b/src/auth/actions.ts index 28d4e0ba..1f0ba968 100644 --- a/src/auth/actions.ts +++ b/src/auth/actions.ts @@ -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 }); diff --git a/src/auth/client.ts b/src/auth/client.ts new file mode 100644 index 00000000..49645be4 --- /dev/null +++ b/src/auth/client.ts @@ -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)); diff --git a/src/components/SwitcherItem.tsx b/src/components/SwitcherItem.tsx index b69b00fc..38eabe07 100644 --- a/src/components/SwitcherItem.tsx +++ b/src/components/SwitcherItem.tsx @@ -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()} - :
{renderIcon()}
+ :
+ {renderIcon()} +
); }; diff --git a/src/components/cmdk/CommandKClient.tsx b/src/components/cmdk/CommandKClient.tsx index 1c8e6a56..3bf4bd5d 100644 --- a/src/components/cmdk/CommandKClient.tsx +++ b/src/components/cmdk/CommandKClient.tsx @@ -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({ diff --git a/src/photo/form/index.ts b/src/photo/form/index.ts index 62ed6e0b..515c0787 100644 --- a/src/photo/form/index.ts +++ b/src/photo/form/index.ts @@ -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': diff --git a/src/platforms/github.ts b/src/platforms/github.ts index 8bb89823..afb14efb 100644 --- a/src/platforms/github.ts +++ b/src/platforms/github.ts @@ -133,8 +133,6 @@ export const getGitHubPublicFork = async (): Promise => { }; export const getGitHubMeta = async (params: RepoParams) => { - console.log('getGitHubMeta', params); - const urlOwner = getGitHubUrlOwner(params); const urlRepo = getGitHubUrlRepo(params); const urlBranch = getGitHubUrlBranch(params); diff --git a/src/state/AppState.ts b/src/state/AppState.ts index 8deaa844..6a24e41c 100644 --- a/src/state/AppState.ts +++ b/src/state/AppState.ts @@ -20,10 +20,13 @@ export interface AppStateContext { setIsCommandKOpen?: Dispatch> shareModalProps?: ShareModalProps setShareModalProps?: Dispatch> - // ADMIN + // AUTH userEmail?: string setUserEmail?: Dispatch> isUserSignedIn?: boolean + isUserSignedInEager?: boolean + clearAuthStateAndRedirect?: () => void + // ADMIN adminUpdateTimes?: Date[] registerAdminUpdate?: () => void photosCount?: number diff --git a/src/state/AppStateProvider.tsx b/src/state/AppStateProvider.tsx index a961194e..83452742 100644 --- a/src/state/AppStateProvider.tsx +++ b/src/state/AppStateProvider.tsx @@ -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(); + const [isUserSignedInEager, setIsUserSignedInEager] = + useState(false); + // ADMIN const [adminUpdateTimes, setAdminUpdateTimes] = useState([]); 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 ( { - document.cookie = - `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`; + if (typeof document !== 'undefined') { + document.cookie = + `${name}=${value};Path=${path};Max-Age=${maxAge};SameSite=${sameSite}`; + } }; export const getCookie = (name: string) => { - const cookie: Record = {}; - document.cookie.split(';').forEach(function(el) { - const split = el.split('='); - cookie[split[0].trim()] = split.slice(1).join('='); - }); - return cookie[name]; + if (typeof document !== 'undefined') { + const cookie: Record = {}; + 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) => { - document.cookie = `${name}=;Max-Age=0`; + if (typeof document !== 'undefined') { + document.cookie = `${name}=;Max-Age=0`; + } };