diff --git a/app/apis/admin/create-category.ts b/app/apis/admin/create-category.ts index b18165b..a98e922 100644 --- a/app/apis/admin/create-category.ts +++ b/app/apis/admin/create-category.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { HttpServer } from '~/libs/http-server' -import type { TCategorySchema } from '~/pages/dashboard-category-create' +import type { TCategorySchema } from '~/pages/form-category' const categoryResponseSchema = z.object({ data: z.object({ @@ -17,14 +17,9 @@ type TParameters = { export const createCategoryRequest = async (parameters: TParameters) => { const { accessToken, payload } = parameters try { - const { ...restPayload } = payload - const transformedPayload = { - ...restPayload, - } - const { data } = await HttpServer({ accessToken }).post( '/api/category/create', - transformedPayload, + payload, ) return categoryResponseSchema.parse(data) } catch (error) { diff --git a/app/apis/admin/create-news.ts b/app/apis/admin/create-news.ts index 0f1ab84..f3ac04f 100644 --- a/app/apis/admin/create-news.ts +++ b/app/apis/admin/create-news.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { HttpServer } from '~/libs/http-server' -import type { TContentSchema } from '~/pages/contents-form' +import type { TContentSchema } from '~/pages/form-contents' const newsResponseSchema = z.object({ data: z.object({ diff --git a/app/apis/admin/update-category.ts b/app/apis/admin/update-category.ts new file mode 100644 index 0000000..0be5e0f --- /dev/null +++ b/app/apis/admin/update-category.ts @@ -0,0 +1,30 @@ +import { z } from 'zod' + +import { HttpServer } from '~/libs/http-server' +import type { TCategorySchema } from '~/pages/form-category' + +const categoryResponseSchema = z.object({ + data: z.object({ + Message: z.string(), + }), +}) + +type TParameters = { + accessToken: string + payload: TCategorySchema +} + +export const updateCategoryRequest = async (parameters: TParameters) => { + const { accessToken, payload } = parameters + try { + const { id, ...restPayload } = payload + const { data } = await HttpServer({ accessToken }).put( + `/api/category/${id}/update`, + restPayload, + ) + return categoryResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/apis/admin/update-news.ts b/app/apis/admin/update-news.ts index 109b187..806f801 100644 --- a/app/apis/admin/update-news.ts +++ b/app/apis/admin/update-news.ts @@ -1,7 +1,7 @@ import { z } from 'zod' import { HttpServer } from '~/libs/http-server' -import type { TContentSchema } from '~/pages/contents-form' +import type { TContentSchema } from '~/pages/form-contents' const newsResponseSchema = z.object({ data: z.object({ diff --git a/app/apis/common/get-categories.ts b/app/apis/common/get-categories.ts index c074cee..778e58d 100644 --- a/app/apis/common/get-categories.ts +++ b/app/apis/common/get-categories.ts @@ -6,6 +6,8 @@ export const categoryResponseSchema = z.object({ id: z.string(), name: z.string(), code: z.string(), + sequence: z.number().nullable(), + description: z.string().nullable(), }) const categoriesResponseSchema = z.object({ data: z.array(categoryResponseSchema), diff --git a/app/layouts/news/header-menu.tsx b/app/layouts/news/header-menu.tsx index 8bfab06..8b8cdab 100644 --- a/app/layouts/news/header-menu.tsx +++ b/app/layouts/news/header-menu.tsx @@ -7,7 +7,11 @@ import { HeaderSearch } from './header-search' export const HeaderMenu = () => { const loaderData = useRouteLoaderData('routes/_news') - const menu = loaderData?.categoriesData + const menu = loaderData?.categoriesData?.sort((a, b) => { + if (a.sequence === null) return 1 + if (b.sequence === null) return -1 + return a.sequence - b.sequence + }) return ( <> diff --git a/app/pages/dashboard-categories/index.tsx b/app/pages/dashboard-categories/index.tsx index f3ee82e..163e64c 100644 --- a/app/pages/dashboard-categories/index.tsx +++ b/app/pages/dashboard-categories/index.tsx @@ -2,6 +2,7 @@ import DT from 'datatables.net-dt' import DataTable from 'datatables.net-react' import { Link, useRouteLoaderData } from 'react-router' +import type { TCategoryResponse } from '~/apis/common/get-categories' import { Button } from '~/components/ui/button' import { UiTable } from '~/components/ui/table' import { TitleDashboard } from '~/components/ui/title-dashboard' @@ -13,26 +14,33 @@ export const CategoriesPage = () => { const categoriesData = loaderData?.dataCategories DataTable.use(DT) - const dataTable = categoriesData + const dataTable = categoriesData?.sort((a, b) => { + if (a.sequence === null) return 1 + if (b.sequence === null) return -1 + return a.sequence - b.sequence + }) const dataColumns = [ { title: 'No', render: ( data: unknown, type: unknown, - row: unknown, + row: TCategoryResponse, meta: { row: number }, ) => { - return meta.row + 1 + return `
${meta.row + 1}
${ + row.sequence === null + ? '' + : `
Urutan: ${row.sequence}
` + }` }, }, { - title: 'Nama', - data: 'name', + title: 'Kategori', }, { - title: 'Kode', - data: 'code', + title: 'Deskripsi', + data: 'description', }, { title: 'Action', @@ -40,6 +48,12 @@ export const CategoriesPage = () => { }, ] const dataSlot = { + 1: (_value: unknown, _type: unknown, data: TCategoryResponse) => ( +
+
{data.name}
+
Kode: {data.code}
+
+ ), 3: (value: string) => ( - - - - - - ) -} diff --git a/app/pages/dashboard-category-update/index.tsx b/app/pages/dashboard-category-update/index.tsx deleted file mode 100644 index ca8e173..0000000 --- a/app/pages/dashboard-category-update/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import React from 'react' - -export const UpdateCategoryPage = () => { - return
UpdateCategoryPage
-} diff --git a/app/pages/form-category/index.tsx b/app/pages/form-category/index.tsx new file mode 100644 index 0000000..1dce834 --- /dev/null +++ b/app/pages/form-category/index.tsx @@ -0,0 +1,133 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { useEffect, useState } from 'react' +import { useFetcher, useNavigate } from 'react-router' +import { RemixFormProvider, useRemixForm } from 'remix-hook-form' +import { z } from 'zod' + +import type { TCategoryResponse } from '~/apis/common/get-categories' +import { Button } from '~/components/ui/button' +import { Input } from '~/components/ui/input' +import { TitleDashboard } from '~/components/ui/title-dashboard' +import { urlFriendlyCode } from '~/utils/formatter' + +export const createCategorySchema = z.object({ + id: z.string().optional(), + name: z.string().min(3, 'Nama minimal 3 karakter'), + code: z.string(), + sequence: z.preprocess(Number, z.number().optional()), + description: z.string(), +}) +export type TCategorySchema = z.infer +type TProperties = { + categoryData?: TCategoryResponse +} + +export const FormCategoryPage = (properties: TProperties) => { + const { categoryData } = properties || {} + const fetcher = useFetcher() + const navigate = useNavigate() + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(createCategorySchema), + values: { + id: categoryData?.id || undefined, + code: categoryData?.code || '', + name: categoryData?.name || '', + sequence: categoryData?.sequence || undefined, + description: categoryData?.description || '', + }, + }) + const [error, setError] = useState() + const [disabled, setDisabled] = useState(false) + + const { handleSubmit, watch, setValue } = formMethods + const watchName = watch('name') + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + setDisabled(false) + return + } + navigate('/lg-admin/categories') + setDisabled(true) + setError(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) + + useEffect(() => { + setValue('code', urlFriendlyCode(watchName)) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [watchName]) + + return ( +
+ +
+ + + {error && ( +
{error}
+ )} +
+ + + +
+
+ + +
+
+
+
+
+ ) +} diff --git a/app/pages/contents-form/index.tsx b/app/pages/form-contents/index.tsx similarity index 94% rename from app/pages/contents-form/index.tsx rename to app/pages/form-contents/index.tsx index fc0ce44..d255481 100644 --- a/app/pages/contents-form/index.tsx +++ b/app/pages/form-contents/index.tsx @@ -60,7 +60,7 @@ type TProperties = { newsData?: z.infer } -export const ContentsFormPage = (properties: TProperties) => { +export const FormContentsPage = (properties: TProperties) => { const { newsData } = properties || {} const fetcher = useFetcher() const navigate = useNavigate() @@ -75,7 +75,7 @@ export const ContentsFormPage = (properties: TProperties) => { fetcher, resolver: zodResolver(contentSchema), values: { - id: newsData?.id || '', + id: newsData?.id || undefined, categories: newsData?.categories || [], tags: newsData?.tags || [], title: newsData?.title || '', @@ -124,16 +124,17 @@ export const ContentsFormPage = (properties: TProperties) => { label="Judul" placeholder="Masukkan Judul" name="title" - className="border-0 bg-white shadow focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none" + className="border-0 bg-white shadow read-only:bg-gray-100 focus:ring-1 focus:ring-[#2E2F7C] focus:outline-none disabled:bg-gray-100" labelClassName="text-sm font-medium text-[#363636]" containerClassName="flex-1" + disabled={!!newsData} /> diff --git a/app/pages/news-categories/index.tsx b/app/pages/news-categories/index.tsx index fffb282..40fc18a 100644 --- a/app/pages/news-categories/index.tsx +++ b/app/pages/news-categories/index.tsx @@ -1,18 +1,17 @@ -import { useLocation, useRouteLoaderData } from 'react-router' +import { useParams, useRouteLoaderData } from 'react-router' import { Card } from '~/components/ui/card' import { CategorySection } from '~/components/ui/category-section' -import { DUMMY_DESCRIPTION } from '~/data/contents' import type { loader } from '~/routes/_news' import { BERITA } from './data' export const NewsCategoriesPage = () => { - const { pathname } = useLocation() - const code = pathname.split('/')[2] + const parameters = useParams() const loaderData = useRouteLoaderData('routes/_news') - const { name } = - loaderData?.categoriesData.find((item) => item.code === code) || {} + const { name, description } = + loaderData?.categoriesData.find((item) => item.code === parameters.code) || + {} const { items } = BERITA return ( @@ -20,7 +19,7 @@ export const NewsCategoriesPage = () => { diff --git a/app/routes/_admin.lg-admin._dashboard.categories.create.tsx b/app/routes/_admin.lg-admin._dashboard.categories.create.tsx index 39b04d5..8554e85 100644 --- a/app/routes/_admin.lg-admin._dashboard.categories.create.tsx +++ b/app/routes/_admin.lg-admin._dashboard.categories.create.tsx @@ -1,4 +1,4 @@ -import { CreateCategoryPage } from '~/pages/dashboard-category-create' +import { FormCategoryPage } from '~/pages/form-category' -const DashboardCategoriesCreateLayout = () => +const DashboardCategoriesCreateLayout = () => export default DashboardCategoriesCreateLayout diff --git a/app/routes/_admin.lg-admin._dashboard.categories.update.$id.tsx b/app/routes/_admin.lg-admin._dashboard.categories.update.$id.tsx new file mode 100644 index 0000000..80e787a --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.categories.update.$id.tsx @@ -0,0 +1,24 @@ +import { getCategories } from '~/apis/common/get-categories' +import { handleCookie } from '~/libs/cookies' +import { FormCategoryPage } from '~/pages/form-category' + +import type { Route } from './+types/_admin.lg-admin._dashboard.categories.update.$id' + +export const loader = async ({ request, params }: Route.LoaderArgs) => { + const { staffToken } = await handleCookie(request) + const { data: categoriesData } = await getCategories({ + accessToken: staffToken, + }) + const categoryData = categoriesData.find( + (category) => category.id === params.id, + ) + return { categoryData } +} + +const DashboardCategoriesUpdateLayout = ({ + loaderData, +}: Route.ComponentProps) => { + const categoryData = loaderData.categoryData + return +} +export default DashboardCategoriesUpdateLayout diff --git a/app/routes/_admin.lg-admin._dashboard.categories.update.$slug.tsx b/app/routes/_admin.lg-admin._dashboard.categories.update.$slug.tsx deleted file mode 100644 index fd8f151..0000000 --- a/app/routes/_admin.lg-admin._dashboard.categories.update.$slug.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { UpdateCategoryPage } from '~/pages/dashboard-category-update' - -const DashboardCategoriesUpdateLayout = () => -export default DashboardCategoriesUpdateLayout diff --git a/app/routes/_admin.lg-admin._dashboard.contents.create.tsx b/app/routes/_admin.lg-admin._dashboard.contents.create.tsx index fd94316..8e24f2e 100644 --- a/app/routes/_admin.lg-admin._dashboard.contents.create.tsx +++ b/app/routes/_admin.lg-admin._dashboard.contents.create.tsx @@ -1,4 +1,4 @@ -import { ContentsFormPage } from '~/pages/contents-form' +import { FormContentsPage } from '~/pages/form-contents' -const DashboardContentCreateLayout = () => +const DashboardContentCreateLayout = () => export default DashboardContentCreateLayout diff --git a/app/routes/_admin.lg-admin._dashboard.contents.update.$slug.tsx b/app/routes/_admin.lg-admin._dashboard.contents.update.$slug.tsx index 0fa7c06..8fde4c6 100644 --- a/app/routes/_admin.lg-admin._dashboard.contents.update.$slug.tsx +++ b/app/routes/_admin.lg-admin._dashboard.contents.update.$slug.tsx @@ -1,20 +1,20 @@ import { getNewsBySlug } from '~/apis/common/get-news-by-slug' import { handleCookie } from '~/libs/cookies' -import { ContentsFormPage } from '~/pages/contents-form' +import { FormContentsPage } from '~/pages/form-contents' import type { Route } from './+types/_admin.lg-admin._dashboard.contents.update.$slug' -export const loader = async ({ request }: Route.LoaderArgs) => { +export const loader = async ({ request, params }: Route.LoaderArgs) => { const { staffToken } = await handleCookie(request) const { data: newsData } = await getNewsBySlug({ accessToken: staffToken, - slug: request.url.split('/').pop() ?? '', + slug: params.slug, }) return { newsData } } const DashboardContentUpdateLayout = ({ loaderData }: Route.ComponentProps) => { const newsData = loaderData.newsData - return + return } export default DashboardContentUpdateLayout diff --git a/app/routes/_news.category.$name.tsx b/app/routes/_news.category.$code.tsx similarity index 100% rename from app/routes/_news.category.$name.tsx rename to app/routes/_news.category.$code.tsx diff --git a/app/routes/actions.admin.categories.create.ts b/app/routes/actions.admin.categories.create.ts index 8dec87d..ae64a49 100644 --- a/app/routes/actions.admin.categories.create.ts +++ b/app/routes/actions.admin.categories.create.ts @@ -8,7 +8,7 @@ import { handleCookie } from '~/libs/cookies' import { createCategorySchema, type TCategorySchema, -} from '~/pages/dashboard-category-create' +} from '~/pages/form-category' import type { Route } from './+types/actions.register' diff --git a/app/routes/actions.admin.categories.update.ts b/app/routes/actions.admin.categories.update.ts new file mode 100644 index 0000000..dab861f --- /dev/null +++ b/app/routes/actions.admin.categories.update.ts @@ -0,0 +1,67 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { updateCategoryRequest } from '~/apis/admin/update-category' +import { handleCookie } from '~/libs/cookies' +import { + createCategorySchema, + type TCategorySchema, +} from '~/pages/form-category' + +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( + request, + zodResolver(createCategorySchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: categoryData } = await updateCategoryRequest({ + accessToken: staffToken, + payload, + }) + + return data( + { + success: true, + categoryData, + }, + { + 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 }, + ) + } +} diff --git a/app/routes/actions.admin.contents.create.ts b/app/routes/actions.admin.contents.create.ts index 30b0bbf..f9d8416 100644 --- a/app/routes/actions.admin.contents.create.ts +++ b/app/routes/actions.admin.contents.create.ts @@ -5,7 +5,7 @@ import { XiorError } from 'xior' import { createNewsRequest } from '~/apis/admin/create-news' import { handleCookie } from '~/libs/cookies' -import { contentSchema, type TContentSchema } from '~/pages/contents-form' +import { contentSchema, type TContentSchema } from '~/pages/form-contents' import type { Route } from './+types/actions.register' diff --git a/app/routes/actions.admin.contents.update.ts b/app/routes/actions.admin.contents.update.ts index b0b5fe4..259c80d 100644 --- a/app/routes/actions.admin.contents.update.ts +++ b/app/routes/actions.admin.contents.update.ts @@ -5,7 +5,7 @@ import { XiorError } from 'xior' import { updateNewsRequest } from '~/apis/admin/update-news' import { handleCookie } from '~/libs/cookies' -import { contentSchema, type TContentSchema } from '~/pages/contents-form' +import { contentSchema, type TContentSchema } from '~/pages/form-contents' import type { Route } from './+types/actions.register'