Add admin tags page with global delete function
This commit is contained in:
parent
35af0057c2
commit
74bc870b3d
26
src/admin/AdminGrid.tsx
Normal file
26
src/admin/AdminGrid.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import { cc } from '@/utility/css';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export default function AdminGrid ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title?: string,
|
||||
children: ReactNode,
|
||||
}) {
|
||||
return <div className="space-y-4">
|
||||
{title &&
|
||||
<div className="font-bold">
|
||||
{title}
|
||||
</div>}
|
||||
<div className="min-w-[14rem] overflow-x-scroll">
|
||||
<div className={cc(
|
||||
'w-full',
|
||||
'grid grid-cols-[auto_1fr_auto_auto] ',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { cc } from '@/utility/css';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
@ -16,25 +17,29 @@ export default function AdminNav({
|
||||
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) ? 'font-bold' : 'text-dim',
|
||||
)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span>({count})</span>
|
||||
</Link>)}
|
||||
</div>
|
||||
</div>
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<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) ? 'font-bold' : 'text-dim',
|
||||
)}
|
||||
>
|
||||
<span>{label}</span>
|
||||
<span>({count})</span>
|
||||
</Link>)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
10
src/admin/DeleteButton.tsx
Normal file
10
src/admin/DeleteButton.tsx
Normal file
@ -0,0 +1,10 @@
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
|
||||
export default function DeleteButton () {
|
||||
return <SubmitButtonWithStatus
|
||||
title="Delete"
|
||||
icon={<span className="inline-flex text-[18px]">×</span>}
|
||||
>
|
||||
Delete
|
||||
</SubmitButtonWithStatus>;
|
||||
}
|
||||
23
src/admin/EditButton.tsx
Normal file
23
src/admin/EditButton.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import Link from 'next/link';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
|
||||
export default function EditButton ({
|
||||
href,
|
||||
label = 'Edit',
|
||||
}: {
|
||||
href: string,
|
||||
label?: string,
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
title="Edit"
|
||||
href={href}
|
||||
className="button"
|
||||
>
|
||||
<FaRegEdit className="translate-y-[-0.5px]" />
|
||||
<span className="hidden sm:inline-block">
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
import { Fragment, ReactNode } from 'react';
|
||||
import { Fragment } from 'react';
|
||||
import PhotoUploadInput from '@/photo/PhotoUploadInput';
|
||||
import Link from 'next/link';
|
||||
import PhotoTiny from '@/photo/PhotoTiny';
|
||||
@ -11,7 +11,6 @@ import {
|
||||
deleteBlobPhotoAction,
|
||||
syncCacheAction,
|
||||
} from '@/photo/actions';
|
||||
import { FaRegEdit } from 'react-icons/fa';
|
||||
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
|
||||
import { pathForBlobUrl } from '@/services/blob';
|
||||
import {
|
||||
@ -33,12 +32,17 @@ import {
|
||||
PaginationParams,
|
||||
getPaginationForSearchParams,
|
||||
} from '@/site/pagination';
|
||||
import AdminGrid from '@/admin/AdminGrid';
|
||||
import DeleteButton from '@/admin/DeleteButton';
|
||||
import EditButton from '@/admin/EditButton';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
const DEBUG_PHOTO_BLOBS = false;
|
||||
|
||||
export default async function AdminPage({ searchParams }: PaginationParams) {
|
||||
export default async function AdminTagsPage({
|
||||
searchParams,
|
||||
}: PaginationParams) {
|
||||
const { offset, limit } = getPaginationForSearchParams(searchParams);
|
||||
|
||||
const [
|
||||
@ -85,7 +89,7 @@ export default async function AdminPage({ searchParams }: PaginationParams) {
|
||||
label={`Photos Files (${blobPhotoUrls.length})`}
|
||||
/>}
|
||||
<div className="space-y-4">
|
||||
<AdminGrid title={`Photos (${count})`}>
|
||||
<AdminGrid>
|
||||
{photos.map(photo =>
|
||||
<Fragment key={photo.id}>
|
||||
<PhotoTiny
|
||||
@ -149,59 +153,6 @@ export default async function AdminPage({ searchParams }: PaginationParams) {
|
||||
);
|
||||
}
|
||||
|
||||
function AdminGrid ({
|
||||
title,
|
||||
children,
|
||||
}: {
|
||||
title: string,
|
||||
children: ReactNode,
|
||||
}) {
|
||||
return <div className="space-y-4">
|
||||
<div className="font-bold">
|
||||
{title}
|
||||
</div>
|
||||
<div className="min-w-[14rem] overflow-x-scroll">
|
||||
<div className={cc(
|
||||
'w-full',
|
||||
'grid grid-cols-[auto_1fr_auto_auto] ',
|
||||
'gap-2 sm:gap-3 items-center',
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>;
|
||||
}
|
||||
|
||||
function EditButton ({
|
||||
href,
|
||||
label = 'Edit',
|
||||
}: {
|
||||
href: string,
|
||||
label?: string,
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
title="Edit"
|
||||
href={href}
|
||||
className="button"
|
||||
>
|
||||
<FaRegEdit className="translate-y-[-0.5px]" />
|
||||
<span className="hidden sm:inline-block">
|
||||
{label}
|
||||
</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function DeleteButton () {
|
||||
return <SubmitButtonWithStatus
|
||||
title="Delete"
|
||||
icon={<span className="inline-flex text-[18px]">×</span>}
|
||||
>
|
||||
Delete
|
||||
</SubmitButtonWithStatus>;
|
||||
}
|
||||
|
||||
function BlobUrls ({
|
||||
blobUrls,
|
||||
label,
|
||||
|
||||
42
src/app/(auth-state)/admin/tags/page.tsx
Normal file
42
src/app/(auth-state)/admin/tags/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import FormWithConfirm from '@/components/FormWithConfirm';
|
||||
import SiteGrid from '@/components/SiteGrid';
|
||||
import { deletePhotoTagGloballyAction } from '@/photo/actions';
|
||||
import { getUniqueTagsCached } from '@/cache';
|
||||
import AdminGrid from '@/admin/AdminGrid';
|
||||
import { Fragment } from 'react';
|
||||
import DeleteButton from '@/admin/DeleteButton';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
export default async function AdminPhotosPage() {
|
||||
const tags = await getUniqueTagsCached();
|
||||
|
||||
return (
|
||||
<SiteGrid
|
||||
contentMain={
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<AdminGrid>
|
||||
{tags.map(tag =>
|
||||
<Fragment key={tag}>
|
||||
<div className="flex-grow w-full">
|
||||
{tag}
|
||||
</div>
|
||||
<div />
|
||||
<div />
|
||||
<FormWithConfirm
|
||||
action={deletePhotoTagGloballyAction}
|
||||
confirmText={
|
||||
// eslint-disable-next-line max-len
|
||||
`Are you sure you want to remove "${tag}?" from all photos?`}
|
||||
>
|
||||
<input type="hidden" name="tag" value={tag} />
|
||||
<DeleteButton />
|
||||
</FormWithConfirm>
|
||||
</Fragment>)}
|
||||
</AdminGrid>
|
||||
</div>
|
||||
</div>}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user