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",
"autoprefixer": "10.4.16",
"camelcase-keys": "^9.1.2",
"clsx": "^2.1.0",
"date-fns": "^3.0.6",
"eslint": "8.56.0",
"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';
export default function AdminGrid ({
@ -15,7 +15,7 @@ export default function AdminGrid ({
</div>}
{/* py-[1px] fixes Safari vertical scroll bug */}
<div className="min-w-[14rem] overflow-x-scroll py-[1px]">
<div className={cc(
<div className={clsx(
'w-full',
'grid grid-cols-[auto_1fr_auto] ',
'gap-2 sm:gap-3 items-center',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { Analytics } from '@vercel/analytics/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 { Metadata } from 'next';
import { BASE_URL, SITE_DESCRIPTION, SITE_TITLE } from '@/site/config';
@ -69,7 +69,7 @@ export default function RootLayout({
>
<body className={ibmPlexMono.variable}>
<ThemeProviderClient>
<main className={cc(
<main className={clsx(
'px-3 pb-3',
'lg:px-6 lg:pb-6',
)}>

View File

@ -1,7 +1,7 @@
import { auth } from '@/auth';
import SignInForm from '@/auth/SignInForm';
import { PATH_ADMIN } from '@/site/paths';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
import { redirect } from 'next/navigation';
export default async function SignInPage() {
@ -12,7 +12,7 @@ export default async function SignInPage() {
}
return (
<div className={cc(
<div className={clsx(
'fixed top-0 left-0 right-0 bottom-0',
'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 { Camera } from '.';
import EntityLink, { EntityLinkExternalProps } from '@/components/EntityLink';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
export default function PhotoCamera({
camera,
@ -30,7 +30,7 @@ export default function PhotoCamera({
icon={showAppleIcon
? <AiFillApple
title="Apple"
className={cc(
className={clsx(
'text-icon',
'translate-x-[-2.5px] translate-y-[2px]',
)}
@ -38,7 +38,7 @@ export default function PhotoCamera({
/>
: <IoMdCamera
size={13}
className={cc(
className={clsx(
'text-icon',
'translate-x-[-1px] translate-y-[3.5px]',
)}

View File

@ -2,7 +2,7 @@ import { ReactNode } from 'react';
import Link from 'next/link';
import { FiArrowLeft } from 'react-icons/fi';
import SiteGrid from './SiteGrid';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
import Badge from './Badge';
function AdminChildPage({
@ -23,11 +23,11 @@ function AdminChildPage({
contentMain={
<div className="space-y-6">
{(backPath || breadcrumb || accessory) &&
<div className={cc(
<div className={clsx(
'flex flex-wrap items-center gap-x-2 gap-y-3',
'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-grow',
)}>

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
import Link from 'next/link';
import { ReactNode } from 'react';
import Badge from './Badge';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
export interface EntityLinkExternalProps {
type?: 'icon-last' | 'icon-first' | 'icon-only' | 'text-only'
@ -41,7 +41,7 @@ export default function EntityLink({
<Link
href={href}
title={title}
className={cc(
className={clsx(
'inline-flex gap-[0.23rem]',
!badged && 'text-main hover:text-gray-900 dark:hover:text-gray-100',
dim && 'text-dim',
@ -59,7 +59,7 @@ export default function EntityLink({
</span>}
</>}
{icon && type !== 'text-only' &&
<span className={cc(
<span className={clsx(
'flex-shrink-0',
'text-dim inline-flex min-w-[0.9rem]',
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';
export default function ErrorNote({
@ -7,7 +7,7 @@ export default function ErrorNote({
children: React.ReactNode
}) {
return (
<div className={cc(
<div className={clsx(
'flex items-center gap-3',
'px-3 py-2 border',
'text-red-600 dark:text-red-500/90',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
'use client';
import { useEffect, useState } from 'react';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
import Link from 'next/link';
import { BiError } from 'react-icons/bi';
import Spinner from '@/components/Spinner';
@ -49,7 +49,7 @@ export default function OGTile({
return (
<Link
href={path}
className={cc(
className={clsx(
'group',
'block w-full rounded-md overflow-hidden',
'border shadow-sm',
@ -62,14 +62,14 @@ export default function OGTile({
style={{ aspectRatio }}
>
{loadingState === 'loading' &&
<div className={cc(
<div className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-10',
'flex items-center justify-center',
)}>
<Spinner size={40} />
</div>}
{loadingState === 'failed' &&
<div className={cc(
<div className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-[11]',
'flex items-center justify-center',
'text-red-400',
@ -79,7 +79,7 @@ export default function OGTile({
{(loadingState === 'loading' || loadingState === 'loaded') &&
<img
alt={title}
className={cc(
className={clsx(
'absolute top-0 left-0 right-0 bottom-0 z-0',
'w-full',
loadingState === 'loading' && 'opacity-0',
@ -109,7 +109,7 @@ export default function OGTile({
}}
/>}
</div>
<div className={cc(
<div className={clsx(
'md:text-lg',
'flex flex-col gap-1 p-3',
'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 { BiLogoGithub } from 'react-icons/bi';
@ -11,7 +11,7 @@ export default function RepoLink() {
<Link
href="http://github.com/sambecker/exif-photo-blog"
target="_blank"
className={cc(
className={clsx(
'flex items-center gap-1',
'text-black dark:text-white',
'hover:underline',

View File

@ -2,7 +2,7 @@
import Modal from '@/components/Modal';
import { TbPhotoShare } from 'react-icons/tb';
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
import { BiCopy } from 'react-icons/bi';
import { ReactNode } from 'react';
import { shortenUrl } from '@/utility/url';
@ -22,7 +22,7 @@ export default function ShareModal({
return (
<Modal onClosePath={pathClose}>
<div className="space-y-3 md:space-y-4 w-full">
<div className={cc(
<div className={clsx(
'flex items-center gap-x-3',
'text-xl md:text-3xl leading-snug',
)}>
@ -32,7 +32,7 @@ export default function ShareModal({
</div>
</div>
{children}
<div className={cc(
<div className={clsx(
'rounded-md',
'w-full overflow-hidden',
'flex items-center justify-stretch',
@ -42,7 +42,7 @@ export default function ShareModal({
{shortenUrl(pathShare)}
</div>
<div
className={cc(
className={clsx(
'p-3 border-l',
'border-gray-200 bg-gray-100 active:bg-gray-200',
// 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({
className,
@ -14,7 +14,7 @@ export default function SiteGrid({
sideHiddenOnMobile?: boolean
}) {
return (
<div className={cc(
<div className={clsx(
className,
'grid',
'grid-cols-1 md:grid-cols-12',
@ -22,14 +22,14 @@ export default function SiteGrid({
'gap-y-4',
'max-w-7xl',
)}>
<div className={cc(
<div className={clsx(
'col-span-1 md:col-span-9',
sideFirstOnMobile && 'order-2 md:order-none',
)}>
{contentMain}
</div>
{contentSide &&
<div className={cc(
<div className={clsx(
'col-span-1 md:col-span-3',
sideFirstOnMobile && 'order-1 md:order-none',
sideHiddenOnMobile && 'hidden md:block',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { cc } from '@/utility/css';
import { clsx } from 'clsx';
import { Photo } from '.';
import PhotoGrid from './PhotoGrid';
import Link from 'next/link';
@ -23,7 +23,7 @@ export default function PhotoLightbox({
const showOverageTile = countNotShown > 0;
return (
<div className={cc(
<div className={clsx(
'border dark:border-gray-800 p-1.5 lg:p-2 rounded-md',
'bg-gray-50 dark:bg-gray-950',
)}>
@ -33,7 +33,7 @@ export default function PhotoLightbox({
additionalTile={showOverageTile
? <Link
href={moreLink}
className={cc(
className={clsx(
'flex flex-col items-center justify-center',
'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 ShareButton from '@/components/ShareButton';
import AnimateItems from '@/components/AnimateItems';
@ -37,19 +37,19 @@ export default function PhotoSetHeader({
animateOnFirstLoadOnly
items={[<div
key="PhotosHeader"
className={cc(
className={clsx(
'grid gap-0.5 sm:gap-1 items-start',
HIGH_DENSITY_GRID
? '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',
)}>
<span className={cc(
<span className={clsx(
'inline-flex',
HIGH_DENSITY_GRID && 'sm:col-span-2',
)}>
{entity}
</span>
<span className={cc(
<span className={clsx(
'inline-flex gap-2 items-center self-start',
'uppercase text-dim',
HIGH_DENSITY_GRID
@ -63,7 +63,7 @@ export default function PhotoSetHeader({
{selectedPhotoIndex === undefined &&
<ShareButton path={sharePath} dim />}
</span>
<span className={cc(
<span className={clsx(
'hidden sm:inline-block',
'text-right uppercase',
'text-dim',

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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