Compare commits
32
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7df4592a47 | ||
|
|
77eb755783 | ||
|
|
8c6a85090d | ||
|
|
a6134cb32a | ||
|
|
d9c5ff0b18 | ||
|
|
924354366c | ||
|
|
dce74c5c7c | ||
|
|
d4c263cad3 | ||
|
|
e2be5105e9 | ||
|
|
eb5ff2e46c | ||
|
|
e1269f4daa | ||
|
|
487623e4fb | ||
|
|
eff44c42a1 | ||
|
|
3e232dcdc2 | ||
|
|
aa52b62ae7 | ||
|
|
3dec77bd0a | ||
|
|
ec81922001 | ||
|
|
d7b70b21d1 | ||
|
|
a7cf85dd66 | ||
|
|
bb22068856 | ||
|
|
209fa3fff1 | ||
|
|
935d23887c | ||
|
|
363f1582c9 | ||
|
|
1a9b47953a | ||
|
|
182ff7facc | ||
|
|
af8968ac01 | ||
|
|
4bd66af0ff | ||
|
|
73826fb4c3 | ||
|
|
2f9407e258 | ||
|
|
534bf0e10a | ||
|
|
df479a0a8d | ||
|
|
1948d2c67f |
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(npm run typecheck:*)"
|
|
||||||
],
|
|
||||||
"deny": [],
|
|
||||||
"ask": []
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
-52
@@ -1,52 +0,0 @@
|
|||||||
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
|
||||||
import { PieLabelProps } from "recharts/types/polar/Pie"
|
|
||||||
|
|
||||||
type ChartProps = {
|
|
||||||
name: string
|
|
||||||
value: number
|
|
||||||
color: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData: ChartProps[] = [
|
|
||||||
{ name: "Active", value: 49, color: "#23A1EB" },
|
|
||||||
{ name: "Done", value: 24, color: "#81CBF8" },
|
|
||||||
{ name: "Canceled", value: 27, color: "#BCE1FB" },
|
|
||||||
]
|
|
||||||
|
|
||||||
const RADIAN = Math.PI / 180;
|
|
||||||
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => {
|
|
||||||
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
|
||||||
const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN);
|
|
||||||
const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
|
||||||
{`${((percent ?? 1) * 100).toFixed(0)}%`}
|
|
||||||
</text>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const SessionStatusChart = () => {
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
|
||||||
<PieChart width={500} height={320}>
|
|
||||||
<Pie
|
|
||||||
data={chartData}
|
|
||||||
dataKey="value"
|
|
||||||
nameKey="name"
|
|
||||||
cx="50%"
|
|
||||||
cy="50%"
|
|
||||||
innerRadius={40}
|
|
||||||
outerRadius={100}
|
|
||||||
labelLine={false}
|
|
||||||
label={renderCustomizedLabel}
|
|
||||||
>
|
|
||||||
{chartData.map((entry, index) => (
|
|
||||||
<Cell key={`cell-${index}`} fill={entry.color} />
|
|
||||||
))}
|
|
||||||
</Pie>
|
|
||||||
<Tooltip />
|
|
||||||
</PieChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
|
||||||
|
|
||||||
type ChartProps = {
|
|
||||||
name: string
|
|
||||||
activeUser: number
|
|
||||||
activeSession: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const chartData: ChartProps[] = [
|
|
||||||
{ name: "2014", activeUser: 0, activeSession: 0 },
|
|
||||||
{ name: "2015", activeUser: 15, activeSession: 25 },
|
|
||||||
{ name: "2016", activeUser: 30, activeSession: 40 },
|
|
||||||
{ name: "2017", activeUser: 45, activeSession: 55 },
|
|
||||||
{ name: "2018", activeUser: 60, activeSession: 70 },
|
|
||||||
{ name: "2019", activeUser: 75, activeSession: 85 },
|
|
||||||
{ name: "2020", activeUser: 90, activeSession: 95 },
|
|
||||||
{ name: "2021", activeUser: 85, activeSession: 80 },
|
|
||||||
{ name: "2022", activeUser: 95, activeSession: 90 },
|
|
||||||
{ name: "2023", activeUser: 100, activeSession: 100 },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const UserGrowthChart = () => {
|
|
||||||
return (
|
|
||||||
<ResponsiveContainer width="100%" height={320}>
|
|
||||||
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
|
|
||||||
<CartesianGrid />
|
|
||||||
<XAxis dataKey="name" />
|
|
||||||
<YAxis tickCount={10} />
|
|
||||||
<Tooltip />
|
|
||||||
<Legend />
|
|
||||||
<Line dataKey="activeUser" stroke="#23A1EB" />
|
|
||||||
<Line dataKey="activeSession" stroke="#0877C1" />
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ReactElement } from "react";
|
|
||||||
import { UserGrowthChart } from "./_components/chart/user-growth";
|
|
||||||
import { SessionStatusChart } from "./_components/chart/session-status";
|
|
||||||
|
|
||||||
const Overview = () => {
|
|
||||||
return (
|
|
||||||
<div className="bg-white px-6 py-4 rounded-md shadow">
|
|
||||||
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
|
||||||
<p className="text-neutral-400 text-p3">Total Users</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
|
||||||
|
|
||||||
<div className="space-y-14">
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Overview
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-5">
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => <Overview key={index} />}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
|
||||||
Trends & Analytics
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-7 gap-x-5">
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
|
|
||||||
<div className="flex items-center justify-between mb-7">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
|
|
||||||
<div></div>
|
|
||||||
</div>
|
|
||||||
<UserGrowthChart />
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
|
|
||||||
<SessionStatusChart />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-5">
|
|
||||||
<div className="bg-white px-7 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left rounded-full">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">4.9</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white px-6 py-4 rounded-lg">
|
|
||||||
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<table className="w-full">
|
|
||||||
<thead>
|
|
||||||
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
|
||||||
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
|
||||||
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
|
||||||
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
|
|
||||||
<tbody>
|
|
||||||
<For data={Array.from({ length: 5 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<tr key={index} className="shadow rounded-lg">
|
|
||||||
<td className="py-4 px-5">{index + 1}</td>
|
|
||||||
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
|
||||||
<td className="py-4 px-5">1000</td>
|
|
||||||
</tr>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
MENTORING: 'Mentoring',
|
|
||||||
PLATFORM: 'Platform'
|
|
||||||
} as const
|
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
type FeedbackStatus = 'done' | 'todo';
|
|
||||||
|
|
||||||
interface FeedbackType {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
rating: number
|
|
||||||
status: FeedbackStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
rating: 4.5,
|
|
||||||
status: i % 2 === 0 ? 'done' : 'todo',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<FeedbackType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rating',
|
|
||||||
header: 'Rating',
|
|
||||||
accessorKey: 'rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'bg-success-200 text-success-500',
|
|
||||||
todo: 'bg-primary-200 text-primary-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<FeedbackStatus, string> = {
|
|
||||||
done: 'Done',
|
|
||||||
todo: 'To Do',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Lihat Feedback
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<div className="mb-8 flex justify-between items-center">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
|
||||||
<For data={Object.values(TABS)}>
|
|
||||||
{(tab) => (
|
|
||||||
<Button
|
|
||||||
key={tab}
|
|
||||||
variant="text"
|
|
||||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama mentor/mentee"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
|
|||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
<BackofficeSidebar />
|
<BackofficeSidebar />
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
-85
@@ -1,85 +0,0 @@
|
|||||||
import { ArrowRightOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
|
||||||
|
|
||||||
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={isOpen}
|
|
||||||
onClose={onClose}
|
|
||||||
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<div className="overflow-y-auto max-h-[80vh]">
|
|
||||||
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
|
||||||
Create Roadmaps
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-5 gap-6 mb-12">
|
|
||||||
<div className="col-span-3">
|
|
||||||
<label className={labelClass}>Prompt</label>
|
|
||||||
<Textarea
|
|
||||||
className="min-w-full w-full h-[calc(100%-2rem)]"
|
|
||||||
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="col-span-2 space-y-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Roadmap Name</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Roadmap Name</label>
|
|
||||||
<Select className="w-full">
|
|
||||||
<option value="pemula">Pemula</option>
|
|
||||||
<option value="menengah">Menengah</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Model</label>
|
|
||||||
<Select className="w-full">
|
|
||||||
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
|
|
||||||
<option value="gpt-4">GPT 4</option>
|
|
||||||
<option value="gpt-4-32k">GPT 4 32k</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-full">
|
|
||||||
<Button>
|
|
||||||
Generate Roadmap
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
|
|
||||||
Roadmap Preview
|
|
||||||
</h2>
|
|
||||||
<div className="space-y-2.5">
|
|
||||||
<For data={Array.from({ length: 3 })}>
|
|
||||||
{(_, index) => (
|
|
||||||
<Accordion
|
|
||||||
key={index}
|
|
||||||
title={`Day ${index + 1}`}
|
|
||||||
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end mt-12">
|
|
||||||
<Button type="button" className="flex items-center gap-5">
|
|
||||||
Submit Roadmap
|
|
||||||
<ArrowRightOutlined />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
|
|
||||||
|
|
||||||
type LearningStatus = 'active' | 'inactive'
|
|
||||||
|
|
||||||
type RoadmapType = {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
learningLevel: string
|
|
||||||
status: LearningStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
|
|
||||||
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
|
|
||||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): React.ReactElement {
|
|
||||||
const [openCreateModal, setOpenCreateModal] = useState(false)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<RoadmapType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'id',
|
|
||||||
header: 'No',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Nama Roadmap',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'learningLevel',
|
|
||||||
header: 'Tingkat Belajar',
|
|
||||||
accessorKey: 'learningLevel',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<LearningStatus, string> = {
|
|
||||||
inactive: 'bg-danger-200 text-danger-700',
|
|
||||||
active: 'bg-success-200 text-success-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<LearningStatus, string> = {
|
|
||||||
inactive: 'Inactive',
|
|
||||||
active: 'Active',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex gap-[8px]">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<EditOutlined /> Action
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
>
|
|
||||||
<DeleteOutlined /> Delete
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex items-center justify-between mb-9">
|
|
||||||
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
|
|
||||||
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
|
|
||||||
<PlusOutlined /> Buat Roadmap
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama roadmap"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Tingkat Belajar</option>
|
|
||||||
<option value="pemula">Pemula</option>
|
|
||||||
<option value="menengah">Menengah</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-114
@@ -1,114 +0,0 @@
|
|||||||
import { Icon } from "@iconify/react";
|
|
||||||
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
const TOPICS = [
|
|
||||||
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
|
||||||
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
|
||||||
|
|
||||||
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
|
||||||
|
|
||||||
Saya ingin tahu secara khusus tentang ___.
|
|
||||||
1.Pertanyaan Anda
|
|
||||||
2. ...
|
|
||||||
3. ...`
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
|
||||||
|
|
||||||
type ModalProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalDetailSession: FC<ModalProps> = ({ open, setOpen }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
|
||||||
Detail Sesi
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
|
|
||||||
<div className="col-span-4 flex items-center gap-x-12">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h2>
|
|
||||||
<div>
|
|
||||||
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
|
||||||
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Icon icon="ph:arrow-right" className="text-9xl text-primary-500" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="col-span-5 flex items-center gap-12">
|
|
||||||
<div>
|
|
||||||
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h2>
|
|
||||||
<div>
|
|
||||||
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
|
||||||
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<h2 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h2>
|
|
||||||
<div className="py-2 px-6 rounded-md text-center bg-success-200 text-success-500 font-semibold">
|
|
||||||
Finished
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p3 font-medium mb-2.5">Topics</h1>
|
|
||||||
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
|
|
||||||
<For data={TOPICS}>
|
|
||||||
{(item, index) => (
|
|
||||||
<div
|
|
||||||
key={index}
|
|
||||||
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
|
|
||||||
>
|
|
||||||
<span>{item.icon} </span>
|
|
||||||
<span>{item.name}</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Tanggal</label>
|
|
||||||
<Input type="date" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Waktu</label>
|
|
||||||
<Input type="time" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div className="relative md:col-span-full">
|
|
||||||
<label className={labelClass}>Lokasi</label>
|
|
||||||
<Select className="min-w-full w-full">
|
|
||||||
<option value="online">Online</option>
|
|
||||||
<option value="offline">Offline</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div className="md:col-span-full">
|
|
||||||
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
|
||||||
<Textarea
|
|
||||||
className="min-w-full w-full h-40"
|
|
||||||
placeholder={placeholder}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react";
|
|
||||||
import { ModalDetailSession } from "./_components/modal/detail";
|
|
||||||
|
|
||||||
type SessionStatus = 'ongoing' | 'finished';
|
|
||||||
|
|
||||||
interface SessionType {
|
|
||||||
id: string
|
|
||||||
mentorName: string
|
|
||||||
menteeName: string
|
|
||||||
datetime: number
|
|
||||||
status: SessionStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: `DS-${i + 1}`,
|
|
||||||
mentorName: 'Ahmad Wijuana',
|
|
||||||
menteeName: 'Sofia Wijuana',
|
|
||||||
datetime: new Date().getTime(),
|
|
||||||
status: i % 2 === 0 ? 'ongoing' : 'finished',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const [openDetail, setOpenDetail] = useState(false);
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<SessionType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'id',
|
|
||||||
header: 'ID Sesi',
|
|
||||||
accessorKey: 'id',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'mentorName',
|
|
||||||
header: 'Nama Mentor',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menteeName',
|
|
||||||
header: 'Nama Mentee',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'datetime',
|
|
||||||
header: 'Waktu',
|
|
||||||
accessorKey: 'datetime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<SessionStatus, string> = {
|
|
||||||
ongoing: 'bg-warning-200 text-warning-700',
|
|
||||||
finished: 'bg-success-200 text-success-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<SessionStatus, string> = {
|
|
||||||
ongoing: 'On Going',
|
|
||||||
finished: 'Finished',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-52") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setOpenDetail(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Cek Detail
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Session Management</h1>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="finished">Finished</option>
|
|
||||||
<option value="ongoing">On Going</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils"
|
|
||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
|
||||||
|
|
||||||
export const GeneralSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<ToggleInput label="Mode Maintenance" />
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Platform</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Bahasa</label>
|
|
||||||
<Select defaultValue="id" className="w-full">
|
|
||||||
<option value="id">Indonesia</option>
|
|
||||||
<option value="en">English</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Logo Platform</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Favicon</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
|
|
||||||
<div className="grid gap-x-8 gap-y-5 mb-20">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>URL Syarat & Ketentuan</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>URL Kebijakan Privasi</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
export const NotificationSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-8 flex-wrap">
|
|
||||||
<ToggleInput label="Nyalakan Notifikasi Email" />
|
|
||||||
<ToggleInput label="Beritahu mentor ketika ada request" />
|
|
||||||
<ToggleInput label="Beritahu mentee untuk update sesi " />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="mb-20">
|
|
||||||
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
|
|
||||||
API Integrasi Notifikasi (URL)
|
|
||||||
</label>
|
|
||||||
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils"
|
|
||||||
import { FC } from "react"
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
|
||||||
|
|
||||||
export const PaymentSettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Integrasi Payment Gateway</label>
|
|
||||||
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Mata Uang</label>
|
|
||||||
<Select defaultValue="idr">
|
|
||||||
<option value="idr">Rupiah (IDR)</option>
|
|
||||||
<option value="usd">Dollar (USD)</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Tarif Komisi untuk Platform</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
|
|
||||||
<div className="mb-32">
|
|
||||||
<label className={labelClass}>Masukkan Format Invoice</label>
|
|
||||||
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
|
||||||
|
|
||||||
export const SecuritySettings: FC = () => {
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-8 mb-20">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Durasi Session Timeout</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
|
|
||||||
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
|
|
||||||
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-5">
|
|
||||||
<Button type="button" variant="bordered">
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button">
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-118
@@ -1,118 +0,0 @@
|
|||||||
import { DeleteOutlined, UserSwitchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
|
|
||||||
type UserRolesPermissionType = {
|
|
||||||
id: number
|
|
||||||
role: string
|
|
||||||
totalUser: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
|
|
||||||
totalUser: 10
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const UserRolesPermission: FC = () => {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<UserRolesPermissionType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'role',
|
|
||||||
header: 'Role',
|
|
||||||
accessorKey: 'role',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'totalUser',
|
|
||||||
header: 'Total User',
|
|
||||||
accessorKey: 'totalUser',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-96") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<UserSwitchOutlined className="text-[16px]" /> Manage Permissions
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<DeleteOutlined className="text-[16px]" /> Delete Role
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="mb-8 flex items-center justify-between">
|
|
||||||
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
|
||||||
<Button type="button">
|
|
||||||
Add Rols
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="bg-white shadow p-8 rounded-lg">
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
|
||||||
import { useState } from "react"
|
|
||||||
import { GeneralSettings } from "./_components/general"
|
|
||||||
import { UserRolesPermission } from "./_components/user-roles-permission"
|
|
||||||
import { NotificationSettings } from "./_components/notification"
|
|
||||||
import { SecuritySettings } from "./_components/security"
|
|
||||||
import { PaymentSettings } from "./_components/payment"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
general: "General Settings",
|
|
||||||
userRolePermissions: "User Roles & Permissions",
|
|
||||||
notification: "Notification Settings",
|
|
||||||
security: "Security",
|
|
||||||
payment: "Payment",
|
|
||||||
} as const
|
|
||||||
type Tabs = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
export default function Components(): React.ReactElement {
|
|
||||||
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Settings</h1>
|
|
||||||
|
|
||||||
<div className="flex items-start gap-x-8">
|
|
||||||
<div className="w-64 bg-white p-2.5 shadow space-y-2 rounded-md">
|
|
||||||
<For data={Object.values(TABS)}>
|
|
||||||
{(tab) => (
|
|
||||||
<button
|
|
||||||
key={tab}
|
|
||||||
className={cn(
|
|
||||||
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100",
|
|
||||||
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
|
|
||||||
)}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
<div className="bg-white px-8 py-6 shadow space-y-2 rounded-md flex-1">
|
|
||||||
{activeTab === TABS.general && <GeneralSettings />}
|
|
||||||
{activeTab === TABS.userRolePermissions && <UserRolesPermission />}
|
|
||||||
{activeTab === TABS.notification && <NotificationSettings />}
|
|
||||||
{activeTab === TABS.security && <SecuritySettings />}
|
|
||||||
{activeTab === TABS.payment && <PaymentSettings />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { FC } from "react";
|
|
||||||
|
|
||||||
type ModalDeleteProps = {
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
hadnleDelete?: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
|
||||||
isOpen={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<Modal.Header className="gap-8">
|
|
||||||
<img
|
|
||||||
src="/chibi-delete.webp"
|
|
||||||
alt="Delete item?"
|
|
||||||
width={148}
|
|
||||||
className="self-center"
|
|
||||||
/>
|
|
||||||
<div className="text-center">
|
|
||||||
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
|
||||||
Hapus Akun
|
|
||||||
</h2>
|
|
||||||
<p className="text-p3 text-neutral-400">
|
|
||||||
Apakah kamu yakin untuk menghapus akun ini?
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content className="flex gap-4">
|
|
||||||
<Button
|
|
||||||
variant="bordered"
|
|
||||||
size="lg"
|
|
||||||
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
size="lg"
|
|
||||||
className="w-full"
|
|
||||||
onClick={() => hadnleDelete?.()}
|
|
||||||
>
|
|
||||||
Hapus Akun
|
|
||||||
</Button>
|
|
||||||
</Modal.Content>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-86
@@ -1,86 +0,0 @@
|
|||||||
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { FC, useState } from "react";
|
|
||||||
import { ModalDelete } from "../delete";
|
|
||||||
import { ModalProps } from "../type";
|
|
||||||
import { ModalSuspendOrBan } from "../suspend-or-ban";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
|
||||||
|
|
||||||
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
|
|
||||||
const [openDelete, setOpenDelete] = useState(false)
|
|
||||||
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex items-center gap-x-8 mb-8">
|
|
||||||
<div className="size-[100px] rounded-full overflow-hidden">
|
|
||||||
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Button type="button" size="sm" variant="bordered" className="mb-4">
|
|
||||||
Upload Foto
|
|
||||||
</Button>
|
|
||||||
<p className="text-neutral-400 font-medium">
|
|
||||||
Setidaknya rekomendasi ukuran 240x240 px. <br />
|
|
||||||
.jpg, .jpeg, .png diperbolehkan
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<div className="mb-8">
|
|
||||||
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
|
|
||||||
<div className="grid grid-cols-2 gap-8">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Depan</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nama Belakang</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Email</label>
|
|
||||||
<Input type="email" className="min-w-full w-full" placeholder="Email" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Nomor Telepon</label>
|
|
||||||
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
|
|
||||||
<div className="flex items-center gap-5">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="bordered"
|
|
||||||
className="border-danger-500 text-danger-500"
|
|
||||||
onClick={() => setOpenSuspendOrBan(true)}
|
|
||||||
>
|
|
||||||
Suspend/Ban
|
|
||||||
</Button>
|
|
||||||
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
|
|
||||||
Delete Akun
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-end gap-x-5">
|
|
||||||
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
|
|
||||||
Batal
|
|
||||||
</Button>
|
|
||||||
<Button type="button" disabled>
|
|
||||||
Simpan
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
|
|
||||||
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-110
@@ -1,110 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons"
|
|
||||||
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils"
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
|
|
||||||
import { FC, useState } from "react"
|
|
||||||
import dayjs from "dayjs"
|
|
||||||
|
|
||||||
interface ActivityLogType {
|
|
||||||
id: number
|
|
||||||
date: string
|
|
||||||
menu: string
|
|
||||||
activity: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
date: new Date().toISOString(),
|
|
||||||
menu: 'Mentoring',
|
|
||||||
activity: 'Mentoring Session',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export const ActivityLog: FC = () => {
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
})
|
|
||||||
|
|
||||||
const columns: ColumnDef<ActivityLogType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-16") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'date',
|
|
||||||
header: 'Waktu',
|
|
||||||
accessorKey: 'date',
|
|
||||||
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'menu',
|
|
||||||
header: 'Menu',
|
|
||||||
accessorKey: 'menu',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'activity',
|
|
||||||
header: 'Activity',
|
|
||||||
accessorKey: 'activity',
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-8 shadow rounded-lg">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-9">
|
|
||||||
<div className="relative w-1/2">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari aktivitas"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="w-1/4">
|
|
||||||
<Input type="date" className="min-w-full w-full" />
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Menu</option>
|
|
||||||
<option value="profle">Profile</option>
|
|
||||||
<option value="profle-2">Profile</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-89
File diff suppressed because one or more lines are too long
-68
@@ -1,68 +0,0 @@
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
|
||||||
import { Modal } from "@imphnen-frontend-service/ui/molecules"
|
|
||||||
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
|
||||||
import { FC, useEffect, useState } from "react"
|
|
||||||
import { AccountProfile } from "./account-profile"
|
|
||||||
import { DetailProfile } from "./detail-profile"
|
|
||||||
import { ActivityLog } from "./activity-log"
|
|
||||||
import { ModalDetailUserProps } from "./type"
|
|
||||||
|
|
||||||
const TABS = {
|
|
||||||
account: 'account profile',
|
|
||||||
detail: 'detail profile',
|
|
||||||
activity: 'activity logs',
|
|
||||||
} as const
|
|
||||||
type TabType = typeof TABS[keyof typeof TABS]
|
|
||||||
|
|
||||||
export const ModalDetailUser: FC<ModalDetailUserProps> = ({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}) => {
|
|
||||||
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) setActiveTab(TABS.account)
|
|
||||||
}, [open])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
className="xl:max-w-[84rem] bg-white px-10 py-9"
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
|
||||||
Detail - Muhammad Firdaus Oiwobo
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
|
|
||||||
<For data={Object.values(TABS)}>
|
|
||||||
{(tab) => (
|
|
||||||
<Button
|
|
||||||
key={tab}
|
|
||||||
variant="text"
|
|
||||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<Show condition={activeTab === TABS.account}>
|
|
||||||
<AccountProfile open={open} setOpen={setOpen} />
|
|
||||||
</Show>
|
|
||||||
<Show condition={activeTab === TABS.detail}>
|
|
||||||
<DetailProfile />
|
|
||||||
</Show>
|
|
||||||
<Show condition={activeTab === TABS.activity}>
|
|
||||||
<ActivityLog />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-5
@@ -1,5 +0,0 @@
|
|||||||
import { ModalProps } from "../type";
|
|
||||||
|
|
||||||
export interface ModalDetailUserProps extends ModalProps {
|
|
||||||
userId?: number | null
|
|
||||||
}
|
|
||||||
-42
@@ -1,42 +0,0 @@
|
|||||||
import { FC } from "react";
|
|
||||||
import { ModalProps } from "../type";
|
|
||||||
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
|
||||||
import { cn } from "@imphnen-frontend-service/utils";
|
|
||||||
import { Button, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
|
|
||||||
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
|
||||||
|
|
||||||
export const ModalSuspendOrBan: FC<ModalProps> = ({ open, setOpen }) => {
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
isOpen={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
className="bg-white px-10 py-9 xl:max-w-[32rem]"
|
|
||||||
closeButtonClassName="hidden"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
|
||||||
Suspend/Ban
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Suspend/Ban</label>
|
|
||||||
<Select defaultValue="suspend" className="w-full">
|
|
||||||
<option value="suspend">Suspend</option>
|
|
||||||
<option value="banned">Banned</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label className={labelClass}>Alasan</label>
|
|
||||||
<Textarea placeholder="Masukkan alasan suspend/ban" className="w-full h-40" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button type="button" className="w-full">
|
|
||||||
Selesai
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
-4
@@ -1,4 +0,0 @@
|
|||||||
export interface ModalProps {
|
|
||||||
open: boolean
|
|
||||||
setOpen: (open: boolean) => void
|
|
||||||
}
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { SearchOutlined } from "@ant-design/icons";
|
|
||||||
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
|
||||||
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils";
|
|
||||||
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
|
||||||
import { ReactElement, useState } from "react";
|
|
||||||
import { ModalDetailUser } from "./_components/modal/detail";
|
|
||||||
|
|
||||||
type UserStatus = 'active' | 'inactive';
|
|
||||||
|
|
||||||
interface UserType {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
email: string
|
|
||||||
rating: number
|
|
||||||
status: UserStatus
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
|
|
||||||
id: i + 1,
|
|
||||||
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
|
||||||
email: 'fullname23@gmail.com',
|
|
||||||
rating: 4.5,
|
|
||||||
status: i % 2 === 0 ? 'active' : 'inactive',
|
|
||||||
}))
|
|
||||||
|
|
||||||
export default function Components(): ReactElement {
|
|
||||||
const TABS = ['mentor', 'mentee'] as const
|
|
||||||
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
|
|
||||||
const [showDetail, setShowDetail] = useState(false)
|
|
||||||
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
|
|
||||||
|
|
||||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
|
||||||
pageIndex: 0,
|
|
||||||
pageSize: 9,
|
|
||||||
});
|
|
||||||
|
|
||||||
const columns: ColumnDef<UserType>[] = [
|
|
||||||
{
|
|
||||||
id: 'select',
|
|
||||||
meta: { cellClassName: cn("w-20") },
|
|
||||||
header: ({ table }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={table.getIsAllRowsSelected()}
|
|
||||||
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
className="rounded"
|
|
||||||
checked={row.getIsSelected()}
|
|
||||||
onChange={row.getToggleSelectedHandler()}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'name',
|
|
||||||
header: 'Name',
|
|
||||||
accessorKey: 'name',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
header: 'Email',
|
|
||||||
accessorKey: 'email',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'rating',
|
|
||||||
header: 'Rating',
|
|
||||||
accessorKey: 'rating',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'status',
|
|
||||||
header: 'Status',
|
|
||||||
accessorKey: 'status',
|
|
||||||
cell: ({ row }) => {
|
|
||||||
const status = row.original.status;
|
|
||||||
const statusColors: Record<UserStatus, string> = {
|
|
||||||
active: 'bg-success-200 text-success-500',
|
|
||||||
inactive: 'bg-danger-200 text-danger-500',
|
|
||||||
};
|
|
||||||
const statusText: Record<UserStatus, string> = {
|
|
||||||
active: 'Active',
|
|
||||||
inactive: 'Inactive',
|
|
||||||
};
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
|
||||||
>
|
|
||||||
{statusText[status]}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
header: 'Action',
|
|
||||||
meta: { cellClassName: cn("w-72") },
|
|
||||||
cell: ({ row }) => (
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
setSelectedUserId(row.original.id);
|
|
||||||
setShowDetail(true);
|
|
||||||
}}
|
|
||||||
className="flex items-center gap-2 w-max"
|
|
||||||
>
|
|
||||||
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
|
|
||||||
</Button>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const table = useReactTable({
|
|
||||||
data: mockData,
|
|
||||||
columns,
|
|
||||||
state: {
|
|
||||||
pagination,
|
|
||||||
rowSelection,
|
|
||||||
},
|
|
||||||
enableRowSelection: true,
|
|
||||||
onRowSelectionChange: setRowSelection,
|
|
||||||
getCoreRowModel: getCoreRowModel(),
|
|
||||||
getPaginationRowModel: getPaginationRowModel(),
|
|
||||||
onPaginationChange: setPagination,
|
|
||||||
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
|
||||||
manualPagination: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<BackofficeWrapper title="Dimentorin.dev">
|
|
||||||
<div className="mb-8 flex justify-between items-center">
|
|
||||||
<h1 className="text-p1 font-semibold text-neutral-700">User Management</h1>
|
|
||||||
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
|
||||||
<For data={TABS}>
|
|
||||||
{(tab) => (
|
|
||||||
<Button
|
|
||||||
key={tab}
|
|
||||||
variant="text"
|
|
||||||
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
|
||||||
onClick={() => setActiveTab(tab)}
|
|
||||||
>
|
|
||||||
{tab}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</For>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
|
||||||
<div className="flex justify-between items-center gap-5 mb-2">
|
|
||||||
<div className="relative w-full">
|
|
||||||
<Input
|
|
||||||
placeholder="Cari berdasarkan nama lengkap"
|
|
||||||
className="pl-12 w-full max-h-full"
|
|
||||||
/>
|
|
||||||
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
|
||||||
<SearchOutlined />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Rating</option>
|
|
||||||
<option value="4.5">4.5</option>
|
|
||||||
<option value="5">5</option>
|
|
||||||
</Select>
|
|
||||||
<Select>
|
|
||||||
<option selected disabled>Status</option>
|
|
||||||
<option value="active">Active</option>
|
|
||||||
<option value="inactive">Inactive</option>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DataTable data={mockData} columns={columns} table={table} />
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<ModalDetailUser
|
|
||||||
open={showDetail}
|
|
||||||
setOpen={setShowDetail}
|
|
||||||
userId={selectedUserId}
|
|
||||||
/>
|
|
||||||
</BackofficeWrapper>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.8 KiB |
@@ -1,12 +1,20 @@
|
|||||||
import { StarFilled } from "@ant-design/icons"
|
import { StarFilled } from "@ant-design/icons"
|
||||||
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
import { cn, For } from "@imphnen-frontend-service/utils"
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { TMentorDetail } from "@imphnen-frontend-service/service"
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onBook: () => void
|
onBook: () => void
|
||||||
|
mentor?: TMentorDetail
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
export const ProfileSection: React.FC<Props> = ({ onBook, mentor }) => {
|
||||||
|
const name = mentor?.fullname || mentor?.legal_name || "Mentor"
|
||||||
|
const role = mentor?.current_role || "Mentor"
|
||||||
|
const company = mentor?.current_company || "IMPHNEN"
|
||||||
|
const expertise = mentor?.expertise ?? []
|
||||||
|
const softSkills = mentor?.languages ?? []
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
<div className="bg-white px-4 py-5 space-y-6 md:px-8 md:pt-6 md:pb-0 xl:space-y-0 xl:py-[30px] xl:flex xl:gap-x-9 xl:justify-between">
|
||||||
<div
|
<div
|
||||||
@@ -33,10 +41,10 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
{name}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
<p className="text-xs mb-4 text-neutral-600 md:mb-5 md:text-[15px] xl:text-[19px] xl:mb-5">
|
||||||
UI Designer at Oray orayan Studios
|
{role} at {company}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
<div className="flex items-center gap-x-2.5 w-full max-w-max border border-primary-50 p-1.5 rounded-md mx-auto md:ms-0">
|
||||||
<div
|
<div
|
||||||
@@ -56,7 +64,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Expertise</h2>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
<For data={['UI Design', 'UX Reseacrh']}>
|
<For data={expertise}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
{item}
|
{item}
|
||||||
@@ -68,7 +76,7 @@ export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
|||||||
<div>
|
<div>
|
||||||
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Soft Skills</h2>
|
||||||
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-3">
|
||||||
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
|
<For data={softSkills}>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
<div key={item} className="bg-primary-300 text-primary-600 px-3 py-2 rounded-md text-[10px] md:font-medium xl:text-xs xl:font-semibold">
|
||||||
{item}
|
{item}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,40 +1,70 @@
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { For } from '@imphnen-frontend-service/utils';
|
||||||
|
import { TMentorDetail } from '@imphnen-frontend-service/service';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
export const MentorCard: FC = () => {
|
type TMentorCardProps = {
|
||||||
|
mentor: TMentorDetail;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MentorCard: FC<TMentorCardProps> = ({ mentor }) => {
|
||||||
|
const experience =
|
||||||
|
mentor.years_of_experience > 0
|
||||||
|
? `${mentor.years_of_experience} Years Experience`
|
||||||
|
: 'New Mentor';
|
||||||
|
const role = mentor.current_role || 'Mentor';
|
||||||
|
const company = mentor.current_company || 'IMPHNEN';
|
||||||
|
const softSkills = mentor.expertise || [];
|
||||||
|
const shownSkills = softSkills.slice(0, 2);
|
||||||
|
const extraCount = Math.max(0, softSkills.length - shownSkills.length);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
<div className="p-2.5 rounded-md bg-white shadow flex gap-x-4 items-start md:p-4 md:flex-col md:rounded-lg md:gap-y-4">
|
||||||
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
<div className="size-[60px] rounded-md overflow-hidden md:w-full md:h-auto md:aspect-square">
|
||||||
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
<img src="/image/testimonial.webp" alt={mentor.fullname ?? 'Mentor'} className="w-full object-cover" />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
<Button variant="text" size="sm" className="bg-primary-100 h-auto px-1.5 py-1 text-[8px] font-normal mb-2 hover:bg-primary-100 md:text-[10px] md:font-medium md:mb-4">
|
||||||
3-5 Years Experience
|
{experience}
|
||||||
</Button>
|
</Button>
|
||||||
<h2 className="mb-1">
|
<h2 className="mb-1">
|
||||||
<Link to="/mentoring/detail" className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
<Link to={`/mentoring/${mentor.id}`} className="text-xs font-semibold text-primary-500 md:text-[15px] md:font-semibold lg:text-[19px]">
|
||||||
Fullname
|
{mentor.fullname || mentor.legal_name || 'Mentor'}
|
||||||
</Link>
|
</Link>
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
<p className="text-[8px] text-neutral-500 mb-3 md:text-[10px] md:font-medium md:mb-4 lg:text-xs">
|
||||||
Full Stack Enjoyer at name company
|
{role} at {company}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{softSkills.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||||
<div className="space-x-2">
|
<div className="space-x-2">
|
||||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]">
|
<For data={shownSkills}>
|
||||||
Communication
|
{(skill) => (
|
||||||
|
<Button
|
||||||
|
key={skill}
|
||||||
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hover:bg-primary-200 lg:text-[10px]"
|
||||||
|
>
|
||||||
|
{skill}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="text" size="sm" className="bg-primary-200 h-auto px-1.5 py-1 text-[8px] font-normal hidden hover:bg-primary-200 md:inline-block lg:text-[10px]">
|
)}
|
||||||
Communication
|
</For>
|
||||||
</Button>
|
{extraCount > 0 && (
|
||||||
<Button variant="text" size="sm" className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]">
|
<Button
|
||||||
+2
|
variant="text"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto px-1.5 py-1 text-[8px] font-normal text-neutral-500 hover:bg-transparent lg:text-[10px]"
|
||||||
|
>
|
||||||
|
+{extraCount}
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -3,27 +3,24 @@ import { BannerSection } from './_components/banner-section';
|
|||||||
import { Topics } from './_components/topics';
|
import { Topics } from './_components/topics';
|
||||||
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { SearchOutlined } from '@ant-design/icons';
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
import { For } from '@imphnen-frontend-service/utils';
|
import { For, Show } from '@imphnen-frontend-service/utils';
|
||||||
import { MentorCard } from './_components/mentor-card';
|
import { MentorCard } from './_components/mentor-card';
|
||||||
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { useGetMentors } from '@imphnen-frontend-service/service';
|
||||||
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
import { motion, useInView, Variants } from 'framer-motion';
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
const TEMP_DATA = [
|
|
||||||
{ id: 1, name: 'John Doe' },
|
|
||||||
{ id: 2, name: 'John Doe' },
|
|
||||||
{ id: 3, name: 'John Doe' },
|
|
||||||
{ id: 4, name: 'John Doe' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const Components: FC = (): ReactElement => {
|
export const Components: FC = (): ReactElement => {
|
||||||
const [pagination, setPagination] = useState<PaginationState>({
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
pageIndex: 0,
|
pageIndex: 0,
|
||||||
pageSize: 3,
|
pageSize: 8,
|
||||||
});
|
});
|
||||||
|
const [search, setSearch] = useState('');
|
||||||
|
|
||||||
|
const { data: mentors, isLoading } = useGetMentors({ search });
|
||||||
|
|
||||||
const table = useReactTable({
|
const table = useReactTable({
|
||||||
data: TEMP_DATA,
|
data: mentors ?? [],
|
||||||
columns: [],
|
columns: [],
|
||||||
state: { pagination },
|
state: { pagination },
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -31,6 +28,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
onPaginationChange: setPagination,
|
onPaginationChange: setPagination,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const pageMentors = table.getRowModel().rows.map((row) => row.original);
|
||||||
|
|
||||||
const ref = useRef(null)
|
const ref = useRef(null)
|
||||||
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
@@ -91,6 +90,8 @@ export const Components: FC = (): ReactElement => {
|
|||||||
<Input
|
<Input
|
||||||
placeholder="Cari berdasarkan nama, posisi/peran"
|
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||||
className="relative min-w-full w-full"
|
className="relative min-w-full w-full"
|
||||||
|
value={search}
|
||||||
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
<SearchOutlined className="absolute right-2.5 top-1/2 -translate-y-1/2 text-primary-500 size-2.5 cursor-text md:me-12 lg:me-0" />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
@@ -101,13 +102,31 @@ export const Components: FC = (): ReactElement => {
|
|||||||
initial="hidden"
|
initial="hidden"
|
||||||
animate={isInView ? 'visible' : 'hidden'}
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
>
|
>
|
||||||
<For data={Array.from({ length: 8 })}>
|
<Show
|
||||||
{(_, index) => (
|
condition={!isLoading}
|
||||||
<motion.div key={index} variants={childVariants}>
|
fallback={
|
||||||
<MentorCard />
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Memuat data mentor...
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={pageMentors.length > 0}
|
||||||
|
fallback={
|
||||||
|
<motion.p variants={childVariants} className="text-center text-neutral-500 col-span-full py-10">
|
||||||
|
Tidak ada mentor ditemukan
|
||||||
|
</motion.p>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<For data={pageMentors}>
|
||||||
|
{(mentor, index) => (
|
||||||
|
<motion.div key={mentor.id ?? index} variants={childVariants}>
|
||||||
|
<MentorCard mentor={mentor} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<Pagination table={table} />
|
<Pagination table={table} />
|
||||||
|
|||||||
@@ -1,166 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { FC, ReactElement, useState } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { ProfileForm, ProfileSidebar, ProfileHeader } from '../_components';
|
|
||||||
import { ArrowLeftOutlined } from '@ant-design/icons';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { NotificationModal, NotificationType } from '../_components/modals/notification-modal';
|
|
||||||
import { ProfileProvider, useProfile } from '../_components/contexts/profile-context';
|
|
||||||
import { EditProfileModal } from '../_components/modals/edit-profile-modal';
|
|
||||||
|
|
||||||
const ProfileByIdPage: FC = (): ReactElement => {
|
|
||||||
const params = useParams();
|
|
||||||
const id = (params && params.id) ? params.id as string : undefined;
|
|
||||||
|
|
||||||
if (!id) {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-red-600 text-lg">Profile ID not found.</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProfileProvider profileId={id} profileType="user">
|
|
||||||
<ProfileByIdContent />
|
|
||||||
</ProfileProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ProfileByIdContent: FC = (): ReactElement => {
|
|
||||||
const { profileData, isLoading, error, profileType } = useProfile();
|
|
||||||
|
|
||||||
const [notification, setNotification] = useState<{
|
|
||||||
isOpen: boolean;
|
|
||||||
type: 'success' | 'error';
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
}>({
|
|
||||||
isOpen: false,
|
|
||||||
type: 'success',
|
|
||||||
title: '',
|
|
||||||
message: ''
|
|
||||||
});
|
|
||||||
|
|
||||||
const [isEditProfileModalOpen, setIsEditProfileModalOpen] = useState(false);
|
|
||||||
|
|
||||||
const getProfileTitle = () => {
|
|
||||||
if (profileData?.fullname) {
|
|
||||||
return `${profileData.fullname}'s Profile`;
|
|
||||||
}
|
|
||||||
return profileType === 'user' ? 'User Profile' : 'Mentor Profile';
|
|
||||||
};
|
|
||||||
|
|
||||||
const showNotification = (type: NotificationType['type'], title: string, message?: string) => {
|
|
||||||
setNotification({
|
|
||||||
isOpen: true,
|
|
||||||
type,
|
|
||||||
title,
|
|
||||||
message
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const hideNotification = () => {
|
|
||||||
setNotification(prev => ({ ...prev, isOpen: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const openEditProfileModal = () => {
|
|
||||||
setIsEditProfileModalOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const closeEditProfileModal = () => {
|
|
||||||
setIsEditProfileModalOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Set isViewOnly to true for this page
|
|
||||||
const isViewOnly = true;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto"></div>
|
|
||||||
<p className="mt-4 text-gray-600">Loading profile...</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen flex items-center justify-center">
|
|
||||||
<div className="text-center">
|
|
||||||
<p className="text-red-600 text-lg">Failed to load profile</p>
|
|
||||||
<p className="text-gray-600 mt-2">Profile not found or you don't have permission to view it.</p>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="min-h-screen">
|
|
||||||
<div className="">
|
|
||||||
<div className="w-full px-8 md:px-[60px] lg:px-20 py-4">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<Button variant="primary" className="flex items-center gap-2">
|
|
||||||
<ArrowLeftOutlined />
|
|
||||||
Kembali ke Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full px-8 md:px-[60px] lg:px-20 py-6">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<h1 className="text-2xl font-semibold text-gray-900">
|
|
||||||
{getProfileTitle()}
|
|
||||||
</h1>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full px-8 md:px-[60px] lg:px-20 pb-12">
|
|
||||||
<div className="max-w-7xl mx-auto">
|
|
||||||
<div className="grid gap-8 lg:grid-cols-12">
|
|
||||||
<div className="lg:col-span-12">
|
|
||||||
<ProfileHeader onEditProfileClick={openEditProfileModal} isViewOnly={isViewOnly} />
|
|
||||||
</div>
|
|
||||||
<div className="lg:col-span-8 order-1">
|
|
||||||
<ProfileForm
|
|
||||||
showNotification={showNotification}
|
|
||||||
isViewOnly={isViewOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="lg:col-span-4 order-2">
|
|
||||||
<ProfileSidebar
|
|
||||||
showNotification={showNotification}
|
|
||||||
isViewOnly={isViewOnly}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<NotificationModal
|
|
||||||
isOpen={notification.isOpen}
|
|
||||||
onClose={hideNotification}
|
|
||||||
type={notification.type}
|
|
||||||
title={notification.title}
|
|
||||||
message={notification.message}
|
|
||||||
header="Profile"
|
|
||||||
/>
|
|
||||||
{/* Only render EditProfileModal if not in view-only mode */}
|
|
||||||
{!isViewOnly && (
|
|
||||||
<EditProfileModal
|
|
||||||
isOpen={isEditProfileModalOpen}
|
|
||||||
onClose={closeEditProfileModal}
|
|
||||||
showNotification={showNotification}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileByIdPage;
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import { EditOutlined } from '@ant-design/icons';
|
|
||||||
|
|
||||||
interface EditSectionButtonProps {
|
|
||||||
onClick: () => void;
|
|
||||||
disabled?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditSectionButton: FC<EditSectionButtonProps> = ({ onClick, disabled = false }) => {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled}
|
|
||||||
className={`flex items-center gap-1 px-3 py-1 text-sm transition-colors duration-200 rounded-md ${
|
|
||||||
disabled
|
|
||||||
? 'text-gray-400 cursor-not-allowed opacity-50'
|
|
||||||
: 'text-[#23A1EB] hover:text-[#1e90d6] hover:bg-[#23A1EB]/10'
|
|
||||||
}`}
|
|
||||||
aria-label="Edit section"
|
|
||||||
>
|
|
||||||
<span>Edit</span>
|
|
||||||
<EditOutlined className="w-3 h-3" />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
export { EditSectionButton } from './edit-section-button';
|
|
||||||
export { ModalButton } from './modal-button';
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
import { FC, ReactNode } from 'react';
|
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'secondary' | 'danger';
|
|
||||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
|
||||||
|
|
||||||
interface ModalButtonProps {
|
|
||||||
children: ReactNode;
|
|
||||||
onClick?: () => void;
|
|
||||||
type?: 'button' | 'submit' | 'reset';
|
|
||||||
variant?: ButtonVariant;
|
|
||||||
size?: ButtonSize;
|
|
||||||
disabled?: boolean;
|
|
||||||
loading?: boolean;
|
|
||||||
className?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const getVariantClasses = (variant: ButtonVariant): string => {
|
|
||||||
switch (variant) {
|
|
||||||
case 'primary':
|
|
||||||
return 'bg-[#23A1EB] hover:bg-[#1e90d6] text-white shadow-lg hover:shadow-xl';
|
|
||||||
case 'secondary':
|
|
||||||
return 'bg-white hover:bg-gray-200 text-[#23A1EB] shadow-md hover:shadow-lg';
|
|
||||||
case 'danger':
|
|
||||||
return 'bg-red-600 hover:bg-red-500 text-white shadow-lg hover:shadow-xl';
|
|
||||||
default:
|
|
||||||
return 'bg-[#23A1EB] hover:bg-[#1e90d6] text-white shadow-lg hover:shadow-xl';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSizeClasses = (size: ButtonSize): string => {
|
|
||||||
switch (size) {
|
|
||||||
case 'sm':
|
|
||||||
return 'px-3 py-1.5 text-sm';
|
|
||||||
case 'md':
|
|
||||||
return 'px-4 py-2 text-sm';
|
|
||||||
case 'lg':
|
|
||||||
return 'px-6 py-3 text-base';
|
|
||||||
default:
|
|
||||||
return 'px-4 py-2 text-sm';
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ModalButton: FC<ModalButtonProps> = ({
|
|
||||||
children,
|
|
||||||
onClick,
|
|
||||||
type = 'button',
|
|
||||||
variant = 'primary',
|
|
||||||
size = 'md',
|
|
||||||
disabled = false,
|
|
||||||
loading = false,
|
|
||||||
className = '',
|
|
||||||
}) => {
|
|
||||||
const baseClasses = 'font-medium rounded-lg transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-[#23A1EB] disabled:opacity-50 disabled:cursor-not-allowed';
|
|
||||||
const variantClasses = getVariantClasses(variant);
|
|
||||||
const sizeClasses = getSizeClasses(size);
|
|
||||||
|
|
||||||
const combinedClasses = `${baseClasses} ${variantClasses} ${sizeClasses} ${className}`;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
type={type}
|
|
||||||
onClick={onClick}
|
|
||||||
disabled={disabled || loading}
|
|
||||||
className={combinedClasses}
|
|
||||||
>
|
|
||||||
{loading ? (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="w-4 h-4 border-2 border-current border-t-transparent rounded-full animate-spin"></div>
|
|
||||||
<span>Loading...</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
children
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export * from './profile-context';
|
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React, { createContext, useContext, useMemo, useCallback } from 'react';
|
|
||||||
import { useParams } from 'react-router-dom';
|
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useAuthStore } from '@imphnen-frontend-service/utils';
|
|
||||||
import {
|
|
||||||
|
|
||||||
useUserMe,
|
|
||||||
useUserById,
|
|
||||||
useUpdateUserMe,
|
|
||||||
useUpdateUserById,
|
|
||||||
UserDetailResponseDto,
|
|
||||||
UserUpdateRequestDto,
|
|
||||||
|
|
||||||
useMentorMe,
|
|
||||||
useMentorById,
|
|
||||||
useUpdateMentorMe,
|
|
||||||
useUpdateMentorById,
|
|
||||||
MentorDetailResponseDto,
|
|
||||||
MentorUpdateRequestDto
|
|
||||||
} from '@imphnen-frontend-service/service';
|
|
||||||
|
|
||||||
|
|
||||||
type ProfileData = UserDetailResponseDto | MentorDetailResponseDto;
|
|
||||||
type ProfileUpdateData = UserUpdateRequestDto | MentorUpdateRequestDto;
|
|
||||||
|
|
||||||
|
|
||||||
const canAccessMentorFeatures = (user: { role?: { name?: string; permissions?: Array<{ name?: string }> } } | null) => {
|
|
||||||
if (!user?.role) return false;
|
|
||||||
|
|
||||||
const roleName = user.role.name?.toLowerCase() || '';
|
|
||||||
const isMentorRole = roleName.includes('mentor') || roleName.includes('admin');
|
|
||||||
|
|
||||||
if (isMentorRole) return true;
|
|
||||||
|
|
||||||
|
|
||||||
const permissions = user.role.permissions || [];
|
|
||||||
const hasMentorPermission = permissions.some((permission: { name?: string }) =>
|
|
||||||
permission.name?.toLowerCase().includes('mentor')
|
|
||||||
);
|
|
||||||
|
|
||||||
return hasMentorPermission;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface ProfileContextType {
|
|
||||||
profileData: ProfileData | undefined;
|
|
||||||
isLoading: boolean;
|
|
||||||
error: unknown;
|
|
||||||
isOwnProfile: boolean;
|
|
||||||
profileId: string | null;
|
|
||||||
profileType: 'user' | 'mentor';
|
|
||||||
updateProfile: (data: ProfileUpdateData) => Promise<void>;
|
|
||||||
isUpdating: boolean;
|
|
||||||
canAccessMentor: boolean;
|
|
||||||
}const ProfileContext = createContext<ProfileContextType | undefined>(undefined);
|
|
||||||
|
|
||||||
interface ProfileProviderProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
profileId?: string;
|
|
||||||
profileType?: 'user' | 'mentor';
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProfileProvider: React.FC<ProfileProviderProps> = ({
|
|
||||||
children,
|
|
||||||
profileId,
|
|
||||||
profileType: forcedProfileType
|
|
||||||
}) => {
|
|
||||||
const params = useParams();
|
|
||||||
const { session } = useAuthStore();
|
|
||||||
const queryClient = useQueryClient();
|
|
||||||
|
|
||||||
|
|
||||||
const canAccessMentor = useMemo(() => {
|
|
||||||
return canAccessMentorFeatures(session?.user || null);
|
|
||||||
}, [session?.user]);
|
|
||||||
|
|
||||||
|
|
||||||
const isMentorRole = useMemo(() => {
|
|
||||||
const roleName = session?.user?.role?.name?.toLowerCase() || '';
|
|
||||||
return roleName === 'mentor';
|
|
||||||
}, [session?.user?.role?.name]);
|
|
||||||
|
|
||||||
|
|
||||||
const profileType: 'user' | 'mentor' = useMemo(() => {
|
|
||||||
if (forcedProfileType) {
|
|
||||||
|
|
||||||
if (forcedProfileType === 'mentor' && !isMentorRole) {
|
|
||||||
return 'user';
|
|
||||||
}
|
|
||||||
return forcedProfileType;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if ((params?.mentor || (typeof window !== 'undefined' && window.location.pathname.includes('/mentor'))) && isMentorRole) {
|
|
||||||
return 'mentor';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'user';
|
|
||||||
}, [forcedProfileType, params, isMentorRole]);
|
|
||||||
|
|
||||||
|
|
||||||
const id = profileId || (params?.id as string) || undefined;
|
|
||||||
const isOwnProfile = !id;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const userMeQuery = useUserMe({
|
|
||||||
queryKey: ['user-me'],
|
|
||||||
enabled: isOwnProfile && profileType === 'user',
|
|
||||||
});
|
|
||||||
const userByIdQuery = useUserById(id || '', {
|
|
||||||
queryKey: ['user-by-id', id],
|
|
||||||
enabled: !isOwnProfile && !!id && profileType === 'user',
|
|
||||||
});
|
|
||||||
const updateUserMeMutation = useUpdateUserMe();
|
|
||||||
const updateUserByIdMutation = useUpdateUserById();
|
|
||||||
|
|
||||||
const mentorMeQuery = useMentorMe({
|
|
||||||
queryKey: ['mentor-me'],
|
|
||||||
enabled: isOwnProfile && profileType === 'mentor' && canAccessMentor,
|
|
||||||
});
|
|
||||||
const mentorByIdQuery = useMentorById(id || '', {
|
|
||||||
queryKey: ['mentor-by-id', id],
|
|
||||||
enabled: !isOwnProfile && !!id && profileType === 'mentor' && canAccessMentor,
|
|
||||||
});
|
|
||||||
const updateMentorMeMutation = useUpdateMentorMe();
|
|
||||||
const updateMentorByIdMutation = useUpdateMentorById();
|
|
||||||
|
|
||||||
|
|
||||||
const selectedUserQuery = isOwnProfile ? userMeQuery : userByIdQuery;
|
|
||||||
const selectedMentorQuery = isOwnProfile ? mentorMeQuery : mentorByIdQuery;
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: profileData,
|
|
||||||
isLoading,
|
|
||||||
error
|
|
||||||
} = useMemo(() => {
|
|
||||||
|
|
||||||
if (canAccessMentor && profileType === 'mentor') {
|
|
||||||
return selectedMentorQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedUserQuery;
|
|
||||||
}, [profileType, canAccessMentor, selectedUserQuery, selectedMentorQuery]);
|
|
||||||
|
|
||||||
|
|
||||||
const selectedUserMutation = isOwnProfile ? updateUserMeMutation : updateUserByIdMutation;
|
|
||||||
const selectedMentorMutation = isOwnProfile ? updateMentorMeMutation : updateMentorByIdMutation;
|
|
||||||
|
|
||||||
const updateMutation = useMemo(() => {
|
|
||||||
|
|
||||||
if (canAccessMentor && profileType === 'mentor') {
|
|
||||||
return selectedMentorMutation;
|
|
||||||
}
|
|
||||||
|
|
||||||
return selectedUserMutation;
|
|
||||||
}, [profileType, canAccessMentor, selectedUserMutation, selectedMentorMutation]);
|
|
||||||
|
|
||||||
|
|
||||||
const updateProfile = useCallback(async (data: ProfileUpdateData) => {
|
|
||||||
try {
|
|
||||||
if (canAccessMentor && profileType === 'mentor') {
|
|
||||||
|
|
||||||
if (isOwnProfile) {
|
|
||||||
await updateMentorMeMutation.mutateAsync(data as MentorUpdateRequestDto);
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['mentor-me'] });
|
|
||||||
} else if (id) {
|
|
||||||
await updateMentorByIdMutation.mutateAsync({ id, data: data as MentorUpdateRequestDto });
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['mentor-by-id', id] });
|
|
||||||
}
|
|
||||||
} else if (isOwnProfile) {
|
|
||||||
|
|
||||||
await updateUserMeMutation.mutateAsync(data as UserUpdateRequestDto);
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['user-me'] });
|
|
||||||
} else if (id) {
|
|
||||||
await updateUserByIdMutation.mutateAsync({ id, data: data as UserUpdateRequestDto });
|
|
||||||
|
|
||||||
await queryClient.invalidateQueries({ queryKey: ['user-by-id', id] });
|
|
||||||
}
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Failed to update profile:', error);
|
|
||||||
|
|
||||||
let apiMessage = '';
|
|
||||||
if (typeof error === 'object' && error !== null) {
|
|
||||||
const errObj = error as { response?: { data?: unknown } };
|
|
||||||
const data = errObj.response?.data;
|
|
||||||
if (data) {
|
|
||||||
try {
|
|
||||||
const parsed = typeof data === 'string' ? JSON.parse(data) : data;
|
|
||||||
if (parsed && typeof parsed.message === 'string') {
|
|
||||||
apiMessage = parsed.message;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
apiMessage = typeof data === 'string' ? data : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (apiMessage) {
|
|
||||||
throw new Error(apiMessage);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}, [
|
|
||||||
profileType,
|
|
||||||
isOwnProfile,
|
|
||||||
canAccessMentor,
|
|
||||||
id,
|
|
||||||
queryClient,
|
|
||||||
updateUserMeMutation,
|
|
||||||
updateUserByIdMutation,
|
|
||||||
updateMentorMeMutation,
|
|
||||||
updateMentorByIdMutation
|
|
||||||
]); const isUpdating = updateMutation.isPending;
|
|
||||||
|
|
||||||
const value: ProfileContextType = useMemo(() => ({
|
|
||||||
profileData,
|
|
||||||
isLoading,
|
|
||||||
error,
|
|
||||||
isOwnProfile,
|
|
||||||
profileId: isOwnProfile ? null : (id || null),
|
|
||||||
profileType,
|
|
||||||
updateProfile,
|
|
||||||
isUpdating,
|
|
||||||
canAccessMentor
|
|
||||||
}), [profileData, isLoading, error, isOwnProfile, id, profileType, updateProfile, isUpdating, canAccessMentor]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ProfileContext.Provider value={value}>
|
|
||||||
{children}
|
|
||||||
</ProfileContext.Provider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const useProfile = (): ProfileContextType => {
|
|
||||||
const context = useContext(ProfileContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useProfile must be used within a ProfileProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export type { ProfileContextType };
|
|
||||||
export type { ProfileData, ProfileUpdateData };
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
|
||||||
import { Guard } from '@imphnen-frontend-service/utils';
|
|
||||||
|
|
||||||
interface MentorGuardProps {
|
|
||||||
children: React.ReactNode;
|
|
||||||
fallback?: React.ReactNode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MentorGuard: React.FC<MentorGuardProps> = ({
|
|
||||||
children,
|
|
||||||
fallback = (
|
|
||||||
<div className="p-4 text-center text-red-500">
|
|
||||||
<p>Akses ditolak: Anda tidak memiliki izin untuk mengakses fitur mentor.</p>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}) => {
|
|
||||||
return (
|
|
||||||
<Guard
|
|
||||||
permissions={['mentor', 'admin']}
|
|
||||||
fallback={fallback}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Guard>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
|
|
||||||
export * from './profile';
|
|
||||||
|
|
||||||
|
|
||||||
export * from './sections';
|
|
||||||
|
|
||||||
|
|
||||||
export * from './buttons';
|
|
||||||
|
|
||||||
|
|
||||||
export * from './shared';
|
|
||||||
|
|
||||||
|
|
||||||
export * from './modals';
|
|
||||||
|
|
||||||
|
|
||||||
export * from './contexts';
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
import { useUploadCV } from '@imphnen-frontend-service/service';
|
|
||||||
import { FileUploader } from '../shared/file-uploader';
|
|
||||||
|
|
||||||
interface CVData {
|
|
||||||
fileName: string;
|
|
||||||
fileUrl?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CVModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: CVData;
|
|
||||||
onSave: (value: CVData) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CVModal: FC<CVModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [cvData, setCvData] = useState(initialValue);
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
const uploadCVMutation = useUploadCV();
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCvData(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
await onSave(cvData);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setCvData(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleFileSelect = async (file: File) => {
|
|
||||||
try {
|
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
|
|
||||||
if (!file.type.includes('pdf')) {
|
|
||||||
throw new Error('Please select a PDF file');
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const uploadResult = await uploadCVMutation.mutateAsync(file);
|
|
||||||
|
|
||||||
console.log('CV upload response:', uploadResult);
|
|
||||||
|
|
||||||
|
|
||||||
interface UploadData {
|
|
||||||
original_filename?: string;
|
|
||||||
filename?: string;
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
|
|
||||||
|
|
||||||
setCvData({
|
|
||||||
fileName: uploadData.original_filename || uploadData.filename || file.name,
|
|
||||||
fileUrl: uploadData.url || '',
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('CV uploaded successfully, URL:', uploadData.url);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('CV upload error:', error);
|
|
||||||
|
|
||||||
const fileInput = document.getElementById('cv-upload') as HTMLInputElement;
|
|
||||||
if (fileInput) fileInput.value = '';
|
|
||||||
} finally {
|
|
||||||
setIsUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
|
|
||||||
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit CV/Resume</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="px-6 py-6 space-y-6">
|
|
||||||
{}
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Upload CV/Resume
|
|
||||||
</h3>
|
|
||||||
<FileUploader
|
|
||||||
accept=".pdf"
|
|
||||||
maxSize={10 * 1024 * 1024}
|
|
||||||
onFileSelect={handleFileSelect}
|
|
||||||
isLoading={isUploading}
|
|
||||||
dragAndDrop={true}
|
|
||||||
description="Klik atau tarik file PDF yang ingin di upload"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 mt-2">
|
|
||||||
Format yang didukung: PDF • Maksimal ukuran: 10MB
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
{cvData.fileName && (
|
|
||||||
<div className="p-4 bg-blue-50 border border-blue-200 rounded-lg">
|
|
||||||
<h4 className="text-sm font-medium text-blue-900 mb-3">File Terpilih</h4>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-12 h-12 bg-blue-500 rounded-lg flex items-center justify-center">
|
|
||||||
<span className="text-white text-xs font-bold">PDF</span>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="font-medium text-blue-900 text-sm">{cvData.fileName}</p>
|
|
||||||
<p className="text-xs text-blue-600">Siap untuk disimpan</p>
|
|
||||||
</div>
|
|
||||||
{cvData.fileUrl && (
|
|
||||||
<a
|
|
||||||
href={cvData.fileUrl}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-blue-600 hover:text-blue-800 text-sm font-medium underline"
|
|
||||||
>
|
|
||||||
Preview
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex gap-3 p-6 pt-4 border-t border-gray-100">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading || isUploading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading || isUploading || !cvData.fileName}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan CV'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
|
|
||||||
interface DescriptionModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: string;
|
|
||||||
onSave: (value: string) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DescriptionModal: FC<DescriptionModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [description, setDescription] = useState(initialValue);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setDescription(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
await onSave(description);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setDescription(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Description</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="px-6 py-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="description-textarea" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Description
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
id="description-textarea"
|
|
||||||
value={description}
|
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
|
||||||
rows={8}
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors resize-none"
|
|
||||||
placeholder="Write your description here..."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-3 p-6 pt-4">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,253 +0,0 @@
|
|||||||
import React, { FC, useState, useEffect } from 'react';
|
|
||||||
import { Modal, InputField } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { useProfile } from '../contexts/profile-context';
|
|
||||||
import { CameraOutlined } from '@ant-design/icons';
|
|
||||||
import { useUploadAvatar } from '@imphnen-frontend-service/service';
|
|
||||||
|
|
||||||
interface EditProfileModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
showNotification: (type: 'success' | 'error', title: string, message?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, showNotification }) => {
|
|
||||||
const { profileData, profileType, updateProfile } = useProfile();
|
|
||||||
const uploadAvatarMutation = useUploadAvatar();
|
|
||||||
const [formData, setFormData] = useState({
|
|
||||||
fullname: '',
|
|
||||||
avatar: ''
|
|
||||||
});
|
|
||||||
const [previewUrl, setPreviewUrl] = useState<string>('/image/testimonial.webp');
|
|
||||||
const [isUploading, setIsUploading] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (profileData) {
|
|
||||||
const fullname = profileData.fullname ||
|
|
||||||
(profileType === 'mentor' && 'legal_name' in profileData ? profileData.legal_name : '') || '';
|
|
||||||
|
|
||||||
|
|
||||||
const avatar = (profileType === 'user' && 'avatar' in profileData)
|
|
||||||
? profileData.avatar || '/image/testimonial.webp'
|
|
||||||
: '/image/testimonial.webp';
|
|
||||||
|
|
||||||
console.log('Modal - Profile data avatar URL:', avatar);
|
|
||||||
console.log('Modal - Profile data:', profileData);
|
|
||||||
|
|
||||||
setFormData({
|
|
||||||
fullname,
|
|
||||||
avatar: (profileType === 'user' && 'avatar' in profileData) ? profileData.avatar || '' : ''
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
setPreviewUrl(avatar);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
setPreviewUrl('/image/testimonial.webp');
|
|
||||||
}
|
|
||||||
}, [profileData, profileType]);
|
|
||||||
|
|
||||||
const handleImageError = () => {
|
|
||||||
console.log('Image failed to load:', previewUrl);
|
|
||||||
setPreviewUrl('/image/testimonial.webp');
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setFormData(prev => ({ ...prev, fullname: e.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleImageUpload = async (file: File) => {
|
|
||||||
try {
|
|
||||||
setIsUploading(true);
|
|
||||||
|
|
||||||
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.onload = () => {
|
|
||||||
const result = reader.result as string;
|
|
||||||
setPreviewUrl(result);
|
|
||||||
};
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
|
|
||||||
|
|
||||||
const uploadResult = await uploadAvatarMutation.mutateAsync(file);
|
|
||||||
|
|
||||||
console.log('Avatar upload response:', uploadResult);
|
|
||||||
|
|
||||||
|
|
||||||
interface UploadData {
|
|
||||||
url?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const uploadData = ('data' in uploadResult ? (uploadResult as { data: UploadData }).data : uploadResult as UploadData);
|
|
||||||
|
|
||||||
|
|
||||||
setFormData(prev => ({ ...prev, avatar: uploadData.url || '' }));
|
|
||||||
|
|
||||||
|
|
||||||
setPreviewUrl(uploadData.url || '/image/testimonial.webp');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Avatar upload error:', error);
|
|
||||||
showNotification('error', 'Upload Failed', 'Failed to upload avatar image');
|
|
||||||
|
|
||||||
const originalAvatar = (profileType === 'user' && profileData && 'avatar' in profileData) ? profileData.avatar : '';
|
|
||||||
setPreviewUrl(originalAvatar || '/image/testimonial.webp');
|
|
||||||
} finally {
|
|
||||||
setIsUploading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
const updates: Record<string, string> = {};
|
|
||||||
|
|
||||||
|
|
||||||
if (formData.fullname.trim() !== '') {
|
|
||||||
if (profileType === 'user') {
|
|
||||||
updates.fullname = formData.fullname;
|
|
||||||
} else if (profileType === 'mentor') {
|
|
||||||
updates.legal_name = formData.fullname;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (formData.avatar && formData.avatar !== (profileData && 'avatar' in profileData ? profileData.avatar : '')) {
|
|
||||||
updates.avatar = formData.avatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(updates).length > 0) {
|
|
||||||
await updateProfile(updates);
|
|
||||||
showNotification('success', 'Profile Updated', 'Your profile has been successfully updated.');
|
|
||||||
}
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (err: unknown) {
|
|
||||||
console.error('Profile update error:', err);
|
|
||||||
let apiMessage = '';
|
|
||||||
if (typeof err === 'object' && err !== null) {
|
|
||||||
const errObj = err as { response?: { data?: { message?: string } } };
|
|
||||||
let backendMsg = '';
|
|
||||||
if (errObj.response?.data?.message) {
|
|
||||||
backendMsg = errObj.response.data.message;
|
|
||||||
}
|
|
||||||
let msg = '';
|
|
||||||
if ('message' in err && typeof (err as { message?: string }).message === 'string') {
|
|
||||||
msg = (err as { message?: string }).message || '';
|
|
||||||
|
|
||||||
if (msg.trim().startsWith('{') && msg.trim().endsWith('}')) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(msg);
|
|
||||||
if (parsed && typeof parsed.message === 'string') {
|
|
||||||
msg = parsed.message;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Ignore JSON parse errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (backendMsg && msg && backendMsg !== msg) {
|
|
||||||
apiMessage = backendMsg + '\n' + msg;
|
|
||||||
} else if (backendMsg) {
|
|
||||||
apiMessage = backendMsg;
|
|
||||||
} else if (msg) {
|
|
||||||
apiMessage = msg;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showNotification('error', 'Failed to save changes', apiMessage || 'Please try again.');
|
|
||||||
}
|
|
||||||
}; return (
|
|
||||||
<Modal isOpen={isOpen} onClose={onClose}>
|
|
||||||
<Modal.Header>
|
|
||||||
<Modal.Title>Edit Profile</Modal.Title>
|
|
||||||
</Modal.Header>
|
|
||||||
<Modal.Content>
|
|
||||||
{}
|
|
||||||
<div className="flex justify-center pt-6 pb-4">
|
|
||||||
<div className="relative">
|
|
||||||
{}
|
|
||||||
<input
|
|
||||||
id="avatar-upload"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
onChange={(e) => {
|
|
||||||
const file = e.target.files?.[0];
|
|
||||||
if (file) handleImageUpload(file);
|
|
||||||
}}
|
|
||||||
className="hidden"
|
|
||||||
disabled={isUploading}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<label htmlFor="avatar-upload" className="cursor-pointer block relative group">
|
|
||||||
<img
|
|
||||||
src={previewUrl}
|
|
||||||
alt="Profile"
|
|
||||||
className="w-24 h-24 rounded-full object-cover border-4 border-blue-100 shadow-lg transition-all duration-300 group-hover:border-blue-200"
|
|
||||||
onError={handleImageError}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Hover overlay */}
|
|
||||||
<div className="absolute inset-0 rounded-full bg-opacity-0 group-hover:bg-opacity-20 transition-all duration-300 flex items-center justify-center">
|
|
||||||
<span className="text-white text-xs opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
|
||||||
Change Photo
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<label htmlFor="avatar-upload" className="cursor-pointer">
|
|
||||||
<div className="absolute bottom-0 right-0 w-8 h-8 bg-blue-500 hover:bg-blue-600 text-white rounded-full flex items-center justify-center transition-all duration-300 shadow-lg border-2 border-white">
|
|
||||||
{isUploading ? (
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
|
|
||||||
) : (
|
|
||||||
<CameraOutlined className="text-xs" />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div> <div className="px-6 pb-6 space-y-6">
|
|
||||||
{}
|
|
||||||
<div className="space-y-2">
|
|
||||||
<InputField
|
|
||||||
label="Full Name"
|
|
||||||
name="fullname"
|
|
||||||
value={formData.fullname}
|
|
||||||
onChange={handleNameChange}
|
|
||||||
placeholder="Enter your full name"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
<p className="text-xs text-gray-500 pl-1">
|
|
||||||
This name will be displayed on your profile
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal.Content>
|
|
||||||
<Modal.Footer>
|
|
||||||
<div className="flex gap-3 w-full">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={onClose}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isUploading}
|
|
||||||
>
|
|
||||||
{isUploading ? (
|
|
||||||
<>
|
|
||||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
|
||||||
Saving...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
'Save Changes'
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</Modal.Footer>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,184 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
interface Education {
|
|
||||||
id: string;
|
|
||||||
institution: string;
|
|
||||||
degree: string;
|
|
||||||
field: string;
|
|
||||||
period: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EducationModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: Education[];
|
|
||||||
onSave: (value: Education[]) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
showNotification?: (type: 'success' | 'error', title: string, message?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const EducationModal: FC<EducationModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
showNotification,
|
|
||||||
}) => {
|
|
||||||
const [educations, setEducations] = useState<Education[]>(initialValue);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setEducations(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
|
|
||||||
const hasEmpty = educations.some(edu =>
|
|
||||||
!edu.institution.trim() || !edu.degree.trim() || !edu.field.trim() || !edu.period.trim()
|
|
||||||
);
|
|
||||||
if (hasEmpty) {
|
|
||||||
if (showNotification) {
|
|
||||||
showNotification('error', 'Data Tidak Lengkap', 'Semua field harus diisi pada setiap pendidikan.');
|
|
||||||
} else {
|
|
||||||
alert('Semua field harus diisi pada setiap pendidikan.');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await onSave(educations);
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setEducations(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addEducation = () => {
|
|
||||||
const newEducation: Education = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
institution: '',
|
|
||||||
degree: '',
|
|
||||||
field: '',
|
|
||||||
period: '',
|
|
||||||
};
|
|
||||||
setEducations([...educations, newEducation]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeEducation = (id: string) => {
|
|
||||||
setEducations(educations.filter(edu => edu.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateEducation = (id: string, field: keyof Education, value: string) => {
|
|
||||||
setEducations(educations.map(edu =>
|
|
||||||
edu.id === id ? { ...edu, [field]: value } : edu
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
></button>
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl max-w-5xl w-full max-h-[95vh] overflow-hidden">
|
|
||||||
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Education</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{educations.map((education, index) => (
|
|
||||||
<div key={education.id} className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900">Education {index + 1}</h3>
|
|
||||||
<ModalButton
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeEducation(education.id)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Institution"
|
|
||||||
value={education.institution}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateEducation(education.id, 'institution', e.target.value)}
|
|
||||||
placeholder="Enter institution name"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Degree"
|
|
||||||
value={education.degree}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateEducation(education.id, 'degree', e.target.value)}
|
|
||||||
placeholder="Enter degree"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Field"
|
|
||||||
value={education.field}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateEducation(education.id, 'field', e.target.value)}
|
|
||||||
placeholder="Enter field of study"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Period"
|
|
||||||
value={education.period}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateEducation(education.id, 'period', e.target.value)}
|
|
||||||
placeholder="e.g., Sep 2022 - Current"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={addEducation}
|
|
||||||
className="w-full border-2 border-dashed border-gray-300 rounded-lg p-4 text-gray-500 hover:border-[#23A1EB] hover:text-[#23A1EB] transition-colors flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
Add Education
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-white shadow-md"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
|
|
||||||
interface Experience {
|
|
||||||
id: string;
|
|
||||||
company: string;
|
|
||||||
position: string;
|
|
||||||
duration: string;
|
|
||||||
period: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExperienceModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: Experience[];
|
|
||||||
onSave: (value: Experience[]) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
showNotification?: (type: 'success' | 'error', title: string, message?: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ExperienceModal: FC<ExperienceModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
showNotification,
|
|
||||||
}) => {
|
|
||||||
const [experiences, setExperiences] = useState<Experience[]>(initialValue);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setExperiences(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
|
|
||||||
const hasEmpty = experiences.some(exp =>
|
|
||||||
!exp.company.trim() || !exp.position.trim() || !exp.duration.trim() || !exp.period.trim()
|
|
||||||
);
|
|
||||||
if (hasEmpty) {
|
|
||||||
if (showNotification) {
|
|
||||||
showNotification('error', 'Data Tidak Lengkap', 'Semua field harus diisi pada setiap pengalaman kerja.');
|
|
||||||
} else {
|
|
||||||
alert('Semua field harus diisi pada setiap pengalaman kerja.');
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await onSave(experiences);
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setExperiences(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addExperience = () => {
|
|
||||||
const newExperience: Experience = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
company: '',
|
|
||||||
position: '',
|
|
||||||
duration: '',
|
|
||||||
period: '',
|
|
||||||
};
|
|
||||||
setExperiences([...experiences, newExperience]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeExperience = (id: string) => {
|
|
||||||
setExperiences(experiences.filter(exp => exp.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateExperience = (id: string, field: keyof Experience, value: string) => {
|
|
||||||
setExperiences(experiences.map(exp =>
|
|
||||||
exp.id === id ? { ...exp, [field]: value } : exp
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl max-w-5xl w-full max-h-[95vh] overflow-hidden">
|
|
||||||
<div className="p-6 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Experience</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
|
||||||
<div className="space-y-6">
|
|
||||||
{experiences.map((experience, index) => (
|
|
||||||
<div key={experience.id} className="border border-gray-200 rounded-lg p-4">
|
|
||||||
<div className="flex items-center justify-between mb-4">
|
|
||||||
<h3 className="text-lg font-medium text-gray-900">Experience {index + 1}</h3>
|
|
||||||
<ModalButton
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
onClick={() => removeExperience(experience.id)}
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
||||||
<InputField
|
|
||||||
label="Company"
|
|
||||||
value={experience.company}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateExperience(experience.id, 'company', e.target.value)}
|
|
||||||
placeholder="Enter company name"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Position"
|
|
||||||
value={experience.position}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateExperience(experience.id, 'position', e.target.value)}
|
|
||||||
placeholder="Enter position"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Duration"
|
|
||||||
value={experience.duration}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateExperience(experience.id, 'duration', e.target.value)}
|
|
||||||
placeholder="e.g., 7 Months"
|
|
||||||
/>
|
|
||||||
<InputField
|
|
||||||
label="Period"
|
|
||||||
value={experience.period}
|
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => updateExperience(experience.id, 'period', e.target.value)}
|
|
||||||
placeholder="e.g., Jan 2024 - Present"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={addExperience}
|
|
||||||
className="w-full border-2 border-dashed border-gray-300 rounded-lg p-4 text-gray-500 hover:border-[#23A1EB] hover:text-[#23A1EB] transition-colors flex items-center justify-center gap-2"
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
Add Experience
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center justify-end gap-3 p-6 border-t border-gray-200 bg-gray-50">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
className="bg-white shadow-md"
|
|
||||||
onClick={handleCancel}
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
export { NotificationModal } from './notification-modal';
|
|
||||||
export { CVModal } from './cv-modal';
|
|
||||||
export { DescriptionModal } from './description-modal';
|
|
||||||
export { EducationModal } from './education-modal';
|
|
||||||
export { ExperienceModal } from './experience-modal';
|
|
||||||
export { SocialMediaModal } from './social-media-modal';
|
|
||||||
export { SkillsModal } from './skills-modal';
|
|
||||||
export { EditProfileModal } from './edit-profile-modal';
|
|
||||||
export { LanguagesModal } from './languages-modal';
|
|
||||||
export { PersonalInfoModal } from './personal-info-modal';
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
|
|
||||||
interface Language {
|
|
||||||
id?: string;
|
|
||||||
name: string;
|
|
||||||
level: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LanguagesModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: Language[];
|
|
||||||
onSave: (languages: Language[]) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const LanguagesModal: FC<LanguagesModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [languages, setLanguages] = useState<Language[]>(initialValue);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const languagesWithIds = initialValue.map(lang => ({
|
|
||||||
...lang,
|
|
||||||
id: lang.id || `lang-${Date.now()}-${Math.random()}`
|
|
||||||
}));
|
|
||||||
setLanguages(languagesWithIds);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleAddLanguage = () => {
|
|
||||||
setLanguages([...languages, {
|
|
||||||
id: `lang-${Date.now()}-${Math.random()}`,
|
|
||||||
name: '',
|
|
||||||
level: ''
|
|
||||||
}]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLanguageChange = (index: number, field: keyof Language, value: string) => {
|
|
||||||
const newLanguages = [...languages];
|
|
||||||
newLanguages[index] = { ...newLanguages[index], [field]: value };
|
|
||||||
setLanguages(newLanguages);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveLanguage = (index: number) => {
|
|
||||||
const newLanguages = languages.filter((_, i) => i !== index);
|
|
||||||
setLanguages(newLanguages);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
|
|
||||||
const languagesToSave = languages.map(({ id, ...lang }) => lang);
|
|
||||||
onSave(languagesToSave);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
const languagesWithIds = initialValue.map(lang => ({
|
|
||||||
...lang,
|
|
||||||
id: lang.id || `lang-${Date.now()}-${Math.random()}`
|
|
||||||
}));
|
|
||||||
setLanguages(languagesWithIds);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
{}
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
type="button"
|
|
||||||
aria-label="Close modal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto max-h-[95vh] overflow-hidden">
|
|
||||||
{}
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Languages</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
|
||||||
<div className="space-y-4">
|
|
||||||
{languages.map((language, index) => (
|
|
||||||
<div key={language.id} className="flex flex-col sm:flex-row sm:items-end gap-3 sm:gap-2">
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-gray-700 mb-1">Language Name</p>
|
|
||||||
<Input
|
|
||||||
value={language.name}
|
|
||||||
onChange={(e) => handleLanguageChange(index, 'name', e.target.value)}
|
|
||||||
placeholder="e.g., English"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex-1">
|
|
||||||
<p className="text-sm font-medium text-gray-700 mb-1">Level</p>
|
|
||||||
<Input
|
|
||||||
value={language.level}
|
|
||||||
onChange={(e) => handleLanguageChange(index, 'level', e.target.value)}
|
|
||||||
placeholder="e.g., Fluent"
|
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="sm:flex-shrink-0">
|
|
||||||
<ModalButton variant="danger" onClick={() => handleRemoveLanguage(index)} className="w-full sm:w-auto">
|
|
||||||
Remove
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
<ModalButton variant="secondary" onClick={handleAddLanguage} className="w-full">
|
|
||||||
Add Language
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{}
|
|
||||||
<div className="flex gap-3 p-6 pt-4">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1 bg-white shadow-md"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Saving...' : 'Save'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { FC } from 'react';
|
|
||||||
import { CloseOutlined } from '@ant-design/icons';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
|
|
||||||
export type NotificationType = {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
type: 'success' | 'error';
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
header: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface NotificationModalProps extends NotificationType {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
type: 'success' | 'error';
|
|
||||||
title: string;
|
|
||||||
message?: string;
|
|
||||||
header: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NotificationModal: FC<NotificationModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
type,
|
|
||||||
title,
|
|
||||||
message,
|
|
||||||
header,
|
|
||||||
}) => {
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
const isSuccess = type === 'success';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-[9999] flex items-center justify-center p-4">
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={onClose}
|
|
||||||
aria-label="Close modal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl max-w-md w-full overflow-hidden">
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">{header}</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="p-8 text-center">
|
|
||||||
|
|
||||||
<div className="mb-6">
|
|
||||||
{isSuccess ? (
|
|
||||||
<div className="w-30 h-30 mx-auto mb-4">
|
|
||||||
<img
|
|
||||||
src="/image/success.png"
|
|
||||||
alt="Success"
|
|
||||||
width={80}
|
|
||||||
height={80}
|
|
||||||
className="w-full h-full object-contain"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="w-20 h-20 bg-red-500 rounded-full flex items-center justify-center mx-auto mb-4">
|
|
||||||
<CloseOutlined className="text-white text-3xl" />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<h2 className={`text-lg font-medium ${isSuccess ? 'text-green-600' : 'text-red-600'}`}>
|
|
||||||
{title}
|
|
||||||
</h2>
|
|
||||||
{!isSuccess && message && (
|
|
||||||
<div className="mt-2 text-sm text-red-500 whitespace-pre-line">{message}</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={onClose}
|
|
||||||
className="w-full bg-[#23A1EB] hover:bg-[#23A1EB]/90"
|
|
||||||
>
|
|
||||||
Selesai
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
|
|
||||||
interface PersonalInfo {
|
|
||||||
email: string;
|
|
||||||
phone: string;
|
|
||||||
location: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PersonalInfoModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: PersonalInfo;
|
|
||||||
onSave: (value: PersonalInfo) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PersonalInfoModal: FC<PersonalInfoModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [personalInfo, setPersonalInfo] = useState(initialValue);
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setPersonalInfo(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
await onSave(personalInfo);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setPersonalInfo(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
|
|
||||||
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Personal Information</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="px-6 py-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="personal-email" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Email
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="personal-email"
|
|
||||||
type="email"
|
|
||||||
value={personalInfo.email}
|
|
||||||
onChange={(e) => setPersonalInfo({ ...personalInfo, email: e.target.value })}
|
|
||||||
placeholder="Enter your email"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="personal-phone" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Phone
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="personal-phone"
|
|
||||||
type="tel"
|
|
||||||
value={personalInfo.phone}
|
|
||||||
onChange={(e) => setPersonalInfo({ ...personalInfo, phone: e.target.value })}
|
|
||||||
placeholder="Enter your phone number"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="personal-location" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Location
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="personal-location"
|
|
||||||
type="text"
|
|
||||||
value={personalInfo.location}
|
|
||||||
onChange={(e) => setPersonalInfo({ ...personalInfo, location: e.target.value })}
|
|
||||||
placeholder="Enter your location"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-3 p-6 pt-4">
|
|
||||||
<ModalButton variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1 bg-white shadow-md"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
-105
@@ -1,105 +0,0 @@
|
|||||||
import { FC, useState } from 'react';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
|
|
||||||
interface ProfileBasicInfo {
|
|
||||||
name: string;
|
|
||||||
title: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ProfileBasicInfoModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: ProfileBasicInfo;
|
|
||||||
onSave: (value: ProfileBasicInfo) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ProfileBasicInfoModal: FC<ProfileBasicInfoModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
}) => {
|
|
||||||
const [profileInfo, setProfileInfo] = useState(initialValue);
|
|
||||||
|
|
||||||
const handleSave = () => {
|
|
||||||
onSave(profileInfo);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setProfileInfo(initialValue);
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
aria-label="Close modal"
|
|
||||||
type="button"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto">
|
|
||||||
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Profile</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="px-6 py-4 space-y-4">
|
|
||||||
<div>
|
|
||||||
<label htmlFor="profile-name" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Full Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="profile-name"
|
|
||||||
type="text"
|
|
||||||
value={profileInfo.name}
|
|
||||||
onChange={(e) => setProfileInfo({ ...profileInfo, name: e.target.value })}
|
|
||||||
placeholder="Enter your full name"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label htmlFor="profile-title" className="block text-sm font-medium text-gray-700 mb-2">
|
|
||||||
Professional Title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
id="profile-title"
|
|
||||||
type="text"
|
|
||||||
value={profileInfo.title}
|
|
||||||
onChange={(e) => setProfileInfo({ ...profileInfo, title: e.target.value })}
|
|
||||||
placeholder="Enter your professional title"
|
|
||||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#23A1EB] focus:border-[#23A1EB] outline-none transition-colors"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-3 p-6 pt-4">
|
|
||||||
<ModalButton
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton
|
|
||||||
variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
>
|
|
||||||
Simpan
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { FC, useState, useEffect } from 'react';
|
|
||||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
|
||||||
import { ModalButton } from '../buttons/modal-button';
|
|
||||||
import { InputField } from '@imphnen-frontend-service/ui/molecules';
|
|
||||||
|
|
||||||
interface Skill {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SkillsModalProps {
|
|
||||||
isOpen: boolean;
|
|
||||||
onClose: () => void;
|
|
||||||
initialValue: Skill[];
|
|
||||||
onSave: (value: Skill[]) => Promise<void>;
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const SkillsModal: FC<SkillsModalProps> = ({
|
|
||||||
isOpen,
|
|
||||||
onClose,
|
|
||||||
initialValue,
|
|
||||||
onSave,
|
|
||||||
isLoading = false,
|
|
||||||
}) => {
|
|
||||||
const [skills, setSkills] = useState<Skill[]>(initialValue);
|
|
||||||
const [newSkill, setNewSkill] = useState({ name: '' });
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setSkills(initialValue);
|
|
||||||
}, [initialValue]);
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
|
||||||
try {
|
|
||||||
await onSave(skills);
|
|
||||||
|
|
||||||
onClose();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Save failed:', error);
|
|
||||||
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleCancel = () => {
|
|
||||||
setSkills(initialValue);
|
|
||||||
setNewSkill({ name: '' });
|
|
||||||
onClose();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addSkill = () => {
|
|
||||||
if (newSkill.name.trim()) {
|
|
||||||
const skill: Skill = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
name: newSkill.name.trim(),
|
|
||||||
};
|
|
||||||
setSkills([...skills, skill]);
|
|
||||||
setNewSkill({ name: '' });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const removeSkill = (id: string) => {
|
|
||||||
setSkills(skills.filter(skill => skill.id !== id));
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isOpen) return null;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4">
|
|
||||||
|
|
||||||
<button
|
|
||||||
className="absolute inset-0 bg-black/20 backdrop-blur-sm"
|
|
||||||
onClick={handleCancel}
|
|
||||||
type="button"
|
|
||||||
aria-label="Close modal"
|
|
||||||
/>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="relative bg-white rounded-xl shadow-xl w-full max-w-5xl mx-auto max-h-[95vh] overflow-hidden">
|
|
||||||
|
|
||||||
<div className="p-6 pb-4 border-b border-gray-200">
|
|
||||||
<div className="flex">
|
|
||||||
<h2 className="text-xl font-semibold text-gray-900 px-3 py-1 bg-[#23A1EB]/10 rounded-md flex-1">Edit Skills</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="p-6 max-h-[60vh] overflow-y-auto">
|
|
||||||
|
|
||||||
<div className="mb-6 p-4 border border-gray-200 rounded-lg bg-gray-50">
|
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-3">Add New Skill</h3>
|
|
||||||
<div className="flex flex-col sm:flex-row gap-3">
|
|
||||||
<div className="flex-1">
|
|
||||||
<InputField
|
|
||||||
label="Skill Name"
|
|
||||||
value={newSkill.name}
|
|
||||||
onChange={(e) => setNewSkill({ ...newSkill, name: e.target.value })}
|
|
||||||
placeholder="e.g., React, JavaScript, etc."
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex sm:items-end">
|
|
||||||
<ModalButton
|
|
||||||
onClick={addSkill}
|
|
||||||
variant="primary"
|
|
||||||
size="sm"
|
|
||||||
className="w-full sm:w-auto"
|
|
||||||
>
|
|
||||||
<PlusOutlined />
|
|
||||||
<span className="sm:hidden ml-2">Add Skill</span>
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<h3 className="text-sm font-medium text-gray-700 mb-3">Current Skills</h3>
|
|
||||||
{skills.length === 0 ? (
|
|
||||||
<p className="text-gray-500 text-sm py-4">No skills added yet.</p>
|
|
||||||
) : (
|
|
||||||
<div className="space-y-2">
|
|
||||||
{skills.map((skill) => (
|
|
||||||
<div
|
|
||||||
key={skill.id}
|
|
||||||
className="flex items-center justify-between p-3 border border-gray-200 rounded-md bg-white"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<span className="font-medium text-gray-900">{skill.name}</span>
|
|
||||||
</div>
|
|
||||||
<ModalButton
|
|
||||||
onClick={() => removeSkill(skill.id)}
|
|
||||||
variant="danger"
|
|
||||||
size="sm"
|
|
||||||
className="text-red-500 hover:text-red-700"
|
|
||||||
>
|
|
||||||
<DeleteOutlined />
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex gap-3 p-6 pt-4">
|
|
||||||
<ModalButton variant="secondary"
|
|
||||||
onClick={handleCancel}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
Batal
|
|
||||||
</ModalButton>
|
|
||||||
<ModalButton variant="primary"
|
|
||||||
onClick={handleSave}
|
|
||||||
className="flex-1"
|
|
||||||
disabled={isLoading}
|
|
||||||
loading={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading ? 'Menyimpan...' : 'Simpan'}
|
|
||||||
</ModalButton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user