feat: enhance TextEditor and Input components with improved styling and error handling
This commit is contained in:
parent
9117a99cc3
commit
894698c5d6
@ -19,6 +19,10 @@ html,
|
||||
body {
|
||||
}
|
||||
|
||||
.ProseMirror-focused {
|
||||
@apply outline-none;
|
||||
}
|
||||
|
||||
table.dataTable thead > tr {
|
||||
border-bottom: 2px solid #c2c2c2;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
65
app/routes/actions.admin.contents.create.ts
Normal file
65
app/routes/actions.admin.contents.create.ts
Normal 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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user