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>
)
}
File diff suppressed because one or more lines are too long
@@ -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;