Add admin sub-nav

This commit is contained in:
Sam Becker 2023-10-05 22:01:23 -05:00
parent d30c8a14de
commit c9599120d2
12 changed files with 188 additions and 98 deletions

40
src/admin/AdminNav.tsx Normal file
View File

@ -0,0 +1,40 @@
'use client';
import { cc } from '@/utility/css';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
export default function AdminNav({
items,
}: {
items: {
label: string,
href: string,
count: number,
}[]
}) {
const pathname = usePathname();
return (
<div className={cc(
'border-b border-gray-900 pb-2',
)}>
<div className={cc(
'flex gap-2 md:gap-4',
)}>
{items.map(({ label, href, count }) =>
<Link
key={label}
href={href}
className={cc(
'flex gap-0.5',
!pathname.startsWith(href) && 'text-dim',
)}
>
<span>{label}</span>
<span>({count})</span>
</Link>)}
</div>
</div>
);
}

View File

@ -0,0 +1,43 @@
import AdminNav from '@/admin/AdminNav';
import {
getPhotosCountIncludingHiddenCached,
getUniqueTagsCached,
} from '@/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
export default async function AdminLayout({
children,
}: {
children: React.ReactNode
}) {
const [
photosCount,
tagsCount,
] = await Promise.all([
getPhotosCountIncludingHiddenCached(),
getUniqueTagsCached().then(tags => tags.length),
]);
const navItemPhotos = {
label: 'Photos',
href: PATH_ADMIN_PHOTOS,
count: photosCount,
};
const navItemTags = {
label: 'Tags',
href: PATH_ADMIN_TAGS,
count: tagsCount,
};
const navItems = tagsCount > 0
? [navItemPhotos, navItemTags]
: [navItemPhotos];
return (
<div className="mt-4 space-y-6">
<AdminNav items={navItems} />
{children}
</div>
);
}

View File

