diff --git a/src/admin/AdminNav.tsx b/src/admin/AdminNav.tsx index 250f1897..f4c5f9e0 100644 --- a/src/admin/AdminNav.tsx +++ b/src/admin/AdminNav.tsx @@ -20,7 +20,7 @@ export default function AdminNav({
+
- + t.tag === tag); + + if (!tagData) { redirect(PATH_ADMIN); } + + return ( + + + + ); +}; diff --git a/src/app/(auth-state)/admin/tags/page.tsx b/src/app/(auth-state)/admin/tags/page.tsx index 36810ec7..34603124 100644 --- a/src/app/(auth-state)/admin/tags/page.tsx +++ b/src/app/(auth-state)/admin/tags/page.tsx @@ -8,6 +8,8 @@ import { photoQuantityText } from '@/photo'; import { getUniqueTagsWithCountCached } from '@/cache'; import PhotoTag from '@/tag/PhotoTag'; import { formatTag } from '@/tag'; +import EditButton from '@/admin/EditButton'; +import { pathForAdminTagEdit } from '@/site/paths'; export const runtime = 'edge'; @@ -28,7 +30,7 @@ export default async function AdminPhotosPage() {
{photoQuantityText(count, false)}
-
+ setIsSigningIn(false)); diff --git a/src/components/AdminChildPage.tsx b/src/components/AdminChildPage.tsx index 159618ad..a5dc11a7 100644 --- a/src/components/AdminChildPage.tsx +++ b/src/components/AdminChildPage.tsx @@ -3,16 +3,24 @@ import Link from 'next/link'; import { FiArrowLeft } from 'react-icons/fi'; function AdminChildPage({ + backPath, + backLabel, children, }: { + backPath?: string + backLabel?: string children: ReactNode, }) { return ( -
- - - Admin - +
+ {backPath && + + + {backLabel || 'Back'} + }
{children}
diff --git a/src/middleware.ts b/src/middleware.ts index 2cd56a2c..cd6aa351 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,13 +1,18 @@ import { auth } from './auth'; import { NextRequest, NextResponse } from 'next/server'; import type { NextApiRequest, NextApiResponse } from 'next'; -import { PREFIX_PHOTO, PREFIX_TAG } from './site/paths'; +import { + PATH_ADMIN, + PATH_ADMIN_PHOTOS, + PREFIX_PHOTO, + PREFIX_TAG, +} from './site/paths'; export default function middleware(req: NextRequest, res:NextResponse) { const pathname = req.nextUrl.pathname; - if (pathname === '/admin') { - return NextResponse.redirect(new URL('/admin/photos', req.url)); + if (pathname === PATH_ADMIN) { + return NextResponse.redirect(new URL(PATH_ADMIN_PHOTOS, req.url)); } else if (/^\/photos\/(.)+$/.test(pathname)) { // Accept /photos/* paths, but serve /p/* const matches = pathname.match(/^\/photos\/(.+)$/); diff --git a/src/photo/PhotoForm.tsx b/src/photo/PhotoForm.tsx index 8014b9e5..8e91c28f 100644 --- a/src/photo/PhotoForm.tsx +++ b/src/photo/PhotoForm.tsx @@ -12,6 +12,7 @@ import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import Link from 'next/link'; import { cc } from '@/utility/css'; import CanvasBlurCapture from '@/components/CanvasBlurCapture'; +import { PATH_ADMIN_PHOTOS } from '@/site/paths'; const THUMBNAIL_WIDTH = 300; const THUMBNAIL_HEIGHT = 200; @@ -108,11 +109,11 @@ export default function PhotoForm({ readOnly hidden />} -
+
{type === 'edit' && Cancel } diff --git a/src/photo/actions.ts b/src/photo/actions.ts index 930c55ad..ed6e071f 100644 --- a/src/photo/actions.ts +++ b/src/photo/actions.ts @@ -20,6 +20,7 @@ import { } from '@/cache'; import { IS_PRO_MODE } from '@/site/config'; import { getNextImageUrlForRequest } from '@/utility/image'; +import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths'; export async function createPhotoAction(formData: FormData) { const requestOrigin = formData.get('requestOrigin') as string | undefined; @@ -42,7 +43,7 @@ export async function createPhotoAction(formData: FormData) { revalidateAllKeys(); - redirect('/admin/photos'); + redirect(PATH_ADMIN_PHOTOS); } export async function updatePhotoAction(formData: FormData) { @@ -52,7 +53,7 @@ export async function updatePhotoAction(formData: FormData) { revalidatePhotosKey(); - redirect('/admin/photos'); + redirect(PATH_ADMIN_PHOTOS); } export async function deletePhotoAction(formData: FormData) { @@ -74,12 +75,12 @@ export async function deletePhotoTagGloballyAction(formData: FormData) { export async function renamePhotoTagGloballyAction(formData: FormData) { const tag = formData.get('tag') as string; - const newTag = formData.get('newTag') as string; - - if (tag && newTag && tag !== newTag) { - await sqlRenamePhotoTagGlobally(tag, newTag); + const updatedTag = formData.get('updatedTag') as string; + if (tag && updatedTag && tag !== updatedTag) { + await sqlRenamePhotoTagGlobally(tag, updatedTag); revalidatePhotosKey(); + redirect(PATH_ADMIN_TAGS); } } diff --git a/src/services/postgres.ts b/src/services/postgres.ts index 2a30424d..7df26527 100644 --- a/src/services/postgres.ts +++ b/src/services/postgres.ts @@ -139,10 +139,10 @@ export const sqlDeletePhotoTagGlobally = (tag: string) => WHERE ${tag}=ANY(tags) `; -export const sqlRenamePhotoTagGlobally = (tag: string, newTag: string) => +export const sqlRenamePhotoTagGlobally = (tag: string, updatedTag: string) => sql` UPDATE photos - SET tags=ARRAY_REPLACE(tags, ${tag}, ${newTag}) + SET tags=ARRAY_REPLACE(tags, ${tag}, ${updatedTag}) WHERE ${tag}=ANY(tags) `; diff --git a/src/site/paths.ts b/src/site/paths.ts index 49df5470..d1868093 100644 --- a/src/site/paths.ts +++ b/src/site/paths.ts @@ -28,6 +28,7 @@ export const PATH_ADMIN_UPLOAD_BLOB = `${PATH_ADMIN_UPLOAD}/blob`; // Modifiers const SHARE = 'share'; const NEXT = 'next'; +const EDIT = 'edit'; // Absolute paths export const ABSOLUTE_PATH_FOR_HOME_IMAGE = `${BASE_URL}/home-image`; @@ -44,6 +45,12 @@ export const pathForGrid = (next?: number) => export const pathForAdminPhotos = (next?: number) => pathWithNext(PATH_ADMIN_PHOTOS, next); +export const pathForAdminPhotoEdit = (photo: PhotoOrPhotoId) => + `${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/${EDIT}`; + +export const pathForAdminTagEdit = (tag: string) => + `${PATH_ADMIN_TAGS}/${tag}/${EDIT}`; + export const pathForOg = (next?: number) => pathWithNext(PATH_OG, next); @@ -70,9 +77,6 @@ export const pathForPhotoShare = ( ) => `${pathForPhoto(photo, tag, camera)}/${SHARE}`; -export const pathForPhotoEdit = (photo: PhotoOrPhotoId) => - `${PATH_ADMIN_PHOTOS}/${getPhotoId(photo)}/edit`; - export const pathForTag = (tag: string, next?: number) => pathWithNext( `${PREFIX_TAG}/${tag}`, diff --git a/src/tag/TagForm.tsx b/src/tag/TagForm.tsx new file mode 100644 index 00000000..c572b807 --- /dev/null +++ b/src/tag/TagForm.tsx @@ -0,0 +1,79 @@ +'use client'; + +import { photoQuantityText } from '@/photo'; +import PhotoTag from './PhotoTag'; +import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; +import Link from 'next/link'; +import { PATH_ADMIN_TAGS } from '@/site/paths'; +import FieldSetWithStatus from '@/components/FieldSetWithStatus'; +import { useMemo, useState } from 'react'; +import { renamePhotoTagGloballyAction } from '@/photo/actions'; +import { parameterize } from '@/utility/string'; + +export default function TagForm({ + tag, + count, +}: { + tag: string + count: number +}) { + const [updatedTagRaw, setUpdatedTagRaw] = useState(tag); + + const updatedTag = useMemo(() => + parameterize(updatedTagRaw) + , [updatedTagRaw]); + + const isFormValid = ( + updatedTag && + updatedTag !== tag + ); + + return ( +
+
+ +
+ {photoQuantityText(count, false)} +
+
+
+ + {/* Form data: tag to be replaced */} + + {/* Form data: updated tag */} + +
+ + Cancel + + + Update + +
+ +
+ ); +} diff --git a/src/utility/string.ts b/src/utility/string.ts index 73590a36..74b3b94f 100644 --- a/src/utility/string.ts +++ b/src/utility/string.ts @@ -1,9 +1,9 @@ export const convertStringToArray = ( string?: string, - parameterize = true, + shouldParameterize = true, ) => string - ? string.split(',').map(tag => parameterize - ? tag.trim().replaceAll(' ', '-').toLowerCase() + ? string.split(',').map(tag => shouldParameterize + ? parameterize(tag) : tag.trim()) : undefined; @@ -19,5 +19,5 @@ export const capitalizeWords = (string: string) => export const parameterize = (string: string) => string .trim() - .replaceAll(' ', '-') + .replaceAll(/\s+/g, '-') .toLowerCase();