diff --git a/app/apis/admin/create-ads.ts b/app/apis/admin/create-ads.ts new file mode 100644 index 0000000..21291f9 --- /dev/null +++ b/app/apis/admin/create-ads.ts @@ -0,0 +1,28 @@ +import { z } from 'zod' + +import { HttpServer, type THttpServer } from '~/libs/http-server' +import type { TAdsSchema } from '~/pages/form-advertisements' + +const advertisementsResponseSchema = z.object({ + data: z.object({ + Message: z.string(), + }), +}) + +type TParameters = { + payload: TAdsSchema +} & THttpServer + +export const createAdsRequest = async (parameters: TParameters) => { + const { payload, ...restParameters } = parameters + try { + const { data } = await HttpServer(restParameters).post( + '/api/advertisements/create', + payload, + ) + return advertisementsResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/apis/common/get-ads.ts b/app/apis/common/get-ads.ts new file mode 100644 index 0000000..a618adc --- /dev/null +++ b/app/apis/common/get-ads.ts @@ -0,0 +1,25 @@ +import { z } from 'zod' + +import { HttpServer, type THttpServer } from '~/libs/http-server' + +const adResponseSchema = z.object({ + id: z.string(), + image_url: z.string(), + url: z.string(), +}) +const adsResponseSchema = z.object({ + data: z.array(adResponseSchema), +}) + +export type TAdResponse = z.infer +export type TAdsResponse = z.infer + +export const getAds = async (parameters?: THttpServer) => { + try { + const { data } = await HttpServer(parameters).get(`/api/ads`) + return adsResponseSchema.parse(data) + } catch (error) { + // eslint-disable-next-line unicorn/no-useless-promise-resolve-reject + return Promise.reject(error) + } +} diff --git a/app/components/ui/input-file.tsx b/app/components/ui/input-file.tsx index 29d6f00..5762e61 100644 --- a/app/components/ui/input-file.tsx +++ b/app/components/ui/input-file.tsx @@ -25,6 +25,7 @@ type TInputProperties = Omit< rules?: RegisterOptions containerClassName?: string labelClassName?: string + category?: string } export const InputFile = >( @@ -40,6 +41,7 @@ export const InputFile = >( className, containerClassName, labelClassName, + category, ...restProperties } = properties const { setIsUploadOpen, uploadedFile, setUploadedFile, isUploadOpen } = @@ -86,7 +88,7 @@ export const InputFile = >( size="fit" className="absolute right-3 h-[42px]" onClick={() => { - setIsUploadOpen(name as TUpload) + setIsUploadOpen((category || name) as TUpload) }} > diff --git a/app/components/ui/banner.tsx b/app/layouts/news/banner.tsx similarity index 100% rename from app/components/ui/banner.tsx rename to app/layouts/news/banner.tsx diff --git a/app/layouts/news/default.tsx b/app/layouts/news/default.tsx index ab703cd..5be5663 100644 --- a/app/layouts/news/default.tsx +++ b/app/layouts/news/default.tsx @@ -2,8 +2,8 @@ import { type PropsWithChildren } from 'react' import { PopupModal } from '~/components/popup/modal' import { SuccessModal } from '~/components/popup/success-modal' -import { Banner } from '~/components/ui/banner' import { useNewsContext } from '~/contexts/news' +import { Banner } from '~/layouts/news/banner' import { FormForgotPassword } from '~/layouts/news/form-forgot-password' import { FormLogin } from '~/layouts/news/form-login' import { FormRegister } from '~/layouts/news/form-register' diff --git a/app/pages/dashboard-advertisements/index.tsx b/app/pages/dashboard-advertisements/index.tsx index 819e36d..cb2a9ad 100644 --- a/app/pages/dashboard-advertisements/index.tsx +++ b/app/pages/dashboard-advertisements/index.tsx @@ -1,23 +1,36 @@ import type { ConfigColumns } from 'datatables.net-dt' import type { DataTableSlots } from 'datatables.net-react' -import { Link } from 'react-router' -import { twMerge } from 'tailwind-merge' +import { Link, useRouteLoaderData } from 'react-router' import { Button } from '~/components/ui/button' import { UiTable } from '~/components/ui/table' import { TitleDashboard } from '~/components/ui/title-dashboard' -import { BANNER } from '~/data/contents' - -type TStatusColors = 'draft' | 'active' | 'inactive' +import type { loader } from '~/routes/_admin.lg-admin._dashboard.advertisements._index' export const AdvertisementsPage = () => { - const dataBanner = BANNER + const loaderData = useRouteLoaderData( + 'routes/_admin.lg-admin._dashboard.advertisements._index', + ) + const { adsData: dataTable } = loaderData || {} + const dataColumns: ConfigColumns[] = [ - { title: 'No', data: 'id' }, - { title: 'Banner', data: 'urlImage' }, - { title: 'Link', data: 'link' }, - { title: 'Tgl Create', data: 'createdAt' }, - { title: 'Status', data: 'status' }, + { + title: 'No', + render: ( + _data: unknown, + _type: unknown, + _row: unknown, + meta: { row: number }, + ) => { + return meta.row + 1 + }, + }, + { title: 'Banner', data: 'image_url' }, + { title: 'Link', data: 'url' }, + { + title: 'Action', + data: 'id', + }, ] const dataSlot: DataTableSlots = { 1: (value: string) => { @@ -25,30 +38,22 @@ export const AdvertisementsPage = () => {
{`banner
) }, - 4: (value: string) => { - const statusColors = { - draft: 'bg-gray-300', - active: 'bg-[#04D182]', - inactive: 'bg-[#F96D19]', - } - const status = value as TStatusColors - return ( - - {status} - - ) - }, + 3: (value: string) => ( + + ), } return ( @@ -68,7 +73,7 @@ export const AdvertisementsPage = () => { { }, { title: 'Penulis', + data: 'author', }, { title: 'Judul', data: 'title' }, { @@ -57,12 +58,10 @@ export const ContentsPage = () => { ] const dataSlot: DataTableSlots = { 1: (value: string) => formatDate(value), - 2: (_value: unknown, _type: unknown, data: TNewsResponse) => ( + 2: (value: TAuthor) => (
-
{data.author.name}
-
- ID: {data.author.id.slice(0, 8)} -
+
{value.name}
+
ID: {value.id.slice(0, 8)}
), 3: (value: string) => {value}, diff --git a/app/pages/form-advertisements/index.tsx b/app/pages/form-advertisements/index.tsx new file mode 100644 index 0000000..2686f02 --- /dev/null +++ b/app/pages/form-advertisements/index.tsx @@ -0,0 +1,103 @@ +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 { TAdResponse } from '~/apis/common/get-ads' +import { Button } from '~/components/ui/button' +import { Input } from '~/components/ui/input' +import { InputFile } from '~/components/ui/input-file' +import { TitleDashboard } from '~/components/ui/title-dashboard' + +export const adsSchema = z.object({ + id: z.string().optional(), + image: z.string().url({ + message: 'Gambar must be a valid URL', + }), + url: z.string().url({ + message: 'URL must be valid', + }), +}) +export type TAdsSchema = z.infer +type TProperties = { + adData?: TAdResponse +} + +export const FormAdvertisementsPage = (properties: TProperties) => { + const { adData } = properties || {} + const fetcher = useFetcher() + const navigate = useNavigate() + const formMethods = useRemixForm({ + mode: 'onSubmit', + fetcher, + resolver: zodResolver(adsSchema), + values: { + id: adData?.id || undefined, + image: adData?.image_url || '', + url: adData?.url || '', + }, + }) + const [error, setError] = useState() + + const { handleSubmit } = formMethods + + useEffect(() => { + if (!fetcher.data?.success) { + setError(fetcher.data?.message) + return + } + navigate('/lg-admin/advertisements') + setError(undefined) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fetcher]) + + return ( +
+ +
+ + + {error && ( +
{error}
+ )} +
+ + + +
+
+
+
+
+ ) +} diff --git a/app/pages/form-category/index.tsx b/app/pages/form-category/index.tsx index b5a60e7..c35dfd1 100644 --- a/app/pages/form-category/index.tsx +++ b/app/pages/form-category/index.tsx @@ -10,14 +10,14 @@ import { Input } from '~/components/ui/input' import { TitleDashboard } from '~/components/ui/title-dashboard' import { urlFriendlyCode } from '~/utils/formatter' -export const createCategorySchema = z.object({ +export const categorySchema = 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 +export type TCategorySchema = z.infer type TProperties = { categoryData?: TCategoryResponse } @@ -29,7 +29,7 @@ export const FormCategoryPage = (properties: TProperties) => { const formMethods = useRemixForm({ mode: 'onSubmit', fetcher, - resolver: zodResolver(createCategorySchema), + resolver: zodResolver(categorySchema), values: { id: categoryData?.id || undefined, code: categoryData?.code || '', diff --git a/app/pages/form-subscriptions-plan/index.tsx b/app/pages/form-subscriptions-plan/index.tsx index da5c4d8..e728ec9 100644 --- a/app/pages/form-subscriptions-plan/index.tsx +++ b/app/pages/form-subscriptions-plan/index.tsx @@ -10,7 +10,7 @@ import { Input } from '~/components/ui/input' import { TitleDashboard } from '~/components/ui/title-dashboard' import { urlFriendlyCode } from '~/utils/formatter' -export const createSubscribePlanSchema = z.object({ +export const subscribePlanSchema = z.object({ id: z.string().optional(), name: z.string().min(3, 'Nama minimal 3 karakter'), code: z.string(), @@ -18,7 +18,7 @@ export const createSubscribePlanSchema = z.object({ price: z.preprocess(Number, z.number().optional()), status: z.number().optional(), }) -export type TSubscribePlanSchema = z.infer +export type TSubscribePlanSchema = z.infer type TProperties = { subscribePlanData?: TSubscribePlanSchema } @@ -30,7 +30,7 @@ export const FormSubscribePlanPage = (properties: TProperties) => { const formMethods = useRemixForm({ mode: 'onSubmit', fetcher, - resolver: zodResolver(createSubscribePlanSchema), + resolver: zodResolver(subscribePlanSchema), values: { id: subscribePlanData?.id || undefined, code: subscribePlanData?.code || '', diff --git a/app/pages/form-tag/index.tsx b/app/pages/form-tag/index.tsx index 91d4c9b..b0938ba 100644 --- a/app/pages/form-tag/index.tsx +++ b/app/pages/form-tag/index.tsx @@ -10,12 +10,12 @@ import { Input } from '~/components/ui/input' import { TitleDashboard } from '~/components/ui/title-dashboard' import { urlFriendlyCode } from '~/utils/formatter' -export const createTagSchema = z.object({ +export const tagSchema = z.object({ id: z.string().optional(), name: z.string().min(3, 'Nama minimal 3 karakter'), code: z.string(), }) -export type TTagSchema = z.infer +export type TTagSchema = z.infer type TProperties = { tagData?: TTagResponse } @@ -27,7 +27,7 @@ export const FormTagPage = (properties: TProperties) => { const formMethods = useRemixForm({ mode: 'onSubmit', fetcher, - resolver: zodResolver(createTagSchema), + resolver: zodResolver(tagSchema), values: { id: tagData?.id || undefined, code: tagData?.code || '', diff --git a/app/routes/_admin.lg-admin._dashboard.advertisements._index.tsx b/app/routes/_admin.lg-admin._dashboard.advertisements._index.tsx new file mode 100644 index 0000000..766675a --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.advertisements._index.tsx @@ -0,0 +1,42 @@ +import { isRouteErrorResponse } from 'react-router' + +import { getAds } from '~/apis/common/get-ads' +import { AdvertisementsPage } from '~/pages/dashboard-advertisements' + +import type { Route } from './+types/_admin.lg-admin._dashboard.advertisements._index' + +export const loader = async ({}: Route.LoaderArgs) => { + const { data: adsData } = await getAds() + return { adsData } +} + +export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => { + let message = 'Oops!' + let details = 'An unexpected error occurred.' + let stack: string | undefined + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error' + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message + stack = error.stack + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ) +} +const DashboardAdvertisementsLayout = () => +export default DashboardAdvertisementsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.advertisements.create.tsx b/app/routes/_admin.lg-admin._dashboard.advertisements.create.tsx new file mode 100644 index 0000000..79910da --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.advertisements.create.tsx @@ -0,0 +1,4 @@ +import { FormAdvertisementsPage } from '~/pages/form-advertisements' + +const DashboardAdvertisementsCreateLayout = () => +export default DashboardAdvertisementsCreateLayout diff --git a/app/routes/_admin.lg-admin._dashboard.advertisements.tsx b/app/routes/_admin.lg-admin._dashboard.advertisements.tsx deleted file mode 100644 index 5bd076d..0000000 --- a/app/routes/_admin.lg-admin._dashboard.advertisements.tsx +++ /dev/null @@ -1,4 +0,0 @@ -import { AdvertisementsPage } from '~/pages/dashboard-advertisements' - -const DashboardAdvertisementsLayout = () => -export default DashboardAdvertisementsLayout diff --git a/app/routes/_admin.lg-admin._dashboard.advertisements.update.$id.tsx b/app/routes/_admin.lg-admin._dashboard.advertisements.update.$id.tsx new file mode 100644 index 0000000..9ed7469 --- /dev/null +++ b/app/routes/_admin.lg-admin._dashboard.advertisements.update.$id.tsx @@ -0,0 +1,49 @@ +import { isRouteErrorResponse } from 'react-router' + +import { getAds } from '~/apis/common/get-ads' +import { FormAdvertisementsPage } from '~/pages/form-advertisements' + +import type { Route } from './+types/_admin.lg-admin._dashboard.advertisements.update.$id' + +export const loader = async ({ params }: Route.LoaderArgs) => { + const { data: adsData } = await getAds() + const adData = adsData.find((ads) => ads.id === params.id) + return { adData } +} + +export const ErrorBoundary = ({ error }: Route.ErrorBoundaryProps) => { + let message = 'Oops!' + let details = 'An unexpected error occurred.' + let stack: string | undefined + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error' + details = + error.status === 404 + ? 'The requested page could not be found.' + : error.statusText || details + } else if (import.meta.env.DEV && error && error instanceof Error) { + details = error.message + stack = error.stack + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ) +} + +const DashboardAdvertisementsCreateLayout = ({ + loaderData, +}: Route.ComponentProps) => { + const { adData } = loaderData || {} + return +} +export default DashboardAdvertisementsCreateLayout diff --git a/app/routes/actions.admin.advertisements.create.ts b/app/routes/actions.admin.advertisements.create.ts new file mode 100644 index 0000000..af41ee2 --- /dev/null +++ b/app/routes/actions.admin.advertisements.create.ts @@ -0,0 +1,64 @@ +import { zodResolver } from '@hookform/resolvers/zod' +import { data } from 'react-router' +import { getValidatedFormData } from 'remix-hook-form' +import { XiorError } from 'xior' + +import { createAdsRequest } from '~/apis/admin/create-ads' +import { handleCookie } from '~/libs/cookies' +import { adsSchema, type TAdsSchema } from '~/pages/form-advertisements' + +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(adsSchema), + false, + ) + + if (errors) { + return data({ success: false, errors, defaultValues }, { status: 400 }) + } + + const { data: categoryData } = await createAdsRequest({ + 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.categories.create.ts b/app/routes/actions.admin.categories.create.ts index ae64a49..002e497 100644 --- a/app/routes/actions.admin.categories.create.ts +++ b/app/routes/actions.admin.categories.create.ts @@ -5,10 +5,7 @@ import { XiorError } from 'xior' import { createCategoryRequest } from '~/apis/admin/create-category' import { handleCookie } from '~/libs/cookies' -import { - createCategorySchema, - type TCategorySchema, -} from '~/pages/form-category' +import { categorySchema, type TCategorySchema } from '~/pages/form-category' import type { Route } from './+types/actions.register' @@ -21,7 +18,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createCategorySchema), + zodResolver(categorySchema), false, ) diff --git a/app/routes/actions.admin.categories.update.ts b/app/routes/actions.admin.categories.update.ts index dab861f..ad21633 100644 --- a/app/routes/actions.admin.categories.update.ts +++ b/app/routes/actions.admin.categories.update.ts @@ -5,10 +5,7 @@ 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 { categorySchema, type TCategorySchema } from '~/pages/form-category' import type { Route } from './+types/actions.register' @@ -21,7 +18,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createCategorySchema), + zodResolver(categorySchema), false, ) diff --git a/app/routes/actions.admin.subscribe-plan.create.ts b/app/routes/actions.admin.subscribe-plan.create.ts index 6323a2b..320b3ed 100644 --- a/app/routes/actions.admin.subscribe-plan.create.ts +++ b/app/routes/actions.admin.subscribe-plan.create.ts @@ -6,7 +6,7 @@ import { XiorError } from 'xior' import { createSubscribePlanRequest } from '~/apis/admin/create-subscribe-plan' import { handleCookie } from '~/libs/cookies' import { - createSubscribePlanSchema, + subscribePlanSchema, type TSubscribePlanSchema, } from '~/pages/form-subscriptions-plan' @@ -21,7 +21,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createSubscribePlanSchema), + zodResolver(subscribePlanSchema), false, ) diff --git a/app/routes/actions.admin.subscribe-plan.update.ts b/app/routes/actions.admin.subscribe-plan.update.ts index 4126f0d..faf8dd5 100644 --- a/app/routes/actions.admin.subscribe-plan.update.ts +++ b/app/routes/actions.admin.subscribe-plan.update.ts @@ -6,7 +6,7 @@ import { XiorError } from 'xior' import { updateSubscribePlanRequest } from '~/apis/admin/update-subscribe-plan' import { handleCookie } from '~/libs/cookies' import { - createSubscribePlanSchema, + subscribePlanSchema, type TSubscribePlanSchema, } from '~/pages/form-subscriptions-plan' @@ -21,7 +21,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createSubscribePlanSchema), + zodResolver(subscribePlanSchema), false, ) diff --git a/app/routes/actions.admin.tags.create.ts b/app/routes/actions.admin.tags.create.ts index 8dcf3c0..e456535 100644 --- a/app/routes/actions.admin.tags.create.ts +++ b/app/routes/actions.admin.tags.create.ts @@ -5,7 +5,7 @@ import { XiorError } from 'xior' import { createTagsRequest } from '~/apis/admin/create-tags' import { handleCookie } from '~/libs/cookies' -import { createTagSchema, type TTagSchema } from '~/pages/form-tag' +import { tagSchema, type TTagSchema } from '~/pages/form-tag' import type { Route } from './+types/actions.register' @@ -18,7 +18,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createTagSchema), + zodResolver(tagSchema), false, ) diff --git a/app/routes/actions.admin.tags.update.ts b/app/routes/actions.admin.tags.update.ts index ca3cfd3..5a91b86 100644 --- a/app/routes/actions.admin.tags.update.ts +++ b/app/routes/actions.admin.tags.update.ts @@ -5,7 +5,7 @@ import { XiorError } from 'xior' import { updateTagRequest } from '~/apis/admin/update-tag' import { handleCookie } from '~/libs/cookies' -import { createTagSchema, type TTagSchema } from '~/pages/form-tag' +import { tagSchema, type TTagSchema } from '~/pages/form-tag' import type { Route } from './+types/actions.register' @@ -18,7 +18,7 @@ export const action = async ({ request }: Route.ActionArgs) => { receivedValues: defaultValues, } = await getValidatedFormData( request, - zodResolver(createTagSchema), + zodResolver(tagSchema), false, )