From 47ebc655532ccfa3a48270426ee6dc214a003366 Mon Sep 17 00:00:00 2001 From: Sam Becker Date: Mon, 8 Jan 2024 12:52:22 -0600 Subject: [PATCH] =?UTF-8?q?Add=20=E2=80=A2=E2=80=A2=E2=80=A2=20admin=20but?= =?UTF-8?q?ton=20to=20individual=20photos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .vscode/settings.json | 1 + package.json | 1 + pnpm-lock.yaml | 198 +++++++++++++++++++++++++++++ src/admin/AdminPhotoMenu.tsx | 10 ++ src/admin/AdminPhotoMenuClient.tsx | 52 ++++++++ src/components/Badge.tsx | 2 +- src/photo/PhotoLarge.tsx | 24 +++- src/site/globals.css | 10 +- 8 files changed, 289 insertions(+), 9 deletions(-) create mode 100644 src/admin/AdminPhotoMenu.tsx create mode 100644 src/admin/AdminPhotoMenuClient.tsx diff --git a/.vscode/settings.json b/.vscode/settings.json index 27747e68..e664ce2a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -14,6 +14,7 @@ "favs", "ghijklmnopqrstuv", "Hasselblad", + "headlessui", "hgetall", "hset", "Lightbox", diff --git a/package.json b/package.json index 53c6716b..b5e37b83 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@aws-sdk/client-s3": "3.485.0", "@aws-sdk/s3-request-presigner": "3.485.0", + "@headlessui/react": "2.0.0-alpha.4", "@next/bundle-analyzer": "14.0.4", "@tailwindcss/forms": "^0.5.7", "@testing-library/jest-dom": "^6.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b6162f91..4ab6cca2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: '@aws-sdk/s3-request-presigner': specifier: 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': specifier: 14.0.4 version: 14.0.4 @@ -1159,6 +1162,62 @@ packages: engines: {node: '>=14'} 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: resolution: {integrity: sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ==} engines: {node: '>=10.10.0'} @@ -1593,6 +1652,123 @@ packages: resolution: {integrity: sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==} 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: resolution: {integrity: sha512-UY+FGM/2jjMkzQLn8pxcHGMaVLh9aEitG3zY2CiY7XHdLiz3bZOwa6oDxNqEMv7zZkV+cj5DOdz0cQ1BP5Hjgw==} dev: false @@ -2127,6 +2303,19 @@ packages: tailwindcss: 3.4.1 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: resolution: {integrity: sha512-fB0R+fa3AUqbLHWyxXa2kGVtf1Fe1ZZFr0Zp6AIbIAzXb2mKbEXl+PCQNUOaq5lbTab5tfctfXRNsWXxa2f7Aw==} engines: {node: '>=14'} @@ -2996,6 +3185,11 @@ packages: wrap-ansi: 7.0.0 dev: false + /clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + dev: false + /clsx@2.1.0: resolution: {integrity: sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==} engines: {node: '>=6'} @@ -6305,6 +6499,10 @@ packages: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} dev: false + /tabbable@6.2.0: + resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} + dev: false + /tailwindcss@3.4.1: resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} engines: {node: '>=14.0.0'} diff --git a/src/admin/AdminPhotoMenu.tsx b/src/admin/AdminPhotoMenu.tsx new file mode 100644 index 00000000..82126b4e --- /dev/null +++ b/src/admin/AdminPhotoMenu.tsx @@ -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) + ? + : null; +} diff --git a/src/admin/AdminPhotoMenuClient.tsx b/src/admin/AdminPhotoMenuClient.tsx new file mode 100644 index 00000000..a32dd09c --- /dev/null +++ b/src/admin/AdminPhotoMenuClient.tsx @@ -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 ( +
+ + + + + + + + Edit Photo + + + + +
+ ); +} diff --git a/src/components/Badge.tsx b/src/components/Badge.tsx index 7662d470..d1c3c369 100644 --- a/src/components/Badge.tsx +++ b/src/components/Badge.tsx @@ -26,7 +26,7 @@ export default function Badge({ 'px-[0.3rem] py-1 rounded-[0.25rem]', 'text-[0.7rem] font-medium', highContrast - ? 'text-invert bg-main' + ? 'text-invert bg-primary' : 'text-medium bg-gray-300/30 dark:bg-gray-700/50', interactive && highContrast ? 'hover:opacity-70' diff --git a/src/photo/PhotoLarge.tsx b/src/photo/PhotoLarge.tsx index faa64436..caffb248 100644 --- a/src/photo/PhotoLarge.tsx +++ b/src/photo/PhotoLarge.tsx @@ -10,6 +10,8 @@ import PhotoCamera from '../camera/PhotoCamera'; import { cameraFromPhoto } from '@/camera'; import PhotoFilmSimulation from '@/simulation/PhotoFilmSimulation'; import { sortTags } from '@/tag'; +import AdminPhotoMenu from '@/admin/AdminPhotoMenu'; +import { Suspense } from 'react'; export default function PhotoLarge({ photo, @@ -72,12 +74,22 @@ export default function PhotoLarge({ )}> {renderMiniGrid(<>
- - {titleForPhoto(photo)} - +
+
+ + {titleForPhoto(photo)} + +
+ + + +
{tags.length > 0 && }
diff --git a/src/site/globals.css b/src/site/globals.css index b62a0eb4..528d5fe8 100644 --- a/src/site/globals.css +++ b/src/site/globals.css @@ -112,7 +112,7 @@ hover:text-gray-600 hover:dark:text-gray-400 } - /* Common Utilities */ + /* Common Utilities: Text */ .text-main { @apply text-gray-900 dark:text-gray-100 @@ -141,7 +141,13 @@ @apply 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 bg-gray-900 dark:bg-gray-100 }