Compare commits
No commits in common. "8f8fb6b97c7ec8e740215d52f2769d84adaedd85" and "aca894729e47280919b626773c06377b41eb47d0" have entirely different histories.
8f8fb6b97c
...
aca894729e
@ -1,39 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { HttpServer } from '~/libs/http-server'
|
|
||||||
import type { TContentSchema } from '~/pages/contents-create'
|
|
||||||
|
|
||||||
const newsResponseSchema = z.object({
|
|
||||||
data: z.object({
|
|
||||||
Message: z.string(),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
type TParameter = {
|
|
||||||
accessToken: string
|
|
||||||
payload: TContentSchema
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createNewsRequest = async (parameters: TParameter) => {
|
|
||||||
const { accessToken, payload } = parameters
|
|
||||||
try {
|
|
||||||
const { categories, tags, ...restPayload } = payload
|
|
||||||
const transformedPayload = {
|
|
||||||
...restPayload,
|
|
||||||
categories: categories.map((category) => category?.id),
|
|
||||||
tags: tags?.map((tag) => tag?.id),
|
|
||||||
live_at: new Date(payload?.live_at).toISOString(),
|
|
||||||
}
|
|
||||||
if (transformedPayload.tags?.length === 0) {
|
|
||||||
delete transformedPayload.tags
|
|
||||||
}
|
|
||||||
const { data } = await HttpServer({ accessToken }).post(
|
|
||||||
'/api/news/create',
|
|
||||||
transformedPayload,
|
|
||||||
)
|
|
||||||
return newsResponseSchema.parse(data)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
|
||||||
|
|
||||||
const tagSchema = z.object({
|
|
||||||
data: z.array(
|
|
||||||
z.object({
|
|
||||||
id: z.string(),
|
|
||||||
code: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type TTagSchema = z.infer<typeof tagSchema>
|
|
||||||
|
|
||||||
export const getTags = async (parameters?: THttpServer) => {
|
|
||||||
try {
|
|
||||||
const { data } = await HttpServer(parameters).get(`/api/tag`)
|
|
||||||
return tagSchema.parse(data)
|
|
||||||
} catch (error) {
|
|
||||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -19,10 +19,6 @@ 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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import type { CSSProperties, MouseEventHandler, ReactNode } from 'react'
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
type TProperties = {
|
|
||||||
children: ReactNode
|
|
||||||
onClick: MouseEventHandler
|
|
||||||
disabled?: boolean
|
|
||||||
isActive?: boolean
|
|
||||||
className?: string
|
|
||||||
title: string
|
|
||||||
style?: CSSProperties
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditorButton = (properties: TProperties) => {
|
|
||||||
const { children, onClick, disabled, className, isActive, title, style } =
|
|
||||||
properties
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={twMerge(
|
|
||||||
'flex h-6 w-8 items-center justify-center rounded-md text-xl hover:!bg-[#FCB017] disabled:cursor-not-allowed disabled:text-slate-400 disabled:opacity-50',
|
|
||||||
isActive ? 'bg-[#FCB01755]' : '',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
style={style}
|
|
||||||
title={title}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,432 +0,0 @@
|
|||||||
import {
|
|
||||||
ArrowUturnLeftIcon,
|
|
||||||
ArrowUturnRightIcon,
|
|
||||||
Bars3BottomLeftIcon,
|
|
||||||
Bars3BottomRightIcon,
|
|
||||||
Bars3Icon,
|
|
||||||
Bars4Icon,
|
|
||||||
BoldIcon,
|
|
||||||
CodeBracketIcon,
|
|
||||||
DocumentTextIcon,
|
|
||||||
H1Icon,
|
|
||||||
H2Icon,
|
|
||||||
H3Icon,
|
|
||||||
ItalicIcon,
|
|
||||||
LinkIcon,
|
|
||||||
LinkSlashIcon,
|
|
||||||
ListBulletIcon,
|
|
||||||
MoonIcon,
|
|
||||||
NumberedListIcon,
|
|
||||||
PhotoIcon,
|
|
||||||
StrikethroughIcon,
|
|
||||||
SunIcon,
|
|
||||||
SwatchIcon,
|
|
||||||
} from '@heroicons/react/20/solid'
|
|
||||||
import type { Editor } from '@tiptap/react'
|
|
||||||
import {
|
|
||||||
type SetStateAction,
|
|
||||||
type Dispatch,
|
|
||||||
useState,
|
|
||||||
useRef,
|
|
||||||
useCallback,
|
|
||||||
} from 'react'
|
|
||||||
import { HexColorInput, HexColorPicker } from 'react-colorful'
|
|
||||||
|
|
||||||
import { useClickOutside } from '~/hooks/use-click-outside'
|
|
||||||
import { isHexCompatible, rgbToHex } from '~/utils/color'
|
|
||||||
|
|
||||||
import { EditorButton } from './editor-button'
|
|
||||||
|
|
||||||
type TProperties = {
|
|
||||||
editor: Editor | null
|
|
||||||
setIsPlainHTML: Dispatch<SetStateAction<boolean>>
|
|
||||||
category: string
|
|
||||||
darkMode: boolean
|
|
||||||
setDarkMode: Dispatch<SetStateAction<boolean>>
|
|
||||||
disabled?: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditorMenuBar = (properties: TProperties) => {
|
|
||||||
const {
|
|
||||||
editor,
|
|
||||||
setIsPlainHTML,
|
|
||||||
// category,
|
|
||||||
darkMode,
|
|
||||||
setDarkMode,
|
|
||||||
disabled = false,
|
|
||||||
} = properties
|
|
||||||
// const [isOpenImage, setIsOpenImage] = useState(false)
|
|
||||||
const [isOpenColor, setIsOpenColor] = useState(false)
|
|
||||||
const popover = useRef<HTMLDivElement>(null)
|
|
||||||
const close = useCallback(() => setIsOpenColor(false), [])
|
|
||||||
|
|
||||||
useClickOutside(popover, close)
|
|
||||||
|
|
||||||
const setLink = useCallback(() => {
|
|
||||||
const previousUrl = editor?.getAttributes('link').href
|
|
||||||
const url = globalThis.prompt('URL', previousUrl)
|
|
||||||
|
|
||||||
// cancelled
|
|
||||||
if (url === null) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// empty
|
|
||||||
if (url === '') {
|
|
||||||
editor?.chain().focus().extendMarkRange('link').unsetLink().run()
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// update link
|
|
||||||
editor?.chain().focus().extendMarkRange('link').setLink({ href: url }).run()
|
|
||||||
}, [editor])
|
|
||||||
|
|
||||||
if (!editor) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const addImage = (url: string) => {
|
|
||||||
if (url) {
|
|
||||||
editor.chain().focus().setImage({ src: url }).run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentColor: string = editor.getAttributes('textStyle').color
|
|
||||||
const rgbColor = isHexCompatible(currentColor)
|
|
||||||
? currentColor
|
|
||||||
: rgbToHex(currentColor)
|
|
||||||
const handleChangeColor = (selectedColor: string) => {
|
|
||||||
if (selectedColor.length === 7) {
|
|
||||||
editor.chain().focus().setColor(selectedColor).run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadEnabled = false
|
|
||||||
const toggleUpload = () => {
|
|
||||||
if (uploadEnabled) {
|
|
||||||
// setIsOpenImage(true)
|
|
||||||
} else {
|
|
||||||
const urlImage = globalThis.prompt('URL')
|
|
||||||
if (urlImage) {
|
|
||||||
addImage(urlImage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleDark = () => {
|
|
||||||
setDarkMode(!darkMode)
|
|
||||||
// localStorage.setItem(editorKey, JSON.stringify(!darkMode))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<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
|
|
||||||
onClick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
disabled={
|
|
||||||
disabled || !editor.can().chain().focus().toggleBold().run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('bold')}
|
|
||||||
title="Bold"
|
|
||||||
>
|
|
||||||
<BoldIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
disabled={
|
|
||||||
disabled || !editor.can().chain().focus().toggleItalic().run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('italic')}
|
|
||||||
title="Italic"
|
|
||||||
>
|
|
||||||
<ItalicIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
|
||||||
disabled={
|
|
||||||
disabled || !editor.can().chain().focus().toggleStrike().run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('strike')}
|
|
||||||
title="Strike"
|
|
||||||
>
|
|
||||||
<StrikethroughIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<div className="relative">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => setIsOpenColor(true)}
|
|
||||||
title="Text Color"
|
|
||||||
style={{
|
|
||||||
color: rgbColor,
|
|
||||||
}}
|
|
||||||
isActive={true}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<SwatchIcon />
|
|
||||||
</EditorButton>
|
|
||||||
{isOpenColor && (
|
|
||||||
<div
|
|
||||||
className="border-md absolute top-8 left-0"
|
|
||||||
ref={popover}
|
|
||||||
>
|
|
||||||
<HexColorPicker
|
|
||||||
className="z-10"
|
|
||||||
color={rgbColor}
|
|
||||||
onChange={handleChangeColor}
|
|
||||||
/>
|
|
||||||
<div className="">
|
|
||||||
<HexColorInput
|
|
||||||
color={rgbColor}
|
|
||||||
onChange={handleChangeColor}
|
|
||||||
prefixed
|
|
||||||
className="relative z-10 mt-1 flex w-full rounded-lg border px-3 py-2 text-sm focus:ring-0 focus-visible:outline-0"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().setTextAlign('left').run()}
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!editor.can().chain().focus().setTextAlign('left').run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive({ textAlign: 'left' })}
|
|
||||||
title="Align Left"
|
|
||||||
>
|
|
||||||
<Bars3BottomLeftIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().setTextAlign('center').run()
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!editor.can().chain().focus().setTextAlign('center').run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive({ textAlign: 'center' })}
|
|
||||||
title="Align Center"
|
|
||||||
>
|
|
||||||
<Bars3Icon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!editor.can().chain().focus().setTextAlign('right').run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive({ textAlign: 'right' })}
|
|
||||||
title="Align Right"
|
|
||||||
>
|
|
||||||
<Bars3BottomRightIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().setTextAlign('justify').run()
|
|
||||||
}
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!editor.can().chain().focus().setTextAlign('justify').run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive({ textAlign: 'justify' })}
|
|
||||||
title="Align Justify"
|
|
||||||
>
|
|
||||||
<Bars4Icon />
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().toggleHeading({ level: 1 }).run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('heading', { level: 1 })}
|
|
||||||
title="Heading 1"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<H1Icon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().toggleHeading({ level: 2 }).run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('heading', { level: 2 })}
|
|
||||||
title="Heading 2"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<H2Icon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() =>
|
|
||||||
editor.chain().focus().toggleHeading({ level: 3 }).run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('heading', { level: 3 })}
|
|
||||||
title="Heading 3"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<H3Icon />
|
|
||||||
</EditorButton>
|
|
||||||
{/* <EditorButton
|
|
||||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
|
||||||
isActive={editor.isActive('paragraph')}
|
|
||||||
title="Paragraph"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<RiParagraph />
|
|
||||||
</EditorButton> */}
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
isActive={editor.isActive('bulletList')}
|
|
||||||
title="Bullet List"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<ListBulletIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
isActive={editor.isActive('orderedList')}
|
|
||||||
title="Ordered List"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<NumberedListIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
|
||||||
isActive={editor.isActive('codeBlock')}
|
|
||||||
title="Code Block"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<CodeBracketIcon />
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
{/* <div className="flex items-start gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().toggleBlockquote().run()}
|
|
||||||
isActive={editor.isActive('blockquote')}
|
|
||||||
title="Blockquote"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<RiDoubleQuotesL />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().setHorizontalRule().run()}
|
|
||||||
title="Horizontal Rule"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<RiSeparator />
|
|
||||||
</EditorButton>
|
|
||||||
</div> */}
|
|
||||||
{/* <div className="flex items-start gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().setHardBreak().run()}
|
|
||||||
title="Hard Break"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<RiTextWrap />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => {
|
|
||||||
editor.chain().focus().unsetAllMarks().run()
|
|
||||||
editor.chain().focus().clearNodes().run()
|
|
||||||
}}
|
|
||||||
title="Clear Format"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<RiFormatClear />
|
|
||||||
</EditorButton>
|
|
||||||
</div> */}
|
|
||||||
<div className="flex items-start gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => toggleUpload()}
|
|
||||||
title="Insert Image"
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<PhotoIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => setLink()}
|
|
||||||
disabled={
|
|
||||||
disabled ||
|
|
||||||
!editor
|
|
||||||
.can()
|
|
||||||
.chain()
|
|
||||||
.focus()
|
|
||||||
.extendMarkRange('link')
|
|
||||||
.setLink({ href: '' })
|
|
||||||
.run()
|
|
||||||
}
|
|
||||||
isActive={editor.isActive('link')}
|
|
||||||
title="Set Link"
|
|
||||||
>
|
|
||||||
<LinkIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
|
||||||
disabled={disabled || !editor.isActive('link')}
|
|
||||||
title="Unset Link"
|
|
||||||
>
|
|
||||||
<LinkSlashIcon />
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().undo().run()}
|
|
||||||
disabled={disabled || !editor.can().chain().focus().undo().run()}
|
|
||||||
title="Undo"
|
|
||||||
>
|
|
||||||
<ArrowUturnLeftIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => editor.chain().focus().redo().run()}
|
|
||||||
disabled={disabled || !editor.can().chain().focus().redo().run()}
|
|
||||||
title="Redo"
|
|
||||||
>
|
|
||||||
<ArrowUturnRightIcon />
|
|
||||||
</EditorButton>
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => toggleDark()}
|
|
||||||
title={darkMode ? 'Switch to Light Mode' : 'Switch to Dark Mode'}
|
|
||||||
isActive={true}
|
|
||||||
>
|
|
||||||
{darkMode ? <MoonIcon /> : <SunIcon />}
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<div className="flex gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => setIsPlainHTML(true)}
|
|
||||||
title="Switch to Plain Text"
|
|
||||||
isActive={true}
|
|
||||||
>
|
|
||||||
<DocumentTextIcon />
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <Dialog
|
|
||||||
isOpen={isOpenImage}
|
|
||||||
setIsOpen={setIsOpenImage}
|
|
||||||
title="Insert Image"
|
|
||||||
showCloseButton={true}
|
|
||||||
>
|
|
||||||
<UploadProvider
|
|
||||||
data={{
|
|
||||||
onCancel: () => setIsOpenImage(false),
|
|
||||||
onSave: (file) => {
|
|
||||||
addImage(file)
|
|
||||||
setIsOpenImage(false)
|
|
||||||
},
|
|
||||||
category: category,
|
|
||||||
maxFileSize: 300,
|
|
||||||
selectedFile: '',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Upload />
|
|
||||||
</UploadProvider>
|
|
||||||
</Dialog> */}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,56 +0,0 @@
|
|||||||
import { CodeBracketSquareIcon } from '@heroicons/react/20/solid'
|
|
||||||
import MonacoEditor from '@monaco-editor/react'
|
|
||||||
import type { Dispatch, SetStateAction } from 'react'
|
|
||||||
import { Controller } from 'react-hook-form'
|
|
||||||
import { useRemixFormContext } from 'remix-hook-form'
|
|
||||||
|
|
||||||
import { EditorButton } from './editor-button'
|
|
||||||
|
|
||||||
type TProperties = {
|
|
||||||
name: string
|
|
||||||
disabled?: boolean
|
|
||||||
setIsPlainHTML: Dispatch<SetStateAction<boolean>>
|
|
||||||
}
|
|
||||||
|
|
||||||
// const MonacoEditor = dynamic(() => import('@monaco-editor/react'), {
|
|
||||||
// ssr: false,
|
|
||||||
// })
|
|
||||||
|
|
||||||
export const EditorTextArea = (properties: TProperties) => {
|
|
||||||
const { setIsPlainHTML, name, disabled = false } = properties
|
|
||||||
|
|
||||||
const { control } = useRemixFormContext()
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="flex justify-end rounded-[5px_5px_0_0] border-x border-t border-[#D2D2D2] px-4 py-3">
|
|
||||||
<div className="flex gap-1 px-1">
|
|
||||||
<EditorButton
|
|
||||||
onClick={() => setIsPlainHTML(false)}
|
|
||||||
title="Switch to Rich Text"
|
|
||||||
isActive={true}
|
|
||||||
>
|
|
||||||
<CodeBracketSquareIcon />
|
|
||||||
</EditorButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name={name}
|
|
||||||
control={control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<MonacoEditor
|
|
||||||
language="html"
|
|
||||||
onChange={(newValue) => {
|
|
||||||
field.onChange(newValue)
|
|
||||||
}}
|
|
||||||
value={field.value}
|
|
||||||
options={{
|
|
||||||
readOnly: disabled,
|
|
||||||
wordWrap: 'on',
|
|
||||||
}}
|
|
||||||
className="mb-1 h-96 max-w-none overflow-y-auto rounded-[0_0_5px_5px] border border-[#D2D2D2] p-1"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -1,150 +0,0 @@
|
|||||||
import { Field, Label } from '@headlessui/react'
|
|
||||||
import { Color } from '@tiptap/extension-color'
|
|
||||||
import Highlight from '@tiptap/extension-highlight'
|
|
||||||
import Image from '@tiptap/extension-image'
|
|
||||||
import Link from '@tiptap/extension-link'
|
|
||||||
import TextAlign from '@tiptap/extension-text-align'
|
|
||||||
import TextStyle from '@tiptap/extension-text-style'
|
|
||||||
import { EditorContent, useEditor } from '@tiptap/react'
|
|
||||||
import StarterKit from '@tiptap/starter-kit'
|
|
||||||
import { useEffect, useId, useState } from 'react'
|
|
||||||
import {
|
|
||||||
get,
|
|
||||||
type FieldError,
|
|
||||||
type FieldValues,
|
|
||||||
type Path,
|
|
||||||
type RegisterOptions,
|
|
||||||
} from 'react-hook-form'
|
|
||||||
import { useRemixFormContext } from 'remix-hook-form'
|
|
||||||
import { twMerge } from 'tailwind-merge'
|
|
||||||
|
|
||||||
import { EditorMenuBar } from './editor-menubar'
|
|
||||||
import { EditorTextArea } from './editor-textarea'
|
|
||||||
|
|
||||||
type TProperties<TFormValues extends FieldValues> = {
|
|
||||||
id?: string
|
|
||||||
name: Path<TFormValues>
|
|
||||||
label?: string
|
|
||||||
placeholder?: string
|
|
||||||
labelClassName?: string
|
|
||||||
className?: string
|
|
||||||
inputClassName?: string
|
|
||||||
containerClassName?: string
|
|
||||||
rules?: RegisterOptions
|
|
||||||
disabled?: boolean
|
|
||||||
isRequired?: boolean
|
|
||||||
category: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TextEditor = <TFormValues extends Record<string, unknown>>(
|
|
||||||
properties: TProperties<TFormValues>,
|
|
||||||
) => {
|
|
||||||
const {
|
|
||||||
id,
|
|
||||||
label,
|
|
||||||
name,
|
|
||||||
labelClassName,
|
|
||||||
className,
|
|
||||||
inputClassName,
|
|
||||||
category,
|
|
||||||
disabled = false,
|
|
||||||
containerClassName,
|
|
||||||
} = properties
|
|
||||||
|
|
||||||
const [isPlainHTML, setIsPlainHTML] = useState(false)
|
|
||||||
const [init, setInit] = useState(true)
|
|
||||||
const [darkMode, setDarkMode] = useState(false)
|
|
||||||
const generatedId = useId()
|
|
||||||
|
|
||||||
const {
|
|
||||||
setValue,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useRemixFormContext()
|
|
||||||
|
|
||||||
const watchContent = watch(name)
|
|
||||||
const error: FieldError = get(errors, name)
|
|
||||||
|
|
||||||
const editor = useEditor({
|
|
||||||
editable: !disabled,
|
|
||||||
extensions: [
|
|
||||||
StarterKit,
|
|
||||||
Highlight,
|
|
||||||
Image.configure({
|
|
||||||
inline: true,
|
|
||||||
}),
|
|
||||||
TextStyle,
|
|
||||||
Color.configure({
|
|
||||||
types: ['textStyle'],
|
|
||||||
}),
|
|
||||||
Link.configure({
|
|
||||||
openOnClick: false,
|
|
||||||
}),
|
|
||||||
TextAlign.configure({
|
|
||||||
types: ['heading', 'paragraph'],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
immediatelyRender: false,
|
|
||||||
content: watchContent,
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
setValue(name, editor.getHTML() as any) // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
||||||
},
|
|
||||||
})
|
|
||||||
useEffect(() => {
|
|
||||||
if (
|
|
||||||
watchContent &&
|
|
||||||
watchContent.length > 0 &&
|
|
||||||
editor &&
|
|
||||||
(isPlainHTML || init)
|
|
||||||
) {
|
|
||||||
editor.commands.setContent(watchContent)
|
|
||||||
setInit(false)
|
|
||||||
}
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [watchContent])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Field
|
|
||||||
className={twMerge('relative', containerClassName)}
|
|
||||||
disabled={disabled}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
{label && (
|
|
||||||
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
|
|
||||||
{label}{' '}
|
|
||||||
{error && <span className="text-red-500">{error.message}</span>}
|
|
||||||
</Label>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{isPlainHTML ? (
|
|
||||||
<EditorTextArea
|
|
||||||
setIsPlainHTML={setIsPlainHTML}
|
|
||||||
name={name}
|
|
||||||
disabled={disabled}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div className={twMerge('', className)}>
|
|
||||||
<EditorMenuBar
|
|
||||||
disabled={disabled}
|
|
||||||
category={category}
|
|
||||||
editor={editor}
|
|
||||||
setIsPlainHTML={setIsPlainHTML}
|
|
||||||
darkMode={darkMode}
|
|
||||||
setDarkMode={setDarkMode}
|
|
||||||
/>
|
|
||||||
<EditorContent
|
|
||||||
readOnly={disabled}
|
|
||||||
editor={editor}
|
|
||||||
id={id ?? generatedId}
|
|
||||||
className={twMerge(
|
|
||||||
'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>
|
|
||||||
)}
|
|
||||||
</Field>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@ -35,26 +35,13 @@ type TInputProperties<T extends FieldValues> = ComponentProps<
|
|||||||
rules?: RegisterOptions
|
rules?: RegisterOptions
|
||||||
placeholder?: string
|
placeholder?: string
|
||||||
options?: TComboboxOption[]
|
options?: TComboboxOption[]
|
||||||
labelClassName?: string
|
|
||||||
containerClassName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Combobox = <TFormValues extends Record<string, unknown>>(
|
export const Combobox = <TFormValues extends Record<string, unknown>>(
|
||||||
properties: TInputProperties<TFormValues>,
|
properties: TInputProperties<TFormValues>,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const { id, label, name, rules, disabled, placeholder, options, ...rest } =
|
||||||
id,
|
properties
|
||||||
label,
|
|
||||||
name,
|
|
||||||
rules,
|
|
||||||
disabled,
|
|
||||||
placeholder,
|
|
||||||
options,
|
|
||||||
className,
|
|
||||||
labelClassName,
|
|
||||||
containerClassName,
|
|
||||||
...rest
|
|
||||||
} = properties
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
@ -71,11 +58,11 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className={twMerge('relative', containerClassName)}
|
className="relative"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
|
<Label className="mb-1 block text-gray-700">
|
||||||
{label} {error && <span className="text-red-500">{error.message}</span>}
|
{label} {error && <span className="text-red-500">{error.message}</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
@ -95,10 +82,7 @@ export const Combobox = <TFormValues extends Record<string, unknown>>(
|
|||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
displayValue={(option: TComboboxOption) => option?.name}
|
displayValue={(option: TComboboxOption) => option?.name}
|
||||||
onChange={(event) => setQuery(event.target.value)}
|
onChange={(event) => setQuery(event.target.value)}
|
||||||
className={twMerge(
|
className="focus:inheriten h-[42px] w-full rounded-md border border-[#DFDFDF] p-2"
|
||||||
'focus:inheriten h-[42px] w-full rounded-md border border-[#DFDFDF] p-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
/>
|
/>
|
||||||
<ComboboxButton className="group absolute inset-y-0 right-0 px-2.5">
|
<ComboboxButton className="group absolute inset-y-0 right-0 px-2.5">
|
||||||
<ChevronDownIcon className="size-4 fill-gray-500" />
|
<ChevronDownIcon className="size-4 fill-gray-500" />
|
||||||
|
|||||||
@ -22,8 +22,6 @@ type TInputProperties<T extends FieldValues> = Omit<
|
|||||||
label?: ReactNode
|
label?: ReactNode
|
||||||
name: Path<T>
|
name: Path<T>
|
||||||
rules?: RegisterOptions
|
rules?: RegisterOptions
|
||||||
containerClassName?: string
|
|
||||||
labelClassName?: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = <TFormValues extends Record<string, unknown>>(
|
export const Input = <TFormValues extends Record<string, unknown>>(
|
||||||
@ -37,9 +35,6 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
|||||||
type = 'text',
|
type = 'text',
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
className,
|
|
||||||
containerClassName,
|
|
||||||
labelClassName,
|
|
||||||
...rest
|
...rest
|
||||||
} = properties
|
} = properties
|
||||||
const [inputType, setInputType] = useState(type)
|
const [inputType, setInputType] = useState(type)
|
||||||
@ -53,19 +48,16 @@ export const Input = <TFormValues extends Record<string, unknown>>(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Field
|
<Field
|
||||||
className={twMerge('relative', containerClassName)}
|
className="relative"
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
id={id}
|
id={id}
|
||||||
>
|
>
|
||||||
<Label className={twMerge('mb-1 block text-gray-700', labelClassName)}>
|
<Label className="mb-1 block text-gray-700">
|
||||||
{label} {error && <span className="text-red-500">{error.message}</span>}
|
{label} {error && <span className="text-red-500">{error.message}</span>}
|
||||||
</Label>
|
</Label>
|
||||||
<HeadlessInput
|
<HeadlessInput
|
||||||
type={inputType}
|
type={inputType}
|
||||||
className={twMerge(
|
className="h-[42px] w-full rounded-md border border-[#DFDFDF] p-2"
|
||||||
'h-[42px] w-full rounded-md border border-[#DFDFDF] p-2',
|
|
||||||
className,
|
|
||||||
)}
|
|
||||||
placeholder={inputType === 'password' ? '******' : placeholder}
|
placeholder={inputType === 'password' ? '******' : placeholder}
|
||||||
{...register(name, rules)}
|
{...register(name, rules)}
|
||||||
{...rest}
|
{...rest}
|
||||||
|
|||||||
@ -19,7 +19,6 @@ import {
|
|||||||
const DefaultTextEditor = () => {
|
const DefaultTextEditor = () => {
|
||||||
const editor = useEditor({
|
const editor = useEditor({
|
||||||
extensions: [StarterKit],
|
extensions: [StarterKit],
|
||||||
immediatelyRender: false,
|
|
||||||
content:
|
content:
|
||||||
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.</p>',
|
'<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quis lobortis nisl cursus bibendum sit nulla accumsan sodales ornare. At urna viverra non suspendisse neque, lorem. Pretium condimentum pellentesque gravida id etiam sit sed arcu euismod. Rhoncus proin orci duis scelerisque molestie cursus tincidunt aliquam.</p>',
|
||||||
})
|
})
|
||||||
|
|||||||
@ -4,7 +4,7 @@ type TitleDashboardProperties = {
|
|||||||
export const TitleDashboard = (properties: TitleDashboardProperties) => {
|
export const TitleDashboard = (properties: TitleDashboardProperties) => {
|
||||||
const { title } = properties
|
const { title } = properties
|
||||||
return (
|
return (
|
||||||
<div className="container">
|
<div className="container mx-auto">
|
||||||
<div className="mb-5 flex items-center justify-between">
|
<div className="mb-5 flex items-center justify-between">
|
||||||
<h1 className="text-xl font-bold">{title}</h1>
|
<h1 className="text-xl font-bold">{title}</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import { useEffect, type RefObject } from 'react'
|
|
||||||
|
|
||||||
type Event = MouseEvent | TouchEvent
|
|
||||||
|
|
||||||
export const useClickOutside = <T extends HTMLElement = HTMLElement>(
|
|
||||||
reference: RefObject<T | null>,
|
|
||||||
handler: (event: Event) => void,
|
|
||||||
) => {
|
|
||||||
useEffect(() => {
|
|
||||||
const listener = (event: Event) => {
|
|
||||||
const element = reference?.current
|
|
||||||
if (!element || element.contains((event?.target as Node) || undefined)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handler(event)
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('mousedown', listener)
|
|
||||||
document.addEventListener('touchstart', listener)
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener('mousedown', listener)
|
|
||||||
document.removeEventListener('touchstart', listener)
|
|
||||||
}
|
|
||||||
}, [reference, handler])
|
|
||||||
}
|
|
||||||
@ -5,8 +5,6 @@ import { MENU } from './menu'
|
|||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const { pathname } = useLocation()
|
const { pathname } = useLocation()
|
||||||
const segments = pathname.split('/')
|
|
||||||
const path = segments.length > 3 ? segments.slice(0, 3).join('/') : pathname
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[calc(100dvh-80px)] flex-col gap-y-10 overflow-y-auto bg-white p-5">
|
<div className="flex min-h-[calc(100dvh-80px)] flex-col gap-y-10 overflow-y-auto bg-white p-5">
|
||||||
@ -21,19 +19,19 @@ export const Sidebar = () => {
|
|||||||
to={url}
|
to={url}
|
||||||
key={`${group}-${title}`}
|
key={`${group}-${title}`}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
path === url ? 'bg-[#707FDD]/10 font-bold' : '',
|
pathname === url ? 'bg-[#707FDD]/10 font-bold' : '',
|
||||||
'group/menu flex h-[42px] w-[200px] items-center gap-x-3 rounded-md px-5 transition-all hover:bg-[#707FDD]/10',
|
'group/menu flex h-[42px] w-[200px] items-center gap-x-3 rounded-md px-5 transition-all hover:bg-[#707FDD]/10',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon
|
<Icon
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
path === url ? 'text-[#5363AB]' : 'text-[#A6ABC8]',
|
pathname === url ? 'text-[#5363AB]' : 'text-[#A6ABC8]',
|
||||||
'h-[18px] w-[18px] transition-all group-hover/menu:text-[#5363AB]',
|
'h-[18px] w-[18px] transition-all group-hover/menu:text-[#5363AB]',
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<span
|
<span
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
path === url ? 'text-[#5363AB]' : 'text-[#273240]',
|
pathname === url ? 'text-[#5363AB]' : 'text-[#273240]',
|
||||||
'text-base transition-all group-hover/menu:text-[#5363AB]',
|
'text-base transition-all group-hover/menu:text-[#5363AB]',
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -1,188 +1,45 @@
|
|||||||
import { DevTool } from '@hookform/devtools'
|
import { Field, Input, Label, Select } from '@headlessui/react'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { useEffect, useState } from 'react'
|
|
||||||
import { useFetcher, useNavigate, useRouteLoaderData } from 'react-router'
|
|
||||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
|
||||||
import { z } from 'zod'
|
|
||||||
|
|
||||||
import { TextEditor } from '~/components/text-editor'
|
import { SearchIcon } from '~/components/icons/search'
|
||||||
import { Button } from '~/components/ui/button'
|
import DefaultTextEditor from '~/components/ui/text-editor'
|
||||||
import { Combobox } from '~/components/ui/combobox'
|
|
||||||
import { Input } from '~/components/ui/input'
|
|
||||||
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
import { TitleDashboard } from '~/components/ui/title-dashboard'
|
||||||
import type { loader } from '~/routes/_admin.lg-admin'
|
|
||||||
|
|
||||||
export const contentSchema = z.object({
|
|
||||||
categories: z
|
|
||||||
.array(
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
code: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
)
|
|
||||||
.refine((data) => !!data, {
|
|
||||||
message: 'Please select a category',
|
|
||||||
}),
|
|
||||||
tags: z
|
|
||||||
.array(
|
|
||||||
z
|
|
||||||
.object({
|
|
||||||
id: z.string(),
|
|
||||||
code: z.string(),
|
|
||||||
name: z.string(),
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.nullable(),
|
|
||||||
)
|
|
||||||
.optional(),
|
|
||||||
title: z.string().min(1, {
|
|
||||||
message: 'Judul is required',
|
|
||||||
}),
|
|
||||||
content: z.string().min(1, {
|
|
||||||
message: 'Konten is required',
|
|
||||||
}),
|
|
||||||
featured_image: z.string().optional(),
|
|
||||||
is_premium: z.boolean().optional(),
|
|
||||||
live_at: z.string().min(1, {
|
|
||||||
message: 'Tanggal live is required',
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
export type TContentSchema = z.infer<typeof contentSchema>
|
|
||||||
|
|
||||||
export const CreateContentsPage = () => {
|
export const CreateContentsPage = () => {
|
||||||
const fetcher = useFetcher()
|
|
||||||
const navigate = useNavigate()
|
|
||||||
const loaderData = useRouteLoaderData<typeof loader>('routes/_admin.lg-admin')
|
|
||||||
const categories = loaderData?.categoriesData
|
|
||||||
const tags = loaderData?.tagsData
|
|
||||||
const [error, setError] = useState<string>()
|
|
||||||
const [disabled, setDisabled] = useState(false)
|
|
||||||
|
|
||||||
const formMethods = useRemixForm<TContentSchema>({
|
|
||||||
mode: 'onSubmit',
|
|
||||||
fetcher,
|
|
||||||
resolver: zodResolver(contentSchema),
|
|
||||||
})
|
|
||||||
|
|
||||||
const { handleSubmit, control, watch } = formMethods
|
|
||||||
const watchCategories = watch('categories')
|
|
||||||
const watchTags = watch('tags')
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!fetcher.data?.success) {
|
|
||||||
setError(fetcher.data?.message)
|
|
||||||
setDisabled(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
navigate('/lg-admin/contents')
|
|
||||||
setDisabled(true)
|
|
||||||
setError(undefined)
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [fetcher])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<TitleDashboard title="Buat Artikel" />
|
<TitleDashboard title="Buat Artikel" />
|
||||||
<RemixFormProvider {...formMethods}>
|
<div className="mb-8 flex items-center gap-5 rounded-lg bg-gray-50 text-[#363636]">
|
||||||
<fetcher.Form
|
<div className="w-[400px]">
|
||||||
method="post"
|
<Field>
|
||||||
onSubmit={handleSubmit}
|
<Label className="mb-2 block text-sm font-medium">Pilih Tags</Label>
|
||||||
action="/actions/admin/contents/create"
|
<div className="relative">
|
||||||
className="space-y-4"
|
|
||||||
>
|
|
||||||
{error && (
|
|
||||||
<div className="text-sm text-red-500 capitalize">{error}</div>
|
|
||||||
)}
|
|
||||||
<div className="flex items-end justify-between gap-4">
|
|
||||||
<Input
|
<Input
|
||||||
id="title"
|
type="text"
|
||||||
label="Judul"
|
placeholder="Cari Tags"
|
||||||
placeholder="Masukkan Judul"
|
className="w-full rounded-lg bg-white p-2 pr-10 pl-4 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||||
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
|
<div className="absolute inset-y-0 right-0 flex items-center pr-3">
|
||||||
id="featured_image"
|
<SearchIcon className="h-5 w-5" />
|
||||||
label="Gambar Unggulan"
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
disabled={disabled}
|
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
className="text-md h-[42px] rounded-md"
|
|
||||||
>
|
|
||||||
Save
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end justify-between gap-4">
|
</div>
|
||||||
<Combobox
|
</Field>
|
||||||
multiple
|
|
||||||
id="categories"
|
|
||||||
name="categories"
|
|
||||||
label="Kategori"
|
|
||||||
placeholder={
|
|
||||||
watchCategories
|
|
||||||
? watchCategories.map((category) => category?.name).join(', ')
|
|
||||||
: 'Pilih Kategori'
|
|
||||||
}
|
|
||||||
options={categories}
|
|
||||||
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"
|
|
||||||
/>
|
|
||||||
<Combobox
|
|
||||||
multiple
|
|
||||||
id="tags"
|
|
||||||
name="tags"
|
|
||||||
label="Tags"
|
|
||||||
placeholder={
|
|
||||||
watchTags
|
|
||||||
? watchTags.map((tag) => tag?.name).join(', ')
|
|
||||||
: 'Pilih Tags'
|
|
||||||
}
|
|
||||||
options={tags}
|
|
||||||
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
|
|
||||||
id="live_at"
|
|
||||||
label="Tanggal Live"
|
|
||||||
placeholder="Pilih Tanggal"
|
|
||||||
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]"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TextEditor
|
<div className="w-[235px]">
|
||||||
id="content"
|
<Field>
|
||||||
name="content"
|
<Label className="mb-2 block text-sm font-medium">Status</Label>
|
||||||
label="Konten"
|
<Select className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none">
|
||||||
placeholder="Masukkan Konten"
|
<option>Pilih Status</option>
|
||||||
className="shadow"
|
<option>Aktif</option>
|
||||||
inputClassName="bg-white focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none border-0"
|
<option>Nonaktif</option>
|
||||||
labelClassName="text-sm font-medium text-[#363636]"
|
</Select>
|
||||||
category="content"
|
</Field>
|
||||||
/>
|
</div>
|
||||||
</fetcher.Form>
|
</div>
|
||||||
</RemixFormProvider>
|
|
||||||
|
|
||||||
<DevTool control={control} />
|
<section>
|
||||||
|
<DefaultTextEditor />
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,7 +91,7 @@ export const ContentsPage = () => {
|
|||||||
<Button
|
<Button
|
||||||
as={Link}
|
as={Link}
|
||||||
to="/lg-admin/contents/create"
|
to="/lg-admin/contents/create"
|
||||||
className="text-md h-[42px] rounded-md"
|
className="text-md rounded-md"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
Create New
|
Create New
|
||||||
|
|||||||
@ -1,8 +1,6 @@
|
|||||||
import { Outlet, redirect } from 'react-router'
|
import { Outlet, redirect } from 'react-router'
|
||||||
|
|
||||||
import { getStaff } from '~/apis/admin/get-staff'
|
import { getStaff } from '~/apis/admin/get-staff'
|
||||||
import { getCategories } from '~/apis/common/get-categories'
|
|
||||||
import { getTags } from '~/apis/common/get-tags'
|
|
||||||
import { AUTH_PAGES } from '~/configs/pages'
|
import { AUTH_PAGES } from '~/configs/pages'
|
||||||
import { AdminProvider } from '~/contexts/admin'
|
import { AdminProvider } from '~/contexts/admin'
|
||||||
import { AdminDefaultLayout } from '~/layouts/admin/default'
|
import { AdminDefaultLayout } from '~/layouts/admin/default'
|
||||||
@ -30,13 +28,9 @@ export const loader = async ({ request }: Route.LoaderArgs) => {
|
|||||||
})
|
})
|
||||||
staffData = data
|
staffData = data
|
||||||
}
|
}
|
||||||
const { data: categoriesData } = await getCategories()
|
|
||||||
const { data: tagsData } = await getTags()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
staffData,
|
staffData,
|
||||||
categoriesData,
|
|
||||||
tagsData,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,64 +0,0 @@
|
|||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
|
||||||
import { data } from 'react-router'
|
|
||||||
import { getValidatedFormData } from 'remix-hook-form'
|
|
||||||
import { XiorError } from 'xior'
|
|
||||||
|
|
||||||
import { createNewsRequest } from '~/apis/admin/create-news'
|
|
||||||
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 })
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data: newsData } = await createNewsRequest({
|
|
||||||
accessToken: staffToken,
|
|
||||||
payload,
|
|
||||||
})
|
|
||||||
|
|
||||||
return data(
|
|
||||||
{
|
|
||||||
success: true,
|
|
||||||
newsData,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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 },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
export const isHexCompatible = (hexColor?: string): boolean => {
|
|
||||||
if (hexColor === undefined) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const hexColorRegex = /^#([\dA-Fa-f]{6}|[\dA-Fa-f]{3})$/
|
|
||||||
return hexColorRegex.test(hexColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const rgbToHex = (rgb: string): string => {
|
|
||||||
// Extract the integers by matching against a regex
|
|
||||||
const result = rgb.match(/\d+/g)
|
|
||||||
|
|
||||||
if (!result) {
|
|
||||||
return '#000000' // Set default color to #000000 if the RGB string is invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
const [red, green, blue] = result.map(Number)
|
|
||||||
|
|
||||||
// Ensure the values are valid
|
|
||||||
if (
|
|
||||||
red < 0 ||
|
|
||||||
red > 255 ||
|
|
||||||
green < 0 ||
|
|
||||||
green > 255 ||
|
|
||||||
blue < 0 ||
|
|
||||||
blue > 255
|
|
||||||
) {
|
|
||||||
return '#000000' // Set default color to #000000 if the RGB values are invalid
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert each component to hex
|
|
||||||
const redHex = red.toString(16).padStart(2, '0')
|
|
||||||
const greenHex = green.toString(16).padStart(2, '0')
|
|
||||||
const blueHex = blue.toString(16).padStart(2, '0')
|
|
||||||
|
|
||||||
// Return the combined string
|
|
||||||
return `#${redHex}${greenHex}${blueHex}`
|
|
||||||
}
|
|
||||||
@ -17,16 +17,9 @@
|
|||||||
"@headlessui/react": "^2.2.0",
|
"@headlessui/react": "^2.2.0",
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"@hookform/resolvers": "^4.1.1",
|
"@hookform/resolvers": "^4.1.1",
|
||||||
"@monaco-editor/react": "^4.7.0",
|
|
||||||
"@react-router/fs-routes": "^7.1.3",
|
"@react-router/fs-routes": "^7.1.3",
|
||||||
"@react-router/node": "^7.1.3",
|
"@react-router/node": "^7.1.3",
|
||||||
"@react-router/serve": "^7.1.3",
|
"@react-router/serve": "^7.1.3",
|
||||||
"@tiptap/extension-color": "^2.11.5",
|
|
||||||
"@tiptap/extension-highlight": "^2.11.5",
|
|
||||||
"@tiptap/extension-image": "^2.11.5",
|
|
||||||
"@tiptap/extension-link": "^2.11.5",
|
|
||||||
"@tiptap/extension-text-align": "^2.11.5",
|
|
||||||
"@tiptap/extension-text-style": "^2.11.5",
|
|
||||||
"@tiptap/react": "^2.11.5",
|
"@tiptap/react": "^2.11.5",
|
||||||
"@tiptap/starter-kit": "^2.11.5",
|
"@tiptap/starter-kit": "^2.11.5",
|
||||||
"chart.js": "^4.4.8",
|
"chart.js": "^4.4.8",
|
||||||
@ -39,7 +32,6 @@
|
|||||||
"jose": "^6.0.8",
|
"jose": "^6.0.8",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-chartjs-2": "^5.3.0",
|
"react-chartjs-2": "^5.3.0",
|
||||||
"react-colorful": "^5.6.1",
|
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-router": "^7.1.3",
|
"react-router": "^7.1.3",
|
||||||
|
|||||||
121
pnpm-lock.yaml
generated
121
pnpm-lock.yaml
generated
@ -17,9 +17,6 @@ importers:
|
|||||||
'@hookform/resolvers':
|
'@hookform/resolvers':
|
||||||
specifier: ^4.1.1
|
specifier: ^4.1.1
|
||||||
version: 4.1.1(react-hook-form@7.54.2(react@19.0.0))
|
version: 4.1.1(react-hook-form@7.54.2(react@19.0.0))
|
||||||
'@monaco-editor/react':
|
|
||||||
specifier: ^4.7.0
|
|
||||||
version: 4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
|
||||||
'@react-router/fs-routes':
|
'@react-router/fs-routes':
|
||||||
specifier: ^7.1.3
|
specifier: ^7.1.3
|
||||||
version: 7.1.3(@react-router/dev@7.1.3(@react-router/serve@7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3))(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(lightningcss@1.29.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.16)(lightningcss@1.29.1)))(typescript@5.7.3)
|
version: 7.1.3(@react-router/dev@7.1.3(@react-router/serve@7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3))(@types/node@20.17.16)(babel-plugin-macros@3.1.0)(lightningcss@1.29.1)(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)(vite@5.4.14(@types/node@20.17.16)(lightningcss@1.29.1)))(typescript@5.7.3)
|
||||||
@ -29,24 +26,6 @@ importers:
|
|||||||
'@react-router/serve':
|
'@react-router/serve':
|
||||||
specifier: ^7.1.3
|
specifier: ^7.1.3
|
||||||
version: 7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)
|
version: 7.1.3(react-router@7.1.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(typescript@5.7.3)
|
||||||
'@tiptap/extension-color':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))
|
|
||||||
'@tiptap/extension-highlight':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
|
||||||
'@tiptap/extension-image':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
|
||||||
'@tiptap/extension-link':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)
|
|
||||||
'@tiptap/extension-text-align':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
|
||||||
'@tiptap/extension-text-style':
|
|
||||||
specifier: ^2.11.5
|
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
|
||||||
'@tiptap/react':
|
'@tiptap/react':
|
||||||
specifier: ^2.11.5
|
specifier: ^2.11.5
|
||||||
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@ -83,9 +62,6 @@ importers:
|
|||||||
react-chartjs-2:
|
react-chartjs-2:
|
||||||
specifier: ^5.3.0
|
specifier: ^5.3.0
|
||||||
version: 5.3.0(chart.js@4.4.8)(react@19.0.0)
|
version: 5.3.0(chart.js@4.4.8)(react@19.0.0)
|
||||||
react-colorful:
|
|
||||||
specifier: ^5.6.1
|
|
||||||
version: 5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
|
||||||
react-dom:
|
react-dom:
|
||||||
specifier: ^19.0.0
|
specifier: ^19.0.0
|
||||||
version: 19.0.0(react@19.0.0)
|
version: 19.0.0(react@19.0.0)
|
||||||
@ -732,16 +708,6 @@ packages:
|
|||||||
'@mjackson/node-fetch-server@0.2.0':
|
'@mjackson/node-fetch-server@0.2.0':
|
||||||
resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==}
|
resolution: {integrity: sha512-EMlH1e30yzmTpGLQjlFmaDAjyOeZhng1/XCd7DExR8PNAnG/G1tyruZxEoUe11ClnwGhGrtsdnyyUx1frSzjng==}
|
||||||
|
|
||||||
'@monaco-editor/loader@1.5.0':
|
|
||||||
resolution: {integrity: sha512-hKoGSM+7aAc7eRTRjpqAZucPmoNOC4UUbknb/VNoTkEIkCPhqV8LfbsgM1webRM7S/z21eHEx9Fkwx8Z/C/+Xw==}
|
|
||||||
|
|
||||||
'@monaco-editor/react@4.7.0':
|
|
||||||
resolution: {integrity: sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA==}
|
|
||||||
peerDependencies:
|
|
||||||
monaco-editor: '>= 0.25.0 < 1'
|
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
@ -1508,12 +1474,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-color@2.11.5':
|
|
||||||
resolution: {integrity: sha512-9gZF6EIpfOJYUt1TtFY37e8iqwKcOmBl8CkFaxq+4mWVvYd2D7KbA0r4tYTxSO0fOBJ5fA/1qJrpvgRlyocp/A==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
'@tiptap/extension-text-style': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-document@2.11.5':
|
'@tiptap/extension-document@2.11.5':
|
||||||
resolution: {integrity: sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==}
|
resolution: {integrity: sha512-7I4BRTpIux2a0O2qS3BDmyZ5LGp3pszKbix32CmeVh7lN9dV7W5reDqtJJ9FCZEEF+pZ6e1/DQA362dflwZw2g==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1547,11 +1507,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-highlight@2.11.5':
|
|
||||||
resolution: {integrity: sha512-VBZfT869L9CiTLF8qr+3FBUtJcmlyUTECORNo0ceEiNDg4H6V9uNPwaROMXrWiQCc+DYVCOkx541QrXwNMzxlg==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-history@2.11.5':
|
'@tiptap/extension-history@2.11.5':
|
||||||
resolution: {integrity: sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==}
|
resolution: {integrity: sha512-b+wOS33Dz1azw6F1i9LFTEIJ/gUui0Jwz5ZvmVDpL2ZHBhq1Ui0/spTT+tuZOXq7Y/uCbKL8Liu4WoedIvhboQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1564,22 +1519,11 @@ packages:
|
|||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
'@tiptap/pm': ^2.7.0
|
'@tiptap/pm': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-image@2.11.5':
|
|
||||||
resolution: {integrity: sha512-HbUq9AL8gb8eSuQfY/QKkvMc66ZFN/b6jvQAILGArNOgalUfGizoC6baKTJShaExMSPjBZlaAHtJiQKPaGRHaA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-italic@2.11.5':
|
'@tiptap/extension-italic@2.11.5':
|
||||||
resolution: {integrity: sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==}
|
resolution: {integrity: sha512-9VGfb2/LfPhQ6TjzDwuYLRvw0A6VGbaIp3F+5Mql8XVdTBHb2+rhELbyhNGiGVR78CaB/EiKb6dO9xu/tBWSYA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-link@2.11.5':
|
|
||||||
resolution: {integrity: sha512-4Iu/aPzevbYpe50xDI0ZkqRa6nkZ9eF270Ue2qaF3Ab47nehj+9Jl78XXzo8+LTyFMnrETI73TAs1aC/IGySeQ==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
'@tiptap/pm': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-list-item@2.11.5':
|
'@tiptap/extension-list-item@2.11.5':
|
||||||
resolution: {integrity: sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==}
|
resolution: {integrity: sha512-Mp5RD/pbkfW1vdc6xMVxXYcta73FOwLmblQlFNn/l/E5/X1DUSA4iGhgDDH4EWO3swbs03x2f7Zka/Xoj3+WLg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -1600,11 +1544,6 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@tiptap/core': ^2.7.0
|
'@tiptap/core': ^2.7.0
|
||||||
|
|
||||||
'@tiptap/extension-text-align@2.11.5':
|
|
||||||
resolution: {integrity: sha512-Ei0zDpH5N9EV59ogydK4HTKa4lCPicCsQllM5n/Nf2tUJPir3aiYxzJ73FzhComD4Hpo1ANYnmssBhy8QeoPZA==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.7.0
|
|
||||||
|
|
||||||
'@tiptap/extension-text-style@2.11.5':
|
'@tiptap/extension-text-style@2.11.5':
|
||||||
resolution: {integrity: sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==}
|
resolution: {integrity: sha512-YUmYl0gILSd/u/ZkOmNxjNXVw+mu8fpC2f8G4I4tLODm0zCx09j9DDEJXSrM5XX72nxJQqtSQsCpNKnL0hfeEQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -3234,9 +3173,6 @@ packages:
|
|||||||
linkify-it@5.0.0:
|
linkify-it@5.0.0:
|
||||||
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==}
|
||||||
|
|
||||||
linkifyjs@4.2.0:
|
|
||||||
resolution: {integrity: sha512-pCj3PrQyATaoTYKHrgWRF3SJwsm61udVh+vuls/Rl6SptiDhgE7ziUIudAedRY9QEfynmM7/RmLEfPUyw1HPCw==}
|
|
||||||
|
|
||||||
lint-staged@15.4.3:
|
lint-staged@15.4.3:
|
||||||
resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==}
|
resolution: {integrity: sha512-FoH1vOeouNh1pw+90S+cnuoFwRfUD9ijY2GKy5h7HS3OR7JVir2N2xrsa0+Twc1B7cW72L+88geG5cW4wIhn7g==}
|
||||||
engines: {node: '>=18.12.0'}
|
engines: {node: '>=18.12.0'}
|
||||||
@ -3401,9 +3337,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||||
engines: {node: '>=16 || 14 >=14.17'}
|
engines: {node: '>=16 || 14 >=14.17'}
|
||||||
|
|
||||||
monaco-editor@0.52.2:
|
|
||||||
resolution: {integrity: sha512-GEQWEZmfkOGLdd3XK8ryrfWz3AIP8YymVXiPHEdewrUq7mh0qrKrfHLNCXcbB6sTnMLnOZ3ztSiKcciFUkIJwQ==}
|
|
||||||
|
|
||||||
morgan@1.10.0:
|
morgan@1.10.0:
|
||||||
resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
|
resolution: {integrity: sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==}
|
||||||
engines: {node: '>= 0.8.0'}
|
engines: {node: '>= 0.8.0'}
|
||||||
@ -3858,12 +3791,6 @@ packages:
|
|||||||
chart.js: ^4.1.1
|
chart.js: ^4.1.1
|
||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||||
|
|
||||||
react-colorful@5.6.1:
|
|
||||||
resolution: {integrity: sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==}
|
|
||||||
peerDependencies:
|
|
||||||
react: '>=16.8.0'
|
|
||||||
react-dom: '>=16.8.0'
|
|
||||||
|
|
||||||
react-d3-tree@3.6.2:
|
react-d3-tree@3.6.2:
|
||||||
resolution: {integrity: sha512-1ExQlmEnv5iOw9XfZ3EcESDjzGXVKPAmyDJTJbvVfiwkplZtP7CcNEY0tKZf4XSW0FzYJf4aFXprGJen+95yuw==}
|
resolution: {integrity: sha512-1ExQlmEnv5iOw9XfZ3EcESDjzGXVKPAmyDJTJbvVfiwkplZtP7CcNEY0tKZf4XSW0FzYJf4aFXprGJen+95yuw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -4208,9 +4135,6 @@ packages:
|
|||||||
stable-hash@0.0.4:
|
stable-hash@0.0.4:
|
||||||
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
resolution: {integrity: sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==}
|
||||||
|
|
||||||
state-local@1.0.7:
|
|
||||||
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
|
|
||||||
|
|
||||||
statuses@2.0.1:
|
statuses@2.0.1:
|
||||||
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
resolution: {integrity: sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
@ -5290,17 +5214,6 @@ snapshots:
|
|||||||
|
|
||||||
'@mjackson/node-fetch-server@0.2.0': {}
|
'@mjackson/node-fetch-server@0.2.0': {}
|
||||||
|
|
||||||
'@monaco-editor/loader@1.5.0':
|
|
||||||
dependencies:
|
|
||||||
state-local: 1.0.7
|
|
||||||
|
|
||||||
'@monaco-editor/react@4.7.0(monaco-editor@0.52.2)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)':
|
|
||||||
dependencies:
|
|
||||||
'@monaco-editor/loader': 1.5.0
|
|
||||||
monaco-editor: 0.52.2
|
|
||||||
react: 19.0.0
|
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@nodelib/fs.stat': 2.0.5
|
'@nodelib/fs.stat': 2.0.5
|
||||||
@ -6035,11 +5948,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
|
|
||||||
'@tiptap/extension-color@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5)))':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
|
||||||
'@tiptap/extension-text-style': 2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))
|
|
||||||
|
|
||||||
'@tiptap/extension-document@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
'@tiptap/extension-document@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -6068,10 +5976,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
|
|
||||||
'@tiptap/extension-highlight@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
|
||||||
|
|
||||||
'@tiptap/extension-history@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)':
|
'@tiptap/extension-history@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -6082,20 +5986,10 @@ snapshots:
|
|||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
'@tiptap/pm': 2.11.5
|
'@tiptap/pm': 2.11.5
|
||||||
|
|
||||||
'@tiptap/extension-image@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
|
||||||
|
|
||||||
'@tiptap/extension-italic@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
'@tiptap/extension-italic@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
|
|
||||||
'@tiptap/extension-link@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))(@tiptap/pm@2.11.5)':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
|
||||||
'@tiptap/pm': 2.11.5
|
|
||||||
linkifyjs: 4.2.0
|
|
||||||
|
|
||||||
'@tiptap/extension-list-item@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
'@tiptap/extension-list-item@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -6112,10 +6006,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
|
|
||||||
'@tiptap/extension-text-align@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
|
||||||
|
|
||||||
'@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
'@tiptap/extension-text-style@2.11.5(@tiptap/core@2.11.5(@tiptap/pm@2.11.5))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
'@tiptap/core': 2.11.5(@tiptap/pm@2.11.5)
|
||||||
@ -7992,8 +7882,6 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro: 2.1.0
|
uc.micro: 2.1.0
|
||||||
|
|
||||||
linkifyjs@4.2.0: {}
|
|
||||||
|
|
||||||
lint-staged@15.4.3:
|
lint-staged@15.4.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
chalk: 5.4.1
|
chalk: 5.4.1
|
||||||
@ -8140,8 +8028,6 @@ snapshots:
|
|||||||
|
|
||||||
minipass@7.1.2: {}
|
minipass@7.1.2: {}
|
||||||
|
|
||||||
monaco-editor@0.52.2: {}
|
|
||||||
|
|
||||||
morgan@1.10.0:
|
morgan@1.10.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
basic-auth: 2.0.1
|
basic-auth: 2.0.1
|
||||||
@ -8571,11 +8457,6 @@ snapshots:
|
|||||||
chart.js: 4.4.8
|
chart.js: 4.4.8
|
||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
|
|
||||||
react-colorful@5.6.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
|
||||||
dependencies:
|
|
||||||
react: 19.0.0
|
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
|
||||||
|
|
||||||
react-d3-tree@3.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
react-d3-tree@3.6.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@bkrem/react-transition-group': 1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
'@bkrem/react-transition-group': 1.3.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@ -8995,8 +8876,6 @@ snapshots:
|
|||||||
|
|
||||||
stable-hash@0.0.4: {}
|
stable-hash@0.0.4: {}
|
||||||
|
|
||||||
state-local@1.0.7: {}
|
|
||||||
|
|
||||||
statuses@2.0.1: {}
|
statuses@2.0.1: {}
|
||||||
|
|
||||||
stream-shift@1.0.3: {}
|
stream-shift@1.0.3: {}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user