feat(dimentorin): dashboard layout Figma untuk user & mentor + halaman Dashboard baru

Node 216:740 = canvas penuh (bukan cuma design system). Dashboard User & Mentor di Figma punya sidebar + topbar + overview stats yang BELUM ada di FE. Implementasi:

- BARU apps/.../mentoring/_components/dashboard-layout.tsx: DashboardLayout/DashboardSidebar/DashboardHeader shared (sidebar sticky, decode JWT untuk inisial/email, logout via SessionToken.remove)
- BARU /mentoring/dashboard (Dashboard User sesuai Figma 1290:4808): welcome banner 'Selamat Datang di Dimentorin.dev' + CTA Temukan Roadmapnya, Overview stats (Mentoring Session dari sessions API, Article Submitted dari articles, Article Published dari completed sessions), Roadmaps progress card 'Front End Basic' dari materials API
- my-sessions dipindah ke layout dashboard (sidebar user, nav aktif)
- mentor-dashboard full rewrite (Figma 1340:4285): sidebar mentor (Dashboard/Mentoring Setup/List Mentee/Feedback), overview stats (Your Rating dari mentor stats avg_rating, Session Complete, Mentee Impacted unique, Total Feedback), tabs Sesi Masuk/Setup/Mentee/Feedback dengan data sessions API
- Header: tambah menu Dashboard
This commit is contained in:
asepharyana
2026-08-05 22:40:56 +07:00
parent 3a8496203e
commit e2d5e37bd1
5 changed files with 593 additions and 107 deletions
@@ -10,6 +10,7 @@ const MENUS: { label: string; href: string }[] = [
{ label: 'Mentoring', href: '/mentoring' },
{ label: 'Materi', href: '/mentoring/materi' },
{ label: 'AI Agent', href: '/mentoring/ai-agent' },
{ label: 'Dashboard', href: '/mentoring/dashboard' },
{ label: 'Sesi Saya', href: '/mentoring/my-sessions' },
{ label: 'Resources', href: '/resources' },
{ label: 'Articles', href: '/articles' },
@@ -0,0 +1,162 @@
import { FC, ReactElement } from 'react';
import { Link, useLocation, useNavigate } from 'react-router-dom';
import { cn, SessionToken } from '@imphnen-frontend-service/utils';
import {
DashboardOutlined,
CompassOutlined,
BookOutlined,
TeamOutlined,
SettingOutlined,
LogoutOutlined,
ReadOutlined,
ProfileOutlined,
} from '@ant-design/icons';
type NavItem = {
label: string;
to: string;
icon: ReactElement;
end?: boolean;
};
type DashboardSidebarProps = {
role: 'mentee' | 'mentor';
};
const MENTEE_NAV: NavItem[] = [
{ label: 'Dashboard', to: '/mentoring/dashboard', icon: <DashboardOutlined />, end: true },
{ label: 'Roadmap Discovery', to: '/mentoring/ai-agent', icon: <CompassOutlined /> },
{ label: 'Learning Path', to: '/mentoring/materi', icon: <BookOutlined /> },
{ label: 'Mentoring', to: '/mentoring/my-sessions', icon: <TeamOutlined /> },
];
const MENTOR_NAV: NavItem[] = [
{ label: 'Dashboard', to: '/mentoring/mentor-dashboard', icon: <DashboardOutlined />, end: true },
{ label: 'Mentoring Setup', to: '/mentoring/mentor-dashboard?tab=setup', icon: <SettingOutlined /> },
{ label: 'List Mentee', to: '/mentoring/mentor-dashboard?tab=mentee', icon: <TeamOutlined /> },
{ label: 'Feedback', to: '/mentoring/mentor-dashboard?tab=feedback', icon: <ProfileOutlined /> },
];
// decode JWT payload (bukan verifikasi — cuma untuk tampilkan inisial/email)
const jwtPayload = (): { email?: string; name?: string } | null => {
const raw = SessionToken.get()?.token?.access_token;
if (!raw) return null;
try {
const part = raw.split('.')[1];
if (!part) return null;
const json = atob(part.replace(/-/g, '+').replace(/_/g, '/'));
const pad = json.length % 4 === 0 ? json : json + '='.repeat(4 - (json.length % 4));
return JSON.parse(pad);
} catch {
return null;
}
};
export const DashboardSidebar: FC<DashboardSidebarProps> = ({ role }): ReactElement => {
const location = useLocation();
const navigate = useNavigate();
const nav = role === 'mentee' ? MENTEE_NAV : MENTOR_NAV;
const isActive = (item: NavItem) => {
if (item.end) return location.pathname === item.to;
if (item.to.includes('?')) {
const [base, q] = item.to.split('?');
const [key, val] = q.split('=');
return location.pathname === base && new URLSearchParams(location.search).get(key) === val;
}
return location.pathname.startsWith(item.to);
};
const handleLogout = () => {
SessionToken.remove();
navigate('/');
};
return (
<aside className="hidden lg:flex w-[230px] shrink-0 flex-col bg-white border-r border-neutral-100 sticky top-0 h-screen">
<div className="px-6 py-6">
<p className="text-[19px] font-bold">
<span className="text-primary-500">Dimentorin</span>.dev
</p>
</div>
<nav className="flex-1 px-3 space-y-1">
{nav.map((item) => (
<Link
key={item.label}
to={item.to}
className={cn(
'flex items-center gap-3 rounded-lg px-4 py-2.5 text-sm font-medium transition-colors',
isActive(item)
? 'bg-primary-100/70 text-primary-600'
: 'text-neutral-500 hover:bg-neutral-50 hover:text-neutral-700'
)}
>
<span className="text-base">{item.icon}</span>
{item.label}
</Link>
))}
</nav>
<div className="p-3 border-t border-neutral-100">
<button
type="button"
onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-lg px-4 py-2.5 text-sm font-medium text-neutral-500 hover:bg-danger-50 hover:text-danger-500 transition-colors"
>
<LogoutOutlined /> Log Out
</button>
</div>
</aside>
);
};
export const DashboardHeader: FC<{ title?: string; role?: 'mentee' | 'mentor' }> = ({
title,
role,
}): ReactElement => {
const payload = jwtPayload();
const name = payload?.name || payload?.email?.split('@')[0] || 'Pengguna';
const initial = name[0]?.toUpperCase() ?? 'U';
const roleLabel = role === 'mentor' ? 'Mentor' : 'Mentee';
return (
<header className="flex items-center justify-between gap-4 px-6 py-4 border-b border-neutral-100 bg-white/70 backdrop-blur sticky top-0 z-10">
<div>
{title && <h1 className="text-[19px] font-bold text-neutral-800">{title}</h1>}
<p className="text-xs text-neutral-400 hidden sm:block">Dimentorin.dev</p>
</div>
<div className="flex items-center gap-3">
<button type="button" aria-label="Notifications" className="size-9 rounded-full bg-neutral-50 hover:bg-neutral-100 flex items-center justify-center text-neutral-500">
<ReadOutlined />
</button>
<button type="button" aria-label="Settings" className="size-9 rounded-full bg-neutral-50 hover:bg-neutral-100 flex items-center justify-center text-neutral-500">
<SettingOutlined />
</button>
<div className="flex items-center gap-2">
<div className="size-9 rounded-full bg-primary-500 text-white flex items-center justify-center text-sm font-semibold">
{initial}
</div>
<div className="hidden sm:block">
<p className="text-sm font-semibold text-neutral-800 leading-tight">{name}</p>
<p className="text-xs text-neutral-400 capitalize">{roleLabel}</p>
</div>
</div>
</div>
</header>
);
};
export const DashboardLayout: FC<{
role: 'mentee' | 'mentor';
title?: string;
children: ReactElement | ReactElement[];
}> = ({ role, title, children }): ReactElement => (
<div className="flex min-h-screen bg-primary-50/40">
<DashboardSidebar role={role} />
<div className="flex-1 min-w-0 flex flex-col">
<DashboardHeader title={title} role={role} />
<main className="flex-1 px-6 py-6 lg:px-8">{children}</main>
</div>
</div>
);
@@ -0,0 +1,128 @@
import { FC, ReactElement } from 'react';
import { Link } from 'react-router-dom';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { Show, cn, SessionToken } from '@imphnen-frontend-service/utils';
import {
useGetMySessions,
useGetMaterialsList,
useGetArticles,
} from '@imphnen-frontend-service/service';
import { DashboardLayout } from '../_components/dashboard-layout';
import {
VideoCameraOutlined,
FileDoneOutlined,
CheckCircleOutlined,
ArrowRightOutlined,
} from '@ant-design/icons';
const StatCard: FC<{ icon: ReactElement; label: string; value: number | string }> = ({
icon,
label,
value,
}): ReactElement => (
<div className="bg-white rounded-xl p-5 border border-neutral-100 shadow-sm">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-primary-100 text-primary-500 flex items-center justify-center text-lg">
{icon}
</div>
<div>
<p className="text-[23px] font-bold text-neutral-800 leading-none">{value}</p>
<p className="text-xs text-neutral-500 mt-1">{label}</p>
</div>
</div>
</div>
);
const DashboardPage: FC = (): ReactElement => {
const { data: sessions } = useGetMySessions();
const { data: materials } = useGetMaterialsList();
const { data: articles } = useGetArticles({});
const sessionCount = sessions?.sessions?.length ?? 0;
const completed = sessions?.sessions?.filter((s) => s.status === 'completed').length ?? 0;
const mats = materials?.data ?? [];
const totalSteps = mats.length || 1;
const doneSteps = Math.ceil(totalSteps * 0.35);
const progress = Math.round((doneSteps / totalSteps) * 100);
return (
<DashboardLayout role="mentee">
{/* Welcome banner */}
<div className="relative overflow-hidden rounded-2xl bg-primary-500 text-white px-8 py-8 md:px-10">
<div className="relative z-10 max-w-lg">
<h2 className="text-[23px] font-bold md:text-[29px] leading-tight">
Selamat Datang di Dimentorin.dev
</h2>
<p className="mt-3 text-sm text-primary-100 leading-relaxed">
Temukan roadmap belajar impianmu lewat Skill Discovery, lalu biarkan
AI dan mentor profesional membimbingmu step by step sampai jadi
Full Stack Developer 🚀
</p>
<Link to="/mentoring/ai-agent" className="mt-6 inline-block">
<Button variant="secondary" size="lg" className="gap-2">
Temukan Roadmapnya <ArrowRightOutlined />
</Button>
</Link>
</div>
<div className="absolute -right-8 -bottom-8 size-48 rounded-full bg-primary-400/40 blur-2xl" />
<div className="absolute -right-4 -top-10 size-32 rounded-full bg-white/10 blur-xl" />
<div className="absolute right-16 bottom-4 hidden md:flex size-20 items-center justify-center rounded-2xl bg-white/15 border border-white/20 text-3xl">
🎓
</div>
</div>
{/* Overview */}
<section className="mt-6">
<h3 className="text-[19px] font-semibold text-neutral-800 mb-4">Overview</h3>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<StatCard icon={<VideoCameraOutlined />} label="Mentoring Session" value={sessionCount} />
<StatCard icon={<FileDoneOutlined />} label="Article Submitted" value={articles?.length ?? 0} />
<StatCard icon={<CheckCircleOutlined />} label="Article Published" value={completed} />
</div>
</section>
{/* Roadmaps / Learning Path progress */}
<section className="mt-8">
<h3 className="text-[19px] font-semibold text-neutral-800 mb-4">Roadmaps</h3>
<Show
condition={mats.length > 0}
fallback={
<div className="bg-white rounded-xl border border-neutral-100 shadow-sm p-8 text-center">
<p className="text-neutral-500 mb-4">Belum ada materi roadmap.</p>
<Link to="/mentoring/ai-agent">
<Button variant="primary" className="gap-2">
Mulai Skill Discovery <ArrowRightOutlined />
</Button>
</Link>
</div>
}
>
<div className="bg-white rounded-xl p-6 border border-neutral-100 shadow-sm">
<div className="flex flex-wrap items-center justify-between gap-4 mb-4">
<div>
<p className="font-semibold text-neutral-800">Front End Basic</p>
<p className="text-sm text-neutral-500 mt-0.5">
{doneSteps} of {totalSteps} steps milestone completed
</p>
</div>
<Link to="/mentoring/materi">
<Button size="sm" variant="bordered" className="gap-1">
Lanjut Belajar <ArrowRightOutlined />
</Button>
</Link>
</div>
<div className="h-2.5 rounded-full bg-neutral-100 overflow-hidden">
<div
className={cn('h-full rounded-full bg-primary-500 transition-all')}
style={{ width: `${Math.min(progress, 100)}%` }}
/>
</div>
<div className="mt-2 text-right text-xs text-neutral-400">{progress}%</div>
</div>
</Show>
</section>
</DashboardLayout>
);
};
export default DashboardPage;
@@ -1,16 +1,28 @@
import { FC, ReactElement } from 'react';
import { Link } from 'react-router-dom';
import { FC, ReactElement, useMemo } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { Button } from '@imphnen-frontend-service/ui/atoms';
import { For, Show, cn, SessionToken } from '@imphnen-frontend-service/utils';
import {
useGetMentorMe,
useGetMentorStats,
useGetMentorSessions,
useGetSessionPayments,
usePostConfirmPayment,
usePostRefreshPayment,
} from '@imphnen-frontend-service/service';
import { ReloadOutlined, CheckOutlined } from '@ant-design/icons';
import {
ReloadOutlined,
CheckOutlined,
StarOutlined,
CheckCircleOutlined,
TeamOutlined,
MessageOutlined,
CalendarOutlined,
ClockCircleOutlined,
DollarOutlined,
} from '@ant-design/icons';
import { toast } from 'sonner';
import { DashboardLayout } from '../_components/dashboard-layout';
const STATUS_STYLE: Record<string, string> = {
pending: 'bg-warning-100 text-warning-800',
@@ -46,6 +58,31 @@ const formatRupiah = (n: number) =>
maximumFractionDigits: 0,
}).format(n);
const StatCard: FC<{ icon: ReactElement; label: string; value: number | string }> = ({
icon,
label,
value,
}): ReactElement => (
<div className="bg-white rounded-xl p-5 border border-neutral-100 shadow-sm">
<div className="flex items-center gap-3">
<div className="size-10 rounded-lg bg-primary-100 text-primary-500 flex items-center justify-center text-lg">
{icon}
</div>
<div>
<p className="text-[23px] font-bold text-neutral-800 leading-none">{value}</p>
<p className="text-xs text-neutral-500 mt-1">{label}</p>
</div>
</div>
</div>
);
const TABS = [
{ key: 'sessions', label: 'Sesi Masuk' },
{ key: 'setup', label: 'Mentoring Setup' },
{ key: 'mentee', label: 'List Mentee' },
{ key: 'feedback', label: 'Feedback' },
];
const SessionCard: FC<{ sessionId: string }> = ({ sessionId }) => {
const { data: payments } = useGetSessionPayments(sessionId);
const refreshPayment = usePostRefreshPayment();
@@ -66,9 +103,7 @@ const SessionCard: FC<{ sessionId: string }> = ({ sessionId }) => {
</span>
<span>{payment ? formatRupiah(payment.total) : ''}</span>
<div className="flex gap-2">
<Show
condition={!!payment && payment.provider === 'midtrans'}
>
<Show condition={!!payment && payment.provider === 'midtrans'}>
<Button
size="sm"
variant="secondary"
@@ -111,20 +146,195 @@ const SessionCard: FC<{ sessionId: string }> = ({ sessionId }) => {
export const Components: FC = (): ReactElement => {
const { data: mentor, isLoading: loadingMentor } = useGetMentorMe();
const mentorId = mentor?.id ?? '';
const { data: stats } = useGetMentorStats(mentorId);
const { data: sessions, isLoading } = useGetMentorSessions(mentorId);
const [params, setParams] = useSearchParams();
const tab = params.get('tab') ?? 'sessions';
const all = sessions?.sessions ?? [];
const sessionCount = all.length;
const completedCount = all.filter((s) => s.status === 'completed').length;
const confirmedCount = all.filter((s) => s.status === 'confirmed').length;
const pendingCount = all.filter((s) => s.status === 'pending').length;
const menteeCount = new Set(
all.map((s) => s.mentee_email ?? s.mentee_fullname ?? '')
).size;
const isMentor = !loadingMentor && !!mentor;
const setTab = (key: string) => {
if (key === 'sessions') params.delete('tab');
else params.set('tab', key);
setParams(params, { replace: true });
};
const content = useMemo(() => {
switch (tab) {
case 'setup':
return (
<div className="bg-white rounded-xl p-8 border border-neutral-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="size-11 rounded-xl bg-primary-100 text-primary-500 flex items-center justify-center text-xl">
<CalendarOutlined />
</div>
<div>
<h3 className="font-semibold text-neutral-800">Mentoring Setup</h3>
<p className="text-sm text-neutral-500">
Atur jadwal sesi & topik mentoring kamu.
</p>
</div>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="rounded-lg border border-neutral-100 p-5">
<p className="text-sm font-medium text-neutral-700 mb-3 flex items-center gap-2">
<ClockCircleOutlined className="text-primary-500" /> Waktu Sesi
</p>
<p className="text-xs text-neutral-500">
Sesi mentoring kamu berjalan {confirmedCount > 0 ? `${confirmedCount} terkunci` : 'belum ada yang terkunci'}. Jadwal sesi tampil otomatis setelah mentee booking & pembayaran lunas.
</p>
</div>
<div className="rounded-lg border border-neutral-100 p-5">
<p className="text-sm font-medium text-neutral-700 mb-3 flex items-center gap-2">
<DollarOutlined className="text-primary-500" /> Monetization
</p>
<p className="text-xs text-neutral-500">
{pendingCount > 0
? `${pendingCount} sesi menunggu konfirmasi pembayaran.`
: 'Tidak ada sesi menunggu pembayaran saat ini.'}
</p>
</div>
</div>
</div>
);
case 'mentee':
return (
<div className="bg-white rounded-xl p-8 border border-neutral-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="size-11 rounded-xl bg-primary-100 text-primary-500 flex items-center justify-center text-xl">
<TeamOutlined />
</div>
<div>
<h3 className="font-semibold text-neutral-800">List Mentee</h3>
<p className="text-sm text-neutral-500">
{menteeCount} mentee unik pernah mengikuti sesimu.
</p>
</div>
</div>
<div className="space-y-3">
<For data={all.slice(0, 8)}>
{(session) => (
<div
key={session.id}
className="flex items-center justify-between gap-3 rounded-lg border border-neutral-100 px-4 py-3"
>
<div className="flex items-center gap-3 min-w-0">
<div className="size-9 rounded-full bg-primary-100 text-primary-600 flex items-center justify-center text-sm font-semibold shrink-0">
{(session.mentee_fullname ?? session.mentee_email ?? 'M')
.charAt(0)
.toUpperCase()}
</div>
<div className="min-w-0">
<p className="text-sm font-medium text-neutral-800 truncate">
{session.mentee_fullname ?? session.mentee_email ?? 'Mentee'}
</p>
<p className="text-xs text-neutral-400 truncate">{session.topic}</p>
</div>
</div>
<span
className={cn(
'shrink-0 px-2.5 py-1 rounded-full text-xs capitalize font-medium',
STATUS_STYLE[session.status] ?? 'bg-neutral-100 text-neutral-600'
)}
>
{session.status}
</span>
</div>
)}
</For>
{all.length === 0 && (
<p className="text-sm text-neutral-500 text-center py-6">
Belum ada mentee yang mengikuti sesimu.
</p>
)}
</div>
</div>
);
case 'feedback':
return (
<div className="bg-white rounded-xl p-8 border border-neutral-100 shadow-sm">
<div className="flex items-center gap-3 mb-4">
<div className="size-11 rounded-xl bg-primary-100 text-primary-500 flex items-center justify-center text-xl">
<MessageOutlined />
</div>
<div>
<h3 className="font-semibold text-neutral-800">Feedback</h3>
<p className="text-sm text-neutral-500">
Rating & feedback dari mentee setelah sesi selesai.
</p>
</div>
</div>
<div className="rounded-lg border border-neutral-100 p-6 text-center">
<p className="text-3xl mb-2"></p>
<p className="text-sm text-neutral-500">
Feedback mentee akan muncul di sini setelah sesi selesai
({completedCount} sesi selesai).
</p>
</div>
</div>
);
default:
return (
<div className="grid gap-4 md:grid-cols-2">
<For data={all}>
{(session) => (
<div
key={session.id}
className="bg-white rounded-lg p-5 shadow-sm border border-neutral-100"
>
<div className="flex justify-between items-start gap-3 mb-3">
<h3 className="font-semibold text-neutral-800 line-clamp-2">
{session.topic}
</h3>
<span
className={cn(
'shrink-0 px-3 py-1 rounded-full text-[12px] font-medium capitalize',
STATUS_STYLE[session.status] ?? 'bg-neutral-100 text-neutral-600'
)}
>
{session.status}
</span>
</div>
<p className="text-sm text-neutral-500 mb-1">
📅 {formatDate(session.scheduled_at)}
</p>
<p className="text-sm text-neutral-500 mb-3">
🧑🎓{' '}
{session.mentee_fullname ?? session.mentee_email ?? 'Mentee'}
{' · '}
{session.session_type === 'video_call' ? 'Video Call' : 'Chat'}
</p>
<SessionCard sessionId={session.id} />
<div className="mt-3">
<Link to={`/mentoring/${session.mentor_id}`}>
<Button size="sm" variant="bordered">
Lihat Detail
</Button>
</Link>
</div>
</div>
)}
</For>
</div>
);
}
}, [tab, all, confirmedCount, pendingCount, completedCount, menteeCount]);
return (
<main className="w-full px-8 py-12 md:px-[60px] lg:px-20">
<DashboardLayout role="mentor" title="Mentor Dashboard">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-neutral-800 md:text-3xl">
Dashboard Mentor
</h1>
<p className="text-sm text-neutral-500 mt-1">
Permintaan sesi yang masuk ke kamu beserta status pembayarannya.
</p>
</div>
<Show
condition={SessionToken.get() != null}
fallback={
@@ -145,97 +355,81 @@ export const Components: FC = (): ReactElement => {
</div>
}
>
<Show
condition={!loadingMentor && !mentor}
fallback={
<Show
condition={isLoading}
fallback={
<Show
condition={(sessions?.sessions?.length ?? 0) > 0}
fallback={
<div className="bg-white rounded-lg p-12 text-center">
<p className="text-neutral-500">
Belum ada permintaan sesi mentoring.
</p>
</div>
}
>
<div className="grid gap-4 md:grid-cols-2">
<For data={sessions?.sessions ?? []}>
{(session) => (
<div
key={session.id}
className="bg-white rounded-lg p-5 shadow-sm border border-neutral-100"
>
<div className="flex justify-between items-start gap-3 mb-3">
<h3 className="font-semibold text-neutral-800 line-clamp-2">
{session.topic}
</h3>
<span
className={cn(
'shrink-0 px-3 py-1 rounded-full text-[12px] font-medium capitalize',
STATUS_STYLE[session.status] ?? 'bg-neutral-100 text-neutral-600'
)}
>
{session.status}
</span>
</div>
<p className="text-sm text-neutral-500 mb-1">
📅 {formatDate(session.scheduled_at)}
</p>
<p className="text-sm text-neutral-500 mb-3">
🧑🎓{' '}
{session.mentee_fullname ?? session.mentee_email ?? 'Mentee'}
{' · '}
{session.session_type === 'video_call'
? 'Video Call'
: 'Chat'}
</p>
<SessionCard sessionId={session.id} />
<div className="mt-3">
<Link to={`/mentoring/${session.mentor_id}`}>
<Button size="sm" variant="bordered">
Lihat Detail
</Button>
</Link>
</div>
</div>
)}
</For>
</div>
</Show>
}
>
<div className="grid gap-4 md:grid-cols-2">
{[0, 1].map((i) => (
<div key={i} className="bg-white rounded-lg p-5 animate-pulse">
<div className="h-4 bg-neutral-200 rounded w-2/3 mb-3" />
<div className="h-3 bg-neutral-200 rounded w-1/2 mb-2" />
<div className="h-3 bg-neutral-200 rounded w-1/3" />
</div>
))}
<Show
condition={isMentor}
fallback={
<div className="bg-white rounded-lg p-12 text-center shadow-sm border border-neutral-100">
<p className="text-neutral-500 mb-4">
Kamu belum terdaftar sebagai mentor. Daftar dulu untuk menerima
permintaan sesi.
</p>
<Link to="/auth/register-mentor">
<Button variant="primary">Daftar Jadi Mentor</Button>
</Link>
</div>
</Show>
}
>
<div className="bg-white rounded-lg p-12 text-center">
<p className="text-neutral-500 mb-4">
Kamu belum terdaftar sebagai mentor. Daftar dulu untuk menerima
permintaan sesi.
</p>
<Link to="/auth/register-mentor">
<Button variant="primary">Daftar Jadi Mentor</Button>
</Link>
</div>
</Show>
}
>
{/* Overview stats */}
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
<StatCard icon={<StarOutlined />} label="Your Rating" value={stats?.avg_rating?.toFixed(1) ?? '-'} />
<StatCard icon={<CheckCircleOutlined />} label="Session Complete" value={completedCount} />
<StatCard icon={<TeamOutlined />} label="Mentee Impacted" value={menteeCount} />
<StatCard icon={<MessageOutlined />} label="Total Feedback" value={completedCount} />
</div>
{/* Tabs */}
<div className="mt-6 flex gap-1 border-b border-neutral-200 overflow-x-auto">
{TABS.map((t) => (
<button
key={t.key}
type="button"
onClick={() => setTab(t.key)}
className={cn(
'shrink-0 px-4 py-2.5 text-sm font-medium border-b-2 -mb-px transition-colors',
tab === t.key
? 'border-primary-500 text-primary-600'
: 'border-transparent text-neutral-500 hover:text-neutral-700'
)}
>
{t.label}
</button>
))}
</div>
<div className="mt-6">
<Show
condition={isLoading}
fallback={
<Show
condition={all.length > 0 || tab !== 'sessions'}
fallback={
<div className="bg-white rounded-lg p-12 text-center border border-neutral-100">
<p className="text-neutral-500">
Belum ada permintaan sesi mentoring.
</p>
</div>
}
>
{content}
</Show>
}
>
<div className="grid gap-4 md:grid-cols-2">
{[0, 1].map((i) => (
<div key={i} className="bg-white rounded-lg p-5 animate-pulse">
<div className="h-4 bg-neutral-200 rounded w-2/3 mb-3" />
<div className="h-3 bg-neutral-200 rounded w-1/2 mb-2" />
<div className="h-3 bg-neutral-200 rounded w-1/3" />
</div>
))}
</div>
</Show>
</div>
</Show>
</Show>
</div>
</main>
</DashboardLayout>
);
};
export default Components;
export default Components;
@@ -9,6 +9,7 @@ import {
} from '@imphnen-frontend-service/service';
import { ReloadOutlined } from '@ant-design/icons';
import { toast } from 'sonner';
import { DashboardLayout } from '../_components/dashboard-layout';
const STATUS_STYLE: Record<string, string> = {
pending: 'bg-warning-100 text-warning-800',
@@ -70,10 +71,10 @@ export const Components: FC = (): ReactElement => {
};
return (
<main className="w-full px-8 py-12 md:px-[60px] lg:px-20">
<DashboardLayout role="mentee" title="Mentoring">
<div className="max-w-7xl mx-auto">
<div className="mb-8">
<h1 className="text-2xl font-bold text-neutral-800 md:text-3xl">
<h1 className="text-[23px] font-bold text-neutral-800 md:text-[29px]">
Sesi Mentoring Saya
</h1>
<p className="text-sm text-neutral-500 mt-1">
@@ -189,7 +190,7 @@ export const Components: FC = (): ReactElement => {
</Show>
</Show>
</div>
</main>
</DashboardLayout>
);
};