Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b150a7a3b | ||
|
|
aedb96ad33 | ||
|
|
78e06eebb6 | ||
|
|
65d43e935c | ||
|
|
b390b63aaf | ||
|
|
7dfe69659b | ||
|
|
4518a556ad | ||
|
|
93f741815f | ||
|
|
4564c32965 | ||
|
|
f3a19fa460 | ||
|
|
228f92e9ed | ||
|
|
dcd4ef88d6 |
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Backoffice
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-backoffice && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Dimentorin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-dimentorin && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: Deploy Gacha
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-gacha && git pull
|
||||||
|
fi
|
||||||
|
echo "VITE_API_URL=${{ secrets.VITE_API_URL }}" > ~/imphnen-frontend-service-gacha/apps/gacha/.env
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -2,6 +2,8 @@ name: Deploy Landing
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
paths:
|
paths:
|
||||||
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 11 KiB |
@@ -0,0 +1,52 @@
|
|||||||
|
import { Cell, Pie, PieChart, ResponsiveContainer, Tooltip } from "recharts"
|
||||||
|
import { PieLabelProps } from "recharts/types/polar/Pie"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
value: number
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "Active", value: 49, color: "#23A1EB" },
|
||||||
|
{ name: "Done", value: 24, color: "#81CBF8" },
|
||||||
|
{ name: "Canceled", value: 27, color: "#BCE1FB" },
|
||||||
|
]
|
||||||
|
|
||||||
|
const RADIAN = Math.PI / 180;
|
||||||
|
const renderCustomizedLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent }: PieLabelProps) => {
|
||||||
|
const radius = innerRadius + (outerRadius - innerRadius) * 0.5;
|
||||||
|
const x = cx + radius * Math.cos(-(midAngle ?? 0) * RADIAN);
|
||||||
|
const y = cy + radius * Math.sin(-(midAngle ?? 0) * RADIAN);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<text x={x} y={y} fill="white" textAnchor={x > cx ? 'start' : 'end'} dominantBaseline="central">
|
||||||
|
{`${((percent ?? 1) * 100).toFixed(0)}%`}
|
||||||
|
</text>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SessionStatusChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<PieChart width={500} height={320}>
|
||||||
|
<Pie
|
||||||
|
data={chartData}
|
||||||
|
dataKey="value"
|
||||||
|
nameKey="name"
|
||||||
|
cx="50%"
|
||||||
|
cy="50%"
|
||||||
|
innerRadius={40}
|
||||||
|
outerRadius={100}
|
||||||
|
labelLine={false}
|
||||||
|
label={renderCustomizedLabel}
|
||||||
|
>
|
||||||
|
{chartData.map((entry, index) => (
|
||||||
|
<Cell key={`cell-${index}`} fill={entry.color} />
|
||||||
|
))}
|
||||||
|
</Pie>
|
||||||
|
<Tooltip />
|
||||||
|
</PieChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { CartesianGrid, Legend, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts"
|
||||||
|
|
||||||
|
type ChartProps = {
|
||||||
|
name: string
|
||||||
|
activeUser: number
|
||||||
|
activeSession: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const chartData: ChartProps[] = [
|
||||||
|
{ name: "2014", activeUser: 0, activeSession: 0 },
|
||||||
|
{ name: "2015", activeUser: 15, activeSession: 25 },
|
||||||
|
{ name: "2016", activeUser: 30, activeSession: 40 },
|
||||||
|
{ name: "2017", activeUser: 45, activeSession: 55 },
|
||||||
|
{ name: "2018", activeUser: 60, activeSession: 70 },
|
||||||
|
{ name: "2019", activeUser: 75, activeSession: 85 },
|
||||||
|
{ name: "2020", activeUser: 90, activeSession: 95 },
|
||||||
|
{ name: "2021", activeUser: 85, activeSession: 80 },
|
||||||
|
{ name: "2022", activeUser: 95, activeSession: 90 },
|
||||||
|
{ name: "2023", activeUser: 100, activeSession: 100 },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const UserGrowthChart = () => {
|
||||||
|
return (
|
||||||
|
<ResponsiveContainer width="100%" height={320}>
|
||||||
|
<LineChart data={chartData} width={500} height={320} margin={{ left: -32 }}>
|
||||||
|
<CartesianGrid />
|
||||||
|
<XAxis dataKey="name" />
|
||||||
|
<YAxis tickCount={10} />
|
||||||
|
<Tooltip />
|
||||||
|
<Legend />
|
||||||
|
<Line dataKey="activeUser" stroke="#23A1EB" />
|
||||||
|
<Line dataKey="activeSession" stroke="#0877C1" />
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ReactElement } from "react";
|
||||||
|
import { UserGrowthChart } from "./_components/chart/user-growth";
|
||||||
|
import { SessionStatusChart } from "./_components/chart/session-status";
|
||||||
|
|
||||||
|
const Overview = () => {
|
||||||
|
return (
|
||||||
|
<div className="bg-white px-6 py-4 rounded-md shadow">
|
||||||
|
<h3 className="text-primary-500 text-p2 font-semibold mb-2.5">0</h3>
|
||||||
|
<p className="text-neutral-400 text-p3">Total Users</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-5">Overview</h1>
|
||||||
|
|
||||||
|
<div className="space-y-14">
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
||||||
|
Overview
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-5">
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => <Overview key={index} />}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="bg-white text-md text-neutral-900 mb-5 border-primary-200">
|
||||||
|
Trends & Analytics
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-7 gap-x-5">
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg col-span-5">
|
||||||
|
<div className="flex items-center justify-between mb-7">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700">User Growth</h2>
|
||||||
|
<div></div>
|
||||||
|
</div>
|
||||||
|
<UserGrowthChart />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg col-span-2">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-7">Session Status</h2>
|
||||||
|
<SessionStatusChart />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-5">
|
||||||
|
<div className="bg-white px-7 py-4 rounded-lg">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top 5 Mentors</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-label1 bg-primary-50 text-left rounded-full">
|
||||||
|
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
||||||
|
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
||||||
|
<th className="font-medium py-4 px-5 rounded-r-lg">Avg Rating</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<tr key={index} className="shadow rounded-lg">
|
||||||
|
<td className="py-4 px-5">{index + 1}</td>
|
||||||
|
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||||
|
<td className="py-4 px-5">4.9</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white px-6 py-4 rounded-lg">
|
||||||
|
<h2 className="font-semibold text-p3 text-neutral-700 mb-5">Top Booked Mentoring Topics</h2>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table className="w-full">
|
||||||
|
<thead>
|
||||||
|
<tr className="text-label1 bg-primary-50 text-left font-medium">
|
||||||
|
<th className="font-medium py-4 px-5 w-[10%] rounded-l-lg">No.</th>
|
||||||
|
<th className="font-medium py-4 px-5 w-3/5">Nama Lengkap</th>
|
||||||
|
<th className="font-medium py-4 px-5 rounded-r-lg">Total Sesi</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
<For data={Array.from({ length: 5 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<tr key={index} className="shadow rounded-lg">
|
||||||
|
<td className="py-4 px-5">{index + 1}</td>
|
||||||
|
<td className="py-4 px-5">Mursid Al-Catraz</td>
|
||||||
|
<td className="py-4 px-5">1000</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
MENTORING: 'Mentoring',
|
||||||
|
PLATFORM: 'Platform'
|
||||||
|
} as const
|
||||||
|
type Tabs = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
type FeedbackStatus = 'done' | 'todo';
|
||||||
|
|
||||||
|
interface FeedbackType {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
rating: number
|
||||||
|
status: FeedbackStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: FeedbackType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||||
|
email: 'fullname23@gmail.com',
|
||||||
|
rating: 4.5,
|
||||||
|
status: i % 2 === 0 ? 'done' : 'todo',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tabs>(TABS.MENTORING)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<FeedbackType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
accessorKey: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating',
|
||||||
|
header: 'Rating',
|
||||||
|
accessorKey: 'rating',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<FeedbackStatus, string> = {
|
||||||
|
done: 'bg-success-200 text-success-500',
|
||||||
|
todo: 'bg-primary-200 text-primary-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<FeedbackStatus, string> = {
|
||||||
|
done: 'Done',
|
||||||
|
todo: 'To Do',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Lihat Feedback
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<div className="mb-8 flex justify-between items-center">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700">Feedback</h1>
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama mentor/mentee"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<SearchOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ export const AppLayout: FC = (): ReactElement => {
|
|||||||
<div className="bg-primary-50 min-h-screen flex justify-center">
|
<div className="bg-primary-50 min-h-screen flex justify-center">
|
||||||
<div className="bg-primary-50 min-h-screen w-full flex">
|
<div className="bg-primary-50 min-h-screen w-full flex">
|
||||||
<BackofficeSidebar />
|
<BackofficeSidebar />
|
||||||
<div className="flex-1 overflow-auto lg:max-w-[1000px] 2xl:max-w-[1280px] mx-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
import { ArrowRightOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Accordion, Modal, ModalProps } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const ModalCreateRoadmap: FC<Omit<ModalProps, 'children'>> = ({ isOpen, onClose }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div className="overflow-y-auto max-h-[80vh]">
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Create Roadmaps
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-5 gap-6 mb-12">
|
||||||
|
<div className="col-span-3">
|
||||||
|
<label className={labelClass}>Prompt</label>
|
||||||
|
<Textarea
|
||||||
|
className="min-w-full w-full h-[calc(100%-2rem)]"
|
||||||
|
placeholder="Create a learning roadmap for (subject) at the (level) level. Include topics, estimated duration, and logical progression."
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2 space-y-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Roadmap Name</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Roadmap" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Roadmap Name</label>
|
||||||
|
<Select className="w-full">
|
||||||
|
<option value="pemula">Pemula</option>
|
||||||
|
<option value="menengah">Menengah</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Model</label>
|
||||||
|
<Select className="w-full">
|
||||||
|
<option value="gpt-3.5-turbo">GPT 3.5 Turbo</option>
|
||||||
|
<option value="gpt-4">GPT 4</option>
|
||||||
|
<option value="gpt-4-32k">GPT 4 32k</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-full">
|
||||||
|
<Button>
|
||||||
|
Generate Roadmap
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h2 className="text-neutral-600 text-p2 font-semibold mb-8">
|
||||||
|
Roadmap Preview
|
||||||
|
</h2>
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
<For data={Array.from({ length: 3 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<Accordion
|
||||||
|
key={index}
|
||||||
|
title={`Day ${index + 1}`}
|
||||||
|
description="Lorem ipsum dolor sit amet, consectetur adipiscing elit. In convallis tincidunt nisl, id consequat mi malesuada vel. Nulla facilisi. Nam in turpis ligula."
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end mt-12">
|
||||||
|
<Button type="button" className="flex items-center gap-5">
|
||||||
|
Submit Roadmap
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
import { DeleteOutlined, EditOutlined, PlusOutlined, SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { ModalCreateRoadmap } from "./_components/modal/create-roadmap";
|
||||||
|
|
||||||
|
type LearningStatus = 'active' | 'inactive'
|
||||||
|
|
||||||
|
type RoadmapType = {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
learningLevel: string
|
||||||
|
status: LearningStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: RoadmapType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i === 0 ? 'Ahmad Wijuana' : 'Anna Wiguana',
|
||||||
|
learningLevel: ['Pemula', 'Menengah'][Math.floor(Math.random() * 2)],
|
||||||
|
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): React.ReactElement {
|
||||||
|
const [openCreateModal, setOpenCreateModal] = useState(false)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<RoadmapType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
header: 'No',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Nama Roadmap',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'learningLevel',
|
||||||
|
header: 'Tingkat Belajar',
|
||||||
|
accessorKey: 'learningLevel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<LearningStatus, string> = {
|
||||||
|
inactive: 'bg-danger-200 text-danger-700',
|
||||||
|
active: 'bg-success-200 text-success-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<LearningStatus, string> = {
|
||||||
|
inactive: 'Inactive',
|
||||||
|
active: 'Active',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex gap-[8px]">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<EditOutlined /> Action
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<DeleteOutlined /> Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Content & Roadmap</h1>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex items-center justify-between mb-9">
|
||||||
|
<h2 className="text-p2 font-semibold text-neutral-600">AI Roadmaps</h2>
|
||||||
|
<Button type="button" variant="primary" className="flex items-center gap-2" onClick={() => setOpenCreateModal(true)}>
|
||||||
|
<PlusOutlined /> Buat Roadmap
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama roadmap"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<SearchOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Tingkat Belajar</option>
|
||||||
|
<option value="pemula">Pemula</option>
|
||||||
|
<option value="menengah">Menengah</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalCreateRoadmap isOpen={openCreateModal} onClose={() => setOpenCreateModal(false)} />
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const TOPICS = [
|
||||||
|
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
||||||
|
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
||||||
|
]
|
||||||
|
|
||||||
|
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||||
|
|
||||||
|
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||||
|
|
||||||
|
Saya ingin tahu secara khusus tentang ___.
|
||||||
|
1.Pertanyaan Anda
|
||||||
|
2. ...
|
||||||
|
3. ...`
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
type ModalProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalDetailSession: FC<ModalProps> = ({ open, setOpen }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="xl:max-w-[64rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Detail Sesi
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-9 px-9 py-7 border rounded-md gap-12 mb-10">
|
||||||
|
<div className="col-span-4 flex items-center gap-x-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentor</h2>
|
||||||
|
<div>
|
||||||
|
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
||||||
|
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Icon icon="ph:arrow-right" className="text-9xl text-primary-500" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="col-span-5 flex items-center gap-12">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-primary-500 mb-4">Mentee</h2>
|
||||||
|
<div>
|
||||||
|
<p className="text-p3 font-semibold mb-2.5">Muhammad Firdaus Oi Oi Oi, S.H., M.H.</p>
|
||||||
|
<p className="text-neutral-600">UI Designer at Oray orayan Studios</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-p2 font-semibold text-neutral-700 mb-4">Status</h2>
|
||||||
|
<div className="py-2 px-6 rounded-md text-center bg-success-200 text-success-500 font-semibold">
|
||||||
|
Finished
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p3 font-medium mb-2.5">Topics</h1>
|
||||||
|
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5 mb-8">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow font-medium"
|
||||||
|
>
|
||||||
|
<span>{item.icon} </span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-2.5 md:grid-cols-2 md:gap-5">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tanggal</label>
|
||||||
|
<Input type="date" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Waktu</label>
|
||||||
|
<Input type="time" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div className="relative md:col-span-full">
|
||||||
|
<label className={labelClass}>Lokasi</label>
|
||||||
|
<Select className="min-w-full w-full">
|
||||||
|
<option value="online">Online</option>
|
||||||
|
<option value="offline">Offline</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="md:col-span-full">
|
||||||
|
<label className={labelClass}>Pertanyaan Untuk Senpai</label>
|
||||||
|
<Textarea
|
||||||
|
className="min-w-full w-full h-40"
|
||||||
|
placeholder={placeholder}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react";
|
||||||
|
import { ModalDetailSession } from "./_components/modal/detail";
|
||||||
|
|
||||||
|
type SessionStatus = 'ongoing' | 'finished';
|
||||||
|
|
||||||
|
interface SessionType {
|
||||||
|
id: string
|
||||||
|
mentorName: string
|
||||||
|
menteeName: string
|
||||||
|
datetime: number
|
||||||
|
status: SessionStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: SessionType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: `DS-${i + 1}`,
|
||||||
|
mentorName: 'Ahmad Wijuana',
|
||||||
|
menteeName: 'Sofia Wijuana',
|
||||||
|
datetime: new Date().getTime(),
|
||||||
|
status: i % 2 === 0 ? 'ongoing' : 'finished',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [openDetail, setOpenDetail] = useState(false);
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<SessionType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'id',
|
||||||
|
header: 'ID Sesi',
|
||||||
|
accessorKey: 'id',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'mentorName',
|
||||||
|
header: 'Nama Mentor',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menteeName',
|
||||||
|
header: 'Nama Mentee',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'datetime',
|
||||||
|
header: 'Waktu',
|
||||||
|
accessorKey: 'datetime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<SessionStatus, string> = {
|
||||||
|
ongoing: 'bg-warning-200 text-warning-700',
|
||||||
|
finished: 'bg-success-200 text-success-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<SessionStatus, string> = {
|
||||||
|
ongoing: 'On Going',
|
||||||
|
finished: 'Finished',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-52") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setOpenDetail(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Cek Detail
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Session Management</h1>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<SearchOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="finished">Finished</option>
|
||||||
|
<option value="ongoing">On Going</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalDetailSession open={openDetail} setOpen={setOpenDetail} />
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { Button, Input, Select, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const GeneralSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">General Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<ToggleInput label="Mode Maintenance" />
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Platform Settings</h2>
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Bahasa</label>
|
||||||
|
<Select defaultValue="id" className="w-full">
|
||||||
|
<option value="id">Indonesia</option>
|
||||||
|
<option value="en">English</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Logo Platform</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Favicon</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Legal Settings</h2>
|
||||||
|
<div className="grid gap-x-8 gap-y-5 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Syarat & Ketentuan</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>URL Kebijakan Privasi</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { Button, Textarea, ToggleInput } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const NotificationSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Notification Settings</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-8 flex-wrap">
|
||||||
|
<ToggleInput label="Nyalakan Notifikasi Email" />
|
||||||
|
<ToggleInput label="Beritahu mentor ketika ada request" />
|
||||||
|
<ToggleInput label="Beritahu mentee untuk update sesi " />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mb-20">
|
||||||
|
<label className="text-neutral-800 font-medium inline-block mb-2 text-p3">
|
||||||
|
API Integrasi Notifikasi (URL)
|
||||||
|
</label>
|
||||||
|
<Textarea placeholder="Masukkan url API notifikasi yang akan digunakan" className="w-full h-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Button, Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const PaymentSettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Payment</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-x-8 gap-y-5 mb-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Integrasi Payment Gateway</label>
|
||||||
|
<Textarea className="min-w-full w-full h-20" placeholder="Durasi dalam menit" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Mata Uang</label>
|
||||||
|
<Select defaultValue="idr">
|
||||||
|
<option value="idr">Rupiah (IDR)</option>
|
||||||
|
<option value="usd">Dollar (USD)</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Harga sesi mentoring <span className="text-neutral-600">(default)</span></label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan harga sesi mentoring" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Tarif Komisi untuk Platform</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Masukkan persentase komisi" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 className="text-p3 font-semibold text-neutral-700 mb-5">Invoice</h2>
|
||||||
|
<div className="mb-32">
|
||||||
|
<label className={labelClass}>Masukkan Format Invoice</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" placeholder="InvoiceDimentorin.png" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-medium mb-1.5 inline-block md:text-xs md:mb-2 xl:text-p3')
|
||||||
|
|
||||||
|
export const SecuritySettings: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700 mb-8">Security Settings</h1>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-8 mb-20">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Durasi Session Timeout</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Durasi dalam menit" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Auto logout setelah X menit tidak aktif</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Blokir Setelah Upaya Gagal</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="x kali perobaan login" />
|
||||||
|
<p className="text-[10px] text-neutral-800">Misal: 5 kali salah login = lock akun</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-5">
|
||||||
|
<Button type="button" variant="bordered">
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button">
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
import { DeleteOutlined, UserSwitchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
|
||||||
|
type UserRolesPermissionType = {
|
||||||
|
id: number
|
||||||
|
role: string
|
||||||
|
totalUser: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: UserRolesPermissionType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
role: ['Admin', 'Super Admin', 'Mentee', 'Mentor'][Math.floor(Math.random() * 4)],
|
||||||
|
totalUser: 10
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const UserRolesPermission: FC = () => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<UserRolesPermissionType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'role',
|
||||||
|
header: 'Role',
|
||||||
|
accessorKey: 'role',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'totalUser',
|
||||||
|
header: 'Total User',
|
||||||
|
accessorKey: 'totalUser',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-96") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<UserSwitchOutlined className="text-[16px]" /> Manage Permissions
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<DeleteOutlined className="text-[16px]" /> Delete Role
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8 flex items-center justify-between">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-700">User Roles & Permissions</h1>
|
||||||
|
<Button type="button">
|
||||||
|
Add Rols
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="bg-white shadow p-8 rounded-lg">
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { BackofficeWrapper } from "@imphnen-frontend-service/ui/organisms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { useState } from "react"
|
||||||
|
import { GeneralSettings } from "./_components/general"
|
||||||
|
import { UserRolesPermission } from "./_components/user-roles-permission"
|
||||||
|
import { NotificationSettings } from "./_components/notification"
|
||||||
|
import { SecuritySettings } from "./_components/security"
|
||||||
|
import { PaymentSettings } from "./_components/payment"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
general: "General Settings",
|
||||||
|
userRolePermissions: "User Roles & Permissions",
|
||||||
|
notification: "Notification Settings",
|
||||||
|
security: "Security",
|
||||||
|
payment: "Payment",
|
||||||
|
} as const
|
||||||
|
type Tabs = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
export default function Components(): React.ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Tabs>(TABS.general)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700 mb-8">Settings</h1>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-x-8">
|
||||||
|
<div className="w-64 bg-white p-2.5 shadow space-y-2 rounded-md">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<button
|
||||||
|
key={tab}
|
||||||
|
className={cn(
|
||||||
|
"px-4 py-3 w-full text-left font-medium rounded-md text-neutral-400 cursor-pointer select-none hover:bg-primary-100",
|
||||||
|
activeTab === tab && "bg-primary-500 text-white hover:bg-primary-600"
|
||||||
|
)}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
<div className="bg-white px-8 py-6 shadow space-y-2 rounded-md flex-1">
|
||||||
|
{activeTab === TABS.general && <GeneralSettings />}
|
||||||
|
{activeTab === TABS.userRolePermissions && <UserRolesPermission />}
|
||||||
|
{activeTab === TABS.notification && <NotificationSettings />}
|
||||||
|
{activeTab === TABS.security && <SecuritySettings />}
|
||||||
|
{activeTab === TABS.payment && <PaymentSettings />}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
type ModalDeleteProps = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
hadnleDelete?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalDelete: FC<ModalDeleteProps> = ({ open, setOpen, hadnleDelete }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className="min-w-[400px] bg-primary-50 rounded-lg p-[40px] flex flex-col gap-8 text-center"
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<Modal.Header className="gap-8">
|
||||||
|
<img
|
||||||
|
src="/chibi-delete.webp"
|
||||||
|
alt="Delete item?"
|
||||||
|
width={148}
|
||||||
|
className="self-center"
|
||||||
|
/>
|
||||||
|
<div className="text-center">
|
||||||
|
<h2 className="text-p1 font-semibold text-danger-500 mb-3">
|
||||||
|
Hapus Akun
|
||||||
|
</h2>
|
||||||
|
<p className="text-p3 text-neutral-400">
|
||||||
|
Apakah kamu yakin untuk menghapus akun ini?
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Modal.Header>
|
||||||
|
<Modal.Content className="flex gap-4">
|
||||||
|
<Button
|
||||||
|
variant="bordered"
|
||||||
|
size="lg"
|
||||||
|
className="w-full border-danger-500 text-danger-500 hover:bg-danger-50 hover:text-danger-600 hover:border-danger-600"
|
||||||
|
onClick={() => setOpen(false)}
|
||||||
|
>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
onClick={() => hadnleDelete?.()}
|
||||||
|
>
|
||||||
|
Hapus Akun
|
||||||
|
</Button>
|
||||||
|
</Modal.Content>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { Button, Input } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useState } from "react";
|
||||||
|
import { ModalDelete } from "../delete";
|
||||||
|
import { ModalProps } from "../type";
|
||||||
|
import { ModalSuspendOrBan } from "../suspend-or-ban";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
export const AccountProfile: FC<ModalProps> = ({ setOpen }) => {
|
||||||
|
const [openDelete, setOpenDelete] = useState(false)
|
||||||
|
const [openSuspendOrBan, setOpenSuspendOrBan] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-x-8 mb-8">
|
||||||
|
<div className="size-[100px] rounded-full overflow-hidden">
|
||||||
|
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button type="button" size="sm" variant="bordered" className="mb-4">
|
||||||
|
Upload Foto
|
||||||
|
</Button>
|
||||||
|
<p className="text-neutral-400 font-medium">
|
||||||
|
Setidaknya rekomendasi ukuran 240x240 px. <br />
|
||||||
|
.jpg, .jpeg, .png diperbolehkan
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="mb-7 text-p2 font-semibold">Informasi Pribadi</h1>
|
||||||
|
<div className="grid grid-cols-2 gap-8">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Depan</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Depan" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nama Belakang</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="Nama Belakang" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Email</label>
|
||||||
|
<Input type="email" className="min-w-full w-full" placeholder="Email" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nomor Telepon</label>
|
||||||
|
<Input type="text" className="min-w-full w-full" placeholder="+62 81234567890" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h1 className="mb-7 text-p2 font-semibold">Status Akun</h1>
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="bordered"
|
||||||
|
className="border-danger-500 text-danger-500"
|
||||||
|
onClick={() => setOpenSuspendOrBan(true)}
|
||||||
|
>
|
||||||
|
Suspend/Ban
|
||||||
|
</Button>
|
||||||
|
<Button type="button" size="sm" variant="danger" onClick={() => setOpenDelete(true)}>
|
||||||
|
Delete Akun
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-x-5">
|
||||||
|
<Button type="button" variant="bordered" onClick={() => setOpen(false)}>
|
||||||
|
Batal
|
||||||
|
</Button>
|
||||||
|
<Button type="button" disabled>
|
||||||
|
Simpan
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ModalDelete open={openDelete} setOpen={setOpenDelete} />
|
||||||
|
<ModalSuspendOrBan open={openSuspendOrBan} setOpen={setOpenSuspendOrBan} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons"
|
||||||
|
import { Input, Select } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { DataTable } from "@imphnen-frontend-service/ui/organisms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table"
|
||||||
|
import { FC, useState } from "react"
|
||||||
|
import dayjs from "dayjs"
|
||||||
|
|
||||||
|
interface ActivityLogType {
|
||||||
|
id: number
|
||||||
|
date: string
|
||||||
|
menu: string
|
||||||
|
activity: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: ActivityLogType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
date: new Date().toISOString(),
|
||||||
|
menu: 'Mentoring',
|
||||||
|
activity: 'Mentoring Session',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export const ActivityLog: FC = () => {
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
})
|
||||||
|
|
||||||
|
const columns: ColumnDef<ActivityLogType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-16") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'date',
|
||||||
|
header: 'Waktu',
|
||||||
|
accessorKey: 'date',
|
||||||
|
cell: (info) => dayjs(info.row.original.date).format('DD MMMM YYYY, HH:mm WIB'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'menu',
|
||||||
|
header: 'Menu',
|
||||||
|
accessorKey: 'menu',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'activity',
|
||||||
|
header: 'Activity',
|
||||||
|
accessorKey: 'activity',
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-8 shadow rounded-lg">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-9">
|
||||||
|
<div className="relative w-1/2">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari aktivitas"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<SearchOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="w-1/4">
|
||||||
|
<Input type="date" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Menu</option>
|
||||||
|
<option value="profle">Profile</option>
|
||||||
|
<option value="profle-2">Profile</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { Icon } from "@iconify/react"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const SOCIAL_LINKS = [
|
||||||
|
{ icon: <Icon icon="mdi:linkedin" className="text-2xl" />, url: "https://linkedin.com", label: "LinkedIn" },
|
||||||
|
{ icon: <Icon icon="mdi:github" className="text-2xl" />, url: "https://github.com", label: "Github" },
|
||||||
|
{ icon: <Icon icon="mingcute:meta-line" className="text-2xl" />, url: "https://facebook.com", label: "Facebook (Meta)" },
|
||||||
|
{ icon: <Icon icon="mdi:stack-overflow" className="text-2xl" />, url: "https://stackoverflow.com", label: "Stack Overflow" }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const DetailProfile: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="shadow rounded-lg p-8 mb-7">
|
||||||
|
<div className="flex justify-between items-start mb-6">
|
||||||
|
<div className="flex items-center gap-x-6">
|
||||||
|
<div className="size-[54px] rounded-full overflow-hidden">
|
||||||
|
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800">Muhammad Firdaus Oiwobo</h1>
|
||||||
|
<p className="text-p3 font-medium text-neutral-600">Mentor</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button type="button" variant="text" className="bg-primary-100">
|
||||||
|
Actively Seeking Job
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-5">
|
||||||
|
<For data={SOCIAL_LINKS}>
|
||||||
|
{({ icon, url, label }) => (
|
||||||
|
<a key={url} href={url} target="_blank" rel="noreferrer">
|
||||||
|
<Button type="button" size="sm" className="flex items-center gap-x-2">
|
||||||
|
{icon}
|
||||||
|
{label}
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-7">
|
||||||
|
<div className="text-pretty px-8 py-10 rounded-lg shadow h-max">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Description</h1>
|
||||||
|
<p className="text-p3 font-medium text-neutral-600">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="px-8 py-10 rounded-lg shadow h-max">
|
||||||
|
<h1 className="text-p2 font-semibold text-neutral-800 mb-6">Personal Informations</h1>
|
||||||
|
<div className="grid gap-6">
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="ic:outline-mail" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">rzalaxib23@gmail.com</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Email Address</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="cil:phone" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">+62 888 8888 8888</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Phone Number</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-x-5">
|
||||||
|
<div className="bg-primary-50 rounded-full p-2.5 flex items-center justify-center text-primary-500">
|
||||||
|
<Icon icon="ion:location-outline" className="text-3xl" />
|
||||||
|
</div>
|
||||||
|
<div className="text-p3">
|
||||||
|
<p className="text-neutral-800 font-semibold">Jl. Mergosari, Kec. Suryakencana, Banjaran</p>
|
||||||
|
<p className="text-neutral-600 font-medium">Location</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules"
|
||||||
|
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC, useEffect, useState } from "react"
|
||||||
|
import { AccountProfile } from "./account-profile"
|
||||||
|
import { DetailProfile } from "./detail-profile"
|
||||||
|
import { ActivityLog } from "./activity-log"
|
||||||
|
import { ModalDetailUserProps } from "./type"
|
||||||
|
|
||||||
|
const TABS = {
|
||||||
|
account: 'account profile',
|
||||||
|
detail: 'detail profile',
|
||||||
|
activity: 'activity logs',
|
||||||
|
} as const
|
||||||
|
type TabType = typeof TABS[keyof typeof TABS]
|
||||||
|
|
||||||
|
export const ModalDetailUser: FC<ModalDetailUserProps> = ({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}) => {
|
||||||
|
const [activeTab, setActiveTab] = useState<TabType>(TABS.account)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!open) setActiveTab(TABS.account)
|
||||||
|
}, [open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="xl:max-w-[84rem] bg-white px-10 py-9"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Detail - Muhammad Firdaus Oiwobo
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md w-max mb-8">
|
||||||
|
<For data={Object.values(TABS)}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Show condition={activeTab === TABS.account}>
|
||||||
|
<AccountProfile open={open} setOpen={setOpen} />
|
||||||
|
</Show>
|
||||||
|
<Show condition={activeTab === TABS.detail}>
|
||||||
|
<DetailProfile />
|
||||||
|
</Show>
|
||||||
|
<Show condition={activeTab === TABS.activity}>
|
||||||
|
<ActivityLog />
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { ModalProps } from "../type";
|
||||||
|
|
||||||
|
export interface ModalDetailUserProps extends ModalProps {
|
||||||
|
userId?: number | null
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { FC } from "react";
|
||||||
|
import { ModalProps } from "../type";
|
||||||
|
import { Modal } from "@imphnen-frontend-service/ui/molecules";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { Button, Select, Textarea } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
export const ModalSuspendOrBan: FC<ModalProps> = ({ open, setOpen }) => {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={open}
|
||||||
|
onClose={() => setOpen(false)}
|
||||||
|
className="bg-white px-10 py-9 xl:max-w-[32rem]"
|
||||||
|
closeButtonClassName="hidden"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 className="bg-primary-50 px-6 py-3 text-neutral-800 text-p2 font-semibold mb-8">
|
||||||
|
Suspend/Ban
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Suspend/Ban</label>
|
||||||
|
<Select defaultValue="suspend" className="w-full">
|
||||||
|
<option value="suspend">Suspend</option>
|
||||||
|
<option value="banned">Banned</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Alasan</label>
|
||||||
|
<Textarea placeholder="Masukkan alasan suspend/ban" className="w-full h-40" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Button type="button" className="w-full">
|
||||||
|
Selesai
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface ModalProps {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
import { SearchOutlined } from "@ant-design/icons";
|
||||||
|
import { Button, Input, Select } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { BackofficeWrapper, DataTable } from "@imphnen-frontend-service/ui/organisms";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { ColumnDef, getCoreRowModel, getPaginationRowModel, PaginationState, RowSelectionState, useReactTable } from "@tanstack/react-table";
|
||||||
|
import { ReactElement, useState } from "react";
|
||||||
|
import { ModalDetailUser } from "./_components/modal/detail";
|
||||||
|
|
||||||
|
type UserStatus = 'active' | 'inactive';
|
||||||
|
|
||||||
|
interface UserType {
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
email: string
|
||||||
|
rating: number
|
||||||
|
status: UserStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockData: UserType[] = Array.from({ length: 90 }, (_, i) => ({
|
||||||
|
id: i + 1,
|
||||||
|
name: i % 3 === 0 ? 'Ahmad Wijuana' : 'Sofia Wijuana',
|
||||||
|
email: 'fullname23@gmail.com',
|
||||||
|
rating: 4.5,
|
||||||
|
status: i % 2 === 0 ? 'active' : 'inactive',
|
||||||
|
}))
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const TABS = ['mentor', 'mentee'] as const
|
||||||
|
const [activeTab, setActiveTab] = useState<'mentor' | 'mentee'>('mentor')
|
||||||
|
const [showDetail, setShowDetail] = useState(false)
|
||||||
|
const [selectedUserId, setSelectedUserId] = useState<number | null>(null)
|
||||||
|
|
||||||
|
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 9,
|
||||||
|
});
|
||||||
|
|
||||||
|
const columns: ColumnDef<UserType>[] = [
|
||||||
|
{
|
||||||
|
id: 'select',
|
||||||
|
meta: { cellClassName: cn("w-20") },
|
||||||
|
header: ({ table }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={table.getIsAllRowsSelected()}
|
||||||
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
className="rounded"
|
||||||
|
checked={row.getIsSelected()}
|
||||||
|
onChange={row.getToggleSelectedHandler()}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Name',
|
||||||
|
accessorKey: 'name',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'email',
|
||||||
|
header: 'Email',
|
||||||
|
accessorKey: 'email',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'rating',
|
||||||
|
header: 'Rating',
|
||||||
|
accessorKey: 'rating',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'status',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'status',
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const status = row.original.status;
|
||||||
|
const statusColors: Record<UserStatus, string> = {
|
||||||
|
active: 'bg-success-200 text-success-500',
|
||||||
|
inactive: 'bg-danger-200 text-danger-500',
|
||||||
|
};
|
||||||
|
const statusText: Record<UserStatus, string> = {
|
||||||
|
active: 'Active',
|
||||||
|
inactive: 'Inactive',
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`py-2 px-4 rounded-md text-center ${statusColors[status]}`}
|
||||||
|
>
|
||||||
|
{statusText[status]}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: 'Action',
|
||||||
|
meta: { cellClassName: cn("w-72") },
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setSelectedUserId(row.original.id);
|
||||||
|
setShowDetail(true);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-2 w-max"
|
||||||
|
>
|
||||||
|
<SearchOutlined className="text-[16px]" /> Lihat Detail & Action
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: mockData,
|
||||||
|
columns,
|
||||||
|
state: {
|
||||||
|
pagination,
|
||||||
|
rowSelection,
|
||||||
|
},
|
||||||
|
enableRowSelection: true,
|
||||||
|
onRowSelectionChange: setRowSelection,
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
pageCount: Math.ceil(mockData.length / pagination.pageSize),
|
||||||
|
manualPagination: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<BackofficeWrapper title="Dimentorin.dev">
|
||||||
|
<div className="mb-8 flex justify-between items-center">
|
||||||
|
<h1 className="text-p1 font-semibold text-neutral-700">User Management</h1>
|
||||||
|
<div className="flex gap-2 bg-primary-100 p-1.5 rounded-md">
|
||||||
|
<For data={TABS}>
|
||||||
|
{(tab) => (
|
||||||
|
<Button
|
||||||
|
key={tab}
|
||||||
|
variant="text"
|
||||||
|
className={cn("px-3 py-2 capitalize", activeTab === tab && "bg-white")}
|
||||||
|
onClick={() => setActiveTab(tab)}
|
||||||
|
>
|
||||||
|
{tab}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section className="flex flex-col gap-6 p-8 bg-white rounded-md">
|
||||||
|
<div className="flex justify-between items-center gap-5 mb-2">
|
||||||
|
<div className="relative w-full">
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama lengkap"
|
||||||
|
className="pl-12 w-full max-h-full"
|
||||||
|
/>
|
||||||
|
<div className="absolute left-3 top-1/2 transform -translate-y-1/2 text-[16px]">
|
||||||
|
<SearchOutlined />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Rating</option>
|
||||||
|
<option value="4.5">4.5</option>
|
||||||
|
<option value="5">5</option>
|
||||||
|
</Select>
|
||||||
|
<Select>
|
||||||
|
<option selected disabled>Status</option>
|
||||||
|
<option value="active">Active</option>
|
||||||
|
<option value="inactive">Inactive</option>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DataTable data={mockData} columns={columns} table={table} />
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ModalDetailUser
|
||||||
|
open={showDetail}
|
||||||
|
setOpen={setShowDetail}
|
||||||
|
userId={selectedUserId}
|
||||||
|
/>
|
||||||
|
</BackofficeWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 111 KiB |
|
After Width: | Height: | Size: 30 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 33 KiB |
|
After Width: | Height: | Size: 9.4 KiB |
|
After Width: | Height: | Size: 3.5 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 28 KiB |
@@ -3,7 +3,7 @@ import { motion } from 'framer-motion'
|
|||||||
|
|
||||||
export function HeroSection() {
|
export function HeroSection() {
|
||||||
return (
|
return (
|
||||||
<section className="relative w-full py-20 md:py-28 lg:py-32">
|
<section className="relative w-full z-0 py-20 md:py-28 lg:py-32">
|
||||||
{/* Background gradients and shapes */}
|
{/* Background gradients and shapes */}
|
||||||
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
<div className="absolute inset-0 -z-10 overflow-x-clip hidden md:block">
|
||||||
<motion.div
|
<motion.div
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { CloseOutlined, MenuOutlined } from "@ant-design/icons";
|
|||||||
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
import { cn, For, Show } from "@imphnen-frontend-service/utils";
|
||||||
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion";
|
||||||
import { FC, useState } from "react";
|
import { FC, useMemo, useState } from "react";
|
||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link, NavLink, useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const MENUS: { label: string; href: string }[] = [
|
const MENUS: { label: string; href: string }[] = [
|
||||||
{ label: "Home", href: "/" },
|
{ label: "Home", href: "/" },
|
||||||
@@ -30,8 +30,15 @@ export const Header: FC = () => {
|
|||||||
else setShow(true)
|
else setShow(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isActive = useMemo(() => {
|
||||||
|
return (path: string) => {
|
||||||
|
if (path === '/' && location.pathname !== '/') return false
|
||||||
|
return location.pathname.includes(path)
|
||||||
|
}
|
||||||
|
}, [location.pathname]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky overflow-hidden">
|
<div className={cn("w-full px-8 pt-8 top-0 z-50 md:px-[60px] md:pt-[60px] lg:px-20 sticky", !show && "overflow-hidden")}>
|
||||||
<motion.header
|
<motion.header
|
||||||
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
className="bg-white shadow-lg rounded-lg h-12 flex justify-between w-full max-w-7xl xl:mx-auto md:h-[60px] lg:h-[71px]"
|
||||||
aria-roledescription="nav"
|
aria-roledescription="nav"
|
||||||
@@ -63,18 +70,22 @@ export const Header: FC = () => {
|
|||||||
variant="text"
|
variant="text"
|
||||||
className={cn(
|
className={cn(
|
||||||
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
"px-10 py-1.5 text-neutral-300 hover:text-neutral-400 lg:px-2.5 lg:py-2",
|
||||||
location.pathname === menu.href && "text-primary-500 hover:text-primary-500"
|
isActive(menu.href) && "text-primary-500 hover:text-primary-500"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Link to={menu.href}>{menu.label}</Link>
|
<NavLink to={menu.href}>{menu.label}</NavLink>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</For>
|
</For>
|
||||||
<Button type="button" className="px-12 py-1 md:hidden">Login</Button>
|
<Link to="/auth/login">
|
||||||
|
<Button type="button" className="px-12 py-1 lg:hidden">Login</Button>
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
<Link to="/auth/login">
|
||||||
|
<Button type="button" className="px-5 py-2 hidden lg:block">Login</Button>
|
||||||
|
</Link>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="text"
|
variant="text"
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
|
||||||
|
export default function DetailArticle() {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<motion.section
|
||||||
|
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.5, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className="w-full max-w-[900px] mx-auto mb-8 md:mb-[60px]">
|
||||||
|
<div className="mb-10 flex items-center justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||||
|
>
|
||||||
|
<Icon icon="material-symbols:arrow-back-rounded" />
|
||||||
|
<span>Back</span>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
className="flex gap-x-2 text-[10px] px-2.5 py-2 md:text-[15px]"
|
||||||
|
>
|
||||||
|
<span>Read Next</span>
|
||||||
|
<Icon icon="material-symbols:arrow-forward-rounded" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center leading-tight">
|
||||||
|
<h1
|
||||||
|
className="text-[23px] text-neutral-800 font-semibold mb-6 md:text-[37px] md:mb-10 xl:text-[46px]"
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</h1>
|
||||||
|
<p
|
||||||
|
className="text-[15px] font-semibold text-neutral-800 mb-2 md:text-[19px] md:mb-1 xl:text-[23px] xl:mb-2"
|
||||||
|
>
|
||||||
|
Writer’s Name
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-600 mb-6 md:text-[15px] xl:text-[19px]">
|
||||||
|
14 March 2025
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="text-[10px] text-primary-600 font-medium bg-primary-200 px-3 py-1 rounded-4xl w-max mx-auto md:text-xs xl:text-[15px]"
|
||||||
|
>
|
||||||
|
5 min read
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full max-w-[900px] mx-auto">
|
||||||
|
<div className="mb-8 w-full h-auto aspect-16/10 overflow-hidden md:aspect-31/16 md:mb-[60px] xl:aspect-45/26">
|
||||||
|
<img src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp" alt="Cover Article" className="size-full object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"text-balance text-[15px] font-medium text-neutral-600 space-y-8",
|
||||||
|
"md:space-y-12 md:text-[19px] xl:text-[23px] xl:max-w-[780px] xl:mx-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<h2 className="text-[19px] text-neutral-800 font-semibold leading-tight md:text-[23px] xl:text-[29px]">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus.
|
||||||
|
</h2>
|
||||||
|
<p className="leading-tight">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<img src="/image/code-article.webp" alt="Code" className="w-full h-auto mb-12" />
|
||||||
|
|
||||||
|
<p className="leading-tight">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
<p className="leading-tight">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { Link } from "react-router-dom"
|
||||||
|
|
||||||
|
export type ArticleCardProps = {
|
||||||
|
variant?: "default" | "recent" | "featured"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ArticleCard: FC<ArticleCardProps> = ({ variant = 'default' }) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
variant === "recent" && "md:grid md:grid-cols-2 md:gap-7 xl:flex xl:items-center",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-full h-auto aspect-16/10 overflow-hidden mb-8 md:h-60 md:w-full xl:aspect-auto",
|
||||||
|
variant === "featured" && "md:h-[300px]",
|
||||||
|
variant === "recent" && "md:h-auto md:w-full md:aspect-15/7 md:mb-0 xl:aspect-square xl:w-[162px] xl:h-auto",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/image/59de8a682dfbe42455613ea07477ff3e30532c1f.webp"
|
||||||
|
alt="Cover's Article"
|
||||||
|
className={cn("w-full object-cover", variant === "recent" && "h-full")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cn("leading-tight", variant === "recent" && "xl:flex-1")}>
|
||||||
|
<div className="w-max bg-primary-200 text-primary-600 px-3 py-1 text-xs font-medium rounded-4xl mb-4 xl:mb-[15px]">
|
||||||
|
5 min read
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
to="/articles/detail"
|
||||||
|
className={cn(
|
||||||
|
"text-[15px] text-neutral-800 font-semibold mb-4 line-clamp-2 md:text-[19px] xl:mb-[23px]",
|
||||||
|
variant === "featured" && "text-[19px] md:text-[23px]",
|
||||||
|
variant === "recent" && "mb-7 xl:text-[27px]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
||||||
|
</Link>
|
||||||
|
<Show condition={variant !== "recent"}>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"text-xs text-neutral-600 mb-6 md:text-[15px] md:mb-8 xl:text-[19px]",
|
||||||
|
variant === "featured" && "text-[15px] md:text-[19px] md:mb-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris.
|
||||||
|
</p>
|
||||||
|
</Show>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"flex justify-between items-center text-xs text-neutral-600 md:text-[15px] xl:text-[19px]",
|
||||||
|
variant === "default" && "mb-4",
|
||||||
|
variant === "featured" && "text-[15px] md:text-[19px] xl:text-base",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<p>Writer’s Name</p>
|
||||||
|
<p>14 March 2025</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show condition={variant === "default"}>
|
||||||
|
<Button type="button" size="sm" className="px-2.5 py-2">
|
||||||
|
Baca Artikel
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { ReactElement, useRef, useState } from "react";
|
||||||
|
import { ArticleCard } from "./_components/card/article";
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
const CATEGORIES = ['UI/UX Design', 'Software/Web Dev', 'Data & AI', 'Cloud & DevOps', 'Cybersecurity', 'IT & Network', 'Project Management', 'QA & Testing'] as const
|
||||||
|
type Category = typeof CATEGORIES[number]
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
const [activeTab, setActiveTab] = useState<Category>('UI/UX Design')
|
||||||
|
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main ref={ref}>
|
||||||
|
<motion.section
|
||||||
|
className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<div className="max-w-7xl mx-auto mb-8 md:mb-20">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[23px] font-semibold text-neutral-800 mb-9 md:text-[29px] xl:text-[46px] xl:font-bold"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Featuring Articles
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<div className="grid gap-12 xl:grid-cols-2">
|
||||||
|
<motion.div variants={childVariants}>
|
||||||
|
<ArticleCard variant="featured" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<motion.h1
|
||||||
|
className="text-[19px] text-neutral-800 font-semibold mb-10 md:text-[29px] xl:text-[37px] xl:mb-12"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Recent Articles
|
||||||
|
</motion.h1>
|
||||||
|
<div className="grid gap-10">
|
||||||
|
<For data={Array.from({ length: 3 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<motion.div key={index} variants={childVariants}>
|
||||||
|
<ArticleCard variant="recent" />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[19px] text-neutral-800 font-semibold mb-4 md:text-[23px] xl:text-[29px]"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Category
|
||||||
|
</motion.h1>
|
||||||
|
<div className="scrollbar-hide mb-8 w-full overflow-auto mx-auto">
|
||||||
|
<motion.div className="min-w-max w-auto flex gap-x-3" variants={childVariants}>
|
||||||
|
<For data={CATEGORIES}>
|
||||||
|
{(category) => (
|
||||||
|
<Button
|
||||||
|
key={category}
|
||||||
|
type="button"
|
||||||
|
variant="text"
|
||||||
|
onClick={() => setActiveTab(category)}
|
||||||
|
className={cn(
|
||||||
|
"relative text-[10px] text-neutral-400 font-medium px-3 py-2 rounded-4xl md:text-xs md:font-semibold xl:text-[15px]",
|
||||||
|
"before:w-0 before:absolute before:h-0.5 before:mx-auto before:inset-x-0 before:bg-primary-500 hover:before:w-full before:bottom-0 before:left-0 before:transition-all before:duration-300",
|
||||||
|
activeTab === category && "text-primary-500 before:w-full"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{category}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-8 xl:grid-cols-3">
|
||||||
|
<For data={Array.from({ length: 3 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<motion.div key={index} variants={childVariants}>
|
||||||
|
<ArticleCard />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ export default function Layout() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen flex-col bg-primary-50">
|
<div className="flex min-h-screen flex-col bg-primary-50">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 z-0">
|
<main className="flex-1">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
|
|||||||
@@ -0,0 +1,163 @@
|
|||||||
|
import { CloseOutlined } from "@ant-design/icons"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { AnimatePresence, motion } from "framer-motion"
|
||||||
|
import { FC, useCallback, useEffect, useState } from "react"
|
||||||
|
import { TopicStep } from "./steps/topic"
|
||||||
|
import { cn, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { ScheduleStep } from "./steps/schedule"
|
||||||
|
import { ProfileStep } from "./steps/profile"
|
||||||
|
import { QrisPaymentStep } from "./steps/qris-payement"
|
||||||
|
import { VAPaymentStep } from "./steps/va-payment"
|
||||||
|
import { SuccessStep } from "./steps/success"
|
||||||
|
import { PaymentStep } from "./steps/payment"
|
||||||
|
|
||||||
|
const STEPS = ['topic', 'schedule', 'profile', 'payment', 'qr-payment', 'va-payment', 'success'] as const
|
||||||
|
type Step = typeof STEPS[number]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
open: boolean
|
||||||
|
setOpen: (open: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppointmentModal: FC<Props> = ({ open, setOpen }) => {
|
||||||
|
const [step, setStep] = useState<Step>('topic')
|
||||||
|
const [selectedTopics, setSelectedTopics] = useState<number[]>([])
|
||||||
|
|
||||||
|
const handleStep = (action: 'next' | 'prev') => {
|
||||||
|
if (action === 'next' && step === 'success') {
|
||||||
|
setOpen(false)
|
||||||
|
} else if (action === 'next') {
|
||||||
|
setStep(STEPS[STEPS.indexOf(step) + 1])
|
||||||
|
} else if (action === 'prev' && step !== 'topic') {
|
||||||
|
setStep(STEPS[STEPS.indexOf(step) - 1])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEscapeKey = useCallback(
|
||||||
|
(e: KeyboardEvent) => {
|
||||||
|
if (e.key === "Escape" && open) setOpen(false)
|
||||||
|
},
|
||||||
|
[open, setOpen]
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
document.body.style.overflow = "hidden"
|
||||||
|
window.addEventListener("keydown", handleEscapeKey)
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.body.style.overflow = ""
|
||||||
|
window.removeEventListener("keydown", handleEscapeKey)
|
||||||
|
}
|
||||||
|
}, [handleEscapeKey, open])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence>
|
||||||
|
{open && (
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0, top: "50%" }}
|
||||||
|
animate={{ opacity: 1, top: 0 }}
|
||||||
|
exit={{ opacity: 0, top: "50%" }}
|
||||||
|
className="fixed inset-0 z-100 flex items-center justify-center"
|
||||||
|
>
|
||||||
|
<div className="fixed inset-0 bg-primary-900/30" onClick={() => setOpen(false)} />
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"relative bg-primary-50 px-5 py-6 w-full max-w-[280px] mx-4 rounded-lg md:max-w-[627px] md:py-10 md:px-[60px] md:rounded-xl xl:max-w-[668px]",
|
||||||
|
step === 'success' && 'md:max-w-[400px] md:p-10 xl:max-w-[400px]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Show condition={step !== 'success'}>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
className="absolute top-3 right-3 bg-primary-200 p-1 shadow md:bg-white md:p-2 md:top-8 md:right-10"
|
||||||
|
>
|
||||||
|
<CloseOutlined className="md:text-lg" />
|
||||||
|
</Button>
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{step !== 'success' && (
|
||||||
|
<img
|
||||||
|
src="/logos/simple.svg"
|
||||||
|
alt="IMPHNEN Logo"
|
||||||
|
className="mb-6 h-8 w-auto mx-auto md:h-12 xl:h-[60px]"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="scrollbar-hide max-h-[50dvh] overflow-y-auto">
|
||||||
|
{step !== 'success' && (
|
||||||
|
<div className="relative mb-7 bg-primary-500 p-4 overflow-hidden rounded-md md:px-7 md:py-5 md:flex md:justify-between md:gap-x-6">
|
||||||
|
<p className="text-xs font-semibold text-primary-50 md:text-[19px] md:w-[290px] xl:text-[23px]">
|
||||||
|
<Show
|
||||||
|
condition={step !== 'qr-payment' && step !== 'va-payment'}
|
||||||
|
fallback="Yosha~! Saatnya Membayar :)"
|
||||||
|
>
|
||||||
|
Yosha~! Saatnya Level Up dengan Sesi Mentoring bersama Senpai!
|
||||||
|
</Show>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<img
|
||||||
|
src="/image/mascot-character.webp"
|
||||||
|
alt="IMPHNEN Mascot"
|
||||||
|
className="hidden w-[166px] h-auto absolute right-0 -bottom-3 md:block xl:w-[207px] xl:-bottom-5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{step === 'topic' && <TopicStep selectedTopics={selectedTopics} setSelectedTopics={setSelectedTopics} />}
|
||||||
|
{step === 'schedule' && <ScheduleStep />}
|
||||||
|
{step === 'profile' && <ProfileStep />}
|
||||||
|
{step === 'payment' && <PaymentStep selectedTopics={selectedTopics} />}
|
||||||
|
{step === 'qr-payment' && <QrisPaymentStep />}
|
||||||
|
{step === 'va-payment' && <VAPaymentStep />}
|
||||||
|
{step === 'success' && <SuccessStep />}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show condition={step !== 'success'}>
|
||||||
|
<hr className="my-6 border-primary-300" />
|
||||||
|
</Show>
|
||||||
|
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="text"
|
||||||
|
className={cn((step === 'topic' || step === 'success') && 'hidden')}
|
||||||
|
onClick={() => handleStep('prev')}
|
||||||
|
>
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="primary"
|
||||||
|
className={cn((step === 'topic' || step === 'success') && 'w-full')}
|
||||||
|
disabled={selectedTopics.length === 0 && step === 'topic'}
|
||||||
|
onClick={() => handleStep('next')}
|
||||||
|
>
|
||||||
|
<Show
|
||||||
|
condition={step !== 'success'}
|
||||||
|
fallback="Halman Booking"
|
||||||
|
>
|
||||||
|
<Show condition={step !== 'payment'} fallback="Bayar Sekarang">
|
||||||
|
Selanjutnya
|
||||||
|
</Show>
|
||||||
|
</Show>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTopics: number[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PaymentStep: FC<Props> = ({ selectedTopics }) => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md grid gap-6 md:grid-cols-2 md:gap-8 xl:grid-cols-9"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="space-y-6 xl:col-span-4">
|
||||||
|
<div>
|
||||||
|
<p className="w-max mx-auto text-[10px] text-neutral-400 font-medium mb-2 md:text-xs md:ms-0">
|
||||||
|
Senpai Kamu
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div className="aspect-square mb-2 size-10 mx-auto rounded-full overflow-hidden md:size-20 md:ms-0">
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div className="w-44 mx-auto text-center md:ms-0 md:text-left md:mb-2 md:text-[15px] md:w-auto">
|
||||||
|
<h3 className="text-xs font-semibold mb-1 xl:text-[15px]">
|
||||||
|
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 md:text-xs">
|
||||||
|
UI Designer at Oray orayan Studios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Topics :
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<Show key={topic.id} condition={selectedTopics.includes(topic.id)}>
|
||||||
|
<div
|
||||||
|
className="px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium"
|
||||||
|
>
|
||||||
|
<span>{topic.icon} </span>
|
||||||
|
<span>{topic.name}</span>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-6 xl:col-span-5">
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Metode Pembayaran
|
||||||
|
</p>
|
||||||
|
<div className="mb-3">
|
||||||
|
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||||
|
Virtual Account
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||||
|
<For data={['BCA', 'BRI', 'BNI', 'MANDIRI', 'BCA-Virtual', 'BNI-Virtual', 'MANDIRI-Virtual']}>
|
||||||
|
{(bank, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||||
|
>
|
||||||
|
<input type="radio" name="payment" id={bank} className="size-2" />
|
||||||
|
<label htmlFor={bank} className="block">
|
||||||
|
<img src="/image/payment/bca.webp" alt={bank} className="object-scale-down" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[8px] text-neutral-400 mb-2 md:text-[10px]">
|
||||||
|
QRIS
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-3 gap-2 md:grid-cols-4">
|
||||||
|
<div
|
||||||
|
className="p-1 bg-white rounded-xs border border-primary-50 flex items-center gap-x-1"
|
||||||
|
>
|
||||||
|
<input type="radio" name="payment" id="QRIS" className="size-2" />
|
||||||
|
<label htmlFor="QRIS" className="block">
|
||||||
|
<img src="/image/payment/qris.webp" alt="QRIS" className="object-scale-down" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 font-medium mb-2 md:text-xs">
|
||||||
|
Detail Pembayaran
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 space-y-2.5 text-[10px] text-neutral-400 font-medium md:text-xs md:space-y-3">
|
||||||
|
<p className="font-normal">Subtotal :</p>
|
||||||
|
<p className="text-right">Rp. 50.000</p>
|
||||||
|
<p className="font-normal">Service Fee :</p>
|
||||||
|
<p className="text-right">Rp. 2.000</p>
|
||||||
|
|
||||||
|
<hr className="border-neutral-200 col-span-full mt-1" />
|
||||||
|
|
||||||
|
<p className="text-neutral-700">Total :</p>
|
||||||
|
<p className="text-right text-neutral-700">Rp. 52.000</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Input } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
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 ProfileStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 3 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Lengkapi profil kamu agar senpai dapat mengenal kamu lebih baik.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="space-y-2.5 md:space-y-5">
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Nomor WhatsApp</label>
|
||||||
|
<Input className="min-w-full w-full" placeholder="080000000" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>CV (Opsional)</label>
|
||||||
|
<Input type="file" className="min-w-full w-full" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label className={labelClass}>Portoflio (Opsional)</label>
|
||||||
|
<Input className="min-w-full w-full" placeholder="Link Url" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
const PAYMENT_STEP = [
|
||||||
|
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||||
|
'Cari fitur bayar menggunakan QRIS',
|
||||||
|
'Scan kode QR di samping',
|
||||||
|
'Konfimasi pembayaran, dan proses selesai.'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const QrisPaymentStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:flex md:justify-between md:gap-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="mb-7 md:mb-0">
|
||||||
|
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||||
|
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||||
|
Cara melakukan pembayaran
|
||||||
|
</p>
|
||||||
|
<ul className="list-decimal pl-2.5 mb-4 md:mb-7">
|
||||||
|
<For data={PAYMENT_STEP}>
|
||||||
|
{(step, index) => (
|
||||||
|
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||||
|
{step}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div className="text-center md:text-start">
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5 font-medium md:text-xs">
|
||||||
|
Biaya yang harus dibayarkan
|
||||||
|
</p>
|
||||||
|
<p className="text-xs font-semibold text-neutral-700 md:text-[15px]">Rp. 52.000</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-2 font-medium text-center md:text-xs">
|
||||||
|
QR Code Pembayaran
|
||||||
|
</p>
|
||||||
|
<div className="bg-primary-50 p-2.5 rounded-lg size-[120px] mx-auto md:size-[142px] md:me-0">
|
||||||
|
<img src="/image/sample-qrcode.webp" alt="QR Code" className="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { Input, Select, Textarea } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
const placeholder = `Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.
|
||||||
|
|
||||||
|
Saat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.
|
||||||
|
|
||||||
|
Saya ingin tahu secara khusus tentang ___.
|
||||||
|
1.Pertanyaan Anda
|
||||||
|
2. ...
|
||||||
|
3. ...`
|
||||||
|
|
||||||
|
const labelClass = cn('text-neutral-800 text-[10px] font-semibold mb-1.5 inline-block md:text-xs md:mb-2 xl:text-[15px]')
|
||||||
|
|
||||||
|
export const ScheduleStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 2 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Pilih jadwal sesuai preferensi kamu & siapkan pertanyaanmu
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { CheckOutlined } from "@ant-design/icons"
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
export const SuccessStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="pb-6 rounded-md md:pb-8"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h2 className="text-[15px] font-semibold mb-3 text-primary-500 text-center md:text-[19px] md:mb-7 xl:text-[23px] xl:mb-8">
|
||||||
|
Booking Senpai Berhasil
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-3 flex justify-center items-center mx-auto size-[60px] bg-green-200 rounded-full",
|
||||||
|
"md:size-[100px] md:mb-7 xl:size-[120px] xl:mb-8"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="bg-green-500 rounded-full size-10 flex justify-center items-center text-white md:size-16 xl:size-20">
|
||||||
|
<CheckOutlined className="text-xl md:text-3xl xl:text-5xl" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 text-center md:text-xs md:max-w-[220px] md:mx-auto xl:text-[15px]">
|
||||||
|
Untuk detail dan instruksi berikutnya dapat dilihat di dashboard pada halaman booking
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import { CheckOutlined } from "@ant-design/icons"
|
||||||
|
import { cn, For, Show } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
import { TOPICS } from "../../../sections/topics"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
selectedTopics: number[]
|
||||||
|
setSelectedTopics: (topics: number[]) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TopicStep: FC<Props> = ({ selectedTopics, setSelectedTopics }) => {
|
||||||
|
const handleSelectTopic = (topicId: number) => {
|
||||||
|
if (selectedTopics.includes(topicId)) {
|
||||||
|
setSelectedTopics(selectedTopics.filter((id) => id !== topicId))
|
||||||
|
} else {
|
||||||
|
setSelectedTopics([...selectedTopics, topicId])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<motion.div
|
||||||
|
className="mb-6 flex items-center gap-x-4 bg-white rounded-md overflow-hidden shadow/5 md:gap-x-10"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="w-16 aspect-4/5 overflow-hidden md:w-[134px] xl:w-[140px]">
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 pe-4">
|
||||||
|
<h3 className="text-xs font-semibold mb-1 line-clamp-2 md:text-[19px] md:mb-2 xl:text-[23px]">
|
||||||
|
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] line-clamp-2 text-neutral-600 md:text-[15px] xl:text-[19px]">
|
||||||
|
UI Designer at Oray orayan Studios
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="bg-white p-5 rounded-md shadow/5 md:px-6"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<h3 className="text-xs font-semibold mb-1.5 md:text-[15px] md:mb-2 xl:text-[19px]">
|
||||||
|
Langkah 1 dari 3
|
||||||
|
</h3>
|
||||||
|
<p className="text-[10px] text-neutral-600 mb-5 md:text-xs xl:text-[15px]">
|
||||||
|
Pilih topik yang ingin kamu diskusikan dengan Senpai kamu~
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="grid gap-3 md:grid-cols-2 md:content-center">
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<div
|
||||||
|
key={topic.id}
|
||||||
|
className={cn(
|
||||||
|
"relative px-2.5 py-2 text-neutral-800 bg-white border border-primary-100 rounded-md shadow text-[10px] font-medium cursor-pointer",
|
||||||
|
"md:text-xs xl:text-xs xl:font-semibold",
|
||||||
|
selectedTopics.includes(topic.id) && "bg-primary-50 border-primary-200"
|
||||||
|
)}
|
||||||
|
onClick={() => handleSelectTopic(topic.id)}
|
||||||
|
>
|
||||||
|
<span>{topic.icon} </span>
|
||||||
|
<span>{topic.name}</span>
|
||||||
|
|
||||||
|
<Show condition={selectedTopics.includes(topic.id)}>
|
||||||
|
<div className="absolute top-2 right-2 bg-primary-500 size-[14px] rounded-sm flex justify-center items-center text-white">
|
||||||
|
<CheckOutlined />
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { motion } from "framer-motion"
|
||||||
|
|
||||||
|
const PAYMENT_STEP = [
|
||||||
|
'Buka aplikasi e-wallet atau m-banking kamu',
|
||||||
|
'Pergi ke fitur transfer via Virtual Account',
|
||||||
|
'Pilih bank dan kode VA sesuai dengan yang ada pada halaman ini',
|
||||||
|
'Konfimasi pembayaran, dan proses selesai.'
|
||||||
|
]
|
||||||
|
|
||||||
|
export const VAPaymentStep = () => {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
className="bg-white px-6 py-5 rounded-md md:flex md:items-center md:gap-11"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 20 }}
|
||||||
|
>
|
||||||
|
<div className="mb-6 md:mb-0">
|
||||||
|
<h3 className="text-xs font-semibold mb-4 text-neutral-600 text-center md:text-start">
|
||||||
|
Silahkan ikuti instruksi pembayaran di bawah ini
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1.5">
|
||||||
|
Cara melakukan pembayaran
|
||||||
|
</p>
|
||||||
|
<ul className="list-decimal pl-2.5">
|
||||||
|
<For data={PAYMENT_STEP}>
|
||||||
|
{(step, index) => (
|
||||||
|
<li key={index} className="text-[10px] text-neutral-400 leading-tight font-medium">
|
||||||
|
{step}
|
||||||
|
</li>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-center md:text-start">
|
||||||
|
<div className="w-max mx-auto mb-3 md:ms-0 md:mb-4">
|
||||||
|
<img src="/image/payment/bca.webp" alt="VA BCA" className="h-5 w-auto" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-[10px] text-neutral-400 mb-1 font-medium md:text-xs">
|
||||||
|
Kode Virtual Account
|
||||||
|
</p>
|
||||||
|
<p className="text-xs font-semibold text-neutral-700 mb-1.5 md:text-[15px]">
|
||||||
|
8091239861969812
|
||||||
|
</p>
|
||||||
|
<p className="text-[8px] text-neutral-400 md:text-[10px]">
|
||||||
|
A/N Unknown
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
|
||||||
|
const EDUCATION = [
|
||||||
|
{ name: 'Universitas Widyabakti', major: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||||
|
{ name: 'SMKN 99 Banjaran', major: 'Rekayasa Perangkat Lunak', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const EducationSection = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-7 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Education</h2>
|
||||||
|
<div className="space-y-4 divide-y">
|
||||||
|
<For data={EDUCATION}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className={cn("flex items-center gap-x-4", index !== EDUCATION.length - 1 && "pb-4")}>
|
||||||
|
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
|
<div className='flex-1 text-[10px] font-medium'>
|
||||||
|
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-neutral-600 xl:text-xs">{item.major}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const EXPERIENCE = [
|
||||||
|
{ name: 'Sunday.com', position: 'Intern Front End', duration: '2 Months', range: 'Oct 2024 - Present' },
|
||||||
|
{ name: 'CodeX Digital', position: 'Intern Front End', duration: '6 Months', range: 'May 2024 - Oct 2024' },
|
||||||
|
]
|
||||||
|
|
||||||
|
export const ExperienceSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="px-7 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs font-semibold mb-5 md:text-[15px] xl:text-[19px]">Experience</h2>
|
||||||
|
<div className="space-y-4 divide-y">
|
||||||
|
<For data={EXPERIENCE}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className={cn("flex items-center gap-x-4", index !== EXPERIENCE.length - 1 && "pb-4")}>
|
||||||
|
<div className="rounded-full size-6 bg-neutral-200 md:size-7 xl:size-8"></div>
|
||||||
|
<div className='flex-1 text-[10px] font-medium'>
|
||||||
|
<p className="text-neutral-800 md:text-xs xl:text-[15px]">{item.name}</p>
|
||||||
|
<p>
|
||||||
|
<span className="text-neutral-600 xl:text-xs">{item.position}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal">{item.duration}</span>
|
||||||
|
<span className="text-neutral-300"> · </span>
|
||||||
|
<span className="text-neutral-400 font-normal xl:font-medium">{item.range}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { StarFilled } from "@ant-design/icons"
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onBook: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProfileSection: React.FC<Props> = ({ onBook }) => {
|
||||||
|
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={cn(
|
||||||
|
"flex flex-col items-center justify-center",
|
||||||
|
"md:flex-row md:justify-start md:gap-x-8 md:items-start",
|
||||||
|
"xl:items-center xl:gap-x-10",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"mb-5 h-20 w-auto aspect-4/5 rounded-sm overflow-hidden",
|
||||||
|
"md:aspect-33/40 md:h-40 md:rounded-md md:border md:border-primary-50 md:mb-0",
|
||||||
|
"xl:aspect-5/6 md:h-60 md:rounded-lg"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<img src="/image/testimonial.webp" alt="Mentor" className="w-full object-cover" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="md:max-w-[242px] xl:max-w-[306px]">
|
||||||
|
<h1
|
||||||
|
className={cn(
|
||||||
|
"mb-1 text-[15px] font-semibold text-center leading-snug",
|
||||||
|
"md:text-[19px] md:mb-2 md:text-start xl:text-[23px]",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Muhammad Firdaus Oi Oi Oi, S.H., M.H.
|
||||||
|
</h1>
|
||||||
|
<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
|
||||||
|
</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="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-5 flex justify-center items-center xl:size-[29.4px]"
|
||||||
|
>
|
||||||
|
<StarFilled className="text-xs xl:text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] font-medium mb-1 text-neutral-800 xl:text-xs">Excelent Sensei</p>
|
||||||
|
<p className="text-[8px] text-neutral-600 xl:text-[10px]">4.8/5.0</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-3 xl:gap-y-4 xl:max-w-[402px]">
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
<For data={['UI Design', 'UX Reseacrh']}>
|
||||||
|
{(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">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<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">
|
||||||
|
<For data={['Design Thinking', 'Communication', 'Problem Solving', '19:00 WIB']}>
|
||||||
|
{(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">
|
||||||
|
{item}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden md:flex xl:hidden justify-between">
|
||||||
|
<div>
|
||||||
|
<p className="text-primary-500 text-[15px] font-semibold">Jum, 4 April 2025</p>
|
||||||
|
<p className="text-neutral-500 text-xs font-medium">Jum, 4 April 2025</p>
|
||||||
|
</div>
|
||||||
|
<Button type="button" size="sm" onClick={onBook}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms"
|
||||||
|
import { cn, For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
const SCHEDULES = [
|
||||||
|
{
|
||||||
|
date: "Kamis, 20 Maret 2025",
|
||||||
|
availableCount: 10,
|
||||||
|
schedule: [
|
||||||
|
{ time: "19:00 WIB", available: true },
|
||||||
|
{ time: "20:00 WIB", available: true },
|
||||||
|
{ time: "21:00 WIB", available: true },
|
||||||
|
{ time: "22:00 WIB", available: true },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
date: "Jumat, 21 Maret 2025",
|
||||||
|
availableCount: 1,
|
||||||
|
schedule: [
|
||||||
|
{ time: "19:00 WIB", available: true },
|
||||||
|
{ time: "20:00 WIB", available: true },
|
||||||
|
{ time: "21:00 WIB", available: false },
|
||||||
|
{ time: "22:00 WIB", available: true },
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onBook: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SenpaiScheduleSection: FC<Props> = ({ onBook }) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Schedule</h2>
|
||||||
|
<div className="p-4 bg-primary-50 border border-primary-100 rounded-md space-y-5">
|
||||||
|
<For data={SCHEDULES}>
|
||||||
|
{(item) => (
|
||||||
|
<div key={item.date}>
|
||||||
|
<div className="flex justify-between items-center mb-4">
|
||||||
|
<p className="text-[15px] font-medium">{item.date}</p>
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
"rounded-4xl py-1 px-2.5 text-xs font-medium",
|
||||||
|
item.availableCount > 5 ? "bg-primary-200 text-primary-500" : "bg-danger-100 text-danger-600"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{item.availableCount} slot tersisa
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-x-3">
|
||||||
|
<For data={item.schedule}>
|
||||||
|
{(schedule, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="bg-primary-300 text-primary-600 px-3 py-2 text-xs rounded-md font-semibold"
|
||||||
|
>
|
||||||
|
{schedule.time}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
<hr className="border-primary-200" />
|
||||||
|
|
||||||
|
<Button type="button" size="sm" className="w-full" onClick={onBook}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { StarFilled } from "@ant-design/icons";
|
||||||
|
import { For } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC } from "react";
|
||||||
|
|
||||||
|
const SENPAI_STATISTIC = [
|
||||||
|
{ name: 'Total Sessions', count: 8 },
|
||||||
|
{ name: 'Mentee Impact', count: 1000 },
|
||||||
|
{ name: 'Response Time', count: '30 Minute' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const StatisticsSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div className="md:mb-10">
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Senpai Statistics</h2>
|
||||||
|
<div className="grid gap-4 xl:flex">
|
||||||
|
<For data={SENPAI_STATISTIC}>
|
||||||
|
{(item, index) => (
|
||||||
|
<div key={index} className="px-2.5 py-2 border border-primary-50 rounded-md shadow flex items-center gap-x-2.5">
|
||||||
|
<div
|
||||||
|
className="bg-gradient-to-tr from-primary-500 to-primary-200 rounded-sm text-white size-7 flex justify-center items-center xl:size-[29.4px]"
|
||||||
|
>
|
||||||
|
<StarFilled className="text-sm" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-[10px] text-neutral-600 font-medium md:text-xs xl:font-semibold">{item.count}</p>
|
||||||
|
<p className="text-[8px] text-neutral-800 md:text-[10px]">{item.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { For } from "@imphnen-frontend-service/utils"
|
||||||
|
import { FC } from "react"
|
||||||
|
|
||||||
|
export const TOPICS = [
|
||||||
|
{ id: 1, icon: '💼', name: 'Career & Self Development' },
|
||||||
|
{ id: 2, icon: '🏢', name: 'Industry Insight' },
|
||||||
|
{ id: 3, icon: '🗂️', name: 'Project Management & IT Tools' },
|
||||||
|
{ id: 4, icon: '🖥️', name: 'Basic IT' },
|
||||||
|
{ id: 5, icon: '💻', name: 'Programming/Software Dev' },
|
||||||
|
{ id: 6, icon: '🗃️', name: 'Data & Database' },
|
||||||
|
{ id: 7, icon: <span className="font-bold text-primary-500">AI</span>, name: 'AI Tips' }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const TopicsSection: FC = () => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h2 className="text-xs font-semibold mb-3 md:text-[15px] xl:text-[19px]">Topics</h2>
|
||||||
|
<div className="p-5 bg-primary-50 border border-primary-100 rounded-md flex flex-wrap gap-2.5">
|
||||||
|
<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 text-[10px] font-medium"
|
||||||
|
>
|
||||||
|
<span>{item.icon} </span>
|
||||||
|
<span>{item.name}</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { FC, useState } from 'react'
|
||||||
|
import { ProfileSection } from './_components/sections/profile'
|
||||||
|
import { StatisticsSection } from './_components/sections/senpai-statistics'
|
||||||
|
import { TopicsSection } from './_components/sections/topics'
|
||||||
|
import { ExperienceSection } from './_components/sections/experience'
|
||||||
|
import { EducationSection } from './_components/sections/education'
|
||||||
|
import { SenpaiScheduleSection } from './_components/sections/senpai-schedule'
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
|
import { AppointmentModal } from './_components/modals/appointment'
|
||||||
|
|
||||||
|
export const Components: FC = () => {
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<section className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto space-y-8 md:bg-white xl:bg-transparent">
|
||||||
|
<ProfileSection onBook={() => setOpen(true)} />
|
||||||
|
|
||||||
|
<Button type="button" size="sm" className="w-full md:hidden" onClick={() => setOpen(true)}>
|
||||||
|
Book Your Senpai!
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<div className="bg-white px-4 py-5 md:px-8 md:pb-6 md:pt-0 xl:py-7 xl:flex xl:gap-x-10">
|
||||||
|
<div className="space-y-10 md:space-y-7 xl:flex-1">
|
||||||
|
<StatisticsSection />
|
||||||
|
<TopicsSection />
|
||||||
|
|
||||||
|
<div className="px-6 py-8 rounded-md shadow-md">
|
||||||
|
<h2 className="text-xs text-neutral-800 font-semibold mb-5 md:text-[15px] xl:text-[19px]">Senpai Resume</h2>
|
||||||
|
<p className="text-[10px] font-medium text-neutral-600 text-pretty md:text-[15px]">
|
||||||
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut et massa mi. Aliquam in hendrerit urna. Pellentesque sit amet sapien fringilla, mattis ligula consectetur, ultrices mauris. Maecenas vitae mattis tellus. Nullam quis imperdiet augue. Vestibulum auctor ornare leo, non suscipit magna interdum eu. Curabitur pellentesque nibh nibh, at maximus ante fermentum sit amet. Pellentesque commodo lacus at sodales sodales. Quisque sagittis orci ut diam condimentum, vel euismod erat placerat. In iaculis arcu eros, eget tempus orci facilisis id.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ExperienceSection />
|
||||||
|
<EducationSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden xl:block xl:w-[400px]">
|
||||||
|
<SenpaiScheduleSection onBook={() => setOpen(true)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<AppointmentModal open={open} setOpen={setOpen} />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Components
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { FC, useRef } from 'react';
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion';
|
||||||
|
|
||||||
|
export const BannerSection: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"relative max-w-7xl mx-auto bg-white p-6 rounded-lg overflow-hidden shadow-md",
|
||||||
|
"md:flex md:justify-between md:items-center md:px-5 md:rounded-xl lg:px-16 lg:py-10",
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="hidden absolute -left-9 -top-24 w-[17rem] md:block lg:w-[28rem] lg:-left-14 lg:-top-52"
|
||||||
|
initial={{ x: -120, opacity: 0, rotate: -6 }}
|
||||||
|
animate={{ x: 0, opacity: 1, rotate: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<img src="/image/mentoring-banner.webp" alt="" className="w-full" />
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="text-center md:text-start md:max-w-[calc(100%-14rem)] md:ms-auto lg:max-w-[calc(100%-20rem)]"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<motion.h1 className="mb-4 text-xl font-semibold text-primary-500 leading-tight md:text-2xl lg:text-5xl lg:font-bold lg:mb-6" variants={childVariants}>
|
||||||
|
Find your perfect sensei
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p className="text-xs font-semibold text-neutral-500 md:text-[15px] md:font-medium lg:text-xl" variants={childVariants}>
|
||||||
|
Di sini, kamu nggak cuma grinding sendirian—sensei dari dunia nyata siap membimbingmu, dan AI mentor bakal jadi support system terbaikmu. Saatnya jadi protagonist dalam perjalanan karier IT-mu!
|
||||||
|
</motion.p>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { FC } from 'react';
|
||||||
|
import { Link } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const MentorCard: FC = () => {
|
||||||
|
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="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" />
|
||||||
|
</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">
|
||||||
|
3-5 Years Experience
|
||||||
|
</Button>
|
||||||
|
<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]">
|
||||||
|
Fullname
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<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
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-[8px] text-neutral-500 mb-1 md:mb-2 lg:text-[10px]">Soft Skills :</p>
|
||||||
|
<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]">
|
||||||
|
Communication
|
||||||
|
</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
|
||||||
|
</Button>
|
||||||
|
<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]">
|
||||||
|
+2
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { CloudServerOutlined, CodeOutlined, DatabaseOutlined, ProjectOutlined, TabletOutlined } from '@ant-design/icons'
|
||||||
|
import { Icon } from '@iconify/react'
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||||
|
import { For } from '@imphnen-frontend-service/utils'
|
||||||
|
import { FC, ReactElement, useRef } from 'react'
|
||||||
|
import { motion, useInView, Variants } from 'framer-motion'
|
||||||
|
|
||||||
|
const TOPICS: {
|
||||||
|
name: string
|
||||||
|
icon: React.ReactNode
|
||||||
|
}[] = [
|
||||||
|
{ name: 'UI/UX & Design', icon: <TabletOutlined /> },
|
||||||
|
{ name: 'Software/Web Dev', icon: <CodeOutlined /> },
|
||||||
|
{ name: 'Data & AI', icon: <DatabaseOutlined /> },
|
||||||
|
{ name: 'Cloud & DevOps', icon: <CloudServerOutlined /> },
|
||||||
|
{ name: 'Cybersecurity', icon: <Icon icon="mage:security-shield" /> },
|
||||||
|
{ name: 'IT & Networking', icon: <Icon icon="carbon:data-structured" /> },
|
||||||
|
{ name: 'Project Management', icon: <ProjectOutlined /> },
|
||||||
|
{ name: 'QA & Testing', icon: <Icon icon="carbon:exam-mode" /> }
|
||||||
|
]
|
||||||
|
|
||||||
|
export const Topics: FC = (): ReactElement => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
delay: 0.4,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={ref}>
|
||||||
|
<motion.div
|
||||||
|
className="grid grid-cols-4 gap-6 px-3 md:px-12 md:gap-x-10 md:gap-y-14 lg:grid-cols-8 lg:px-0"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
>
|
||||||
|
<For data={TOPICS}>
|
||||||
|
{(topic) => (
|
||||||
|
<motion.div key={topic.name} className="flex flex-col items-center" variants={childVariants}>
|
||||||
|
<Button variant="secondary" size='sm' className="size-7 mb-3 p-0 md:mb-4 md:size-8 md:text-base lg:size-10 lg:text-lg">
|
||||||
|
{topic.icon}
|
||||||
|
</Button>
|
||||||
|
<p className="text-[8px] text-neutral-700 text-center break-words md:text-xs md:font-medium lg:text-[15px]">
|
||||||
|
{topic.name}
|
||||||
|
</p>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
import { FC, ReactElement, useRef, useState } from 'react';
|
||||||
|
import { BannerSection } from './_components/banner-section';
|
||||||
|
import { Topics } from './_components/topics';
|
||||||
|
import { Input } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { For } from '@imphnen-frontend-service/utils';
|
||||||
|
import { MentorCard } from './_components/mentor-card';
|
||||||
|
import { Pagination } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
import { getCoreRowModel, getPaginationRowModel, PaginationState, useReactTable } from '@tanstack/react-table';
|
||||||
|
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 => {
|
||||||
|
const [pagination, setPagination] = useState<PaginationState>({
|
||||||
|
pageIndex: 0,
|
||||||
|
pageSize: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const table = useReactTable({
|
||||||
|
data: TEMP_DATA,
|
||||||
|
columns: [],
|
||||||
|
state: { pagination },
|
||||||
|
getCoreRowModel: getCoreRowModel(),
|
||||||
|
getPaginationRowModel: getPaginationRowModel(),
|
||||||
|
onPaginationChange: setPagination,
|
||||||
|
});
|
||||||
|
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<BannerSection />
|
||||||
|
|
||||||
|
<section className="w-full px-8 md:px-[60px] md:pb-[60px] lg:px-20">
|
||||||
|
<motion.div
|
||||||
|
className="max-w-7xl mx-auto bg-white rounded-lg py-6 md:rounded-4xl"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
>
|
||||||
|
<div className="mb-10 md:px-6 lg:mb-[60px]">
|
||||||
|
<motion.h1
|
||||||
|
className="text-center text-[15px] text-primary-500 font-semibold mb-8 md:text-2xl md:mb-10 lg:text-3xl lg:mb-[60px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
transition={{ duration: 0.4, delay: 0.3 }}
|
||||||
|
>
|
||||||
|
Choose Your Topics
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<Topics />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div ref={ref} className="md:px-6">
|
||||||
|
<motion.div
|
||||||
|
className="relative mb-6 md:px-12 lg:px-0 lg:w-[480px] lg:mx-auto"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={{ duration: 0.4 }}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
placeholder="Cari berdasarkan nama, posisi/peran"
|
||||||
|
className="relative min-w-full w-full"
|
||||||
|
/>
|
||||||
|
<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
|
||||||
|
className="grid gap-2 mb-10 md:grid-cols-2 md:gap-6 lg:grid-cols-4"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={isInView ? 'visible' : 'hidden'}
|
||||||
|
>
|
||||||
|
<For data={Array.from({ length: 8 })}>
|
||||||
|
{(_, index) => (
|
||||||
|
<motion.div key={index} variants={childVariants}>
|
||||||
|
<MentorCard />
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<Pagination table={table} />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Components
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
import { MailOutlined } from "@ant-design/icons";
|
||||||
|
import { Icon } from "@iconify/react";
|
||||||
|
import { Button } from "@imphnen-frontend-service/ui/atoms";
|
||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const Collaborate: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const isInView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, scale: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
scale: 1,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:pt-0 lg:pb-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl py-6 mx-auto md:flex md:gap-[60px] md:items-center xl:py-14">
|
||||||
|
<div className="relative mb-8 flex items-center justify-center h-[124px] md:mb-0 md:h-[180px] md:w-[202px] xl:h-[380px] xl:w-[428px]">
|
||||||
|
<motion.div
|
||||||
|
className="relative"
|
||||||
|
variants={containerVariants}
|
||||||
|
animate={isInView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="size-[104px] bg-primary-200 rounded-full flex justify-between items-center md:size-[148px] xl:size-[304px]"
|
||||||
|
initial={{ scale: 0 }}
|
||||||
|
whileInView={{ scale: 1 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="size-[88px] rounded-full bg-gradient-to-bl from-primary-500 to-primary-300 mx-auto md:size-[132px] xl:size-[276px]"
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md -top-3 -right-2 p-1 rotate-6 xl:p-2.5 xl:rounded-lg"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="bi:terminal" className="text-primary-500 size-4 md:size-[18px] xl:size-9" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md top-1/3 -left-6 p-1.5 -rotate-10 xl:p-2.5 xl:-left-12"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="ion:git-branch-outline" className="text-primary-500 size-6 md:size-[30px] xl:size-14" />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute bg-white shadow rounded-md top-3/5 -right-3 p-1 xl:p-2 xl:-right-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<Icon icon="bi:github" className="size-[18px] md:size-[26px] xl:size-[52px]" />
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="h-full w-auto aspect-3/5 absolute inset-0 mx-auto bg-contain bg-no-repeat"
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.5 }}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="/image/anime-girl.webp"
|
||||||
|
alt="Anime Girl"
|
||||||
|
style={{
|
||||||
|
opacity: "1",
|
||||||
|
maskImage: "linear-gradient(to bottom, black 80%, transparent 100%)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"px-6 py-5 rounded-lg bg-white border-2 border-primary-200 text-center text-primary-500 shadow",
|
||||||
|
"md:flex-1 md:text-start xl:py-8 xl:px-12"
|
||||||
|
)}
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
whileInView={{ opacity: 1 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<motion.h1
|
||||||
|
className="text-[15px] font-semibold mb-3 md:text-[23px] xl:text-[37px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Let's Collaborate!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-xs font-medium leading-tight mb-6 md:text-[15px] xl:text-[19px]"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
Klik tombol di bawah ini untuk mendaftar sebagai partner resource dan bantu membangun dunia IT yang lebih seru!
|
||||||
|
</motion.p>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="flex items-center mx-auto md:ms-0"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.2 }}
|
||||||
|
>
|
||||||
|
<Button type="button" size="sm" className="flex items-center gap-2">
|
||||||
|
<span>Collaboration Now</span>
|
||||||
|
<MailOutlined />
|
||||||
|
</Button>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const Contribute: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full p-8 md:py-14 md:px-[60px] lg:py-16 lg:px-20 xl:py-20">
|
||||||
|
<div className="relative max-w-7xl mx-auto xl:py-14">
|
||||||
|
<div className="md:max-w-[514px] mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="font-semibold text-[19px] text-primary-500 mb-4 text-center leading-tight md:text-[37px] md:mb-6 xl:text-[46px] xl:font-bold"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Want to contribute? <br /> help senpai build the IT ecosystem!
|
||||||
|
</motion.h1>
|
||||||
|
<motion.p
|
||||||
|
className="text-center text-xs text-neutral-500 md:text-[15px] md:font-medium md:w-[395px] md:mx-auto xl:text-[19px] xl:w-full"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5, delay: 0.1 }}
|
||||||
|
>
|
||||||
|
Punya platform, artikel, atau tool keren untuk membantu mentee belajar IT? Bergabunglah dengan kami dan jadilah bagian dari ekosistem pembelajaran yang lebih besar!
|
||||||
|
</motion.p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="hidden xl:block absolute inset-0 text-neutral-800"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 left-0 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg -rotate-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Learning Platform
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-0 right-36 text-2xl font-semibold bg-white border rounded-md px-5 py-2 shadow-lg rotate-3"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
AI Tools
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/3 left-24 text-[23px] font-semibold bg-white border rounded-md px-5 py-2 shadow-lg"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
UI Assets
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-1/3 right-0 text-[27.38px] font-semibold bg-white border rounded-md px-6 py-2 shadow-lg -rotate-3"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Refferences
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-2/3 left-4 text-[31.62px] font-semibold bg-white border rounded-md px-6 py-2.5 shadow-lg leading-tight rotate-1"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Coding <br />Playground
|
||||||
|
</motion.div>
|
||||||
|
<motion.div
|
||||||
|
className="absolute top-[70%] right-16 text-2xl font-semibold bg-white border rounded-md px-5 py-2.5 shadow-lg leading-tight rotate-6"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
Challenge <br />Platform
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
import { FC, useRef } from "react";
|
||||||
|
import { motion, useInView, Variants } from "framer-motion";
|
||||||
|
|
||||||
|
export const WorkWithUs: FC = () => {
|
||||||
|
const ref = useRef(null)
|
||||||
|
const inView = useInView(ref, { once: true, amount: 0.2 })
|
||||||
|
|
||||||
|
const containerVariants: Variants = {
|
||||||
|
hidden: { opacity: 0 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
transition: {
|
||||||
|
staggerChildren: 0.1,
|
||||||
|
delayChildren: 0.8,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const childVariants: Variants = {
|
||||||
|
hidden: { opacity: 0, y: 20 },
|
||||||
|
visible: {
|
||||||
|
opacity: 1,
|
||||||
|
y: 0,
|
||||||
|
transition: {
|
||||||
|
duration: 0.4,
|
||||||
|
ease: [0.25, 0.46, 0.45, 0.94],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section ref={ref} className="w-full px-8 md:px-[60px] lg:py-16 lg:px-20">
|
||||||
|
<div className="max-w-7xl mx-auto">
|
||||||
|
<motion.h1
|
||||||
|
className="text-[19px] font-semibold text-primary-500 mb-5 text-center md:text-[23px] md:mb-7 xl:text-[37px] xl:mb-10"
|
||||||
|
initial={{ opacity: 0, y: 20 }}
|
||||||
|
whileInView={{ opacity: 1, y: 0 }}
|
||||||
|
viewport={{ once: true }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
Why Work with Us
|
||||||
|
</motion.h1>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="grid gap-4 md:grid-cols-3 md:gap-5 xl:grid-flow-col xl:grid-cols-2 xl:grid-rows-2 xl:gap-6"
|
||||||
|
variants={containerVariants}
|
||||||
|
initial="hidden"
|
||||||
|
animate={inView ? "visible" : "hidden"}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:row-span-2 xl:px-6 xl:flex xl:flex-col xl:justify-end"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:h-auto xl:w-full xl:mb-8">
|
||||||
|
<img src="/image/work-with-us/exposure.webp" alt="Exposure" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start xl:px-0">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||||
|
Exposure Luas
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||||
|
Web kamu akan tampil di halaman resource kami!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 md:py-8 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||||
|
<img src="/image/work-with-us/handshake.webp" alt="Handshake" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px]">
|
||||||
|
Bantu Generasi Baru
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px]">
|
||||||
|
Bagikan ilmu dan bantu lebih banyak orang berkembang!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div
|
||||||
|
className="px-5 py-6 bg-white rounded-lg shadow md:px-0 xl:flex xl:items-center xl:px-6 xl:py-2 xl:gap-8"
|
||||||
|
variants={childVariants}
|
||||||
|
>
|
||||||
|
<div className="h-[60px] w-full mb-6 xl:w-auto xl:h-[124px] xl:mb-0">
|
||||||
|
<img src="/image/work-with-us/networking.webp" alt="Networking" className="h-full w-auto object-scale-down mx-auto" />
|
||||||
|
</div>
|
||||||
|
<div className="text-center md:px-6 xl:text-start">
|
||||||
|
<h1 className="text-primary-500 text-[15px] mb-2 font-semibold leading-tight md:text-[19px] md:px-4 xl:px-0">
|
||||||
|
Networking Komunitas
|
||||||
|
</h1>
|
||||||
|
<p className="text-[10px] text-neutral-500 leading-tight md:text-[15px] md:px-1 xl:px-0">
|
||||||
|
Bertemu dengan para developer, mentor, dan inovator lainnya!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ReactElement } from "react";
|
||||||
|
import { Contribute } from "./_components/contribute";
|
||||||
|
import { WorkWithUs } from "./_components/work-with-us";
|
||||||
|
import { Collaborate } from "./_components/collaborate";
|
||||||
|
|
||||||
|
export default function Components(): ReactElement {
|
||||||
|
return (
|
||||||
|
<main>
|
||||||
|
<Contribute />
|
||||||
|
<WorkWithUs />
|
||||||
|
<Collaborate />
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -51,6 +51,11 @@ export const Components: FC = (): ReactElement => {
|
|||||||
Daftar Disini
|
Daftar Disini
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='text-center w-full mb-1'>
|
||||||
|
<a href="/auth/register-mentor" className="text-primary-500 font-medium">
|
||||||
|
Daftar Sebagai Mentor
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
|
<div className="flex-grow border-t border-blue-400 opacity-50"></div>
|
||||||
<span className="px-3 text-blue-400">Or</span>
|
<span className="px-3 text-blue-400">Or</span>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import { FC, ReactElement, useState } from 'react';
|
||||||
|
import { ControlledInputField, RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { Button, Select } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
import { InputField, RegisterMentorStep, SelectField } from '@imphnen-frontend-service/ui/molecules';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
const [ step, setStep ] = useState(1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
|
<div className="bg-white xl:min-w-[1130px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className="xl:border-2 xl:border-primary-500/50 xl:w-[726px] rounded-lg py-[32px] px-[48px] flex justify-center">
|
||||||
|
<div className="xl:w-[714px]">
|
||||||
|
<Button
|
||||||
|
className='xl:hidden gap-3'
|
||||||
|
variant='secondary'
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
<RegisterMentorStep step={step} />
|
||||||
|
<h3 className="mt-5 text-3xl font-semibold text-primary-500">
|
||||||
|
Register
|
||||||
|
</h3>
|
||||||
|
<h5 className="text-primary-500 font-medium">
|
||||||
|
Welcome to the team, Mentor - powered by IMPHNEN
|
||||||
|
</h5>
|
||||||
|
<form>
|
||||||
|
<div className={`${step !== 1 ? 'hidden' : ''}`}>
|
||||||
|
<div className='mt-7 mb-2'>
|
||||||
|
<InputField
|
||||||
|
label="Nama Lengkap"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SelectField
|
||||||
|
label="Jenis Kelamin"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Pilih Jenis Kelamin
|
||||||
|
</option>
|
||||||
|
<option value="laki-laki">Laki - Laki</option>
|
||||||
|
<option value="perempuan">Perempuan</option>
|
||||||
|
</SelectField>
|
||||||
|
<div className='my-2'>
|
||||||
|
<InputField
|
||||||
|
label="No Telp/Whatsapp Only"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Nama Lengkap"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SelectField
|
||||||
|
label="Domisili"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Pilih Domisili
|
||||||
|
</option>
|
||||||
|
<option value="aceh">Aceh</option>
|
||||||
|
<option value="jawa-selatan">Jawa Selatan</option>
|
||||||
|
</SelectField>
|
||||||
|
<div className='mt-2'>
|
||||||
|
<SelectField
|
||||||
|
label="Pendidikan Terakhir"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Apa pendidikan terakhir senpai
|
||||||
|
</option>
|
||||||
|
<option value="SMA">SMA</option>
|
||||||
|
<option value="S1">S1</option>
|
||||||
|
<option value="S2">S2</option>
|
||||||
|
<option value="S3">S3</option>
|
||||||
|
</SelectField>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${step !== 2 ? 'hidden' : ''}`}>
|
||||||
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
|
<InputField
|
||||||
|
label="Perusahaan Saat Ini"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Masukkan Perusahaan tempat kerja, Senpai~! ✨ (Pastikan tidak typo, ya~ 😆)"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Masukan jabatan senpai di perusahaan"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Masukan jabatan senpai di perusahaan"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
<SelectField
|
||||||
|
label="Topik Keahlian Yang Ingin Diajarkan"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Apa yang ingin anda ajarkan senpaii
|
||||||
|
</option>
|
||||||
|
<option value="Frontend Developer">Frontend Developer</option>
|
||||||
|
<option value="Backend Developer">Backend Developer</option>
|
||||||
|
<option value="Other">Other</option>
|
||||||
|
</SelectField>
|
||||||
|
<InputField
|
||||||
|
label="Jika Mengisi “Other”, Tulis Topik yang Kamu Kuasai"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Tolong isi jika tidak ada di pilihan senpai"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Skill Utama yang Kamu Miliki (boleh lebih dari satu)"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Isi Skill nya dong senpai"
|
||||||
|
name={'fullname'}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${step !== 3 ? 'hidden' : ''}`}>
|
||||||
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
|
<SelectField
|
||||||
|
label="Durasi Sesi Mentoring"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Placeholder
|
||||||
|
</option>
|
||||||
|
</SelectField>
|
||||||
|
<SelectField
|
||||||
|
label="Format Pengajaran yang Anda Sediakan"
|
||||||
|
defaultValue=""
|
||||||
|
size="lg"
|
||||||
|
error=""
|
||||||
|
>
|
||||||
|
<option value="" disabled hidden>
|
||||||
|
Placeholder
|
||||||
|
</option>
|
||||||
|
</SelectField>
|
||||||
|
<InputField
|
||||||
|
type="file"
|
||||||
|
label="Unggah Curiculum Vitae"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
label="Unggah Portofolio"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Google Drive, Behance, GitHub, Notion, dan lainnya"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
type='number'
|
||||||
|
label="Honorarium yang Diharapkan per Sesi (Online) Durasi rata-rata sesi: 2,5–3 jam"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Isi Nominalnya dong senpaiii!!!"
|
||||||
|
/>
|
||||||
|
<h6 className='text-xs'>Silahkan isi dalam angka (Contoh:500.000)</h6>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className={`${step !== 4 ? 'hidden' : ''}`}>
|
||||||
|
<div className='mt-7 mb-2 gap-2 flex flex-col'>
|
||||||
|
<InputField
|
||||||
|
type='email'
|
||||||
|
label="Email"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="isi emailnya dong senpai!!!"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
type='password'
|
||||||
|
label="Buat Password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Buat Password"
|
||||||
|
/>
|
||||||
|
<InputField
|
||||||
|
type='password'
|
||||||
|
label="Buat Password"
|
||||||
|
size="lg"
|
||||||
|
className="w-full"
|
||||||
|
placeholder="Buat Password"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className='flex justify-end mt-4 gap-2'>
|
||||||
|
<Button
|
||||||
|
variant='secondary'
|
||||||
|
onClick={() => setStep(step - 1)}
|
||||||
|
type='button'
|
||||||
|
className={`${step === 1 ? 'hidden' : ''}`}
|
||||||
|
>
|
||||||
|
<ArrowLeftOutlined />
|
||||||
|
Kembali
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
onClick={() => setStep(step + 1)}
|
||||||
|
type='button'
|
||||||
|
className={`${step === 4 ? 'hidden' : ''}`}
|
||||||
|
>
|
||||||
|
Lanjutkan
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant='primary'
|
||||||
|
onClick={() => setStep(step + 1)}
|
||||||
|
type='button'
|
||||||
|
className={`${step === 4 ? '' : 'hidden'}`}
|
||||||
|
>
|
||||||
|
Selesaikan
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { FC, ReactElement, useEffect } from 'react';
|
||||||
|
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
|
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className="border-2 border-primary-500/50 xl:w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||||
|
<div className="xl:w-[704px] flex flex-col justify-center items-center">
|
||||||
|
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||||
|
Register Akun Mentor Berhasil
|
||||||
|
</h2>
|
||||||
|
<h4 className="mt-3 text-primary-500 font-medium text-xl text-center">
|
||||||
|
Data kamu telah kami terima dan sedang dalam proses verifikasi.
|
||||||
|
</h4>
|
||||||
|
<svg className="mt-10" width="148" height="148" viewBox="0 0 148 148" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<circle cx="74" cy="74" r="73.6348" fill="#42CDF8"/>
|
||||||
|
<rect x="23.5078" y="24.8463" width="99.646" height="99.646" rx="49.823" fill="#0185D0"/>
|
||||||
|
<circle cx="73.3284" cy="74.6689" r="33.11" transform="rotate(150 73.3284 74.6689)" stroke="#F0F8FF" stroke-width="1.1825"/>
|
||||||
|
<path d="M44.6436 91.2314C42.1053 92.6969 41.2086 95.9711 43.0096 98.2836C46.1026 102.255 49.9569 105.594 54.3651 108.095C60.3178 111.473 67.0644 113.199 73.9077 113.096C80.7511 112.993 87.4427 111.065 93.2912 107.51C97.6222 104.878 101.374 101.424 104.347 97.3614C106.077 94.9958 105.083 91.75 102.501 90.3615C99.9201 88.9729 96.7369 89.9826 94.884 92.2537C92.8925 94.6949 90.4923 96.7901 87.7782 98.4398C83.545 101.013 78.7015 102.409 73.7482 102.483C68.7949 102.558 63.9116 101.308 59.6029 98.8634C56.8404 97.2961 54.3784 95.2739 52.3144 92.8938C50.3941 90.6794 47.182 89.7658 44.6436 91.2314Z" fill="#F0F8FF"/>
|
||||||
|
</svg>
|
||||||
|
<h4 className="mt-10 text-primary-500 font-medium text-xl text-center">
|
||||||
|
Mohon tunggu maksimal 2 hari kerja untuk proses aktivasi akun. Kami akan menghubungi kamu jika proses telah selesai.
|
||||||
|
</h4>
|
||||||
|
<a href="/auth/login">
|
||||||
|
<Button className="gap-3 mt-10" size="lg">
|
||||||
|
Kembali
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { FC, ReactElement } from 'react';
|
||||||
|
import { Outlet } from 'react-router-dom';
|
||||||
|
|
||||||
|
export const AppLayout: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className="bg-primary-50 min-h-screen">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { FC, ReactElement, useEffect } from 'react';
|
||||||
|
import { RegisterResetBanner } from '@imphnen-frontend-service/ui/organisms';
|
||||||
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
|
import { ArrowRightOutlined } from '@ant-design/icons';
|
||||||
|
|
||||||
|
export const Components: FC = (): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]">
|
||||||
|
<div className="bg-white xl:min-w-[1220px] min-h-[712px] p-10 rounded-2xl shadow-md flex gap-6">
|
||||||
|
<RegisterResetBanner />
|
||||||
|
<div className="border-2 border-primary-500/50 xl:w-[696px] rounded-lg py-[32px] px-[48px] flex justify-center bg-primary-50">
|
||||||
|
<div className="xl:w-[704px] flex flex-col justify-center items-center">
|
||||||
|
<h2 className="text-4xl text-primary-500 font-semibold">
|
||||||
|
Register Akun Mentor Berhasil
|
||||||
|
</h2>
|
||||||
|
<h4 className="mt-3 text-primary-500 font-medium text-xl">
|
||||||
|
Akun Kamu Telah Berhasil Diverifikasi!
|
||||||
|
</h4>
|
||||||
|
<svg
|
||||||
|
width="198"
|
||||||
|
height="198"
|
||||||
|
viewBox="0 0 198 198"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
className="mt-10"
|
||||||
|
>
|
||||||
|
<circle cx="99" cy="99" r="99" fill="#BCF8B0" />
|
||||||
|
<rect
|
||||||
|
x="31.1152"
|
||||||
|
y="32.9141"
|
||||||
|
width="133.971"
|
||||||
|
height="133.971"
|
||||||
|
rx="66.9857"
|
||||||
|
fill="#35BA43"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M127.935 75.8846H122.722C121.991 75.8846 121.297 76.2202 120.85 76.7945L90.0997 115.748L75.355 97.0657C75.1319 96.7825 74.8476 96.5535 74.5234 96.3959C74.1992 96.2384 73.8435 96.1563 73.483 96.1558H68.2697C67.77 96.1558 67.4941 96.7301 67.7999 97.1179L88.2277 122.998C89.1824 124.206 91.0171 124.206 91.9792 122.998L128.405 76.8392C128.711 76.4589 128.435 75.8846 127.935 75.8846Z"
|
||||||
|
fill="#E0FBD8"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<h4 className="mt-10 text-primary-500 font-medium text-xl text-center">
|
||||||
|
Kamu sekarang bisa login dan mulai menggunakan layanan kami. Selamat bergabung! SENPAIIII
|
||||||
|
</h4>
|
||||||
|
<a href="/auth/login">
|
||||||
|
<Button className="gap-3 mt-10" size="lg">
|
||||||
|
Masuk Jalur Mentor Isekai
|
||||||
|
<ArrowRightOutlined />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Components;
|
||||||
@@ -32,7 +32,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
const m = Math.floor(seconds / 60);
|
const m = Math.floor(seconds / 60);
|
||||||
const s = seconds % 60;
|
const s = seconds % 60;
|
||||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
<div className='flex flex-col justify-center items-center min-h-screen py-[60px] px-[80px]'>
|
||||||
@@ -51,7 +51,7 @@ export const Components: FC = (): ReactElement => {
|
|||||||
name={'otp'}
|
name={'otp'}
|
||||||
maxLength={6}
|
maxLength={6}
|
||||||
control={form.control}
|
control={form.control}
|
||||||
/>
|
/>
|
||||||
<Button type="button" className="mt-2" disabled={disabled} onClick={
|
<Button type="button" className="mt-2" disabled={disabled} onClick={
|
||||||
() => {
|
() => {
|
||||||
resendOTP({email: searchParams.get("email") || ""})
|
resendOTP({email: searchParams.get("email") || ""})
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { Outlet, ScrollRestoration } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function RootLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Outlet />
|
||||||
|
<ScrollRestoration />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
add404PageToRoutesChildren,
|
add404PageToRoutesChildren,
|
||||||
addErrorElementToRoutes,
|
addErrorElementToRoutes,
|
||||||
convertPagesToRoute,
|
convertPagesToRoute,
|
||||||
|
ModalLoginProvider,
|
||||||
QueryProvider,
|
QueryProvider,
|
||||||
} from '@imphnen-frontend-service/utils';
|
} from '@imphnen-frontend-service/utils';
|
||||||
import { Toaster } from 'sonner';
|
import { Toaster } from 'sonner';
|
||||||
@@ -35,8 +36,10 @@ if (!rootElement) throw new Error('Failed to find the root element');
|
|||||||
createRoot(rootElement).render(
|
createRoot(rootElement).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<QueryProvider>
|
<QueryProvider>
|
||||||
<Toaster position="top-right" />
|
<ModalLoginProvider>
|
||||||
<RouterProvider router={router} />
|
<Toaster position="top-right" />
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</ModalLoginProvider>
|
||||||
</QueryProvider>
|
</QueryProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ const mappingPublicRoutes = [
|
|||||||
'/auth/register/otp',
|
'/auth/register/otp',
|
||||||
'/auth/register/success',
|
'/auth/register/success',
|
||||||
'/auth/new-password',
|
'/auth/new-password',
|
||||||
|
'/auth/register-mentor',
|
||||||
|
'/auth/register-mentor/pending',
|
||||||
|
'/auth/register-mentor/success',
|
||||||
|
'/resources',
|
||||||
];
|
];
|
||||||
|
|
||||||
const mappingRoutePermissions = [
|
const mappingRoutePermissions = [
|
||||||
@@ -71,6 +75,11 @@ const mappingRoutePermissions = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const mappingPublicPrefixRoutes = [
|
||||||
|
'/mentoring',
|
||||||
|
'/articles',
|
||||||
|
]
|
||||||
|
|
||||||
//TODO : Fix this later
|
//TODO : Fix this later
|
||||||
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
// const redirectToFirstAccessibleRoute = (userPermissions: string[]) => {
|
||||||
// const fallback = mappingRoutePermissions.find((route) =>
|
// const fallback = mappingRoutePermissions.find((route) =>
|
||||||
@@ -87,6 +96,13 @@ export const middleware = async ({ request }: LoaderFunctionArgs) => {
|
|||||||
const token = session_token?.token?.access_token;
|
const token = session_token?.token?.access_token;
|
||||||
const userPermissions =
|
const userPermissions =
|
||||||
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
session?.role?.permissions?.map?.((perm) => perm?.name) ?? [];
|
||||||
|
|
||||||
|
// Allow to access the landing page without authentication
|
||||||
|
// So, if the route prefix is in the mappingPublicPrefixRoutes, we return null
|
||||||
|
// to indicate that we don't need to authenticate the user
|
||||||
|
if (mappingPublicPrefixRoutes.some((prefix) => pathname.startsWith(prefix))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (mappingPublicRoutes.includes(pathname)) {
|
if (mappingPublicRoutes.includes(pathname)) {
|
||||||
if (token) return redirect('/dashboard');
|
if (token) return redirect('/dashboard');
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
backoffice:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/backoffice.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3001:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: backoffice
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
dimentorin:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/dimentorin.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3002:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: dimentorin
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
version: '3.8'
|
||||||
|
services:
|
||||||
|
gacha:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: docker/gacha.Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3003:80"
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: gacha
|
||||||
@@ -21,7 +21,7 @@ export const usePostLogin = (): UseMutationResult<
|
|||||||
mutationFn: async (payload) => await postLogin(payload),
|
mutationFn: async (payload) => await postLogin(payload),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePostRegister = (): UseMutationResult<
|
export const usePostRegister = (): UseMutationResult<
|
||||||
TResponseMessage,
|
TResponseMessage,
|
||||||
TResponseError,
|
TResponseError,
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
export * from './button';
|
export * from './button';
|
||||||
export * from './input';
|
export * from './input';
|
||||||
export * from './textarea';
|
export * from './textarea';
|
||||||
|
export * from './select'
|
||||||
|
export * from './toggle';
|
||||||
@@ -9,7 +9,7 @@ import { EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons'; // Import
|
|||||||
import { cn } from '@imphnen-frontend-service/utils';
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
import { Button } from '../button';
|
import { Button } from '../button';
|
||||||
|
|
||||||
type TInputType = 'text' | 'email' | 'number' | 'password' | 'file';
|
type TInputType = 'text' | 'email' | 'number' | 'password' | 'file' | 'date' | 'time';
|
||||||
type TInputSize = 'sm' | 'md' | 'lg';
|
type TInputSize = 'sm' | 'md' | 'lg';
|
||||||
type Width = 'standard' | 'custom'
|
type Width = 'standard' | 'custom'
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from "./select"
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
FC,
|
||||||
|
ReactElement,
|
||||||
|
SelectHTMLAttributes,
|
||||||
|
} from 'react';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
type TSelectSize = 'sm' | 'md' | 'lg';
|
||||||
|
type Width = 'standard' | 'custom';
|
||||||
|
|
||||||
|
type TSelectProps = Omit<
|
||||||
|
SelectHTMLAttributes<HTMLSelectElement>,
|
||||||
|
'size'
|
||||||
|
> & {
|
||||||
|
size?: TSelectSize;
|
||||||
|
widthform?: Width;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<TSelectSize, string> = {
|
||||||
|
sm: 'text-[10px] max-h-[34px]',
|
||||||
|
md: 'text-[12px] max-h-[36px]',
|
||||||
|
lg: 'text-[15px] max-h-[38px]',
|
||||||
|
};
|
||||||
|
|
||||||
|
const disabledClass =
|
||||||
|
'opacity-50 hover:border-neutral-200 cursor-not-allowed';
|
||||||
|
|
||||||
|
export const Select: FC<TSelectProps> = ({
|
||||||
|
size = 'md',
|
||||||
|
widthform = 'standard',
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}): ReactElement => {
|
||||||
|
const mergedClassName = cn(
|
||||||
|
`appearance-none px-[12px] py-[8px] text-neutral-800 bg-white placeholder:text-neutral-300 border border-neutral-200 hover:border-blue-300 focus:outline-1 focus:outline-blue-500 rounded-md`,
|
||||||
|
sizeClasses[size],
|
||||||
|
widthform === 'standard' && 'min-w-70',
|
||||||
|
disabled && disabledClass,
|
||||||
|
className
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<select className={mergedClassName} disabled={disabled} {...rest}>
|
||||||
|
{children}
|
||||||
|
</select>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './toggle'
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { cn } from "@imphnen-frontend-service/utils";
|
||||||
|
import { DetailedHTMLProps, FC, HTMLAttributes } from "react";
|
||||||
|
|
||||||
|
export type ToggleInputProps = DetailedHTMLProps<
|
||||||
|
HTMLAttributes<HTMLInputElement>,
|
||||||
|
HTMLInputElement
|
||||||
|
> & {
|
||||||
|
label?: string
|
||||||
|
labelClassName?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ToggleInput: FC<ToggleInputProps> = ({ label, className, labelClassName, ...rest }) => {
|
||||||
|
return (
|
||||||
|
<label className={cn("flex items-center gap-5 cursor-pointer mb-8", className)}>
|
||||||
|
<span className={cn("text-p3 text-gray-800 font-medium", labelClassName)}>{label}</span>
|
||||||
|
<input type="checkbox" className="sr-only peer" {...rest} />
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"w-14 h-7 bg-gray-300 rounded-3xl relative transition-colors",
|
||||||
|
"peer-checked:bg-blue-600 peer-focus:outline peer-focus:outline-blue-500 peer-checked:[&>div]:translate-x-6.5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="absolute left-0.5 top-0.5 h-6 w-6 bg-white rounded-full shadow transition-transform peer-checked:translate-x-6.5" />
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -4,4 +4,6 @@ export * from './input-field';
|
|||||||
export * from './pagination';
|
export * from './pagination';
|
||||||
export * from './modal/modal';
|
export * from './modal/modal';
|
||||||
export * from './stepper';
|
export * from './stepper';
|
||||||
export * from './accordion';
|
export * from './accordion';
|
||||||
|
export * from './register-mentor-step';
|
||||||
|
export * from './select-field'
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './register-mentor-step';
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
import { FC, ReactElement } from "react";
|
||||||
|
|
||||||
|
type TRegisterMentorStep = {
|
||||||
|
step: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const steps = [
|
||||||
|
"Informasi Personal",
|
||||||
|
"Detail Mentor",
|
||||||
|
"Sesi Mentoring & Dokumen Pendukung",
|
||||||
|
"Informasi Akun Pribadi",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const RegisterMentorStep: FC<TRegisterMentorStep> = ({ step }): ReactElement => {
|
||||||
|
const progressPercent = ((step - 1) / (steps.length - 1)) * 100;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="relative w-full px-8">
|
||||||
|
{/* Garis dasar (abu/biru muda) */}
|
||||||
|
<div className="absolute top-5 left-8 right-8 h-0.5 bg-blue-100 z-0" />
|
||||||
|
|
||||||
|
{/* Garis progress biru (dinamis) */}
|
||||||
|
<div
|
||||||
|
className="absolute top-5 left-8 h-0.5 bg-blue-500 z-0 transition-all duration-300"
|
||||||
|
style={{ width: `calc(${progressPercent}% - 0.5rem)` }} // -0.5rem agar tidak overlap ke lingkaran
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex justify-between relative z-10">
|
||||||
|
{steps.map((label, index) => {
|
||||||
|
const currentStep = index + 1;
|
||||||
|
const isActive = currentStep === step;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={index} className="flex flex-col items-center flex-1 text-center">
|
||||||
|
{/* Lingkaran angka */}
|
||||||
|
<div
|
||||||
|
className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${
|
||||||
|
isActive
|
||||||
|
? "bg-gradient-to-b from-purple-600 to-blue-500 text-white border-transparent"
|
||||||
|
: "bg-white text-gray-700 border-blue-400"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<span className="font-bold">{currentStep}</span>
|
||||||
|
</div>
|
||||||
|
{/* Label */}
|
||||||
|
<span className="text-xs text-blue-600 mt-2 leading-tight">{label}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './select-field'
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import {
|
||||||
|
FC,
|
||||||
|
ReactElement,
|
||||||
|
SelectHTMLAttributes,
|
||||||
|
DetailedHTMLProps,
|
||||||
|
} from 'react';
|
||||||
|
import { Select } from '../../atoms'; // Custom select atom kamu
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
export type TSelectSize = 'sm' | 'md' | 'lg';
|
||||||
|
|
||||||
|
export type TSelectFieldProps = Omit<
|
||||||
|
DetailedHTMLProps<SelectHTMLAttributes<HTMLSelectElement>, HTMLSelectElement>,
|
||||||
|
'size'
|
||||||
|
> & {
|
||||||
|
label: string;
|
||||||
|
size?: TSelectSize;
|
||||||
|
error?: string;
|
||||||
|
helperText?: string;
|
||||||
|
htmlFor?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const sizeClasses: Record<TSelectSize, { label: string; helperText: string }> = {
|
||||||
|
lg: {
|
||||||
|
label: 'text-p3 font-medium',
|
||||||
|
helperText: 'text-label3 font-normal',
|
||||||
|
},
|
||||||
|
md: {
|
||||||
|
label: 'text-label1 font-medium',
|
||||||
|
helperText: 'text-label2 font-normal',
|
||||||
|
},
|
||||||
|
sm: {
|
||||||
|
label: 'text-label2 font-medium',
|
||||||
|
helperText: 'text-label2 font-normal',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectField: FC<TSelectFieldProps> = ({
|
||||||
|
label,
|
||||||
|
size = 'md',
|
||||||
|
error,
|
||||||
|
helperText,
|
||||||
|
htmlFor,
|
||||||
|
disabled,
|
||||||
|
className,
|
||||||
|
children,
|
||||||
|
...rest
|
||||||
|
}): ReactElement => {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-[8px]">
|
||||||
|
<label
|
||||||
|
htmlFor={htmlFor}
|
||||||
|
className={cn(
|
||||||
|
'items-start justify-item-start text-start !text-neutral-800',
|
||||||
|
sizeClasses[size].label
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
{...(htmlFor && { id: htmlFor })}
|
||||||
|
size={size}
|
||||||
|
disabled={disabled}
|
||||||
|
className={cn(
|
||||||
|
error &&
|
||||||
|
'border-danger-500 hover:border-danger-500 focus:outline-danger-500',
|
||||||
|
className,
|
||||||
|
disabled && 'opacity-50 cursor-not-allowed'
|
||||||
|
)}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
{error ? (
|
||||||
|
<p className="text-danger-500 text-label1 text-left">{error}</p>
|
||||||
|
) : (
|
||||||
|
helperText && (
|
||||||
|
<p
|
||||||
|
className={cn(
|
||||||
|
'text-label2 text-left',
|
||||||
|
sizeClasses[size].helperText
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{helperText}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,14 +6,14 @@ import { FC, ReactElement } from "react";
|
|||||||
function AuthBanner({text, href}: {text: string, href: string}){
|
function AuthBanner({text, href}: {text: string, href: string}){
|
||||||
return (
|
return (
|
||||||
<div className='hidden xl:block relative rounded-md'>
|
<div className='hidden xl:block relative rounded-md'>
|
||||||
<img src="/image/95319c4f9953dfe6180200e529dfcea5.webp" alt="Banner" className='w-[420px] h-[632px] object-[80%] object-cover rounded-lg' />
|
<img src="/image/95319c4f9953dfe6180200e529dfcea5.webp" alt="Banner" className='w-[420px] min-h-[632px] object-[80%] object-cover rounded-lg' />
|
||||||
<div className='absolute top-0 bg-gradient-to-b from-primary-500 to-transparent w-full rounded-t-lg h-[163px] py-5 px-5'>
|
<div className='absolute top-0 bg-gradient-to-b from-primary-500 to-transparent w-full rounded-t-lg h-[163px] py-5 px-5'>
|
||||||
<Button variant='secondary' className='gap-2' onClick={() => document.location.href = `${href}`}>
|
<Button variant='secondary' className='gap-2' onClick={() => document.location.href = `${href}`}>
|
||||||
<ArrowLeftOutlined />
|
<ArrowLeftOutlined />
|
||||||
<p>{text}</p>
|
<p>{text}</p>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className='absolute bottom-0 bg-gradient-to-t from-primary-500 to-transparent w-full rounded-b-lg h-[263px] flex flex-col items-center justify-center'>
|
<div className='absolute bottom-0 bg-gradient-to-t from-primary-500 to-transparent w-full rounded-b-lg min-h-[263px] flex flex-col items-center justify-center'>
|
||||||
<img src="/image/9261045e09137f3fcb925a78c55b6ddb.webp" alt="Logo" width={317} />
|
<img src="/image/9261045e09137f3fcb925a78c55b6ddb.webp" alt="Logo" width={317} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
import {
|
import {
|
||||||
AppstoreOutlined,
|
AppstoreOutlined,
|
||||||
AuditOutlined,
|
AuditOutlined,
|
||||||
|
BookOutlined,
|
||||||
|
CommentOutlined,
|
||||||
InboxOutlined,
|
InboxOutlined,
|
||||||
LogoutOutlined,
|
LogoutOutlined,
|
||||||
ReloadOutlined,
|
ReloadOutlined,
|
||||||
|
ScheduleOutlined,
|
||||||
|
SettingOutlined,
|
||||||
UsergroupAddOutlined,
|
UsergroupAddOutlined,
|
||||||
UserOutlined,
|
UserOutlined,
|
||||||
UserSwitchOutlined,
|
UserSwitchOutlined,
|
||||||
@@ -11,102 +15,53 @@ import {
|
|||||||
import { Button } from '../../atoms';
|
import { Button } from '../../atoms';
|
||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Link, useLocation } from 'react-router-dom';
|
import { Link, useLocation } from 'react-router-dom';
|
||||||
import { useSession } from '@imphnen-frontend-service/utils';
|
import { cn, For, useSession } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
|
const MENUS = [
|
||||||
|
{ label: 'Dashboard & Set Gacha', href: '/dashboard', icon: <AppstoreOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Dashboard - Dimentorin', href: '/dashboard-dimentorin', icon: <AppstoreOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Gacha Roll', href: '/gacha-roll', icon: <ReloadOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Permissions', href: '/permissions', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Roles', href: '/roles', icon: <UsergroupAddOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Data Akun', href: '/accounts', icon: <UserOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Validasi Transaksi', href: '/transactions', icon: <AuditOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Data Pengiriman Hadiah', href: '/prizes', icon: <InboxOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'User - Dimentorin', href: '/users-dimentorin', icon: <UserSwitchOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Session - Dimentorin', href: '/session-dimentorin', icon: <ScheduleOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Content & Roadmap', href: '/roadmap-dimentorin', icon: <BookOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Feedback & Review', href: '/feedback-review-dimentorin', icon: <CommentOutlined className="text-[20px]" /> },
|
||||||
|
{ label: 'Settings - Dimentorin', href: '/settings-dimentorin', icon: <SettingOutlined className="text-[20px]" /> },
|
||||||
|
]
|
||||||
|
|
||||||
export const BackofficeSidebar: FC = (): ReactElement => {
|
export const BackofficeSidebar: FC = (): ReactElement => {
|
||||||
const { signOut } = useSession();
|
const { signOut } = useSession();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const isActive = (path: string) => location.pathname.includes(path);
|
const isActive = (path: string) => {
|
||||||
|
if (path === '/dashboard' && location.pathname === '/dashboard-dimentorin') return false
|
||||||
|
return location.pathname.includes(path)
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<aside className="sticky top-0 left-0 w-[280px] bg-white min-h-screen py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
<aside className="sticky top-0 left-0 w-[280px] bg-white h-svh py-[60px] px-[28px] shadow-xl flex flex-col justify-between">
|
||||||
<div className="flex flex-col gap-20 justify-between items-center">
|
<div className="flex flex-col gap-20 justify-between items-center">
|
||||||
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
|
<img src="/logos/simple.svg" alt="IMPHNEN Logo" className="w-[150px]" />
|
||||||
|
|
||||||
<nav className="flex flex-col gap-4 w-full">
|
<nav className="flex flex-col gap-4 w-full h-[calc(100svh-20rem)] overflow-y-auto">
|
||||||
<Link
|
<For data={MENUS}>
|
||||||
to="/dashboard"
|
{({ label, href, icon }) => (
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
<Link
|
||||||
isActive('/dashboard')
|
key={href}
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
to={href}
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
className={cn(
|
||||||
}`}
|
"flex items-center justify-items-start gap-3 px-[8px] py-[10px]",
|
||||||
>
|
isActive(href) ? "bg-primary-500 text-white rounded-md" : "text-gray-700 hover:bg-gray-100"
|
||||||
<AppstoreOutlined className="text-[20px]" />
|
)}
|
||||||
<span className="text-p3 font-medium">Dashboard & Set Gacha</span>
|
>
|
||||||
</Link>
|
{icon}
|
||||||
|
<span className="text-p3 font-medium">{label}</span>
|
||||||
<Link
|
</Link>
|
||||||
to="/gacha-roll"
|
)}
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
</For>
|
||||||
isActive('/gacha-roll')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<ReloadOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Gacha Roll</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/permissions"
|
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
|
||||||
isActive('/permissions')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<UserSwitchOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Permissions</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/roles"
|
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
|
||||||
isActive('/roles')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<UsergroupAddOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Roles</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/accounts"
|
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
|
||||||
isActive('/accounts')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<UserOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Data Akun</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/transactions"
|
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
|
||||||
isActive('/transactions')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<AuditOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Validasi Transaksi</span>
|
|
||||||
</Link>
|
|
||||||
|
|
||||||
<Link
|
|
||||||
to="/prizes"
|
|
||||||
className={`flex items-center justify-items-start gap-3 px-[8px] py-[10px] ${
|
|
||||||
isActive('/prizes')
|
|
||||||
? 'bg-primary-500 text-white rounded-md'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
<InboxOutlined className="text-[20px]" />
|
|
||||||
<span className="text-p3 font-medium">Data Pengiriman Hadiah</span>
|
|
||||||
</Link>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { cn } from "@imphnen-frontend-service/utils"
|
||||||
|
import { Icon } from '@iconify/react'
|
||||||
|
import { FC, ReactElement, ReactNode } from "react"
|
||||||
|
import { Button } from "../../atoms"
|
||||||
|
|
||||||
|
export type TBackofficeWrapperProps = {
|
||||||
|
children: ReactNode
|
||||||
|
title?: string
|
||||||
|
className?: string
|
||||||
|
classHeader?: string
|
||||||
|
classTitle?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BackofficeWrapper: FC<TBackofficeWrapperProps> = ({
|
||||||
|
children,
|
||||||
|
title,
|
||||||
|
className,
|
||||||
|
classHeader,
|
||||||
|
classTitle,
|
||||||
|
}): ReactElement => {
|
||||||
|
return (
|
||||||
|
<main className={cn("w-full px-[48px] py-[40px] flex flex-col gap-8", className)}>
|
||||||
|
<header className={cn("bg-white py-5 px-7 rounded-md shadow flex items-center justify-between", classHeader)}>
|
||||||
|
<h1 className={cn("text-[19px] text-primary-500 font-semibold", classTitle)}>{title}</h1>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-x-6">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
className="max-h-full p-3"
|
||||||
|
>
|
||||||
|
<Icon icon="mdi:bell-outline" className="size-6" />
|
||||||
|
</Button>
|
||||||
|
<div className="flex items-center gap-x-6">
|
||||||
|
<div className="text-neutral-600 font-medium">
|
||||||
|
<p className="text-p3">Rizal Syaepulloh</p>
|
||||||
|
<p className="text-label1">Super Admin</p>
|
||||||
|
</div>
|
||||||
|
<div className="size-12 rounded-full overflow-hidden">
|
||||||
|
<img src="/images/asd687hwq6nds4dfjj2983.webp" alt="Profile" className="size-full object-cover" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section>
|
||||||
|
{children}
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './backoffice-wrapper';
|
||||||
@@ -6,10 +6,12 @@ import {
|
|||||||
flexRender,
|
flexRender,
|
||||||
ColumnDef,
|
ColumnDef,
|
||||||
Table,
|
Table,
|
||||||
|
RowData,
|
||||||
} from '@tanstack/react-table';
|
} from '@tanstack/react-table';
|
||||||
import { Pagination } from '../../molecules';
|
import { Pagination } from '../../molecules';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
|
||||||
interface DataTableProps<T> {
|
interface DataTableProps<T> {
|
||||||
data: T[];
|
data: T[];
|
||||||
@@ -18,7 +20,7 @@ interface DataTableProps<T> {
|
|||||||
pageSize?: number;
|
pageSize?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DataTable = <T,>({
|
export const DataTable = <T extends RowData>({
|
||||||
data,
|
data,
|
||||||
columns,
|
columns,
|
||||||
pageSize = 9,
|
pageSize = 9,
|
||||||
@@ -49,7 +51,7 @@ export const DataTable = <T,>({
|
|||||||
{headerGroup.headers.map((header) => (
|
{headerGroup.headers.map((header) => (
|
||||||
<th
|
<th
|
||||||
key={header.id}
|
key={header.id}
|
||||||
className="py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg"
|
className={cn("py-4 px-5 font-normal first:rounded-l-lg last:rounded-r-lg", header?.column?.columnDef?.meta?.headerClassName)}
|
||||||
>
|
>
|
||||||
{header.isPlaceholder
|
{header.isPlaceholder
|
||||||
? null
|
? null
|
||||||
@@ -68,7 +70,7 @@ export const DataTable = <T,>({
|
|||||||
{row.getVisibleCells().map((cell, index) => (
|
{row.getVisibleCells().map((cell, index) => (
|
||||||
<td
|
<td
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
className="py-3 px-5 first:rounded-l-lg last:rounded-r-lg"
|
className={cn("py-3 px-5 first:rounded-l-lg last:rounded-r-lg", cell?.column?.columnDef?.meta?.cellClassName)}
|
||||||
>
|
>
|
||||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||