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 {
}
.ProseMirror-focused {
@apply outline-none;
}
table.dataTable thead > tr {
border-bottom: 2px solid #c2c2c2;
}

View File

@ -121,7 +121,7 @@ export const EditorMenuBar = (properties: TProperties) => {
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 max-w-[150px] flex-wrap items-start gap-1 px-1">
<EditorButton

View File

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

View File

@ -23,6 +23,7 @@ type TInputProperties<T extends FieldValues> = Omit<
name: Path<T>
rules?: RegisterOptions
containerClassName?: string
labelClassName?: string
}
export const Input = <TFormValues extends Record<string, unknown>>(
@ -38,6 +39,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
disabled,
className,
containerClassName,
labelClassName,
...rest
} = properties
const [inputType, setInputType] = useState(type)
@ -55,7 +57,7 @@ export const Input = <TFormValues extends Record<string, unknown>>(
disabled={disabled}
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>
<HeadlessInput

View File

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