Add ••• admin button to individual photos

This commit is contained in:
Sam Becker 2024-01-08 12:52:22 -06:00
parent 74ca2ba383
commit 47ebc65553
8 changed files with 289 additions and 9 deletions

View File

@ -14,6 +14,7 @@
"favs", "favs",
"ghijklmnopqrstuv", "ghijklmnopqrstuv",
"Hasselblad", "Hasselblad",
"headlessui",
"hgetall", "hgetall",
"hset", "hset",
"Lightbox", "Lightbox",

View File

@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "3.485.0", "@aws-sdk/client-s3": "3.485.0",
"@aws-sdk/s3-request-presigner": "3.485.0", "@aws-sdk/s3-request-presigner": "3.485.0",
"@headlessui/react": "2.0.0-alpha.4",
"@next/bundle-analyzer": "14.0.4", "@next/bundle-analyzer": "14.0.4",
"@tailwindcss/forms": "^0.5.7", "@tailwindcss/forms": "^0.5.7",
"@testing-library/jest-dom": "^6.2.0", "@testing-library/jest-dom": "^6.2.0",

198
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ dependencies:
'@aws-sdk/s3-request-presigner': '@aws-sdk/s3-request-presigner':
specifier: 3.485.0 specifier: 3.485.0
version: 3.485.0 version: 3.485.0
'@headlessui/react':
specifier: 2.0.0-alpha.4
version: 2.0.0-alpha.4(react-dom@18.2.0)(react@18.2.0)
'@next/bundle-analyzer': '@next/bundle-analyzer':
specifier: 14.0.4 specifier: 14.0.4
version: 14.0.4 version: 14.0.4
@ -1159,6 +1162,62 @@ packages:
engines: {node: '>=14'} engines: {node: '>=14'}
dev: false dev: false
/@floating-ui/core@1.5.3:
resolution: {integrity: sha512-O0WKDOo0yhJuugCx6trZQj5jVJ9yR0ystG2JaNAemYUWce+pmM6WUEFIibnWyEJKdrDxhm75NoSRME35FNaM/Q==}
dependencies:
'@floating-ui/utils': 0.2.1
dev: false
/@floating-ui/dom@1.5.4:
resolution: {integrity: sha512-jByEsHIY+eEdCjnTVu+E3ephzTOzkQ8hgUfGwos+bg7NlH33Zc5uO+QHz1mrQUOgIKKDD1RtS201P9NvAfq3XQ==}
dependencies:
'@floating-ui/core': 1.5.3
'@floating-ui/utils': 0.2.1
dev: false
/@floating-ui/react-dom@2.0.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-UsBK30Bg+s6+nsgblXtZmwHhgS2vmbuQK22qgt2pTQM6M3X6H1+cQcLXqgRY3ihVLcZJE6IvqDQozhsnIVqK/Q==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 1.5.4
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@floating-ui/react@0.26.5(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-LJeSQa+yOwV0Tdpc/C3Vr92QMrwRqRMTk4yOwsRJKc57x3Lcw317GE0EV+ECM7+Z89yEAPBe7nzbDEWfkWCrBA==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/react-dom': 2.0.5(react-dom@18.2.0)(react@18.2.0)
'@floating-ui/utils': 0.2.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tabbable: 6.2.0
dev: false
/@floating-ui/utils@0.2.1:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
dev: false
/@headlessui/react@2.0.0-alpha.4(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-spykSTXskDUYjSFhdId97Bqclo1F9Ky2pgLmyNKdV4f7aRDncdc/mjMPx67eEWRN2xQobksaUCcnn5K/AcRXsg==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
dependencies:
'@floating-ui/react': 0.26.5(react-dom@18.2.0)(react@18.2.0)
'@react-aria/focus': 3.16.0(react@18.2.0)
'@react-aria/interactions': 3.0.0-nightly.2584(react@18.2.0)
'@tanstack/react-virtual': 3.0.0-beta.60(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@humanwhocodes/config-array@0.11.13: /@humanwhocodes/config-array@0.11.13:
resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==}
engines: {node: '>=10.10.0'} engines: {node: '>=10.10.0'}
@ -1593,6 +1652,123 @@ packages:
resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==}
dev: false dev: false
/@react-aria/focus@3.16.0(react@18.2.0):
resolution: {integrity: sha512-GP6EYI07E8NKQQcXHjpIocEU0vh0oi0Vcsd+/71fKS0NnTR0TUOEeil0JuuQ9ymkmPDTu51Aaaa4FxVsuN/23A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/interactions': 3.20.1(react@18.2.0)
'@react-aria/utils': 3.23.0(react@18.2.0)
'@react-types/shared': 3.22.0(react@18.2.0)
'@swc/helpers': 0.5.2
clsx: 2.1.0
react: 18.2.0
dev: false
/@react-aria/interactions@3.0.0-nightly.2584(react@18.2.0):
resolution: {integrity: sha512-6DqYQx8XnbCfIen33uLz4kdgevrXLW6aoxsBOTY/Mzq9n0LHzbG/5H87obrOxRNVYh62RcQolo/qfqEpXZ7bVA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.1-nightly.4295(react@18.2.0)
'@react-aria/utils': 3.0.0-nightly.2584(react@18.2.0)
'@react-types/shared': 3.0.0-nightly.2584(react@18.2.0)
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/interactions@3.20.1(react@18.2.0):
resolution: {integrity: sha512-PLNBr87+SzRhe9PvvF9qvzYeP4ofTwfKSorwmO+hjr3qoczrSXf4LRQlb27wB6hF10C7ZE/XVbUI1lj4QQrZ/g==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.1(react@18.2.0)
'@react-aria/utils': 3.23.0(react@18.2.0)
'@react-types/shared': 3.22.0(react@18.2.0)
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/ssr@3.9.1(react@18.2.0):
resolution: {integrity: sha512-NqzkLFP8ZVI4GSorS0AYljC13QW2sc8bDqJOkBvkAt3M8gbcAXJWVRGtZBCRscki9RZF+rNlnPdg0G0jYkhJcg==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/ssr@3.9.1-nightly.4295(react@18.2.0):
resolution: {integrity: sha512-cv0+RaS3LJeZiSJ4pVGqSAyiyL+rieLiR3ctyoU7EwkArY1W7fI3NSkMEbNhHe4YoqqjPy1ZzAcpSA11EceiBg==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/utils@3.0.0-nightly.2584(react@18.2.0):
resolution: {integrity: sha512-A6NP3Yc9MMA+PiRBMTpMlx5plaiK7ejl3cppdkKiNPHtFmZrzxn6o9WHth4NToqIUkJRWHIrpTK8a/gBgVFPOg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.1-nightly.4295(react@18.2.0)
'@react-stately/utils': 3.0.0-nightly.2584(react@18.2.0)
'@react-types/shared': 3.0.0-nightly.2584(react@18.2.0)
'@swc/helpers': 0.5.2
clsx: 1.2.1
react: 18.2.0
dev: false
/@react-aria/utils@3.23.0(react@18.2.0):
resolution: {integrity: sha512-fJA63/VU4iQNT8WUvrmll3kvToqMurD69CcgVmbQ56V7ZbvlzFi44E7BpnoaofScYLLtFWRjVdaHsohT6O/big==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.1(react@18.2.0)
'@react-stately/utils': 3.9.0(react@18.2.0)
'@react-types/shared': 3.22.0(react@18.2.0)
'@swc/helpers': 0.5.2
clsx: 2.1.0
react: 18.2.0
dev: false
/@react-stately/utils@3.0.0-nightly.2584(react@18.2.0):
resolution: {integrity: sha512-UOW2P+H3O7goB1mNEIwUdxr28CVHrKKvi+N1CQ0TGDwr+Bp6oIZK2aXE6aQluzgwZ36aRvLPW5dAoovpzTTcQQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-stately/utils@3.9.0(react@18.2.0):
resolution: {integrity: sha512-yPKFY1F88HxuZ15BG2qwAYxtpE4HnIU0Ofi4CuBE0xC6I8mwo4OQjDzi+DZjxQngM9D6AeTTD6F1V8gkozA0Gw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-types/shared@3.0.0-nightly.2584(react@18.2.0):
resolution: {integrity: sha512-SVqvg7B3rtzN1ypQni5g6sfpUNf4wODRDtiOalBFSJ02YuaUIr7gXVjafPYIXOC1BkJbZtPun/Pv4mCwNHFNbA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/@react-types/shared@3.22.0(react@18.2.0):
resolution: {integrity: sha512-yVOekZWbtSmmiThGEIARbBpnmUIuePFlLyctjvCbgJgGhz8JnEJOipLQ/a4anaWfzAgzSceQP8j/K+VOOePleA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/@rushstack/eslint-patch@1.6.1: /@rushstack/eslint-patch@1.6.1:
resolution: {integrity: sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==} resolution: {integrity: sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==}
dev: false dev: false
@ -2127,6 +2303,19 @@ packages:
tailwindcss: 3.4.1 tailwindcss: 3.4.1
dev: false dev: false
/@tanstack/react-virtual@3.0.0-beta.60(react@18.2.0):
resolution: {integrity: sha512-F0wL9+byp7lf/tH6U5LW0ZjBqs+hrMXJrj5xcIGcklI0pggvjzMNW9DdIBcyltPNr6hmHQ0wt8FDGe1n1ZAThA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@tanstack/virtual-core': 3.0.0-beta.60
react: 18.2.0
dev: false
/@tanstack/virtual-core@3.0.0-beta.60:
resolution: {integrity: sha512-QlCdhsV1+JIf0c0U6ge6SQmpwsyAT0oQaOSZk50AtEeAyQl9tQrd6qCHAslxQpgphrfe945abvKG8uYvw3hIGA==}
dev: false
/@testing-library/dom@9.3.3: /@testing-library/dom@9.3.3:
resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==}
engines: {node: '>=14'} engines: {node: '>=14'}
@ -2996,6 +3185,11 @@ packages:
wrap-ansi: 7.0.0 wrap-ansi: 7.0.0
dev: false dev: false
/clsx@1.2.1:
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
engines: {node: '>=6'}
dev: false
/clsx@2.1.0: /clsx@2.1.0:
resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==}
engines: {node: '>=6'} engines: {node: '>=6'}
@ -6305,6 +6499,10 @@ packages:
resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==}
dev: false dev: false
/tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
dev: false
/tailwindcss@3.4.1: /tailwindcss@3.4.1:
resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==}
engines: {node: '>=14.0.0'} engines: {node: '>=14.0.0'}

View File

@ -0,0 +1,10 @@
import { authCached } from '@/cache';
import AdminPhotoMenuClient, { AdminPhotoMenuClientProps }
from './AdminPhotoMenuClient';
export default async function AdminPhotoMenu(props: AdminPhotoMenuClientProps) {
const session = await authCached();
return Boolean(session?.user?.email)
? <AdminPhotoMenuClient {...props} />
: null;
}

View File

@ -0,0 +1,52 @@
'use client';
import { pathForAdminPhotoEdit } from '@/site/paths';
import { Menu } from '@headlessui/react';
import clsx from 'clsx/lite';
import Link from 'next/link';
import { FiMoreHorizontal } from 'react-icons/fi';
export interface AdminPhotoMenuClientProps {
photoId: string
className?: string
buttonClassName?: string
}
export default function AdminPhotoMenuClient({
photoId,
className,
buttonClassName,
}: AdminPhotoMenuClientProps) {
return (
<div className={clsx(
className,
'relative',
)}>
<Menu>
<Menu.Button className={clsx(
buttonClassName,
'p-1 py-1 min-h-0 border-none shadow-none',
'text-dim',
)}
>
<FiMoreHorizontal size={16} />
</Menu.Button>
<Menu.Items className={clsx(
'absolute top-6 right-1',
'text-sm',
'px-3 py-1.5 rounded-md border',
'bg-content',
)}>
<Menu.Item>
<Link
className="whitespace-nowrap"
href={pathForAdminPhotoEdit(photoId)}
>
Edit Photo
</Link>
</Menu.Item>
</Menu.Items>
</Menu>
</div>
);
}

View File

@ -26,7 +26,7 @@ export default function Badge({
'px-[0.3rem] py-1 rounded-[0.25rem]', 'px-[0.3rem] py-1 rounded-[0.25rem]',
'text-[0.7rem] font-medium', 'text-[0.7rem] font-medium',
highContrast highContrast
? 'text-invert bg-main' ? 'text-invert bg-primary'
: 'text-medium bg-gray-300/30 dark:bg-gray-700/50', : 'text-medium bg-gray-300/30 dark:bg-gray-700/50',
interactive && highContrast interactive && highContrast
? 'hover:opacity-70' ? 'hover:opacity-70'

View File

@ -10,6 +10,8 @@ import PhotoCamera from '../camera/PhotoCamera';
import { cameraFromPhoto } from '@/camera'; import { cameraFromPhoto } from '@/camera';
import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation';
import { sortTags } from '@/tag'; import { sortTags } from '@/tag';
import AdminPhotoMenu from '@/admin/AdminPhotoMenu';
import { Suspense } from 'react';
export default function PhotoLarge({ export default function PhotoLarge({
photo, photo,
@ -72,12 +74,22 @@ export default function PhotoLarge({
)}> )}>
{renderMiniGrid(<> {renderMiniGrid(<>
<div className="-space-y-0.5"> <div className="-space-y-0.5">
<div className="relative flex gap-1 items-start">
<div className="flex-grow">
<Link <Link
href={pathForPhoto(photo)} href={pathForPhoto(photo)}
className="font-bold uppercase" className="font-bold uppercase"
> >
{titleForPhoto(photo)} {titleForPhoto(photo)}
</Link> </Link>
</div>
<Suspense>
<AdminPhotoMenu
photoId={photo.id}
buttonClassName="translate-y-[-3px]"
/>
</Suspense>
</div>
{tags.length > 0 && {tags.length > 0 &&
<PhotoTags tags={tags} />} <PhotoTags tags={tags} />}
</div> </div>

View File

@ -112,7 +112,7 @@
hover:text-gray-600 hover:text-gray-600
hover:dark:text-gray-400 hover:dark:text-gray-400
} }
/* Common Utilities */ /* Common Utilities: Text */
.text-main { .text-main {
@apply @apply
text-gray-900 dark:text-gray-100 text-gray-900 dark:text-gray-100
@ -141,7 +141,13 @@
@apply @apply
text-red-500 dark:text-red-400 text-red-500 dark:text-red-400
} }
.bg-main { /* Common Utilities: Background */
.bg-content {
@apply
bg-white border-gray-200
dark:bg-black dark:border-gray-800
}
.bg-primary {
@apply @apply
bg-gray-900 dark:bg-gray-100 bg-gray-900 dark:bg-gray-100
} }