diff --git a/apps/backoffice/src/app/(protected)/cms-events/_components/modal-add-event.tsx b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-add-event.tsx new file mode 100644 index 0000000..1f55fdb --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-add-event.tsx @@ -0,0 +1,202 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; +import { useItem, useConfirmItem } from '../_hook/use-item'; + +interface IModalAddEvent { + isOpen: boolean; + onClose: () => void; + handleAdd?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; + onDataCapture?: (data: any) => void; +} + +const ModalAddEvent = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleAdd, + onDataCapture, +}: IModalAddEvent) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +interface IStepOneProps { + nextStep: () => void; + onClose: () => void; + onDataCapture?: (data: any) => void; +} + +const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture); + + return ( + <> + +

+ Tambah Event +

+
+ +
+
+ + + + + + + +
+ + +
+
+ + +
+
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleAdd?: () => Promise; + resetStep: () => void; +} + +const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => { + const { onConfirm, onCancel } = useConfirmItem( + onClose, + resetStep, + handleAdd, + { + success: 'Data event berhasil ditambahkan', + error: 'Data event gagal ditambahkan', + } + ); + + return ( + <> + +

+ Tambah Event +

+

+ Apakah kamu yakin ingin +
menambahkan event ini? +

+
+ + + + + + ); +}; + +export default ModalAddEvent; diff --git a/apps/backoffice/src/app/(protected)/cms-events/_components/modal-delete-event.tsx b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-delete-event.tsx new file mode 100644 index 0000000..3b85692 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-delete-event.tsx @@ -0,0 +1,72 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { useConfirmItem } from '../_hook/use-item'; + +interface IModalDeleteEvent { + isOpen: boolean; + onClose: () => void; + handleDelete?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalDeleteEvent = ({ + isOpen, + onClose, + resetStep, + handleDelete, +}: IModalDeleteEvent) => { + const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, { + success: 'Data event berhasil dihapus', + error: 'Data event gagal dihapus', + }); + + return ( + + + Delete? +
+

+ Delete Event +

+

+ Apakah kamu yakin untuk menghapus event ini? Menghapus data ini + mungkin akan mempengaruhi fungsional sistem +

+
+
+ + + + +
+ ); +}; + +export default ModalDeleteEvent; diff --git a/apps/backoffice/src/app/(protected)/cms-events/_components/modal-update-event.tsx b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-update-event.tsx new file mode 100644 index 0000000..1844d0e --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-events/_components/modal-update-event.tsx @@ -0,0 +1,166 @@ +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; + +interface IModalUpdateEvent { + isOpen: boolean; + onClose: () => void; + handleUpdate?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; + initialValues?: { + name?: string; + description?: string; + detail_link?: string; + location?: string; + price?: number; + start_date?: string; + end_date?: string; + is_online?: boolean; + }; + onDataCapture?: (data: any) => void; +} + +const ModalUpdateEvent = ({ + isOpen, + onClose, + resetStep, + handleUpdate, + initialValues, + onDataCapture, +}: IModalUpdateEvent) => { + const form = useForm({ + mode: 'all', + defaultValues: initialValues, + }); + + useEffect(() => { + if (isOpen) { + form.reset(initialValues); + } + }, [isOpen, initialValues]); + + const onSubmit = form.handleSubmit(async (data) => { + try { + onDataCapture?.(data); + if (handleUpdate) await handleUpdate(); + toast.success('Perubahan event berhasil dilakukan'); + onClose(); + resetStep(); + } catch (error) { + console.log(error); + toast.error('Perubahan event gagal dilakukan'); + } + }); + + return ( + + +

+ Update Event +

+
+ +
+
+ + + + + + + +
+ + +
+
+ + +
+
+
+ ); +}; + +export default ModalUpdateEvent; diff --git a/apps/backoffice/src/app/(protected)/cms-events/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/cms-events/_hook/use-item.ts new file mode 100644 index 0000000..03c25df --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-events/_hook/use-item.ts @@ -0,0 +1,57 @@ +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; + +export const useItem = ( + nextStep: () => void, + initialValues?: any, + onDataCapture?: (data: any) => void, +) => { + const form = useForm({ + mode: 'all', + defaultValues: initialValues, + }); + + const onSubmit = form.handleSubmit((data) => { + onDataCapture?.(data); + nextStep(); + }); + + return { + form, + onSubmit, + }; +}; + +export const useConfirmItem = ( + onClose: () => void, + resetStep: () => void, + actionFunction?: () => Promise, + messages?: { + success?: string; + error?: string; + } +) => { + const onConfirm = async () => { + try { + if (actionFunction) { + await actionFunction(); + } + toast.success(messages?.success); + onClose(); + resetStep(); + } catch (error) { + console.log(error); + toast.error(messages?.error); + } + }; + + const onCancel = () => { + onClose(); + resetStep(); + }; + + return { + onConfirm, + onCancel, + }; +}; diff --git a/apps/backoffice/src/app/(protected)/cms-events/page.tsx b/apps/backoffice/src/app/(protected)/cms-events/page.tsx new file mode 100644 index 0000000..0e778f3 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-events/page.tsx @@ -0,0 +1,288 @@ +import { FC, Fragment, ReactElement, useRef, useState } from 'react'; +import { + SearchOutlined, + EditOutlined, + DeleteOutlined, + PlusOutlined, +} from '@ant-design/icons'; +import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; +import { DataTable } from '@imphnen-frontend-service/ui/organisms'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import ModalAddEvent from './_components/modal-add-event'; +import ModalUpdateEvent from './_components/modal-update-event'; +import ModalDeleteEvent from './_components/modal-delete-event'; +import { useQueryState } from '@imphnen-frontend-service/utils'; +import { + useEventList, + useCreateEvent, + useUpdateEvent, + useDeleteEvent, + TEventsListItem, +} from '@imphnen-frontend-service/service'; +import React from 'react'; + +export const Components: FC = (): ReactElement => { + const [showModalAddItem, setShowModalAddItem] = useState(false); + const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); + const [showModalDeleteItem, setShowModalDeleteItem] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); + + const { + step: currentStep, + nextStep, + prevStep, + resetStep, + } = useQueryState('step', { + defaultValue: 1, + maxValue: 2, + minValue: 1, + }); + + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 9, + }); + + const [rowSelection, setRowSelection] = React.useState({}); + + const { data: eventsData, isLoading } = useEventList({ + search, + page: pagination.pageIndex + 1, + per_page: pagination.pageSize, + }); + const createEvent = useCreateEvent(); + const updateEvent = useUpdateEvent(); + const deleteEvent = useDeleteEvent(); + + const events: TEventsListItem[] = eventsData?.data ?? []; + const totalItems = eventsData?.meta?.total ?? events.length; + + const handleAdd = async (): Promise => { + if (pendingFormData.current) { + await createEvent.mutateAsync(pendingFormData.current); + } + return true; + }; + + const handleUpdate = async (): Promise => { + if (selectedEvent && pendingFormData.current) { + await updateEvent.mutateAsync({ id: selectedEvent.id, data: pendingFormData.current }); + } + return true; + }; + + const handleDelete = async (): Promise => { + if (selectedEvent) { + await deleteEvent.mutateAsync(selectedEvent.id); + } + return true; + }; + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + header: 'Name', + accessorKey: 'name', + }, + { + header: 'Location', + accessorKey: 'location', + cell: ({ row }) => row.original.location || '-', + }, + { + header: 'Price', + accessorKey: 'price', + cell: ({ row }) => + row.original.price === 0 + ? 'Free' + : `Rp ${row.original.price.toLocaleString('id-ID')}`, + }, + { + header: 'Start Date', + accessorKey: 'start_date', + cell: ({ row }) => + new Date(row.original.start_date).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + }), + }, + { + header: 'Online', + accessorKey: 'is_online', + cell: ({ row }) => ( + + {row.original.is_online ? 'Online' : 'Offline'} + + ), + }, + { + header: 'Action', + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + + const table = useReactTable({ + data: events, + columns, + state: { + pagination, + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + pageCount: Math.ceil(totalItems / pagination.pageSize), + manualPagination: true, + }); + + return ( + +
+
+

CMS Events

+
+ +
+
+
+ setSearch(e.target.value)} + /> +
+ +
+
+
+ +
+
+ + {isLoading ? ( +
Loading...
+ ) : ( + + )} +
+
+ + setShowModalAddItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleAdd={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} + /> + setShowModalUpdateItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleUpdate={handleUpdate} + initialValues={selectedEvent ? { + name: selectedEvent.name, + description: selectedEvent.description, + detail_link: selectedEvent.detail_link, + location: selectedEvent.location, + price: selectedEvent.price, + start_date: selectedEvent.start_date, + end_date: selectedEvent.end_date, + is_online: selectedEvent.is_online, + } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} + /> + setShowModalDeleteItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleDelete={handleDelete} + /> +
+ ); +}; + +export default Components; diff --git a/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-add-testimonial.tsx b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-add-testimonial.tsx new file mode 100644 index 0000000..0c6ad65 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-add-testimonial.tsx @@ -0,0 +1,146 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; +import { useItem, useConfirmItem } from '../_hook/use-item'; + +interface IModalAddTestimonial { + isOpen: boolean; + onClose: () => void; + handleAdd?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; + onDataCapture?: (data: any) => void; +} + +const ModalAddTestimonial = ({ + isOpen, + onClose, + currentStep, + nextStep, + resetStep, + handleAdd, + onDataCapture, +}: IModalAddTestimonial) => { + return ( + { + onClose(); + resetStep(); + }} + disableEscapeKeyDown={true} + > + {currentStep === 1 && } + {currentStep === 2 && ( + + )} + + ); +}; + +interface IStepOneProps { + nextStep: () => void; + onClose: () => void; + onDataCapture?: (data: any) => void; +} + +const StepOne = ({ nextStep, onDataCapture }: IStepOneProps) => { + const { form, onSubmit } = useItem(nextStep, undefined, onDataCapture); + + return ( + <> + +

+ Tambah Testimonial +

+
+ +
+
+ + +
+ + +
+
+ + ); +}; + +interface IStepTwoProps { + onClose: () => void; + handleAdd?: () => Promise; + resetStep: () => void; +} + +const StepTwo = ({ onClose, handleAdd, resetStep }: IStepTwoProps) => { + const { onConfirm, onCancel } = useConfirmItem( + onClose, + resetStep, + handleAdd, + { + success: 'Data testimonial berhasil ditambahkan', + error: 'Data testimonial gagal ditambahkan', + } + ); + + return ( + <> + +

+ Tambah Testimonial +

+

+ Apakah kamu yakin ingin +
menambahkan testimonial ini? +

+
+ + + + + + ); +}; + +export default ModalAddTestimonial; diff --git a/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-delete-testimonial.tsx b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-delete-testimonial.tsx new file mode 100644 index 0000000..32c102c --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-delete-testimonial.tsx @@ -0,0 +1,72 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { useConfirmItem } from '../_hook/use-item'; + +interface IModalDeleteTestimonial { + isOpen: boolean; + onClose: () => void; + handleDelete?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; +} + +const ModalDeleteTestimonial = ({ + isOpen, + onClose, + resetStep, + handleDelete, +}: IModalDeleteTestimonial) => { + const { onConfirm } = useConfirmItem(onClose, resetStep, handleDelete, { + success: 'Data testimonial berhasil dihapus', + error: 'Data testimonial gagal dihapus', + }); + + return ( + + + Delete? +
+

+ Delete Testimonial +

+

+ Apakah kamu yakin untuk menghapus testimonial ini? Menghapus data ini + mungkin akan mempengaruhi fungsional sistem +

+
+
+ + + + +
+ ); +}; + +export default ModalDeleteTestimonial; diff --git a/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-update-testimonial.tsx b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-update-testimonial.tsx new file mode 100644 index 0000000..bdcdb3e --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-testimonials/_components/modal-update-testimonial.tsx @@ -0,0 +1,104 @@ +import { useEffect } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { Modal } from '@imphnen-frontend-service/ui/molecules'; +import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'; + +interface IModalUpdateTestimonial { + isOpen: boolean; + onClose: () => void; + handleUpdate?: () => Promise; + currentStep?: number; + nextStep: () => void; + prevStep: () => void; + resetStep: () => void; + initialValues?: { + role?: string; + content?: string; + }; + onDataCapture?: (data: any) => void; +} + +const ModalUpdateTestimonial = ({ + isOpen, + onClose, + resetStep, + handleUpdate, + initialValues, + onDataCapture, +}: IModalUpdateTestimonial) => { + const form = useForm({ + mode: 'all', + defaultValues: initialValues, + }); + + useEffect(() => { + if (isOpen) { + form.reset(initialValues); + } + }, [isOpen, initialValues]); + + const onSubmit = form.handleSubmit(async (data) => { + try { + onDataCapture?.(data); + if (handleUpdate) await handleUpdate(); + toast.success('Perubahan testimonial berhasil dilakukan'); + onClose(); + resetStep(); + } catch (error) { + console.log(error); + toast.error('Perubahan testimonial gagal dilakukan'); + } + }); + + return ( + + +

