Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6c5c5db47b | ||
|
|
583b6b224e | ||
|
|
ce0579de5d | ||
|
|
7df4592a47 | ||
|
|
77eb755783 | ||
|
|
8c6a85090d | ||
|
|
a6134cb32a | ||
|
|
d9c5ff0b18 | ||
|
|
924354366c | ||
|
|
dce74c5c7c | ||
|
|
d4c263cad3 | ||
|
|
e2be5105e9 | ||
|
|
eb5ff2e46c | ||
|
|
e1269f4daa | ||
|
|
487623e4fb | ||
|
|
eff44c42a1 | ||
|
|
3e232dcdc2 | ||
|
|
aa52b62ae7 | ||
|
|
3dec77bd0a | ||
|
|
ec81922001 | ||
|
|
d7b70b21d1 | ||
|
|
a7cf85dd66 | ||
|
|
bb22068856 | ||
|
|
209fa3fff1 | ||
|
|
935d23887c | ||
|
|
363f1582c9 | ||
|
|
1a9b47953a | ||
|
|
182ff7facc | ||
|
|
af8968ac01 | ||
|
|
4bd66af0ff | ||
|
|
73826fb4c3 | ||
|
|
2f9407e258 | ||
|
|
534bf0e10a | ||
|
|
df479a0a8d | ||
|
|
1948d2c67f |
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
-52
@@ -1,52 +0,0 @@
|
|||||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
|
||||||
import { PieLabelProps } from "recharts/types/polar/Pie"
|
|
||||||
|
|
||||||
type ChartProps = {
|
|
||||||
name: string
|
|
||||||
value: number
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData: ChartProps[] = [
|
|
||||||
{ name: "Active", value: 49, color: "#23A1EB" },
|
|
||||||
{ name: "Done", value: 24, color: "#81CBF8" },
|
|
||||||
{ name: "Canceled", value: 27, color: "#BCE1FB" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const RADIAN = Math.PI / 180;
|
|
||||||
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => {
|
|
||||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
||||||
const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN);
|
|
||||||
const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
|
||||||
{`${((percent ?? 1) * 100).toFixed(0)}%`}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SessionStatusChart = () => {
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
|
||||||
<PieChart width={500} height={320}>
|
|
||||||
<Pie
|
|
||||||
data={chartData}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={40}
|
|
||||||
outerRadius={100}
|
|
||||||
labelLine={false}
|
|
||||||
label={renderCustomizedLabel}
|
|
||||||
>
|
|
||||||
{chartData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
|
||||||
|
|
||||||
type ChartProps = {
|
|
||||||
name: string
|
|
||||||
activeUser: number
|
|
||||||
activeSession: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData: ChartProps[] = [
|
|
||||||
{ name: "2014", activeUser: 0, activeSession: 0 },
|
|
||||||
{ name: "2015", activeUser: 15, activeSession: 25 },
|
|
||||||
{ name: "2016", activeUser: 30, activeSession: 40 },
|
|
||||||
{ name: "2017", activeUser: 45, activeSession: 55 },
|
|
||||||
{ name: "2018", activeUser: 60, activeSession: 70 },
|
|
||||||
{ name: "2019", activeUser: 75, activeSession: 85 },
|
|
||||||
{ name: "2020", activeUser: 90, activeSession: 95 },
|
|
||||||
{ name: "2021", activeUser: 85, activeSession: 80 },
|
|
||||||
{ name: "2022", activeUser: 95, activeSession: 90 },
|
|
||||||
{ name: "2023", activeUser: 100, activeSession: 100 },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const UserGrowthChart = () => {
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
|
||||||
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
|
|
||||||
<CartesianGrid />
|
|
||||||
<XAxis dataKey="name" />
|
|
||||||
<YAxis tickCount={10} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line dataKey="activeUser" stroke="#23A1EB" />
|
|
||||||
<Line dataKey="activeSession" stroke="#0877C1" />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ReactElement } from "react";
|
|
||||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
|
||||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
|
||||||
|
|
||||||
const Overview = () => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
|
||||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
|
||||||
<p className="text-neutral-400 text-p3">Total Users</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
|
||||||
|
|
||||||
<div className="space-y-14">
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Overview
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-5">
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => <Overview key={index} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Trends & Analytics
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-x-5">
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
|
|
||||||
<div className="flex items-center justify-between mb-7">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<UserGrowthChart />
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
|
|
||||||
<SessionStatusChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-5">
|
|
||||||
<div className="bg-white px-7 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left rounded-full">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">4.9</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">1000</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
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, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
MENTORING: 'Mentoring',
|
|
||||||
PLATFORM: 'Platform'
|
|
||||||
} as const
|
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
type FeedbackStatus = 'done' | 'todo';
|
|
||||||
|
|
||||||
interface FeedbackType {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
rating: number
|
|
||||||
status: FeedbackStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
rating: 4.5,
|
|
||||||
status: i % 2 === 0 ? 'done' : 'todo',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<FeedbackType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
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: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rating',
|
|
||||||
header: 'Rating',
|
|
||||||
accessorKey: 'rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'bg-success-200 text-success-500',
|
|
||||||
todo: 'bg-primary-200 text-primary-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'Done',
|
|
||||||
todo: 'To Do',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Lihat Feedback
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<div className="mb-8 flex justify-between items-center">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama mentor/mentee"
|
|
||||||
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>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
|
|||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
<BackofficeSidebar />
|
<BackofficeSidebar />
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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/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',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): React.ReactElement {
|
|
||||||
const [openCreateModal, setOpenCreateModal] = useState(false)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<RoadmapType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
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: 'id',
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Nama Roadmap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'learningLevel',
|
|
||||||
header: 'Tingkat Belajar',
|
|
||||||
accessorKey: 'learningLevel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
|
|
||||||
|
|
||||||
<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)}>
|
|
||||||
<PlusOutlined /> Buat Roadmap
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama roadmap"
|
|
||||||
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>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Tingkat Belajar</option>
|
|
||||||
<option value="pemula">Pemula</option>
|
|
||||||
<option value="menengah">Menengah</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
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/modal/detail";
|
|
||||||
|
|
||||||
type SessionStatus = 'ongoing' | 'finished';
|
|
||||||
|
|
||||||
interface SessionType {
|
|
||||||
id: string
|
|
||||||
mentorName: string
|
|
||||||
menteeName: string
|
|
||||||
datetime: number
|
|
||||||
status: SessionStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: `DS-${i + 1}`,
|
|
||||||
mentorName: 'Ahmad Wijuana',
|
|
||||||
menteeName: 'Sofia Wijuana',
|
|
||||||
datetime: new Date().getTime(),
|
|
||||||
status: i % 2 === 0 ? 'ongoing' : 'finished',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const [openDetail, setOpenDetail] = useState(false);
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<SessionType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
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: 'id',
|
|
||||||
header: 'ID Sesi',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mentorName',
|
|
||||||
header: 'Nama Mentor',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menteeName',
|
|
||||||
header: 'Nama Mentee',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'datetime',
|
|
||||||
header: 'Waktu',
|
|
||||||
accessorKey: 'datetime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<SessionStatus, string> = {
|
|
||||||
ongoing: 'bg-warning-200 text-warning-700',
|
|
||||||
finished: 'bg-success-200 text-success-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<SessionStatus, string> = {
|
|
||||||
ongoing: 'On Going',
|
|
||||||
finished: 'Finished',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-52") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setOpenDetail(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Cek Detail
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Session Management</h1>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap"
|
|
||||||
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>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="finished">Finished</option>
|
|
||||||
<option value="ongoing">On Going</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { cn } 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 GeneralSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<ToggleInput label="Mode Maintenance" />
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Platform</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Bahasa</label>
|
|
||||||
<Select defaultValue="id" className="w-full">
|
|
||||||
<option value="id">Indonesia</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Logo Platform</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Favicon</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
|
|
||||||
<div className="grid gap-x-8 gap-y-5 mb-20">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>URL Syarat & Ketentuan</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>URL Kebijakan Privasi</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
export const NotificationSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-8 flex-wrap">
|
|
||||||
<ToggleInput label="Nyalakan Notifikasi Email" />
|
|
||||||
<ToggleInput label="Beritahu mentor ketika ada request" />
|
|
||||||
<ToggleInput label="Beritahu mentee untuk update sesi " />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-20">
|
|
||||||
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
|
|
||||||
API Integrasi Notifikasi (URL)
|
|
||||||
</label>
|
|
||||||
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { cn } 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 PaymentSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Integrasi Payment Gateway</label>
|
|
||||||
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Mata Uang</label>
|
|
||||||
<Select defaultValue="idr">
|
|
||||||
<option value="idr">Rupiah (IDR)</option>
|
|
||||||
<option value="usd">Dollar (USD)</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Tarif Komisi untuk Platform</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
|
|
||||||
<div className="mb-32">
|
|
||||||
<label className={labelClass}>Masukkan Format Invoice</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { cn } 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 SecuritySettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-8 mb-20">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Durasi Session Timeout</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
|
|
||||||
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
|
|
||||||
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
import { DeleteOutlined, UserSwitchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button } 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";
|
|
||||||
|
|
||||||
type UserRolesPermissionType = {
|
|
||||||
id: number
|
|
||||||
role: string
|
|
||||||
totalUser: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
|
|
||||||
totalUser: 10
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const UserRolesPermission: FC = () => {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<UserRolesPermissionType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
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: 'role',
|
|
||||||
header: 'Role',
|
|
||||||
accessorKey: 'role',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'totalUser',
|
|
||||||
header: 'Total User',
|
|
||||||
accessorKey: 'totalUser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-96") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<UserSwitchOutlined className="text-[16px]" /> Manage Permissions
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<DeleteOutlined className="text-[16px]" /> Delete Role
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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>
|
|
||||||
<div className="mb-8 flex items-center justify-between">
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
|
||||||
<Button type="button">
|
|
||||||
Add Rols
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white shadow p-8 rounded-lg">
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { GeneralSettings } from "./_components/general"
|
|
||||||
import { UserRolesPermission } from "./_components/user-roles-permission"
|
|
||||||
import { NotificationSettings } from "./_components/notification"
|
|
||||||
import { SecuritySettings } from "./_components/security"
|
|
||||||
import { PaymentSettings } from "./_components/payment"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
general: "General Settings",
|
|
||||||
userRolePermissions: "User Roles & Permissions",
|
|
||||||
notification: "Notification Settings",
|
|
||||||
security: "Security",
|
|
||||||
payment: "Payment",
|
|
||||||
} as const
|
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
export default function Components(): React.ReactElement {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Settings</h1>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-x-8">
|
|
||||||
<div className="w-64 bg-white p-2.5 shadow space-y-2 rounded-md">
|
|
||||||
<For data={Object.values(TABS)}>
|
|
||||||
{(tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
className={cn(
|
|
||||||
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100",
|
|
||||||
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
|
|
||||||
)}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-8 py-6 shadow space-y-2 rounded-md flex-1">
|
|
||||||
{activeTab === TABS.general && <GeneralSettings />}
|
|
||||||
{activeTab === TABS.userRolePermissions && <UserRolesPermission />}
|
|
||||||
{activeTab === TABS.notification && <NotificationSettings />}
|
|
||||||
{activeTab === TABS.security && <SecuritySettings />}
|
|
||||||
{activeTab === TABS.payment && <PaymentSettings />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-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,187 +0,0 @@
|
|||||||
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, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react";
|
|
||||||
import { ModalDetailUser } from "./_components/modal/detail";
|
|
||||||
|
|
||||||
type UserStatus = 'active' | 'inactive';
|
|
||||||
|
|
||||||
interface UserType {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
rating: number
|
|
||||||
status: UserStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
rating: 4.5,
|
|
||||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const TABS = ['mentor', 'mentee'] as const
|
|
||||||
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
|
|
||||||
const [showDetail, setShowDetail] = useState(false)
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<UserType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
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: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rating',
|
|
||||||
header: 'Rating',
|
|
||||||
accessorKey: 'rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<UserStatus, string> = {
|
|
||||||
active: 'bg-success-200 text-success-500',
|
|
||||||
inactive: 'bg-danger-200 text-danger-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<UserStatus, string> = {
|
|
||||||
active: 'Active',
|
|
||||||
inactive: 'Inactive',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
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>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<div className="mb-8 flex justify-between items-center">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700">User Management</h1>
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
|
||||||
<For data={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>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap"
|
|
||||||
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>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalDetailUser
|
|
||||||
open={showDetail}
|
|
||||||
setOpen={setShowDetail}
|
|
||||||
userId={selectedUserId}
|
|
||||||
/>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -2,12 +2,26 @@ import { Button } from "@imphnen-frontend-service/ui/atoms"
|
|||||||
import { cn, Show } from "@imphnen-frontend-service/utils"
|
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { Link } from "react-router-dom"
|
import { Link } from "react-router-dom"
|
||||||
|
import { TArticleListItem } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
export type ArticleCardProps = {
|
export type ArticleCardProps = {
|
||||||
variant?: "default" | "recent" | "featured"
|
variant?: "default" | "recent" | "featured"
|
||||||
|
article?: TArticleListItem
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
function formatDate(iso?: string) {
|
||||||
|
if (!iso) return ""
|
||||||
|
return new Date(iso).toLocaleDateString("id-ID", { day: "numeric", month: "long", year: "numeric" })
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default', article }) => {
|
||||||
|
const title = article?.title ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||||
|
const excerpt = article?.excerpt ?? "Lorem ipsum dolor sit amet, consectetur adipiscing elit."
|
||||||
|
const author = article?.author_name ?? "IMPHNEN Editorial"
|
||||||
|
const date = formatDate(article?.created_at)
|
||||||
|
const slug = article?.slug ?? ""
|
||||||
|
const cover = article?.cover_url
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
@@ -22,25 +36,25 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"
|
src={cover ?? "/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"}
|
||||||
alt="Cover's Article"
|
alt={title}
|
||||||
className={cn("w-full object-cover", variant === "recent" && "h-full")}
|
className={cn("w-full object-cover", variant === "recent" && "h-full")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
|
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
|
||||||
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
|
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
|
||||||
5 min read
|
{article?.category ?? "Artikel"}
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
to="/articles/detail"
|
to={slug ? `/articles/${slug}` : "/articles"}
|
||||||
className={cn(
|
className={cn(
|
||||||
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
|
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
|
||||||
variant === "featured" && "text-[19px] md:text-[23px]",
|
variant === "featured" && "text-[19px] md:text-[23px]",
|
||||||
variant === "recent" && "mb-7 xl:text-[27px]"
|
variant === "recent" && "mb-7 xl:text-[27px]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
{title}
|
||||||
</Link>
|
</Link>
|
||||||
<Show condition={variant !== "recent"}>
|
<Show condition={variant !== "recent"}>
|
||||||
<p
|
<p
|
||||||
@@ -49,7 +63,7 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
|||||||
variant === "featured" && "text-[15px] md:text-[19px] md:mb-6",
|
variant === "featured" && "text-[15px] md:text-[19px] md:mb-6",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
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.
|
{excerpt}
|
||||||
</p>
|
</p>
|
||||||
</Show>
|
</Show>
|
||||||
<div
|
<div
|
||||||
@@ -59,14 +73,16 @@ export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
|||||||
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
|
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<p>Writer’s Name</p>
|
<p>{author}</p>
|
||||||
<p>14 March 2025</p>
|
<p>{date}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show condition={variant === "default"}>
|
<Show condition={variant === "default"}>
|
||||||
|
<Link to={slug ? `/articles/${slug}` : "/articles"}>
|
||||||
<Button type="button" size="sm" className="px-2.5 py-2">
|
<Button type="button" size="sm" className="px-2.5 py-2">
|
||||||
Baca Artikel
|
Baca Artikel
|
||||||
</Button>
|
</Button>
|
||||||
|
</Link>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,22 @@
|
|||||||
import { ReactElement, useRef, useState } from "react";
|
import { ReactElement, useRef, useState } from "react";
|
||||||
import { ArticleCard } from "./_components/card/article";
|
import { ArticleCard } from "./_components/card/article";
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
import { motion, useInView, Variants } from "framer-motion";
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
import { useGetArticleCategories, useGetArticles } from "@imphnen-frontend-service/service";
|
||||||
const CATEGORIES = ['UI/UX Design', 'Software/Web Dev', 'Data & AI', 'Cloud & DevOps', 'Cybersecurity', 'IT & Network', 'Project Management', 'QA & Testing'] as const
|
|
||||||
type Category = typeof CATEGORIES[number]
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
export default function Components(): ReactElement {
|
||||||
const [activeTab, setActiveTab] = useState<Category>('UI/UX Design')
|
const [activeTab, setActiveTab] = useState<string>("Semua")
|
||||||
|
const { data: articles = [], isLoading } = useGetArticles({ per_page: 20 })
|
||||||
|
const { data: categories = [] } = useGetArticleCategories()
|
||||||
|
|
||||||
|
const tabs = ["Semua", ...categories]
|
||||||
|
|
||||||
|
const featured = articles[0]
|
||||||
|
const recent = articles.slice(1, 4)
|
||||||
|
const filtered = activeTab === "Semua"
|
||||||
|
? articles
|
||||||
|
: articles.filter((a) => a.category === activeTab)
|
||||||
|
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
@@ -52,9 +60,13 @@ export default function Components(): ReactElement {
|
|||||||
Featuring Articles
|
Featuring Articles
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
condition={!isLoading}
|
||||||
|
fallback={<p className="text-neutral-500">Memuat artikel...</p>}
|
||||||
|
>
|
||||||
<div className="grid gap-12 xl:grid-cols-2">
|
<div className="grid gap-12 xl:grid-cols-2">
|
||||||
<motion.div variants={childVariants}>
|
<motion.div variants={childVariants}>
|
||||||
<ArticleCard variant="featured" />
|
<ArticleCard variant="featured" article={featured} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
@@ -65,16 +77,17 @@ export default function Components(): ReactElement {
|
|||||||
Recent Articles
|
Recent Articles
|
||||||
</motion.h1>
|
</motion.h1>
|
||||||
<div className="grid gap-10">
|
<div className="grid gap-10">
|
||||||
<For data={Array.from({ length: 3 })}>
|
<For data={recent}>
|
||||||
{(_, index) => (
|
{(article, index) => (
|
||||||
<motion.div key={index} variants={childVariants}>
|
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||||
<ArticleCard variant="recent" />
|
<ArticleCard variant="recent" article={article} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
@@ -86,7 +99,7 @@ export default function Components(): ReactElement {
|
|||||||
</motion.h1>
|
</motion.h1>
|
||||||
<div className="scrollbar-hide mb-8 w-full overflow-auto mx-auto">
|
<div className="scrollbar-hide mb-8 w-full overflow-auto mx-auto">
|
||||||
<motion.div className="min-w-max w-auto flex gap-x-3" variants={childVariants}>
|
<motion.div className="min-w-max w-auto flex gap-x-3" variants={childVariants}>
|
||||||
<For data={CATEGORIES}>
|
<For data={tabs}>
|
||||||
{(category) => (
|
{(category) => (
|
||||||
<Button
|
<Button
|
||||||
key={category}
|
key={category}
|
||||||
@@ -106,15 +119,20 @@ export default function Components(): ReactElement {
|
|||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<Show
|
||||||
|
condition={filtered.length > 0}
|
||||||
|
fallback={<p className="text-neutral-500 text-center py-10">Belum ada artikel di kategori ini.</p>}
|
||||||
|
>
|
||||||
<div className="grid gap-8 xl:grid-cols-3">
|
<div className="grid gap-8 xl:grid-cols-3">
|
||||||
<For data={Array.from({ length: 3 })}>
|
<For data={filtered}>
|
||||||
{(_, index) => (
|
{(article, index) => (
|
||||||
<motion.div key={index} variants={childVariants}>
|
<motion.div key={article.id ?? index} variants={childVariants}>
|
||||||
<ArticleCard />
|
<ArticleCard article={article} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
</motion.section>
|
</motion.section>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
+29
-5
@@ -10,6 +10,7 @@ import { QrisPaymentStep } from "./steps/qris-payement"
|
|||||||
import { VAPaymentStep } from "./steps/va-payment"
|
import { VAPaymentStep } from "./steps/va-payment"
|
||||||
import { SuccessStep } from "./steps/success"
|
import { SuccessStep } from "./steps/success"
|
||||||
import { PaymentStep } from "./steps/payment"
|
import { PaymentStep } from "./steps/payment"
|
||||||
|
import { TBookSessionRequest, usePostBookSession } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||||
type Step = typeof STEPS[number]
|
type Step = typeof STEPS[number]
|
||||||
@@ -17,13 +18,32 @@ type Step = typeof STEPS[number]
|
|||||||
type Props = {
|
type Props = {
|
||||||
open: boolean
|
open: boolean
|
||||||
setOpen: (open: boolean) => void
|
setOpen: (open: boolean) => void
|
||||||
|
mentorId?: string
|
||||||
|
mentor?: { fullname?: string | null; current_role?: string; current_company?: string }
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
export const AppointmentModal: FC<Props> = ({ open, setOpen, mentorId, mentor }) => {
|
||||||
const [step, setStep] = useState<Step>('topic')
|
const [step, setStep] = useState<Step>('topic')
|
||||||
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||||
|
const [booking, setBooking] = useState<TBookSessionRequest | null>(null)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const bookMutation = usePostBookSession(mentorId ?? "")
|
||||||
|
|
||||||
const handleStep = (action: 'next' | 'prev') => {
|
const handleStep = async (action: 'next' | 'prev') => {
|
||||||
|
setError("")
|
||||||
|
if (step === 'payment' && action === 'next') {
|
||||||
|
if (!mentorId || !booking) {
|
||||||
|
setError("Data booking tidak lengkap.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await bookMutation.mutateAsync({ ...booking, topic: booking.topic || `Mentoring #${selectedTopics[0] ?? 1}` })
|
||||||
|
setStep('success')
|
||||||
|
} catch {
|
||||||
|
setError("Gagal membuat sesi mentoring. Coba lagi.")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
if (action === 'next' && step === 'success') {
|
if (action === 'next' && step === 'success') {
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
} else if (action === 'next') {
|
} else if (action === 'next') {
|
||||||
@@ -113,13 +133,17 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
|
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
||||||
{step === 'schedule' && <ScheduleStep />}
|
{step === 'schedule' && <ScheduleStep onChange={(data) => setBooking(data)} />}
|
||||||
{step === 'profile' && <ProfileStep />}
|
{step === 'profile' && <ProfileStep />}
|
||||||
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} mentor={mentor} booking={booking} />}
|
||||||
{step === 'qr-payment' && <QrisPaymentStep />}
|
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||||
{step === 'va-payment' && <VAPaymentStep />}
|
{step === 'va-payment' && <VAPaymentStep />}
|
||||||
{step === 'success' && <SuccessStep />}
|
{step === 'success' && <SuccessStep />}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<p className="text-danger-600 text-xs mt-4 font-medium">{error}</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show condition={step !== 'success'}>
|
<Show condition={step !== 'success'}>
|
||||||
@@ -148,7 +172,7 @@ export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
|||||||
condition={step !== 'success'}
|
condition={step !== 'success'}
|
||||||
fallback="Halman Booking"
|
fallback="Halman Booking"
|
||||||
>
|
>
|
||||||
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
|
<Show condition={step !== 'payment'} fallback={bookMutation.isPending ? "Memproses..." : "Bayar Sekarang"}>
|
||||||
Selanjutnya
|
Selanjutnya
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
+17
-6
@@ -2,12 +2,18 @@ import { For, Show } from "@imphnen-frontend-service/utils"
|
|||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
import { TOPICS } from "../../../sections/topics"
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
selectedTopics: number[]
|
selectedTopics: number[]
|
||||||
|
mentor?: { fullname?: string | null; current_role?: string; current_company?: string } | null
|
||||||
|
booking?: TBookSessionRequest | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
|
export const PaymentStep: FC<Props> = ({ selectedTopics, mentor, booking }) => {
|
||||||
|
const rate = 50000
|
||||||
|
const serviceFee = 2000
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
|
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
|
||||||
@@ -26,11 +32,16 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
|
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
|
||||||
<h3 className="text-xs font-semibold mb-1 xl:text-[15px]">
|
<h3 className="text-xs font-semibold mb-1 xl:text-[15px]">
|
||||||
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
{mentor?.fullname || "Mentor"}
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-[10px] text-neutral-600 md:text-xs">
|
<p className="text-[10px] text-neutral-600 md:text-xs">
|
||||||
UI Designer at Oray orayan Studios
|
{mentor?.current_role || "Mentor"} at {mentor?.current_company || "IMPHNEN"}
|
||||||
</p>
|
</p>
|
||||||
|
{booking?.scheduled_at && (
|
||||||
|
<p className="text-[10px] text-neutral-500 md:text-xs mt-1">
|
||||||
|
{new Date(booking.scheduled_at).toLocaleString("id-ID", { dateStyle: "full", timeStyle: "short" })}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,14 +115,14 @@ export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
|
|||||||
</p>
|
</p>
|
||||||
<div className="grid grid-cols-2 space-y-2.5 text-[10px] text-neutral-400 font-medium md:text-xs md:space-y-3">
|
<div className="grid grid-cols-2 space-y-2.5 text-[10px] text-neutral-400 font-medium md:text-xs md:space-y-3">
|
||||||
<p className="font-normal">Subtotal :</p>
|
<p className="font-normal">Subtotal :</p>
|
||||||
<p className="text-right">Rp. 50.000</p>
|
<p className="text-right">Rp. {rate.toLocaleString("id-ID")}</p>
|
||||||
<p className="font-normal">Service Fee :</p>
|
<p className="font-normal">Service Fee :</p>
|
||||||
<p className="text-right">Rp. 2.000</p>
|
<p className="text-right">Rp. {serviceFee.toLocaleString("id-ID")}</p>
|
||||||
|
|
||||||
<hr className="border-neutral-200 col-span-full mt-1" />
|
<hr className="border-neutral-200 col-span-full mt-1" />
|
||||||
|
|
||||||
<p className="text-neutral-700">Total :</p>
|
<p className="text-neutral-700">Total :</p>
|
||||||
<p className="text-right text-neutral-700">Rp. 52.000</p>
|
<p className="text-right text-neutral-700">Rp. {(rate + serviceFee).toLocaleString("id-ID")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+29
-5
@@ -1,6 +1,8 @@
|
|||||||
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn } from "@imphnen-frontend-service/utils"
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
import { motion } from "framer-motion"
|
import { motion } from "framer-motion"
|
||||||
|
import { useEffect, useState } from "react"
|
||||||
|
import { TBookSessionRequest } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||||
|
|
||||||
@@ -13,7 +15,27 @@ Saya ingin tahu secara khusus tentang ___.
|
|||||||
|
|
||||||
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 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 ScheduleStep = () => {
|
type Props = {
|
||||||
|
onChange: (data: TBookSessionRequest) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ScheduleStep = ({ onChange }: Props) => {
|
||||||
|
const [date, setDate] = useState("")
|
||||||
|
const [time, setTime] = useState("")
|
||||||
|
const [sessionType, setSessionType] = useState("online")
|
||||||
|
const [description, setDescription] = useState("")
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!date || !time) return
|
||||||
|
onChange({
|
||||||
|
topic: "Mentoring Session",
|
||||||
|
scheduled_at: `${date}T${time}:00`,
|
||||||
|
description,
|
||||||
|
duration_minutes: 60,
|
||||||
|
session_type: sessionType,
|
||||||
|
})
|
||||||
|
}, [date, time, sessionType, description])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="bg-white px-6 py-5 rounded-md md:px-6"
|
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||||
@@ -31,15 +53,15 @@ export const ScheduleStep = () => {
|
|||||||
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>Tanggal</label>
|
<label className={labelClass}>Tanggal</label>
|
||||||
<Input type="date" className="min-w-full w-full" />
|
<Input type="date" className="min-w-full w-full" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label className={labelClass}>Waktu</label>
|
<label className={labelClass}>Waktu</label>
|
||||||
<Input type="time" className="min-w-full w-full" />
|
<Input type="time" className="min-w-full w-full" value={time} onChange={(e) => setTime(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative md:col-span-full">
|
<div className="relative md:col-span-full">
|
||||||
<label className={labelClass}>Lokasi</label>
|
<label className={labelClass}>Lokasi</label>
|
||||||
<Select className="min-w-full w-full">
|
<Select className="min-w-full w-full" value={sessionType} onChange={(e) => setSessionType(e.target.value)}>
|
||||||
<option value="online">Online</option>
|
<option value="online">Online</option>
|
||||||
<option value="offline">Offline</option>
|
<option value="offline">Offline</option>
|
||||||
</Select>
|
</Select>
|
||||||
@@ -48,7 +70,9 @@ export const ScheduleStep = () => {
|
|||||||
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="min-w-full w-full h-40"
|
className="min-w-full w-full h-40"
|
||||||
placeholder={placeholder} />
|
placeholder={placeholder}
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|||||||
@@ -1,12 +1,20 @@
|
|||||||
import { StarFilled } from "@ant-design/icons"
|
import { StarFilled } from "@ant-design/icons"
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBook: () => void
|
onBook: () => void
|
||||||
|
mentor?: TMentorDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
|
||||||
|
const name = mentor?.fullname || mentor?.legal_name || "Mentor"
|
||||||
|
const role = mentor?.current_role || "Mentor"
|
||||||
|
const company = mentor?.current_company || "IMPHNEN"
|
||||||
|
const expertise = mentor?.expertise ?? []
|
||||||
|
const softSkills = mentor?.languages ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
||||||
<div
|
<div
|
||||||
@@ -33,10 +41,10 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
{name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
||||||
UI Designer at Oray orayan Studios
|
{role} at {company}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
||||||
<div
|
<div
|
||||||
@@ -56,7 +64,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
<For data={['UI Design', 'UX Reseacrh']}>
|
<For data={expertise}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
{item}
|
{item}
|
||||||
@@ -68,7 +76,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
|
<For data={softSkills}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
{item}
|
{item}
|
||||||
|
|||||||
+51
-42
@@ -1,61 +1,69 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { useGetMentorAvailability } from "@imphnen-frontend-service/service"
|
||||||
import { FC } from "react"
|
import { FC } from "react"
|
||||||
|
|
||||||
const SCHEDULES = [
|
|
||||||
{
|
|
||||||
date: "Kamis, 20 Maret 2025",
|
|
||||||
availableCount: 10,
|
|
||||||
schedule: [
|
|
||||||
{ time: "19:00 WIB", available: true },
|
|
||||||
{ time: "20:00 WIB", available: true },
|
|
||||||
{ time: "21:00 WIB", available: true },
|
|
||||||
{ time: "22:00 WIB", available: true },
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
date: "Jumat, 21 Maret 2025",
|
|
||||||
availableCount: 1,
|
|
||||||
schedule: [
|
|
||||||
{ time: "19:00 WIB", available: true },
|
|
||||||
{ time: "20:00 WIB", available: true },
|
|
||||||
{ time: "21:00 WIB", available: false },
|
|
||||||
{ time: "22:00 WIB", available: true },
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBook: () => void
|
onBook: () => void
|
||||||
|
mentorId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
|
const DAY_NAMES = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"]
|
||||||
|
|
||||||
|
function formatDate(dateStr: string) {
|
||||||
|
const d = new Date(dateStr + "T00:00:00")
|
||||||
|
return `${DAY_NAMES[d.getDay()]}, ${d.getDate()} ${d.toLocaleString("id-ID", { month: "long" })} ${d.getFullYear()}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(timeStr: string) {
|
||||||
|
const [h] = timeStr.split(":").map(Number)
|
||||||
|
return `${String(h).padStart(2, "0")}:00 WIB`
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SenpaiScheduleSection: FC<Props> = ({ onBook, mentorId }) => {
|
||||||
|
const { data: availability, isLoading } = useGetMentorAvailability(mentorId ?? "")
|
||||||
|
|
||||||
|
const slots = availability?.slots ?? []
|
||||||
|
const byDate = new Map<string, typeof slots>()
|
||||||
|
for (const s of slots) {
|
||||||
|
if (!byDate.has(s.date)) byDate.set(s.date, [])
|
||||||
|
byDate.get(s.date)!.push(s)
|
||||||
|
}
|
||||||
|
const days = [...byDate.entries()]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
|
||||||
<For data={SCHEDULES}>
|
<Show
|
||||||
{(item) => (
|
condition={!isLoading && days.length > 0}
|
||||||
<div key={item.date}>
|
fallback={
|
||||||
<div className="flex justify-between items-center mb-4">
|
<p className="text-xs text-neutral-500">
|
||||||
<p className="text-[15px] font-medium">{item.date}</p>
|
{isLoading ? "Memuat jadwal senpai..." : "Belum ada jadwal tersedia."}
|
||||||
<p
|
</p>
|
||||||
className={cn(
|
}
|
||||||
"rounded-4xl py-1 px-2.5 text-xs font-medium",
|
|
||||||
item.availableCount > 5 ? "bg-primary-200 text-primary-500" : "bg-danger-100 text-danger-600"
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{item.availableCount} slot tersisa
|
<For data={days}>
|
||||||
|
{([date, daySlots]) => (
|
||||||
|
<div key={date}>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<p className="text-[15px] font-medium">{formatDate(date)}</p>
|
||||||
|
<p className="rounded-4xl py-1 px-2.5 text-xs font-medium bg-primary-200 text-primary-500">
|
||||||
|
{daySlots.length} slot tersisa
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-x-3">
|
<div className="flex flex-wrap gap-x-3 gap-y-2">
|
||||||
<For data={item.schedule}>
|
<For data={daySlots}>
|
||||||
{(schedule, index) => (
|
{(schedule) => (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={`${schedule.date}-${schedule.time}`}
|
||||||
className="bg-primary-300 text-primary-600 px-3 py-2 text-xs rounded-md font-semibold"
|
className={`px-3 py-2 text-xs rounded-md font-semibold ${
|
||||||
|
schedule.available
|
||||||
|
? "bg-primary-300 text-primary-600"
|
||||||
|
: "bg-neutral-200 text-neutral-400 line-through"
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{schedule.time}
|
{formatTime(schedule.time)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
@@ -63,6 +71,7 @@ export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<hr className="border-primary-200" />
|
<hr className="border-primary-200" />
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,40 +1,70 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { For } from '@imphnen-frontend-service/utils';
|
||||||
|
import { TMentorDetail } from '@imphnen-frontend-service/service';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export const MentorCard: FC = () => {
|
type TMentorCardProps = {
|
||||||
|
mentor: TMentorDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
|
||||||
|
const experience =
|
||||||
|
mentor.years_of_experience > 0
|
||||||
|
? `${mentor.years_of_experience} Years Experience`
|
||||||
|
: 'New Mentor';
|
||||||
|
const role = mentor.current_role || 'Mentor';
|
||||||
|
const company = mentor.current_company || 'IMPHNEN';
|
||||||
|
const softSkills = mentor.expertise || [];
|
||||||
|
const shownSkills = softSkills.slice(0, 2);
|
||||||
|
const extraCount = Math.max(0, softSkills.length - shownSkills.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||||
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||||
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||||
3-5 Years Experience
|
{experience}
|
||||||
</Button>
|
</Button>
|
||||||
<h2 className="mb-1">
|
<h2 className="mb-1">
|
||||||
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||||
Fullname
|
{mentor.fullname || mentor.legal_name || 'Mentor'}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||||
Full Stack Enjoyer at name company
|
{role} at {company}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{softSkills.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
<For data={shownSkills}>
|
||||||
Communication
|
{(skill) => (
|
||||||
|
<Button
|
||||||
|
key={skill}
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]"
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
)}
|
||||||
Communication
|
</For>
|
||||||
</Button>
|
{extraCount > 0 && (
|
||||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
<Button
|
||||||
+2
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]"
|
||||||
|
>
|
||||||
|
+{extraCount}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,27 +3,24 @@ import { BannerSection } from './_components/banner-section';
|
|||||||
import { Topics } from './_components/topics';
|
import { Topics } from './_components/topics';
|
||||||
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { For } from '@imphnen-frontend-service/utils';
|
import { For, Show } from '@imphnen-frontend-service/utils';
|
||||||
import { MentorCard } from './_components/mentor-card';
|
import { MentorCard } from './_components/mentor-card';
|
||||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useGetMentors } from '@imphnen-frontend-service/service';
|
||||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
import { motion, useInView, Variants } from 'framer-motion';
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
const TEMP_DATA = [
|
|
||||||
{ id: 1, name: 'John Doe' },
|
|
||||||
{ id: 2, name: 'John Doe' },
|
|
||||||
{ id: 3, name: 'John Doe' },
|
|
||||||
{ id: 4, name: 'John Doe' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 3,
|
pageSize: 8,
|
||||||
});
|
});
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const { data: mentors, isLoading } = useGetMentors({ search });
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: TEMP_DATA,
|
data: mentors ?? [],
|
||||||
columns: [],
|
columns: [],
|
||||||
state: { pagination },
|
state: { pagination },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -31,6 +28,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageMentors = table.getRowModel().rows.map((row) => row.original);
|
||||||
|
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
@@ -91,6 +90,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||||
className="relative min-w-full w-full"
|
className="relative min-w-full w-full"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -101,13 +102,31 @@ export const Components: FC = (): ReactElement => {
|
|||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isInView ? 'visible' : 'hidden'}
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
>
|
>
|
||||||
<For data={Array.from({ length: 8 })}>
|
<Show
|
||||||
{(_, index) => (
|
condition={!isLoading}
|
||||||
<motion.div key={index} variants={childVariants}>
|
fallback={
|
||||||
<MentorCard />
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Memuat data mentor...
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={pageMentors.length > 0}
|
||||||
|
fallback={
|
||||||
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Tidak ada mentor ditemukan
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For data={pageMentors}>
|
||||||
|
{(mentor, index) => (
|
||||||
|
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||||
|
<MentorCard mentor={mentor} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<Pagination table={table} />
|
<Pagination table={table} />
|
||||||
|
|||||||
@@ -1,11 +1,80 @@
|
|||||||
import { FC, ReactElement, useState } from 'react';
|
import { FC, ReactElement, useState } from 'react';
|
||||||
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
import { Button, Select } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||||
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { usePostRegisterMentor, TMentorRegisterRequest } from '@imphnen-frontend-service/service';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [ step, setStep ] = useState(1)
|
const [step, setStep] = useState(1)
|
||||||
|
const [submitting, setSubmitting] = useState(false)
|
||||||
|
const [error, setError] = useState("")
|
||||||
|
const [form, setForm] = useState({
|
||||||
|
email: "", password: "", confirmPassword: "",
|
||||||
|
fullname: "", phone_number: "", gender: "", domicile: "",
|
||||||
|
legal_name: "", phone_for_verification: "", identity_document_url: "",
|
||||||
|
last_education: "", bio: "", linkedin_url: "", github_url: "",
|
||||||
|
cv_url: "", portfolio_url: "", industries: "", expertise: "", languages: "",
|
||||||
|
current_company: "", current_role: "", years_of_experience: "",
|
||||||
|
topics_of_interest: "", preferred_mentee_level: "", preferred_mentoring_formats: "",
|
||||||
|
availability_commitment: "", mentoring_rate_amount: "",
|
||||||
|
})
|
||||||
|
const setF = (k: string, v: string) => setForm((f) => ({ ...f, [k]: v }))
|
||||||
|
const registerMutation = usePostRegisterMentor()
|
||||||
|
const navigate = useNavigate()
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setError("")
|
||||||
|
if (form.password !== form.confirmPassword) {
|
||||||
|
setError("Password dan konfirmasi tidak sama.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const payload: TMentorRegisterRequest = {
|
||||||
|
email: form.email,
|
||||||
|
password: form.password,
|
||||||
|
fullname: form.fullname,
|
||||||
|
phone_number: form.phone_number || null,
|
||||||
|
identity_and_verification: {
|
||||||
|
legal_name: form.legal_name || form.fullname,
|
||||||
|
gender: form.gender || null,
|
||||||
|
domicile: form.domicile || null,
|
||||||
|
identity_document_url: form.identity_document_url || "https://example.com/identity.jpg",
|
||||||
|
phone_for_verification: form.phone_for_verification || null,
|
||||||
|
},
|
||||||
|
professional_profile: {
|
||||||
|
bio: form.bio,
|
||||||
|
last_education: form.last_education || null,
|
||||||
|
linkedin_url: form.linkedin_url || null,
|
||||||
|
github_url: form.github_url || null,
|
||||||
|
cv_url: form.cv_url || null,
|
||||||
|
portfolio_url: form.portfolio_url || null,
|
||||||
|
industries: form.industries.split(",").map((s) => s.trim()).filter(Boolean),
|
||||||
|
expertise: form.expertise.split(",").map((s) => s.trim()).filter(Boolean),
|
||||||
|
languages: form.languages.split(",").map((s) => s.trim()).filter(Boolean),
|
||||||
|
current_company: form.current_company,
|
||||||
|
current_role: form.current_role,
|
||||||
|
years_of_experience: Number(form.years_of_experience) || 0,
|
||||||
|
},
|
||||||
|
mentoring_logistics: {
|
||||||
|
topics_of_interest: form.topics_of_interest.split(",").map((s) => s.trim()).filter(Boolean),
|
||||||
|
preferred_mentee_level: [form.preferred_mentee_level].filter(Boolean),
|
||||||
|
preferred_mentoring_formats: form.preferred_mentoring_formats.split(",").map((s) => s.trim()).filter(Boolean),
|
||||||
|
availability_commitment: form.availability_commitment,
|
||||||
|
mentoring_rate_amount: Number(form.mentoring_rate_amount) || 0,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
setSubmitting(true)
|
||||||
|
try {
|
||||||
|
await registerMutation.mutateAsync(payload)
|
||||||
|
navigate('/auth/register-mentor/pending')
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const msg = (e as { response?: { data?: { message?: string } } })?.response?.data?.message
|
||||||
|
setError(msg || "Gagal mendaftar sebagai mentor. Coba lagi.")
|
||||||
|
} finally {
|
||||||
|
setSubmitting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
@@ -13,13 +82,6 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<RegisterResetBanner />
|
<RegisterResetBanner />
|
||||||
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||||
<div className="xl:w-[714px]">
|
<div className="xl:w-[714px]">
|
||||||
<Button
|
|
||||||
className='xl:hidden gap-3'
|
|
||||||
variant='secondary'
|
|
||||||
>
|
|
||||||
<ArrowLeftOutlined />
|
|
||||||
Login
|
|
||||||
</Button>
|
|
||||||
<RegisterMentorStep step={step} />
|
<RegisterMentorStep step={step} />
|
||||||
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||||
Register
|
Register
|
||||||
@@ -27,60 +89,44 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<h5 className="text-primary-500 font-medium">
|
<h5 className="text-primary-500 font-medium">
|
||||||
Welcome to the team, Mentor - powered by IMPHNEN
|
Welcome to the team, Mentor - powered by IMPHNEN
|
||||||
</h5>
|
</h5>
|
||||||
<form>
|
<form onSubmit={(e) => e.preventDefault()}>
|
||||||
<div className={`${step !== 1 ? 'hidden' : ''}`}>
|
<div className={`${step !== 1 ? 'hidden' : ''}`}>
|
||||||
<div className='mt-7 mb-2'>
|
<div className='mt-7 mb-2'>
|
||||||
<InputField
|
<InputField
|
||||||
label="Nama Lengkap"
|
label="Nama Lengkap" size="lg" className="w-full"
|
||||||
size="lg"
|
placeholder="Nama Lengkap" name="fullname"
|
||||||
className="w-full"
|
value={form.fullname} onChange={(e) => setF('fullname', e.target.value)}
|
||||||
placeholder="Nama Lengkap"
|
|
||||||
name={'fullname'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Jenis Kelamin"
|
label="Jenis Kelamin" defaultValue="" size="lg" error=""
|
||||||
defaultValue=""
|
value={form.gender} onChange={(e) => setF('gender', e.target.value)}
|
||||||
size="lg"
|
|
||||||
error=""
|
|
||||||
>
|
>
|
||||||
<option value="" disabled hidden>
|
<option value="" disabled hidden>Pilih Jenis Kelamin</option>
|
||||||
Pilih Jenis Kelamin
|
<option value="Laki-laki">Laki - Laki</option>
|
||||||
</option>
|
<option value="Perempuan">Perempuan</option>
|
||||||
<option value="laki-laki">Laki - Laki</option>
|
|
||||||
<option value="perempuan">Perempuan</option>
|
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<div className='my-2'>
|
<div className='my-2'>
|
||||||
<InputField
|
<InputField
|
||||||
label="No Telp/Whatsapp Only"
|
label="No Telp/Whatsapp Only" size="lg" className="w-full"
|
||||||
size="lg"
|
placeholder="Nomor WhatsApp" name="phone_number"
|
||||||
className="w-full"
|
value={form.phone_number} onChange={(e) => setF('phone_number', e.target.value)}
|
||||||
placeholder="Nama Lengkap"
|
|
||||||
name={'fullname'}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Domisili"
|
label="Domisili" defaultValue="" size="lg" error=""
|
||||||
defaultValue=""
|
value={form.domicile} onChange={(e) => setF('domicile', e.target.value)}
|
||||||
size="lg"
|
|
||||||
error=""
|
|
||||||
>
|
>
|
||||||
<option value="" disabled hidden>
|
<option value="" disabled hidden>Pilih Domisili</option>
|
||||||
Pilih Domisili
|
<option value="Aceh">Aceh</option>
|
||||||
</option>
|
<option value="Jawa Barat">Jawa Barat</option>
|
||||||
<option value="aceh">Aceh</option>
|
|
||||||
<option value="jawa-selatan">Jawa Selatan</option>
|
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<div className='mt-2'>
|
<div className='mt-2'>
|
||||||
<SelectField
|
<SelectField
|
||||||
label="Pendidikan Terakhir"
|
label="Pendidikan Terakhir" defaultValue="" size="lg" error=""
|
||||||
defaultValue=""
|
value={form.last_education} onChange={(e) => setF('last_education', e.target.value)}
|
||||||
size="lg"
|
|
||||||
error=""
|
|
||||||
>
|
>
|
||||||
<option value="" disabled hidden>
|
<option value="" disabled hidden>Apa pendidikan terakhir senpai</option>
|
||||||
Apa pendidikan terakhir senpai
|
|
||||||
</option>
|
|
||||||
<option value="SMA">SMA</option>
|
<option value="SMA">SMA</option>
|
||||||
<option value="S1">S1</option>
|
<option value="S1">S1</option>
|
||||||
<option value="S2">S2</option>
|
<option value="S2">S2</option>
|
||||||
@@ -90,145 +136,85 @@ export const Components: FC = (): ReactElement => {
|
|||||||
</div>
|
</div>
|
||||||
<div className={`${step !== 2 ? 'hidden' : ''}`}>
|
<div className={`${step !== 2 ? 'hidden' : ''}`}>
|
||||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
<InputField
|
<InputField label="Perusahaan Saat Ini" size="lg" className="w-full"
|
||||||
label="Perusahaan Saat Ini"
|
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨" name="current_company"
|
||||||
size="lg"
|
value={form.current_company} onChange={(e) => setF('current_company', e.target.value)} />
|
||||||
className="w-full"
|
<InputField label="Masukan jabatan senpai di perusahaan" size="lg" className="w-full"
|
||||||
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
placeholder="Jabatan" name="current_role"
|
||||||
name={'fullname'}
|
value={form.current_role} onChange={(e) => setF('current_role', e.target.value)} />
|
||||||
/>
|
<InputField label="Topik Keahlian Yang Ingin Diajarkan (pisahkan koma)" size="lg" className="w-full"
|
||||||
<InputField
|
placeholder="mis. Backend Developer, Rust" name="expertise"
|
||||||
label="Masukan jabatan senpai di perusahaan"
|
value={form.expertise} onChange={(e) => setF('expertise', e.target.value)} />
|
||||||
size="lg"
|
<InputField label="Industri (pisahkan koma)" size="lg" className="w-full"
|
||||||
className="w-full"
|
placeholder="mis. Software, Education" name="industries"
|
||||||
placeholder="Masukan jabatan senpai di perusahaan"
|
value={form.industries} onChange={(e) => setF('industries', e.target.value)} />
|
||||||
name={'fullname'}
|
<InputField label="Skill Utama / Bahasa (pisahkan koma)" size="lg" className="w-full"
|
||||||
/>
|
placeholder="mis. Rust, TypeScript" name="languages"
|
||||||
<SelectField
|
value={form.languages} onChange={(e) => setF('languages', e.target.value)} />
|
||||||
label="Topik Keahlian Yang Ingin Diajarkan"
|
<InputField label="Tahun Pengalaman" type="number" size="lg" className="w-full"
|
||||||
defaultValue=""
|
placeholder="mis. 3" name="years_of_experience"
|
||||||
size="lg"
|
value={form.years_of_experience} onChange={(e) => setF('years_of_experience', e.target.value)} />
|
||||||
error=""
|
<InputField label="Bio Singkat" size="lg" className="w-full"
|
||||||
>
|
placeholder="Ceritakan pengalaman Anda" name="bio"
|
||||||
<option value="" disabled hidden>
|
value={form.bio} onChange={(e) => setF('bio', e.target.value)} />
|
||||||
Apa yang ingin anda ajarkan senpaii
|
|
||||||
</option>
|
|
||||||
<option value="Frontend Developer">Frontend Developer</option>
|
|
||||||
<option value="Backend Developer">Backend Developer</option>
|
|
||||||
<option value="Other">Other</option>
|
|
||||||
</SelectField>
|
|
||||||
<InputField
|
|
||||||
label="Jika Mengisi “Other”, Tulis Topik yang Kamu Kuasai"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Tolong isi jika tidak ada di pilihan senpai"
|
|
||||||
name={'fullname'}
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Skill Utama yang Kamu Miliki (boleh lebih dari satu)"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Isi Skill nya dong senpai"
|
|
||||||
name={'fullname'}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
||||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
<SelectField
|
<InputField label="Topik Mentoring (pisahkan koma)" size="lg" className="w-full"
|
||||||
label="Durasi Sesi Mentoring"
|
placeholder="mis. Rust Programming, Backend" name="topics_of_interest"
|
||||||
defaultValue=""
|
value={form.topics_of_interest} onChange={(e) => setF('topics_of_interest', e.target.value)} />
|
||||||
size="lg"
|
<SelectField label="Tingkat Mentee" defaultValue="" size="lg" error=""
|
||||||
error=""
|
value={form.preferred_mentee_level} onChange={(e) => setF('preferred_mentee_level', e.target.value)}>
|
||||||
>
|
<option value="" disabled hidden>Pilih tingkat mentee</option>
|
||||||
<option value="" disabled hidden>
|
<option value="beginner">Beginner</option>
|
||||||
Placeholder
|
<option value="intermediate">Intermediate</option>
|
||||||
</option>
|
<option value="advanced">Advanced</option>
|
||||||
</SelectField>
|
</SelectField>
|
||||||
<SelectField
|
<InputField label="Format Pengajaran (pisahkan koma, mis. online,offline)" size="lg" className="w-full"
|
||||||
label="Format Pengajaran yang Anda Sediakan"
|
placeholder="online,offline" name="preferred_mentoring_formats"
|
||||||
defaultValue=""
|
value={form.preferred_mentoring_formats} onChange={(e) => setF('preferred_mentoring_formats', e.target.value)} />
|
||||||
size="lg"
|
<InputField label="Komitmen Ketersediaan" size="lg" className="w-full"
|
||||||
error=""
|
placeholder="mis. 2 jam per minggu" name="availability_commitment"
|
||||||
>
|
value={form.availability_commitment} onChange={(e) => setF('availability_commitment', e.target.value)} />
|
||||||
<option value="" disabled hidden>
|
<InputField label="Honorarium yang Diharapkan per Sesi" type="number" size="lg" className="w-full"
|
||||||
Placeholder
|
placeholder="mis. 500000" name="mentoring_rate_amount"
|
||||||
</option>
|
value={form.mentoring_rate_amount} onChange={(e) => setF('mentoring_rate_amount', e.target.value)} />
|
||||||
</SelectField>
|
<InputField label="URL LinkedIn" size="lg" className="w-full"
|
||||||
<InputField
|
placeholder="https://linkedin.com/in/..." name="linkedin_url"
|
||||||
type="file"
|
value={form.linkedin_url} onChange={(e) => setF('linkedin_url', e.target.value)} />
|
||||||
label="Unggah Curiculum Vitae"
|
<InputField label="URL Portofolio" size="lg" className="w-full"
|
||||||
size="lg"
|
placeholder="Google Drive, GitHub, Behance" name="portfolio_url"
|
||||||
className="w-full"
|
value={form.portfolio_url} onChange={(e) => setF('portfolio_url', e.target.value)} />
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Unggah Portofolio"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Google Drive, Behance, GitHub, Notion, dan lainnya"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
type='number'
|
|
||||||
label="Honorarium yang Diharapkan per Sesi (Online) Durasi rata-rata sesi: 2,5–3 jam"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Isi Nominalnya dong senpaiii!!!"
|
|
||||||
/>
|
|
||||||
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
||||||
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
<InputField
|
<InputField type='email' label="Email" size="lg" className="w-full"
|
||||||
type='email'
|
placeholder="isi emailnya dong senpai!!!" name="email"
|
||||||
label="Email"
|
value={form.email} onChange={(e) => setF('email', e.target.value)} />
|
||||||
size="lg"
|
<InputField type='password' label="Buat Password" size="lg" className="w-full"
|
||||||
className="w-full"
|
placeholder="Buat Password" name="password"
|
||||||
placeholder="isi emailnya dong senpai!!!"
|
value={form.password} onChange={(e) => setF('password', e.target.value)} />
|
||||||
/>
|
<InputField type='password' label="Konfirmasi Password" size="lg" className="w-full"
|
||||||
<InputField
|
placeholder="Ulangi Password" name="confirmPassword"
|
||||||
type='password'
|
value={form.confirmPassword} onChange={(e) => setF('confirmPassword', e.target.value)} />
|
||||||
label="Buat Password"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Buat Password"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
type='password'
|
|
||||||
label="Buat Password"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
placeholder="Buat Password"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{error && <p className="text-danger-600 text-xs mt-3 font-medium">{error}</p>}
|
||||||
<div className='flex justify-end mt-4 gap-2'>
|
<div className='flex justify-end mt-4 gap-2'>
|
||||||
<Button
|
<Button variant='secondary' onClick={() => setStep(step - 1)} type='button'
|
||||||
variant='secondary'
|
className={`${step === 1 ? 'hidden' : ''}`}>
|
||||||
onClick={() => setStep(step - 1)}
|
<ArrowLeftOutlined /> Kembali
|
||||||
type='button'
|
|
||||||
className={`${step === 1 ? 'hidden' : ''}`}
|
|
||||||
>
|
|
||||||
<ArrowLeftOutlined />
|
|
||||||
Kembali
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant='primary' onClick={() => setStep(step + 1)} type='button'
|
||||||
variant='primary'
|
className={`${step === 4 ? 'hidden' : ''}`}>
|
||||||
onClick={() => setStep(step + 1)}
|
Lanjutkan <ArrowRightOutlined />
|
||||||
type='button'
|
|
||||||
className={`${step === 4 ? 'hidden' : ''}`}
|
|
||||||
>
|
|
||||||
Lanjutkan
|
|
||||||
<ArrowRightOutlined />
|
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button variant='primary' onClick={handleSubmit} type='button'
|
||||||
variant='primary'
|
className={`${step === 4 ? '' : 'hidden'}`} disabled={submitting}>
|
||||||
onClick={() => setStep(step + 1)}
|
{submitting ? "Mengirim..." : "Selesaikan"} <ArrowRightOutlined />
|
||||||
type='button'
|
|
||||||
className={`${step === 4 ? '' : 'hidden'}`}
|
|
||||||
>
|
|
||||||
Selesaikan
|
|
||||||
<ArrowRightOutlined />
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Navbar } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className='bg-blue-50'>
|
||||||
|
<Navbar />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -17,7 +17,9 @@ const mappingPublicRoutes = [
|
|||||||
'/auth/register-mentor',
|
'/auth/register-mentor',
|
||||||
'/auth/register-mentor/pending',
|
'/auth/register-mentor/pending',
|
||||||
'/auth/register-mentor/success',
|
'/auth/register-mentor/success',
|
||||||
|
'/mentoring',
|
||||||
'/resources',
|
'/resources',
|
||||||
|
'/articles',
|
||||||
];
|
];
|
||||||
|
|
||||||
const mappingRoutePermissions = [
|
const mappingRoutePermissions = [
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { api } from '../';
|
||||||
|
import {
|
||||||
|
TArticleDetail,
|
||||||
|
TArticleListItem,
|
||||||
|
TArticleListParams,
|
||||||
|
TMentorAvailability,
|
||||||
|
TMentorDetail,
|
||||||
|
TMentorListParams,
|
||||||
|
TMentorRegisterRequest,
|
||||||
|
TSessionFeedbackRequest,
|
||||||
|
TSessionListResponse,
|
||||||
|
TBookSessionRequest,
|
||||||
|
} from '../../types/dimentorin';
|
||||||
|
import { TResponseMessage } from '../../types/common';
|
||||||
|
|
||||||
|
export const getMentors = async (
|
||||||
|
params?: TMentorListParams
|
||||||
|
): Promise<TMentorDetail[]> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/public',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorDetail = async (id: string): Promise<TMentorDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/public/${id}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorMe = async (): Promise<TMentorDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/me',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorMeStatus = async (): Promise<{ status: string }> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/mentors/me/status',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorAvailability = async (
|
||||||
|
id: string
|
||||||
|
): Promise<TMentorAvailability> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/${id}/availability`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMentorSessions = async (
|
||||||
|
id: string
|
||||||
|
): Promise<TSessionListResponse> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/mentors/${id}/sessions`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMySessions = async (): Promise<TSessionListResponse> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/sessions/me',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postBookSession = async (
|
||||||
|
mentorId: string,
|
||||||
|
payload: TBookSessionRequest
|
||||||
|
): Promise<TResponseMessage> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/dimentorin/mentors/${mentorId}/sessions/create`,
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postSessionFeedback = async (
|
||||||
|
sessionId: string,
|
||||||
|
payload: TSessionFeedbackRequest
|
||||||
|
): Promise<TResponseMessage> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: `/dimentorin/sessions/${sessionId}/feedback/create`,
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getArticles = async (
|
||||||
|
params?: TArticleListParams
|
||||||
|
): Promise<TArticleListItem[]> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/articles',
|
||||||
|
params,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getArticleCategories = async (): Promise<string[]> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: '/dimentorin/articles/categories',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getArticleBySlug = async (slug: string): Promise<TArticleDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/articles/slug/${slug}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getArticleById = async (id: string): Promise<TArticleDetail> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'GET',
|
||||||
|
url: `/dimentorin/articles/${id}`,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const postRegisterMentor = async (
|
||||||
|
payload: TMentorRegisterRequest
|
||||||
|
): Promise<TResponseMessage> => {
|
||||||
|
const { data } = await api({
|
||||||
|
method: 'POST',
|
||||||
|
url: '/dimentorin/mentors/create',
|
||||||
|
data: payload,
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
};
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import axios, { AxiosRequestConfig } from 'axios';
|
import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||||
|
import Cookies from 'js-cookie';
|
||||||
|
|
||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './dimentorin';
|
||||||
export * from './gacha';
|
export * from './gacha';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
|
|
||||||
@@ -9,3 +11,24 @@ const config: AxiosRequestConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const api = axios.create(config);
|
export const api = axios.create(config);
|
||||||
|
|
||||||
|
api.interceptors.request.use((config) => {
|
||||||
|
const raw = Cookies.get('token');
|
||||||
|
if (raw) {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
const token = parsed?.token?.access_token;
|
||||||
|
if (token) {
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore malformed token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
});
|
||||||
|
|
||||||
|
api.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
(error: AxiosError) => Promise.reject(error)
|
||||||
|
);
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
import { useMutation, useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||||
|
import {
|
||||||
|
getArticleById,
|
||||||
|
getArticleBySlug,
|
||||||
|
getArticleCategories,
|
||||||
|
getArticles,
|
||||||
|
getMentorAvailability,
|
||||||
|
getMentorDetail,
|
||||||
|
getMentorMe,
|
||||||
|
getMentorMeStatus,
|
||||||
|
getMentorSessions,
|
||||||
|
getMentors,
|
||||||
|
getMySessions,
|
||||||
|
postBookSession,
|
||||||
|
postRegisterMentor,
|
||||||
|
postSessionFeedback,
|
||||||
|
} from '../../api/dimentorin';
|
||||||
|
import {
|
||||||
|
TArticleDetail,
|
||||||
|
TArticleListItem,
|
||||||
|
TArticleListParams,
|
||||||
|
TBookSessionRequest,
|
||||||
|
TMentorAvailability,
|
||||||
|
TMentorDetail,
|
||||||
|
TMentorListParams,
|
||||||
|
TMentorRegisterRequest,
|
||||||
|
TSessionFeedbackRequest,
|
||||||
|
TSessionListResponse,
|
||||||
|
} from '../../types/dimentorin';
|
||||||
|
import { TResponseError, TResponseMessage } from '../../types/common';
|
||||||
|
|
||||||
|
export const useGetMentors = (
|
||||||
|
params?: TMentorListParams
|
||||||
|
): UseQueryResult<TMentorDetail[], TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentors', params],
|
||||||
|
queryFn: async () => await getMentors(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMentorDetail = (
|
||||||
|
id: string
|
||||||
|
): UseQueryResult<TMentorDetail, TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentor-detail', id],
|
||||||
|
queryFn: async () => await getMentorDetail(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMentorMe = (): UseQueryResult<
|
||||||
|
TMentorDetail,
|
||||||
|
TResponseError
|
||||||
|
> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentor-me'],
|
||||||
|
queryFn: async () => await getMentorMe(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMentorMeStatus = (): UseQueryResult<
|
||||||
|
{ status: string },
|
||||||
|
TResponseError
|
||||||
|
> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentor-me-status'],
|
||||||
|
queryFn: async () => await getMentorMeStatus(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMentorAvailability = (
|
||||||
|
id: string
|
||||||
|
): UseQueryResult<TMentorAvailability, TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentor-availability', id],
|
||||||
|
queryFn: async () => await getMentorAvailability(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMentorSessions = (
|
||||||
|
id: string
|
||||||
|
): UseQueryResult<TSessionListResponse, TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-mentor-sessions', id],
|
||||||
|
queryFn: async () => await getMentorSessions(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetMySessions = (): UseQueryResult<
|
||||||
|
TSessionListResponse,
|
||||||
|
TResponseError
|
||||||
|
> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-my-sessions'],
|
||||||
|
queryFn: async () => await getMySessions(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePostBookSession = (mentorId: string) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ['post-book-session', mentorId],
|
||||||
|
mutationFn: async (payload: TBookSessionRequest) =>
|
||||||
|
await postBookSession(mentorId, payload),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePostSessionFeedback = (sessionId: string) => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ['post-session-feedback', sessionId],
|
||||||
|
mutationFn: async (payload: TSessionFeedbackRequest) =>
|
||||||
|
await postSessionFeedback(sessionId, payload),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usePostRegisterMentor = () => {
|
||||||
|
return useMutation({
|
||||||
|
mutationKey: ['post-register-mentor'],
|
||||||
|
mutationFn: async (payload: TMentorRegisterRequest) =>
|
||||||
|
await postRegisterMentor(payload),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetArticles = (
|
||||||
|
params?: TArticleListParams
|
||||||
|
): UseQueryResult<TArticleListItem[], TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-articles', params],
|
||||||
|
queryFn: async () => await getArticles(params),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetArticleCategories = (): UseQueryResult<
|
||||||
|
string[],
|
||||||
|
TResponseError
|
||||||
|
> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-article-categories'],
|
||||||
|
queryFn: async () => await getArticleCategories(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetArticleBySlug = (
|
||||||
|
slug: string
|
||||||
|
): UseQueryResult<TArticleDetail, TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-article-by-slug', slug],
|
||||||
|
queryFn: async () => await getArticleBySlug(slug),
|
||||||
|
enabled: !!slug,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetArticleById = (
|
||||||
|
id: string
|
||||||
|
): UseQueryResult<TArticleDetail, TResponseError> => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['get-article-by-id', id],
|
||||||
|
queryFn: async () => await getArticleById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export type { TResponseMessage };
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './dimentorin';
|
||||||
export * from './gacha';
|
export * from './gacha';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { TResponseList } from '../common';
|
||||||
|
|
||||||
|
export type TMentorListItem = {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
fullname: string | null;
|
||||||
|
email: string | null;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMentorDetail = {
|
||||||
|
id: string;
|
||||||
|
user_id: string;
|
||||||
|
fullname: string | null;
|
||||||
|
email: string | null;
|
||||||
|
legal_name: string | null;
|
||||||
|
gender: string | null;
|
||||||
|
domicile: string | null;
|
||||||
|
bio: string | null;
|
||||||
|
last_education: string | null;
|
||||||
|
linkedin_url: string | null;
|
||||||
|
github_url: string | null;
|
||||||
|
cv_url: string | null;
|
||||||
|
portfolio_url: string | null;
|
||||||
|
phone_for_verification: string | null;
|
||||||
|
industries: string[];
|
||||||
|
expertise: string[];
|
||||||
|
languages: string[];
|
||||||
|
current_company: string;
|
||||||
|
current_role: string;
|
||||||
|
years_of_experience: number;
|
||||||
|
topics_of_interest: string[];
|
||||||
|
preferred_mentee_level: string[];
|
||||||
|
preferred_mentoring_formats: string[];
|
||||||
|
availability_commitment: string;
|
||||||
|
mentoring_rate: number;
|
||||||
|
status: string;
|
||||||
|
created_at: string;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMentorListParams = {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
search?: string;
|
||||||
|
sort_by?: string;
|
||||||
|
order?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMentorListResponse = TMentorListItem[] | TResponseList<TMentorListItem>;
|
||||||
|
|
||||||
|
export type TAvailabilitySlot = {
|
||||||
|
date: string;
|
||||||
|
time: string;
|
||||||
|
available: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMentorAvailability = {
|
||||||
|
mentor_id: string;
|
||||||
|
availability_commitment: string;
|
||||||
|
preferred_formats: string[];
|
||||||
|
slots: TAvailabilitySlot[];
|
||||||
|
booked_dates: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSessionListItem = {
|
||||||
|
id: string;
|
||||||
|
mentor_id: string;
|
||||||
|
mentee_id: string;
|
||||||
|
topic: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
duration_minutes: number;
|
||||||
|
session_type: string;
|
||||||
|
status: string;
|
||||||
|
rating: number | null;
|
||||||
|
created_at: string;
|
||||||
|
mentee_email: string | null;
|
||||||
|
mentee_fullname: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSessionListResponse = {
|
||||||
|
sessions: TSessionListItem[];
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TBookSessionRequest = {
|
||||||
|
topic: string;
|
||||||
|
scheduled_at: string;
|
||||||
|
description?: string | null;
|
||||||
|
duration_minutes?: number | null;
|
||||||
|
session_type?: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TSessionFeedbackRequest = {
|
||||||
|
feedback: string;
|
||||||
|
rating: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TArticleListItem = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
category: string;
|
||||||
|
excerpt: string;
|
||||||
|
cover_url: string | null;
|
||||||
|
author_name: string | null;
|
||||||
|
created_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TArticleDetail = TArticleListItem & {
|
||||||
|
content: string;
|
||||||
|
is_published: boolean;
|
||||||
|
updated_at: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TArticleListParams = {
|
||||||
|
page?: number;
|
||||||
|
per_page?: number;
|
||||||
|
category?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TMentorRegisterRequest = {
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
fullname: string;
|
||||||
|
phone_number?: string | null;
|
||||||
|
identity_and_verification: {
|
||||||
|
legal_name: string;
|
||||||
|
gender?: string | null;
|
||||||
|
domicile?: string | null;
|
||||||
|
identity_document_url: string;
|
||||||
|
phone_for_verification?: string | null;
|
||||||
|
};
|
||||||
|
professional_profile: {
|
||||||
|
bio: string;
|
||||||
|
last_education?: string | null;
|
||||||
|
linkedin_url?: string | null;
|
||||||
|
github_url?: string | null;
|
||||||
|
cv_url?: string | null;
|
||||||
|
portfolio_url?: string | null;
|
||||||
|
industries: string[];
|
||||||
|
expertise: string[];
|
||||||
|
languages: string[];
|
||||||
|
current_company: string;
|
||||||
|
current_role: string;
|
||||||
|
years_of_experience: number;
|
||||||
|
};
|
||||||
|
mentoring_logistics: {
|
||||||
|
topics_of_interest: string[];
|
||||||
|
preferred_mentee_level: string[];
|
||||||
|
preferred_mentoring_formats: string[];
|
||||||
|
availability_commitment: string;
|
||||||
|
mentoring_rate_amount: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './auth';
|
export * from './auth';
|
||||||
|
export * from './dimentorin';
|
||||||
export * from './gacha';
|
export * from './gacha';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
export * from './roles';
|
export * from './roles';
|
||||||
|
|||||||
@@ -2,4 +2,3 @@ export * from './button';
|
|||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './textarea';
|
export * from './textarea';
|
||||||
export * from './select'
|
export * from './select'
|
||||||
export * from './toggle';
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './toggle'
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { DetailedHTMLProps, FC, HTMLAttributes } from "react";
|
|
||||||
|
|
||||||
export type ToggleInputProps = DetailedHTMLProps<
|
|
||||||
HTMLAttributes<HTMLInputElement>,
|
|
||||||
HTMLInputElement
|
|
||||||
> & {
|
|
||||||
label?: string
|
|
||||||
labelClassName?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ToggleInput: FC<ToggleInputProps> = ({ label, className, labelClassName, ...rest }) => {
|
|
||||||
return (
|
|
||||||
<label className={cn("flex items-center gap-5 cursor-pointer mb-8", className)}>
|
|
||||||
<span className={cn("text-p3 text-gray-800 font-medium", labelClassName)}>{label}</span>
|
|
||||||
<input type="checkbox" className="sr-only peer" {...rest} />
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"w-14 h-7 bg-gray-300 rounded-3xl relative transition-colors",
|
|
||||||
"peer-checked:bg-blue-600 peer-focus:outline peer-focus:outline-blue-500 peer-checked:[&>div]:translate-x-6.5",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="absolute left-0.5 top-0.5 h-6 w-6 bg-white rounded-full shadow transition-transform peer-checked:translate-x-6.5" />
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
BookOutlined,
|
|
||||||
CommentOutlined,
|
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
ScheduleOutlined,
|
|
||||||
SettingOutlined,
|
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
@@ -15,53 +11,102 @@ import {
|
|||||||
import { Button } from '../../atoms';
|
import { Button } from '../../atoms';
|
||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { cn, For, useSession } from '@imphnen-frontend-service/utils';
|
import { useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
const MENUS = [
|
|
||||||
{ label: 'Dashboard & Set Gacha', href: '/dashboard', icon: <AppstoreOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Dashboard - Dimentorin', href: '/dashboard-dimentorin', icon: <AppstoreOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Gacha Roll', href: '/gacha-roll', icon: <ReloadOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Permissions', href: '/permissions', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Roles', href: '/roles', icon: <UsergroupAddOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Data Akun', href: '/accounts', icon: <UserOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Validasi Transaksi', href: '/transactions', icon: <AuditOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Data Pengiriman Hadiah', href: '/prizes', icon: <InboxOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'User - Dimentorin', href: '/users-dimentorin', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Session - Dimentorin', href: '/session-dimentorin', icon: <ScheduleOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Content & Roadmap', href: '/roadmap-dimentorin', icon: <BookOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Feedback & Review', href: '/feedback-review-dimentorin', icon: <CommentOutlined className="text-[20px]" /> },
|
|
||||||
{ label: 'Settings - Dimentorin', href: '/settings-dimentorin', icon: <SettingOutlined className="text-[20px]" /> },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const BackofficeSidebar: FC = (): ReactElement => {
|
export const BackofficeSidebar: FC = (): ReactElement => {
|
||||||
const { signOut } = useSession();
|
const { signOut } = useSession();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isActive = (path: string) => {
|
const isActive = (path: string) => location.pathname.includes(path);
|
||||||
if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') return false
|
|
||||||
return location.pathname.includes(path)
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sticky top-0 left-0 w-[280px] bg-white h-svh py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
<aside className="sticky top-0 left-0 w-[280px] bg-white min-h-screen py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
||||||
<div className="flex flex-col gap-20 justify-between items-center">
|
<div className="flex flex-col gap-20 justify-between items-center">
|
||||||
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
|
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
|
||||||
|
|
||||||
<nav className="flex flex-col gap-4 w-full h-[calc(100svh-20rem)] overflow-y-auto">
|
<nav className="flex flex-col gap-4 w-full">
|
||||||
<For data={MENUS}>
|
|
||||||
{({ label, href, icon }) => (
|
|
||||||
<Link
|
<Link
|
||||||
key={href}
|
to="/dashboard"
|
||||||
to={href}
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
className={cn(
|
isActive('/dashboard')
|
||||||
"flex items-center justify-items-start gap-3 px-[8px] py-[10px]",
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
isActive(href) ? "bg-primary-500 text-white rounded-md" : "text-gray-700 hover:bg-gray-100"
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
)}
|
}`}
|
||||||
>
|
>
|
||||||
{icon}
|
<AppstoreOutlined className="text-[20px]" />
|
||||||
<span className="text-p3 font-medium">{label}</span>
|
<span className="text-p3 font-medium">Dashboard & Set Gacha</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/gacha-roll"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/gacha-roll')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ReloadOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Gacha Roll</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/permissions"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/permissions')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<UserSwitchOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Permissions</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/roles"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/roles')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<UsergroupAddOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Roles</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/accounts"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/accounts')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<UserOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Data Akun</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/transactions"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/transactions')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AuditOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Validasi Transaksi</span>
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
to="/prizes"
|
||||||
|
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
||||||
|
isActive('/prizes')
|
||||||
|
? 'bg-primary-500 text-white rounded-md'
|
||||||
|
: 'text-gray-700 hover:bg-gray-100'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<InboxOutlined className="text-[20px]" />
|
||||||
|
<span className="text-p3 font-medium">Data Pengiriman Hadiah</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
import { cn } from "@imphnen-frontend-service/utils"
|
|
||||||
import { Icon } from '@iconify/react'
|
|
||||||
import { FC, ReactElement, ReactNode } from "react"
|
|
||||||
import { Button } from "../../atoms"
|
|
||||||
|
|
||||||
export type TBackofficeWrapperProps = {
|
|
||||||
children: ReactNode
|
|
||||||
title?: string
|
|
||||||
className?: string
|
|
||||||
classHeader?: string
|
|
||||||
classTitle?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
|
||||||
children,
|
|
||||||
title,
|
|
||||||
className,
|
|
||||||
classHeader,
|
|
||||||
classTitle,
|
|
||||||
}): ReactElement => {
|
|
||||||
return (
|
|
||||||
<main className={cn("w-full px-[48px] py-[40px] flex flex-col gap-8", className)}>
|
|
||||||
<header className={cn("bg-white py-5 px-7 rounded-md shadow flex items-center justify-between", classHeader)}>
|
|
||||||
<h1 className={cn("text-[19px] text-primary-500 font-semibold", classTitle)}>{title}</h1>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-x-6">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant="secondary"
|
|
||||||
className="max-h-full p-3"
|
|
||||||
>
|
|
||||||
<Icon icon="mdi:bell-outline" className="size-6" />
|
|
||||||
</Button>
|
|
||||||
<div className="flex items-center gap-x-6">
|
|
||||||
<div className="text-neutral-600 font-medium">
|
|
||||||
<p className="text-p3">Rizal Syaepulloh</p>
|
|
||||||
<p className="text-label1">Super Admin</p>
|
|
||||||
</div>
|
|
||||||
<div className="size-12 rounded-full overflow-hidden">
|
|
||||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="size-full object-cover" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<section>
|
|
||||||
{children}
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user