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:
@@ -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,58 +146,146 @@ 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 (
|
||||
<main className="w-full px-8 py-12 md:px-[60px] lg:px-20">
|
||||
<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.
|
||||
<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>
|
||||
|
||||
<Show
|
||||
condition={SessionToken.get() != null}
|
||||
fallback={
|
||||
<div className="bg-white rounded-lg p-12 text-center shadow-sm border border-neutral-100">
|
||||
<p className="text-[46px] mb-2">🔐</p>
|
||||
<h2 className="text-xl font-semibold text-neutral-800 mb-2">
|
||||
Login dulu, Senpai!
|
||||
</h2>
|
||||
<p className="text-neutral-500 mb-6 max-w-md mx-auto">
|
||||
Dashboard mentor hanya untuk yang sudah terdaftar. Masuk untuk
|
||||
melihat permintaan sesi dan konfirmasi pembayaran.
|
||||
</p>
|
||||
<Link to="/auth/login">
|
||||
<Button variant="primary" size="lg">
|
||||
Login Sekarang
|
||||
</Button>
|
||||
</Link>
|
||||
</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"
|
||||
>
|
||||
<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.
|
||||
<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={sessions?.sessions ?? []}>
|
||||
<For data={all}>
|
||||
{(session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
@@ -189,9 +312,7 @@ export const Components: FC = (): ReactElement => {
|
||||
🧑🎓{' '}
|
||||
{session.mentee_fullname ?? session.mentee_email ?? 'Mentee'}
|
||||
{' · '}
|
||||
{session.session_type === 'video_call'
|
||||
? 'Video Call'
|
||||
: 'Chat'}
|
||||
{session.session_type === 'video_call' ? 'Video Call' : 'Chat'}
|
||||
</p>
|
||||
|
||||
<SessionCard sessionId={session.id} />
|
||||
@@ -207,6 +328,89 @@ export const Components: FC = (): ReactElement => {
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [tab, all, confirmedCount, pendingCount, completedCount, menteeCount]);
|
||||
|
||||
return (
|
||||
<DashboardLayout role="mentor" title="Mentor Dashboard">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<Show
|
||||
condition={SessionToken.get() != null}
|
||||
fallback={
|
||||
<div className="bg-white rounded-lg p-12 text-center shadow-sm border border-neutral-100">
|
||||
<p className="text-[46px] mb-2">🔐</p>
|
||||
<h2 className="text-xl font-semibold text-neutral-800 mb-2">
|
||||
Login dulu, Senpai!
|
||||
</h2>
|
||||
<p className="text-neutral-500 mb-6 max-w-md mx-auto">
|
||||
Dashboard mentor hanya untuk yang sudah terdaftar. Masuk untuk
|
||||
melihat permintaan sesi dan konfirmasi pembayaran.
|
||||
</p>
|
||||
<Link to="/auth/login">
|
||||
<Button variant="primary" size="lg">
|
||||
Login Sekarang
|
||||
</Button>
|
||||
</Link>
|
||||
</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>
|
||||
}
|
||||
>
|
||||
{/* 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>
|
||||
}
|
||||
>
|
||||
@@ -220,21 +424,11 @@ export const Components: FC = (): ReactElement => {
|
||||
))}
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
</main>
|
||||
</DashboardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user