+ Update Testimonial +

+
+ +
+
+ + +
+ + +
+
+
+ ); +}; + +export default ModalUpdateTestimonial; diff --git a/apps/backoffice/src/app/(protected)/cms-testimonials/_hook/use-item.ts b/apps/backoffice/src/app/(protected)/cms-testimonials/_hook/use-item.ts new file mode 100644 index 0000000..03c25df --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-testimonials/_hook/use-item.ts @@ -0,0 +1,57 @@ +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; + +export const useItem = ( + nextStep: () => void, + initialValues?: any, + onDataCapture?: (data: any) => void, +) => { + const form = useForm({ + mode: 'all', + defaultValues: initialValues, + }); + + const onSubmit = form.handleSubmit((data) => { + onDataCapture?.(data); + nextStep(); + }); + + return { + form, + onSubmit, + }; +}; + +export const useConfirmItem = ( + onClose: () => void, + resetStep: () => void, + actionFunction?: () => Promise, + messages?: { + success?: string; + error?: string; + } +) => { + const onConfirm = async () => { + try { + if (actionFunction) { + await actionFunction(); + } + toast.success(messages?.success); + onClose(); + resetStep(); + } catch (error) { + console.log(error); + toast.error(messages?.error); + } + }; + + const onCancel = () => { + onClose(); + resetStep(); + }; + + return { + onConfirm, + onCancel, + }; +}; diff --git a/apps/backoffice/src/app/(protected)/cms-testimonials/page.tsx b/apps/backoffice/src/app/(protected)/cms-testimonials/page.tsx new file mode 100644 index 0000000..4523f94 --- /dev/null +++ b/apps/backoffice/src/app/(protected)/cms-testimonials/page.tsx @@ -0,0 +1,266 @@ +import { FC, Fragment, ReactElement, useRef, useState } from 'react'; +import { + SearchOutlined, + EditOutlined, + DeleteOutlined, + PlusOutlined, +} from '@ant-design/icons'; +import { Button, Input } from '@imphnen-frontend-service/ui/atoms'; +import { DataTable } from '@imphnen-frontend-service/ui/organisms'; +import { + ColumnDef, + getCoreRowModel, + getPaginationRowModel, + PaginationState, + RowSelectionState, + useReactTable, +} from '@tanstack/react-table'; +import ModalAddTestimonial from './_components/modal-add-testimonial'; +import ModalUpdateTestimonial from './_components/modal-update-testimonial'; +import ModalDeleteTestimonial from './_components/modal-delete-testimonial'; +import { useQueryState } from '@imphnen-frontend-service/utils'; +import { + useTestimonialList, + useCreateTestimonial, + useUpdateTestimonial, + useDeleteTestimonial, + TTestimonialsListItem, +} from '@imphnen-frontend-service/service'; +import React from 'react'; + +export const Components: FC = (): ReactElement => { + const [showModalAddItem, setShowModalAddItem] = useState(false); + const [showModalUpdateItem, setShowModalUpdateItem] = useState(false); + const [showModalDeleteItem, setShowModalDeleteItem] = useState(false); + const [selectedTestimonial, setSelectedTestimonial] = useState(null); + const [search, setSearch] = useState(''); + const pendingFormData = useRef(null); + + const { + step: currentStep, + nextStep, + prevStep, + resetStep, + } = useQueryState('step', { + defaultValue: 1, + maxValue: 2, + minValue: 1, + }); + + const [pagination, setPagination] = React.useState({ + pageIndex: 0, + pageSize: 9, + }); + + const [rowSelection, setRowSelection] = React.useState({}); + + const { data: testimonialsData, isLoading } = useTestimonialList({ + search, + page: pagination.pageIndex + 1, + per_page: pagination.pageSize, + }); + const createTestimonial = useCreateTestimonial(); + const updateTestimonial = useUpdateTestimonial(); + const deleteTestimonial = useDeleteTestimonial(); + + const testimonials: TTestimonialsListItem[] = testimonialsData?.data ?? []; + const totalItems = testimonialsData?.meta?.total ?? testimonials.length; + + const handleAdd = async (): Promise => { + if (pendingFormData.current) { + await createTestimonial.mutateAsync(pendingFormData.current); + } + return true; + }; + + const handleUpdate = async (): Promise => { + if (selectedTestimonial && pendingFormData.current) { + await updateTestimonial.mutateAsync({ id: selectedTestimonial.id, data: pendingFormData.current }); + } + return true; + }; + + const handleDelete = async (): Promise => { + if (selectedTestimonial) { + await deleteTestimonial.mutateAsync(selectedTestimonial.id); + } + return true; + }; + + const columns: ColumnDef[] = [ + { + id: 'select', + header: ({ table }) => ( + + ), + cell: ({ row }) => ( + + ), + }, + { + header: 'User', + accessorKey: 'user_fullname', + }, + { + header: 'Role', + accessorKey: 'role', + }, + { + header: 'Content', + accessorKey: 'content', + cell: ({ row }) => { + const content = row.original.content; + return content.length > 80 ? `${content.substring(0, 80)}...` : content; + }, + }, + { + header: 'Created At', + accessorKey: 'created_at', + cell: ({ row }) => + new Date(row.original.created_at).toLocaleDateString('id-ID', { + day: 'numeric', + month: 'short', + year: 'numeric', + }), + }, + { + header: 'Action', + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]; + + const table = useReactTable({ + data: testimonials, + columns, + state: { + pagination, + rowSelection, + }, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + pageCount: Math.ceil(totalItems / pagination.pageSize), + manualPagination: true, + }); + + return ( + +
+
+

CMS Testimonials

+
+ +
+
+
+ setSearch(e.target.value)} + /> +
+ +
+
+
+ +
+
+ + {isLoading ? ( +
Loading...
+ ) : ( + + )} +
+
+ + setShowModalAddItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleAdd={handleAdd} + onDataCapture={(data) => { pendingFormData.current = data; }} + /> + setShowModalUpdateItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleUpdate={handleUpdate} + initialValues={selectedTestimonial ? { + role: selectedTestimonial.role, + content: selectedTestimonial.content, + } : undefined} + onDataCapture={(data) => { pendingFormData.current = data; }} + /> + setShowModalDeleteItem(false)} + nextStep={nextStep} + prevStep={prevStep} + resetStep={resetStep} + handleDelete={handleDelete} + /> +
+ ); +}; + +export default Components; diff --git a/apps/landing/src/app/(public)/(home)/_components/testimonial-section.tsx b/apps/landing/src/app/(public)/(home)/_components/testimonial-section.tsx index 48c59dd..824e0ad 100644 --- a/apps/landing/src/app/(public)/(home)/_components/testimonial-section.tsx +++ b/apps/landing/src/app/(public)/(home)/_components/testimonial-section.tsx @@ -1,16 +1,72 @@ 'use client'; -import TESTIMONIALS from '@/data/testimonials.json'; import { buttonVariants } from '@components'; import { motion, useInView } from 'framer-motion'; -import Image from 'next/image'; import Link from 'next/link'; -import { useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { FaQuoteLeft } from 'react-icons/fa'; +interface ApiTestimonial { + id: number; + user_id: number; + user_fullname: string; + role: string; + content: string; + created_at: string; + is_deleted: boolean; +} + +interface Testimonial { + id: number; + name: string; + role: string; + text: string; +} + +const AVATAR_COLORS = [ + 'bg-primary-500 text-white', + 'bg-blue-500 text-white', + 'bg-green-500 text-white', + 'bg-purple-500 text-white', + 'bg-orange-500 text-white', + 'bg-pink-500 text-white', +]; + +function getAvatarColor(index: number) { + return AVATAR_COLORS[index % AVATAR_COLORS.length]; +} + +function getInitial(name: string) { + return name.charAt(0).toUpperCase(); +} + export function TestimonialSection() { const ref = useRef(null); const isInView = useInView(ref, { once: true, amount: 0.1 }); + const [testimonials, setTestimonials] = useState([]); + + useEffect(() => { + fetch('https://api.imphnen.dev/v1/landing/cms/testimonials') + .then((res) => { + if (!res.ok) throw new Error('Failed to fetch'); + return res.json(); + }) + .then((json) => { + const items: Testimonial[] = (json.data as ApiTestimonial[]) + .filter((t) => !t.is_deleted) + .slice(0, 6) + .map((t) => ({ + id: t.id, + name: t.user_fullname, + role: t.role, + text: t.content, + })); + setTestimonials(items); + }) + .catch(() => { + setTestimonials([]); + }); + }, []); const containerVariants = { hidden: { opacity: 0 }, @@ -59,7 +115,7 @@ export function TestimonialSection() { initial="hidden" animate={isInView ? 'visible' : 'hidden'} > - {TESTIMONIALS.map((testimonial) => ( + {testimonials.map((testimonial, index) => (
- {testimonial.name} +
+ {getInitial(testimonial.name)} +

{testimonial.name} diff --git a/apps/landing/src/app/(public)/events/page.tsx b/apps/landing/src/app/(public)/events/page.tsx index 1e85764..55f7438 100644 --- a/apps/landing/src/app/(public)/events/page.tsx +++ b/apps/landing/src/app/(public)/events/page.tsx @@ -1,11 +1,28 @@ -'use client'; - -import events from '@/data/events.json'; import { buttonVariants } from '@components'; import { cn } from '@utils'; -import Image from 'next/image'; import { HiCalendar, HiClock, HiLocationMarker } from 'react-icons/hi'; +interface ApiEvent { + id: number; + name: string; + description: string; + detail_link: string; + price: number; + is_online: boolean; + is_deleted: boolean; + location: string; + start_date: string; + end_date: string; + created_at: string; + updated_at: string; +} + +interface ApiResponse { + data: ApiEvent[]; + meta: Record; + version: string; +} + const formatDate = (dateString: string) => { const date = new Date(dateString); return date.toLocaleDateString('id-ID', { @@ -30,110 +47,123 @@ const getEventStatus = (endDate: string) => { return end > now ? 'upcoming' : 'past'; }; -export default function EventsPage() { +async function fetchEvents(): Promise { + const res = await fetch( + 'https://api.imphnen.dev/v1/landing/cms/events', + { next: { revalidate: 60 } } + ); + + if (!res.ok) { + return []; + } + + const json: ApiResponse = await res.json(); + return json.data.filter((e) => !e.is_deleted); +} + +export default async function EventsPage() { + const events = await fetchEvents(); + const sortedEvents = [...events].sort( (a, b) => new Date(b.start_date).getTime() - new Date(a.start_date).getTime() ); + if (sortedEvents.length === 0) { + return ( +
+

+ Belum ada event tersedia. +

+
+ ); + } + return (
-
-
- {sortedEvents[0].name} -
-
-
- - Event Terbaru - - - {getEventStatus(sortedEvents[0].end_date) === 'upcoming' - ? 'Upcoming' - : 'Selesai'} - -
-

- {sortedEvents[0].name} -

-
-
- - {formatDate(sortedEvents[0].start_date)} -
-
- - - {formatTime(sortedEvents[0].start_date)} -{' '} - {formatTime(sortedEvents[0].end_date)} WIB - -
-
- - - {sortedEvents[0].type === 'online' - ? 'Online' - : sortedEvents[0].location} - -
- {sortedEvents[0].price > 0 && ( -
- Rp {sortedEvents[0].price.toLocaleString('id-ID')} -
- )} -
-

- {sortedEvents[0].description} -

- +
+ + Event Terbaru + + - Lihat Detail - + {getEventStatus(sortedEvents[0].end_date) === 'upcoming' + ? 'Upcoming' + : 'Selesai'} + + + {sortedEvents[0].is_online ? 'Online' : 'Onsite'} +
+

+ {sortedEvents[0].name} +

+
+
+ + {formatDate(sortedEvents[0].start_date)} +
+
+ + + {formatTime(sortedEvents[0].start_date)} -{' '} + {formatTime(sortedEvents[0].end_date)} WIB + +
+
+ + + {sortedEvents[0].is_online + ? 'Online' + : sortedEvents[0].location} + +
+ {sortedEvents[0].price > 0 && ( +
+ Rp {sortedEvents[0].price.toLocaleString('id-ID')} +
+ )} +
+

+ {sortedEvents[0].description} +

+ + Lihat Detail +
{sortedEvents.slice(1).map((event) => (
-
- {event.name} -
+ + {event.is_online ? 'Online' : 'Onsite'} +

{event.name} @@ -160,7 +200,7 @@ export default function EventsPage() {
- {event.type === 'online' ? 'Online' : event.location} + {event.is_online ? 'Online' : event.location}
{event.price > 0 && ( diff --git a/apps/landing/src/app/(public)/testimonials/page.tsx b/apps/landing/src/app/(public)/testimonials/page.tsx index 424fc93..7eb935d 100644 --- a/apps/landing/src/app/(public)/testimonials/page.tsx +++ b/apps/landing/src/app/(public)/testimonials/page.tsx @@ -1,11 +1,58 @@ -import TESTIMONIALS from '@/data/testimonials.json'; import { Button } from '@components'; -import Image from 'next/image'; import Link from 'next/link'; import { BsChatLeftQuote } from 'react-icons/bs'; import { FaQuoteLeft } from 'react-icons/fa'; -export default function Page() { +interface ApiTestimonial { + id: number; + user_id: number; + user_fullname: string; + role: string; + content: string; + created_at: string; + is_deleted: boolean; +} + +interface ApiResponse { + data: ApiTestimonial[]; + meta: Record; + version: string; +} + +const AVATAR_COLORS = [ + 'bg-primary-500 text-white', + 'bg-blue-500 text-white', + 'bg-green-500 text-white', + 'bg-purple-500 text-white', + 'bg-orange-500 text-white', + 'bg-pink-500 text-white', +]; + +function getAvatarColor(index: number) { + return AVATAR_COLORS[index % AVATAR_COLORS.length]; +} + +function getInitial(name: string) { + return name.charAt(0).toUpperCase(); +} + +async function fetchTestimonials() { + const res = await fetch( + 'https://api.imphnen.dev/v1/landing/cms/testimonials', + { next: { revalidate: 60 } } + ); + + if (!res.ok) { + return []; + } + + const json: ApiResponse = await res.json(); + return json.data.filter((t) => !t.is_deleted); +} + +export default async function Page() { + const testimonials = await fetchTestimonials(); + return (
@@ -21,31 +68,28 @@ export default function Page() {
- {TESTIMONIALS.map((testimonial) => ( + {testimonials.map((testimonial, index) => (
- {testimonial.name} +
+ {getInitial(testimonial.user_fullname)} +

- {testimonial.name} + {testimonial.user_fullname}

{testimonial.role}

-

{testimonial.text}

+

{testimonial.content}

diff --git a/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx index b3fd23d..bd2f709 100644 --- a/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx +++ b/libs/ui/src/organisms/backoffice-sidebar/backoffice-sidebar.tsx @@ -2,10 +2,12 @@ import { AppstoreOutlined, AuditOutlined, BookOutlined, + CalendarOutlined, CommentOutlined, DownOutlined, InboxOutlined, LogoutOutlined, + MessageOutlined, ReadOutlined, ReloadOutlined, RightOutlined, @@ -118,6 +120,22 @@ const MENUS: MenuItem[] = [ }, ], }, + { + label: 'CMS', + icon: , + children: [ + { + label: 'Events', + href: '/cms-events', + icon: , + }, + { + label: 'Testimonials', + href: '/cms-testimonials', + icon: , + }, + ], + }, { label: 'Permissions', href: '/permissions',