feat: enhance TextEditor and Input components with improved styling and error handling

This commit is contained in:
Ardeman 2025-03-06 03:53:15 +08:00
parent 9117a99cc3
commit 894698c5d6
6 changed files with 97 additions and 23 deletions

View File

@ -19,6 +19,10 @@ html,
body { body {
} }
.ProseMirror-focused {
@apply outline-none;
}
table.dataTable thead > tr { table.dataTable thead > tr {
border-bottom: 2px solid #c2c2c2; border-bottom: 2px solid #c2c2c2;
} }

View File

@ -121,7 +121,7 @@ export const EditorMenuBar = (properties: TProperties) => {
return ( return (
<> <>
<div className="flex items-start justify-between gap-4 rounded-[5px_5px_0_0] border-x border-t border-[#D2D2D2] px-4 py-3"> <div className="flex items-start justify-between gap-4 rounded-[5px_5px_0_0] px-4 py-3">
<div className="flex divide-x"> <div className="flex divide-x">
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1"> <div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
<EditorButton <EditorButton

View File

@ -1,3 +1,4 @@
import { Field, Label } from '@headlessui/react'
import { Color } from '@tiptap/extension-color' import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight' import Highlight from '@tiptap/extension-highlight'
import Image from '@tiptap/extension-image' import Image from '@tiptap/extension-image'
@ -9,6 +10,7 @@ import StarterKit from '@tiptap/starter-kit'
import { useEffect, useId, useState } from 'react' import { useEffect, useId, useState } from 'react'
import { import {
get, get,
type FieldError,
type FieldValues, type FieldValues,
type Path, type Path,
type RegisterOptions, type RegisterOptions,
@ -27,6 +29,7 @@ type TProperties<TFormValues extends FieldValues> = {
labelClassName?: string labelClassName?: string
className?: string className?: string
inputClassName?: string inputClassName?: string
containerClassName?: string
rules?: RegisterOptions rules?: RegisterOptions
disabled?: boolean disabled?: boolean
isRequired?: boolean isRequired?: boolean
@ -41,11 +44,11 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
label, label,
name, name,
labelClassName, labelClassName,
isRequired,
className, className,
inputClassName, inputClassName,
category, category,
disabled = false, disabled = false,
containerClassName,
} = properties } = properties
const [isPlainHTML, setIsPlainHTML] = useState(false) const [isPlainHTML, setIsPlainHTML] = useState(false)
@ -60,7 +63,7 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
} = useRemixFormContext() } = useRemixFormContext()
const watchContent = watch(name) const watchContent = watch(name)
const error = get(errors, name) const error: FieldError = get(errors, name)
const editor = useEditor({ const editor = useEditor({
editable: !disabled, editable: !disabled,
@ -100,15 +103,16 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
}, [watchContent]) }, [watchContent])
return ( return (
<div className={twMerge('', className)}> <Field
{label && ( className={twMerge('relative', containerClassName)}
<label disabled={disabled}
htmlFor={id ?? generatedId} id={id}
className={twMerge('mb-2 block text-sm font-bold', labelClassName)}
> >
{label} {label && (
{isRequired && <sup className="text-[#DF0000]">*</sup>} <Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
</label> {label}{' '}
{error && <span className="text-red-500">{error.message}</span>}
</Label>
)} )}
{isPlainHTML ? ( {isPlainHTML ? (
@ -118,7 +122,7 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
disabled={disabled} disabled={disabled}
/> />
) : ( ) : (
<> <div className={twMerge('', className)}>
<EditorMenuBar <EditorMenuBar
disabled={disabled} disabled={disabled}
category={category} category={category}
@ -132,20 +136,14 @@ export const TextEditor = <TFormValues extends Record<string, unknown>>(
editor={editor} editor={editor}
id={id ?? generatedId} id={id ?? generatedId}
className={twMerge( className={twMerge(
'prose mb-1 max-h-96 max-w-none cursor-text overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] px-4 py-1', 'prose-invert max-h-96 max-w-none cursor-text overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] p-2',
darkMode ? 'bg-[#00000055]' : '', darkMode ? 'bg-[#00000055]' : '',
inputClassName, inputClassName,
)} )}
onClick={() => editor?.commands.focus()} onClick={() => editor?.commands.focus()}
/> />
</>
)}
{error && (
<p className="text-xs text-[#DF0000] italic">
{error?.message?.toString()}
</p>
)}
</div> </div>
)}
</Field>
) )
} }

View File

@ -23,6 +23,7 @@ type TInputProperties<T extends FieldValues> = Omit<
name: Path<T> name: Path<T>
rules?: RegisterOptions rules?: RegisterOptions
containerClassName?: string containerClassName?: string
labelClassName?: string
} }
export const Input = <TFormValues extends Record<string, unknown>>( export const Input = <TFormValues extends Record<string, unknown>>(
@ -38,6 +39,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
disabled, disabled,
className, className,
containerClassName, containerClassName,
labelClassName,
...rest ...rest
} = properties } = properties
const [inputType, setInputType] = useState(type) const [inputType, setInputType] = useState(type)
@ -55,7 +57,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
disabled={disabled} disabled={disabled}
id={id} id={id}
> >
<Label className="mb-1 block text-gray-700"> <Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
{label} {error && <span className="text-red-500">{error.message}</span>} {label} {error && <span className="text-red-500">{error.message}</span>}
</Label> </Label>
<HeadlessInput <HeadlessInput

