Create /admin/recipes page

This commit is contained in:
Sam Becker 2025-03-12 18:04:27 -05:00
parent 3d57de3997
commit 2c35587f5d
4 changed files with 85 additions and 4 deletions

View File

@ -1,16 +1,16 @@
import AdminTagTable from '@/admin/AdminTagTable';
import AdminRecipeTable from '@/admin/AdminRecipeTable';
import SiteGrid from '@/components/SiteGrid';
import { getUniqueTagsHiddenCached } from '@/photo/cache';
import { getUniqueRecipesCached } from '@/photo/cache';
export default async function AdminTagsPage() {
const tags = await getUniqueTagsHiddenCached().catch(() => []);
const recipes = await getUniqueRecipesCached().catch(() => []);
return (
<SiteGrid
contentMain={
<div className="space-y-6">
<div className="space-y-4">
<AdminTagTable {...{ tags }} />
<AdminRecipeTable {...{ recipes }} />
</div>
</div>}
/>

View File

@ -0,0 +1,35 @@
import { photoLabelForCount } from '@/photo';
import { clsx } from 'clsx/lite';
import Badge from '@/components/Badge';
import PhotoRecipe from '@/recipe/PhotoRecipe';
export default function AdminRecipeBadge({
recipe,
count,
hideBadge,
}: {
recipe: string,
count: number,
hideBadge?: boolean,
}) {
const renderBadgeContent = () =>
<div className={clsx(
'inline-flex items-center gap-2',
'translate-y-[1px]',
)}>
<PhotoRecipe {...{ recipe }} />
<div className="text-dim uppercase">
<span>{count}</span>
<span className="hidden xs:inline-block">
&nbsp;
{photoLabelForCount(count)}
</span>
</div>
</div>;
return (
hideBadge
? renderBadgeContent()
: <Badge className="py-[3px]!">{renderBadgeContent()}</Badge>
);
}

View File

@ -0,0 +1,43 @@
import FormWithConfirm from '@/components/FormWithConfirm';
import { deletePhotoTagGloballyAction } from '@/photo/actions';
import AdminTable from '@/admin/AdminTable';
import { Fragment } from 'react';
import DeleteFormButton from '@/admin/DeleteFormButton';
import { photoQuantityText } from '@/photo';
import EditButton from '@/admin/EditButton';
import { pathForAdminRecipeEdit } from '@/app/paths';
import { clsx } from 'clsx/lite';
import { formatRecipe, Recipes, sortRecipesWithCount } from '@/recipe';
import AdminRecipeBadge from './AdminRecipeBadge';
export default function AdminRecipeTable({
recipes,
}: {
recipes: Recipes
}) {
return (
<AdminTable>
{sortRecipesWithCount(recipes).map(({ recipe, count }) =>
<Fragment key={recipe}>
<div className="pr-2 col-span-2">
<AdminRecipeBadge {...{ recipe, count }} />
</div>
<div className={clsx(
'flex flex-nowrap',
'gap-2 sm:gap-3 items-center',
)}>
<EditButton path={pathForAdminRecipeEdit(recipe)} />
<FormWithConfirm
action={deletePhotoTagGloballyAction}
confirmText={
// eslint-disable-next-line max-len
`Are you sure you want to remove "${formatRecipe(recipe)}" from ${photoQuantityText(count, false).toLowerCase()}?`}
>
<input type="hidden" name="recipe" value={recipe} />
<DeleteFormButton clearLocalState />
</FormWithConfirm>
</div>
</Fragment>)}
</AdminTable>
);
}

View File

@ -100,6 +100,9 @@ export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) =>
export const pathForAdminTagEdit = (tag: string) =>
`${PATH_ADMIN_TAGS}/${tag}/${EDIT}`;
export const pathForAdminRecipeEdit = (recipe: string) =>
`${PATH_ADMIN_RECIPES}/${recipe}/${EDIT}`;
type PhotoOrPhotoId = Photo | string;
const getPhotoId = (photoOrPhotoId: PhotoOrPhotoId) =>