Switch to clsx for class concatenation

This commit is contained in:
Sam Becker 2023-12-30 13:46:42 -05:00
parent 4614740da8
commit 91e1fb2166
49 changed files with 439 additions and 337 deletions

View File

@ -27,6 +27,7 @@
"@vercel/speed-insights": "^1.0.2", "@vercel/speed-insights": "^1.0.2",
"autoprefixer": "10.4.16", "autoprefixer": "10.4.16",
"camelcase-keys": "^9.1.2", "camelcase-keys": "^9.1.2",
"clsx": "^2.1.0",
"date-fns": "^3.0.6", "date-fns": "^3.0.6",
"eslint": "8.56.0", "eslint": "8.56.0",
"eslint-config-next": "14.0.4", "eslint-config-next": "14.0.4",

493
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export default function AdminGrid ({ export default function AdminGrid ({
@ -15,7 +15,7 @@ export default function AdminGrid ({
</div>} </div>}
{/* py-[1px] fixes Safari vertical scroll bug */} {/* py-[1px] fixes Safari vertical scroll bug */}
<div className="min-w-[14rem] overflow-x-scroll py-[1px]"> <div className="min-w-[14rem] overflow-x-scroll py-[1px]">
<div className={cc( <div className={clsx(
'w-full', 'w-full',
'grid grid-cols-[auto_1fr_auto] ', 'grid grid-cols-[auto_1fr_auto] ',
'gap-2 sm:gap-3 items-center', 'gap-2 sm:gap-3 items-center',

View File

@ -6,7 +6,7 @@ import {
checkPathPrefix, checkPathPrefix,
isPathAdminConfiguration, isPathAdminConfiguration,
} from '@/site/paths'; } from '@/site/paths';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { BiCog } from 'react-icons/bi'; import { BiCog } from 'react-icons/bi';
@ -25,11 +25,11 @@ export default function AdminNav({
return ( return (
<SiteGrid <SiteGrid
contentMain={ contentMain={
<div className={cc( <div className={clsx(
'flex gap-2 md:gap-4', 'flex gap-2 md:gap-4',
'border-b border-gray-200 dark:border-gray-800 pb-3', 'border-b border-gray-200 dark:border-gray-800 pb-3',
)}> )}>
<div className={cc( <div className={clsx(
'flex gap-2 md:gap-4', 'flex gap-2 md:gap-4',
'flex-grow overflow-x-scroll', 'flex-grow overflow-x-scroll',
)}> )}>
@ -37,7 +37,7 @@ export default function AdminNav({
<Link <Link
key={label} key={label}
href={href} href={href}
className={cc( className={clsx(
'flex gap-0.5', 'flex gap-0.5',
checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim', checkPathPrefix(pathname, href) ? 'font-bold' : 'text-dim',
)} )}

View File

@ -6,7 +6,7 @@ import { fileNameForBlobUrl } from '@/services/blob';
import FormWithConfirm from '@/components/FormWithConfirm'; import FormWithConfirm from '@/components/FormWithConfirm';
import { deleteBlobPhotoAction } from '@/photo/actions'; import { deleteBlobPhotoAction } from '@/photo/actions';
import DeleteButton from './DeleteButton'; import DeleteButton from './DeleteButton';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { pathForAdminUploadUrl } from '@/site/paths'; import { pathForAdminUploadUrl } from '@/site/paths';
import AddButton from './AddButton'; import AddButton from './AddButton';
@ -28,7 +28,7 @@ export default function BlobUrls({
alt={`Upload: ${uploadFileName}`} alt={`Upload: ${uploadFileName}`}
src={url} src={url}
aspectRatio={3.0 / 2.0} aspectRatio={3.0 / 2.0}
className={cc( className={clsx(
'rounded-sm overflow-hidden', 'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800', 'border border-gray-200 dark:border-gray-800',
)} )}
@ -41,7 +41,7 @@ export default function BlobUrls({
> >
{uploadFileName} {uploadFileName}
</Link> </Link>
<div className={cc( <div className={clsx(
'flex flex-nowrap', 'flex flex-nowrap',
'gap-2 sm:gap-3 items-center', 'gap-2 sm:gap-3 items-center',
)}> )}>

View File

@ -1,5 +1,5 @@
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { BiTrash } from 'react-icons/bi'; import { BiTrash } from 'react-icons/bi';
export default function DeleteButton () { export default function DeleteButton () {
@ -7,7 +7,7 @@ export default function DeleteButton () {
title="Delete" title="Delete"
icon={<BiTrash size={16} className="translate-y-[-1.5px]" />} icon={<BiTrash size={16} className="translate-y-[-1.5px]" />}
spinnerColor="text" spinnerColor="text"
className={cc( className={clsx(
'text-red-500 dark:text-red-600', 'text-red-500 dark:text-red-600',
'active:!bg-red-100/50 active:dark:!bg-red-950/50', 'active:!bg-red-100/50 active:dark:!bg-red-950/50',
'!border-red-200 hover:!border-red-300', '!border-red-200 hover:!border-red-300',

View File

@ -2,7 +2,7 @@ import { Fragment } from 'react';
import PhotoUpload from '@/photo/PhotoUpload'; import PhotoUpload from '@/photo/PhotoUpload';
import Link from 'next/link'; import Link from 'next/link';
import PhotoTiny from '@/photo/PhotoTiny'; import PhotoTiny from '@/photo/PhotoTiny';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import FormWithConfirm from '@/components/FormWithConfirm'; import FormWithConfirm from '@/components/FormWithConfirm';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import { deletePhotoAction, syncPhotoExifDataAction } from '@/photo/actions'; import { deletePhotoAction, syncPhotoExifDataAction } from '@/photo/actions';
@ -56,7 +56,7 @@ export default async function AdminPhotosPage({
<div className="space-y-8"> <div className="space-y-8">
<PhotoUpload shouldResize={!PRO_MODE_ENABLED} /> <PhotoUpload shouldResize={!PRO_MODE_ENABLED} />
{blobPhotoUrls.length > 0 && {blobPhotoUrls.length > 0 &&
<div className={cc( <div className={clsx(
'border-b pb-6', 'border-b pb-6',
'border-gray-200 dark:border-gray-700', 'border-gray-200 dark:border-gray-700',
)}> )}>
@ -70,7 +70,7 @@ export default async function AdminPhotosPage({
{photos.map(photo => {photos.map(photo =>
<Fragment key={photo.id}> <Fragment key={photo.id}>
<PhotoTiny <PhotoTiny
className={cc( className={clsx(
'rounded-sm overflow-hidden', 'rounded-sm overflow-hidden',
'border border-gray-200 dark:border-gray-800', 'border border-gray-200 dark:border-gray-800',
)} )}
@ -82,7 +82,7 @@ export default async function AdminPhotosPage({
href={pathForPhoto(photo)} href={pathForPhoto(photo)}
className="lg:w-[50%] flex items-center gap-2" className="lg:w-[50%] flex items-center gap-2"
> >
<span className={cc( <span className={clsx(
'inline-flex items-center gap-2', 'inline-flex items-center gap-2',
photo.hidden && 'text-dim', photo.hidden && 'text-dim',
)}> )}>
@ -94,7 +94,7 @@ export default async function AdminPhotosPage({
/>} />}
</span> </span>
{photo.priorityOrder !== null && {photo.priorityOrder !== null &&
<span className={cc( <span className={clsx(
'text-xs leading-none px-1.5 py-1 rounded-sm', 'text-xs leading-none px-1.5 py-1 rounded-sm',
'dark:text-gray-300', 'dark:text-gray-300',
'bg-gray-100 dark:bg-gray-800', 'bg-gray-100 dark:bg-gray-800',
@ -102,14 +102,14 @@ export default async function AdminPhotosPage({
{photo.priorityOrder} {photo.priorityOrder}
</span>} </span>}
</Link> </Link>
<div className={cc( <div className={clsx(
'lg:w-[50%] uppercase', 'lg:w-[50%] uppercase',
'text-dim', 'text-dim',
)}> )}>
{photo.takenAtNaive} {photo.takenAtNaive}
</div> </div>
</div> </div>
<div className={cc( <div className={clsx(
'flex flex-nowrap', 'flex flex-nowrap',
'gap-2 sm:gap-3 items-center', 'gap-2 sm:gap-3 items-center',
)}> )}>

View File

@ -10,7 +10,7 @@ import PhotoTag from '@/tag/PhotoTag';
import { formatTag } from '@/tag'; import { formatTag } from '@/tag';
import EditButton from '@/admin/EditButton'; import EditButton from '@/admin/EditButton';
import { pathForAdminTagEdit } from '@/site/paths'; import { pathForAdminTagEdit } from '@/site/paths';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default async function AdminTagsPage() { export default async function AdminTagsPage() {
const tags = await getUniqueTagsHiddenCached(); const tags = await getUniqueTagsHiddenCached();
@ -29,7 +29,7 @@ export default async function AdminTagsPage() {
<div className="text-dim uppercase"> <div className="text-dim uppercase">
{photoQuantityText(count, false)} {photoQuantityText(count, false)}
</div> </div>
<div className={cc( <div className={clsx(
'flex flex-nowrap', 'flex flex-nowrap',
'gap-2 sm:gap-3 items-center', 'gap-2 sm:gap-3 items-center',
)}> )}>

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/vendors/fujifilm'; import { FILM_SIMULATION_FORM_INPUT_OPTIONS } from '@/vendors/fujifilm';
import PhotoFilmSimulation from import PhotoFilmSimulation from
'@/simulation/PhotoFilmSimulation'; '@/simulation/PhotoFilmSimulation';
@ -19,7 +19,7 @@ export default function FilmPage() {
return ( return (
<SiteGrid <SiteGrid
contentMain={<div className={cc( contentMain={<div className={clsx(
'flex items-center justify-center min-h-[30rem]', 'flex items-center justify-center min-h-[30rem]',
)}> )}>
<div className="w-[250px] scale-[2.5]"> <div className="w-[250px] scale-[2.5]">
@ -38,7 +38,7 @@ export default function FilmPage() {
<div>1/3200s</div> <div>1/3200s</div>
<div>ISO 125</div> <div>ISO 125</div>
</div> </div>
<div className={cc( <div className={clsx(
'absolute top-0 left-[-2px] right-0 bottom-0', 'absolute top-0 left-[-2px] right-0 bottom-0',
'bg-gradient-to-t', 'bg-gradient-to-t',
'from-white to-[rgba(255,255,255,0.5)]', 'from-white to-[rgba(255,255,255,0.5)]',

View File

@ -1,6 +1,6 @@
import { Analytics } from '@vercel/analytics/react'; import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/react'; import { SpeedInsights } from '@vercel/speed-insights/react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { IBM_Plex_Mono } from 'next/font/google'; import { IBM_Plex_Mono } from 'next/font/google';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config'; import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config';
@ -69,7 +69,7 @@ export default function RootLayout({
> >
<body className={ibmPlexMono.variable}> <body className={ibmPlexMono.variable}>
<ThemeProviderClient> <ThemeProviderClient>
<main className={cc( <main className={clsx(
'px-3 pb-3', 'px-3 pb-3',
'lg:px-6 lg:pb-6', 'lg:px-6 lg:pb-6',
)}> )}>

View File

@ -1,7 +1,7 @@
import { auth } from '@/auth'; import { auth } from '@/auth';
import SignInForm from '@/auth/SignInForm'; import SignInForm from '@/auth/SignInForm';
import { PATH_ADMIN } from '@/site/paths'; import { PATH_ADMIN } from '@/site/paths';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { redirect } from 'next/navigation'; import { redirect } from 'next/navigation';
export default async function SignInPage() { export default async function SignInPage() {
@ -12,7 +12,7 @@ export default async function SignInPage() {
} }
return ( return (
<div className={cc( <div className={clsx(
'fixed top-0 left-0 right-0 bottom-0', 'fixed top-0 left-0 right-0 bottom-0',
'flex items-center justify-center flex-col gap-8', 'flex items-center justify-center flex-col gap-8',
)}> )}>

View File

@ -3,7 +3,7 @@ import { pathForCamera } from '@/site/paths';
import { IoMdCamera } from 'react-icons/io'; import { IoMdCamera } from 'react-icons/io';
import { Camera } from '.'; import { Camera } from '.';
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink'; import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function PhotoCamera({ export default function PhotoCamera({
camera, camera,
@ -30,7 +30,7 @@ export default function PhotoCamera({
icon={showAppleIcon icon={showAppleIcon
? <AiFillApple ? <AiFillApple
title="Apple" title="Apple"
className={cc( className={clsx(
'text-icon', 'text-icon',
'translate-x-[-2.5px] translate-y-[2px]', 'translate-x-[-2.5px] translate-y-[2px]',
)} )}
@ -38,7 +38,7 @@ export default function PhotoCamera({
/> />
: <IoMdCamera : <IoMdCamera
size={13} size={13}
className={cc( className={clsx(
'text-icon', 'text-icon',
'translate-x-[-1px] translate-y-[3.5px]', 'translate-x-[-1px] translate-y-[3.5px]',
)} )}

View File

@ -2,7 +2,7 @@ import { ReactNode } from 'react';
import Link from 'next/link'; import Link from 'next/link';
import { FiArrowLeft } from 'react-icons/fi'; import { FiArrowLeft } from 'react-icons/fi';
import SiteGrid from './SiteGrid'; import SiteGrid from './SiteGrid';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Badge from './Badge'; import Badge from './Badge';
function AdminChildPage({ function AdminChildPage({
@ -23,11 +23,11 @@ function AdminChildPage({
contentMain={ contentMain={
<div className="space-y-6"> <div className="space-y-6">
{(backPath || breadcrumb || accessory) && {(backPath || breadcrumb || accessory) &&
<div className={cc( <div className={clsx(
'flex flex-wrap items-center gap-x-2 gap-y-3', 'flex flex-wrap items-center gap-x-2 gap-y-3',
'min-h-[2.25rem]', // min-h-9 equivalent 'min-h-[2.25rem]', // min-h-9 equivalent
)}> )}>
<div className={cc( <div className={clsx(
'flex flex-wrap items-center gap-x-1.5 sm:gap-x-3 gap-y-1', 'flex flex-wrap items-center gap-x-1.5 sm:gap-x-3 gap-y-1',
'flex-grow', 'flex-grow',
)}> )}>

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function Badge({ export default function Badge({
children, children,
@ -13,12 +13,12 @@ export default function Badge({
}) { }) {
const stylesForType = () => { const stylesForType = () => {
switch (type) { switch (type) {
case 'primary': return cc( case 'primary': return clsx(
'px-1.5 py-[0.3rem] rounded-md', 'px-1.5 py-[0.3rem] rounded-md',
'bg-gray-100/80 dark:bg-gray-900/80', 'bg-gray-100/80 dark:bg-gray-900/80',
'border border-gray-200/60 dark:border-gray-800/75' 'border border-gray-200/60 dark:border-gray-800/75'
); );
case 'secondary': return cc( case 'secondary': return clsx(
'px-[0.3rem] py-1 rounded-[0.25rem]', 'px-[0.3rem] py-1 rounded-[0.25rem]',
'bg-gray-300/30 dark:bg-gray-700/50', 'bg-gray-300/30 dark:bg-gray-700/50',
'text-medium', 'text-medium',
@ -29,7 +29,7 @@ export default function Badge({
} }
}; };
return ( return (
<span className={cc( <span className={clsx(
'leading-none', 'leading-none',
stylesForType(), stylesForType(),
uppercase && 'uppercase tracking-wider', uppercase && 'uppercase tracking-wider',

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function Checklist({ export default function Checklist({
title, title,
@ -12,7 +12,7 @@ export default function Checklist({
}) { }) {
return ( return (
<div> <div>
<div className={cc( <div className={clsx(
'flex items-center gap-3', 'flex items-center gap-3',
'text-gray-600 dark:text-gray-300', 'text-gray-600 dark:text-gray-300',
'pl-[18px] mb-3', 'pl-[18px] mb-3',
@ -22,7 +22,7 @@ export default function Checklist({
{title} {title}
</div> </div>
</div> </div>
<div className={cc( <div className={clsx(
'bg-white dark:bg-black', 'bg-white dark:bg-black',
'dark:text-gray-400', 'dark:text-gray-400',
'border border-gray-200 dark:border-gray-800 rounded-md', 'border border-gray-200 dark:border-gray-800 rounded-md',

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import StatusIcon from './StatusIcon'; import StatusIcon from './StatusIcon';
export default function ChecklistRow({ export default function ChecklistRow({
@ -16,7 +16,7 @@ export default function ChecklistRow({
children: ReactNode children: ReactNode
}) { }) {
return ( return (
<div className={cc( <div className={clsx(
'flex gap-2.5', 'flex gap-2.5',
'px-4 pt-2 pb-2.5', 'px-4 pt-2 pb-2.5',
)}> )}>

View File

@ -1,7 +1,7 @@
import Link from 'next/link'; import Link from 'next/link';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import Badge from './Badge'; import Badge from './Badge';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export interface EntityLinkExternalProps { export interface EntityLinkExternalProps {
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only' type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
@ -41,7 +41,7 @@ export default function EntityLink({
<Link <Link
href={href} href={href}
title={title} title={title}
className={cc( className={clsx(
'inline-flex gap-[0.23rem]', 'inline-flex gap-[0.23rem]',
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100', !badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
dim && 'text-dim', dim && 'text-dim',
@ -59,7 +59,7 @@ export default function EntityLink({
</span>} </span>}
</>} </>}
{icon && type !== 'text-only' && {icon && type !== 'text-only' &&
<span className={cc( <span className={clsx(
'flex-shrink-0', 'flex-shrink-0',
'text-dim inline-flex min-w-[0.9rem]', 'text-dim inline-flex min-w-[0.9rem]',
type === 'icon-first' && 'order-first', type === 'icon-first' && 'order-first',

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { BiErrorAlt } from 'react-icons/bi'; import { BiErrorAlt } from 'react-icons/bi';
export default function ErrorNote({ export default function ErrorNote({
@ -7,7 +7,7 @@ export default function ErrorNote({
children: React.ReactNode children: React.ReactNode
}) { }) {
return ( return (
<div className={cc( <div className={clsx(
'flex items-center gap-3', 'flex items-center gap-3',
'px-3 py-2 border', 'px-3 py-2 border',
'text-red-600 dark:text-red-500/90', 'text-red-600 dark:text-red-500/90',

View File

@ -3,7 +3,7 @@
import { LegacyRef } from 'react'; import { LegacyRef } from 'react';
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import Spinner from './Spinner'; import Spinner from './Spinner';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function FieldSetWithStatus({ export default function FieldSetWithStatus({
id, id,
@ -64,7 +64,7 @@ export default function FieldSetWithStatus({
name={id} name={id}
value={value} value={value}
onChange={e => onChange?.(e.target.value)} onChange={e => onChange?.(e.target.value)}
className={cc( className={clsx(
'w-full', 'w-full',
// Use special class because `select` can't be readonly // Use special class because `select` can't be readonly
readOnly || pending && 'disabled-select', readOnly || pending && 'disabled-select',
@ -93,7 +93,7 @@ export default function FieldSetWithStatus({
type={type} type={type}
autoComplete="off" autoComplete="off"
readOnly={readOnly || pending} readOnly={readOnly || pending}
className={cc(type === 'text' && 'w-full')} className={clsx(type === 'text' && 'w-full')}
autoCapitalize={!capitalize ? 'off' : undefined} autoCapitalize={!capitalize ? 'off' : undefined}
/>} />}
</div> </div>

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import AnimateItems from './AnimateItems'; import AnimateItems from './AnimateItems';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
@ -22,7 +22,7 @@ export default function HeaderList({
items={(title || icon items={(title || icon
? [<div ? [<div
key="header" key="header"
className={cc( className={clsx(
'text-gray-900', 'text-gray-900',
'dark:text-gray-100', 'dark:text-gray-100',
'flex items-center mb-0.5 gap-1', 'flex items-center mb-0.5 gap-1',

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Spinner, { SpinnerColor } from './Spinner'; import Spinner, { SpinnerColor } from './Spinner';
export default function IconButton({ export default function IconButton({
@ -19,7 +19,7 @@ export default function IconButton({
spinnerSize?: number spinnerSize?: number
}) { }) {
return ( return (
<span className={cc( <span className={clsx(
className, className,
'relative inline-flex items-center', 'relative inline-flex items-center',
'w-[1rem] h-[1.1rem]', 'w-[1rem] h-[1.1rem]',
@ -27,7 +27,7 @@ export default function IconButton({
{!isLoading {!isLoading
? <button ? <button
onClick={onClick} onClick={onClick}
className={cc( className={clsx(
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'p-0 border-none shadow-none', 'p-0 border-none shadow-none',
'active:bg-transparent bg-transparent dark:bg-transparent', 'active:bg-transparent bg-transparent dark:bg-transparent',
@ -38,7 +38,7 @@ export default function IconButton({
> >
{icon} {icon}
</button> </button>
: <span className={cc( : <span className={clsx(
'inline-flex items-center justify-center', 'inline-flex items-center justify-center',
'h-full w-full', 'h-full w-full',
)}> )}>

View File

@ -3,7 +3,7 @@
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import IconButton from './IconButton'; import IconButton from './IconButton';
import { useEffect, useState, useTransition } from 'react'; import { useEffect, useState, useTransition } from 'react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { SpinnerColor } from './Spinner'; import { SpinnerColor } from './Spinner';
export default function IconPathButton({ export default function IconPathButton({
@ -57,7 +57,7 @@ export default function IconPathButton({
} }
})} })}
isLoading={shouldShowLoader} isLoading={shouldShowLoader}
className={cc( className={clsx(
'translate-y-[-0.5px]', 'translate-y-[-0.5px]',
'active:translate-y-[1px]', 'active:translate-y-[1px]',
'text-medium', 'text-medium',

View File

@ -4,7 +4,7 @@ import { blobToImage } from '@/utility/blob';
import { useRef, useState } from 'react'; import { useRef, useState } from 'react';
import { CopyExif } from '@/lib/CopyExif'; import { CopyExif } from '@/lib/CopyExif';
import exifr from 'exifr'; import exifr from 'exifr';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Spinner from './Spinner'; import Spinner from './Spinner';
import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo'; import { ACCEPTED_PHOTO_FILE_TYPES } from '@/photo';
import { FiUploadCloud } from 'react-icons/fi'; import { FiUploadCloud } from 'react-icons/fi';
@ -47,13 +47,13 @@ export default function ImageInput({
<div className="flex items-center gap-2 sm:gap-4"> <div className="flex items-center gap-2 sm:gap-4">
<label <label
htmlFor={INPUT_ID} htmlFor={INPUT_ID}
className={cc( className={clsx(
'shrink-0 select-none text-main', 'shrink-0 select-none text-main',
loading && 'pointer-events-none cursor-not-allowed', loading && 'pointer-events-none cursor-not-allowed',
)} )}
> >
<span <span
className={cc( className={clsx(
'button primary normal-case', 'button primary normal-case',
loading && 'disabled' loading && 'disabled'
)} )}
@ -212,7 +212,7 @@ export default function ImageInput({
</div> </div>
<canvas <canvas
ref={ref} ref={ref}
className={cc( className={clsx(
'bg-gray-50 dark:bg-gray-900/50 rounded-md', 'bg-gray-50 dark:bg-gray-900/50 rounded-md',
'border border-gray-200 dark:border-gray-800', 'border border-gray-200 dark:border-gray-800',
'w-[400px]', 'w-[400px]',

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
export default function InfoBlock({ export default function InfoBlock({
@ -21,7 +21,7 @@ export default function InfoBlock({
}; };
return ( return (
<div className={cc( <div className={clsx(
'flex flex-col items-center justify-center', 'flex flex-col items-center justify-center',
'rounded-lg border', 'rounded-lg border',
'bg-gray-50 border-gray-200', 'bg-gray-50 border-gray-200',
@ -29,7 +29,7 @@ export default function InfoBlock({
getPaddingClasses(), getPaddingClasses(),
className, className,
)}> )}>
<div className={cc( <div className={clsx(
'flex flex-col justify-center w-full', 'flex flex-col justify-center w-full',
centered && 'items-center', centered && 'items-center',
'space-y-4', 'space-y-4',

View File

@ -2,7 +2,7 @@
import { ReactNode, useEffect, useRef, useState } from 'react'; import { ReactNode, useEffect, useRef, useState } from 'react';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import useClickInsideOutside from '@/utility/useClickInsideOutside'; import useClickInsideOutside from '@/utility/useClickInsideOutside';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import AnimateItems from './AnimateItems'; import AnimateItems from './AnimateItems';
@ -40,7 +40,7 @@ export default function Modal({
return ( return (
<motion.div <motion.div
className={cc( className={clsx(
'fixed inset-0 z-50 flex items-center justify-center', 'fixed inset-0 z-50 flex items-center justify-center',
'bg-black', 'bg-black',
)} )}
@ -54,7 +54,7 @@ export default function Modal({
duration={0.3} duration={0.3}
items={[<div items={[<div
key="modalContent" key="modalContent"
className={cc( className={clsx(
'p-3 rounded-lg', 'p-3 rounded-lg',
'bg-white dark:bg-black', 'bg-white dark:bg-black',
'dark:border dark:border-gray-800', 'dark:border dark:border-gray-800',

View File

@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { BiError } from 'react-icons/bi'; import { BiError } from 'react-icons/bi';
import Spinner from '@/components/Spinner'; import Spinner from '@/components/Spinner';
@ -49,7 +49,7 @@ export default function OGTile({
return ( return (
<Link <Link
href={path} href={path}
className={cc( className={clsx(
'group', 'group',
'block w-full rounded-md overflow-hidden', 'block w-full rounded-md overflow-hidden',
'border shadow-sm', 'border shadow-sm',
@ -62,14 +62,14 @@ export default function OGTile({
style={{ aspectRatio }} style={{ aspectRatio }}
> >
{loadingState === 'loading' && {loadingState === 'loading' &&
<div className={cc( <div className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-10', 'absolute top-0 left-0 right-0 bottom-0 z-10',
'flex items-center justify-center', 'flex items-center justify-center',
)}> )}>
<Spinner size={40} /> <Spinner size={40} />
</div>} </div>}
{loadingState === 'failed' && {loadingState === 'failed' &&
<div className={cc( <div className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-[11]', 'absolute top-0 left-0 right-0 bottom-0 z-[11]',
'flex items-center justify-center', 'flex items-center justify-center',
'text-red-400', 'text-red-400',
@ -79,7 +79,7 @@ export default function OGTile({
{(loadingState === 'loading' || loadingState === 'loaded') && {(loadingState === 'loading' || loadingState === 'loaded') &&
<img <img
alt={title} alt={title}
className={cc( className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-0', 'absolute top-0 left-0 right-0 bottom-0 z-0',
'w-full', 'w-full',
loadingState === 'loading' && 'opacity-0', loadingState === 'loading' && 'opacity-0',
@ -109,7 +109,7 @@ export default function OGTile({
}} }}
/>} />}
</div> </div>
<div className={cc( <div className={clsx(
'md:text-lg', 'md:text-lg',
'flex flex-col gap-1 p-3', 'flex flex-col gap-1 p-3',
'font-sans leading-none', 'font-sans leading-none',

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { BiLogoGithub } from 'react-icons/bi'; import { BiLogoGithub } from 'react-icons/bi';
@ -11,7 +11,7 @@ export default function RepoLink() {
<Link <Link
href="http://github.com/sambecker/exif-photo-blog" href="http://github.com/sambecker/exif-photo-blog"
target="_blank" target="_blank"
className={cc( className={clsx(
'flex items-center gap-1', 'flex items-center gap-1',
'text-black dark:text-white', 'text-black dark:text-white',
'hover:underline', 'hover:underline',

View File

@ -2,7 +2,7 @@
import Modal from '@/components/Modal'; import Modal from '@/components/Modal';
import { TbPhotoShare } from 'react-icons/tb'; import { TbPhotoShare } from 'react-icons/tb';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { BiCopy } from 'react-icons/bi'; import { BiCopy } from 'react-icons/bi';
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { shortenUrl } from '@/utility/url'; import { shortenUrl } from '@/utility/url';
@ -22,7 +22,7 @@ export default function ShareModal({
return ( return (
<Modal onClosePath={pathClose}> <Modal onClosePath={pathClose}>
<div className="space-y-3 md:space-y-4 w-full"> <div className="space-y-3 md:space-y-4 w-full">
<div className={cc( <div className={clsx(
'flex items-center gap-x-3', 'flex items-center gap-x-3',
'text-xl md:text-3xl leading-snug', 'text-xl md:text-3xl leading-snug',
)}> )}>
@ -32,7 +32,7 @@ export default function ShareModal({
</div> </div>
</div> </div>
{children} {children}
<div className={cc( <div className={clsx(
'rounded-md', 'rounded-md',
'w-full overflow-hidden', 'w-full overflow-hidden',
'flex items-center justify-stretch', 'flex items-center justify-stretch',
@ -42,7 +42,7 @@ export default function ShareModal({
{shortenUrl(pathShare)} {shortenUrl(pathShare)}
</div> </div>
<div <div
className={cc( className={clsx(
'p-3 border-l', 'p-3 border-l',
'border-gray-200 bg-gray-100 active:bg-gray-200', 'border-gray-200 bg-gray-100 active:bg-gray-200',
// eslint-disable-next-line max-len // eslint-disable-next-line max-len

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function SiteGrid({ export default function SiteGrid({
className, className,
@ -14,7 +14,7 @@ export default function SiteGrid({
sideHiddenOnMobile?: boolean sideHiddenOnMobile?: boolean
}) { }) {
return ( return (
<div className={cc( <div className={clsx(
className, className,
'grid', 'grid',
'grid-cols-1 md:grid-cols-12', 'grid-cols-1 md:grid-cols-12',
@ -22,14 +22,14 @@ export default function SiteGrid({
'gap-y-4', 'gap-y-4',
'max-w-7xl', 'max-w-7xl',
)}> )}>
<div className={cc( <div className={clsx(
'col-span-1 md:col-span-9', 'col-span-1 md:col-span-9',
sideFirstOnMobile && 'order-2 md:order-none', sideFirstOnMobile && 'order-2 md:order-none',
)}> )}>
{contentMain} {contentMain}
</div> </div>
{contentSide && {contentSide &&
<div className={cc( <div className={clsx(
'col-span-1 md:col-span-3', 'col-span-1 md:col-span-3',
sideFirstOnMobile && 'order-1 md:order-none', sideFirstOnMobile && 'order-1 md:order-none',
sideHiddenOnMobile && 'hidden md:block', sideHiddenOnMobile && 'hidden md:block',

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
const SIZE_DEFAULT = 12; const SIZE_DEFAULT = 12;
@ -15,7 +15,7 @@ export default function Spinner({
}) { }) {
return ( return (
<span <span
className={cc( className={clsx(
className, className,
color === 'light-gray' && color === 'light-gray' &&
'text-gray-300 dark:text-gray-600', 'text-gray-300 dark:text-gray-600',

View File

@ -3,7 +3,7 @@
import { HTMLProps, useEffect, useRef } from 'react'; import { HTMLProps, useEffect, useRef } from 'react';
import { useFormStatus } from 'react-dom'; import { useFormStatus } from 'react-dom';
import Spinner, { SpinnerColor } from './Spinner'; import Spinner, { SpinnerColor } from './Spinner';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { toastSuccess } from '@/toast'; import { toastSuccess } from '@/toast';
interface Props extends HTMLProps<HTMLButtonElement> { interface Props extends HTMLProps<HTMLButtonElement> {
@ -43,7 +43,7 @@ export default function SubmitButtonWithStatus({
<button <button
type="submit" type="submit"
disabled={disabled} disabled={disabled}
className={cc( className={clsx(
className, className,
'inline-flex items-center gap-2', 'inline-flex items-center gap-2',
styleAsLink && 'link', styleAsLink && 'link',
@ -51,7 +51,7 @@ export default function SubmitButtonWithStatus({
{...buttonProps} {...buttonProps}
> >
{(icon || pending) && {(icon || pending) &&
<span className={cc( <span className={clsx(
'h-4', 'h-4',
'min-w-[1rem]', 'min-w-[1rem]',
'inline-flex justify-center sm:justify-normal', 'inline-flex justify-center sm:justify-normal',
@ -62,7 +62,7 @@ export default function SubmitButtonWithStatus({
? <Spinner size={14} color={spinnerColor} /> ? <Spinner size={14} color={spinnerColor} />
: icon} : icon}
</span>} </span>}
{children && <span className={cc( {children && <span className={clsx(
icon !== undefined && 'hidden sm:inline-block', icon !== undefined && 'hidden sm:inline-block',
)}> )}>
{children} {children}

View File

@ -1,5 +1,5 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function Switcher({ export default function Switcher({
children, children,
@ -7,7 +7,7 @@ export default function Switcher({
children: ReactNode children: ReactNode
}) { }) {
return ( return (
<div className={cc( <div className={clsx(
'flex divide-x', 'flex divide-x',
'divide-gray-300 dark:divide-gray-800', 'divide-gray-300 dark:divide-gray-800',
'border rounded-[0.25rem]', 'border rounded-[0.25rem]',

View File

@ -1,5 +1,5 @@
import Link from 'next/link'; import Link from 'next/link';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function SwitcherItem({ export default function SwitcherItem({
icon, icon,
@ -16,7 +16,7 @@ export default function SwitcherItem({
active?: boolean active?: boolean
noPadding?: boolean noPadding?: boolean
}) { }) {
const className = cc( const className = clsx(
classNameProp, classNameProp,
'py-0.5 px-1.5', 'py-0.5 px-1.5',
'cursor-pointer', 'cursor-pointer',

View File

@ -3,7 +3,7 @@ import { Photo, PhotoDateRange } from '.';
import PhotoLarge from './PhotoLarge'; import PhotoLarge from './PhotoLarge';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import PhotoGrid from './PhotoGrid'; import PhotoGrid from './PhotoGrid';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import PhotoLinks from './PhotoLinks'; import PhotoLinks from './PhotoLinks';
import TagHeader from '@/tag/TagHeader'; import TagHeader from '@/tag/TagHeader';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
@ -95,7 +95,7 @@ export default function PhotoDetailPage({
tag={tag} tag={tag}
animateOnFirstLoadOnly animateOnFirstLoadOnly
/>} />}
contentSide={<div className={cc( contentSide={<div className={clsx(
'grid grid-cols-2', 'grid grid-cols-2',
'gap-0.5 sm:gap-1', 'gap-0.5 sm:gap-1',
'md:flex md:gap-4', 'md:flex md:gap-4',

View File

@ -10,7 +10,7 @@ import NextImage from 'next/image';
import { createPhotoAction, updatePhotoAction } from './actions'; import { createPhotoAction, updatePhotoAction } from './actions';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
import Link from 'next/link'; import Link from 'next/link';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import CanvasBlurCapture from '@/components/CanvasBlurCapture'; import CanvasBlurCapture from '@/components/CanvasBlurCapture';
import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths'; import { PATH_ADMIN_PHOTOS, PATH_ADMIN_UPLOADS } from '@/site/paths';
import { import {
@ -106,7 +106,7 @@ export default function PhotoForm({
<NextImage <NextImage
alt="Upload" alt="Upload"
src={url} src={url}
className={cc( className={clsx(
'border rounded-md overflow-hidden', 'border rounded-md overflow-hidden',
'border-gray-200 dark:border-gray-700' 'border-gray-200 dark:border-gray-700'
)} )}
@ -124,7 +124,7 @@ export default function PhotoForm({
<img <img
alt="blur" alt="blur"
src={formData.blurData} src={formData.blurData}
className={cc( className={clsx(
'border rounded-md overflow-hidden', 'border rounded-md overflow-hidden',
'border-gray-200 dark:border-gray-700' 'border-gray-200 dark:border-gray-700'
)} )}

View File

@ -1,6 +1,6 @@
import { Photo } from '.'; import { Photo } from '.';
import PhotoSmall from './PhotoSmall'; import PhotoSmall from './PhotoSmall';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import AnimateItems from '@/components/AnimateItems'; import AnimateItems from '@/components/AnimateItems';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
import MorePhotos from '@/photo/MorePhotos'; import MorePhotos from '@/photo/MorePhotos';
@ -37,7 +37,7 @@ export default function PhotoGrid({
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<AnimateItems <AnimateItems
className={cc( className={clsx(
'grid gap-0.5 sm:gap-1', 'grid gap-0.5 sm:gap-1',
small small
? 'grid-cols-3 xs:grid-cols-6' ? 'grid-cols-3 xs:grid-cols-6'
@ -56,7 +56,7 @@ export default function PhotoGrid({
<div <div
key={photo.id} key={photo.id}
className={GRID_ASPECT_RATIO !== 0 className={GRID_ASPECT_RATIO !== 0
? cc( ? clsx(
'aspect-square', 'aspect-square',
'overflow-hidden', 'overflow-hidden',
'[&>*]:flex [&>*]:w-full [&>*]:h-full', '[&>*]:flex [&>*]:w-full [&>*]:h-full',

View File

@ -1,7 +1,7 @@
import { Photo, photoHasCameraData, photoHasExifData, titleForPhoto } from '.'; import { Photo, photoHasCameraData, photoHasExifData, titleForPhoto } from '.';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import ImageLarge from '@/components/ImageLarge'; import ImageLarge from '@/components/ImageLarge';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { pathForPhoto, pathForPhotoShare } from '@/site/paths'; import { pathForPhoto, pathForPhotoShare } from '@/site/paths';
import PhotoTags from '@/tag/PhotoTags'; import PhotoTags from '@/tag/PhotoTags';
@ -38,7 +38,7 @@ export default function PhotoLarge({
const camera = cameraFromPhoto(photo); const camera = cameraFromPhoto(photo);
const renderMiniGrid = (children: JSX.Element, rightPadding = true) => const renderMiniGrid = (children: JSX.Element, rightPadding = true) =>
<div className={cc( <div className={clsx(
'flex gap-y-4', 'flex gap-y-4',
'flex-col sm:flex-row md:flex-col', 'flex-col sm:flex-row md:flex-col',
'[&>*]:sm:flex-grow', '[&>*]:sm:flex-grow',
@ -60,7 +60,7 @@ export default function PhotoLarge({
priority={priority} priority={priority}
/>} />}
contentSide={ contentSide={
<div className={cc( <div className={clsx(
'leading-snug', 'leading-snug',
'sticky top-4 self-start', 'sticky top-4 self-start',
'grid grid-cols-2 md:grid-cols-1', 'grid grid-cols-2 md:grid-cols-1',
@ -115,11 +115,11 @@ export default function PhotoLarge({
<li>{photo.isoFormatted}</li> <li>{photo.isoFormatted}</li>
<li>{photo.exposureCompensationFormatted ?? '—'}</li> <li>{photo.exposureCompensationFormatted ?? '—'}</li>
</ul>} </ul>}
<div className={cc( <div className={clsx(
'flex gap-y-4', 'flex gap-y-4',
'flex-col sm:flex-row md:flex-col', 'flex-col sm:flex-row md:flex-col',
)}> )}>
<div className={cc( <div className={clsx(
'grow uppercase', 'grow uppercase',
'text-medium', 'text-medium',
)}> )}>

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { Photo } from '.'; import { Photo } from '.';
import PhotoGrid from './PhotoGrid'; import PhotoGrid from './PhotoGrid';
import Link from 'next/link'; import Link from 'next/link';
@ -23,7 +23,7 @@ export default function PhotoLightbox({
const showOverageTile = countNotShown > 0; const showOverageTile = countNotShown > 0;
return ( return (
<div className={cc( <div className={clsx(
'border dark:border-gray-800 p-1.5 lg:p-2 rounded-md', 'border dark:border-gray-800 p-1.5 lg:p-2 rounded-md',
'bg-gray-50 dark:bg-gray-950', 'bg-gray-50 dark:bg-gray-950',
)}> )}>
@ -33,7 +33,7 @@ export default function PhotoLightbox({
additionalTile={showOverageTile additionalTile={showOverageTile
? <Link ? <Link
href={moreLink} href={moreLink}
className={cc( className={clsx(
'flex flex-col items-center justify-center', 'flex flex-col items-center justify-center',
'gap-0.5 lg:gap-1', 'gap-0.5 lg:gap-1',
)} )}

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { Photo, PhotoDateRange, dateRangeForPhotos } from '.'; import { Photo, PhotoDateRange, dateRangeForPhotos } from '.';
import ShareButton from '@/components/ShareButton'; import ShareButton from '@/components/ShareButton';
import AnimateItems from '@/components/AnimateItems'; import AnimateItems from '@/components/AnimateItems';
@ -37,19 +37,19 @@ export default function PhotoSetHeader({
animateOnFirstLoadOnly animateOnFirstLoadOnly
items={[<div items={[<div
key="PhotosHeader" key="PhotosHeader"
className={cc( className={clsx(
'grid gap-0.5 sm:gap-1 items-start', 'grid gap-0.5 sm:gap-1 items-start',
HIGH_DENSITY_GRID HIGH_DENSITY_GRID
? 'xs:grid-cols-2 sm:grid-cols-4 lg:grid-cols-5' ? 'xs:grid-cols-2 sm:grid-cols-4 lg:grid-cols-5'
: 'xs:grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4', : 'xs:grid-cols-2 sm:grid-cols-4 md:grid-cols-3 lg:grid-cols-4',
)}> )}>
<span className={cc( <span className={clsx(
'inline-flex', 'inline-flex',
HIGH_DENSITY_GRID && 'sm:col-span-2', HIGH_DENSITY_GRID && 'sm:col-span-2',
)}> )}>
{entity} {entity}
</span> </span>
<span className={cc( <span className={clsx(
'inline-flex gap-2 items-center self-start', 'inline-flex gap-2 items-center self-start',
'uppercase text-dim', 'uppercase text-dim',
HIGH_DENSITY_GRID HIGH_DENSITY_GRID
@ -63,7 +63,7 @@ export default function PhotoSetHeader({
{selectedPhotoIndex === undefined && {selectedPhotoIndex === undefined &&
<ShareButton path={sharePath} dim />} <ShareButton path={sharePath} dim />}
</span> </span>
<span className={cc( <span className={clsx(
'hidden sm:inline-block', 'hidden sm:inline-block',
'text-right uppercase', 'text-right uppercase',
'text-dim', 'text-dim',

View File

@ -1,7 +1,7 @@
import { Photo, titleForPhoto } from '.'; import { Photo, titleForPhoto } from '.';
import ImageSmall from '@/components/ImageSmall'; import ImageSmall from '@/components/ImageSmall';
import Link from 'next/link'; import Link from 'next/link';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { pathForPhoto } from '@/site/paths'; import { pathForPhoto } from '@/site/paths';
import { Camera } from '@/camera'; import { Camera } from '@/camera';
import { FilmSimulation } from '@/simulation'; import { FilmSimulation } from '@/simulation';
@ -22,7 +22,7 @@ export default function PhotoSmall({
return ( return (
<Link <Link
href={pathForPhoto(photo, tag, camera, simulation)} href={pathForPhoto(photo, tag, camera, simulation)}
className={cc( className={clsx(
'active:brightness-75', 'active:brightness-75',
selected && 'brightness-50', selected && 'brightness-50',
)} )}

View File

@ -1,7 +1,7 @@
import { Photo, titleForPhoto } from '.'; import { Photo, titleForPhoto } from '.';
import ImageTiny from '@/components/ImageTiny'; import ImageTiny from '@/components/ImageTiny';
import Link from 'next/link'; import Link from 'next/link';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { pathForPhoto } from '@/site/paths'; import { pathForPhoto } from '@/site/paths';
export default function PhotoTiny({ export default function PhotoTiny({
@ -18,7 +18,7 @@ export default function PhotoTiny({
return ( return (
<Link <Link
href={pathForPhoto(photo, tag)} href={pathForPhoto(photo, tag)}
className={cc( className={clsx(
className, className,
'active:brightness-75', 'active:brightness-75',
selected && 'brightness-50', selected && 'brightness-50',

View File

@ -6,7 +6,7 @@ import { useRouter } from 'next/navigation';
import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/site/paths'; import { PATH_ADMIN_UPLOADS, pathForAdminUploadUrl } from '@/site/paths';
import ImageInput from '../components/ImageInput'; import ImageInput from '../components/ImageInput';
import { MAX_IMAGE_SIZE } from '@/services/next-image'; import { MAX_IMAGE_SIZE } from '@/services/next-image';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
export default function PhotoUpload({ export default function PhotoUpload({
shouldResize, shouldResize,
@ -25,7 +25,7 @@ export default function PhotoUpload({
const router = useRouter(); const router = useRouter();
return ( return (
<div className={cc( <div className={clsx(
'space-y-4', 'space-y-4',
isUploading && 'cursor-not-allowed', isUploading && 'cursor-not-allowed',
)}> )}>

View File

@ -2,7 +2,7 @@ import InfoBlock from '@/components/InfoBlock';
import SiteGrid from '@/components/SiteGrid'; import SiteGrid from '@/components/SiteGrid';
import { IS_SITE_READY } from '@/site/config'; import { IS_SITE_READY } from '@/site/config';
import SiteChecklist from '@/site/SiteChecklist'; import SiteChecklist from '@/site/SiteChecklist';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { HiOutlinePhotograph } from 'react-icons/hi'; import { HiOutlinePhotograph } from 'react-icons/hi';
@ -15,7 +15,7 @@ export default function PhotosEmptyState() {
className="text-medium" className="text-medium"
size={24} size={24}
/> />
<div className={cc( <div className={clsx(
'font-bold text-2xl', 'font-bold text-2xl',
'text-gray-700 dark:text-gray-200', 'text-gray-700 dark:text-gray-200',
)}> )}>
@ -40,7 +40,7 @@ export default function PhotosEmptyState() {
2. Change the name of this blog and other configuration 2. Change the name of this blog and other configuration
by editing environment variables referenced in by editing environment variables referenced in
{' '} {' '}
<span className={cc( <span className={clsx(
'px-1.5', 'px-1.5',
'bg-gray-100', 'bg-gray-100',
'border border-gray-200 dark:border-gray-700', 'border border-gray-200 dark:border-gray-700',

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import Link from 'next/link'; import Link from 'next/link';
import { useSession } from 'next-auth/react'; import { useSession } from 'next-auth/react';
import ThemeSwitcher from '@/site/ThemeSwitcher'; import ThemeSwitcher from '@/site/ThemeSwitcher';
@ -10,7 +10,7 @@ import { isPathSignIn } from '@/site/paths';
import { signOutAction } from '@/auth/action'; import { signOutAction } from '@/auth/action';
import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus'; import SubmitButtonWithStatus from '@/components/SubmitButtonWithStatus';
const LINK_STYLE = cc( const LINK_STYLE = clsx(
'cursor-pointer', 'cursor-pointer',
'hover:text-gray-300', 'hover:text-gray-300',
'hover:dark:text-gray-600', 'hover:dark:text-gray-600',
@ -23,7 +23,7 @@ export default function FooterAuth() {
return ( return (
<SiteGrid <SiteGrid
contentMain={<div className={cc( contentMain={<div className={clsx(
'flex items-center', 'flex items-center',
'my-8', 'my-8',
'text-dim', 'text-dim',

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import SiteGrid from '../components/SiteGrid'; import SiteGrid from '../components/SiteGrid';
import ThemeSwitcher from '@/site/ThemeSwitcher'; import ThemeSwitcher from '@/site/ThemeSwitcher';
import { signOut } from 'next-auth/react'; import { signOut } from 'next-auth/react';
@ -15,7 +15,7 @@ export default function FooterStatic({
}) { }) {
return ( return (
<SiteGrid <SiteGrid
contentMain={<div className={cc( contentMain={<div className={clsx(
'my-8', 'my-8',
'flex items-center', 'flex items-center',
'text-dim', 'text-dim',
@ -31,7 +31,7 @@ export default function FooterStatic({
<RepoLink />} <RepoLink />}
{showSignOut && {showSignOut &&
<div <div
className={cc( className={clsx(
'cursor-pointer', 'cursor-pointer',
'hover:text-gray-600 dark:hover:text-gray-400', 'hover:text-gray-600 dark:hover:text-gray-400',
)} )}

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import Link from 'next/link'; import Link from 'next/link';
import SiteGrid from '../components/SiteGrid'; import SiteGrid from '../components/SiteGrid';
@ -55,7 +55,7 @@ export default function Nav({ showTextLinks }: { showTextLinks?: boolean }) {
items={showNav items={showNav
? [<div ? [<div
key="nav" key="nav"
className={cc( className={clsx(
'flex items-center', 'flex items-center',
'w-full min-h-[4rem]', 'w-full min-h-[4rem]',
'leading-none', 'leading-none',

View File

@ -2,7 +2,7 @@
import { ComponentProps, ReactNode, useTransition } from 'react'; import { ComponentProps, ReactNode, useTransition } from 'react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import ChecklistRow from '../components/ChecklistRow'; import ChecklistRow from '../components/ChecklistRow';
import { FiExternalLink } from 'react-icons/fi'; import { FiExternalLink } from 'react-icons/fi';
import { import {
@ -60,7 +60,7 @@ export default function SiteChecklistClient({
<a {...{ <a {...{
href, href,
...external && { target: '_blank', rel: 'noopener noreferrer' }, ...external && { target: '_blank', rel: 'noopener noreferrer' },
className: cc( className: clsx(
'underline hover:no-underline', 'underline hover:no-underline',
), ),
}}> }}>
@ -79,7 +79,7 @@ export default function SiteChecklistClient({
const renderCopyButton = (label: string, text: string, subtle?: boolean) => const renderCopyButton = (label: string, text: string, subtle?: boolean) =>
<IconButton <IconButton
icon={<BiCopy size={15} />} icon={<BiCopy size={15} />}
className={cc(subtle && 'text-gray-300 dark:text-gray-700')} className={clsx(subtle && 'text-gray-300 dark:text-gray-700')}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(text); navigator.clipboard.writeText(text);
toastSuccess(`${label} copied to clipboard`); toastSuccess(`${label} copied to clipboard`);
@ -92,7 +92,7 @@ export default function SiteChecklistClient({
className="overflow-x-scroll overflow-y-hidden" className="overflow-x-scroll overflow-y-hidden"
> >
<span className="inline-flex items-center gap-1"> <span className="inline-flex items-center gap-1">
<span className={cc( <span className={clsx(
'text-medium', 'text-medium',
'rounded-sm', 'rounded-sm',
'bg-gray-100 dark:bg-gray-800', 'bg-gray-100 dark:bg-gray-800',

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { cc } from '@/utility/css'; import { clsx } from 'clsx';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
@ -11,7 +11,7 @@ export default function ToasterWithThemes() {
theme={theme as 'system' | 'light' | 'dark'} theme={theme as 'system' | 'light' | 'dark'}
toastOptions={{ toastOptions={{
classNames: { classNames: {
toast: cc( toast: clsx(
'font-mono font-normal', 'font-mono font-normal',
'!border-gray-200 dark:!border-gray-800', '!border-gray-200 dark:!border-gray-800',
), ),

View File

@ -1,6 +0,0 @@
export const cc = (
...classes: (string | boolean | undefined)[]
): string =>
classes
.filter(s => typeof s === 'string' && s.length)
.join(' ');