Post banner for recent changes
This commit is contained in:
parent
89629f8494
commit
80661561ca
@ -1,6 +1,7 @@
|
|||||||
import { getStorageUploadUrlsNoStore } from '@/services/storage/cache';
|
import { getStorageUploadUrlsNoStore } from '@/services/storage/cache';
|
||||||
import {
|
import {
|
||||||
getPhotosCountIncludingHiddenCached,
|
getPhotosCountIncludingHiddenCached,
|
||||||
|
getPhotosMostRecentUpdateCached,
|
||||||
getUniqueTagsCached,
|
getUniqueTagsCached,
|
||||||
} from '@/photo/cache';
|
} from '@/photo/cache';
|
||||||
import {
|
import {
|
||||||
@ -15,6 +16,7 @@ export default async function AdminNav() {
|
|||||||
countPhotos,
|
countPhotos,
|
||||||
countUploads,
|
countUploads,
|
||||||
countTags,
|
countTags,
|
||||||
|
mostRecentUpdate,
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
getPhotosCountIncludingHiddenCached().catch(() => 0),
|
getPhotosCountIncludingHiddenCached().catch(() => 0),
|
||||||
getStorageUploadUrlsNoStore()
|
getStorageUploadUrlsNoStore()
|
||||||
@ -24,6 +26,7 @@ export default async function AdminNav() {
|
|||||||
return 0;
|
return 0;
|
||||||
}),
|
}),
|
||||||
getUniqueTagsCached().then(tags => tags.length).catch(() => 0),
|
getUniqueTagsCached().then(tags => tags.length).catch(() => 0),
|
||||||
|
getPhotosMostRecentUpdateCached(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const navItemPhotos = {
|
const navItemPhotos = {
|
||||||
@ -44,12 +47,12 @@ export default async function AdminNav() {
|
|||||||
count: countTags,
|
count: countTags,
|
||||||
};
|
};
|
||||||
|
|
||||||
const navItems = [navItemPhotos];
|
const items = [navItemPhotos];
|
||||||
|
|
||||||
if (countUploads > 0) { navItems.push(navItemUploads); }
|
if (countUploads > 0) { items.push(navItemUploads); }
|
||||||
if (countTags > 0) { navItems.push(navItemTags); }
|
if (countTags > 0) { items.push(navItemTags); }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminNavClient items={navItems} />
|
<AdminNavClient {...{ items, mostRecentUpdate }} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,65 +1,91 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import InfoBlock from '@/components/InfoBlock';
|
||||||
import SiteGrid from '@/components/SiteGrid';
|
import SiteGrid from '@/components/SiteGrid';
|
||||||
import {
|
import {
|
||||||
PATH_ADMIN_CONFIGURATION,
|
PATH_ADMIN_CONFIGURATION,
|
||||||
checkPathPrefix,
|
checkPathPrefix,
|
||||||
isPathAdminConfiguration,
|
isPathAdminConfiguration,
|
||||||
} from '@/site/paths';
|
} from '@/site/paths';
|
||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
|
import { differenceInMinutes } from 'date-fns';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import { BiCog } from 'react-icons/bi';
|
import { BiCog } from 'react-icons/bi';
|
||||||
|
import { FaRegClock } from 'react-icons/fa';
|
||||||
|
|
||||||
|
const RECENCY_THRESHOLD = 5;
|
||||||
|
|
||||||
export default function AdminNavClient({
|
export default function AdminNavClient({
|
||||||
items,
|
items,
|
||||||
|
mostRecentUpdate,
|
||||||
}: {
|
}: {
|
||||||
items: {
|
items: {
|
||||||
label: string,
|
label: string,
|
||||||
href: string,
|
href: string,
|
||||||
count: number,
|
count: number,
|
||||||
}[]
|
}[]
|
||||||
|
mostRecentUpdate?: Date
|
||||||
}) {
|
}) {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
|
const { adminUpdates = [] } = useAppState();
|
||||||
|
|
||||||
|
const shouldShowBanner = useMemo(() =>
|
||||||
|
((mostRecentUpdate ? [mostRecentUpdate] : []).concat(adminUpdates))
|
||||||
|
.some(date => differenceInMinutes(new Date(), date) < RECENCY_THRESHOLD)
|
||||||
|
, [mostRecentUpdate, adminUpdates]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SiteGrid
|
<SiteGrid
|
||||||
contentMain={
|
contentMain={
|
||||||
<div className={clsx(
|
<div className="space-y-5">
|
||||||
'flex gap-2 md:gap-4',
|
|
||||||
'border-b border-gray-200 dark:border-gray-800 pb-3',
|
|
||||||
)}>
|
|
||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex gap-2 md:gap-4',
|
'flex gap-2 md:gap-4',
|
||||||
'flex-grow overflow-x-auto',
|
'border-b border-gray-200 dark:border-gray-800 pb-3',
|
||||||
)}>
|
)}>
|
||||||
{items.map(({ label, href, count }) =>
|
<div className={clsx(
|
||||||
<Link
|
'flex gap-2 md:gap-4',
|
||||||
key={label}
|
'flex-grow overflow-x-auto',
|
||||||
href={href}
|
)}>
|
||||||
className={clsx(
|
{items.map(({ label, href, count }) =>
|
||||||
'flex gap-0.5',
|
<Link
|
||||||
checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim',
|
key={label}
|
||||||
)}
|
href={href}
|
||||||
prefetch={false}
|
className={clsx(
|
||||||
>
|
'flex gap-0.5',
|
||||||
<span>{label}</span>
|
checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim',
|
||||||
{count > 0 &&
|
)}
|
||||||
<span>({count})</span>}
|
prefetch={false}
|
||||||
</Link>)}
|
>
|
||||||
|
<span>{label}</span>
|
||||||
|
{count > 0 &&
|
||||||
|
<span>({count})</span>}
|
||||||
|
</Link>)}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
href={PATH_ADMIN_CONFIGURATION}
|
||||||
|
className={isPathAdminConfiguration(pathname)
|
||||||
|
? 'font-bold'
|
||||||
|
: 'text-dim'}
|
||||||
|
>
|
||||||
|
<BiCog
|
||||||
|
size={18}
|
||||||
|
className="inline-block"
|
||||||
|
aria-label="App Configuration"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
{shouldShowBanner &&
|
||||||
href={PATH_ADMIN_CONFIGURATION}
|
<InfoBlock centered={false} padding="tight" color="blue">
|
||||||
className={isPathAdminConfiguration(pathname)
|
<div className="flex items-center gap-3">
|
||||||
? 'font-bold'
|
<FaRegClock className="flex-shrink-0" />
|
||||||
: 'text-dim'}
|
Updates detected—they may take several minutes to show up
|
||||||
>
|
for visitors
|
||||||
<BiCog
|
</div>
|
||||||
size={18}
|
</InfoBlock>}
|
||||||
className="inline-block"
|
|
||||||
aria-label="App Configuration"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -20,7 +20,7 @@ export default function AdminPhotoMenuClient({
|
|||||||
photo: Photo
|
photo: Photo
|
||||||
revalidatePhoto?: RevalidatePhoto
|
revalidatePhoto?: RevalidatePhoto
|
||||||
}) {
|
}) {
|
||||||
const { isUserSignedIn } = useAppState();
|
const { isUserSignedIn, addAdminUpdate } = useAppState();
|
||||||
|
|
||||||
const isFav = isPhotoFav(photo);
|
const isFav = isPhotoFav(photo);
|
||||||
const path = usePathname();
|
const path = usePathname();
|
||||||
@ -62,7 +62,10 @@ export default function AdminPhotoMenuClient({
|
|||||||
photo.id,
|
photo.id,
|
||||||
photo.url,
|
photo.url,
|
||||||
shouldRedirectDelete,
|
shouldRedirectDelete,
|
||||||
).then(() => revalidatePhoto?.(photo.id, true));
|
).then(() => {
|
||||||
|
revalidatePhoto?.(photo.id, true);
|
||||||
|
addAdminUpdate?.();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -94,7 +94,7 @@ export default function AdminPhotoTable({
|
|||||||
>
|
>
|
||||||
<input type="hidden" name="id" value={photo.id} />
|
<input type="hidden" name="id" value={photo.id} />
|
||||||
<input type="hidden" name="url" value={photo.url} />
|
<input type="hidden" name="url" value={photo.url} />
|
||||||
<DeleteButton onFormSubmit={invalidateSwr} />
|
<DeleteButton clearLocalState />
|
||||||
</FormWithConfirm>
|
</FormWithConfirm>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>)}
|
</Fragment>)}
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||||
import { deletePhotoTagGloballyAction } from '@/photo/actions';
|
import { deletePhotoTagGloballyAction } from '@/photo/actions';
|
||||||
import AdminTable from '@/admin/AdminTable';
|
import AdminTable from '@/admin/AdminTable';
|
||||||
@ -11,15 +9,12 @@ import EditButton from '@/admin/EditButton';
|
|||||||
import { pathForAdminTagEdit } from '@/site/paths';
|
import { pathForAdminTagEdit } from '@/site/paths';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import AdminTagBadge from './AdminTagBadge';
|
import AdminTagBadge from './AdminTagBadge';
|
||||||
import { useAppState } from '@/state/AppState';
|
|
||||||
|
|
||||||
export default function AdminTagTable({
|
export default function AdminTagTable({
|
||||||
tags,
|
tags,
|
||||||
}: {
|
}: {
|
||||||
tags: TagsWithMeta
|
tags: TagsWithMeta
|
||||||
}) {
|
}) {
|
||||||
const { invalidateSwr } = useAppState();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AdminTable>
|
<AdminTable>
|
||||||
{sortTagsObject(tags).map(({ tag, count }) =>
|
{sortTagsObject(tags).map(({ tag, count }) =>
|
||||||
@ -39,7 +34,7 @@ export default function AdminTagTable({
|
|||||||
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
`Are you sure you want to remove "${formatTag(tag)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
|
||||||
>
|
>
|
||||||
<input type="hidden" name="tag" value={tag} />
|
<input type="hidden" name="tag" value={tag} />
|
||||||
<DeleteButton onFormSubmit={invalidateSwr} />
|
<DeleteButton clearLocalState />
|
||||||
</FormWithConfirm>
|
</FormWithConfirm>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>)}
|
</Fragment>)}
|
||||||
|
|||||||
@ -1,13 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||||
|
import { useAppState } from '@/state/AppState';
|
||||||
import { clsx } from 'clsx/lite';
|
import { clsx } from 'clsx/lite';
|
||||||
import { ComponentProps } from 'react';
|
import { ComponentProps, useCallback } from 'react';
|
||||||
import { BiTrash } from 'react-icons/bi';
|
import { BiTrash } from 'react-icons/bi';
|
||||||
|
|
||||||
export default function DeleteButton (
|
export default function DeleteButton (
|
||||||
props: ComponentProps<typeof SubmitButtonWithStatus>
|
props: ComponentProps<typeof SubmitButtonWithStatus> & {
|
||||||
|
clearLocalState?: boolean
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
|
const {
|
||||||
|
onFormSubmit: onFormSubmitProps,
|
||||||
|
clearLocalState,
|
||||||
|
...rest
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
const { invalidateSwr, addAdminUpdate } = useAppState();
|
||||||
|
|
||||||
|
const onFormSubmit = useCallback(() => {
|
||||||
|
onFormSubmitProps?.();
|
||||||
|
if (clearLocalState) {
|
||||||
|
invalidateSwr?.();
|
||||||
|
addAdminUpdate?.();
|
||||||
|
}
|
||||||
|
}, [onFormSubmitProps, clearLocalState, invalidateSwr, addAdminUpdate]);
|
||||||
|
|
||||||
return <SubmitButtonWithStatus
|
return <SubmitButtonWithStatus
|
||||||
{...props}
|
{...rest}
|
||||||
title="Delete"
|
title="Delete"
|
||||||
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
|
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
|
||||||
spinnerColor="text"
|
spinnerColor="text"
|
||||||
@ -17,5 +38,6 @@ export default function DeleteButton (
|
|||||||
'!border-red-200 hover:!border-red-300',
|
'!border-red-200 hover:!border-red-300',
|
||||||
'dark:!border-red-900/75 dark:hover:!border-red-900',
|
'dark:!border-red-900/75 dark:hover:!border-red-900',
|
||||||
)}
|
)}
|
||||||
|
onFormSubmit={onFormSubmit}
|
||||||
/>;
|
/>;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,14 +4,32 @@ import { ReactNode } from 'react';
|
|||||||
export default function InfoBlock({
|
export default function InfoBlock({
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
|
color = 'gray',
|
||||||
padding = 'normal',
|
padding = 'normal',
|
||||||
centered = true,
|
centered = true,
|
||||||
}: {
|
}: {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
className?: string
|
className?: string
|
||||||
|
color?: 'gray' | 'blue'
|
||||||
padding?: 'loose' | 'normal' | 'tight';
|
padding?: 'loose' | 'normal' | 'tight';
|
||||||
centered?: boolean;
|
centered?: boolean;
|
||||||
} ) {
|
} ) {
|
||||||
|
const getColorClasses = () => {
|
||||||
|
switch (color) {
|
||||||
|
case 'gray': return [
|
||||||
|
'text-medium',
|
||||||
|
'bg-gray-50 border-gray-200',
|
||||||
|
'dark:bg-gray-900/40 dark:border-gray-800',
|
||||||
|
];
|
||||||
|
case 'blue': return [
|
||||||
|
'text-gray-700/70',
|
||||||
|
'dark:text-gray-300/75',
|
||||||
|
'bg-blue-50 border-blue-200',
|
||||||
|
'dark:bg-blue-900/25 dark:border-blue-800/35',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const getPaddingClasses = () => {
|
const getPaddingClasses = () => {
|
||||||
switch (padding) {
|
switch (padding) {
|
||||||
case 'loose': return 'p-4 md:p-24';
|
case 'loose': return 'p-4 md:p-24';
|
||||||
@ -24,8 +42,7 @@ export default function InfoBlock({
|
|||||||
<div className={clsx(
|
<div className={clsx(
|
||||||
'flex flex-col items-center justify-center',
|
'flex flex-col items-center justify-center',
|
||||||
'rounded-lg border',
|
'rounded-lg border',
|
||||||
'bg-gray-50 border-gray-200',
|
...getColorClasses(),
|
||||||
'dark:bg-gray-900/40 dark:border-gray-800',
|
|
||||||
getPaddingClasses(),
|
getPaddingClasses(),
|
||||||
className,
|
className,
|
||||||
)}>
|
)}>
|
||||||
@ -33,7 +50,6 @@ export default function InfoBlock({
|
|||||||
'flex flex-col justify-center w-full',
|
'flex flex-col justify-center w-full',
|
||||||
centered && 'items-center',
|
centered && 'items-center',
|
||||||
'space-y-4',
|
'space-y-4',
|
||||||
'text-medium',
|
|
||||||
)}>
|
)}>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -19,6 +19,7 @@ import {
|
|||||||
getPhotosFilmSimulationMeta,
|
getPhotosFilmSimulationMeta,
|
||||||
getPhotosDateRange,
|
getPhotosDateRange,
|
||||||
getPhotosNearId,
|
getPhotosNearId,
|
||||||
|
getPhotosMostRecentUpdate,
|
||||||
} from '@/services/vercel-postgres';
|
} from '@/services/vercel-postgres';
|
||||||
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
|
import { parseCachedPhotoDates, parseCachedPhotosDates } from '@/photo';
|
||||||
import { createCameraKey } from '@/camera';
|
import { createCameraKey } from '@/camera';
|
||||||
@ -166,6 +167,12 @@ export const getPhotosCountIncludingHiddenCached =
|
|||||||
[KEY_PHOTOS, KEY_COUNT, KEY_HIDDEN],
|
[KEY_PHOTOS, KEY_COUNT, KEY_HIDDEN],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const getPhotosMostRecentUpdateCached =
|
||||||
|
unstable_cache(
|
||||||
|
() => getPhotosMostRecentUpdate(),
|
||||||
|
[KEY_PHOTOS, KEY_COUNT, KEY_DATE_RANGE],
|
||||||
|
);
|
||||||
|
|
||||||
export const getPhotosTagMetaCached =
|
export const getPhotosTagMetaCached =
|
||||||
unstable_cache(
|
unstable_cache(
|
||||||
getPhotosTagMeta,
|
getPhotosTagMeta,
|
||||||
|
|||||||
@ -187,6 +187,10 @@ const sqlGetPhotosCountIncludingHidden = async () => sql`
|
|||||||
SELECT COUNT(*) FROM photos
|
SELECT COUNT(*) FROM photos
|
||||||
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
`.then(({ rows }) => parseInt(rows[0].count, 10));
|
||||||
|
|
||||||
|
const sqlGetPhotosMostRecentUpdate = async () => sql`
|
||||||
|
SELECT updated_at FROM photos ORDER BY updated_at DESC LIMIT 1
|
||||||
|
`.then(({ rows }) => rows[0] ? rows[0].updated_at as Date : undefined);
|
||||||
|
|
||||||
const sqlGetPhotosDateRange = async () => sql`
|
const sqlGetPhotosDateRange = async () => sql`
|
||||||
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
|
SELECT MIN(taken_at_naive) as start, MAX(taken_at_naive) as end
|
||||||
FROM photos
|
FROM photos
|
||||||
@ -481,6 +485,11 @@ export const getPhotosCountIncludingHidden = () =>
|
|||||||
sqlGetPhotosCountIncludingHidden,
|
sqlGetPhotosCountIncludingHidden,
|
||||||
'getPhotosCountIncludingHidden',
|
'getPhotosCountIncludingHidden',
|
||||||
);
|
);
|
||||||
|
export const getPhotosMostRecentUpdate = () =>
|
||||||
|
safelyQueryPhotos(
|
||||||
|
sqlGetPhotosMostRecentUpdate,
|
||||||
|
'getPhotosMostRecentUpdate',
|
||||||
|
);
|
||||||
|
|
||||||
// TAGS
|
// TAGS
|
||||||
export const getUniqueTags = () =>
|
export const getUniqueTags = () =>
|
||||||
|
|||||||
@ -16,6 +16,8 @@ export interface AppStateContext {
|
|||||||
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
setShouldRespondToKeyboardCommands?: Dispatch<SetStateAction<boolean>>
|
||||||
isCommandKOpen?: boolean
|
isCommandKOpen?: boolean
|
||||||
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
setIsCommandKOpen?: Dispatch<SetStateAction<boolean>>
|
||||||
|
adminUpdates?: Date[]
|
||||||
|
addAdminUpdate?: () => void
|
||||||
shouldShowBaselineGrid?: boolean
|
shouldShowBaselineGrid?: boolean
|
||||||
setShouldShowBaselineGrid?: Dispatch<SetStateAction<boolean>>
|
setShouldShowBaselineGrid?: Dispatch<SetStateAction<boolean>>
|
||||||
clearNextPhotoAnimation?: () => void
|
clearNextPhotoAnimation?: () => void
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { AppStateContext } from './AppState';
|
|||||||
import { AnimationConfig } from '@/components/AnimateItems';
|
import { AnimationConfig } from '@/components/AnimateItems';
|
||||||
import usePathnames from '@/utility/usePathnames';
|
import usePathnames from '@/utility/usePathnames';
|
||||||
import { getCurrentUser } from '@/auth/actions';
|
import { getCurrentUser } from '@/auth/actions';
|
||||||
|
import useSWR from 'swr';
|
||||||
|
|
||||||
export default function AppStateProvider({
|
export default function AppStateProvider({
|
||||||
children,
|
children,
|
||||||
@ -25,19 +26,22 @@ export default function AppStateProvider({
|
|||||||
useState(true);
|
useState(true);
|
||||||
const [isCommandKOpen, setIsCommandKOpen] =
|
const [isCommandKOpen, setIsCommandKOpen] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [adminUpdates, setAdminUpdates] = useState<Date[]>([]);
|
||||||
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
const [shouldShowBaselineGrid, setShouldShowBaselineGrid] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
|
||||||
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
|
const invalidateSwr = useCallback(() => setSwrTimestamp(Date.now()), []);
|
||||||
|
|
||||||
const captureUser = useCallback(() =>
|
const { data } = useSWR('getCurrentUser', getCurrentUser);
|
||||||
getCurrentUser().then(user => setUserEmail?.(user?.email ?? undefined))
|
useEffect(() => setUserEmail(data?.email ?? undefined), [data]);
|
||||||
|
|
||||||
|
const addAdminUpdate = useCallback(() =>
|
||||||
|
setAdminUpdates(updates => [...updates, new Date()])
|
||||||
, []);
|
, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasLoaded?.(true);
|
setHasLoaded?.(true);
|
||||||
captureUser().catch(() => setTimeout(captureUser, 2000));
|
}, []);
|
||||||
}, [captureUser]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppStateContext.Provider
|
<AppStateContext.Provider
|
||||||
@ -56,6 +60,8 @@ export default function AppStateProvider({
|
|||||||
setShouldRespondToKeyboardCommands,
|
setShouldRespondToKeyboardCommands,
|
||||||
isCommandKOpen,
|
isCommandKOpen,
|
||||||
setIsCommandKOpen,
|
setIsCommandKOpen,
|
||||||
|
adminUpdates,
|
||||||
|
addAdminUpdate,
|
||||||
shouldShowBaselineGrid,
|
shouldShowBaselineGrid,
|
||||||
setShouldShowBaselineGrid,
|
setShouldShowBaselineGrid,
|
||||||
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
clearNextPhotoAnimation: () => setNextPhotoAnimation?.(undefined),
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user