View File

@ -102,6 +102,7 @@ export const CreateContentsPage = () => {
placeholder="Masukkan Judul" placeholder="Masukkan Judul"
name="title" name="title"
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
labelClassName="text-sm font-medium text-[#363636]"
containerClassName="flex-1" containerClassName="flex-1"
/> />
<Input <Input
@ -110,6 +111,7 @@ export const CreateContentsPage = () => {
placeholder="Masukkan Gambar Unggulan" placeholder="Masukkan Gambar Unggulan"
name="featured_image" name="featured_image"
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
labelClassName="text-sm font-medium text-[#363636]"
containerClassName="flex-1" containerClassName="flex-1"
/> />
</div> </div>
@ -151,6 +153,7 @@ export const CreateContentsPage = () => {
name="live_at" name="live_at"
type="date" type="date"
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
labelClassName="text-sm font-medium text-[#363636]"
/> />
<Button <Button
disabled={disabled} disabled={disabled}
@ -167,7 +170,9 @@ export const CreateContentsPage = () => {
name="content" name="content"
label="Konten" label="Konten"
placeholder="Masukkan Konten" placeholder="Masukkan Konten"
className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" className="shadow"
inputClassName="bg-white focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none border-0"
labelClassName="text-sm font-medium text-[#363636]"
category="content" category="content"
/> />
</fetcher.Form> </fetcher.Form>

View File

@ -0,0 +1,65 @@
import { zodResolver } from '@hookform/resolvers/zod'
import { data } from 'react-router'
import { getValidatedFormData } from 'remix-hook-form'
import { XiorError } from 'xior'
import { handleCookie } from '~/libs/cookies'
import { contentSchema, type TContentSchema } from '~/pages/contents-create'
import type { Route } from './+types/actions.register'
export const action = async ({ request }: Route.ActionArgs) => {
const { staffToken } = await handleCookie(request)
try {
const {
errors,
data: payload,
receivedValues: defaultValues,
} = await getValidatedFormData<TContentSchema>(
request,
zodResolver(contentSchema),
false,
)
if (errors) {
return data({ success: false, errors, defaultValues }, { status: 400 })
}
// TODO: implement subscribe
console.log('payload', payload) // eslint-disable-line no-console
console.log('staffToken', staffToken) // eslint-disable-line no-console
// const { data: userData } = await getUser({
// accessToken: userToken,
// })
return data(
{
success: true,
},
{
status: 200,
statusText: 'OK',
},
)
} catch (error) {
if (error instanceof XiorError) {
return data(
{
success: false,
message: error?.response?.data?.error?.message || error.message,
},
{
status: error?.response?.status || 500,
},
)
}
return data(
{
success: false,
message: 'Internal server error',
},
{ status: 500 },
)
}
}