Compare commits
10
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b150a7a3b | ||
|
|
aedb96ad33 | ||
|
|
78e06eebb6 | ||
|
|
65d43e935c | ||
|
|
b390b63aaf | ||
|
|
7dfe69659b | ||
|
|
4518a556ad | ||
|
|
93f741815f | ||
|
|
4564c32965 | ||
|
|
f3a19fa460 |
Binary file not shown.
|
After Width: | Height: | Size: 11 KiB |
+52
@@ -0,0 +1,52 @@
|
|||||||
|
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
@@ -0,0 +1,36 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
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 lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+85
@@ -0,0 +1,85 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
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
@@ -0,0 +1,114 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
@@ -0,0 +1,118 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
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
@@ -0,0 +1,55 @@
|
|||||||
|
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
@@ -0,0 +1,86 @@
|
|||||||
|
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
@@ -0,0 +1,110 @@
|
|||||||
|
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
@@ -0,0 +1,68 @@
|
|||||||
|
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
@@ -0,0 +1,5 @@
|
|||||||
|
import { ModalProps } from "../type";
|
||||||
|
|
||||||
|
export interface ModalDetailUserProps extends ModalProps {
|
||||||
|
userId?: number | null
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
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
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ModalProps {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './textarea';
|
export * from './textarea';
|
||||||
export * from './select'
|
export * from './select'
|
||||||
|
export * from './toggle';
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './toggle'
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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,9 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
CommentOutlined,
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
@@ -11,102 +15,53 @@ 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 { useSession } from '@imphnen-frontend-service/utils';
|
import { cn, For, 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) => location.pathname.includes(path);
|
const isActive = (path: string) => {
|
||||||
|
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 min-h-screen py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
<aside className="sticky top-0 left-0 w-[280px] bg-white h-svh 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">
|
<nav className="flex flex-col gap-4 w-full h-[calc(100svh-20rem)] overflow-y-auto">
|
||||||
<Link
|
<For data={MENUS}>
|
||||||
to="/dashboard"
|
{({ label, href, icon }) => (
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
<Link
|
||||||
isActive('/dashboard')
|
key={href}
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
to={href}
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
className={cn(
|
||||||
}`}
|
"flex items-center justify-items-start gap-3 px-[8px] py-[10px]",
|
||||||
>
|
isActive(href) ? "bg-primary-500 text-white rounded-md" : "text-gray-700 hover:bg-gray-100"
|
||||||
<AppstoreOutlined className="text-[20px]" />
|
)}
|
||||||
<span className="text-p3 font-medium">Dashboard & Set Gacha</span>
|
>
|
||||||
</Link>
|
{icon}
|
||||||
|
<span className="text-p3 font-medium">{label}</span>
|
||||||
<Link
|
</Link>
|
||||||
to="/gacha-roll"
|
)}
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
</For>
|
||||||
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>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './backoffice-wrapper';
|
||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
flexRender,
|
flexRender,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
Table,
|
Table,
|
||||||
|
RowData,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { Pagination } from '../../molecules';
|
import { Pagination } from '../../molecules';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
interface DataTableProps<T> {
|
interface DataTableProps<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
@@ -18,7 +20,7 @@ interface DataTableProps<T> {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataTable = <T,>({
|
export const DataTable = <T extends RowData>({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
pageSize = 9,
|
pageSize = 9,
|
||||||
@@ -49,7 +51,7 @@ export const DataTable = <T,>({
|
|||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className="py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg"
|
className={cn("py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg", header?.column?.columnDef?.meta?.headerClassName)}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
@@ -68,7 +70,7 @@ export const DataTable = <T,>({
|
|||||||
{row.getVisibleCells().map((cell, index) => (
|
{row.getVisibleCells().map((cell, index) => (
|
||||||
<td
|
<td
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className="py-3 px-5 first:rounded-l-lg last:rounded-r-lg"
|
className={cn("py-3 px-5 first:rounded-l-lg last:rounded-r-lg", cell?.column?.columnDef?.meta?.cellClassName)}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export * from './datatable';
|
export * from './datatable';
|
||||||
|
export * from './type.d';
|
||||||
+8
@@ -0,0 +1,8 @@
|
|||||||
|
import "@tanstack/react-table"
|
||||||
|
|
||||||
|
declare module "@tanstack/react-table" {
|
||||||
|
interface ColumnMeta {
|
||||||
|
headerClassName?: string
|
||||||
|
cellClassName?: string
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,3 +5,4 @@ export * from './backoffice-sidebar';
|
|||||||
export * from './datatable';
|
export * from './datatable';
|
||||||
export * from './filter';
|
export * from './filter';
|
||||||
export * from './controlled-field';
|
export * from './controlled-field';
|
||||||
|
export * from './backoffice-wrapper';
|
||||||
|
|||||||
Generated
+358
-3
@@ -22,6 +22,7 @@
|
|||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.9.2",
|
"framer-motion": "^12.9.2",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -34,6 +35,7 @@
|
|||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"recharts": "^3.1.2",
|
||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
@@ -8070,6 +8072,32 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@reduxjs/toolkit": {
|
||||||
|
"version": "2.8.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz",
|
||||||
|
"integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@standard-schema/spec": "^1.0.0",
|
||||||
|
"@standard-schema/utils": "^0.3.0",
|
||||||
|
"immer": "^10.0.3",
|
||||||
|
"redux": "^5.0.1",
|
||||||
|
"redux-thunk": "^3.1.0",
|
||||||
|
"reselect": "^5.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17.0.0 || ^18 || ^19",
|
||||||
|
"react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@rollup/pluginutils": {
|
"node_modules/@rollup/pluginutils": {
|
||||||
"version": "5.1.4",
|
"version": "5.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz",
|
||||||
@@ -8879,6 +8907,12 @@
|
|||||||
"@sinonjs/commons": "^3.0.0"
|
"@sinonjs/commons": "^3.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@standard-schema/spec": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@standard-schema/utils": {
|
"node_modules/@standard-schema/utils": {
|
||||||
"version": "0.3.0",
|
"version": "0.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||||
@@ -10971,6 +11005,69 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/d3-array": {
|
||||||
|
"version": "3.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz",
|
||||||
|
"integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-color": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-ease": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-interpolate": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-color": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-path": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-scale": {
|
||||||
|
"version": "4.0.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||||
|
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-time": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-shape": {
|
||||||
|
"version": "3.1.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz",
|
||||||
|
"integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-path": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-time": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@types/d3-timer": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/debug": {
|
"node_modules/@types/debug": {
|
||||||
"version": "4.1.12",
|
"version": "4.1.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
|
||||||
@@ -11346,6 +11443,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/use-sync-external-store": {
|
||||||
|
"version": "0.0.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
|
||||||
|
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/uuid": {
|
"node_modules/@types/uuid": {
|
||||||
"version": "9.0.8",
|
"version": "9.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz",
|
||||||
@@ -16713,6 +16816,127 @@
|
|||||||
"which": "bin/which"
|
"which": "bin/which"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/d3-array": {
|
||||||
|
"version": "3.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz",
|
||||||
|
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"internmap": "1 - 2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-color": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-ease": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-format": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-interpolate": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-color": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-path": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-scale": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2.10.0 - 3",
|
||||||
|
"d3-format": "1 - 3",
|
||||||
|
"d3-interpolate": "1.2.0 - 3",
|
||||||
|
"d3-time": "2.1.1 - 3",
|
||||||
|
"d3-time-format": "2 - 4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-shape": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-path": "^3.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-array": "2 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-time-format": {
|
||||||
|
"version": "4.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||||
|
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"d3-time": "1 - 3"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/d3-timer": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@@ -16816,7 +17040,6 @@
|
|||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/de-indent": {
|
"node_modules/de-indent": {
|
||||||
@@ -16861,6 +17084,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/decimal.js-light": {
|
||||||
|
"version": "2.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
|
||||||
|
"integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/decompress-response": {
|
"node_modules/decompress-response": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||||
@@ -17827,6 +18056,16 @@
|
|||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/es-toolkit": {
|
||||||
|
"version": "1.39.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.39.8.tgz",
|
||||||
|
"integrity": "sha512-A8QO9TfF+rltS8BXpdu8OS+rpGgEdnRhqIVxO/ZmNvnXBYgOdSsxukT55ELyP94gZIntWJ+Li9QRrT2u1Kitpg==",
|
||||||
|
"license": "MIT",
|
||||||
|
"workspaces": [
|
||||||
|
"docs",
|
||||||
|
"benchmarks"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/es6-error": {
|
"node_modules/es6-error": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
|
||||||
@@ -20840,6 +21079,16 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/immer": {
|
||||||
|
"version": "10.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz",
|
||||||
|
"integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/immer"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/immutable": {
|
"node_modules/immutable": {
|
||||||
"version": "5.1.1",
|
"version": "5.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz",
|
||||||
@@ -20978,6 +21227,15 @@
|
|||||||
"node": ">= 0.4"
|
"node": ">= 0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/internmap": {
|
||||||
|
"version": "2.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz",
|
||||||
|
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ipaddr.js": {
|
"node_modules/ipaddr.js": {
|
||||||
"version": "1.9.1",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
|
||||||
@@ -28649,9 +28907,31 @@
|
|||||||
"version": "17.0.2",
|
"version": "17.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/react-redux": {
|
||||||
|
"version": "9.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
|
||||||
|
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/use-sync-external-store": "^0.0.6",
|
||||||
|
"use-sync-external-store": "^1.4.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "^18.2.25 || ^19",
|
||||||
|
"react": "^18.0 || ^19",
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"redux": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-refresh": {
|
"node_modules/react-refresh": {
|
||||||
"version": "0.17.0",
|
"version": "0.17.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
|
||||||
@@ -28865,6 +29145,39 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/recharts": {
|
||||||
|
"version": "3.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/recharts/-/recharts-3.1.2.tgz",
|
||||||
|
"integrity": "sha512-vhNbYwaxNbk/IATK0Ki29k3qvTkGqwvCgyQAQ9MavvvBwjvKnMTswdbklJpcOAoMPN/qxF3Lyqob0zO+ZXkZ4g==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@reduxjs/toolkit": "1.x.x || 2.x.x",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"decimal.js-light": "^2.5.1",
|
||||||
|
"es-toolkit": "^1.39.3",
|
||||||
|
"eventemitter3": "^5.0.1",
|
||||||
|
"immer": "^10.1.1",
|
||||||
|
"react-redux": "8.x.x || 9.x.x",
|
||||||
|
"reselect": "5.1.1",
|
||||||
|
"tiny-invariant": "^1.3.3",
|
||||||
|
"use-sync-external-store": "^1.2.2",
|
||||||
|
"victory-vendor": "^37.0.2"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/recharts/node_modules/eventemitter3": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/redent": {
|
"node_modules/redent": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||||
@@ -28892,6 +29205,21 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/redux": {
|
||||||
|
"version": "5.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
|
||||||
|
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/redux-thunk": {
|
||||||
|
"version": "3.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz",
|
||||||
|
"integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"redux": "^5.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/reflect.getprototypeof": {
|
"node_modules/reflect.getprototypeof": {
|
||||||
"version": "1.0.10",
|
"version": "1.0.10",
|
||||||
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
|
||||||
@@ -29094,6 +29422,12 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/reselect": {
|
||||||
|
"version": "5.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
|
||||||
|
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.8",
|
"version": "1.22.8",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||||
@@ -32068,7 +32402,6 @@
|
|||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
|
||||||
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/tinybench": {
|
"node_modules/tinybench": {
|
||||||
@@ -33841,6 +34174,28 @@
|
|||||||
"extsprintf": "^1.2.0"
|
"extsprintf": "^1.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/victory-vendor": {
|
||||||
|
"version": "37.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz",
|
||||||
|
"integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==",
|
||||||
|
"license": "MIT AND ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/d3-array": "^3.0.3",
|
||||||
|
"@types/d3-ease": "^3.0.0",
|
||||||
|
"@types/d3-interpolate": "^3.0.1",
|
||||||
|
"@types/d3-scale": "^4.0.2",
|
||||||
|
"@types/d3-shape": "^3.1.0",
|
||||||
|
"@types/d3-time": "^3.0.0",
|
||||||
|
"@types/d3-timer": "^3.0.0",
|
||||||
|
"d3-array": "^3.1.6",
|
||||||
|
"d3-ease": "^3.0.1",
|
||||||
|
"d3-interpolate": "^3.0.1",
|
||||||
|
"d3-scale": "^4.0.2",
|
||||||
|
"d3-shape": "^3.1.0",
|
||||||
|
"d3-time": "^3.0.0",
|
||||||
|
"d3-timer": "^3.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.3",
|
"version": "6.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@
|
|||||||
"axios": "^1.8.3",
|
"axios": "^1.8.3",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"framer-motion": "^12.9.2",
|
"framer-motion": "^12.9.2",
|
||||||
"graphql": "^16.11.0",
|
"graphql": "^16.11.0",
|
||||||
"js-cookie": "^3.0.5",
|
"js-cookie": "^3.0.5",
|
||||||
@@ -50,6 +51,7 @@
|
|||||||
"react-hook-form": "^7.56.4",
|
"react-hook-form": "^7.56.4",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"react-router-dom": "^7.3.0",
|
"react-router-dom": "^7.3.0",
|
||||||
|
"recharts": "^3.1.2",
|
||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
|
|||||||
Reference in New Issue
Block a user