diff --git a/.gitignore b/.gitignore index d7a684e..30bf5ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,7 @@ dist tmp out-tsc - +docs/FigmaImage/ # dependencies node_modules diff --git a/apps/dimentorin/e2e/screenshots.test.ts b/apps/dimentorin/e2e/screenshots.test.ts index afab5b1..ab7f0d6 100644 --- a/apps/dimentorin/e2e/screenshots.test.ts +++ b/apps/dimentorin/e2e/screenshots.test.ts @@ -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); } diff --git a/apps/dimentorin/playwright.config.ts b/apps/dimentorin/playwright.config.ts index 3bdf9d6..e6e840f 100644 --- a/apps/dimentorin/playwright.config.ts +++ b/apps/dimentorin/playwright.config.ts @@ -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', + }, }, }); diff --git a/apps/dimentorin/src/routeTree.gen.ts b/apps/dimentorin/src/routeTree.gen.ts index 370aade..0b89644 100644 --- a/apps/dimentorin/src/routeTree.gen.ts +++ b/apps/dimentorin/src/routeTree.gen.ts @@ -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, diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard.tsx index 2a01e94..1d19a78 100644 --- a/apps/dimentorin/src/routes/_authenticated/dashboard.tsx +++ b/apps/dimentorin/src/routes/_authenticated/dashboard.tsx @@ -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 ( -
+
Dimentorin.dev
- - +
+ + + {isNotificationsOpen && ( +
+
+

Notifikasi

+ +
+
+ {notifications.map((n) => ( +
+
+

{n.title}

+ {n.time} +
+

{n.message}

+
+ ))} +
+
+ +
+
+ )} +
+ + + - +
); diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/article-preview-modal.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/article-preview-modal.tsx new file mode 100644 index 0000000..b055d98 --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/article-preview-modal.tsx @@ -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 = ({ isOpen, onClose, article }) => { + if (!isOpen || !article) return null + + return ( +
+
+ + +
+
+ + Article Preview +
+

{article.judul}

+
+
+ + {article.submitDate || 'Not submitted yet'} +
+
+ + {article.materi} +
+
+ {article.status} +
+
+
+ +
+
+

Introduction

+

+ 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. +

+ +
+ Article Cover +
+ +

Key Concepts

+

+ 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. +

+ +
    +
  • First major point of discussion
  • +
  • Second key takeaway for readers
  • +
  • Important consideration for implementation
  • +
+ +

Conclusion

+

+ In conclusion, following these steps will help you achieve the best results. Keep practicing and exploring more advanced topics to further your knowledge. +

+
+
+ +
+ + {article.status !== 'Done' && ( + + )} +
+
+
+ ) +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentor-contact-modal.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentor-contact-modal.tsx new file mode 100644 index 0000000..8acc25c --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentor-contact-modal.tsx @@ -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 = ({ isOpen, onClose, mentor }) => { + if (!isOpen || !mentor) return null + + return ( +
+
+ + +
+
+ Mentor +
+

{mentor.name}

+
+ {mentor.topics.map((t, i) => ( + + {t} + + ))} +
+
+ + + + +
+
+ ) +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentoring-detail-modal.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentoring-detail-modal.tsx new file mode 100644 index 0000000..6984556 --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_components/modals/mentoring-detail-modal.tsx @@ -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 = ({ + isOpen, + onClose, + onContactMentor, + mentor, + session, +}) => { + const [pertanyaan, setPertanyaan] = useState('') + + if (!isOpen) return null + + return ( +
+
+ {/* Header */} +
+

Detail Sesi Mentoring

+ +
+ + {/* Main Content Grid */} +
+ {/* Left Column: Mentor Card */} +
+

Your Senpai

+
+
+ {mentor.name} +
+

+ {mentor.name} +

+

+ {mentor.title} +

+
+
+ + {/* Right Column: Session Details */} +
+ {/* Topics */} +
+ +
+ {mentor.topics.map((topic, i) => ( + + ▪ {topic} + + ))} +
+
+ + {/* Date and Time Row */} +
+
+ +
+ {session.date} +
+
+
+ +
+ {session.time} +
+
+
+ + {/* Location */} +
+ +
+ {session.location} +
+
+ + {/* Pertanyaan Untuk Senpai */} +
+ +