Compare commits
7 Commits
dfd1a46694
...
ea6462f3ea
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea6462f3ea | ||
|
|
6f0a21dde6 | ||
|
|
07cced218e | ||
|
|
9a2d64eee4 | ||
|
|
438d1d40db | ||
|
|
9a5bddef6a | ||
|
|
f97c79aaa9 |
29
app/apis/admin/delete-subscribe-plan.ts
Normal file
29
app/apis/admin/delete-subscribe-plan.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TSubscribePlanSchema } from '~/pages/form-subscriptions-plan'
|
||||
|
||||
const subscribePlanResponseSchema = z.object({
|
||||
data: z.object({
|
||||
Message: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
type TTSubscribePlanId = Pick<TSubscribePlanSchema, 'id'>
|
||||
type TParameters = {
|
||||
payload: TTSubscribePlanId
|
||||
} & THttpServer
|
||||
|
||||
export const deleteSubscribePlanRequest = async (parameters: TParameters) => {
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { id } = payload
|
||||
try {
|
||||
const { data } = await HttpServer(restParameters).delete(
|
||||
`/api/subscribe-plan/${id}/delete`,
|
||||
)
|
||||
return subscribePlanResponseSchema.parse(data)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
30
app/apis/admin/delete-tags.ts
Normal file
30
app/apis/admin/delete-tags.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
import type { TTagSchema } from '~/pages/form-tag'
|
||||
|
||||
const deleteTagsResponseSchema = z.object({
|
||||
data: z.object({
|
||||
Message: z.string(),
|
||||
}),
|
||||
})
|
||||
|
||||
type TTagsId = Pick<TTagSchema, 'id'>
|
||||
type TParameters = {
|
||||
payload: TTagsId
|
||||
} & THttpServer
|
||||
|
||||
export type TDeleteTagsSchema = z.infer<typeof deleteTagsResponseSchema>
|
||||
export const deleteTagsRequest = async (parameters: TParameters) => {
|
||||
const { payload, ...restParameters } = parameters
|
||||
const { id } = payload
|
||||
try {
|
||||
const { data } = await HttpServer(restParameters).delete(
|
||||
`/api/tag/${id}/delete`,
|
||||
)
|
||||
return deleteTagsResponseSchema.parse(data)
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line unicorn/no-useless-promise-resolve-reject
|
||||
return Promise.reject(error)
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,6 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
import type { TUploadSchema } from '~/layouts/admin/form-upload'
|
||||
import type { TUploadSchema } from '~/layouts/admin/dialog-upload'
|
||||
import { HttpServer, type THttpServer } from '~/libs/http-server'
|
||||
|
||||
const uploadResponseSchema = z.object({
|
||||
|
||||
@ -8,6 +8,9 @@ const subscriptionResponseSchema = z.object({
|
||||
id: z.string(),
|
||||
code: z.string(),
|
||||
name: z.string(),
|
||||
length: z.number().optional(),
|
||||
price: z.number().optional(),
|
||||
status: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
})
|
||||
|
||||
@ -127,7 +127,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive('bold')}
|
||||
title="Bold"
|
||||
>
|
||||
<BoldIcon />
|
||||
<BoldIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().toggleItalic().run()}
|
||||
@ -137,7 +137,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive('italic')}
|
||||
title="Italic"
|
||||
>
|
||||
<ItalicIcon />
|
||||
<ItalicIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().toggleStrike().run()}
|
||||
@ -147,7 +147,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive('strike')}
|
||||
title="Strike"
|
||||
>
|
||||
<StrikethroughIcon />
|
||||
<StrikethroughIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<div className="relative">
|
||||
<EditorButton
|
||||
@ -159,7 +159,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={true}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SwatchIcon />
|
||||
<SwatchIcon className="size-4" />
|
||||
</EditorButton>
|
||||
{isOpenColor && (
|
||||
<div
|
||||
@ -191,7 +191,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive({ textAlign: 'left' })}
|
||||
title="Align Left"
|
||||
>
|
||||
<Bars3BottomLeftIcon />
|
||||
<Bars3BottomLeftIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('center').run()}
|
||||
@ -202,7 +202,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive({ textAlign: 'center' })}
|
||||
title="Align Center"
|
||||
>
|
||||
<Bars3Icon />
|
||||
<Bars3Icon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('right').run()}
|
||||
@ -213,7 +213,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive({ textAlign: 'right' })}
|
||||
title="Align Right"
|
||||
>
|
||||
<Bars3BottomRightIcon />
|
||||
<Bars3BottomRightIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().setTextAlign('justify').run()}
|
||||
@ -224,7 +224,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive({ textAlign: 'justify' })}
|
||||
title="Align Justify"
|
||||
>
|
||||
<Bars4Icon />
|
||||
<Bars4Icon className="size-4" />
|
||||
</EditorButton>
|
||||
</div>
|
||||
<div className="flex max-w-[150px] flex-wrap items-start gap-1 px-1">
|
||||
@ -236,7 +236,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Heading 1"
|
||||
disabled={disabled}
|
||||
>
|
||||
<H1Icon />
|
||||
<H1Icon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() =>
|
||||
@ -246,7 +246,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Heading 2"
|
||||
disabled={disabled}
|
||||
>
|
||||
<H2Icon />
|
||||
<H2Icon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() =>
|
||||
@ -256,7 +256,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Heading 3"
|
||||
disabled={disabled}
|
||||
>
|
||||
<H3Icon />
|
||||
<H3Icon className="size-4" />
|
||||
</EditorButton>
|
||||
{/* <EditorButton
|
||||
onClick={() => editor.chain().focus().setParagraph().run()}
|
||||
@ -272,7 +272,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Bullet List"
|
||||
disabled={disabled}
|
||||
>
|
||||
<ListBulletIcon />
|
||||
<ListBulletIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().toggleOrderedList().run()}
|
||||
@ -280,7 +280,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Ordered List"
|
||||
disabled={disabled}
|
||||
>
|
||||
<NumberedListIcon />
|
||||
<NumberedListIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().toggleCodeBlock().run()}
|
||||
@ -288,7 +288,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Code Block"
|
||||
disabled={disabled}
|
||||
>
|
||||
<CodeBracketIcon />
|
||||
<CodeBracketIcon className="size-4" />
|
||||
</EditorButton>
|
||||
</div>
|
||||
{/* <div className="flex items-start gap-1 px-1">
|
||||
@ -334,7 +334,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
title="Insert Image"
|
||||
disabled={disabled}
|
||||
>
|
||||
<PhotoIcon />
|
||||
<PhotoIcon className="size-4" />
|
||||
</EditorButton>
|
||||
{isOpenImage && (
|
||||
<div
|
||||
@ -380,14 +380,14 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
isActive={editor.isActive('link')}
|
||||
title="Set Link"
|
||||
>
|
||||
<LinkIcon />
|
||||
<LinkIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().unsetLink().run()}
|
||||
disabled={disabled || !editor.isActive('link')}
|
||||
title="Unset Link"
|
||||
>
|
||||
<LinkSlashIcon />
|
||||
<LinkSlashIcon className="size-4" />
|
||||
</EditorButton>
|
||||
</div>
|
||||
<div className="flex items-start gap-1 px-1">
|
||||
@ -396,14 +396,14 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
disabled={disabled || !editor.can().chain().focus().undo().run()}
|
||||
title="Undo"
|
||||
>
|
||||
<ArrowUturnLeftIcon />
|
||||
<ArrowUturnLeftIcon className="size-4" />
|
||||
</EditorButton>
|
||||
<EditorButton
|
||||
onClick={() => editor.chain().focus().redo().run()}
|
||||
disabled={disabled || !editor.can().chain().focus().redo().run()}
|
||||
title="Redo"
|
||||
>
|
||||
<ArrowUturnRightIcon />
|
||||
<ArrowUturnRightIcon className="size-4" />
|
||||
</EditorButton>
|
||||
</div>
|
||||
</div>
|
||||
@ -413,7 +413,7 @@ export const EditorMenuBar = (properties: TProperties) => {
|
||||
onClick={() => setIsPlainHTML(true)}
|
||||
title="Switch to Plain Text"
|
||||
>
|
||||
<DocumentTextIcon />
|
||||
<DocumentTextIcon className="size-4" />
|
||||
</EditorButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,15 +1,11 @@
|
||||
import { Dialog, DialogBackdrop, DialogPanel } from '@headlessui/react'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
|
||||
import { useAdminContext } from '~/contexts/admin'
|
||||
|
||||
import { FormUpload } from './form-upload'
|
||||
import { DialogUpload } from './dialog-upload'
|
||||
import { Navbar } from './navbar'
|
||||
import { Sidebar } from './sidebar'
|
||||
|
||||
export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
||||
const { children } = properties
|
||||
const { isUploadOpen, setIsUploadOpen } = useAdminContext()
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<Navbar />
|
||||
@ -18,27 +14,7 @@ export const AdminDashboardLayout = (properties: PropsWithChildren) => {
|
||||
<div className="min-h-[calc(100dvh-80px)] flex-1 p-8">{children}</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={!!isUploadOpen}
|
||||
onClose={() => {
|
||||
setIsUploadOpen(undefined)
|
||||
}}
|
||||
className="relative z-50"
|
||||
transition
|
||||
>
|
||||
<DialogBackdrop
|
||||
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
|
||||
transition
|
||||
/>
|
||||
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
||||
<DialogPanel
|
||||
transition
|
||||
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
||||
>
|
||||
<FormUpload />
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
<DialogUpload />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
143
app/layouts/admin/dialog-upload.tsx
Normal file
143
app/layouts/admin/dialog-upload.tsx
Normal file
@ -0,0 +1,143 @@
|
||||
import { Dialog, DialogBackdrop, DialogPanel, Input } from '@headlessui/react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useEffect, useState, type ChangeEvent } from 'react'
|
||||
import { useFetcher } from 'react-router'
|
||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { uploadCategorySchema, useAdminContext } from '~/contexts/admin'
|
||||
|
||||
export const uploadSchema = z.object({
|
||||
file: z.instanceof(File),
|
||||
category: uploadCategorySchema,
|
||||
})
|
||||
|
||||
export type TUploadSchema = z.infer<typeof uploadSchema>
|
||||
|
||||
export const DialogUpload = () => {
|
||||
const { isUploadOpen, setUploadedFile, setIsUploadOpen } = useAdminContext()
|
||||
const fetcher = useFetcher()
|
||||
const [error, setError] = useState<string>()
|
||||
const maxFileSize = 10 * 1024 // 10MB
|
||||
|
||||
const formMethods = useRemixForm<TUploadSchema>({
|
||||
mode: 'onSubmit',
|
||||
fetcher,
|
||||
resolver: zodResolver(uploadSchema),
|
||||
})
|
||||
|
||||
const { handleSubmit, register, setValue } = formMethods
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetcher.data?.success) {
|
||||
setError(fetcher.data?.message)
|
||||
return
|
||||
}
|
||||
|
||||
setUploadedFile(fetcher.data.uploadData.data.file_url)
|
||||
|
||||
setError(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetcher])
|
||||
|
||||
const handleChange = async function (event: ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const files: File[] = [...event.target.files]
|
||||
|
||||
onChange(files, event)
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = async function (
|
||||
files: File[],
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
const file = files[0]
|
||||
const img = new Image()
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setError('Please upload an image file.')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > maxFileSize * 1024) {
|
||||
setError(`File size is too big!`)
|
||||
return
|
||||
}
|
||||
|
||||
img.addEventListener('load', () => {
|
||||
handleFiles(event)
|
||||
})
|
||||
|
||||
img.src = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0]
|
||||
setValue('file', file)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={!!isUploadOpen}
|
||||
onClose={() => {
|
||||
if (fetcher.state === 'idle') {
|
||||
setIsUploadOpen(undefined)
|
||||
}
|
||||
}}
|
||||
className="relative z-50"
|
||||
transition
|
||||
>
|
||||
<DialogBackdrop
|
||||
className="fixed inset-0 bg-black/50 duration-300 ease-out data-[closed]:opacity-0"
|
||||
transition
|
||||
/>
|
||||
<div className="fixed inset-0 flex w-screen justify-center overflow-y-auto p-0 max-sm:bg-white sm:items-center sm:p-4">
|
||||
<DialogPanel
|
||||
transition
|
||||
className="max-w-lg space-y-6 rounded-lg bg-white p-8 duration-300 ease-out data-[closed]:scale-95 data-[closed]:opacity-0 sm:shadow-lg"
|
||||
>
|
||||
<RemixFormProvider {...formMethods}>
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
action="/actions/admin/upload"
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 capitalize">{error}</div>
|
||||
)}
|
||||
<Input
|
||||
type="file"
|
||||
id="input-file-upload"
|
||||
accept="image/*"
|
||||
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="input-file-upload-type"
|
||||
value={isUploadOpen}
|
||||
{...register('category')}
|
||||
/>
|
||||
<Button
|
||||
disabled={fetcher.state !== 'idle'}
|
||||
isLoading={fetcher.state !== 'idle'}
|
||||
type="submit"
|
||||
className="w-full rounded-md py-2"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
</RemixFormProvider>
|
||||
</DialogPanel>
|
||||
</div>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
import { Input } from '@headlessui/react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useEffect, useState, type ChangeEvent } from 'react'
|
||||
import { useFetcher } from 'react-router'
|
||||
import { RemixFormProvider, useRemixForm } from 'remix-hook-form'
|
||||
import { z } from 'zod'
|
||||
|
||||
import { Button } from '~/components/ui/button'
|
||||
import { uploadCategorySchema, useAdminContext } from '~/contexts/admin'
|
||||
|
||||
export const uploadSchema = z.object({
|
||||
file: z.instanceof(File),
|
||||
category: uploadCategorySchema,
|
||||
})
|
||||
|
||||
export type TUploadSchema = z.infer<typeof uploadSchema>
|
||||
|
||||
export const FormUpload = () => {
|
||||
const { isUploadOpen, setUploadedFile } = useAdminContext()
|
||||
const fetcher = useFetcher()
|
||||
const [error, setError] = useState<string>()
|
||||
const maxFileSize = 10 * 1024 // 10MB
|
||||
|
||||
const formMethods = useRemixForm<TUploadSchema>({
|
||||
mode: 'onSubmit',
|
||||
fetcher,
|
||||
resolver: zodResolver(uploadSchema),
|
||||
})
|
||||
|
||||
const { handleSubmit, register, setValue } = formMethods
|
||||
|
||||
useEffect(() => {
|
||||
if (!fetcher.data?.success) {
|
||||
setError(fetcher.data?.message)
|
||||
return
|
||||
}
|
||||
|
||||
setUploadedFile(fetcher.data.uploadData.data.file_url)
|
||||
|
||||
setError(undefined)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [fetcher])
|
||||
|
||||
const handleChange = async function (event: ChangeEvent<HTMLInputElement>) {
|
||||
event.preventDefault()
|
||||
if (event.target.files && event.target.files[0]) {
|
||||
const files: File[] = [...event.target.files]
|
||||
|
||||
onChange(files, event)
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = async function (
|
||||
files: File[],
|
||||
event: ChangeEvent<HTMLInputElement>,
|
||||
) {
|
||||
const file = files[0]
|
||||
const img = new Image()
|
||||
|
||||
if (!file.type.startsWith('image/')) {
|
||||
setError('Please upload an image file.')
|
||||
return
|
||||
}
|
||||
|
||||
if (file.size > maxFileSize * 1024) {
|
||||
setError(`File size is too big!`)
|
||||
return
|
||||
}
|
||||
|
||||
img.addEventListener('load', () => {
|
||||
handleFiles(event)
|
||||
})
|
||||
|
||||
img.src = URL.createObjectURL(file)
|
||||
}
|
||||
|
||||
const handleFiles = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const files = event.target.files
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0]
|
||||
setValue('file', file)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<RemixFormProvider {...formMethods}>
|
||||
<fetcher.Form
|
||||
method="post"
|
||||
onSubmit={handleSubmit}
|
||||
className="space-y-4"
|
||||
action="/actions/admin/upload"
|
||||
encType="multipart/form-data"
|
||||
>
|
||||
{error && (
|
||||
<div className="text-sm text-red-500 capitalize">{error}</div>
|
||||
)}
|
||||
<Input
|
||||
type="file"
|
||||
id="input-file-upload"
|
||||
accept="image/*"
|
||||
className="h-[42px] w-full cursor-pointer rounded-md border border-[#DFDFDF] p-2"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
id="input-file-upload-type"
|
||||
value={isUploadOpen}
|
||||
{...register('category')}
|
||||
/>
|
||||
<Button
|
||||
disabled={fetcher.state !== 'idle'}
|
||||
isLoading={fetcher.state !== 'idle'}
|
||||
type="submit"
|
||||
className="w-full rounded-md py-2"
|
||||
>
|
||||
Upload
|
||||
</Button>
|
||||
</fetcher.Form>
|
||||
</RemixFormProvider>
|
||||
)
|
||||
}
|
||||
@ -60,7 +60,9 @@ export const ContentsPage = () => {
|
||||
2: (_value: unknown, _type: unknown, data: TNewsResponse) => (
|
||||
<div>
|
||||
<div>{data.author.name}</div>
|
||||
<div className="text-sm text-[#7C7C7C]">ID: {data.id.slice(0, 8)}</div>
|
||||
<div className="text-sm text-[#7C7C7C]">
|
||||
ID: {data.author.id.slice(0, 8)}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
3: (value: string) => <span className="text-sm">{value}</span>,
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { Field, Label, Select } from '@headlessui/react'
|
||||
import { zodResolver } from '@hookform/resolvers/zod'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useFetcher, useNavigate } from 'react-router'
|
||||
@ -13,6 +14,9 @@ export const createSubscribePlanSchema = z.object({
|
||||
id: z.string().optional(),
|
||||
name: z.string().min(3, 'Nama minimal 3 karakter'),
|
||||
code: z.string(),
|
||||
length: z.preprocess(Number, z.number().optional()),
|
||||
price: z.preprocess(Number, z.number().optional()),
|
||||
status: z.boolean().optional(),
|
||||
})
|
||||
export type TSubscribePlanSchema = z.infer<typeof createSubscribePlanSchema>
|
||||
type TProperties = {
|
||||
@ -31,6 +35,9 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
|
||||
id: subscribePlanData?.id || undefined,
|
||||
code: subscribePlanData?.code || '',
|
||||
name: subscribePlanData?.name || '',
|
||||
length: subscribePlanData?.length || undefined,
|
||||
price: subscribePlanData?.price || undefined,
|
||||
status: subscribePlanData?.status || undefined,
|
||||
},
|
||||
})
|
||||
const [error, setError] = useState<string>()
|
||||
@ -99,6 +106,41 @@ export const FormSubscribePlanPage = (properties: TProperties) => {
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-end justify-between gap-4">
|
||||
<Input
|
||||
id="length"
|
||||
label="Length"
|
||||
type="number"
|
||||
placeholder="Masukkan Subscribe Plan Length (days)"
|
||||
name="length"
|
||||
className="border-0 bg-white shadow read-only:bg-gray-100 focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||
labelClassName="text-sm font-medium text-[#363636]"
|
||||
containerClassName="flex-1"
|
||||
/>
|
||||
<Input
|
||||
id="price"
|
||||
label="Price"
|
||||
placeholder="Masukkan Price"
|
||||
type="number"
|
||||
name="price"
|
||||
className="border-0 bg-white shadow read-only:bg-gray-100 focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||
labelClassName="text-sm font-medium text-[#363636]"
|
||||
containerClassName="flex-1"
|
||||
/>
|
||||
|
||||
<Field className={'flex-1'}>
|
||||
<Label className="mb-2 block text-sm font-medium">Status</Label>
|
||||
<Select
|
||||
name="status"
|
||||
id="status"
|
||||
className="w-full rounded-lg bg-white p-2 shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none"
|
||||
>
|
||||
<option disabled>Pilih Status</option>
|
||||
<option value={1}>Aktif</option>
|
||||
<option value={0}>Nonaktif</option>
|
||||
</Select>
|
||||
</Field>
|
||||
</div>
|
||||
</fetcher.Form>
|
||||
</RemixFormProvider>
|
||||
</div>
|
||||
|
||||
@ -29,7 +29,7 @@ export const action = async ({ request }: Route.ActionArgs) => {
|
||||
return data({ success: false, errors, defaultValues }, { status: 400 })
|
||||
}
|
||||
|
||||
const { data: tagsData } = await createSubscribePlanRequest({
|
||||
const { data: subscribePlanData } = await createSubscribePlanRequest({
|
||||
accessToken: staffToken,
|
||||
payload,
|
||||
})
|
||||
@ -37,7 +37,7 @@ export const action = async ({ request }: Route.ActionArgs) => {
|
||||
return data(
|
||||
{
|
||||
success: true,
|
||||
tagsData,
|
||||
subscribePlanData,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
|
||||
@ -5,8 +5,10 @@ import { XiorError } from 'xior'
|
||||
|
||||
import { updateSubscribePlanRequest } from '~/apis/admin/update-subscribe-plan'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
import type { TSubscribePlanSchema } from '~/pages/form-subscriptions-plan'
|
||||
import { createTagSchema } from '~/pages/form-tag'
|
||||
import {
|
||||
createSubscribePlanSchema,
|
||||
type TSubscribePlanSchema,
|
||||
} from '~/pages/form-subscriptions-plan'
|
||||
|
||||
import type { Route } from './+types/actions.register'
|
||||
|
||||
@ -19,7 +21,7 @@ export const action = async ({ request }: Route.ActionArgs) => {
|
||||
receivedValues: defaultValues,
|
||||
} = await getValidatedFormData<TSubscribePlanSchema>(
|
||||
request,
|
||||
zodResolver(createTagSchema),
|
||||
zodResolver(createSubscribePlanSchema),
|
||||
false,
|
||||
)
|
||||
|
||||
@ -27,7 +29,7 @@ export const action = async ({ request }: Route.ActionArgs) => {
|
||||
return data({ success: false, errors, defaultValues }, { status: 400 })
|
||||
}
|
||||
|
||||
const { data: tagData } = await updateSubscribePlanRequest({
|
||||
const { data: subscribePlanData } = await updateSubscribePlanRequest({
|
||||
accessToken: staffToken,
|
||||
payload,
|
||||
})
|
||||
@ -35,7 +37,7 @@ export const action = async ({ request }: Route.ActionArgs) => {
|
||||
return data(
|
||||
{
|
||||
success: true,
|
||||
tagData,
|
||||
subscribePlanData,
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
|
||||
@ -4,7 +4,7 @@ import { getValidatedFormData } from 'remix-hook-form'
|
||||
import { XiorError } from 'xior'
|
||||
|
||||
import { uploadFileRequest } from '~/apis/admin/upload-file'
|
||||
import { uploadSchema, type TUploadSchema } from '~/layouts/admin/form-upload'
|
||||
import { uploadSchema, type TUploadSchema } from '~/layouts/admin/dialog-upload'
|
||||
import { handleCookie } from '~/libs/cookies'
|
||||
|
||||
import type { Route } from './+types/actions.register'
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user