feat: add dimentorin dashboard and settings pages (#42, #43, #54)

Dashboard (/dashboard):
- Welcome header with user name
- Stats cards: upcoming sessions, completed, role (mentor/mentee)
- Mentor registration CTA for non-mentors
- Upcoming sessions list with status badges
- Profile card with avatar and gradient header

Settings (/dashboard/settings):
- Account Details: displays user info and role
- Privacy & Security: 2FA (coming soon), change password, active sessions
- Preferences: email notifications, language, theme
- FAQ & Support: expandable FAQ accordion with contact info

Dashboard Layout:
- Top nav with Dimentorin branding
- Navigation links: Dashboard, Settings
- Auth guard redirects to login if not authenticated
- Logout button

Closes #42, #43, #54

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
maulanasdqn
2026-04-10 14:31:49 +07:00
co-authored by Claude Opus 4.6
parent 7268403ca4
commit 414e4d19b8
3 changed files with 424 additions and 0 deletions
@@ -0,0 +1,66 @@
import { Outlet, Link, useLocation, useNavigate } from 'react-router';
import { useAuthStore } from '@imphnen-frontend-service/service';
import { Icon } from '@iconify/react';
import { useEffect } from 'react';
const navItems = [
{ path: '/dashboard', label: 'Dashboard', icon: 'mdi:view-dashboard' },
{ path: '/dashboard/settings', label: 'Settings', icon: 'mdi:cog' },
];
export default function DashboardLayout() {
const { session, clearSession } = useAuthStore();
const location = useLocation();
const navigate = useNavigate();
useEffect(() => {
if (!session?.token) navigate('/auth/login');
}, [session, navigate]);
return (
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<nav className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
<div className="flex items-center gap-6">
<Link to="/" className="text-xl font-bold text-primary-600 dark:text-primary-400">
Dimentorin
</Link>
<div className="hidden md:flex gap-1">
{navItems.map((item) => (
<Link
key={item.path}
to={item.path}
className={`px-3 py-2 rounded-lg text-sm font-medium flex items-center gap-2 transition-colors ${
location.pathname === item.path
? 'bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800'
}`}
>
<Icon icon={item.icon} width="18" />
{item.label}
</Link>
))}
</div>
</div>
<div className="flex items-center gap-4">
<span className="text-sm text-gray-600 dark:text-gray-400 hidden sm:block">
{session?.user?.fullname || session?.user?.email}
</span>
<button
onClick={() => { clearSession(); navigate('/auth/login'); }}
className="text-sm text-red-500 hover:text-red-600 cursor-pointer flex items-center gap-1"
>
<Icon icon="mdi:logout" width="18" />
<span className="hidden sm:inline">Logout</span>
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Outlet />
</main>
</div>
);
}
+173
View File
@@ -0,0 +1,173 @@
import { Link } from 'react-router';
import {
useAuthStore,
useSessionQuery,
useMySessions,
useMentorMe,
} from '@imphnen-frontend-service/service';
import { Icon } from '@iconify/react';
export default function DashboardPage() {
const { session } = useAuthStore();
const { data: meData } = useSessionQuery(['mentor', 'sessions']);
const { data: sessionsData } = useMySessions();
const { data: mentorData } = useMentorMe();
const user = session?.user;
const mentor = meData?.user?.mentor;
const sessions = sessionsData?.data || [];
const upcomingSessions = sessions.filter(
(s: { status: string }) => s.status === 'confirmed' || s.status === 'pending'
);
const completedSessions = sessions.filter(
(s: { status: string }) => s.status === 'completed'
);
return (
<div>
<div className="mb-8">
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
Welcome, {user?.fullname || user?.email?.split('@')[0] || 'User'}!
</h1>
<p className="text-gray-600 dark:text-gray-400 mt-1 font-sans">
{mentor ? 'Manage your mentoring sessions and mentees' : 'Find mentors and book sessions'}
</p>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-blue-100 dark:bg-blue-900/30 flex items-center justify-center">
<Icon icon="mdi:calendar-clock" className="text-blue-600 dark:text-blue-400 text-xl" />
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">Upcoming</span>
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">{upcomingSessions.length}</p>
</div>
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-green-100 dark:bg-green-900/30 flex items-center justify-center">
<Icon icon="mdi:check-circle" className="text-green-600 dark:text-green-400 text-xl" />
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">Completed</span>
</div>
<p className="text-3xl font-bold text-gray-900 dark:text-white">{completedSessions.length}</p>
</div>
<div className="bg-white dark:bg-gray-900 rounded-xl p-6 shadow-sm border border-gray-200 dark:border-gray-700">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-purple-100 dark:bg-purple-900/30 flex items-center justify-center">
<Icon icon="mdi:account-star" className="text-purple-600 dark:text-purple-400 text-xl" />
</div>
<span className="text-sm text-gray-600 dark:text-gray-400">Role</span>
</div>
<p className="text-lg font-bold text-gray-900 dark:text-white">
{mentor ? 'Mentor' : 'Mentee'}
</p>
{mentor?.status && (
<span className={`inline-block mt-1 px-2 py-0.5 rounded text-xs font-medium ${
mentor.status === 'verified'
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400'
: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400'
}`}>
{mentor.status}
</span>
)}
</div>
</div>
{/* Mentor CTA */}
{!mentor && (
<div className="mb-8 bg-primary-50 dark:bg-primary-900/20 border border-primary-200 dark:border-primary-800 rounded-xl p-6">
<div className="flex items-center justify-between flex-wrap gap-4">
<div>
<h3 className="font-bold text-primary-900 dark:text-primary-100 text-lg">Become a Mentor</h3>
<p className="text-primary-700 dark:text-primary-300 text-sm mt-1">
Share your knowledge and help others grow in their career.
</p>
</div>
<Link
to="/auth/register-mentor"
className="px-6 py-2 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
Register as Mentor
</Link>
</div>
</div>
)}
{/* Upcoming Sessions */}
<div className="mb-8">
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-bold text-gray-900 dark:text-white">
{upcomingSessions.length > 0 ? 'Upcoming Sessions' : 'No Upcoming Sessions'}
</h2>
<Link
to="/mentoring"
className="text-sm text-primary-600 dark:text-primary-400 hover:text-primary-700 flex items-center gap-1"
>
Browse Mentors
<Icon icon="mdi:chevron-right" width="18" />
</Link>
</div>
{upcomingSessions.length > 0 ? (
<div className="space-y-3">
{upcomingSessions.slice(0, 5).map((s: any) => (
<div key={s.id} className="bg-white dark:bg-gray-900 rounded-lg p-4 shadow-sm border border-gray-200 dark:border-gray-700 flex items-center justify-between">
<div className="flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-primary-100 dark:bg-primary-900/30 flex items-center justify-center">
<Icon icon="mdi:video" className="text-primary-600 dark:text-primary-400" />
</div>
<div>
<p className="font-medium text-gray-900 dark:text-white">{s.topic}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{new Date(s.scheduled_at).toLocaleDateString('en-US', {
weekday: 'short', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit'
})}
{' '}&middot;{' '}{s.duration_minutes} min
</p>
</div>
</div>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${
s.status === 'confirmed'
? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400'
: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400'
}`}>
{s.status}
</span>
</div>
))}
</div>
) : (
<div className="bg-white dark:bg-gray-900 rounded-xl p-8 text-center shadow-sm border border-gray-200 dark:border-gray-700">
<Icon icon="mdi:calendar-blank" className="text-5xl text-gray-300 dark:text-gray-600 mx-auto mb-3" />
<p className="text-gray-500 dark:text-gray-400">
No upcoming sessions. Browse mentors to book your first session!
</p>
</div>
)}
</div>
{/* Profile Card */}
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="bg-gradient-to-r from-primary-600 to-primary-500 h-20"></div>
<div className="px-6 pb-6">
<div className="flex items-end gap-4 -mt-10 mb-4">
{user?.avatar ? (
<img src={user.avatar} alt="" className="w-20 h-20 rounded-full border-4 border-white dark:border-gray-900 shadow-lg object-cover" />
) : (
<div className="w-20 h-20 rounded-full border-4 border-white dark:border-gray-900 shadow-lg bg-gray-200 dark:bg-gray-700 flex items-center justify-center">
<span className="text-3xl text-gray-400">U</span>
</div>
)}
<div className="pb-1">
<h3 className="font-bold text-gray-900 dark:text-white text-lg">{user?.fullname}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{user?.email}</p>
</div>
</div>
</div>
</div>
</div>
);
}
@@ -0,0 +1,185 @@
import { useState } from 'react';
import {
useAuthStore,
useSessionQuery,
} from '@imphnen-frontend-service/service';
import { Icon } from '@iconify/react';
import { toast } from 'sonner';
type SettingsSection = 'account' | 'privacy' | 'preferences' | 'faq';
const faqItems = [
{ q: 'How do I book a mentoring session?', a: 'Browse available mentors, select one, and click "Book Session" to schedule a meeting.' },
{ q: 'How do I become a mentor?', a: 'Go to the registration page and fill in your professional details. Your application will be reviewed by our team.' },
{ q: 'Can I cancel a session?', a: 'Yes, you can cancel a pending or confirmed session from your dashboard before the scheduled time.' },
{ q: 'How do I update my profile?', a: 'Go to Settings > Account Details to update your personal information.' },
{ q: 'Is my data secure?', a: 'Yes, we use encryption and secure protocols to protect your data. See our Privacy Policy for details.' },
];
export default function SettingsPage() {
const { session } = useAuthStore();
const { data: meData } = useSessionQuery();
const [activeSection, setActiveSection] = useState<SettingsSection>('account');
const [expandedFaq, setExpandedFaq] = useState<number | null>(null);
const user = session?.user;
const sections = [
{ id: 'account' as const, label: 'Account Details', icon: 'mdi:account-circle' },
{ id: 'privacy' as const, label: 'Privacy & Security', icon: 'mdi:shield-lock' },
{ id: 'preferences' as const, label: 'Preferences', icon: 'mdi:tune' },
{ id: 'faq' as const, label: 'FAQ & Support', icon: 'mdi:help-circle' },
];
return (
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">Settings</h1>
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
{/* Sidebar */}
<div className="md:col-span-1">
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 overflow-hidden">
{sections.map((s) => (
<button
key={s.id}
onClick={() => setActiveSection(s.id)}
className={`w-full px-4 py-3 flex items-center gap-3 text-left text-sm font-medium transition-colors cursor-pointer ${
activeSection === s.id
? 'bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 border-l-3 border-primary-600'
: 'text-gray-600 dark:text-gray-400 hover:bg-gray-50 dark:hover:bg-gray-800'
}`}
>
<Icon icon={s.icon} width="20" />
{s.label}
</button>
))}
</div>
</div>
{/* Content */}
<div className="md:col-span-3">
{activeSection === 'account' && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Account Details</h2>
<div className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Full Name</label>
<input type="text" defaultValue={user?.fullname || ''} disabled className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white disabled:opacity-60" />
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Email</label>
<input type="email" defaultValue={user?.email || ''} disabled className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white disabled:opacity-60" />
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Role</label>
<input type="text" defaultValue={user?.role?.name || 'User'} disabled className="w-full px-4 py-2.5 border border-gray-300 dark:border-gray-600 rounded-lg bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white disabled:opacity-60" />
</div>
<p className="text-sm text-gray-500 dark:text-gray-400">
To update your profile, use the profile page from the main menu.
</p>
</div>
</div>
)}
{activeSection === 'privacy' && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Privacy & Security</h2>
<div className="space-y-6">
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Two-Factor Authentication</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Add an extra layer of security to your account</p>
</div>
<span className="px-3 py-1 bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 rounded-full text-xs font-medium">Coming Soon</span>
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Change Password</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Update your account password</p>
</div>
<button
onClick={() => toast.info('Use the forgot password flow to reset your password')}
className="px-4 py-2 text-sm bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors cursor-pointer"
>
Change
</button>
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Active Sessions</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">You are currently logged in on this device</p>
</div>
<span className="flex items-center gap-1 text-green-600 dark:text-green-400 text-sm font-medium">
<Icon icon="mdi:circle" width="8" />
Active
</span>
</div>
</div>
</div>
)}
{activeSection === 'preferences' && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Preferences</h2>
<div className="space-y-6">
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Email Notifications</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Receive email updates about your sessions</p>
</div>
<span className="px-3 py-1 bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 rounded-full text-xs font-medium">Enabled</span>
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Language</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Choose your preferred language</p>
</div>
<span className="text-sm text-gray-700 dark:text-gray-300 font-medium">English</span>
</div>
<div className="flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-800 rounded-lg">
<div>
<p className="font-medium text-gray-900 dark:text-white">Theme</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Toggle between light and dark mode</p>
</div>
<span className="text-sm text-gray-700 dark:text-gray-300 font-medium">System</span>
</div>
</div>
</div>
)}
{activeSection === 'faq' && (
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-sm border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-xl font-bold text-gray-900 dark:text-white mb-6">FAQ & Support</h2>
<div className="space-y-3">
{faqItems.map((item, i) => (
<div key={i} className="border border-gray-200 dark:border-gray-700 rounded-lg overflow-hidden">
<button
onClick={() => setExpandedFaq(expandedFaq === i ? null : i)}
className="w-full px-4 py-3 flex items-center justify-between text-left cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-800"
>
<span className="font-medium text-gray-900 dark:text-white text-sm">{item.q}</span>
<Icon icon={expandedFaq === i ? 'mdi:chevron-up' : 'mdi:chevron-down'} className="text-gray-400 shrink-0" />
</button>
{expandedFaq === i && (
<div className="px-4 pb-3">
<p className="text-sm text-gray-600 dark:text-gray-400">{item.a}</p>
</div>
)}
</div>
))}
</div>
<div className="mt-6 p-4 bg-primary-50 dark:bg-primary-900/20 rounded-lg">
<p className="text-sm text-primary-800 dark:text-primary-200">
<strong>Need more help?</strong> Contact us at{' '}
<a href="mailto:support@imphnen.dev" className="underline">support@imphnen.dev</a>
</p>
</div>
</div>
)}
</div>
</div>
</div>
);
}