feat: integrate dimentorin pages with real API, convert modals to pages

Roadmap: integrated with /v1/landing/cms/roadmap API (was dummy data)
- Created roadmap service layer (types, API, hooks)
- List page with search/filter, create/edit as dedicated pages

Session: converted modal detail to dedicated /session-dimentorin/$id page
Users: converted modal detail/delete to dedicated /users-dimentorin/$id page

Deleted all dimentorin modal components (roadmap, session, users).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-11 22:19:06 +07:00
co-authored by Claude Opus 4.6
parent 8749e83dad
commit b2ed098067
23 changed files with 968 additions and 766 deletions
@@ -1,85 +0,0 @@
import { ArrowRightOutlined } from "@ant-design/icons";
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
import { cn, For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
return (
<Modal
isOpen={isOpen}
onClose={onClose}
className="xl:max-w-[64rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div className="overflow-y-auto max-h-[80vh]">
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Create Roadmaps
</h1>
<div className="grid grid-cols-5 gap-6 mb-12">
<div className="col-span-3">
<label className={labelClass}>Prompt</label>
<Textarea
className="min-w-full w-full h-[calc(100%-2rem)]"
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
/>
</div>
<div className="col-span-2 space-y-8">
<div>
<label className={labelClass}>Roadmap Name</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
</div>
<div>
<label className={labelClass}>Roadmap Name</label>
<Select className="w-full">
<option value="pemula">Pemula</option>
<option value="menengah">Menengah</option>
</Select>
</div>
<div>
<label className={labelClass}>Model</label>
<Select className="w-full">
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
<option value="gpt-4">GPT 4</option>
<option value="gpt-4-32k">GPT 4 32k</option>
</Select>
</div>
</div>
<div className="col-span-full">
<Button>
Generate Roadmap
</Button>
</div>
</div>
<div>
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
Roadmap Preview
</h2>
<div className="space-y-2.5">
<For data={Array.from({ length: 3 })}>
{(_, index) => (
<Accordion
key={index}
title={`Day ${index + 1}`}
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
/>
)}
</For>
</div>
</div>
<div className="flex justify-end mt-12">
<Button type="button" className="flex items-center gap-5">
Submit Roadmap
<ArrowRightOutlined />
</Button>
</div>
</div>
</Modal>
)
}
@@ -1,114 +0,0 @@
import { Icon } from "@iconify/react";
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { cn, For } from "@imphnen-frontend-service/utils";
import { FC } from "react";
const TOPICS = [
{ id: 2, icon: '🏢', name: 'Industry Insight' },
{ id: 4, icon: '🖥️', name: 'Basic IT' },
]
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
Saya ingin tahu secara khusus tentang ___.
1.Pertanyaan Anda
2. ...
3. ...`
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
type ModalProps = {
open: boolean
setOpen: (open: boolean) => void
}
export const ModalDetailSession: FC<ModalProps> = ({ open, setOpen }) => {
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="xl:max-w-[64rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail Sesi
</h1>
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
<div className="col-span-4 flex items-center gap-x-12">
<div>
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h2>
<div>
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
</div>
</div>
<Icon icon="ph:arrow-right" className="text-9xl text-primary-500" />
</div>
<div className="col-span-5 flex items-center gap-12">
<div>
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h2>
<div>
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
</div>
</div>
<div>
<h2 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h2>
<div className="py-2 px-6 rounded-md text-center bg-success-200 text-success-500 font-semibold">
Finished
</div>
</div>
</div>
</div>
<div>
<h1 className="text-p3 font-medium mb-2.5">Topics</h1>
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
</div>
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
<div>
<label className={labelClass}>Tanggal</label>
<Input type="date" className="min-w-full w-full" />
</div>
<div>
<label className={labelClass}>Waktu</label>
<Input type="time" className="min-w-full w-full" />
</div>
<div className="relative md:col-span-full">
<label className={labelClass}>Lokasi</label>
<Select className="min-w-full w-full">
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
</div>
<div className="md:col-span-full">
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
<Textarea
className="min-w-full w-full h-40"
placeholder={placeholder}
/>
</div>
</div>
</div>
</div>
</Modal>
)
}
@@ -1,55 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { FC } from "react";
type ModalDeleteProps = {
open: boolean
setOpen: (open: boolean) => void
hadnleDelete?: () => void
}
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
return (
<Modal
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
isOpen={open}
onClose={() => setOpen(false)}
closeButtonClassName="hidden"
>
<Modal.Header className="gap-8">
<img
src="/chibi-delete.webp"
alt="Delete item?"
width={148}
className="self-center"
/>
<div className="text-center">
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
Hapus Akun
</h2>
<p className="text-p3 text-neutral-400">
Apakah kamu yakin untuk menghapus akun ini?
</p>
</div>
</Modal.Header>
<Modal.Content className="flex gap-4">
<Button
variant="bordered"
size="lg"
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
onClick={() => setOpen(false)}
>
Batal
</Button>
<Button
variant="danger"
size="lg"
className="w-full"
onClick={() => hadnleDelete?.()}
>
Hapus Akun
</Button>
</Modal.Content>
</Modal>
)
}
@@ -1,86 +0,0 @@
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
import { cn } from "@imphnen-frontend-service/utils";
import { FC, useState } from "react";
import { ModalDelete } from "../delete";
import { ModalProps } from "../type";
import { ModalSuspendOrBan } from "../suspend-or-ban";
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
const [openDelete, setOpenDelete] = useState(false)
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
return (
<div>
<div className="flex items-center gap-x-8 mb-8">
<div className="size-[100px] rounded-full overflow-hidden">
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
</div>
<div>
<Button type="button" size="sm" variant="bordered" className="mb-4">
Upload Foto
</Button>
<p className="text-neutral-400 font-medium">
Setidaknya rekomendasi ukuran 240x240 px. <br />
.jpg, .jpeg, .png diperbolehkan
</p>
</div>
</div>
<div>
<div className="mb-8">
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
<div className="grid grid-cols-2 gap-8">
<div>
<label className={labelClass}>Nama Depan</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
</div>
<div>
<label className={labelClass}>Nama Belakang</label>
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
</div>
<div>
<label className={labelClass}>Email</label>
<Input type="email" className="min-w-full w-full" placeholder="Email" />
</div>
<div>
<label className={labelClass}>Nomor Telepon</label>
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
</div>
</div>
</div>
<div>
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
<div className="flex items-center gap-5">
<Button
type="button"
size="sm"
variant="bordered"
className="border-danger-500 text-danger-500"
onClick={() => setOpenSuspendOrBan(true)}
>
Suspend/Ban
</Button>
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
Delete Akun
</Button>
</div>
</div>
<div className="flex justify-end gap-x-5">
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
Batal
</Button>
<Button type="button" disabled>
Simpan
</Button>
</div>
</div>
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
</div>
)
}
@@ -1,110 +0,0 @@
import { SearchOutlined } from "@ant-design/icons"
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
import { cn } from "@imphnen-frontend-service/utils"
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
import { FC, useState } from "react"
import dayjs from "dayjs"
interface ActivityLogType {
id: number
date: string
menu: string
activity: string
}
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
date: new Date().toISOString(),
menu: 'Mentoring',
activity: 'Mentoring Session',
}))
export const ActivityLog: FC = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
pageIndex: 0,
pageSize: 9,
})
const columns: ColumnDef<ActivityLogType>[] = [
{
id: 'select',
meta: { cellClassName: cn("w-16") },
header: ({ table }) => (
<input
type="checkbox"
className="rounded"
checked={table.getIsAllRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>
),
cell: ({ row }) => (
<input
type="checkbox"
className="rounded"
checked={row.getIsSelected()}
onChange={row.getToggleSelectedHandler()}
/>
),
},
{
id: 'date',
header: 'Waktu',
accessorKey: 'date',
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
},
{
id: 'menu',
header: 'Menu',
accessorKey: 'menu',
},
{
id: 'activity',
header: 'Activity',
accessorKey: 'activity',
}
]
const table = useReactTable({
data: mockData,
columns,
state: {
pagination,
rowSelection,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
manualPagination: false,
});
return (
<div className="p-8 shadow rounded-lg">
<div className="flex justify-between items-center gap-5 mb-9">
<div className="relative w-1/2">
<Input
placeholder="Cari aktivitas"
className="pl-12 w-full max-h-full"
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<div className="w-1/4">
<Input type="date" className="min-w-full w-full" />
</div>
<Select>
<option selected disabled>Menu</option>
<option value="profle">Profile</option>
<option value="profle-2">Profile</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
</div>
)
}
@@ -1,89 +0,0 @@
import { Icon } from "@iconify/react"
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { For } from "@imphnen-frontend-service/utils"
import { FC } from "react"
const SOCIAL_LINKS = [
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
]
export const DetailProfile: FC = () => {
return (
<div>
<div className="shadow rounded-lg p-8 mb-7">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-x-6">
<div className="size-[54px] rounded-full overflow-hidden">
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
</div>
<div>
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
</div>
</div>
<Button type="button" variant="text" className="bg-primary-100">
Actively Seeking Job
</Button>
</div>
<div className="flex items-center gap-5">
<For data={SOCIAL_LINKS}>
{({ icon, url, label }) => (
<a key={url} href={url} target="_blank" rel="noreferrer">
<Button type="button" size="sm" className="flex items-center gap-x-2">
{icon}
{label}
</Button>
</a>
)}
</For>
</div>
</div>
<div className="grid grid-cols-2 gap-7">
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
<p className="text-p3 font-medium text-neutral-600">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
</p>
</div>
<div className="px-8 py-10 rounded-lg shadow h-max">
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
<div className="grid gap-6">
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ic:outline-mail" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
<p className="text-neutral-600 font-medium">Email Address</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="cil:phone" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
<p className="text-neutral-600 font-medium">Phone Number</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ion:location-outline" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
<p className="text-neutral-600 font-medium">Location</p>
</div>
</div>
</div>
</div>
</div>
</div>
)
}
@@ -1,68 +0,0 @@
import { Button } from "@imphnen-frontend-service/ui/atoms"
import { Modal } from "@imphnen-frontend-service/ui/molecules"
import { cn, For, Show } from "@imphnen-frontend-service/utils"
import { FC, useEffect, useState } from "react"
import { AccountProfile } from "./account-profile"
import { DetailProfile } from "./detail-profile"
import { ActivityLog } from "./activity-log"
import { ModalDetailUserProps } from "./type"
const TABS = {
account: 'account profile',
detail: 'detail profile',
activity: 'activity logs',
} as const
type TabType = typeof TABS[keyof typeof TABS]
export const ModalDetailUser: FC<ModalDetailUserProps> = ({
open,
setOpen,
}) => {
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
useEffect(() => {
if (!open) setActiveTab(TABS.account)
}, [open])
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="xl:max-w-[84rem] bg-white px-10 py-9"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail - Muhammad Firdaus Oiwobo
</h1>
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
<For data={Object.values(TABS)}>
{(tab) => (
<Button
key={tab}
variant="text"
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
onClick={() => setActiveTab(tab)}
>
{tab}
</Button>
)}
</For>
</div>
<div>
<Show condition={activeTab === TABS.account}>
<AccountProfile open={open} setOpen={setOpen} />
</Show>
<Show condition={activeTab === TABS.detail}>
<DetailProfile />
</Show>
<Show condition={activeTab === TABS.activity}>
<ActivityLog />
</Show>
</div>
</div>
</Modal>
)
}
@@ -1,5 +0,0 @@
import { ModalProps } from "../type";
export interface ModalDetailUserProps extends ModalProps {
userId?: number | null
}
@@ -1,42 +0,0 @@
import { FC } from "react";
import { ModalProps } from "../type";
import { Modal } from "@imphnen-frontend-service/ui/molecules";
import { cn } from "@imphnen-frontend-service/utils";
import { Button, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const ModalSuspendOrBan: FC<ModalProps> = ({ open, setOpen }) => {
return (
<Modal
isOpen={open}
onClose={() => setOpen(false)}
className="bg-white px-10 py-9 xl:max-w-[32rem]"
closeButtonClassName="hidden"
>
<div>
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Suspend/Ban
</h1>
<div className="space-y-6">
<div>
<label className={labelClass}>Suspend/Ban</label>
<Select defaultValue="suspend" className="w-full">
<option value="suspend">Suspend</option>
<option value="banned">Banned</option>
</Select>
</div>
<div>
<label className={labelClass}>Alasan</label>
<Textarea placeholder="Masukkan alasan suspend/ban" className="w-full h-40" />
</div>
<Button type="button" className="w-full">
Selesai
</Button>
</div>
</div>
</Modal>
)
}
@@ -1,4 +0,0 @@
export interface ModalProps {
open: boolean
setOpen: (open: boolean) => void
}
@@ -1,34 +1,22 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from '@ant-design/icons'
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
import { cn } from '@imphnen-frontend-service/utils'
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
import { useState } from 'react'
import { ModalCreateRoadmap } from './_components/roadmap-dimentorin/modal/create-roadmap'
type LearningStatus = 'active' | 'inactive'
type RoadmapType = {
id: number
name: string
learningLevel: string
status: LearningStatus
}
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
id: i + 1,
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
status: i % 2 === 0 ? 'active' : 'inactive',
}))
import { toast } from 'sonner'
import { useRoadmapList, useDeleteRoadmap, TRoadmapListItem, TRoadmapStatus } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/roadmap-dimentorin')({
component: RoadmapDimentorinPage,
})
function RoadmapDimentorinPage(): React.ReactElement {
const [openCreateModal, setOpenCreateModal] = useState(false)
const navigate = useNavigate()
const [search, setSearch] = useState('')
const [statusFilter, setStatusFilter] = useState('')
const [deletingId, setDeletingId] = useState<string | null>(null)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
@@ -36,7 +24,40 @@ function RoadmapDimentorinPage(): React.ReactElement {
pageSize: 9,
})
const columns: ColumnDef<RoadmapType>[] = [
const { data: roadmapData, isLoading } = useRoadmapList()
const deleteRoadmap = useDeleteRoadmap()
const allItems: TRoadmapListItem[] = roadmapData ?? []
const filteredItems = allItems.filter((item) => {
const matchSearch = !search || item.title.toLowerCase().includes(search.toLowerCase())
const matchStatus = !statusFilter || item.status === statusFilter
return matchSearch && matchStatus
})
const handleDelete = async (id: string) => {
try {
await deleteRoadmap.mutateAsync(id)
toast.success('Roadmap berhasil dihapus')
setDeletingId(null)
} catch (error) {
console.log(error)
toast.error('Gagal menghapus roadmap')
}
}
const statusColors: Record<TRoadmapStatus, string> = {
upcoming: 'bg-warning-200 text-warning-700',
in_progress: 'bg-primary-200 text-primary-700',
completed: 'bg-success-200 text-success-500',
}
const statusText: Record<TRoadmapStatus, string> = {
upcoming: 'Upcoming',
in_progress: 'In Progress',
completed: 'Completed',
}
const columns: ColumnDef<TRoadmapListItem>[] = [
{
id: 'select',
meta: { cellClassName: cn('w-20') },
@@ -58,19 +79,17 @@ function RoadmapDimentorinPage(): React.ReactElement {
),
},
{
id: 'id',
header: 'No',
accessorKey: 'id',
id: 'title',
header: 'Title',
accessorKey: 'title',
},
{
id: 'name',
header: 'Nama Roadmap',
accessorKey: 'name',
},
{
id: 'learningLevel',
header: 'Tingkat Belajar',
accessorKey: 'learningLevel',
id: 'description',
header: 'Description',
accessorKey: 'description',
cell: ({ row }) => (
<span className="line-clamp-2">{row.original.description}</span>
),
},
{
id: 'status',
@@ -78,55 +97,81 @@ function RoadmapDimentorinPage(): React.ReactElement {
accessorKey: 'status',
cell: ({ row }) => {
const status = row.original.status
const statusColors: Record<LearningStatus, string> = {
inactive: 'bg-danger-200 text-danger-700',
active: 'bg-success-200 text-success-500',
}
const statusText: Record<LearningStatus, string> = {
inactive: 'Inactive',
active: 'Active',
}
return (
<div
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
>
{statusText[status]}
<div className={`py-2 px-4 rounded-md text-center ${statusColors[status] ?? 'bg-neutral-200 text-neutral-700'}`}>
{statusText[status] ?? status}
</div>
)
},
},
{
id: 'votes',
header: 'Votes',
accessorKey: 'votes',
},
{
header: 'Action',
meta: { cellClassName: cn('w-72') },
cell: ({ row }) => (
<div className="flex gap-[8px]">
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
}}
className="flex items-center gap-2"
>
<EditOutlined /> Action
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
{deletingId === row.original.id ? (
<>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
className="flex items-center gap-2"
>
Konfirmasi
</Button>
<Button
variant="bordered"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeletingId(null)
}}
className="flex items-center gap-2"
>
Batal
</Button>
</>
) : (
<>
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
navigate({ to: '/roadmap-dimentorin/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2"
>
<EditOutlined /> Edit
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeletingId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined /> Delete
</Button>
</>
)}
</div>
),
},
]
const table = useReactTable({
data: mockData,
data: filteredItems,
columns,
state: {
pagination,
@@ -137,7 +182,7 @@ function RoadmapDimentorinPage(): React.ReactElement {
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onPaginationChange: setPagination,
pageCount: Math.ceil(mockData.length / pagination.pageSize),
pageCount: Math.ceil(filteredItems.length / pagination.pageSize),
manualPagination: false,
})
@@ -148,7 +193,12 @@ function RoadmapDimentorinPage(): React.ReactElement {
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
<div className="flex items-center justify-between mb-9">
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
<Button
type="button"
variant="primary"
className="flex items-center gap-2"
onClick={() => navigate({ to: '/roadmap-dimentorin/create' })}
>
<PlusOutlined /> Buat Roadmap
</Button>
</div>
@@ -156,24 +206,29 @@ function RoadmapDimentorinPage(): React.ReactElement {
<div className="flex justify-between items-center gap-5 mb-2">
<div className="relative w-full">
<Input
placeholder="Cari berdasarkan nama roadmap"
placeholder="Cari berdasarkan judul roadmap"
className="pl-12 w-full max-h-full"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
<SearchOutlined />
</div>
</div>
<Select>
<option selected disabled>Tingkat Belajar</option>
<option value="pemula">Pemula</option>
<option value="menengah">Menengah</option>
<Select value={statusFilter} onChange={(e) => setStatusFilter(e.target.value)}>
<option value="">Semua Status</option>
<option value="upcoming">Upcoming</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</Select>
</div>
<DataTable data={mockData} columns={columns} table={table} />
{isLoading ? (
<div className="text-center py-8 text-neutral-400">Loading...</div>
) : (
<DataTable data={filteredItems} columns={columns} table={table} />
)}
</section>
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
</BackofficeWrapper>
)
}
@@ -0,0 +1,132 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useEffect } from 'react'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useRoadmapList, useUpdateRoadmap, TRoadmapStatus } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/roadmap-dimentorin/$id')({
component: RoadmapEditPage,
})
function RoadmapEditPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const updateRoadmap = useUpdateRoadmap()
const { data: roadmapData, isLoading } = useRoadmapList()
const roadmap = roadmapData?.find((r) => r.id === id)
const form = useForm<{
title: string
description: string
status: TRoadmapStatus
}>({
mode: 'all',
defaultValues: {
title: '',
description: '',
status: 'upcoming',
},
})
useEffect(() => {
if (roadmap) {
form.reset({
title: roadmap.title,
description: roadmap.description,
status: roadmap.status,
})
}
}, [roadmap])
const onSubmit = form.handleSubmit(async (data) => {
try {
await updateRoadmap.mutateAsync({ id, data })
toast.success('Roadmap berhasil diperbarui')
navigate({ to: '/roadmap-dimentorin' })
} catch (error) {
console.log(error)
toast.error('Gagal memperbarui roadmap')
}
})
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/roadmap-dimentorin' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Edit Roadmap</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Title"
name="title"
type="text"
placeholder="Masukkan judul roadmap"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Description"
name="description"
type="text"
placeholder="Masukkan deskripsi roadmap"
size="lg"
className="w-full"
/>
<div className="flex flex-col gap-2">
<label className="text-p3 font-medium text-neutral-800">Status</label>
<select
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-p3 focus:border-primary-500 focus:outline-none"
{...form.register('status')}
>
<option value="upcoming">Upcoming</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Update Roadmap
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/roadmap-dimentorin' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -0,0 +1,109 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { useForm } from 'react-hook-form'
import { toast } from 'sonner'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Button } from '@imphnen-frontend-service/ui/atoms'
import { ControlledInputField } from '@imphnen-frontend-service/ui/organisms'
import { useCreateRoadmap, TRoadmapStatus } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/roadmap-dimentorin/create')({
component: RoadmapCreatePage,
})
function RoadmapCreatePage() {
const navigate = useNavigate()
const createRoadmap = useCreateRoadmap()
const form = useForm<{
title: string
description: string
status: TRoadmapStatus
}>({
mode: 'all',
defaultValues: {
title: '',
description: '',
status: 'upcoming',
},
})
const onSubmit = form.handleSubmit(async (data) => {
try {
await createRoadmap.mutateAsync(data)
toast.success('Roadmap berhasil ditambahkan')
navigate({ to: '/roadmap-dimentorin' })
} catch (error) {
console.log(error)
toast.error('Gagal menambahkan roadmap')
}
})
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="max-w-2xl mx-auto w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/roadmap-dimentorin' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Buat Roadmap</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<form onSubmit={onSubmit} className="flex flex-col gap-6">
<ControlledInputField
control={form.control}
label="Title"
name="title"
type="text"
placeholder="Masukkan judul roadmap"
size="lg"
className="w-full"
/>
<ControlledInputField
control={form.control}
label="Description"
name="description"
type="text"
placeholder="Masukkan deskripsi roadmap"
size="lg"
className="w-full"
/>
<div className="flex flex-col gap-2">
<label className="text-p3 font-medium text-neutral-800">Status</label>
<select
className="w-full rounded-lg border border-gray-300 px-4 py-3 text-p3 focus:border-primary-500 focus:outline-none"
{...form.register('status')}
>
<option value="upcoming">Upcoming</option>
<option value="in_progress">In Progress</option>
<option value="completed">Completed</option>
</select>
</div>
<div className="flex gap-3 pt-4">
<Button
variant="primary"
size="lg"
className="w-full"
type="submit"
>
Buat Roadmap
</Button>
<Button
variant="bordered"
size="lg"
className="w-full"
onClick={() => navigate({ to: '/roadmap-dimentorin' })}
>
Batal
</Button>
</div>
</form>
</div>
</div>
</main>
)
}
@@ -1,11 +1,10 @@
import { createFileRoute } from '@tanstack/react-router'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { SearchOutlined } from '@ant-design/icons'
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { BackofficeWrapper, DataTable } from '@imphnen-frontend-service/ui/organisms'
import { cn } from '@imphnen-frontend-service/utils'
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from '@tanstack/react-table'
import { ReactElement, useState } from 'react'
import { ModalDetailSession } from './_components/session-dimentorin/modal/detail'
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
export const Route = createFileRoute('/_authenticated/session-dimentorin')({
@@ -13,7 +12,7 @@ export const Route = createFileRoute('/_authenticated/session-dimentorin')({
})
function SessionDimentorinPage(): ReactElement {
const [openDetail, setOpenDetail] = useState(false)
const navigate = useNavigate()
const [statusFilter, setStatusFilter] = useState('')
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
@@ -96,13 +95,13 @@ function SessionDimentorinPage(): ReactElement {
{
header: 'Action',
meta: { cellClassName: cn('w-52') },
cell: () => (
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setOpenDetail(true)
navigate({ to: '/session-dimentorin/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2 w-max"
>
@@ -159,8 +158,6 @@ function SessionDimentorinPage(): ReactElement {
<DataTable data={sessions} columns={columns} table={table} />
)}
</section>
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
</BackofficeWrapper>
)
}
@@ -0,0 +1,158 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Input, Select, Textarea } from '@imphnen-frontend-service/ui/atoms'
import { cn, For } from '@imphnen-frontend-service/utils'
import { useMySessions, TSessionListItem } from '@imphnen-frontend-service/service'
const TOPICS = [
{ id: 2, icon: '\uD83C\uDFE2', name: 'Industry Insight' },
{ id: 4, icon: '\uD83D\uDDA5\uFE0F', name: 'Basic IT' },
]
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
Saya ingin tahu secara khusus tentang ___.
1.Pertanyaan Anda
2. ...
3. ...`
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
export const Route = createFileRoute('/_authenticated/session-dimentorin/$id')({
component: SessionDetailPage,
})
function SessionDetailPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const { data: sessionsData, isLoading } = useMySessions()
const session: TSessionListItem | undefined = sessionsData?.sessions?.find((s) => s.id === id)
const statusColors: Record<string, string> = {
pending: 'bg-warning-200 text-warning-700',
confirmed: 'bg-primary-200 text-primary-700',
ongoing: 'bg-warning-200 text-warning-700',
completed: 'bg-success-200 text-success-500',
cancelled: 'bg-danger-200 text-danger-500',
}
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/session-dimentorin' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Detail Sesi</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail Sesi
</h2>
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
<div className="col-span-4 flex items-center gap-x-12">
<div>
<h3 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h3>
<div>
<p className="text-p3 font-semibold mb-2.5">{session?.mentor_id ?? '-'}</p>
</div>
</div>
</div>
<div className="col-span-5 flex items-center gap-12">
<div>
<h3 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h3>
<div>
<p className="text-p3 font-semibold mb-2.5">{session?.mentee_fullname ?? '-'}</p>
<p className="text-neutral-600">{session?.mentee_email ?? '-'}</p>
</div>
</div>
<div>
<h3 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h3>
<div className={`py-2 px-6 rounded-md text-center capitalize font-semibold ${statusColors[session?.status ?? ''] ?? 'bg-neutral-200 text-neutral-700'}`}>
{session?.status ?? '-'}
</div>
</div>
</div>
</div>
<div>
<h3 className="text-p3 font-medium mb-2.5">Topics</h3>
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
{session?.topic ? (
<div className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium">
<span>{session.topic}</span>
</div>
) : (
<For data={TOPICS}>
{(item, index) => (
<div
key={index}
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
>
<span>{item.icon} </span>
<span>{item.name}</span>
</div>
)}
</For>
)}
</div>
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
<div>
<label className={labelClass}>Tanggal</label>
<Input
type="date"
className="min-w-full w-full"
value={session?.scheduled_at ? new Date(session.scheduled_at).toISOString().split('T')[0] : ''}
readOnly
/>
</div>
<div>
<label className={labelClass}>Waktu</label>
<Input
type="time"
className="min-w-full w-full"
value={session?.scheduled_at ? new Date(session.scheduled_at).toTimeString().slice(0, 5) : ''}
readOnly
/>
</div>
<div className="relative md:col-span-full">
<label className={labelClass}>Tipe Sesi</label>
<Select className="min-w-full w-full" value={session?.session_type ?? 'online'} disabled>
<option value="online">Online</option>
<option value="offline">Offline</option>
</Select>
</div>
<div className="md:col-span-full">
<label className={labelClass}>Durasi (menit)</label>
<Input
type="number"
className="min-w-full w-full"
value={session?.duration_minutes ?? ''}
readOnly
/>
</div>
</div>
</div>
</div>
</div>
</main>
)
}
@@ -1,6 +1,6 @@
import { createFileRoute } from '@tanstack/react-router'
import { SearchOutlined } from '@ant-design/icons'
import { Button, Input, Select } from '@imphnen-frontend-service/ui/atoms'
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { DeleteOutlined, SearchOutlined } from '@ant-design/icons'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import {
BackofficeWrapper,
DataTable,
@@ -15,10 +15,11 @@ import {
useReactTable,
} from '@tanstack/react-table'
import { ReactElement, useState } from 'react'
import { ModalDetailUser } from './_components/users-dimentorin/modal/detail'
import { toast } from 'sonner'
import {
useMentorList,
useUserList,
useDeleteMentor,
MentorDetailResponseDto,
TUsersListItem,
} from '@imphnen-frontend-service/service'
@@ -29,10 +30,10 @@ export const Route = createFileRoute('/_authenticated/users-dimentorin')({
function UsersDimentorinPage(): ReactElement {
const TABS = ['mentor', 'mentee'] as const
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
const [showDetail, setShowDetail] = useState(false)
const [selectedUserId, setSelectedUserId] = useState<string | null>(null)
const [search, setSearch] = useState('')
const [deletingId, setDeletingId] = useState<string | null>(null)
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [pagination, setPagination] = useState<PaginationState>({
@@ -52,6 +53,8 @@ function UsersDimentorinPage(): ReactElement {
per_page: pagination.pageSize,
})
const deleteMentor = useDeleteMentor()
const mentors: MentorDetailResponseDto[] = mentorData?.data ?? []
const mentees: TUsersListItem[] = menteeData?.data ?? []
const mentorTotal = mentorData?.meta?.total ?? mentors.length
@@ -60,6 +63,17 @@ function UsersDimentorinPage(): ReactElement {
const isLoading = activeTab === 'mentor' ? mentorLoading : menteeLoading
const totalItems = activeTab === 'mentor' ? mentorTotal : menteeTotal
const handleDelete = async (id: string) => {
try {
await deleteMentor.mutateAsync(id)
toast.success('Akun berhasil dihapus')
setDeletingId(null)
} catch (error) {
console.log(error)
toast.error('Gagal menghapus akun')
}
}
const mentorColumns: ColumnDef<MentorDetailResponseDto>[] = [
{
id: 'select',
@@ -119,18 +133,59 @@ function UsersDimentorinPage(): ReactElement {
header: 'Action',
meta: { cellClassName: cn('w-72') },
cell: ({ row }) => (
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedUserId(row.original.id)
setShowDetail(true)
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
</Button>
<div className="flex gap-[8px]">
{deletingId === row.original.id ? (
<>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
handleDelete(row.original.id)
}}
className="flex items-center gap-2"
>
Konfirmasi
</Button>
<Button
variant="bordered"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeletingId(null)
}}
className="flex items-center gap-2"
>
Batal
</Button>
</>
) : (
<>
<Button
variant="primary"
size="sm"
onClick={(e) => {
e.stopPropagation()
navigate({ to: '/users-dimentorin/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail
</Button>
<Button
variant="danger"
size="sm"
onClick={(e) => {
e.stopPropagation()
setDeletingId(row.original.id)
}}
className="flex items-center gap-2"
>
<DeleteOutlined />
</Button>
</>
)}
</div>
),
},
]
@@ -185,12 +240,11 @@ function UsersDimentorinPage(): ReactElement {
size="sm"
onClick={(e) => {
e.stopPropagation()
setSelectedUserId(row.original.id)
setShowDetail(true)
navigate({ to: '/users-dimentorin/$id', params: { id: row.original.id } })
}}
className="flex items-center gap-2 w-max"
>
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
<SearchOutlined className="text-[16px]" /> Lihat Detail
</Button>
),
},
@@ -273,12 +327,6 @@ function UsersDimentorinPage(): ReactElement {
<DataTable data={mentees} columns={menteeColumns} table={menteeTable} />
)}
</section>
<ModalDetailUser
open={showDetail}
setOpen={setShowDetail}
userId={selectedUserId}
/>
</BackofficeWrapper>
)
}
@@ -0,0 +1,262 @@
import { createFileRoute, useNavigate } from '@tanstack/react-router'
import { ArrowLeftOutlined } from '@ant-design/icons'
import { Icon } from '@iconify/react'
import { Button, Input } from '@imphnen-frontend-service/ui/atoms'
import { cn, For, Show } from '@imphnen-frontend-service/utils'
import { useState } from 'react'
import { toast } from 'sonner'
import {
useMentorById,
useUserById,
useDeleteMentor,
MentorDetailResponseDto,
} from '@imphnen-frontend-service/service'
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
const SOCIAL_LINKS = [
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, key: 'linkedin_url', label: 'LinkedIn' },
{ icon: <Icon icon="mdi:github" className="text-2xl" />, key: 'github_url', label: 'Github' },
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, key: 'portfolio_url', label: 'Portfolio' },
]
const TABS = {
account: 'account profile',
detail: 'detail profile',
} as const
type TabType = typeof TABS[keyof typeof TABS]
export const Route = createFileRoute('/_authenticated/users-dimentorin/$id')({
component: UserDetailPage,
})
function UserDetailPage() {
const { id } = Route.useParams()
const navigate = useNavigate()
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
const deleteMentor = useDeleteMentor()
const { data: mentorData, isLoading: mentorLoading } = useMentorById(id)
const { data: userData, isLoading: userLoading } = useUserById(id)
const mentor: MentorDetailResponseDto | undefined = mentorData
const user = userData?.data
const isLoading = mentorLoading && userLoading
const displayName = mentor?.fullname ?? user?.fullname ?? '-'
const email = mentor?.email ?? user?.email ?? '-'
const handleDelete = async () => {
try {
await deleteMentor.mutateAsync(id)
toast.success('Akun berhasil dihapus')
navigate({ to: '/users-dimentorin' })
} catch (error) {
console.log(error)
toast.error('Gagal menghapus akun')
}
}
if (isLoading) {
return (
<main className="w-full px-[48px] py-[40px]">
<div className="text-center py-8 text-neutral-400">Loading...</div>
</main>
)
}
return (
<main className="w-full px-[48px] py-[40px] flex flex-col gap-8">
<div className="w-full">
<div className="flex items-center gap-3 mb-6">
<button
onClick={() => navigate({ to: '/users-dimentorin' })}
className="text-primary-500 hover:text-primary-600"
>
<ArrowLeftOutlined className="text-[20px]" />
</button>
<h1 className="text-2xl font-bold text-gray-900">Detail - {displayName}</h1>
</div>
<div className="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
<h2 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
Detail - {displayName}
</h2>
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
<For data={Object.values(TABS)}>
{(tab) => (
<Button
key={tab}
variant="text"
className={cn('px-3 py-2 capitalize', activeTab === tab && 'bg-white')}
onClick={() => setActiveTab(tab)}
>
{tab}
</Button>
)}
</For>
</div>
<Show condition={activeTab === TABS.account}>
<div>
<div className="flex items-center gap-x-8 mb-8">
<div className="size-[100px] rounded-full overflow-hidden bg-neutral-200 flex items-center justify-center">
{user?.avatar ? (
<img src={user.avatar} alt="Profile" className="w-full object-cover" />
) : (
<Icon icon="mdi:account" className="text-4xl text-neutral-400" />
)}
</div>
<div>
<p className="text-p2 font-semibold">{displayName}</p>
<p className="text-neutral-400 font-medium">{email}</p>
</div>
</div>
<div>
<div className="mb-8">
<h3 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h3>
<div className="grid grid-cols-2 gap-8">
<div>
<label className={labelClass}>Nama Lengkap</label>
<Input type="text" className="min-w-full w-full" value={displayName} readOnly />
</div>
<div>
<label className={labelClass}>Email</label>
<Input type="email" className="min-w-full w-full" value={email} readOnly />
</div>
<div>
<label className={labelClass}>Status</label>
<Input
type="text"
className="min-w-full w-full"
value={mentor?.status ?? (user?.is_active ? 'active' : 'inactive')}
readOnly
/>
</div>
<div>
<label className={labelClass}>Gender</label>
<Input
type="text"
className="min-w-full w-full"
value={mentor?.gender ?? user?.profile_extension?.gender ?? '-'}
readOnly
/>
</div>
</div>
</div>
<div>
<h3 className="mb-7 text-p2 font-semibold">Status Akun</h3>
<div className="flex items-center gap-5">
<Button
type="button"
size="sm"
variant="danger"
onClick={handleDelete}
>
Delete Akun
</Button>
</div>
</div>
<div className="flex justify-end gap-x-5 mt-8">
<Button type="button" variant="bordered" onClick={() => navigate({ to: '/users-dimentorin' })}>
Kembali
</Button>
</div>
</div>
</div>
</Show>
<Show condition={activeTab === TABS.detail}>
<div>
<div className="shadow rounded-lg p-8 mb-7">
<div className="flex justify-between items-start mb-6">
<div className="flex items-center gap-x-6">
<div className="size-[54px] rounded-full overflow-hidden bg-neutral-200 flex items-center justify-center">
{user?.avatar ? (
<img src={user.avatar} alt="Profile" className="w-full object-cover" />
) : (
<Icon icon="mdi:account" className="text-2xl text-neutral-400" />
)}
</div>
<div>
<h3 className="text-p2 font-semibold text-neutral-800">{displayName}</h3>
<p className="text-p3 font-medium text-neutral-600">
{mentor ? `${mentor.current_role ?? ''} at ${mentor.current_company ?? ''}` : user?.role?.name ?? '-'}
</p>
</div>
</div>
</div>
<div className="flex items-center gap-5">
<For data={SOCIAL_LINKS}>
{({ icon, key, label }) => {
const url = mentor?.[key as keyof MentorDetailResponseDto] as string | undefined
if (!url) return null
return (
<a key={key} href={url} target="_blank" rel="noreferrer">
<Button type="button" size="sm" className="flex items-center gap-x-2">
{icon}
{label}
</Button>
</a>
)
}}
</For>
</div>
</div>
<div className="grid grid-cols-2 gap-7">
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
<h3 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h3>
<p className="text-p3 font-medium text-neutral-600">
{mentor?.bio ?? user?.profile_extension?.bio ?? 'Tidak ada deskripsi.'}
</p>
</div>
<div className="px-8 py-10 rounded-lg shadow h-max">
<h3 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h3>
<div className="grid gap-6">
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ic:outline-mail" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">{email}</p>
<p className="text-neutral-600 font-medium">Email Address</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="cil:phone" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">
{mentor?.phone_for_verification ?? user?.profile_extension?.phone_number ?? '-'}
</p>
<p className="text-neutral-600 font-medium">Phone Number</p>
</div>
</div>
<div className="flex items-center gap-x-5">
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
<Icon icon="ion:location-outline" className="text-3xl" />
</div>
<div className="text-p3">
<p className="text-neutral-800 font-semibold">
{mentor?.domicile ?? user?.profile_extension?.domicile ?? '-'}
</p>
<p className="text-neutral-600 font-medium">Location</p>
</div>
</div>
</div>
</div>
</div>
</div>
</Show>
</div>
</div>
</main>
)
}
+1
View File
@@ -12,6 +12,7 @@ export * from './permissions';
export * from './events';
export * from './testimonials';
export * from './sessions';
export * from './roadmap';
export interface ApiResponse<T> {
data: T;
+22
View File
@@ -0,0 +1,22 @@
import { api, ApiResponse } from '../index';
import type { TRoadmapListItem, TRoadmapDetailItem, TRoadmapCreateRequest, TRoadmapUpdateRequest } from '../../types/roadmap';
export const getRoadmapList = async (): Promise<TRoadmapListItem[]> => {
const response = await api.get<ApiResponse<TRoadmapListItem[]>>('/v1/landing/cms/roadmap');
return response.data.data;
};
export const createRoadmap = async (data: TRoadmapCreateRequest): Promise<TRoadmapDetailItem> => {
const response = await api.post<ApiResponse<TRoadmapDetailItem>>('/v1/landing/cms/roadmap/create', data);
return response.data.data;
};
export const updateRoadmap = async (id: string, data: TRoadmapUpdateRequest): Promise<TRoadmapDetailItem> => {
const response = await api.patch<ApiResponse<TRoadmapDetailItem>>(`/v1/landing/cms/roadmap/update/${id}`, data);
return response.data.data;
};
export const deleteRoadmap = async (id: string): Promise<{ message: string }> => {
const response = await api.delete<{ message: string }>(`/v1/landing/cms/roadmap/delete/${id}`);
return response.data;
};
+1
View File
@@ -12,3 +12,4 @@ export * from './permissions';
export * from './events';
export * from './testimonials';
export * from './sessions';
export * from './roadmap';
+44
View File
@@ -0,0 +1,44 @@
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import {
getRoadmapList,
createRoadmap,
updateRoadmap,
deleteRoadmap,
} from '../../api/roadmap';
import type { TRoadmapCreateRequest, TRoadmapUpdateRequest } from '../../types/roadmap';
export const roadmapKeys = {
all: ['roadmap'] as const,
list: () => [...roadmapKeys.all, 'list'] as const,
};
export const useRoadmapList = () => {
return useQuery({
queryKey: roadmapKeys.list(),
queryFn: () => getRoadmapList(),
});
};
export const useCreateRoadmap = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (data: TRoadmapCreateRequest) => createRoadmap(data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: roadmapKeys.list() }),
});
};
export const useUpdateRoadmap = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: TRoadmapUpdateRequest }) => updateRoadmap(id, data),
onSuccess: () => queryClient.invalidateQueries({ queryKey: roadmapKeys.list() }),
});
};
export const useDeleteRoadmap = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (id: string) => deleteRoadmap(id),
onSuccess: () => queryClient.invalidateQueries({ queryKey: roadmapKeys.list() }),
});
};
+1
View File
@@ -9,3 +9,4 @@ export * from './admin';
export * from './events';
export * from './testimonials';
export * from './sessions';
export * from './roadmap';
+30
View File
@@ -0,0 +1,30 @@
export type TRoadmapStatus = 'upcoming' | 'in_progress' | 'completed';
export type TRoadmapListItem = {
id: string;
title: string;
description: string;
status: TRoadmapStatus;
votes: number;
created_at: string;
updated_at: string;
is_deleted: boolean;
};
export type TRoadmapDetailItem = {
id: string;
title: string;
description: string;
status: TRoadmapStatus;
votes: number;
created_at: string;
updated_at: string;
};
export type TRoadmapCreateRequest = {
title: string;
description: string;
status: TRoadmapStatus;
};
export type TRoadmapUpdateRequest = TRoadmapCreateRequest;