Dashboard users part 2 (#79)
* fix: add FigmaImage directory to .gitignore * feat: enhance dashboard tests and add new components for rendering * feat: add navigation functionality and UI enhancements to dashboard components * feat: add article preview and mentor contact modals, implement article builder and FAQ page * refactor: remove FAQ route and update settings layout for improved responsiveness * feat: enhance two-step authentication UI and functionality with improved styles and user feedback * feat: add mentoring detail and feedback modals, enhance mentoring page functionality and UI * fix: handle JSON parse failure in edit profile modal error handling
This commit is contained in:
+1
-1
@@ -4,7 +4,7 @@
|
||||
dist
|
||||
tmp
|
||||
out-tsc
|
||||
|
||||
docs/FigmaImage/
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
|
||||
@@ -58,9 +58,29 @@ test.describe('Automated Route Discovery & Screenshots', () => {
|
||||
console.log(`Processing: ${baseURL}${route}`);
|
||||
|
||||
try {
|
||||
// Mock auth session if accessing protected routes
|
||||
if (route.startsWith('/dashboard') || route.startsWith('/profile')) {
|
||||
await page.addInitScript(() => {
|
||||
window.localStorage.setItem('auth-storage', JSON.stringify({
|
||||
state: {
|
||||
session: {
|
||||
user: {
|
||||
id: 'sample-user-id',
|
||||
fullname: 'John Doe',
|
||||
email: 'john@example.com',
|
||||
avatar: '/image/mascot-character.webp'
|
||||
},
|
||||
token: 'mock-token'
|
||||
}
|
||||
},
|
||||
version: 0
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
await page.goto(`${baseURL}${route}`, {
|
||||
waitUntil: 'networkidle',
|
||||
timeout: 30000
|
||||
timeout: 60000
|
||||
});
|
||||
|
||||
// Wait for rendering
|
||||
@@ -75,6 +95,118 @@ test.describe('Automated Route Discovery & Screenshots', () => {
|
||||
});
|
||||
|
||||
console.log(`Success: ${fileName}.png`);
|
||||
|
||||
// --- Custom logic for Modals and Tabs ---
|
||||
|
||||
// 1. Mentoring Modals
|
||||
if (route === '/dashboard/mentoring') {
|
||||
// Detail modal
|
||||
const detailButton = page.getByRole('button', { name: 'Cek Detail' }).first();
|
||||
if (await detailButton.isVisible()) {
|
||||
await detailButton.click({ timeout: 5000 });
|
||||
const detailModal = page.locator('div.fixed.inset-0').filter({ hasText: 'Detail Sesi Mentoring' }).first();
|
||||
await detailModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_mentoring_modal_detail.png') });
|
||||
await detailModal.locator('button').first().click({ timeout: 5000 });
|
||||
await detailModal.waitFor({ state: 'hidden', timeout: 5000 });
|
||||
}
|
||||
|
||||
// Contact modal
|
||||
const cancelButton = page.getByRole('button', { name: 'Cancel' }).first();
|
||||
if (await cancelButton.isVisible()) {
|
||||
await cancelButton.click({ timeout: 5000 });
|
||||
const contactModal = page.locator('div.fixed.inset-0').filter({ hasText: 'Hubungi mentor melalui platform berikut' }).first();
|
||||
await contactModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_mentoring_modal_contact.png') });
|
||||
await contactModal.getByRole('button', { name: 'Tutup' }).click({ timeout: 5000 });
|
||||
await contactModal.waitFor({ state: 'hidden', timeout: 5000 });
|
||||
}
|
||||
|
||||
// Feedback modal
|
||||
const feedbackBtn = page.getByRole('button', { name: 'Kirim Feedback' }).first();
|
||||
if (await feedbackBtn.isVisible()) {
|
||||
await feedbackBtn.click({ timeout: 5000 });
|
||||
const feedbackModal = page
|
||||
.locator('div.fixed.inset-0')
|
||||
.filter({ hasText: /Beri Feedback|Feedback Mentor/ })
|
||||
.first();
|
||||
await feedbackModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_mentoring_modal_feedback.png') });
|
||||
const closeFeedbackBtn = feedbackModal.getByRole('button', { name: 'Batal' });
|
||||
if (await closeFeedbackBtn.isVisible()) {
|
||||
await closeFeedbackBtn.click({ timeout: 5000 });
|
||||
await feedbackModal.waitFor({ state: 'hidden', timeout: 5000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Learning Path Tabs
|
||||
if (route === '/dashboard/learning-path') {
|
||||
await page.click('button:has-text("Article")');
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_learning-path_tab_article.png') });
|
||||
}
|
||||
|
||||
// 3. Settings Sections & 2FA Modals
|
||||
if (route === '/dashboard/settings') {
|
||||
const sections = ['Privasi & Keamanan', 'FAQ', 'Laporkan Kendala', 'Umpan Balik'];
|
||||
for (const section of sections) {
|
||||
const sectionButton = page.getByRole('button', { name: section }).first();
|
||||
if (!(await sectionButton.isVisible())) {
|
||||
console.warn(`Section button not found: ${section}`);
|
||||
continue;
|
||||
}
|
||||
|
||||
await sectionButton.click({ timeout: 5000 });
|
||||
await page.waitForTimeout(300);
|
||||
const slug = section
|
||||
.toLowerCase()
|
||||
.replace(/&/g, 'and')
|
||||
.replace(/\s+/g, '_')
|
||||
.replace(/[^a-z0-9_]/g, '');
|
||||
await page.screenshot({ path: path.join(screenshotDir, `dashboard_settings_section_${slug}.png`) });
|
||||
|
||||
// If in Privacy, test 2FA modal
|
||||
if (section === 'Privasi & Keamanan') {
|
||||
const enable2FaButton = page.getByRole('button', { name: 'Aktifkan 2FA' }).first();
|
||||
if (!(await enable2FaButton.isVisible())) {
|
||||
console.warn('2FA trigger button not found');
|
||||
continue;
|
||||
}
|
||||
|
||||
await enable2FaButton.click({ timeout: 5000 });
|
||||
const emailModal = page.locator('div.fixed.inset-0').filter({ hasText: 'Verifikasi Email' }).first();
|
||||
await emailModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_settings_modal_2fa_step1.png') });
|
||||
|
||||
await emailModal.getByRole('button', { name: 'Kirim Kode' }).click({ timeout: 5000 });
|
||||
const otpModal = page.locator('div.fixed.inset-0').filter({ hasText: 'Masukkan Kode OTP' }).first();
|
||||
await otpModal.waitFor({ state: 'visible', timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_settings_modal_2fa_step2.png') });
|
||||
|
||||
await otpModal.locator('button').first().click({ timeout: 5000 });
|
||||
await otpModal.waitFor({ state: 'hidden', timeout: 5000 });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Header Notifications
|
||||
if (route === '/dashboard') {
|
||||
const notificationButton = page.locator('header button').first();
|
||||
if (await notificationButton.isVisible()) {
|
||||
await notificationButton.click({ timeout: 5000 });
|
||||
await page.waitForTimeout(500);
|
||||
await page.screenshot({ path: path.join(screenshotDir, 'dashboard_header_notifications.png') });
|
||||
} else {
|
||||
console.warn('Notification button not found on dashboard header');
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error(`Failed to capture ${route}:`, error.message);
|
||||
}
|
||||
|
||||
@@ -39,5 +39,8 @@ export default defineConfig({
|
||||
reuseExistingServer: true,
|
||||
cwd: '../../', // Run from root where Nx is available
|
||||
timeout: 120000,
|
||||
env: {
|
||||
VITE_BYPASS_AUTH_MIDDLEWARE: 'true',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -32,6 +32,7 @@ import { Route as AuthenticatedDashboardSettingsRouteImport } from './routes/_au
|
||||
import { Route as AuthenticatedDashboardRoadmapDiscoveryRouteImport } from './routes/_authenticated/dashboard/roadmap-discovery'
|
||||
import { Route as AuthenticatedDashboardMentoringRouteImport } from './routes/_authenticated/dashboard/mentoring'
|
||||
import { Route as AuthenticatedDashboardLearningPathRouteImport } from './routes/_authenticated/dashboard/learning-path'
|
||||
import { Route as AuthenticatedDashboardArticleBuilderRouteImport } from './routes/_authenticated/dashboard/article-builder'
|
||||
import { Route as PublicAuthRegisterSuccessRouteImport } from './routes/_public/auth/register_/success'
|
||||
import { Route as PublicAuthRegisterOtpRouteImport } from './routes/_public/auth/register_/otp'
|
||||
import { Route as PublicAuthRegisterMentorSuccessRouteImport } from './routes/_public/auth/register-mentor_/success'
|
||||
@@ -159,6 +160,12 @@ const AuthenticatedDashboardLearningPathRoute =
|
||||
path: '/learning-path',
|
||||
getParentRoute: () => AuthenticatedDashboardRoute,
|
||||
} as any)
|
||||
const AuthenticatedDashboardArticleBuilderRoute =
|
||||
AuthenticatedDashboardArticleBuilderRouteImport.update({
|
||||
id: '/article-builder',
|
||||
path: '/article-builder',
|
||||
getParentRoute: () => AuthenticatedDashboardRoute,
|
||||
} as any)
|
||||
const PublicAuthRegisterSuccessRoute =
|
||||
PublicAuthRegisterSuccessRouteImport.update({
|
||||
id: '/auth/register_/success',
|
||||
@@ -200,6 +207,7 @@ export interface FileRoutesByFullPath {
|
||||
'/mentoring': typeof SiteMentoringRoute
|
||||
'/profile': typeof SiteProfileRoute
|
||||
'/resources': typeof SiteResourcesRoute
|
||||
'/dashboard/article-builder': typeof AuthenticatedDashboardArticleBuilderRoute
|
||||
'/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute
|
||||
'/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute
|
||||
'/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute
|
||||
@@ -227,6 +235,7 @@ export interface FileRoutesByTo {
|
||||
'/mentoring': typeof SiteMentoringRoute
|
||||
'/profile': typeof SiteProfileRoute
|
||||
'/resources': typeof SiteResourcesRoute
|
||||
'/dashboard/article-builder': typeof AuthenticatedDashboardArticleBuilderRoute
|
||||
'/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute
|
||||
'/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute
|
||||
'/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute
|
||||
@@ -259,6 +268,7 @@ export interface FileRoutesById {
|
||||
'/_site/mentoring': typeof SiteMentoringRoute
|
||||
'/_site/profile': typeof SiteProfileRoute
|
||||
'/_site/resources': typeof SiteResourcesRoute
|
||||
'/_authenticated/dashboard/article-builder': typeof AuthenticatedDashboardArticleBuilderRoute
|
||||
'/_authenticated/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute
|
||||
'/_authenticated/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute
|
||||
'/_authenticated/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute
|
||||
@@ -289,6 +299,7 @@ export interface FileRouteTypes {
|
||||
| '/mentoring'
|
||||
| '/profile'
|
||||
| '/resources'
|
||||
| '/dashboard/article-builder'
|
||||
| '/dashboard/learning-path'
|
||||
| '/dashboard/mentoring'
|
||||
| '/dashboard/roadmap-discovery'
|
||||
@@ -316,6 +327,7 @@ export interface FileRouteTypes {
|
||||
| '/mentoring'
|
||||
| '/profile'
|
||||
| '/resources'
|
||||
| '/dashboard/article-builder'
|
||||
| '/dashboard/learning-path'
|
||||
| '/dashboard/mentoring'
|
||||
| '/dashboard/roadmap-discovery'
|
||||
@@ -347,6 +359,7 @@ export interface FileRouteTypes {
|
||||
| '/_site/mentoring'
|
||||
| '/_site/profile'
|
||||
| '/_site/resources'
|
||||
| '/_authenticated/dashboard/article-builder'
|
||||
| '/_authenticated/dashboard/learning-path'
|
||||
| '/_authenticated/dashboard/mentoring'
|
||||
| '/_authenticated/dashboard/roadmap-discovery'
|
||||
@@ -539,6 +552,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof AuthenticatedDashboardLearningPathRouteImport
|
||||
parentRoute: typeof AuthenticatedDashboardRoute
|
||||
}
|
||||
'/_authenticated/dashboard/article-builder': {
|
||||
id: '/_authenticated/dashboard/article-builder'
|
||||
path: '/article-builder'
|
||||
fullPath: '/dashboard/article-builder'
|
||||
preLoaderRoute: typeof AuthenticatedDashboardArticleBuilderRouteImport
|
||||
parentRoute: typeof AuthenticatedDashboardRoute
|
||||
}
|
||||
'/_public/auth/register_/success': {
|
||||
id: '/_public/auth/register_/success'
|
||||
path: '/auth/register/success'
|
||||
@@ -585,6 +605,7 @@ declare module '@tanstack/react-router' {
|
||||
}
|
||||
|
||||
interface AuthenticatedDashboardRouteChildren {
|
||||
AuthenticatedDashboardArticleBuilderRoute: typeof AuthenticatedDashboardArticleBuilderRoute
|
||||
AuthenticatedDashboardLearningPathRoute: typeof AuthenticatedDashboardLearningPathRoute
|
||||
AuthenticatedDashboardMentoringRoute: typeof AuthenticatedDashboardMentoringRoute
|
||||
AuthenticatedDashboardRoadmapDiscoveryRoute: typeof AuthenticatedDashboardRoadmapDiscoveryRoute
|
||||
@@ -593,6 +614,8 @@ interface AuthenticatedDashboardRouteChildren {
|
||||
|
||||
const AuthenticatedDashboardRouteChildren: AuthenticatedDashboardRouteChildren =
|
||||
{
|
||||
AuthenticatedDashboardArticleBuilderRoute:
|
||||
AuthenticatedDashboardArticleBuilderRoute,
|
||||
AuthenticatedDashboardLearningPathRoute:
|
||||
AuthenticatedDashboardLearningPathRoute,
|
||||
AuthenticatedDashboardMentoringRoute: AuthenticatedDashboardMentoringRoute,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createFileRoute, Outlet, Link, useLocation, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { useAuthStore } from '@imphnen-frontend-service/service'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { resolvePersona } from './dashboard/_data/persona-resolver'
|
||||
@@ -40,21 +41,66 @@ export const Route = createFileRoute('/_authenticated/dashboard')({
|
||||
* Header component for the dashboard, containing the app brand and user profile.
|
||||
*/
|
||||
function HeaderDashboard({ persona, user, onLogout }: { persona: 'user' | 'mentor', user: any, onLogout: () => void }) {
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false)
|
||||
|
||||
const notifications = [
|
||||
{ id: 1, title: 'Mentoring Sesi Baru', message: 'Kamu punya sesi mentoring besok jam 20:00 WIB', time: '2 jam yang lalu', unread: true },
|
||||
{ id: 2, title: 'Artikel Disetujui', message: 'Artikel "How to install linux" kamu telah disetujui mentor', time: '5 jam yang lalu', unread: false },
|
||||
{ id: 3, title: 'Roadmap Selesai', message: 'Selamat! Kamu telah menyelesaikan roadmap Front-end Basic', time: '1 hari yang lalu', unread: false },
|
||||
]
|
||||
|
||||
return (
|
||||
<header className="h-[58px] w-[972px] mx-auto mt-[52px] mb-[62px] bg-white rounded-sm shadow-sm flex items-center justify-between px-5">
|
||||
<header className="h-[58px] w-[972px] mx-auto mt-[52px] mb-[62px] bg-white rounded-sm shadow-sm flex items-center justify-between px-5 relative">
|
||||
<Link to="/dashboard" className="text-[19px] font-semibold text-primary-accent">
|
||||
Dimentorin.dev
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<button className="w-7 h-7 rounded-sm bg-white text-neutral-600 flex items-center justify-center cursor-pointer">
|
||||
<Icon icon="lucide:bell" width="16" />
|
||||
</button>
|
||||
<button className="w-7 h-7 rounded-sm bg-white text-neutral-600 flex items-center justify-center cursor-pointer">
|
||||
<Icon icon="lucide:search" width="16" />
|
||||
</button>
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsNotificationsOpen(!isNotificationsOpen)}
|
||||
className={`w-7 h-7 rounded-sm flex items-center justify-center cursor-pointer transition-colors ${
|
||||
isNotificationsOpen ? 'bg-primary-50 text-primary-accent' : 'bg-white text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
<Icon icon="lucide:bell" width="16" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full border-2 border-white" />
|
||||
</button>
|
||||
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-[320px] bg-white rounded-xl shadow-2xl border border-gray-100 z-50 overflow-hidden animate-in fade-in slide-in-from-top-2">
|
||||
<div className="p-4 border-b border-gray-50 flex items-center justify-between">
|
||||
<h3 className="font-bold text-gray-900">Notifikasi</h3>
|
||||
<button className="text-xs text-primary-600 font-medium hover:underline cursor-pointer">Tandai semua dibaca</button>
|
||||
</div>
|
||||
<div className="max-h-[400px] overflow-y-auto">
|
||||
{notifications.map((n) => (
|
||||
<div key={n.id} className={`p-4 border-b border-gray-50 last:border-0 hover:bg-gray-50 transition-colors cursor-pointer ${n.unread ? 'bg-primary-50/30' : ''}`}>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<p className={`text-sm font-bold ${n.unread ? 'text-gray-900' : 'text-gray-700'}`}>{n.title}</p>
|
||||
<span className="text-[10px] text-gray-400 whitespace-nowrap">{n.time}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 line-clamp-2">{n.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="p-3 bg-gray-50 text-center">
|
||||
<button className="text-xs font-bold text-gray-600 hover:text-primary-600 transition-colors cursor-pointer">Lihat Semua Notifikasi</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
to="/dashboard/settings"
|
||||
className="w-7 h-7 rounded-sm bg-white text-neutral-600 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<Icon icon="lucide:settings" width="16" />
|
||||
</Link>
|
||||
|
||||
<button className="h-[42px] flex items-center gap-3 pl-2.5 cursor-pointer border-none bg-transparent">
|
||||
<Link
|
||||
to="/profile"
|
||||
className="h-[42px] flex items-center gap-3 pl-2.5 cursor-pointer border-none bg-transparent"
|
||||
>
|
||||
<div className="flex flex-col items-end text-right">
|
||||
<span className="text-xs font-medium text-neutral-600">{user?.fullname || 'User'}</span>
|
||||
<span className="text-[10px] font-medium text-neutral-600">{persona === 'mentor' ? 'Mentor' : 'Mentee'}</span>
|
||||
@@ -63,7 +109,7 @@ function HeaderDashboard({ persona, user, onLogout }: { persona: 'user' | 'mento
|
||||
className="w-7 h-7 rounded-full bg-bg-placeholder bg-cover bg-center"
|
||||
style={user?.avatar ? { backgroundImage: `url(${user.avatar})` } : {}}
|
||||
/>
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import { FC } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
interface ArticlePreviewModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
article: {
|
||||
judul: string
|
||||
materi: string
|
||||
status: string
|
||||
submitDate: string
|
||||
} | null
|
||||
}
|
||||
|
||||
export const ArticlePreviewModal: FC<ArticlePreviewModalProps> = ({ isOpen, onClose, article }) => {
|
||||
if (!isOpen || !article) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-3xl max-h-[90vh] overflow-hidden flex flex-col relative">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer z-10"
|
||||
>
|
||||
<Icon icon="mdi:close" width="24" />
|
||||
</button>
|
||||
|
||||
<div className="p-8 border-b border-gray-100 dark:border-gray-800">
|
||||
<div className="flex items-center gap-2 text-primary-600 font-semibold text-sm mb-2 uppercase tracking-wider">
|
||||
<Icon icon="mdi:book-open-page-variant" width="18" />
|
||||
Article Preview
|
||||
</div>
|
||||
<h2 className="text-3xl font-bold text-gray-900 dark:text-white">{article.judul}</h2>
|
||||
<div className="flex items-center gap-4 mt-4 text-sm text-gray-500">
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon icon="mdi:calendar-clock" width="16" />
|
||||
{article.submitDate || 'Not submitted yet'}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Icon icon="mdi:tag-outline" width="16" />
|
||||
{article.materi}
|
||||
</div>
|
||||
<div className={`px-2 py-0.5 rounded-full text-xs font-bold ${
|
||||
article.status === 'Done' ? 'bg-green-100 text-green-700' : 'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{article.status}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-8 space-y-6">
|
||||
<div className="prose dark:prose-invert max-w-none">
|
||||
<h3 className="text-xl font-bold mb-4">Introduction</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
|
||||
</p>
|
||||
|
||||
<div className="my-8 aspect-video bg-gray-100 dark:bg-gray-800 rounded-xl flex items-center justify-center overflow-hidden">
|
||||
<img src="/image/code-article.webp" alt="Article Cover" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
|
||||
<h3 className="text-xl font-bold mb-4">Key Concepts</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
||||
</p>
|
||||
|
||||
<ul className="list-disc pl-5 space-y-2 text-gray-600 dark:text-gray-400">
|
||||
<li>First major point of discussion</li>
|
||||
<li>Second key takeaway for readers</li>
|
||||
<li>Important consideration for implementation</li>
|
||||
</ul>
|
||||
|
||||
<h3 className="text-xl font-bold mb-4 mt-8">Conclusion</h3>
|
||||
<p className="text-gray-600 dark:text-gray-400 leading-relaxed">
|
||||
In conclusion, following these steps will help you achieve the best results. Keep practicing and exploring more advanced topics to further your knowledge.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-gray-50 dark:bg-gray-800/50 flex justify-end gap-3 border-t border-gray-100 dark:border-gray-800">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-6 py-2 rounded-xl border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 font-medium hover:bg-white dark:hover:bg-gray-800 transition-colors cursor-pointer"
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
{article.status !== 'Done' && (
|
||||
<button
|
||||
className="px-6 py-2 bg-primary-600 text-white rounded-xl font-medium hover:bg-primary-700 transition-colors cursor-pointer"
|
||||
>
|
||||
Edit Article
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { FC } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
interface MentorContactModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
mentor: {
|
||||
name: string
|
||||
topics: string[]
|
||||
} | null
|
||||
}
|
||||
|
||||
export const MentorContactModal: FC<MentorContactModalProps> = ({ isOpen, onClose, mentor }) => {
|
||||
if (!isOpen || !mentor) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-2xl shadow-2xl w-full max-w-md p-8 relative">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-4 right-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 cursor-pointer"
|
||||
>
|
||||
<Icon icon="mdi:close" width="24" />
|
||||
</button>
|
||||
|
||||
<div className="text-center mb-8">
|
||||
<div className="w-20 h-20 bg-bg-placeholder rounded-full mx-auto mb-4 overflow-hidden">
|
||||
<img src="/image/mascot-character.webp" alt="Mentor" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-1">{mentor.name}</h3>
|
||||
<div className="flex flex-wrap justify-center gap-1 mt-2">
|
||||
{mentor.topics.map((t, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-primary-50 dark:bg-primary-900/20 text-primary-600 dark:text-primary-400 text-[10px] font-bold rounded-full">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Hubungi mentor melalui platform berikut:</p>
|
||||
|
||||
<a
|
||||
href="https://wa.me/628123456789"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-4 bg-green-50 dark:bg-green-900/20 border border-green-100 dark:border-green-800 rounded-xl hover:bg-green-100 transition-colors cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon icon="mdi:whatsapp" width="24" className="text-green-600" />
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-green-900 dark:text-green-400">WhatsApp</p>
|
||||
<p className="text-xs text-green-700 dark:text-green-500">Respon cepat (24 Jam)</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon="mdi:chevron-right" width="20" className="text-green-400 group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="mailto:mentor@dimentorin.dev"
|
||||
className="flex items-center justify-between p-4 bg-blue-50 dark:bg-blue-900/20 border border-blue-100 dark:border-blue-800 rounded-xl hover:bg-blue-100 transition-colors cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon icon="mdi:email-outline" width="24" className="text-blue-600" />
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-blue-900 dark:text-blue-400">Email</p>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-500">Respon dalam 1-2 hari kerja</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon="mdi:chevron-right" width="20" className="text-blue-400 group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="https://discord.gg/dimentorin"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-between p-4 bg-indigo-50 dark:bg-indigo-900/20 border border-indigo-100 dark:border-indigo-800 rounded-xl hover:bg-indigo-100 transition-colors cursor-pointer group"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon icon="mdi:discord" width="24" className="text-indigo-600" />
|
||||
<div className="text-left">
|
||||
<p className="font-bold text-indigo-900 dark:text-indigo-400">Discord</p>
|
||||
<p className="text-xs text-indigo-700 dark:text-indigo-500">Komunitas & Sesi Live</p>
|
||||
</div>
|
||||
</div>
|
||||
<Icon icon="mdi:chevron-right" width="20" className="text-indigo-400 group-hover:translate-x-1 transition-transform" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="w-full mt-8 py-3 bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300 rounded-xl font-semibold hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors cursor-pointer"
|
||||
>
|
||||
Tutup
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
|
||||
interface MentoringDetailModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
onContactMentor: () => void
|
||||
mentor: {
|
||||
name: string
|
||||
title: string
|
||||
topics: string[]
|
||||
image: string
|
||||
}
|
||||
session: {
|
||||
date: string
|
||||
time: string
|
||||
location: string
|
||||
link?: string
|
||||
}
|
||||
}
|
||||
|
||||
export const MentoringDetailModal: FC<MentoringDetailModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onContactMentor,
|
||||
mentor,
|
||||
session,
|
||||
}) => {
|
||||
const [pertanyaan, setPertanyaan] = useState('')
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/50 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl overflow-hidden flex flex-col relative p-8">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-2xl font-bold text-primary-600">Detail Sesi Mentoring</h2>
|
||||
<Button
|
||||
variant="text"
|
||||
size="icon"
|
||||
onClick={onClose}
|
||||
className="text-gray-400 hover:text-gray-600 transition-colors"
|
||||
>
|
||||
<Icon icon="mdi:close" width="24" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Main Content Grid */}
|
||||
<div className="grid grid-cols-[280px_1fr] gap-12">
|
||||
{/* Left Column: Mentor Card */}
|
||||
<div className="flex flex-col items-center">
|
||||
<h3 className="text-lg font-bold text-primary-600 mb-6 w-full text-center">Your Senpai</h3>
|
||||
<div className="flex flex-col items-center text-center w-full bg-gray-50 rounded-xl p-6">
|
||||
<div className="w-40 h-48 mb-6 overflow-hidden rounded-2xl bg-white flex items-center justify-center shadow-sm">
|
||||
<img
|
||||
src={mentor.image || '/image/mascot-character.webp'}
|
||||
alt={mentor.name}
|
||||
className="w-full h-full object-cover"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-lg font-bold text-gray-900 leading-tight mb-1">
|
||||
{mentor.name}
|
||||
</p>
|
||||
<p className="text-sm text-gray-600">
|
||||
{mentor.title}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Session Details */}
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Topics */}
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-3">Topics</label>
|
||||
<div className="flex flex-wrap gap-2 bg-gray-50 p-4 rounded-lg">
|
||||
{mentor.topics.map((topic, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className="px-3 py-1.5 rounded-lg bg-white border border-gray-200 text-xs font-semibold text-gray-700 inline-flex items-center"
|
||||
>
|
||||
▪ {topic}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Date and Time Row */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Tanggal</label>
|
||||
<div className="w-full px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-sm text-gray-700 font-medium">
|
||||
{session.date}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Waktu</label>
|
||||
<div className="w-full px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-sm text-gray-700 font-medium">
|
||||
{session.time}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Location */}
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Lokasi</label>
|
||||
<div className="w-full px-4 py-2.5 rounded-lg border border-gray-300 bg-white text-sm text-gray-700 font-medium">
|
||||
{session.location}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pertanyaan Untuk Senpai */}
|
||||
<div>
|
||||
<label className="block text-sm font-bold text-gray-700 mb-2">Pertanyaan Untuk Senpai</label>
|
||||
<textarea
|
||||
value={pertanyaan}
|
||||
onChange={(e) => setPertanyaan(e.target.value)}
|
||||
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. ..."
|
||||
className="w-full px-4 py-3 rounded-lg border border-gray-300 bg-white text-sm text-gray-700 placeholder:text-gray-400 focus:outline-none focus:ring-2 focus:ring-primary-600 focus:border-transparent resize-none"
|
||||
rows={6}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Contact Button */}
|
||||
<Button
|
||||
variant="primary"
|
||||
className="w-full flex items-center justify-center gap-2"
|
||||
onClick={onContactMentor}
|
||||
>
|
||||
<Icon icon="mdi:phone-outline" width="18" />
|
||||
Hubungi Senpai
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+99
@@ -0,0 +1,99 @@
|
||||
import { FC, useState } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
interface MentoringFeedbackModalProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
mentorName: string
|
||||
}
|
||||
|
||||
export const MentoringFeedbackModal: FC<MentoringFeedbackModalProps> = ({ isOpen, onClose, mentorName }) => {
|
||||
const [step, setStep] = useState(1)
|
||||
const [rating, setRating] = useState(0)
|
||||
const [hoveredRating, setHoveredRating] = useState(0)
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const handleNext = () => setStep(2)
|
||||
const handleBack = () => setStep(1)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/30 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-[16px] shadow-2xl w-full max-w-[440px] overflow-hidden flex flex-col relative p-8">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-6 right-6 text-[#888888] hover:text-[#454545] cursor-pointer z-10"
|
||||
>
|
||||
<Icon icon="mdi:close" width="24" />
|
||||
</button>
|
||||
|
||||
<div className="flex flex-col items-center text-center">
|
||||
<h2 className="text-[23px] font-bold text-[#23A1EB] mb-2">Beri Feedback</h2>
|
||||
<p className="text-[15px] font-medium text-[#888888] mb-8">
|
||||
Bagaimana sesi mentoring kamu bersama {mentorName}?
|
||||
</p>
|
||||
|
||||
{step === 1 ? (
|
||||
<div className="w-full">
|
||||
<div className="flex justify-center gap-2 mb-10">
|
||||
{[1, 2, 3, 4, 5].map((s) => (
|
||||
<button
|
||||
key={s}
|
||||
onMouseEnter={() => setHoveredRating(s)}
|
||||
onMouseLeave={() => setHoveredRating(0)}
|
||||
onClick={() => setRating(s)}
|
||||
className="cursor-pointer transition-transform hover:scale-110"
|
||||
>
|
||||
<Icon
|
||||
icon={s <= (hoveredRating || rating) ? 'mdi:star' : 'mdi:star-outline'}
|
||||
width="48"
|
||||
className={s <= (hoveredRating || rating) ? 'text-[#FFB810]' : 'text-[#D1D1D1]'}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 h-[43px] rounded-lg border border-[#D1D1D1] text-[#6D6D6D] font-bold text-[15px] hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button
|
||||
onClick={handleNext}
|
||||
disabled={rating === 0}
|
||||
className="flex-1 h-[43px] rounded-lg bg-[#23A1EB] text-white font-bold text-[15px] hover:bg-[#1e88c7] transition-colors cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Lanjut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full">
|
||||
<textarea
|
||||
placeholder="Tulis pesan untuk mentor kamu..."
|
||||
className="w-full h-[160px] p-5 border border-[#D1D1D1] rounded-lg mb-8 resize-none outline-none focus:border-[#23A1EB] text-[15px] text-[#454545] placeholder:text-[#BBBBBB] font-medium"
|
||||
/>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<button
|
||||
onClick={handleBack}
|
||||
className="flex-1 h-[43px] rounded-lg border border-[#D1D1D1] text-[#6D6D6D] font-bold text-[15px] hover:bg-gray-50 transition-colors cursor-pointer"
|
||||
>
|
||||
Kembali
|
||||
</button>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="flex-1 h-[43px] rounded-lg bg-[#23A1EB] text-white font-bold text-[15px] hover:bg-[#1e88c7] transition-colors cursor-pointer"
|
||||
>
|
||||
Kirim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { toast } from 'sonner'
|
||||
import { Button } from '@imphnen-frontend-service/ui/atoms'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard/article-builder')({
|
||||
component: ArticleBuilderPage,
|
||||
})
|
||||
|
||||
function ArticleBuilderPage() {
|
||||
const navigate = useNavigate()
|
||||
const [title, setTitle] = useState('')
|
||||
const [blocks, setBlocks] = useState<{ id: string; type: 'heading' | 'paragraph' | 'image'; content: string }[]>([
|
||||
{ id: '1', type: 'heading', content: 'Judul Artikel Kamu' },
|
||||
{ id: '2', type: 'paragraph', content: 'Mulai menulis konten artikel kamu di sini...' },
|
||||
])
|
||||
|
||||
const addBlock = (type: 'heading' | 'paragraph' | 'image') => {
|
||||
const newBlock = {
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
type,
|
||||
content: type === 'image' ? '' : 'Klik untuk edit konten...',
|
||||
}
|
||||
setBlocks([...blocks, newBlock])
|
||||
}
|
||||
|
||||
const removeBlock = (id: string) => {
|
||||
setBlocks(blocks.filter((b) => b.id !== id))
|
||||
}
|
||||
|
||||
const handleSave = () => {
|
||||
toast.success('Artikel berhasil disimpan sebagai draft!')
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
toast.success('Artikel berhasil disubmit!')
|
||||
navigate({ to: '/dashboard/learning-path' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-gray-200 px-6 py-6">
|
||||
<div className="max-w-6xl mx-auto flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => navigate({ to: '/dashboard/learning-path' })}
|
||||
className="text-primary-600 hover:text-primary-700 transition-colors font-medium flex items-center gap-1 cursor-pointer"
|
||||
>
|
||||
<Icon icon="mdi:arrow-left" width="20" />
|
||||
Kembali
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 text-center">
|
||||
<h1 className="text-xl font-bold text-gray-900 mb-1">{title || 'Judul Artikel'}</h1>
|
||||
<p className="text-sm text-gray-500">Saved 27 April 2025 10:00 AM</p>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
variant="text"
|
||||
>
|
||||
Save Draft
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
variant="primary"
|
||||
>
|
||||
Submit →
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Content Area */}
|
||||
<div className="max-w-4xl mx-auto py-8 px-4">
|
||||
<div className="bg-white rounded-2xl shadow-sm border border-gray-100 p-12 min-h-[600px]">
|
||||
{/* Title Input */}
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={(e) => setTitle(e.target.value)}
|
||||
placeholder="Title"
|
||||
className="w-full text-5xl font-bold text-gray-400 border-none outline-none mb-8 placeholder:text-gray-300"
|
||||
/>
|
||||
|
||||
{/* Content Blocks */}
|
||||
<div className="space-y-8">
|
||||
{blocks.map((block) => (
|
||||
<div key={block.id} className="group relative">
|
||||
{block.type === 'heading' && (
|
||||
<h2 className="text-2xl font-semibold text-gray-800">{block.content}</h2>
|
||||
)}
|
||||
|
||||
{block.type === 'paragraph' && (
|
||||
<p className="text-lg text-gray-600 leading-relaxed">{block.content}</p>
|
||||
)}
|
||||
|
||||
{block.type === 'image' && (
|
||||
<div
|
||||
className="w-full aspect-video bg-gray-50 rounded-xl border-2 border-dashed border-gray-200 flex flex-col items-center justify-center hover:border-primary-500 transition-colors cursor-pointer group/img"
|
||||
onClick={() => addBlock('image')}
|
||||
>
|
||||
<Icon icon="mdi:image-plus" width="48" className="text-gray-300 group-hover/img:text-primary-500 mb-2" />
|
||||
<p className="text-sm text-gray-500 group-hover/img:text-primary-500 font-medium">Click to upload image</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={() => removeBlock(block.id)}
|
||||
className="absolute -right-8 top-0 p-2 text-gray-400 hover:text-red-500 cursor-pointer opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Icon icon="mdi:trash-can-outline" width="20" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Add Content Section */}
|
||||
<div className="mt-12 pt-8 border-t border-gray-200 flex items-center gap-3">
|
||||
<button
|
||||
onClick={() => addBlock('paragraph')}
|
||||
className="flex items-center justify-center w-8 h-8 rounded-full text-primary-600 hover:bg-primary-50 transition-colors cursor-pointer"
|
||||
>
|
||||
<Icon icon="mdi:plus" width="24" />
|
||||
</button>
|
||||
<span className="text-gray-400 text-lg">|</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +1,24 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard/learning-path')({
|
||||
component: LearningPathPage,
|
||||
})
|
||||
|
||||
function LearningPathPage() {
|
||||
const navigate = useNavigate()
|
||||
const [activeTab, setActiveTab] = useState<'roadmap' | 'article'>('roadmap')
|
||||
const [isSubmitArticlePopupOpen, setIsSubmitArticlePopupOpen] = useState(false)
|
||||
const [selectedArticles, setSelectedArticles] = useState<number[]>([])
|
||||
|
||||
const handleEditArticle = () => {
|
||||
navigate({ to: '/dashboard/article-builder' })
|
||||
}
|
||||
|
||||
const toggleSelectArticle = (no: number) => {
|
||||
setSelectedArticles((prev) => (prev.includes(no) ? prev.filter((id) => id !== no) : [...prev, no]))
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="w-[972px]">
|
||||
@@ -16,7 +27,7 @@ function LearningPathPage() {
|
||||
type="button"
|
||||
onClick={() => setActiveTab('roadmap')}
|
||||
className={`h-8 px-4 rounded-sm text-xs font-semibold transition-colors cursor-pointer ${
|
||||
activeTab === 'roadmap' ? 'bg-primary-accent text-white' : 'text-text-label hover:bg-primary-50'
|
||||
activeTab === 'roadmap' ? 'bg-primary-accent text-white shadow-sm' : 'text-text-label hover:bg-primary-50'
|
||||
}`}
|
||||
>
|
||||
Roadmap
|
||||
@@ -25,7 +36,7 @@ function LearningPathPage() {
|
||||
type="button"
|
||||
onClick={() => setActiveTab('article')}
|
||||
className={`h-8 px-4 rounded-sm text-xs font-semibold transition-colors cursor-pointer ${
|
||||
activeTab === 'article' ? 'bg-primary-accent text-white' : 'text-text-label hover:bg-primary-50'
|
||||
activeTab === 'article' ? 'bg-primary-accent text-white shadow-sm' : 'text-text-label hover:bg-primary-50'
|
||||
}`}
|
||||
>
|
||||
Article
|
||||
@@ -47,15 +58,21 @@ function LearningPathPage() {
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between rounded-sm bg-primary-50 px-3 py-2">
|
||||
<span className="text-xs text-text-label">1. Submateri 1</span>
|
||||
<span className="text-[10px] font-semibold text-success-600">Done</span>
|
||||
<span className="h-6 px-3 flex items-center justify-center rounded-sm bg-[#cde6dd] text-[10px] font-semibold text-[#2f7c66]">
|
||||
Done
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-sm bg-primary-50 px-3 py-2">
|
||||
<span className="text-xs text-text-label">2. Submateri 2</span>
|
||||
<span className="text-[10px] font-semibold text-primary-accent">To do</span>
|
||||
<span className="h-6 px-3 flex items-center justify-center rounded-sm bg-[#bce1fb] text-[10px] font-semibold text-[#23a1eb]">
|
||||
To do
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between rounded-sm bg-primary-50 px-3 py-2">
|
||||
<span className="text-xs text-text-label">3. Tugas : Membuat Artikel</span>
|
||||
<span className="text-[10px] font-semibold text-primary-accent">To do</span>
|
||||
<span className="h-6 px-3 flex items-center justify-center rounded-sm bg-[#bce1fb] text-[10px] font-semibold text-[#23a1eb]">
|
||||
To do
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
@@ -63,7 +80,7 @@ function LearningPathPage() {
|
||||
{['Day 2 - Materi B', 'Day 3 - Materi C', 'Day 4 - Materi D', 'Day 5 - Materi E'].map((day) => (
|
||||
<article key={day} className="border border-border-light rounded-sm p-4 flex items-center justify-between">
|
||||
<h3 className="text-[15px] font-semibold text-text-label">{day}</h3>
|
||||
<span className="text-xs text-text-muted">Selesaikan materi sebelumnya</span>
|
||||
<span className="text-xs font-medium text-[#ff7a00]">Selesaikan materi sebelumnya</span>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
@@ -72,11 +89,12 @@ function LearningPathPage() {
|
||||
|
||||
{activeTab === 'article' && (
|
||||
<div className="w-full bg-white rounded-sm shadow-sm p-6">
|
||||
<div className="mb-4">
|
||||
<div className="mb-4 relative">
|
||||
<Icon icon="lucide:search" className="absolute left-3 top-1/2 -translate-y-1/2 text-placeholder" width="16" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="w-[320px] h-[34px] border border-border-light rounded-sm px-3 text-[15px] text-text-label placeholder:text-placeholder focus:outline-none focus:border-primary-accent"
|
||||
className="w-full h-[43px] border border-border-light rounded-sm pl-10 pr-3 text-[15px] text-text-label placeholder:text-placeholder focus:outline-none focus:border-primary-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -84,43 +102,87 @@ function LearningPathPage() {
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-primary-50">
|
||||
<th className="px-4 py-3 text-left w-10">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-border-light text-primary-accent" />
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">No.</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Judul Artikel</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Materi</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Status</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3 text-center">Status</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Submit Date</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Action</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3 text-center">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-t border-neutral-100">
|
||||
<td className="text-xs text-text-muted px-4 py-3">1</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">How to install linux dist..</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">Day 1</td>
|
||||
<td className="px-4 py-3"><span className="text-[10px] font-semibold text-success-600">Done</span></td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">22 Maret 2025, 20:30 WIB</td>
|
||||
<td className="px-4 py-3">
|
||||
<button className="h-7 px-2 rounded-sm border border-border-light text-text-label text-[10px] font-semibold cursor-pointer">View</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="border-t border-neutral-100">
|
||||
<td className="text-xs text-text-muted px-4 py-3">2.</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">How to install linux dist..</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">Day 2</td>
|
||||
<td className="px-4 py-3"><span className="text-[10px] font-semibold text-primary-accent">On Progress</span></td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">-</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setIsSubmitArticlePopupOpen(true)}
|
||||
className="h-7 px-2 rounded-sm border border-primary-accent text-primary-accent text-[10px] font-semibold cursor-pointer"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{[
|
||||
{ no: 1, judul: 'How to install linux dist..', materi: 'Day 1', status: 'Done', date: '22 Maret 2025, 20:30 WIB' },
|
||||
{ no: 2, judul: 'How to install linux dist..', materi: 'Day 2', status: 'On Progress', date: '-' },
|
||||
].map((row) => (
|
||||
<tr key={row.no} className={`border-t border-neutral-100 ${row.no % 2 === 0 ? 'bg-primary-50' : 'bg-white'}`}>
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedArticles.includes(row.no)}
|
||||
onChange={() => toggleSelectArticle(row.no)}
|
||||
className="w-4 h-4 rounded border-border-light text-primary-accent"
|
||||
/>
|
||||
</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.no}.</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.judul}</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.materi}</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span
|
||||
className={`h-6 px-3 inline-flex items-center justify-center rounded-sm text-[10px] font-semibold ${
|
||||
row.status === 'Done' ? 'bg-[#cde6dd] text-[#2f7c66]' : 'bg-[#fef39b] text-[#d7a20f]'
|
||||
}`}
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.date}</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<button
|
||||
onClick={handleEditArticle}
|
||||
className="h-7 w-[84px] rounded-sm bg-[#fef39b] text-[#d7a20f] text-[10px] font-semibold cursor-pointer flex items-center justify-center gap-1 hover:opacity-90 transition-all"
|
||||
>
|
||||
<Icon icon="mdi:pencil" width="12" />
|
||||
Edit
|
||||
</button>
|
||||
<button className="h-7 w-[100px] rounded-sm bg-primary-accent text-white text-[10px] font-semibold cursor-pointer flex items-center justify-center gap-1 hover:opacity-90 transition-all">
|
||||
<Icon icon="lucide:search" width="12" />
|
||||
Cek Detail
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<button className="flex items-center gap-2 text-[10px] font-semibold text-text-muted hover:text-primary-accent transition-colors">
|
||||
<Icon icon="mdi:chevron-left" width="16" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
{[1, 2, 3, 4, '...', 7, 8, 9, 10].map((page, idx) => (
|
||||
<button
|
||||
key={idx}
|
||||
className={`h-7 min-w-7 px-2 rounded-sm text-[10px] font-semibold cursor-pointer transition-all ${
|
||||
page === 1 ? 'bg-primary-accent text-white' : 'bg-[#e1f0fd] text-primary-accent hover:bg-primary-100'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button className="flex items-center gap-2 text-[10px] font-semibold text-text-muted hover:text-primary-accent transition-colors">
|
||||
<Icon icon="mdi:chevron-right" width="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,24 +1,88 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { MentorContactModal } from './_components/modals/mentor-contact-modal'
|
||||
import { MentoringFeedbackModal } from './_components/modals/mentoring-feedback-modal'
|
||||
import { MentoringDetailModal } from './_components/modals/mentoring-detail-modal'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard/mentoring')({
|
||||
component: MentoringPage,
|
||||
})
|
||||
|
||||
function MentoringPage() {
|
||||
const [activeModal, setActiveModal] = useState<null | 'detail' | 'contact' | 'feedback-1' | 'feedback-2'>(null)
|
||||
const [activeModal, setActiveModal] = useState<null | 'detail' | 'contact' | 'cancel' | 'feedback'>(null)
|
||||
const [selectedRows, setSelectedRows] = useState<number[]>([])
|
||||
const [selectedMentor, setSelectedMentor] = useState<{
|
||||
name: string
|
||||
title: string
|
||||
topics: string[]
|
||||
image: string
|
||||
} | null>(null)
|
||||
const [selectedSession, setSelectedSession] = useState<{
|
||||
date: string
|
||||
time: string
|
||||
location: string
|
||||
link: string
|
||||
} | null>(null)
|
||||
|
||||
const mentoringRows = useMemo(
|
||||
() =>
|
||||
Array.from({ length: 12 }, (_, idx) => ({
|
||||
no: idx + 1,
|
||||
mentorName: 'Muhammad Firdaus Oi...',
|
||||
mentorTitle: 'UI Designer at Oray orayan Studios',
|
||||
topic: 'Basic IT, Industry Ins...',
|
||||
sessionTime: '22 Maret 2025, 20:00 - 20:30 WIB',
|
||||
sessionDate: '22 Maret 2025',
|
||||
startTime: '20:00',
|
||||
endTime: '20:30',
|
||||
location: 'Online',
|
||||
link: 'https://zoom.us/j/9876543210',
|
||||
status: idx % 3 === 0 ? 'Done' : 'To do',
|
||||
})),
|
||||
[],
|
||||
)
|
||||
|
||||
const handleContactMentor = (row: any) => {
|
||||
setSelectedMentor({
|
||||
name: row.mentorName,
|
||||
title: row.mentorTitle,
|
||||
topics: row.topic.split(', '),
|
||||
image: '/image/mascot-character.webp',
|
||||
})
|
||||
setActiveModal('contact')
|
||||
}
|
||||
|
||||
const handleShowDetail = (row: any) => {
|
||||
setSelectedMentor({
|
||||
name: row.mentorName,
|
||||
title: row.mentorTitle,
|
||||
topics: row.topic.split(', '),
|
||||
image: '/image/mascot-character.webp',
|
||||
})
|
||||
setSelectedSession({
|
||||
date: row.sessionDate,
|
||||
time: `${row.startTime} - ${row.endTime}`,
|
||||
location: row.location,
|
||||
link: row.link,
|
||||
})
|
||||
setActiveModal('detail')
|
||||
}
|
||||
|
||||
const handleShowFeedback = (row: any) => {
|
||||
setSelectedMentor({
|
||||
name: row.mentorName,
|
||||
title: row.mentorTitle,
|
||||
topics: row.topic.split(', '),
|
||||
image: '/image/mascot-character.webp',
|
||||
})
|
||||
setActiveModal('feedback')
|
||||
}
|
||||
|
||||
const toggleSelectRow = (no: number) => {
|
||||
setSelectedRows((prev) => (prev.includes(no) ? prev.filter((id) => id !== no) : [...prev, no]))
|
||||
}
|
||||
|
||||
const rowsPerPage = 10
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const totalPages = Math.max(1, Math.ceil(mentoringRows.length / rowsPerPage))
|
||||
@@ -33,14 +97,18 @@ function MentoringPage() {
|
||||
setCurrentPage(page)
|
||||
}
|
||||
|
||||
const actionButtonBaseClass =
|
||||
'h-7 rounded-sm px-1 text-[10px] font-medium leading-none text-center whitespace-nowrap cursor-pointer inline-flex items-center justify-center gap-1 transition-all hover:opacity-90'
|
||||
|
||||
return (
|
||||
<section className="w-[972px]">
|
||||
<div className="w-full bg-white rounded-sm shadow-sm p-6">
|
||||
<div className="mb-4">
|
||||
<div className="mb-4 relative">
|
||||
<Icon icon="lucide:search" className="absolute left-3 top-1/2 -translate-y-1/2 text-placeholder" width="16" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari berdasarkan nama item"
|
||||
className="w-[320px] h-[34px] border border-border-light rounded-sm px-3 text-[15px] text-text-label placeholder:text-placeholder focus:outline-none focus:border-primary-accent"
|
||||
className="w-full h-[43px] border border-border-light rounded-sm pl-10 pr-3 text-[15px] text-text-label placeholder:text-placeholder focus:outline-none focus:border-primary-accent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -48,47 +116,65 @@ function MentoringPage() {
|
||||
<table className="w-full border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-primary-50">
|
||||
<th className="px-4 py-3 text-left w-10">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-border-light text-primary-accent" />
|
||||
</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">No.</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Nama Mentor</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Topik</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Sesi Mentoring</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Status</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3">Action</th>
|
||||
<th className="text-left text-xs font-semibold text-text-label px-4 py-3 text-center">Status</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-semibold text-text-label min-w-[220px]">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{pagedRows.map((row) => (
|
||||
<tr key={row.no} className={`border-t border-neutral-100 ${row.no % 2 === 0 ? 'bg-primary-50' : 'bg-white'}`}>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.no}</td>
|
||||
<td className="px-4 py-3">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedRows.includes(row.no)}
|
||||
onChange={() => toggleSelectRow(row.no)}
|
||||
className="w-4 h-4 rounded border-border-light text-primary-accent"
|
||||
/>
|
||||
</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.no}.</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.mentorName}</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.topic}</td>
|
||||
<td className="text-xs text-text-muted px-4 py-3">{row.sessionTime}</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`text-[10px] font-semibold ${row.status === 'Done' ? 'text-success-600' : 'text-primary-accent'}`}>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span
|
||||
className={`h-6 px-3 inline-flex items-center justify-center whitespace-nowrap rounded-sm text-[10px] font-semibold ${
|
||||
row.status === 'Done' ? 'bg-[#cde6dd] text-[#2f7c66]' : 'bg-[#bce1fb] text-[#23a1eb]'
|
||||
}`}
|
||||
>
|
||||
{row.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="mx-auto flex w-[200px] items-center justify-center gap-2">
|
||||
{row.status === 'Done' ? (
|
||||
<button
|
||||
onClick={() => setActiveModal('feedback-1')}
|
||||
className="h-7 w-[175px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[10px] font-medium cursor-pointer"
|
||||
onClick={() => handleShowFeedback(row)}
|
||||
className={`${actionButtonBaseClass} w-full px-4 bg-primary-accent text-white`}
|
||||
>
|
||||
<Icon icon="lucide:search" width="12" />
|
||||
Kirim Feedback
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setActiveModal('detail')}
|
||||
className="h-7 w-[84px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[10px] font-medium cursor-pointer"
|
||||
onClick={() => handleShowDetail(row)}
|
||||
className={`${actionButtonBaseClass} flex-1 min-w-[90px] bg-primary-accent text-white`}
|
||||
>
|
||||
<Icon icon="lucide:search" width="12" />
|
||||
Cek Detail
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveModal('contact')}
|
||||
className="h-7 w-[84px] rounded-sm bg-[#ffe8da] text-[#ff5242] text-[10px] font-medium cursor-pointer"
|
||||
onClick={() => handleContactMentor(row)}
|
||||
className={`${actionButtonBaseClass} flex-1 min-w-[90px] bg-[#ffe8da] text-[#ff5242]`}
|
||||
>
|
||||
<Icon icon="lucide:x" width="12" />
|
||||
Cancel
|
||||
</button>
|
||||
</>
|
||||
@@ -101,239 +187,64 @@ function MentoringPage() {
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-center justify-between">
|
||||
<p className="text-xs text-text-muted">
|
||||
Menampilkan {(currentPage - 1) * rowsPerPage + 1} - {Math.min(currentPage * rowsPerPage, mentoringRows.length)} dari {mentoringRows.length}
|
||||
</p>
|
||||
<div className="mt-8 flex items-center justify-between">
|
||||
<button className="flex items-center gap-2 text-[10px] font-semibold text-text-muted hover:text-primary-accent transition-colors">
|
||||
<Icon icon="mdi:chevron-left" width="16" />
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToPage(currentPage - 1)}
|
||||
disabled={currentPage === 1}
|
||||
className="h-7 px-2 rounded-sm border border-border-light text-[10px] font-semibold text-text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
{Array.from({ length: totalPages }, (_, idx) => {
|
||||
const page = idx + 1
|
||||
const isActive = page === currentPage
|
||||
|
||||
// Logic to show page numbers with ellipsis (simplified for now)
|
||||
if (totalPages > 7) {
|
||||
if (page > 4 && page < totalPages - 2 && page !== currentPage) {
|
||||
if (page === 5) return <span key="ellipsis" className="text-text-muted">...</span>
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<button
|
||||
key={page}
|
||||
type="button"
|
||||
onClick={() => goToPage(page)}
|
||||
className={`h-7 min-w-7 px-2 rounded-sm border text-[10px] font-semibold cursor-pointer ${
|
||||
isActive
|
||||
? 'border-primary-accent bg-primary-accent text-white'
|
||||
: 'border-border-light text-text-label'
|
||||
className={`h-7 min-w-7 px-2 rounded-sm text-[10px] font-semibold cursor-pointer transition-all ${
|
||||
isActive ? 'bg-primary-accent text-white' : 'bg-[#e1f0fd] text-primary-accent hover:bg-primary-100'
|
||||
}`}
|
||||
>
|
||||
{page}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => goToPage(currentPage + 1)}
|
||||
disabled={currentPage === totalPages}
|
||||
className="h-7 px-2 rounded-sm border border-border-light text-[10px] font-semibold text-text-label cursor-pointer disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button className="flex items-center gap-2 text-[10px] font-semibold text-text-muted hover:text-primary-accent transition-colors">
|
||||
<Icon icon="mdi:chevron-right" width="16" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{activeModal !== null && (
|
||||
<div className="fixed inset-0 z-50 bg-black/30 flex items-center justify-center p-4">
|
||||
{activeModal === 'detail' && (
|
||||
<div className="w-[800px] h-[732px] rounded-[8px] bg-white p-12 overflow-auto">
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<h2 className="text-[23px] font-semibold text-[#23a1eb]">Detail Sesi Mentoring</h2>
|
||||
<button className="text-[#888888] text-xl cursor-pointer" onClick={() => setActiveModal(null)}>
|
||||
x
|
||||
</button>
|
||||
</div>
|
||||
<MentorContactModal
|
||||
isOpen={activeModal === 'contact'}
|
||||
onClose={() => setActiveModal(null)}
|
||||
mentor={selectedMentor}
|
||||
/>
|
||||
|
||||
<div className="grid grid-cols-[306px_1fr] gap-8">
|
||||
<div>
|
||||
<h3 className="text-[23px] font-semibold text-[#23a1eb] mb-8">Your Senpai</h3>
|
||||
<div className="text-center">
|
||||
<img src="/image/mascot-character.webp" alt="Mentor" className="w-[180px] h-[217px] object-cover mx-auto mb-8" />
|
||||
<p className="text-[19px] font-semibold text-[#454545] leading-tight">
|
||||
Muhammad
|
||||
<br />
|
||||
Firdaus Oi Oi Oi, S.H., M.H.
|
||||
</p>
|
||||
<p className="text-[15px] text-[#6d6d6d] mt-2">UI Designer at Oray orayan Studios</p>
|
||||
</div>
|
||||
</div>
|
||||
<MentoringDetailModal
|
||||
isOpen={activeModal === 'detail' && !!selectedMentor && !!selectedSession}
|
||||
onClose={() => setActiveModal(null)}
|
||||
onContactMentor={() => setActiveModal('contact')}
|
||||
mentor={selectedMentor!}
|
||||
session={selectedSession!}
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-3">Topics</p>
|
||||
<div className="flex items-center gap-2 mb-6">
|
||||
<span className="h-7 px-4 rounded-full bg-primary-50 text-[10px] font-medium text-[#6d6d6d] inline-flex items-center">
|
||||
Industry Insight
|
||||
</span>
|
||||
<span className="h-7 px-4 rounded-full bg-primary-50 text-[10px] font-medium text-[#6d6d6d] inline-flex items-center">
|
||||
Basic IT
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4 mb-4">
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Tanggal</p>
|
||||
<input readOnly value="22 Maret 2025" className="w-full h-[34px] rounded-sm border border-[#d1d1d1] px-5 text-[15px] text-[#6d6d6d]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Waktu</p>
|
||||
<input readOnly value="20:00 - 20:30" className="w-full h-[34px] rounded-sm border border-[#d1d1d1] px-5 text-[15px] text-[#6d6d6d]" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Lokasi</p>
|
||||
<input readOnly value="Online" className="w-full h-[34px] rounded-sm border border-[#d1d1d1] px-5 text-[15px] text-[#6d6d6d]" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Pertanyaan Untuk Senpai</p>
|
||||
<textarea
|
||||
readOnly
|
||||
value={'Hi [Nama Mentor], Saya [Nama Kamu] & saya berharap dapat memiliki sesi mentoring dengan Anda.\n\nSaat ini, saya tertarik untuk mengejar __. Tujuan saya untuk sesi ini adalah __.\n\nSaya ingin tahu secara khusus tentang ___.\n1. Pertanyaan Anda\n2. ...\n3. ...'}
|
||||
className="w-full h-[156px] rounded-sm border border-[#d1d1d1] px-3 py-2 text-xs text-[#6d6d6d] resize-none"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => setActiveModal('contact')}
|
||||
className="w-full h-[34px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[15px] font-semibold cursor-pointer"
|
||||
>
|
||||
Hubungi Senpai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeModal === 'contact' && (
|
||||
<div className="w-[400px] h-[242px] rounded-[8px] bg-white px-10 py-10">
|
||||
<div className="w-[320px] mx-auto text-center">
|
||||
<h3 className="text-[23px] font-semibold text-[#23a1eb]">Hubungi Senpai Sekarang ??</h3>
|
||||
<p className="text-[15px] text-[#888888] mt-8">
|
||||
Tekan tombol di bawah ini untuk terhubung langsung ke WhatsApp senpai kamu^^
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 flex items-center gap-4">
|
||||
<button className="w-[152px] h-[34px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[15px] font-semibold cursor-pointer">
|
||||
Hubungi Senpai
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveModal(null)}
|
||||
className="w-[152px] h-[34px] rounded-sm bg-[#ffe8da] text-[#ff5242] text-[15px] font-semibold cursor-pointer"
|
||||
>
|
||||
Nanti Deh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeModal === 'feedback-1' && (
|
||||
<div className="w-[520px] h-[582px] rounded-[8px] bg-white p-8 overflow-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-[19px] font-semibold text-[#454545]">Feedback Mentor</h3>
|
||||
<span className="text-[15px] font-medium text-[#6d6d6d]">1 dari 2</span>
|
||||
</div>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-3">Seberapa puas kamu dengan sesi mentoring ini?</p>
|
||||
<div className="grid grid-cols-5 gap-2 mb-3">
|
||||
{[1, 2, 3, 4, 5].map((n) => (
|
||||
<button key={n} className="h-10 rounded-sm bg-primary-50 text-[15px] font-medium text-[#81cbf8] cursor-pointer">{n}</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-[#888888] mb-6">1 = Sangat Tidak Puas, 5 = Sangat Puas</p>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-3">Seberapa membantu jawaban mentor untuk kebutuhanmu?</p>
|
||||
<div className="space-y-3 mb-6">
|
||||
{['Tidak Membantu', 'Kurang Membantu', 'Cukup Membantu', 'Membantu', 'Sangat Membantu'].map((item) => (
|
||||
<label key={item} className="flex items-center gap-2 text-xs text-[#6d6d6d]">
|
||||
<input type="radio" name="helpful" />
|
||||
{item}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-3">Apakah kamu akan merekomendasikan platform ini ke rekan mu?</p>
|
||||
<div className="space-y-3 mb-8">
|
||||
{['Ya', 'Tidak', 'Mungkin'].map((item) => (
|
||||
<label key={item} className="flex items-center gap-2 text-xs text-[#6d6d6d]">
|
||||
<input type="radio" name="recommend" />
|
||||
{item}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setActiveModal(null)}
|
||||
className="h-[34px] w-[92px] rounded-sm bg-white text-[#23a1eb] text-[15px] font-semibold border border-transparent cursor-pointer"
|
||||
>
|
||||
Batal
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveModal('feedback-2')}
|
||||
className="h-[34px] w-[92px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[15px] font-semibold cursor-pointer"
|
||||
>
|
||||
Lanjut
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeModal === 'feedback-2' && (
|
||||
<div className="w-[520px] h-[582px] rounded-[8px] bg-white p-8 overflow-auto">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h3 className="text-[19px] font-semibold text-[#454545]">Feedback Mentor</h3>
|
||||
<span className="text-[15px] font-medium text-[#6d6d6d]">2 dari 2</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5 mb-8">
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Apa yang kamu suka dari sesi ini?</p>
|
||||
<textarea className="w-full h-[76px] rounded-sm border border-[#d1d1d1] p-3 text-[15px] text-[#b0b0b0] resize-none" defaultValue="Hal apa yang menurutmu paling membantu atau berkesan dari sesi tadi?" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Apa yang bisa ditingkatkan oleh mentor?</p>
|
||||
<textarea className="w-full h-[76px] rounded-sm border border-[#d1d1d1] p-3 text-[15px] text-[#b0b0b0] resize-none" defaultValue="Ada saran atau masukan yang bisa membantu mentor lebih baik di sesi berikutnya?" />
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[15px] font-medium text-[#454545] mb-2">Testimoni atau ucapan terima kasih untuk mentor</p>
|
||||
<textarea className="w-full h-[76px] rounded-sm border border-[#d1d1d1] p-3 text-[15px] text-[#b0b0b0] resize-none" defaultValue="Kamu juga bisa tulis pesan singkat atau ucapan ke mentor di sini (opsional)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setActiveModal('feedback-1')}
|
||||
className="h-[34px] w-[100px] rounded-sm bg-white text-[#23a1eb] text-[15px] font-semibold cursor-pointer"
|
||||
>
|
||||
Kembali
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveModal(null)}
|
||||
className="h-[34px] w-[100px] rounded-sm bg-[#23a1eb] text-[#f6f6f6] text-[15px] font-semibold cursor-pointer"
|
||||
>
|
||||
Kirim
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<MentoringFeedbackModal
|
||||
isOpen={activeModal === 'feedback' && !!selectedMentor}
|
||||
onClose={() => setActiveModal(null)}
|
||||
mentorName={selectedMentor?.name || ''}
|
||||
/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, useNavigate } from '@tanstack/react-router'
|
||||
import { Icon } from '@iconify/react'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard/roadmap-discovery')({
|
||||
@@ -6,51 +6,77 @@ export const Route = createFileRoute('/_authenticated/dashboard/roadmap-discover
|
||||
})
|
||||
|
||||
function RoadmapDiscoveryPage() {
|
||||
const navigate = useNavigate()
|
||||
|
||||
const handleGenerate = () => {
|
||||
navigate({ to: '/dashboard/learning-path' })
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="w-[972px] h-[438px]">
|
||||
<div className="w-full h-full">
|
||||
<div className="w-[421px] mx-auto text-center">
|
||||
<h2 className="text-[23px] font-normal leading-[1.2] text-[#6d6d6d]">Welcome to Roadmap Discovery</h2>
|
||||
<h1 className="text-[46px] font-bold leading-[1.2] text-[#5d5d5d] mt-[12px]">Start your Journey</h1>
|
||||
<section className="flex flex-col items-center justify-center min-h-screen py-10 bg-gradient-to-t from-bg-light-blue via-bg-light-blue to-white">
|
||||
<div className="text-center mb-12">
|
||||
<h2 className="text-[23px] font-normal text-text-secondary mb-2">
|
||||
Welcome to <span className="text-primary-accent">Roadmap Discovery</span>
|
||||
</h2>
|
||||
<div className="flex items-center justify-center gap-3">
|
||||
<h1 className="text-[46px] font-bold text-neutral-600">
|
||||
Start your <span className="text-primary-accent">Journey</span>
|
||||
</h1>
|
||||
<Icon icon="mdi:sparkles" className="text-primary-accent text-4xl" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[528px] h-[146px] mx-auto mt-10 flex items-start">
|
||||
<img
|
||||
src="/image/mascot-1.png"
|
||||
alt="Mascot"
|
||||
className="w-[146px] h-[146px] object-cover"
|
||||
<div className="flex items-center gap-8 mb-12">
|
||||
<img
|
||||
src="/image/mascot-1.png"
|
||||
alt="Mascot"
|
||||
className="w-[146px] h-[146px] object-contain"
|
||||
/>
|
||||
|
||||
<div className="relative bg-white rounded-xl shadow-sm p-6 border border-neutral-100 max-w-[380px]">
|
||||
{/* Speech bubble arrow */}
|
||||
<div className="absolute left-[-8px] top-1/2 -translate-y-1/2 w-0 h-0 border-t-[8px] border-t-transparent border-r-[8px] border-r-white border-b-[8px] border-b-transparent" />
|
||||
<p className="text-[19px] leading-[1.3] text-text-secondary">
|
||||
Lagi pengen belajar apa? Ketik aja di sini,
|
||||
<br />
|
||||
biar AI bantuin bikin roadmap-nya.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full max-w-[732px]">
|
||||
<div className="flex gap-4 mb-12">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Mau belajar roadmap apa?"
|
||||
className="flex-1 h-[43px] border border-neutral-200 rounded-sm px-4 text-[15px] text-text-label placeholder:text-placeholder focus:outline-none focus:border-primary-accent transition-all bg-white"
|
||||
/>
|
||||
|
||||
<div className="w-[381px] h-[78px] bg-white mt-0">
|
||||
<p className="text-[19px] leading-[1.2] text-[#6d6d6d] px-5 py-4">
|
||||
Lagi pengen belajar apa? Ketik aja di sini,
|
||||
<br />
|
||||
biar AI bantuin bikin roadmap-nya.
|
||||
</p>
|
||||
<div className="relative w-[248px]">
|
||||
<select
|
||||
className="w-full h-[43px] border border-neutral-200 rounded-sm px-4 text-[15px] text-text-label bg-white focus:outline-none focus:border-primary-accent appearance-none cursor-pointer"
|
||||
defaultValue=""
|
||||
>
|
||||
<option value="" disabled>
|
||||
Tingkat Belajar
|
||||
</option>
|
||||
<option value="pemula">Pemula</option>
|
||||
<option value="menengah">Menengah</option>
|
||||
<option value="lanjutan">Lanjutan</option>
|
||||
</select>
|
||||
<Icon
|
||||
icon="mdi:chevron-down"
|
||||
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 pointer-events-none"
|
||||
width="20"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-[732px] h-[43px] mx-auto mt-10">
|
||||
<div className="w-full h-[34px] flex items-center gap-4">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Mau belajar roadmap apa?"
|
||||
className="w-[468px] h-[34px] border border-[#d1d1d1] rounded-sm px-3 text-[15px] text-[#6d6d6d] placeholder:text-[#b0b0b0] focus:outline-none focus:border-[#23a1eb]"
|
||||
/>
|
||||
|
||||
<select
|
||||
className="w-[248px] h-[34px] border border-[#d1d1d1] rounded-sm px-3 text-[15px] text-[#6d6d6d] bg-white focus:outline-none focus:border-[#23a1eb]"
|
||||
defaultValue="Tingkat Belajar"
|
||||
>
|
||||
<option disabled>Tingkat Belajar</option>
|
||||
<option>Pemula</option>
|
||||
<option>Menengah</option>
|
||||
<option>Lanjutan</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button className="w-[140px] h-[34px] mt-[49px] mx-auto flex items-center justify-center gap-2 bg-[#23a1eb] text-[#f6f6f6] rounded-sm text-[15px] font-semibold hover:bg-[#1d8fd3] transition-colors cursor-pointer">
|
||||
<Icon icon="mdi:star-four-points" width="12" />
|
||||
<div className="flex justify-center">
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
className="h-[43px] px-10 bg-primary-accent text-white rounded-sm text-[15px] font-semibold hover:opacity-90 transition-all cursor-pointer flex items-center gap-2 shadow-sm"
|
||||
>
|
||||
<Icon icon="mdi:sparkles" width="16" />
|
||||
Generate
|
||||
</button>
|
||||
</div>
|
||||
|
||||
+22
-2
@@ -1,5 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { useNavigate } from '@tanstack/react-router';
|
||||
import { getUserMockDashboardData } from '../../../dashboard/_data/mock/dashboard-mock';
|
||||
|
||||
/**
|
||||
@@ -8,6 +9,15 @@ import { getUserMockDashboardData } from '../../../dashboard/_data/mock/dashboar
|
||||
*/
|
||||
export function UserDashboard() {
|
||||
const data = getUserMockDashboardData();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleStartDiscovery = () => {
|
||||
navigate({ to: '/dashboard/roadmap-discovery' });
|
||||
};
|
||||
|
||||
const handleContinueLearning = () => {
|
||||
navigate({ to: '/dashboard/learning-path' });
|
||||
};
|
||||
|
||||
const gradientStyle = {
|
||||
background: 'linear-gradient(135deg, #ffffff 30.3%, rgba(255, 255, 255, 0) 100%), #f0f8ff',
|
||||
@@ -28,7 +38,12 @@ export function UserDashboard() {
|
||||
roadmap 30 hari yang direkomendasikan AI khusus buat kamu~
|
||||
</p>
|
||||
</div>
|
||||
<button className="w-[178px] h-[30px] mt-3 bg-primary-accent text-white rounded text-xs font-semibold leading-[14.4px] hover:bg-[#1e8cd1] transition-colors">Temukan Roadmapmu^^</button>
|
||||
<button
|
||||
onClick={handleStartDiscovery}
|
||||
className="w-[178px] h-[30px] mt-3 bg-primary-accent text-white rounded text-xs font-semibold leading-[14.4px] hover:bg-[#1e8cd1] transition-colors cursor-pointer"
|
||||
>
|
||||
Temukan Roadmapmu^^
|
||||
</button>
|
||||
</div>
|
||||
<img
|
||||
src="/image/mascot-character.webp"
|
||||
@@ -70,7 +85,12 @@ export function UserDashboard() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="w-[120px] h-[30px] bg-white border border-primary-accent rounded-sm text-primary-accent font-semibold text-xs leading-[14.4px] cursor-pointer">Lanjut Belajar</button>
|
||||
<button
|
||||
onClick={handleContinueLearning}
|
||||
className="w-[120px] h-[30px] bg-white border border-primary-accent rounded-sm text-primary-accent font-semibold text-xs leading-[14.4px] cursor-pointer"
|
||||
>
|
||||
Lanjut Belajar
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,190 +1,759 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { createFileRoute, Link, useLocation, useNavigate } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
useAuthStore,
|
||||
useSessionQuery,
|
||||
} from '@imphnen-frontend-service/service'
|
||||
import { cn } from '@imphnen-frontend-service/utils'
|
||||
import { Icon } from '@iconify/react'
|
||||
import { toast } from 'sonner'
|
||||
import { resolvePersona } from '../dashboard/_data/persona-resolver'
|
||||
|
||||
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.' },
|
||||
]
|
||||
type SettingsSection = 'account' | 'privacy' | 'preferences' | 'faq' | 'report' | 'feedback'
|
||||
|
||||
export const Route = createFileRoute('/_authenticated/dashboard_/settings')({
|
||||
component: SettingsPage,
|
||||
})
|
||||
|
||||
function SettingsPage() {
|
||||
const { session } = useAuthStore()
|
||||
const { data: meData } = useSessionQuery()
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('account')
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null)
|
||||
/**
|
||||
* Header component for the dashboard, containing the app brand and user profile.
|
||||
* Replicated from dashboard.tsx for consistency.
|
||||
*/
|
||||
function HeaderDashboard({ persona, user, onLogout }: { persona: 'user' | 'mentor', user: any, onLogout: () => void }) {
|
||||
const [isNotificationsOpen, setIsNotificationsOpen] = useState(false)
|
||||
|
||||
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' },
|
||||
const notifications = [
|
||||
{ id: 1, title: 'Mentoring Sesi Baru', message: 'Kamu punya sesi mentoring besok jam 20:00 WIB', time: '2 jam yang lalu', unread: true },
|
||||
{ id: 2, title: 'Artikel Disetujui', message: 'Artikel "How to install linux" kamu telah disetujui mentor', time: '5 jam yang lalu', unread: false },
|
||||
{ id: 3, title: 'Roadmap Selesai', message: 'Selamat! Kamu telah menyelesaikan roadmap Front-end Basic', time: '1 hari yang lalu', unread: false },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white mb-8">Settings</h1>
|
||||
<header className="h-[58px] w-[972px] mx-auto mt-[52px] mb-[62px] bg-white rounded-sm shadow-sm flex items-center justify-between px-5 relative">
|
||||
<Link to="/dashboard" className="text-[19px] font-semibold text-primary-accent">
|
||||
Dimentorin.dev
|
||||
</Link>
|
||||
|
||||
<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>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={() => setIsNotificationsOpen(!isNotificationsOpen)}
|
||||
className={`w-7 h-7 rounded-sm flex items-center justify-center cursor-pointer transition-colors ${
|
||||
isNotificationsOpen ? 'bg-primary-50 text-primary-accent' : 'bg-white text-neutral-600'
|
||||
}`}
|
||||
>
|
||||
<Icon icon="lucide:bell" width="16" />
|
||||
<span className="absolute top-1 right-1 w-2 h-2 bg-red-500 rounded-full border-2 border-white" />
|
||||
</button>
|
||||
|
||||
{/* 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>
|
||||
{isNotificationsOpen && (
|
||||
<div className="absolute right-0 top-full mt-2 w-[320px] bg-white rounded-xl shadow-2xl border border-gray-100 z-50 overflow-hidden animate-in fade-in slide-in-from-top-2">
|
||||
<div className="p-4 border-b border-gray-50 flex items-center justify-between">
|
||||
<h3 className="font-bold text-gray-900">Notifikasi</h3>
|
||||
<button className="text-xs text-primary-600 font-medium hover:underline cursor-pointer">Tandai semua dibaca</button>
|
||||
</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 className="max-h-[400px] overflow-y-auto">
|
||||
{notifications.map((n) => (
|
||||
<div key={n.id} className={`p-4 border-b border-gray-50 last:border-0 hover:bg-gray-50 transition-colors cursor-pointer ${n.unread ? 'bg-primary-50/30' : ''}`}>
|
||||
<div className="flex justify-between items-start mb-1">
|
||||
<p className={`text-sm font-bold ${n.unread ? 'text-gray-900' : 'text-gray-700'}`}>{n.title}</p>
|
||||
<span className="text-[10px] text-gray-400 whitespace-nowrap">{n.time}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 line-clamp-2">{n.message}</p>
|
||||
</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 className="p-3 bg-gray-50 text-center">
|
||||
<button className="text-xs font-bold text-gray-600 hover:text-primary-600 transition-colors cursor-pointer">Lihat Semua Notifikasi</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<Link
|
||||
to="/dashboard/settings"
|
||||
className="w-7 h-7 rounded-sm bg-white text-neutral-600 flex items-center justify-center cursor-pointer"
|
||||
>
|
||||
<Icon icon="lucide:settings" width="16" />
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
to="/profile"
|
||||
className="h-[42px] flex items-center gap-3 pl-2.5 cursor-pointer border-none bg-transparent"
|
||||
>
|
||||
<div className="flex flex-col items-end text-right">
|
||||
<span className="text-xs font-medium text-neutral-600">{user?.fullname || 'User'}</span>
|
||||
<span className="text-[10px] font-medium text-neutral-600">{persona === 'mentor' ? 'Mentor' : 'Mentee'}</span>
|
||||
</div>
|
||||
<div
|
||||
className="w-7 h-7 rounded-full bg-bg-placeholder bg-cover bg-center"
|
||||
style={user?.avatar ? { backgroundImage: `url(${user.avatar})` } : {}}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingsPage() {
|
||||
const { session, clearSession } = useAuthStore()
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { data: meData } = useSessionQuery()
|
||||
const [activeSection, setActiveSection] = useState<SettingsSection>('account')
|
||||
const [expandedFaq, setExpandedFaq] = useState<number | null>(null)
|
||||
const [twoStepAuthStep, setTwoStepAuthStep] = useState<'off' | 'input-email' | 'input-otp' | 'done'>('off')
|
||||
|
||||
const user = session?.user
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const persona = resolvePersona(user, searchParams);
|
||||
|
||||
const sections = [
|
||||
{ id: 'account' as const, label: 'Detail Akun', icon: 'mdi:account-outline', category: 'Account' },
|
||||
{ id: 'privacy' as const, label: 'Privasi & Keamanan', icon: 'mdi:shield-lock-outline', category: 'Account' },
|
||||
{ id: 'preferences' as const, label: 'Preferences', icon: 'mdi:tune-variant', category: 'Account' },
|
||||
{ id: 'faq' as const, label: 'FAQ', icon: 'mdi:help-circle-outline', category: 'Help & Feedback' },
|
||||
{ id: 'report' as const, label: 'Laporkan Kendala', icon: 'mdi:alert-circle-outline', category: 'Help & Feedback' },
|
||||
{ id: 'feedback' as const, label: 'Umpan Balik', icon: 'mdi:message-draw', category: 'Help & Feedback' },
|
||||
]
|
||||
|
||||
const handleLogout = () => {
|
||||
clearSession();
|
||||
navigate({ to: '/auth/login' });
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-bg-light-blue flex">
|
||||
{/* Sidebar Navigation */}
|
||||
<aside className="w-[228px] bg-white flex flex-col sticky top-0 h-screen z-100">
|
||||
{/* Logo */}
|
||||
<div className="pt-[60px] px-6 pb-8">
|
||||
<div className="h-12 flex items-center justify-center">
|
||||
<img
|
||||
src="/logos/logo.svg"
|
||||
alt="Dimentorin"
|
||||
style={{ width: '128px', height: '48px', objectFit: 'contain' }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation Items */}
|
||||
<nav className="flex-1 px-6 space-y-8 overflow-y-auto">
|
||||
<div>
|
||||
<p className="px-3 text-[10px] font-medium text-[#888888] uppercase tracking-wider mb-2">Account</p>
|
||||
<div className="space-y-1">
|
||||
{sections.filter(s => s.category === 'Account').map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
className={`w-full h-8 px-3 rounded-sm flex items-center gap-3 cursor-pointer text-xs font-medium transition-all duration-200 ${
|
||||
activeSection === s.id
|
||||
? 'bg-primary-accent text-white'
|
||||
: 'text-text-muted hover:bg-bg-hover hover:text-primary-accent'
|
||||
}`}
|
||||
>
|
||||
<Icon icon={s.icon} width="16" className={activeSection === s.id ? 'text-white' : 'text-text-muted'} />
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<p className="px-3 text-[10px] font-medium text-[#888888] uppercase tracking-wider mb-2">Help & Feedback</p>
|
||||
<div className="space-y-1">
|
||||
{sections.filter(s => s.category === 'Help & Feedback').map((s) => (
|
||||
<button
|
||||
key={s.id}
|
||||
onClick={() => setActiveSection(s.id)}
|
||||
className={`w-full h-8 px-3 rounded-sm flex items-center gap-3 cursor-pointer text-xs font-medium transition-all duration-200 ${
|
||||
activeSection === s.id
|
||||
? 'bg-primary-accent text-white'
|
||||
: 'text-text-muted hover:bg-bg-hover hover:text-primary-accent'
|
||||
}`}
|
||||
>
|
||||
<Icon icon={s.icon} width="16" className={activeSection === s.id ? 'text-white' : 'text-text-muted'} />
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
{/* Sidebar Footer */}
|
||||
<div className="flex flex-col pt-4 px-6 pb-[34px]">
|
||||
<div className="h-px bg-border-light mb-4" />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="h-8 px-3 rounded-sm flex items-center gap-3 cursor-pointer text-xs font-medium leading-[1.3] text-text-muted transition-all duration-200 hover:bg-bg-hover"
|
||||
>
|
||||
<Icon icon="mdi:logout" width="16" />
|
||||
Log Out
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0 overflow-y-auto">
|
||||
<HeaderDashboard
|
||||
persona={persona}
|
||||
user={session?.user}
|
||||
onLogout={handleLogout}
|
||||
/>
|
||||
|
||||
<main className="w-[1052px] mx-auto px-10 pt-6 pb-10">
|
||||
<h1 className="text-[23px] font-semibold text-[#454545] mb-8">
|
||||
{sections.find(s => s.id === activeSection)?.label}
|
||||
</h1>
|
||||
|
||||
<div className="bg-white rounded-sm shadow-sm p-10">
|
||||
{activeSection === 'account' && (
|
||||
<div className="max-w-[732px]">
|
||||
<div className="flex items-center gap-6 mb-12">
|
||||
<div className="w-[120px] h-[120px] rounded-full bg-bg-placeholder overflow-hidden border-2 border-neutral-50">
|
||||
<img src={user?.avatar || "/image/mascot-character.webp"} alt="Avatar" className="w-full h-full object-cover" />
|
||||
</div>
|
||||
<div>
|
||||
<button className="h-[34px] px-5 border border-primary-accent text-primary-accent rounded-sm text-[15px] font-semibold hover:bg-primary-50 transition-all cursor-pointer mb-2">
|
||||
Upload Foto
|
||||
</button>
|
||||
<p className="text-[10px] text-[#888888] leading-relaxed">
|
||||
Setidaknya rekomendasi ukuran 240x240 px.<br />
|
||||
.jpg, .jpeg, .png diperbolehkan
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8">
|
||||
<h2 className="text-[19px] font-semibold text-[#454545]">Informasi Pribadi</h2>
|
||||
|
||||
<div className="grid grid-cols-2 gap-x-4 gap-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Nama Depan</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Rizal"
|
||||
defaultValue={user?.fullname?.split(' ')[0] || ""}
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Nama Belakang</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Syaepulloh"
|
||||
defaultValue={user?.fullname?.split(' ').slice(1).join(' ') || ""}
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
placeholder="ahmduncl@yahoo.com"
|
||||
defaultValue={user?.email || ""}
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Nomor Telepon</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="+62 823-2321-8888"
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="h-[43px] px-8 bg-neutral-100 text-[#888888] rounded-sm text-[15px] font-semibold cursor-not-allowed mt-4">
|
||||
Simpan Perubahan
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'privacy' && (
|
||||
<div className="space-y-12">
|
||||
<div className="w-full p-6 border border-neutral-100 rounded-sm space-y-6">
|
||||
<div className="flex items-start justify-between gap-6">
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-[19px] font-semibold text-[#454545]">Two Step Authentication</h3>
|
||||
<p className="text-[15px] text-[#888888]">
|
||||
Tambahkan lapisan keamanan ekstra ke akun kamu.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setTwoStepAuthStep(meData?.user?.is_active ? 'input-email' : 'input-email')}
|
||||
className={cn(
|
||||
"h-[34px] px-6 rounded-sm text-xs font-semibold transition-all cursor-pointer",
|
||||
meData?.user?.is_active
|
||||
? "bg-red-50 text-red-600 border border-red-100 hover:bg-red-100"
|
||||
: "bg-primary-accent text-white hover:opacity-90"
|
||||
)}
|
||||
>
|
||||
{meData?.user?.is_active ? 'Nonaktifkan 2FA' : 'Aktifkan 2FA'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-6 border border-neutral-100 rounded-sm space-y-6">
|
||||
<h3 className="text-[19px] font-semibold text-[#454545]">Notifikasi Email</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
'Informasi roadmap, mentoring, dan artikel.',
|
||||
'Pembaruan sistem/update fitur/fixing/patch notes.',
|
||||
'Informasi penawaran/promosi program.'
|
||||
].map((label, i) => (
|
||||
<label key={i} className="flex items-center gap-3 cursor-pointer group">
|
||||
<input type="checkbox" defaultChecked={i === 0} className="w-4 h-4 rounded border-neutral-200 text-primary-accent focus:ring-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d] group-hover:text-[#454545] transition-colors">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-6 border border-neutral-100 rounded-sm space-y-8">
|
||||
<h3 className="text-[19px] font-semibold text-[#454545]">Rubah Password</h3>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Password Lama</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] outline-none focus:border-primary-accent"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Password Baru</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] outline-none focus:border-primary-accent"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Konfirmasi Password Baru</label>
|
||||
<input
|
||||
type="password"
|
||||
placeholder="••••••••"
|
||||
className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] outline-none focus:border-primary-accent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<button className="h-[43px] px-8 bg-primary-accent text-white rounded-sm text-[15px] font-semibold hover:opacity-90 transition-all cursor-pointer">
|
||||
Reset Password
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full p-6 border border-red-100 bg-red-50/30 rounded-sm flex items-start justify-between gap-6">
|
||||
<div className="flex-1 space-y-1">
|
||||
<h3 className="text-[19px] font-semibold text-red-600">Penghapusan Akun?</h3>
|
||||
<p className="text-[15px] text-[#888888]">
|
||||
Tindakan ini tidak dapat dibatalkan. Semua data kamu akan dihapus secara permanen.
|
||||
</p>
|
||||
</div>
|
||||
<button className="h-[34px] px-6 bg-red-500 text-white rounded-sm text-xs font-semibold hover:bg-red-600 transition-all cursor-pointer">
|
||||
Hapus Akun
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'preferences' && (
|
||||
<div className="grid grid-cols-2 gap-8">
|
||||
<div className="p-8 border border-neutral-100 rounded-sm space-y-8">
|
||||
<h3 className="text-[23px] font-semibold text-[#454545]">Learning Roadmap</h3>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Preferensi Belajar/Materi Roadmap</p>
|
||||
<div className="flex gap-4">
|
||||
{['Visual', 'Audio', 'Kinestetik'].map(label => (
|
||||
<label key={label} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-neutral-200 text-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Tujuan Belajar</p>
|
||||
<div className="space-y-3">
|
||||
{['Mempelajari skillset baru', 'Rencana perpindahan karir', 'Persiapan Karir'].map(label => (
|
||||
<label key={label} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-neutral-200 text-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Rata - rata waktu belajar</p>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none bg-white">
|
||||
<option>Waktu Belajar</option>
|
||||
<option>1-2 jam / hari</option>
|
||||
<option>3-5 jam / hari</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Fitur platform yang disukai</p>
|
||||
<div className="space-y-3">
|
||||
{['Video Interaktif', 'Text + Quiz', 'Project Based Learning', 'Guided Daily Task'].map(label => (
|
||||
<label key={label} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-neutral-200 text-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full h-[43px] bg-primary-accent text-white rounded-sm text-[15px] font-semibold hover:opacity-90 transition-all cursor-pointer">
|
||||
Kirim Preferensi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-8 border border-neutral-100 rounded-sm space-y-8">
|
||||
<h3 className="text-[23px] font-semibold text-[#454545]">Mentoring</h3>
|
||||
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Gaya Mentoring</p>
|
||||
<div className="space-y-3">
|
||||
{['Santai & Friendly', 'To The Point', 'Menjelaskan dengan praktik', 'Mulai dari fundamental'].map(label => (
|
||||
<label key={label} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" className="w-4 h-4 rounded border-neutral-200 text-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d]">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Waktu Mentoring</p>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#6d6d6d] outline-none bg-white">
|
||||
<option>Waktu Mentoring</option>
|
||||
<option>Pagi (08:00 - 12:00)</option>
|
||||
<option>Sore (13:00 - 17:00)</option>
|
||||
<option>Malam (19:00 - 22:00)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<p className="text-[15px] font-medium text-[#454545]">Metode Komunikasi</p>
|
||||
<div className="space-y-3">
|
||||
{['Chat', 'Online Meeting', 'Offline(Jika Memungkinkan)', 'Asynchronous', 'Option 5'].map(label => (
|
||||
<label key={label} className="flex items-center gap-3 cursor-pointer group">
|
||||
<input type="radio" name="comm" className="w-4 h-4 border-neutral-200 text-primary-accent focus:ring-primary-accent" />
|
||||
<span className="text-[15px] text-[#6d6d6d] group-hover:text-[#454545] transition-colors">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="w-full h-[43px] bg-primary-accent text-white rounded-sm text-[15px] font-semibold hover:opacity-90 transition-all cursor-pointer">
|
||||
Kirim Preferensi
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'faq' && (
|
||||
<div className="space-y-8">
|
||||
<div className="w-full aspect-[732/240] bg-primary-50 rounded-sm relative overflow-hidden flex items-center justify-center">
|
||||
<div className="text-center space-y-2 z-10">
|
||||
<h2 className="text-[23px] font-bold text-primary-accent">Frequently Asked Questions</h2>
|
||||
<p className="text-[15px] text-primary-800 max-w-[460px] mx-auto">
|
||||
Kamu lagi nyari info penting? Ini tempatnya. Bahkan<br />
|
||||
Main Character pun butuh FAQ kadang-kadang... :v
|
||||
</p>
|
||||
</div>
|
||||
<img src="/image/mascot-character.webp" alt="" className="absolute -left-10 bottom-0 w-[200px] h-auto opacity-20" />
|
||||
<img src="/image/mascot-character.webp" alt="" className="absolute -right-10 bottom-0 w-[200px] h-auto opacity-20 scale-x-[-1]" />
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ q: 'Bagaimana cara AI menyesuaikan roadmap belajar saya?', a: 'AI kami menggunakan data preferensi pembelajaran Anda (seperti topik favorit, gaya belajar, dan ketersediaan waktu) untuk membuat roadmap yang personal. Anda bisa mengedit preferensi ini di Settings > Preferensi Pembelajaran agar rekomendasi lebih akurat.' },
|
||||
{ q: 'Apakah roadmap belajar diperbarui secara otomatis saat saya menyelesaikan materi?', a: 'Ya, sistem kami akan melacak progres Anda dan memberikan materi selanjutnya secara otomatis.' },
|
||||
{ q: 'Bisakah saya memberikan umpan balik untuk platform ini', a: 'Tentu saja! Kami sangat menghargai feedback Anda melalui menu Umpan Balik.' },
|
||||
{ q: 'Bisakah saya mengganti mentor atau membatalkan sesi mentoring?', a: 'Anda dapat membatalkan sesi minimal 24 jam sebelumnya melalui dashboard mentoring.' },
|
||||
{ q: 'Apa yang harus saya lakukan jika mentor tidak hadir dalam sesi yang dijadwalkan?', a: 'Silakan laporkan kendala melalui menu Laporkan Kendala agar tim kami bisa segera menindaklanjuti.' }
|
||||
].map((item, i) => (
|
||||
<div key={i} className="border-b border-neutral-100 pb-4">
|
||||
<button
|
||||
onClick={() => setExpandedFaq(expandedFaq === i ? null : i)}
|
||||
className="w-full flex items-center justify-between text-left py-4 group cursor-pointer"
|
||||
>
|
||||
<span className="text-[15px] font-semibold text-[#454545] group-hover:text-primary-accent transition-colors">{item.q}</span>
|
||||
<Icon
|
||||
icon={expandedFaq === i ? 'mdi:chevron-up' : 'mdi:chevron-down'}
|
||||
className={`text-[#888888] transition-transform ${expandedFaq === i ? 'text-primary-accent' : ''}`}
|
||||
width="24"
|
||||
/>
|
||||
</button>
|
||||
{expandedFaq === i && (
|
||||
<div className="pb-4 animate-in fade-in slide-in-from-top-2">
|
||||
<p className="text-[15px] text-[#888888] leading-relaxed">{item.a}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'report' && (
|
||||
<div className="max-w-[732px] space-y-8">
|
||||
<div className="grid grid-cols-2 gap-x-8 gap-y-6">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Kode Laporan</label>
|
||||
<input readOnly value="IMP-00001" className="w-full h-[43px] px-5 bg-bg-light-blue border border-neutral-100 rounded-sm text-[15px] text-[#888888]" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Waktu Terjadi Kendala</label>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white">
|
||||
<option>Placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Kendala yang dialami</label>
|
||||
<input placeholder="Tulis kendala kamu" className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] outline-none focus:border-primary-accent" />
|
||||
<p className="text-[10px] text-[#888888]">Jelaskan secara singkat masalah yang kamu alami</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Menu/Fitur yang bermasalah</label>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white">
|
||||
<option>Placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Device Yang Digunakan</label>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white">
|
||||
<option>Placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="col-span-2 space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Deskripsi Kendala</label>
|
||||
<textarea
|
||||
placeholder="Deskripsi Detail Kendala"
|
||||
className="w-full h-[200px] p-5 border border-neutral-200 rounded-sm text-[15px] outline-none focus:border-primary-accent resize-none"
|
||||
/>
|
||||
<p className="text-[10px] text-[#888888]">Deskripsikan kendala yang kamu alami secara detail dan kronologisnya</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-medium text-[#454545]">Lampirkan Bukti</label>
|
||||
<select className="w-full h-[43px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white">
|
||||
<option>Placeholder</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button className="h-[43px] px-8 bg-primary-accent text-white rounded-sm text-[15px] font-semibold hover:opacity-90 transition-all cursor-pointer">
|
||||
Laporkan Kendala
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeSection === 'feedback' && (
|
||||
<div className="space-y-12">
|
||||
<div className="space-y-6">
|
||||
<p className="text-[17px] font-semibold text-[#454545] text-center">Seberapa puas anda dengan platform ini?</p>
|
||||
<div className="flex justify-center gap-4">
|
||||
{[1, 2, 3, 4, 5].map(n => (
|
||||
<button
|
||||
key={n}
|
||||
className="w-[124px] h-[47px] rounded-sm bg-primary-50 text-primary-accent font-semibold text-[17px] hover:bg-primary-accent hover:text-white transition-all cursor-pointer border border-primary-100"
|
||||
>
|
||||
{n}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<p className="text-[10px] text-[#888888] text-center tracking-widest uppercase">1 = Sangat Tidak Puas, 5 = Sangat Puas</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-semibold text-[#454545]">Apa hal yang paling kamu sukai dari platform ini</label>
|
||||
<textarea
|
||||
placeholder="Deskripsi Detail Kendala"
|
||||
className="w-full h-[140px] p-5 border border-neutral-200 rounded-sm text-[15px] text-[#454545] outline-none focus:border-primary-accent resize-none placeholder:text-[#BBBBBB]"
|
||||
/>
|
||||
<p className="text-[10px] text-[#888888]">Deskripsikan kendala yang kamu alami secara detail dan kronologisnya</p>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-3 gap-8">
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-semibold text-[#454545]">Seberapa mudah Anda menggunakan platform ini ?</label>
|
||||
<div className="relative">
|
||||
<select className="w-full h-[47px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white cursor-pointer appearance-none">
|
||||
<option>Placeholder</option>
|
||||
<option>Sangat Mudah</option>
|
||||
<option>Mudah</option>
|
||||
<option>Cukup</option>
|
||||
<option>Sulit</option>
|
||||
</select>
|
||||
<Icon icon="mdi:chevron-down" className="absolute right-4 top-1/2 -translate-y-1/2 text-[#888888] pointer-events-none" width="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-semibold text-[#454545]">Platform ini membantu perkembangan karier atau pembelajaran Anda?</label>
|
||||
<div className="relative">
|
||||
<select className="w-full h-[47px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white cursor-pointer appearance-none">
|
||||
<option>Placeholder</option>
|
||||
<option>Sangat Terbantu</option>
|
||||
<option>Terbantu</option>
|
||||
<option>Cukup</option>
|
||||
<option>Tidak Terbantu</option>
|
||||
</select>
|
||||
<Icon icon="mdi:chevron-down" className="absolute right-4 top-1/2 -translate-y-1/2 text-[#888888] pointer-events-none" width="20" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-semibold text-[#454545]">Bagian apa yang perlu kami ditingkatkan?</label>
|
||||
<div className="relative">
|
||||
<select className="w-full h-[47px] px-5 border border-neutral-200 rounded-sm text-[15px] text-[#888888] outline-none bg-white cursor-pointer appearance-none">
|
||||
<option>Placeholder</option>
|
||||
<option>Roadmap</option>
|
||||
<option>Mentoring</option>
|
||||
<option>UI/UX</option>
|
||||
<option>Materi</option>
|
||||
</select>
|
||||
<Icon icon="mdi:chevron-down" className="absolute right-4 top-1/2 -translate-y-1/2 text-[#888888] pointer-events-none" width="20" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-[15px] font-semibold text-[#454545]">Menurut pengalaman kamu, apa yang harus ditingkatkan dari platform ini?</label>
|
||||
<textarea
|
||||
placeholder="Deskripsi Detail Kendala"
|
||||
className="w-full h-[140px] p-5 border border-neutral-200 rounded-sm text-[15px] text-[#454545] outline-none focus:border-primary-accent resize-none placeholder:text-[#BBBBBB]"
|
||||
/>
|
||||
<p className="text-[10px] text-[#888888]">Deskripsikan kendala yang kamu alami secara detail dan kronologisnya</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-start">
|
||||
<button className="h-[47px] px-10 bg-primary-accent text-white rounded-sm text-[15px] font-bold hover:opacity-90 transition-all cursor-pointer">
|
||||
Kirim Feedback
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Two Step Auth Modals */}
|
||||
{twoStepAuthStep !== 'off' && (
|
||||
<div className="fixed inset-0 z-[100] flex items-center justify-center p-4 bg-black/20 backdrop-blur-sm">
|
||||
<div className="bg-white rounded-sm shadow-xl w-full max-w-[430px] p-10 relative">
|
||||
<button
|
||||
onClick={() => setTwoStepAuthStep('off')}
|
||||
className="absolute top-6 right-6 text-[#888888] hover:text-[#454545] cursor-pointer"
|
||||
>
|
||||
<Icon icon="mdi:close" width="24" />
|
||||
</button>
|
||||
|
||||
{twoStepAuthStep === 'input-email' && (
|
||||
<div className="text-center space-y-6">
|
||||
<div className="w-[72px] h-[72px] bg-primary-accent/10 rounded-full flex items-center justify-center mx-auto">
|
||||
<Icon icon="mdi:email-outline" width="36" className="text-primary-accent" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[23px] font-bold text-[#454545]">Verifikasi Email</h3>
|
||||
<p className="text-[15px] text-[#888888] leading-relaxed px-4">
|
||||
Masukkan email kamu untuk menerima kode verifikasi OTP
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-4 pt-2">
|
||||
<input
|
||||
type="email"
|
||||
placeholder="name@example.com"
|
||||
defaultValue={user?.email || ""}
|
||||
className="w-full h-[47px] px-5 border border-neutral-200 rounded-sm bg-white text-[#454545] text-[15px] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
<button
|
||||
onClick={() => setTwoStepAuthStep('input-otp')}
|
||||
className="w-full h-[47px] bg-primary-accent text-white rounded-sm font-semibold text-[15px] hover:opacity-90 transition-all cursor-pointer"
|
||||
>
|
||||
Kirim Kode
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{twoStepAuthStep === 'input-otp' && (
|
||||
<div className="text-center space-y-6">
|
||||
<div className="w-[72px] h-[72px] bg-primary-accent/10 rounded-full flex items-center justify-center mx-auto">
|
||||
<Icon icon="mdi:shield-check-outline" width="36" className="text-primary-accent" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[23px] font-bold text-[#454545]">Masukkan Kode OTP</h3>
|
||||
<p className="text-[15px] text-[#888888] leading-relaxed px-4">
|
||||
Kami telah mengirimkan kode 6 digit ke email kamu
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-8 pt-2">
|
||||
<div className="flex justify-between gap-2">
|
||||
{[1, 2, 3, 4, 5, 6].map((i) => (
|
||||
<input
|
||||
key={i}
|
||||
type="text"
|
||||
maxLength={1}
|
||||
className="w-[52px] h-[58px] border border-neutral-200 rounded-sm text-center text-xl font-bold bg-white text-[#454545] outline-none focus:border-primary-accent transition-all"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<button
|
||||
onClick={() => setTwoStepAuthStep('done')}
|
||||
className="w-full h-[47px] bg-primary-accent text-white rounded-sm font-semibold text-[15px] hover:opacity-90 transition-all cursor-pointer"
|
||||
>
|
||||
Verifikasi
|
||||
</button>
|
||||
<p className="text-[13px] text-[#888888]">
|
||||
Tidak menerima kode? <button className="text-primary-accent font-semibold hover:underline cursor-pointer">Kirim ulang</button>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{twoStepAuthStep === 'done' && (
|
||||
<div className="text-center space-y-6">
|
||||
<div className="w-[72px] h-[72px] bg-success-500/10 rounded-full flex items-center justify-center mx-auto">
|
||||
<Icon icon="mdi:check-circle-outline" width="36" className="text-success-500" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-[23px] font-bold text-[#454545]">
|
||||
{meData?.user?.is_active ? '2FA Berhasil Dinonaktifkan!' : '2FA Berhasil Diaktifkan!'}
|
||||
</h3>
|
||||
<p className="text-[15px] text-[#888888] leading-relaxed px-4">
|
||||
{meData?.user?.is_active
|
||||
? 'Fitur autentikasi telah dinonaktifkan dari akun kamu.'
|
||||
: 'Akun kamu sekarang lebih aman dengan verifikasi dua langkah.'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<button
|
||||
onClick={() => setTwoStepAuthStep('off')}
|
||||
className="w-full h-[47px] bg-primary-accent text-white rounded-sm font-semibold text-[15px] hover:opacity-90 transition-all cursor-pointer"
|
||||
>
|
||||
Selesai
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -132,6 +132,7 @@ export const EditProfileModal: FC<EditProfileModalProps> = ({ isOpen, onClose, s
|
||||
msg = parsed.message;
|
||||
}
|
||||
} catch {
|
||||
// JSON parse failed, use original message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { UserDashboard } from '../../routes/_authenticated/dashboard_/_components/user/user-dashboard';
|
||||
import { MentorDashboard } from '../../routes/_authenticated/dashboard_/_components/mentor/mentor-dashboard';
|
||||
import { LearningPathPage } from '../../routes/_authenticated/dashboard/learning-path';
|
||||
import { MentoringPage } from '../../routes/_authenticated/dashboard/mentoring';
|
||||
import { RoadmapDiscoveryPage } from '../../routes/_authenticated/dashboard/roadmap-discovery';
|
||||
|
||||
/**
|
||||
* Dashboard Route/Persona Render Tests
|
||||
@@ -20,16 +23,6 @@ describe('Dashboard Persona Rendering', () => {
|
||||
expect(screen.getByText('Selamat Datang di Dimentorin.dev')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display welcome card body text', () => {
|
||||
render(<UserDashboard />);
|
||||
expect(screen.getByText(/Yuk, mulai petualanganmu di menu Skill Discovery/)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display CTA button', () => {
|
||||
render(<UserDashboard />);
|
||||
expect(screen.getByText('Temukan Roadmapmu^^')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display overview metrics', () => {
|
||||
render(<UserDashboard />);
|
||||
expect(screen.getByText('Mentoring Session')).toBeDefined();
|
||||
@@ -37,44 +30,75 @@ describe('Dashboard Persona Rendering', () => {
|
||||
expect(screen.getByText('Article Published')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display metrics with zero values', () => {
|
||||
render(<UserDashboard />);
|
||||
const zeros = screen.getAllByText('0');
|
||||
expect(zeros.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display roadmap section', async () => {
|
||||
it('should display roadmap progress and label', async () => {
|
||||
render(<UserDashboard />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Your Roadmap')).toBeDefined();
|
||||
expect(screen.getByText('Roadmaps')).toBeDefined();
|
||||
expect(screen.getByText('Front End Basic')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display progress labels on roadmap', async () => {
|
||||
render(<UserDashboard />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('1/30 days milestones completed')).toBeDefined();
|
||||
expect(screen.getByText('50%')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display roadmap action button', async () => {
|
||||
render(<UserDashboard />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByText('Lanjut Belajar').length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
it('should display articles table section and headers', () => {
|
||||
it('should display article table headers', () => {
|
||||
render(<UserDashboard />);
|
||||
expect(screen.getByText('Your Articles')).toBeDefined();
|
||||
expect(screen.getByText('No.')).toBeDefined();
|
||||
expect(screen.getByText('Judul Artikel')).toBeDefined();
|
||||
expect(screen.getByText('Materi')).toBeDefined();
|
||||
expect(screen.getByText('Status')).toBeDefined();
|
||||
expect(screen.getByText('Submit Date')).toBeDefined();
|
||||
expect(screen.getByText('Action')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('LearningPathPage Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render roadmap tab by default', () => {
|
||||
render(<LearningPathPage />);
|
||||
expect(screen.getByText('Roadmap Kamu')).toBeDefined();
|
||||
expect(screen.getByText('Day 1 - Materi A')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should switch to article tab and show article table', () => {
|
||||
render(<LearningPathPage />);
|
||||
const articleTab = screen.getByRole('button', { name: /Article/i });
|
||||
fireEvent.click(articleTab);
|
||||
expect(screen.getByText('Judul Artikel')).toBeDefined();
|
||||
expect(screen.getByText('Cek Detail')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('MentoringPage Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render search input and table headings', () => {
|
||||
render(<MentoringPage />);
|
||||
expect(screen.getByPlaceholderText('Cari berdasarkan nama item')).toBeDefined();
|
||||
expect(screen.getByText('Nama Mentor')).toBeDefined();
|
||||
expect(screen.getByText('Sesi Mentoring')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display action buttons for mentoring rows', async () => {
|
||||
render(<MentoringPage />);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Cek Detail')).toBeDefined();
|
||||
expect(screen.getByText('Kirim Feedback')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('RoadmapDiscoveryPage Component', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render discovery header and generate button', () => {
|
||||
render(<RoadmapDiscoveryPage />);
|
||||
expect(screen.getByText('Start your Journey')).toBeDefined();
|
||||
expect(screen.getByRole('button', { name: /Generate/i })).toBeDefined();
|
||||
expect(screen.getByPlaceholderText('Mau belajar roadmap apa?')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -88,55 +112,9 @@ describe('Dashboard Persona Rendering', () => {
|
||||
expect(screen.getByText('Selamat Datang di Dimentorin.dev')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display welcome card body text', () => {
|
||||
render(<MentorDashboard />);
|
||||
expect(screen.getByText(/Senpai~ saatnya kamu bantu para junior/)).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display Overviews tab', () => {
|
||||
render(<MentorDashboard />);
|
||||
expect(screen.getByText('Overviews')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display Analytics tab', () => {
|
||||
render(<MentorDashboard />);
|
||||
expect(screen.getByText('Analytics')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display overview metrics labels', () => {
|
||||
render(<MentorDashboard />);
|
||||
expect(screen.getByText('Your Rating')).toBeDefined();
|
||||
expect(screen.getByText('Session Complete')).toBeDefined();
|
||||
expect(screen.getByText('Mentee Impacted')).toBeDefined();
|
||||
expect(screen.getByText('Total Feedback')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display metrics with zero values', () => {
|
||||
render(<MentorDashboard />);
|
||||
const zeros = screen.getAllByText('0');
|
||||
expect(zeros.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should display Topics chart on analytics tab', async () => {
|
||||
render(<MentorDashboard />);
|
||||
const analyticsTab = screen.getByText('Analytics');
|
||||
fireEvent.click(analyticsTab);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Topics')).toBeDefined();
|
||||
expect(screen.getByText('Basic IT')).toBeDefined();
|
||||
expect(screen.getByText('Career & Self...')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
it('should display Session Time Preference chart on analytics tab', async () => {
|
||||
render(<MentorDashboard />);
|
||||
const analyticsTab = screen.getByText('Analytics');
|
||||
fireEvent.click(analyticsTab);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Session Time Preference')).toBeDefined();
|
||||
expect(screen.getByText('17:00 - 17:45')).toBeDefined();
|
||||
expect(screen.getByText('19:00 - 19:45')).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
"backoffice:lint": "nx run backoffice:lint",
|
||||
"dimentorin:dev": "nx dev dimentorin",
|
||||
"dimentorin:test": "nx test dimentorin",
|
||||
"dimentorin:ss": "playwright test --config=apps/dimentorin/playwright.config.ts apps/dimentorin/e2e/screenshots.test.ts",
|
||||
"dimentorin:build": "nx build dimentorin",
|
||||
"dimentorin:prod": "serve ./dist/apps/dimentorin",
|
||||
"dimentorin:lint": "nx run dimentorin:lint",
|
||||
|
||||
Reference in New Issue
Block a user