@ -58,93 +58,91 @@ export default async function AdminPage({ searchParams }: PaginationParams) {
return (
<SiteGrid
contentMain={
<div className="mt-4 space-y-4">
<div className="space-y-8">
<div className="flex items-center">
<div className="flex-grow">
<PhotoUploadInput />
</div>
<form
className="hidden md:block"
action={syncCacheAction}
<div className="space-y-6">
<div className="flex items-center">
<div className="flex-grow">
<PhotoUploadInput />
</div>
<form
className="hidden md:block"
action={syncCacheAction}
>
<SubmitButtonWithStatus
icon={<BiTrash />}
>
<SubmitButtonWithStatus
icon={<BiTrash />}
>
Clear Cache
</SubmitButtonWithStatus>
</form>
</div>
{blobUploadUrls.length > 0 &&
<BlobUrls
blobUrls={blobUploadUrls}
label={`Uploads Files (${blobUploadUrls.length})`}
/>}
{blobPhotoUrls.length > 0 &&
<BlobUrls
blobUrls={blobPhotoUrls}
label={`Photos Files (${blobPhotoUrls.length})`}
/>}
<div className="space-y-4">
<AdminGrid title={`Photos (${count})`}>
{photos.map(photo =>
<Fragment key={photo.id}>
<PhotoTiny
className={cc(
'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800',
)}
photo={photo}
/>
<div className="flex flex-col md:flex-row">
<Link
key={photo.id}
href={pathForPhoto(photo)}
className="sm:w-[50%] flex items-center gap-2"
>
<span className={cc(
'inline-flex items-center gap-2',
photo.hidden && 'text-gray-400 dark:text-gray-500',
)}>
<span>{photo.title || 'Untitled'}</span>
{photo.hidden &&
<AiOutlineEyeInvisible
className="translate-y-[0.25px]"
size={16}
/>}
</span>
{photo.priorityOrder !== null &&
<span className={cc(
'text-xs leading-none px-1.5 py-1 rounded-sm',
'dark:text-gray-300',
'bg-gray-100 dark:bg-gray-800',
)}>
{photo.priorityOrder}
</span>}
</Link>
<div className={cc(
'sm:w-[50%] uppercase',
'text-gray-400 dark:text-gray-500',
)}>
{photo.takenAtNaive}
</div>
</div>
<EditButton href={pathForPhotoEdit(photo)} />
<FormWithConfirm
action={deletePhotoAction}
confirmText={
// eslint-disable-next-line max-len
`Are you sure you want to delete "${titleForPhoto(photo)}?"`}
Clear Cache
</SubmitButtonWithStatus>
</form>
</div>
{blobUploadUrls.length > 0 &&
<BlobUrls
blobUrls={blobUploadUrls}
label={`Uploads Files (${blobUploadUrls.length})`}
/>}
{blobPhotoUrls.length > 0 &&
<BlobUrls
blobUrls={blobPhotoUrls}
label={`Photos Files (${blobPhotoUrls.length})`}
/>}
<div className="space-y-4">
<AdminGrid title={`Photos (${count})`}>
{photos.map(photo =>
<Fragment key={photo.id}>
<PhotoTiny
className={cc(
'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800',
)}
photo={photo}
/>
<div className="flex flex-col md:flex-row">
<Link
key={photo.id}
href={pathForPhoto(photo)}
className="sm:w-[50%] flex items-center gap-2"
>
<input type="hidden" name="id" value={photo.id} />
<input type="hidden" name="url" value={photo.url} />
<DeleteButton />
</FormWithConfirm>
</Fragment>)}
</AdminGrid>
{showMorePhotos &&
<MorePhotos path={pathForAdminPhotos(offset + 1)} />}
</div>
<span className={cc(
'inline-flex items-center gap-2',
photo.hidden && 'text-dim',
)}>
<span>{photo.title || 'Untitled'}</span>
{photo.hidden &&
<AiOutlineEyeInvisible
className="translate-y-[0.25px]"
size={16}
/>}
</span>
{photo.priorityOrder !== null &&
<span className={cc(
'text-xs leading-none px-1.5 py-1 rounded-sm',
'dark:text-gray-300',
'bg-gray-100 dark:bg-gray-800',
)}>
{photo.priorityOrder}
</span>}
</Link>
<div className={cc(
'sm:w-[50%] uppercase',
'text-dim',
)}>
{photo.takenAtNaive}
</div>
</div>
<EditButton href={pathForPhotoEdit(photo)} />
<FormWithConfirm
action={deletePhotoAction}
confirmText={
// eslint-disable-next-line max-len
`Are you sure you want to delete "${titleForPhoto(photo)}?"`}
>
<input type="hidden" name="id" value={photo.id} />
<input type="hidden" name="url" value={photo.url} />
<DeleteButton />
</FormWithConfirm>
</Fragment>)}
</AdminGrid>
{showMorePhotos &&
<MorePhotos path={pathForAdminPhotos(offset + 1)} />}
</div>
</div>}
/>

View File

@ -25,7 +25,7 @@ export default function FooterAuth() {
contentMain={<div className={cc(
'flex items-center',
'my-8',
'text-gray-400 dark:text-gray-500',
'text-dim',
)}>
<div className="flex gap-x-4 gap-y-1 flex-wrap flex-grow">
{hasState

View File

@ -18,7 +18,7 @@ export default function FooterStatic({
contentMain={<div className={cc(
'my-8',
'flex items-center',
'text-gray-400 dark:text-gray-500',
'text-dim',
)}>
<div className="flex gap-x-4 gap-y-1 flex-grow flex-wrap">
<Link

View File

@ -27,7 +27,7 @@ export default function HeaderList({
{title}
</div>,
].concat(items)}
classNameItem="text-gray-400 dark:text-gray-500"
classNameItem="text-dim"
/>
);
}

View File

@ -16,7 +16,7 @@ export default function ShareButton({
<IconPathButton {...{
path,
icon: <TbPhotoShare size={17} className={dim
? 'text-gray-400 dark:text-gray-500'
? 'text-dim'
: undefined} />,
prefetch,
shouldScroll,

View File

@ -27,7 +27,7 @@ export default function StatusIcon({
case 'optional':
return <BiSolidCheckboxMinus
size={18}
className="text-gray-400 dark:text-gray-500"
className="text-dim"
/>;
}
};

View File

@ -35,7 +35,7 @@ export default function PhotoHeader({
{entity}
<span className={cc(
'inline-flex gap-2 items-center self-start',
'uppercase text-gray-400 dark:text-gray-500',
'uppercase text-dim',
'sm:col-span-2 md:col-span-1 lg:col-span-2',
)}>
{selectedPhotoIndex !== undefined
@ -48,7 +48,7 @@ export default function PhotoHeader({
<span className={cc(
'hidden sm:inline-block',
'text-right uppercase',
'text-gray-400 dark:text-gray-500',
'text-dim',
)}>
{start === end
? start

View File

@ -1,4 +1,4 @@
import { PATH_ADMIN_UPLOAD_BLOB_HANDLER } from '@/site/paths';
import { PATH_ADMIN_UPLOAD_BLOB } from '@/site/paths';
import { del, list, put } from '@vercel/blob';
import { upload } from '@vercel/blob/client';
@ -41,7 +41,7 @@ export const uploadPhotoFromClient = async (
file,
{
access: 'public',
handleUploadUrl: PATH_ADMIN_UPLOAD_BLOB_HANDLER,
handleUploadUrl: PATH_ADMIN_UPLOAD_BLOB,
},
);

View File

@ -3,12 +3,14 @@
@tailwind utilities;
@layer base {
/* Core */
body {
@apply
font-mono text-sm md:text-base
bg-white dark:bg-black
text-gray-900 dark:text-gray-100
}
/* Forms */
label {
@apply
font-sans font-medium block uppercase text-xs
@ -70,9 +72,15 @@
disabled:bg-transparent dark:disabled:bg-transparent
disabled:border-gray-100 dark:disabled:border-gray-900
}
/* Toasts */
.toaster [data-sonner-toast] {
@apply
font-mono
!border-gray-200 dark:!border-gray-800
}
/* Common Utilities */
.text-dim {
@apply
text-gray-400 dark:text-gray-500
}
}

View File

@ -19,10 +19,11 @@ export const PATH_SIGN_IN = '/sign-in';
export const PATH_OG = '/og';
export const PATH_CHECKLIST = '/checklist';
// Extended paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_UPLOAD_BLOB_HANDLER = `${PATH_ADMIN_UPLOAD}/blob`;
// Admin paths
export const PATH_ADMIN_PHOTOS = `${PATH_ADMIN}/photos`;
export const PATH_ADMIN_TAGS = `${PATH_ADMIN}/tags`;
export const PATH_ADMIN_UPLOAD = `${PATH_ADMIN}/uploads`;
export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`;
// Modifiers
const SHARE = 'share';