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:
co-authored by
Claude Opus 4.6
parent
8749e83dad
commit
b2ed098067
-85
@@ -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>
|
||||
)
|
||||
}
|
||||
-114
@@ -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>
|
||||
)
|
||||
}
|
||||
-55
@@ -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>
|
||||
)
|
||||
}
|
||||
-86
@@ -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>
|
||||
)
|
||||
}
|
||||
-110
@@ -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>
|
||||
)
|
||||
}
|
||||
-89
File diff suppressed because one or more lines are too long
-68
@@ -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>
|
||||
)
|
||||
}
|
||||
Vendored
-5
@@ -1,5 +0,0 @@
|
||||
import { ModalProps } from "../type";
|
||||
|
||||
export interface ModalDetailUserProps extends ModalProps {
|
||||
userId?: number | null
|
||||
}
|
||||
-42
@@ -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>
|
||||
)
|
||||
}
|
||||
-4
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
@@ -12,3 +12,4 @@ export * from './permissions';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
export * from './roadmap';
|
||||
|
||||
@@ -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() }),
|
||||
});
|
||||
};
|
||||
@@ -9,3 +9,4 @@ export * from './admin';
|
||||
export * from './events';
|
||||
export * from './testimonials';
|
||||
export * from './sessions';
|
||||
export * from './roadmap';
|
||||
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user