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