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
@@ -1,89 +0,0 @@
|
||||
import { Icon } from "@iconify/react"
|
||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||
import { For } from "@imphnen-frontend-service/utils"
|
||||
import { FC } from "react"
|
||||
|
||||
const SOCIAL_LINKS = [
|
||||
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
|
||||
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
|
||||
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
|
||||
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
|
||||
]
|
||||
|
||||
export const DetailProfile: FC = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="shadow rounded-lg p-8 mb-7">
|
||||
<div className="flex justify-between items-start mb-6">
|
||||
<div className="flex items-center gap-x-6">
|
||||
<div className="size-[54px] rounded-full overflow-hidden">
|
||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
|
||||
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
|
||||
</div>
|
||||
</div>
|
||||
<Button type="button" variant="text" className="bg-primary-100">
|
||||
Actively Seeking Job
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-5">
|
||||
<For data={SOCIAL_LINKS}>
|
||||
{({ icon, url, label }) => (
|
||||
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||
<Button type="button" size="sm" className="flex items-center gap-x-2">
|
||||
{icon}
|
||||
{label}
|
||||
</Button>
|
||||
</a>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-7">
|
||||
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
|
||||
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
|
||||
<p className="text-p3 font-medium text-neutral-600">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||
</p>
|
||||
</div>
|
||||
<div className="px-8 py-10 rounded-lg shadow h-max">
|
||||
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
|
||||
<div className="grid gap-6">
|
||||
<div className="flex items-center gap-x-5">
|
||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||
<Icon icon="ic:outline-mail" className="text-3xl" />
|
||||
</div>
|
||||
<div className="text-p3">
|
||||
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
|
||||
<p className="text-neutral-600 font-medium">Email Address</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-5">
|
||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||
<Icon icon="cil:phone" className="text-3xl" />
|
||||
</div>
|
||||
<div className="text-p3">
|
||||
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
|
||||
<p className="text-neutral-600 font-medium">Phone Number</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-x-5">
|
||||
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||
<Icon icon="ion:location-outline" className="text-3xl" />
|
||||
</div>
|
||||
<div className="text-p3">
|
||||
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
|
||||
<p className="text-neutral-600 font-medium">Location</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
-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