Create custom checkbox

This commit is contained in:
Sam Becker 2025-03-15 10:46:15 -05:00
parent f2c32fa84f
commit f76a2e88df
5 changed files with 131 additions and 81 deletions

View File

@ -12,7 +12,6 @@ import {
} from '@/utility/date';
import sleep from '@/utility/sleep';
import { readStreamableValue } from 'ai/rsc';
import { clsx } from 'clsx/lite';
import { useRouter } from 'next/navigation';
import { Dispatch, SetStateAction, useRef, useState } from 'react';
import { BiCheckCircle, BiImageAdd } from 'react-icons/bi';
@ -120,15 +119,9 @@ export default function AdminBatchUploadActions({
<Container padding="tight">
<div className="w-full space-y-4 py-1">
<div className="flex">
<div className={clsx(
'grow',
tagErrorMessage ? 'text-error' : 'text-main',
)}>
<div className="grow text-main">
{showBulkSettings
? (
tagErrorMessage ||
`Apply to ${pluralize(storageUrls.length, 'upload')}`
)
? `Apply to ${pluralize(storageUrls.length, 'upload')}`
: `Found ${pluralize(storageUrls.length, 'upload')}`}
</div>
<FieldSetWithStatus
@ -140,7 +133,7 @@ export default function AdminBatchUploadActions({
/>
</div>
{showBulkSettings && !actionErrorMessage &&
<div className="space-y-3">
<div className="space-y-4 mb-6">
<PhotoTagFieldset
label="Tags"
tags={tags}
@ -149,20 +142,22 @@ export default function AdminBatchUploadActions({
onError={setTagErrorMessage}
readOnly={isAdding}
/>
<FieldSetWithStatus
label="Favorite"
type="checkbox"
value={favorite}
onChange={setFavorite}
readOnly={isAdding}
/>
<FieldSetWithStatus
label="Hidden"
type="checkbox"
value={hidden}
onChange={setHidden}
readOnly={isAdding}
/>
<div className="flex gap-8">
<FieldSetWithStatus
label="Favorite"
type="checkbox"
value={favorite}
onChange={setFavorite}
readOnly={isAdding}
/>
<FieldSetWithStatus
label="Hidden"
type="checkbox"
value={hidden}
onChange={setHidden}
readOnly={isAdding}
/>
</div>
</div>}
<div className="space-y-2">
<ProgressButton

View File

@ -2,7 +2,7 @@
import { useAppState } from '@/state/AppState';
import SignInForm from '@/auth/SignInForm';
import clsx from 'clsx';
import clsx from 'clsx/lite';
import PhotoUploadWithStatus from '@/photo/PhotoUploadWithStatus';
export default function SignInOrUploadClient({

View File

@ -0,0 +1,53 @@
import clsx from 'clsx/lite';
import { InputHTMLAttributes, ReactNode, Ref } from 'react';
import { ImCheckmark } from 'react-icons/im';
const boxStyles = clsx(
'relative',
'inline-flex items-center justify-center',
'size-5 rounded-md border',
);
export default function Checkbox({
ref,
className,
accessory,
type: _type,
...props
}: InputHTMLAttributes<HTMLInputElement> & {
ref?: Ref<HTMLInputElement>
accessory?: ReactNode
}) {
return (
<span className={clsx(
'relative inline-flex items-center justify-center',
'size-5',
'group-has-active:opacity-70',
)}>
{accessory
? accessory
: props.checked
? <span className={clsx(
boxStyles,
'border-transparent',
'bg-blue-600',
)}>
<ImCheckmark className="text-white text-[11px]" />
</span>
: <span className={clsx(
boxStyles,
'border-gray-300 dark:border-gray-700',
'bg-gray-100 dark:bg-gray-700/25',
)} />}
<input
ref={ref}
type="checkbox"
className={clsx(
'absolute inset-0 invisible',
className,
)}
{...props}
/>
</span>
);
}

View File

@ -1,6 +1,6 @@
'use client';
import { Ref } from 'react';
import { Ref, InputHTMLAttributes } from 'react';
import { useFormStatus } from 'react-dom';
import Spinner from './Spinner';
import { clsx } from 'clsx/lite';
@ -8,6 +8,7 @@ import { FieldSetType, AnnotatedTag } from '@/photo/form';
import TagInput from './TagInput';
import { FiChevronDown } from 'react-icons/fi';
import { parameterize } from '@/utility/string';
import Checkbox from './Checkbox';
export default function FieldSetWithStatus({
id: _id,
@ -33,7 +34,6 @@ export default function FieldSetWithStatus({
inputRef,
accessory,
hideLabel,
checkboxAccessory,
}: {
id?: string
label: string
@ -58,59 +58,58 @@ export default function FieldSetWithStatus({
inputRef?: Ref<HTMLInputElement>
accessory?: React.ReactNode
hideLabel?: boolean
checkboxAccessory?: React.ReactNode
}) {
const id = _id || parameterize(label);
const { pending } = useFormStatus();
const renderInput =
<input
ref={inputRef}
id={id}
name={id}
type={type}
value={value}
checked={type === 'checkbox' ? value === 'true' : undefined}
placeholder={placeholder}
onChange={e => onChange?.(type === 'checkbox'
? e.target.value === 'true' ? 'false' : 'true'
: e.target.value)}
spellCheck={spellCheck}
autoComplete="off"
autoCapitalize={!capitalize ? 'off' : undefined}
readOnly={readOnly || pending || loading}
disabled={type === 'checkbox' && (
const inputProps: InputHTMLAttributes<HTMLInputElement> = {
id,
name: id,
type,
value,
checked: type === 'checkbox' ? value === 'true' : undefined,
placeholder,
onChange: e => onChange?.(type === 'checkbox'
? e.target.value === 'true' ? 'false' : 'true'
: e.target.value),
spellCheck,
autoComplete: 'off',
autoCapitalize: !capitalize ? 'off' : undefined,
readOnly: readOnly || pending || loading,
disabled: type === 'checkbox' && (
readOnly || pending || loading
),
className: clsx(
(
type === 'text' ||
type === 'email' ||
type === 'password'
) && 'w-full',
type === 'checkbox' && (
readOnly || pending || loading
)}
className={clsx(
(
type === 'text' ||
type === 'email' ||
type === 'password'
) && 'w-full',
type === 'checkbox' && (
readOnly || pending || loading
) && 'opacity-50 cursor-not-allowed',
Boolean(error) && 'error',
)}
/>;
) && 'opacity-50 cursor-not-allowed',
Boolean(error) && 'error',
),
};
return (
type === 'hidden'
? renderInput
type === 'hidden'
? <input ref={inputRef} {...inputProps} />
: <div className={clsx(
// For managing checkbox active state
'group',
'space-y-1',
type === 'checkbox' && 'flex items-center gap-2',
type === 'checkbox' && 'flex items-center gap-3',
className,
)}>
{!hideLabel && label &&
{!hideLabel &&
<label
className={clsx(
'flex flex-wrap gap-x-2 items-center select-none',
type === 'checkbox' && 'order-2 pt-[4px] ml-1',
)}
htmlFor={id}
className={clsx(
'inline-flex flex-wrap gap-x-2 items-center select-none',
type === 'checkbox' && 'order-2 m-0',
)}
>
{label}
{note && !error &&
@ -132,7 +131,7 @@ export default function FieldSetWithStatus({
<span className="text-gray-400 dark:text-gray-600">
Required
</span>}
{loading &&
{loading && type !== 'checkbox' &&
<span className="translate-y-[1.5px]">
<Spinner />
</span>}
@ -202,11 +201,18 @@ export default function FieldSetWithStatus({
Boolean(error) && 'error',
)}
/>
: type === 'checkbox' && checkboxAccessory
? <span className="w-[13px]">
{checkboxAccessory}
</span>
: renderInput}
: type === 'checkbox'
? <Checkbox
ref={inputRef}
accessory={loading && <Spinner
className="translate-y-[0.5px]"
/>}
{...inputProps}
/>
: <input
ref={inputRef}
{...inputProps}
/>}
{accessory && <div>
{accessory}
</div>}

View File

@ -2,7 +2,6 @@ import FieldSetWithStatus from '@/components/FieldSetWithStatus';
import { ComponentProps, useEffect, useState } from 'react';
import { getPhotosNeedingRecipeTitleCountAction } from '../actions';
import { FilmSimulation } from '@/simulation';
import Spinner from '@/components/Spinner';
export default function ApplyRecipeTitleGloballyCheckbox({
photoId,
@ -22,7 +21,7 @@ export default function ApplyRecipeTitleGloballyCheckbox({
}) {
const [matchingPhotosCount, setMatchingPhotosCount] = useState<number>();
const isLoading = matchingPhotosCount === undefined;
const loading = matchingPhotosCount === undefined;
useEffect(() => {
if (recipeTitle && hasRecipeTitleChanged && recipeData && simulation) {
@ -38,21 +37,18 @@ export default function ApplyRecipeTitleGloballyCheckbox({
onMatchResults((matchingPhotosCount ?? 0) > 0);
}, [matchingPhotosCount, onMatchResults]);
const shouldShowFieldSet = isLoading || matchingPhotosCount > 0;
const shouldShowFieldSet = loading || matchingPhotosCount > 0;
return (
shouldShowFieldSet
? <FieldSetWithStatus {...{
...props,
label: isLoading
label: loading
? 'Scanning photos for matching recipes ...'
: `Apply title to ${matchingPhotosCount} matching photos`,
type: 'checkbox',
readOnly: isLoading,
className: '-mt-4 translate-x-[1px]',
checkboxAccessory: isLoading
? <Spinner className="translate-y-[1.5px]" />
: null,
className: '-mt-4 translate-x-[4px]',
loading,
}} />
: null
);