Merge branch 'main' into static

This commit is contained in:
Sam Becker 2024-02-11 10:10:15 -06:00
commit 48739f2caf
10 changed files with 146 additions and 60 deletions

View File

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

View File

@ -3,19 +3,42 @@
import { ComponentProps } from 'react';
import { pathForAdminPhotoEdit } from '@/site/paths';
import MoreMenu from '../components/MoreMenu';
export interface AdminPhotoMenuClientProps
extends Omit<ComponentProps<typeof MoreMenu>, 'items'> {
photoId: string
}
import { toggleFavoritePhoto } from '@/photo/actions';
import { FaRegEdit, FaRegStar, FaStar } from 'react-icons/fa';
import { Photo } from '@/photo';
import { isPathFavs, isPhotoFav } from '@/tag';
import { usePathname } from 'next/navigation';
export default function AdminPhotoMenuClient({
photoId,
photo,
...props
}: AdminPhotoMenuClientProps) {
}: Omit<ComponentProps<typeof MoreMenu>, 'items'> & {
photo: Photo
}) {
const isFav = isPhotoFav(photo);
const path = usePathname();
const shouldRedirect = isPathFavs(path) && isFav;
return (
<MoreMenu {...{
items: [{ href: pathForAdminPhotoEdit(photoId), label: 'Edit Photo' }],
items: [
{
label: 'Edit Photo',
icon: <FaRegEdit size={14} className="translate-y-[-0.5px]" />,
href: pathForAdminPhotoEdit(photo.id),
}, {
label: isFav ? 'Unfavorite' : 'Favorite',
icon: isFav
? <FaStar
size={14}
className="translate-y-[-1px] text-amber-500"
/>
: <FaRegStar
size={14}
className="translate-x-[-1px]"
/>,
action: () => toggleFavoritePhoto(photo.id, shouldRedirect),
},
],
...props,
}}/>
);

View File

@ -4,10 +4,12 @@ import { clsx } from 'clsx/lite';
export default function Checklist({
title,
icon,
optional,
children,
}: {
title: string
icon?: ReactNode
optional?: boolean
children: ReactNode
}) {
return (
@ -15,11 +17,13 @@ export default function Checklist({
<div className={clsx(
'flex items-center gap-3',
'text-gray-600 dark:text-gray-300',
'pl-[18px] mb-3',
'pl-[18px] mb-3 text-lg',
)}>
{icon}
<div className="text-lg">
{title}
<div className="flex gap-1.5">
<div>{title}</div>
{optional &&
<div className="text-dim">(Optional)</div>}
</div>
</div>
<div className={clsx(

View File

@ -27,7 +27,6 @@ export default function ChecklistRow({
<div className="flex flex-col min-w-0">
<div className="font-bold dark:text-gray-300">
{title}
{optional && ' (optional)'}
</div>
<div>
{children}

View File

@ -2,17 +2,46 @@ import { clsx} from 'clsx/lite';
import Link from 'next/link';
import { Menu } from '@headlessui/react';
import { FiMoreHorizontal } from 'react-icons/fi';
import { ReactNode } from 'react';
import { Fragment, ReactNode, useState } from 'react';
export default function MoreMenu({
items,
className,
buttonClassName,
}: {
items: { href: string, label: ReactNode }[]
items: {
label: ReactNode,
icon?: ReactNode,
href?: string,
action?: () => Promise<void>,
}[]
className?: string
buttonClassName?: string
}) {
const [isLoading, setIsLoading] = useState(false);
const itemClass = clsx(
'block w-full',
'border-none min-h-0 bg-transparent',
'text-left text-main',
'px-3 py-1.5 rounded-[3px]',
'hover:text-main',
'hover:bg-gray-50 active:bg-gray-100',
'hover:dark:bg-gray-900/75 active:dark:bg-gray-900',
'whitespace-nowrap',
'shadow-none',
isLoading && 'cursor-not-allowed opacity-50',
);
const renderItemContent = (
label: ReactNode,
icon?: ReactNode,
) =>
<div className="flex items-center">
<span className="w-6">{icon}</span>
<span>{label}</span>
</div>;
return (
<div className={clsx(
className,
@ -28,28 +57,39 @@ export default function MoreMenu({
<FiMoreHorizontal size={18} />
</Menu.Button>
<Menu.Items className={clsx(
'block outline-none h-auto',
'absolute top-6',
'min-w-[9rem]',
'text-left',
'md:right-1',
'text-sm',
'p-1 rounded-md border',
'bg-content',
'bg-content outline-none',
'shadow-lg dark:shadow-xl',
)}>
{items.map(({ href, label }) =>
<Menu.Item key={href}>
<Link
href={href}
className={clsx(
'block',
'px-3 py-1.5 rounded-[3px]',
'hover:text-main',
'hover:bg-gray-50 active:bg-gray-100',
'hover:dark:bg-gray-900/75 active:dark:bg-gray-900',
'whitespace-nowrap',
)}
>
{label}
</Link>
{items.map(({ label, icon, href, action }) =>
<Menu.Item
key={`${label}`}
disabled={isLoading}
as={Fragment}
>
<>
{href &&
<Link
href={href}
className={itemClass}
>
{renderItemContent(label, icon)}
</Link>}
{action &&
<button
onClick={() => {
setIsLoading(true);
action().finally(() => setIsLoading(false));
}}
className={itemClass}
>
{renderItemContent(label, icon)}
</button>}
</>
</Menu.Item>
)}
</Menu.Items>

View File

@ -89,8 +89,8 @@ export default function PhotoLarge({
</Link>
</div>
<Suspense>
<div className="h-4 translate-y-[-3.5px]">
<AdminPhotoMenu photoId={photo.id} />
<div className="h-4 translate-y-[-3.5px] z-10">
<AdminPhotoMenu photo={photo} />
</div>
</Suspense>
</div>
@ -104,7 +104,7 @@ export default function PhotoLarge({
type="text-only"
/>
{showSimulation && photo.filmSimulation &&
<div className="translate-x-[-0.3rem] relative -z-10">
<div className="translate-x-[-0.3rem]">
<PhotoFilmSimulation
simulation={photo.filmSimulation}
/>

View File

@ -23,7 +23,7 @@ import {
revalidateAllKeysAndPaths,
revalidatePhotosKey,
} from '@/photo/cache';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS } from '@/site/paths';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_TAGS, PATH_ROOT } from '@/site/paths';
import { extractExifDataFromBlobPath } from './server';
import { TAG_FAVS, isTagFavs } from '@/tag';
import { convertPhotoToPhotoDbInsert } from '.';
@ -52,7 +52,10 @@ export async function updatePhotoAction(formData: FormData) {
redirect(PATH_ADMIN_PHOTOS);
}
export async function toggleFavoritePhoto(photoId: string) {
export async function toggleFavoritePhoto(
photoId: string,
shouldRedirect?: boolean,
) {
const photo = await getPhoto(photoId);
if (photo) {
const { tags } = photo;
@ -61,6 +64,9 @@ export async function toggleFavoritePhoto(photoId: string) {
: [...tags, TAG_FAVS];
await sqlUpdatePhoto(convertPhotoToPhotoDbInsert(photo));
revalidateAllKeysAndPaths();
if (shouldRedirect) {
redirect(PATH_ROOT);
}
}
}

View File

@ -29,7 +29,7 @@ export default function SiteChecklistClient({
hasAwsS3Storage,
hasMultipleStorageProviders,
currentStorage,
hasAuth,
hasAuthSecret,
hasAdminUser,
hasTitle,
hasDomain,
@ -198,26 +198,27 @@ export default function SiteChecklistClient({
>
<ChecklistRow
title="Setup auth"
status={hasAuth}
status={hasAuthSecret}
isPending={isPendingPage}
>
Store auth secret in environment variable:
<div className="overflow-x-auto">
<InfoBlock className="my-1.5 inline-flex" padding="tight">
<div className="flex flex-nowrap items-center gap-4">
<span>{secret}</span>
<div className="flex items-center gap-0.5">
{renderCopyButton('Secret', secret)}
<IconButton
icon={<BiRefresh size={18} />}
onClick={refreshSecret}
isLoading={isPendingSecret}
spinnerColor="text"
/>
{!hasAuthSecret &&
<div className="overflow-x-auto">
<InfoBlock className="my-1.5 inline-flex" padding="tight">
<div className="flex flex-nowrap items-center gap-4">
<span>{secret}</span>
<div className="flex items-center gap-0.5">
{renderCopyButton('Secret', secret)}
<IconButton
icon={<BiRefresh size={18} />}
onClick={refreshSecret}
isLoading={isPendingSecret}
spinnerColor="text"
/>
</div>
</div>
</div>
</InfoBlock>
</div>
</InfoBlock>
</div>}
{renderEnvVars(['AUTH_SECRET'])}
</ChecklistRow>
<ChecklistRow
@ -237,6 +238,7 @@ export default function SiteChecklistClient({
<Checklist
title="Content"
icon={<BiPencil size={16} />}
optional
>
<ChecklistRow
title="Add title"
@ -260,6 +262,7 @@ export default function SiteChecklistClient({
<Checklist
title="Settings"
icon={<BiCog size={16} />}
optional
>
<ChecklistRow
title="Pro Mode"

View File

@ -110,7 +110,7 @@ export const CONFIG_CHECKLIST_STATUS = {
HAS_AWS_S3_STORAGE,
hasMultipleStorageProviders: HAS_MULTIPLE_STORAGE_PROVIDERS,
currentStorage: CURRENT_STORAGE,
hasAuth: (process.env.AUTH_SECRET ?? '').length > 0,
hasAuthSecret: (process.env.AUTH_SECRET ?? '').length > 0,
hasAdminUser: (
(process.env.ADMIN_EMAIL ?? '').length > 0 &&
(process.env.ADMIN_PASSWORD ?? '').length > 0
@ -134,5 +134,5 @@ export type ConfigChecklistStatus = typeof CONFIG_CHECKLIST_STATUS;
export const IS_SITE_READY =
CONFIG_CHECKLIST_STATUS.hasPostgres &&
CONFIG_CHECKLIST_STATUS.hasStorage &&
CONFIG_CHECKLIST_STATUS.hasAuth &&
CONFIG_CHECKLIST_STATUS.hasAuthSecret &&
CONFIG_CHECKLIST_STATUS.hasAdminUser;

View File

@ -4,7 +4,11 @@ import {
descriptionForPhotoSet,
photoQuantityText,
} from '@/photo';
import { absolutePathForTag, absolutePathForTagImage } from '@/site/paths';
import {
absolutePathForTag,
absolutePathForTagImage,
getPathComponents,
} from '@/site/paths';
import { capitalizeWords, convertStringToArray } from '@/utility/string';
export const TAG_FAVS = 'favs';
@ -77,3 +81,8 @@ export const generateMetaForTag = (
});
export const isTagFavs = (tag: string) => tag.toLowerCase() === TAG_FAVS;
export const isPhotoFav = ({ tags }: Photo) => tags.some(isTagFavs);
export const isPathFavs = (pathname?: string) =>
getPathComponents(pathname).tag === TAG_FAVS;