diff --git a/.gitignore b/.gitignore index 184e7b7..d7a684e 100644 --- a/.gitignore +++ b/.gitignore @@ -72,4 +72,11 @@ out result .claude/worktrees -.claude/settings.local.json \ No newline at end of file +.claude/settings.local.json + +# Screenshots and Test Reports +**/screenshots +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache \ No newline at end of file diff --git a/apps/backoffice/.env.example b/apps/backoffice/.env.example index 292a14c..723972e 100644 --- a/apps/backoffice/.env.example +++ b/apps/backoffice/.env.example @@ -1 +1,2 @@ VITE_API_URL= +VITE_GITHUB_CLIENT_ID= diff --git a/apps/dimentorin/.env.example b/apps/dimentorin/.env.example index 3452b5c..5fd9090 100644 --- a/apps/dimentorin/.env.example +++ b/apps/dimentorin/.env.example @@ -1 +1,3 @@ -VITE_API_URL= \ No newline at end of file +VITE_API_URL= +VITE_DISABLE_AUTH=false +VITE_GITHUB_CLIENT_ID= \ No newline at end of file diff --git a/apps/dimentorin/e2e/dashboard.mock.spec.ts b/apps/dimentorin/e2e/dashboard.mock.spec.ts new file mode 100644 index 0000000..8314c27 --- /dev/null +++ b/apps/dimentorin/e2e/dashboard.mock.spec.ts @@ -0,0 +1,141 @@ +import { test, expect } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; + +/** + * Test configuration for dashboard screenshot capture + */ +interface DashboardTestCase { + persona: 'user' | 'mentor'; + viewport: 'desktop' | 'mobile'; + roleName: string; + waitForText: string; + screenshotFileName: string; +} + +/** + * Mock authentication token structure + */ +const mockToken = { + access_token: 'mock_access_token_' + Math.random().toString(36).substring(7), + refresh_token: 'mock_refresh_token_' + Math.random().toString(36).substring(7), +}; + +/** + * Create a mock user object based on persona + */ +function createMockUser(persona: 'user' | 'mentor') { + const basUser = { + id: 'mock-user-id', + email: 'test@imphnen.com', + fullname: 'Test User', + is_active: true, + role: { + id: 'role-' + persona, + name: persona === 'mentor' ? 'Mentor Role' : 'User Role', + permissions: [], + }, + }; + return basUser; +} + +/** + * Dashboard test cases + */ +const testCases: DashboardTestCase[] = [ + { + persona: 'user', + viewport: 'desktop', + roleName: 'User Role', + waitForText: 'Roadmaps', + screenshotFileName: 'dashboard_user_mock_desktop.png', + }, + { + persona: 'mentor', + viewport: 'desktop', + roleName: 'Mentor Role', + waitForText: 'Analytics', + screenshotFileName: 'dashboard_mentor_mock_desktop.png', + }, + { + persona: 'user', + viewport: 'mobile', + roleName: 'User Role', + waitForText: 'Roadmaps', + screenshotFileName: 'dashboard_user_mock_mobile.png', + }, + { + persona: 'mentor', + viewport: 'mobile', + roleName: 'Mentor Role', + waitForText: 'Analytics', + screenshotFileName: 'dashboard_mentor_mock_mobile.png', + }, +]; + +test.describe('Dashboard Screenshot Capture', () => { + const screenshotDir = path.resolve(__dirname, '../screenshots'); + + test.beforeAll(() => { + if (!fs.existsSync(screenshotDir)) { + fs.mkdirSync(screenshotDir, { recursive: true }); + } + }); + + /** + * Run test for each dashboard variant + */ + for (const testCase of testCases) { + test(`capture ${testCase.persona} ${testCase.viewport} dashboard screenshot`, async ({ + page, + context, + }) => { + // Set viewport size based on device type + const viewportSize = testCase.viewport === 'desktop' ? { width: 1280, height: 832 } : { width: 375, height: 667 }; + await page.setViewportSize(viewportSize); + + // Set authentication cookie with mock token + const tokenCookie = { + name: 'token', + value: JSON.stringify({ token: mockToken }), + url: 'http://localhost:3000', + secure: false, + httpOnly: false, + sameSite: 'Strict' as const, + }; + await context.addCookies([tokenCookie]); + + // Set user in localStorage + const mockUser = createMockUser(testCase.persona); + await page.goto('http://localhost:3000', { waitUntil: 'domcontentloaded' }); + await page.evaluate( + ({ user }) => { + localStorage.setItem('users', JSON.stringify(user)); + }, + { user: mockUser } + ); + + // Navigate to dashboard with persona parameter for explicit override + const dashboardUrl = `http://localhost:3000/dashboard?persona=${testCase.persona}`; + await page.goto(dashboardUrl, { waitUntil: 'networkidle', timeout: 30000 }); + + // Wait for stable dashboard content + await page.waitForSelector( + `text="${testCase.waitForText}"`, + { timeout: 10000 } + ); + + // Additional wait for content to render + await page.waitForTimeout(1000); + + // Capture screenshot + const screenshotPath = path.join(screenshotDir, testCase.screenshotFileName); + await page.screenshot({ + path: screenshotPath, + fullPage: true, + }); + + console.log(`✓ Captured: ${testCase.screenshotFileName}`); + }); + } +}); diff --git a/apps/dimentorin/e2e/screenshots.test.ts b/apps/dimentorin/e2e/screenshots.test.ts new file mode 100644 index 0000000..afab5b1 --- /dev/null +++ b/apps/dimentorin/e2e/screenshots.test.ts @@ -0,0 +1,83 @@ +import { test, expect } from '@playwright/test'; +import fs from 'fs'; +import path from 'path'; + +/** + * Dynamically extract all routes from the TanStack Router generated file. + */ +function getDynamicRoutes(): string[] { + const routeTreePath = path.resolve(__dirname, '../src/routeTree.gen.ts'); + if (!fs.existsSync(routeTreePath)) { + console.warn('Route tree file not found, falling back to basic routes'); + return ['/']; + } + + const content = fs.readFileSync(routeTreePath, 'utf-8'); + + // Extract the FileRoutesByFullPath interface block + const interfaceMatch = content.match(/export interface FileRoutesByFullPath \{([\s\S]*?)\}/); + if (!interfaceMatch) return ['/']; + + const block = interfaceMatch[1]; + + // Extract all strings in single quotes + const routeMatches = block.match(/'([^']+)'/g); + if (!routeMatches) return ['/']; + + return routeMatches.map(m => { + let route = m.replace(/'/g, ''); + + // Normalize trailing slashes (Optional cleanup) + if (route !== '/' && route.endsWith('/')) { + route = route.slice(0, -1); + } + + // Replace dynamic parameters with sample values + return route + .replace(/\$slug/g, 'sample-article') + .replace(/\$id/g, 'sample-id') + .replace(/\$taskId/g, 'sample-task'); + }); +} + +test.describe('Automated Route Discovery & Screenshots', () => { + const baseURL = process.env.BASE_URL || 'http://localhost:3000'; + const screenshotDir = path.resolve(__dirname, '../screenshots'); + const routes = [...new Set(getDynamicRoutes())]; // Unique routes + + test.beforeAll(() => { + if (!fs.existsSync(screenshotDir)) { + fs.mkdirSync(screenshotDir, { recursive: true }); + } + }); + + console.log(`Discovered ${routes.length} routes for processing.`); + + for (const route of routes) { + test(`capture screenshot: ${route}`, async ({ page }) => { + console.log(`Processing: ${baseURL}${route}`); + + try { + await page.goto(`${baseURL}${route}`, { + waitUntil: 'networkidle', + timeout: 30000 + }); + + // Wait for rendering + await page.waitForTimeout(1500); + + const fileName = route.replace(/\//g, '_').replace(/^_/, '').replace(/[:$]/g, '') || 'home'; + const screenshotPath = path.join(screenshotDir, `${fileName}.png`); + + await page.screenshot({ + path: screenshotPath, + fullPage: true + }); + + console.log(`Success: ${fileName}.png`); + } catch (error) { + console.error(`Failed to capture ${route}:`, error.message); + } + }); + } +}); diff --git a/apps/dimentorin/playwright.config.ts b/apps/dimentorin/playwright.config.ts new file mode 100644 index 0000000..3bdf9d6 --- /dev/null +++ b/apps/dimentorin/playwright.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from '@playwright/test'; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +export default defineConfig({ + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Base URL to use in actions like `await page.goto('/')`. */ + baseURL: 'http://localhost:3000', + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npx nx dev dimentorin', + url: 'http://localhost:3000', + reuseExistingServer: true, + cwd: '../../', // Run from root where Nx is available + timeout: 120000, + }, +}); diff --git a/apps/dimentorin/public/image/mascot-1.png b/apps/dimentorin/public/image/mascot-1.png new file mode 100644 index 0000000..7488c27 Binary files /dev/null and b/apps/dimentorin/public/image/mascot-1.png differ diff --git a/apps/dimentorin/src/index.css b/apps/dimentorin/src/index.css index 931d17d..70b2b40 100644 --- a/apps/dimentorin/src/index.css +++ b/apps/dimentorin/src/index.css @@ -87,6 +87,22 @@ --text-p2: 1.23rem; /* ~19.68px */ --text-label1: 0.98rem; /* ~15.74px */ --text-label2: 0.78rem; /* ~12.59px */ + + /* Custom Colors for CSS Hex Exact Matches - IMPHNEN Dimentorin Brand */ + --color-bg-light-blue: #f7fbff; /* Primary dashboard background */ + --color-primary-accent: #23a1eb; /* Primary interactive color (buttons, links, active states) */ + --color-text-dark: #1a1a1a; /* Ultra-dark text (auth titles, headers) */ + --color-text-secondary: #6d6d6d; /* Secondary text (subtitles, descriptions) */ + --color-text-label: #454545; /* Form labels and descriptive labels */ + --color-text-muted: #888888; /* Muted text (nav items, metric labels) */ + --color-border-light: #bce1fb; /* Light blue borders (inputs, tabs) */ + --color-placeholder: #b0b0b0; /* Input placeholder text, disabled backgrounds */ + --color-bg-hover: #f6f6f6; /* Hover state backgrounds (nav items) */ + --color-divider-blue: #81cbf8; /* Divider lines and gradient fills */ + --color-border-subtle: #e5e5e5; /* Very light borders (buttons) */ + --color-bg-secondary: #f8f8f8; /* Secondary backgrounds (alt button) */ + --color-bg-placeholder: #d9d9d9; /* Placeholder backgrounds (avatars, images) */ + --color-text-tab-default: #4f4f4f; /* Tab button text color */ } @layer base { @@ -124,4 +140,8 @@ .scrollbar-hide::-webkit-scrollbar { @apply hidden; } + + .shadow-auth { + box-shadow: 0 10px 40px rgba(35, 161, 235, 0.1); + } } diff --git a/apps/dimentorin/src/routeTree.gen.ts b/apps/dimentorin/src/routeTree.gen.ts index e89dd51..370aade 100644 --- a/apps/dimentorin/src/routeTree.gen.ts +++ b/apps/dimentorin/src/routeTree.gen.ts @@ -18,7 +18,7 @@ import { Route as SiteProfileRouteImport } from './routes/_site/profile' import { Route as SiteMentoringRouteImport } from './routes/_site/mentoring' import { Route as SiteArticlesRouteImport } from './routes/_site/articles' import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard' -import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard_/index' +import { Route as AuthenticatedDashboardIndexRouteImport } from './routes/_authenticated/dashboard/index' import { Route as SiteProfileIdRouteImport } from './routes/_site/profile_/$id' import { Route as SiteMentoringIdRouteImport } from './routes/_site/mentoring_/$id' import { Route as SiteArticlesSlugRouteImport } from './routes/_site/articles_/$slug' @@ -29,6 +29,9 @@ import { Route as PublicAuthGoogleOauthPopupRouteImport } from './routes/_public import { Route as PublicAuthGoogleCallbackRouteImport } from './routes/_public/auth/google-callback' import { Route as PublicAuthForgotRouteImport } from './routes/_public/auth/forgot' import { Route as AuthenticatedDashboardSettingsRouteImport } from './routes/_authenticated/dashboard_/settings' +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 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' @@ -80,9 +83,9 @@ const AuthenticatedDashboardRoute = AuthenticatedDashboardRouteImport.update({ } as any) const AuthenticatedDashboardIndexRoute = AuthenticatedDashboardIndexRouteImport.update({ - id: '/dashboard_/', - path: '/dashboard/', - getParentRoute: () => AuthenticatedRoute, + id: '/', + path: '/', + getParentRoute: () => AuthenticatedDashboardRoute, } as any) const SiteProfileIdRoute = SiteProfileIdRouteImport.update({ id: '/profile_/$id', @@ -138,6 +141,24 @@ const AuthenticatedDashboardSettingsRoute = path: '/dashboard/settings', getParentRoute: () => AuthenticatedRoute, } as any) +const AuthenticatedDashboardRoadmapDiscoveryRoute = + AuthenticatedDashboardRoadmapDiscoveryRouteImport.update({ + id: '/roadmap-discovery', + path: '/roadmap-discovery', + getParentRoute: () => AuthenticatedDashboardRoute, + } as any) +const AuthenticatedDashboardMentoringRoute = + AuthenticatedDashboardMentoringRouteImport.update({ + id: '/mentoring', + path: '/mentoring', + getParentRoute: () => AuthenticatedDashboardRoute, + } as any) +const AuthenticatedDashboardLearningPathRoute = + AuthenticatedDashboardLearningPathRouteImport.update({ + id: '/learning-path', + path: '/learning-path', + getParentRoute: () => AuthenticatedDashboardRoute, + } as any) const PublicAuthRegisterSuccessRoute = PublicAuthRegisterSuccessRouteImport.update({ id: '/auth/register_/success', @@ -174,11 +195,14 @@ const PublicAuthForgotOtpRoute = PublicAuthForgotOtpRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute - '/dashboard': typeof AuthenticatedDashboardRoute + '/dashboard': typeof AuthenticatedDashboardRouteWithChildren '/articles': typeof SiteArticlesRoute '/mentoring': typeof SiteMentoringRoute '/profile': typeof SiteProfileRoute '/resources': typeof SiteResourcesRoute + '/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute + '/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute + '/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute '/dashboard/settings': typeof AuthenticatedDashboardSettingsRoute '/auth/forgot': typeof PublicAuthForgotRoute '/auth/google-callback': typeof PublicAuthGoogleCallbackRoute @@ -199,11 +223,13 @@ export interface FileRoutesByFullPath { } export interface FileRoutesByTo { '/': typeof IndexRoute - '/dashboard': typeof AuthenticatedDashboardIndexRoute '/articles': typeof SiteArticlesRoute '/mentoring': typeof SiteMentoringRoute '/profile': typeof SiteProfileRoute '/resources': typeof SiteResourcesRoute + '/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute + '/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute + '/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute '/dashboard/settings': typeof AuthenticatedDashboardSettingsRoute '/auth/forgot': typeof PublicAuthForgotRoute '/auth/google-callback': typeof PublicAuthGoogleCallbackRoute @@ -214,6 +240,7 @@ export interface FileRoutesByTo { '/articles/$slug': typeof SiteArticlesSlugRoute '/mentoring/$id': typeof SiteMentoringIdRoute '/profile/$id': typeof SiteProfileIdRoute + '/dashboard': typeof AuthenticatedDashboardIndexRoute '/auth/forgot/otp': typeof PublicAuthForgotOtpRoute '/auth/forgot/summon': typeof PublicAuthForgotSummonRoute '/auth/register-mentor/pending': typeof PublicAuthRegisterMentorPendingRoute @@ -227,11 +254,14 @@ export interface FileRoutesById { '/_authenticated': typeof AuthenticatedRouteWithChildren '/_public': typeof PublicRouteWithChildren '/_site': typeof SiteRouteWithChildren - '/_authenticated/dashboard': typeof AuthenticatedDashboardRoute + '/_authenticated/dashboard': typeof AuthenticatedDashboardRouteWithChildren '/_site/articles': typeof SiteArticlesRoute '/_site/mentoring': typeof SiteMentoringRoute '/_site/profile': typeof SiteProfileRoute '/_site/resources': typeof SiteResourcesRoute + '/_authenticated/dashboard/learning-path': typeof AuthenticatedDashboardLearningPathRoute + '/_authenticated/dashboard/mentoring': typeof AuthenticatedDashboardMentoringRoute + '/_authenticated/dashboard/roadmap-discovery': typeof AuthenticatedDashboardRoadmapDiscoveryRoute '/_authenticated/dashboard_/settings': typeof AuthenticatedDashboardSettingsRoute '/_public/auth/forgot': typeof PublicAuthForgotRoute '/_public/auth/google-callback': typeof PublicAuthGoogleCallbackRoute @@ -242,7 +272,7 @@ export interface FileRoutesById { '/_site/articles_/$slug': typeof SiteArticlesSlugRoute '/_site/mentoring_/$id': typeof SiteMentoringIdRoute '/_site/profile_/$id': typeof SiteProfileIdRoute - '/_authenticated/dashboard_/': typeof AuthenticatedDashboardIndexRoute + '/_authenticated/dashboard/': typeof AuthenticatedDashboardIndexRoute '/_public/auth/forgot_/otp': typeof PublicAuthForgotOtpRoute '/_public/auth/forgot_/summon': typeof PublicAuthForgotSummonRoute '/_public/auth/register-mentor_/pending': typeof PublicAuthRegisterMentorPendingRoute @@ -259,6 +289,9 @@ export interface FileRouteTypes { | '/mentoring' | '/profile' | '/resources' + | '/dashboard/learning-path' + | '/dashboard/mentoring' + | '/dashboard/roadmap-discovery' | '/dashboard/settings' | '/auth/forgot' | '/auth/google-callback' @@ -279,11 +312,13 @@ export interface FileRouteTypes { fileRoutesByTo: FileRoutesByTo to: | '/' - | '/dashboard' | '/articles' | '/mentoring' | '/profile' | '/resources' + | '/dashboard/learning-path' + | '/dashboard/mentoring' + | '/dashboard/roadmap-discovery' | '/dashboard/settings' | '/auth/forgot' | '/auth/google-callback' @@ -294,6 +329,7 @@ export interface FileRouteTypes { | '/articles/$slug' | '/mentoring/$id' | '/profile/$id' + | '/dashboard' | '/auth/forgot/otp' | '/auth/forgot/summon' | '/auth/register-mentor/pending' @@ -311,6 +347,9 @@ export interface FileRouteTypes { | '/_site/mentoring' | '/_site/profile' | '/_site/resources' + | '/_authenticated/dashboard/learning-path' + | '/_authenticated/dashboard/mentoring' + | '/_authenticated/dashboard/roadmap-discovery' | '/_authenticated/dashboard_/settings' | '/_public/auth/forgot' | '/_public/auth/google-callback' @@ -321,7 +360,7 @@ export interface FileRouteTypes { | '/_site/articles_/$slug' | '/_site/mentoring_/$id' | '/_site/profile_/$id' - | '/_authenticated/dashboard_/' + | '/_authenticated/dashboard/' | '/_public/auth/forgot_/otp' | '/_public/auth/forgot_/summon' | '/_public/auth/register-mentor_/pending' @@ -402,12 +441,12 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedDashboardRouteImport parentRoute: typeof AuthenticatedRoute } - '/_authenticated/dashboard_/': { - id: '/_authenticated/dashboard_/' - path: '/dashboard' + '/_authenticated/dashboard/': { + id: '/_authenticated/dashboard/' + path: '/' fullPath: '/dashboard/' preLoaderRoute: typeof AuthenticatedDashboardIndexRouteImport - parentRoute: typeof AuthenticatedRoute + parentRoute: typeof AuthenticatedDashboardRoute } '/_site/profile_/$id': { id: '/_site/profile_/$id' @@ -479,6 +518,27 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof AuthenticatedDashboardSettingsRouteImport parentRoute: typeof AuthenticatedRoute } + '/_authenticated/dashboard/roadmap-discovery': { + id: '/_authenticated/dashboard/roadmap-discovery' + path: '/roadmap-discovery' + fullPath: '/dashboard/roadmap-discovery' + preLoaderRoute: typeof AuthenticatedDashboardRoadmapDiscoveryRouteImport + parentRoute: typeof AuthenticatedDashboardRoute + } + '/_authenticated/dashboard/mentoring': { + id: '/_authenticated/dashboard/mentoring' + path: '/mentoring' + fullPath: '/dashboard/mentoring' + preLoaderRoute: typeof AuthenticatedDashboardMentoringRouteImport + parentRoute: typeof AuthenticatedDashboardRoute + } + '/_authenticated/dashboard/learning-path': { + id: '/_authenticated/dashboard/learning-path' + path: '/learning-path' + fullPath: '/dashboard/learning-path' + preLoaderRoute: typeof AuthenticatedDashboardLearningPathRouteImport + parentRoute: typeof AuthenticatedDashboardRoute + } '/_public/auth/register_/success': { id: '/_public/auth/register_/success' path: '/auth/register/success' @@ -524,16 +584,36 @@ declare module '@tanstack/react-router' { } } -interface AuthenticatedRouteChildren { - AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute - AuthenticatedDashboardSettingsRoute: typeof AuthenticatedDashboardSettingsRoute +interface AuthenticatedDashboardRouteChildren { + AuthenticatedDashboardLearningPathRoute: typeof AuthenticatedDashboardLearningPathRoute + AuthenticatedDashboardMentoringRoute: typeof AuthenticatedDashboardMentoringRoute + AuthenticatedDashboardRoadmapDiscoveryRoute: typeof AuthenticatedDashboardRoadmapDiscoveryRoute AuthenticatedDashboardIndexRoute: typeof AuthenticatedDashboardIndexRoute } +const AuthenticatedDashboardRouteChildren: AuthenticatedDashboardRouteChildren = + { + AuthenticatedDashboardLearningPathRoute: + AuthenticatedDashboardLearningPathRoute, + AuthenticatedDashboardMentoringRoute: AuthenticatedDashboardMentoringRoute, + AuthenticatedDashboardRoadmapDiscoveryRoute: + AuthenticatedDashboardRoadmapDiscoveryRoute, + AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute, + } + +const AuthenticatedDashboardRouteWithChildren = + AuthenticatedDashboardRoute._addFileChildren( + AuthenticatedDashboardRouteChildren, + ) + +interface AuthenticatedRouteChildren { + AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRouteWithChildren + AuthenticatedDashboardSettingsRoute: typeof AuthenticatedDashboardSettingsRoute +} + const AuthenticatedRouteChildren: AuthenticatedRouteChildren = { - AuthenticatedDashboardRoute: AuthenticatedDashboardRoute, + AuthenticatedDashboardRoute: AuthenticatedDashboardRouteWithChildren, AuthenticatedDashboardSettingsRoute: AuthenticatedDashboardSettingsRoute, - AuthenticatedDashboardIndexRoute: AuthenticatedDashboardIndexRoute, } const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren( diff --git a/apps/dimentorin/src/routes/_authenticated.tsx b/apps/dimentorin/src/routes/_authenticated.tsx index ccce983..b4eaafb 100644 --- a/apps/dimentorin/src/routes/_authenticated.tsx +++ b/apps/dimentorin/src/routes/_authenticated.tsx @@ -3,6 +3,13 @@ import { SessionToken } from '@imphnen-frontend-service/service' export const Route = createFileRoute('/_authenticated')({ beforeLoad: () => { + // Development-only bypass explicitly requested via environment flags + const bypassAuth = import.meta.env.MODE === 'development' && import.meta.env.VITE_BYPASS_AUTH_MIDDLEWARE === 'true' + + if (bypassAuth) { + return + } + const session = SessionToken.get() if (!session?.token?.access_token) { throw redirect({ to: '/auth/login' }) diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard.tsx index 88ef5e9..2a01e94 100644 --- a/apps/dimentorin/src/routes/_authenticated/dashboard.tsx +++ b/apps/dimentorin/src/routes/_authenticated/dashboard.tsx @@ -1,65 +1,182 @@ import { createFileRoute, Outlet, Link, useLocation, useNavigate } from '@tanstack/react-router' import { useAuthStore } from '@imphnen-frontend-service/service' import { Icon } from '@iconify/react' +import { resolvePersona } from './dashboard/_data/persona-resolver' -const navItems = [ - { path: '/dashboard', label: 'Dashboard', icon: 'mdi:view-dashboard' }, - { path: '/dashboard/settings', label: 'Settings', icon: 'mdi:cog' }, -] +interface NavItem { + path?: string; + label: string; + icon: string; + isLink?: boolean; +} + +/** + * Get navigation items based on persona. + * Only items with isLink=true will use Link component; others render as non-navigating buttons. + */ +function getNavItems(persona: 'user' | 'mentor'): NavItem[] { + if (persona === 'mentor') { + return [ + { path: '/dashboard', label: 'Dashboard', icon: 'mdi:view-dashboard-outline', isLink: true }, + { path: '/dashboard/mentoring-setup', label: 'Mentoring Setup', icon: 'mdi:cog-outline', isLink: false }, + { path: '/dashboard/list-mentee', label: 'List Mentee', icon: 'mdi:account-group-outline', isLink: false }, + { path: '/dashboard/feedback', label: 'Feedback', icon: 'mdi:message-reply-text-outline', isLink: false }, + ]; + } + + return [ + { path: '/dashboard', label: 'Dashboard', icon: 'mdi:view-dashboard-outline', isLink: true }, + { path: '/dashboard/roadmap-discovery', label: 'Roadmap Discovery', icon: 'mdi:map-marker-path', isLink: true }, + { path: '/dashboard/learning-path', label: 'Learning Path', icon: 'mdi:book-open-page-variant-outline', isLink: true }, + { path: '/dashboard/mentoring', label: 'Mentoring', icon: 'mdi:video-outline', isLink: true }, + ]; +} export const Route = createFileRoute('/_authenticated/dashboard')({ component: DashboardLayout, }) +/** + * Header component for the dashboard, containing the app brand and user profile. + */ +function HeaderDashboard({ persona, user, onLogout }: { persona: 'user' | 'mentor', user: any, onLogout: () => void }) { + return ( +
+ + Dimentorin.dev + + +
+ + + + +
+
+ ); +} + +/** + * Dashboard layout shell with persona-aware sidebar navigation. + * Renders navigation and outlet for nested routes. + */ function DashboardLayout() { const { session, clearSession } = useAuthStore() const location = useLocation() const navigate = useNavigate() + const searchParams = new URLSearchParams(location.search); + const persona = resolvePersona(session?.user, searchParams); + const navItems = getNavItems(persona); + + const handleLogout = () => { + clearSession(); + navigate({ to: '/auth/login' }); + }; + + const isNavItemActive = (path?: string) => { + if (!path) return false; + + // Dashboard should only be active on the dashboard index route. + if (path === '/dashboard') { + return location.pathname === '/dashboard' || location.pathname === '/dashboard/'; + } + + return location.pathname.startsWith(path); + }; + return ( -
- -
- -
+ + {/* Navigation Items */} + + + {/* Sidebar Footer */} +
+
+ +
+ + + {/* Main Content Area */} +
+ + +
+ +
+
) } diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.spec.ts b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.spec.ts new file mode 100644 index 0000000..502b6dd --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.spec.ts @@ -0,0 +1,284 @@ +import { describe, it, expect } from 'vitest'; +import { + getUserMockDashboardData, + getMentorMockDashboardData, + mockUserDashboardData, + mockMentorDashboardData, +} from './dashboard-mock'; +import { resolvePersona, parseSearchParams } from '../persona-resolver'; +import type { TUserItem } from '@imphnen-frontend-service/service'; + +describe('Dashboard Mock Data', () => { + describe('getUserMockDashboardData', () => { + it('should return user dashboard data with correct structure', () => { + const data = getUserMockDashboardData(); + + expect(data).toHaveProperty('mentoringSessions'); + expect(data).toHaveProperty('articleSubmitted'); + expect(data).toHaveProperty('articlePublished'); + expect(data).toHaveProperty('roadmap'); + expect(data).toHaveProperty('articles'); + }); + + it('should include numeric metrics', () => { + const data = getUserMockDashboardData(); + + expect(typeof data.mentoringSessions).toBe('number'); + expect(typeof data.articleSubmitted).toBe('number'); + expect(typeof data.articlePublished).toBe('number'); + }); + + it('should include roadmap items with required fields', () => { + const data = getUserMockDashboardData(); + + expect(data.roadmap.length).toBeGreaterThan(0); + data.roadmap.forEach((item) => { + expect(item).toHaveProperty('id'); + expect(item).toHaveProperty('name'); + expect(item).toHaveProperty('completionPercentage'); + expect(item).toHaveProperty('durationDays'); + expect(typeof item.completionPercentage).toBe('number'); + }); + }); + + it('should include articles with required fields', () => { + const data = getUserMockDashboardData(); + + expect(data.articles.length).toBeGreaterThan(0); + data.articles.forEach((article) => { + expect(article).toHaveProperty('id'); + expect(article).toHaveProperty('no'); + expect(article).toHaveProperty('judul'); + expect(article).toHaveProperty('materi'); + expect(article).toHaveProperty('status'); + expect(article).toHaveProperty('submitDate'); + }); + }); + + it('should have sample row matching spec', () => { + const data = getUserMockDashboardData(); + const firstArticle = data.articles[0]; + + expect(firstArticle.judul).toContain('How to install linux dist'); + expect(firstArticle.materi).toBe('Day 1'); + }); + }); + + describe('getMentorMockDashboardData', () => { + it('should return mentor dashboard data with correct structure', () => { + const data = getMentorMockDashboardData(); + + expect(data).toHaveProperty('rating'); + expect(data).toHaveProperty('sessionComplete'); + expect(data).toHaveProperty('menteeImpacted'); + expect(data).toHaveProperty('totalFeedback'); + expect(data).toHaveProperty('topics'); + expect(data).toHaveProperty('mentoringSetup'); + expect(data).toHaveProperty('payments'); + }); + + it('should include numeric metrics in valid ranges', () => { + const data = getMentorMockDashboardData(); + + expect(data.rating).toBeGreaterThanOrEqual(0); + expect(data.rating).toBeLessThanOrEqual(5); + expect(data.sessionComplete).toBeGreaterThanOrEqual(0); + expect(data.menteeImpacted).toBeGreaterThanOrEqual(0); + expect(data.totalFeedback).toBeGreaterThanOrEqual(0); + }); + + it('should include topic chips', () => { + const data = getMentorMockDashboardData(); + + expect(data.topics.length).toBeGreaterThan(0); + data.topics.forEach((topic) => { + expect(topic).toHaveProperty('id'); + expect(topic).toHaveProperty('label'); + expect(typeof topic.label).toBe('string'); + }); + }); + + it('should include mentoring setup config', () => { + const data = getMentorMockDashboardData(); + const setup = data.mentoringSetup; + + expect(setup).toHaveProperty('sessionRate'); + expect(setup).toHaveProperty('availability'); + expect(setup).toHaveProperty('expertise'); + expect(setup).toHaveProperty('experienceLevel'); + expect(setup).toHaveProperty('status'); + expect(['Incomplete', 'Complete']).toContain(setup.status); + }); + + it('should include payments with required fields', () => { + const data = getMentorMockDashboardData(); + + expect(data.payments.length).toBeGreaterThan(0); + data.payments.forEach((payment) => { + expect(payment).toHaveProperty('id'); + expect(payment).toHaveProperty('no'); + expect(payment).toHaveProperty('tanggalMentoring'); + expect(payment).toHaveProperty('sesi'); + expect(payment).toHaveProperty('namaMentee'); + expect(payment).toHaveProperty('jumlah'); + }); + }); + + it('should have sample payment row matching spec', () => { + const data = getMentorMockDashboardData(); + const firstPayment = data.payments[0]; + + expect(firstPayment.no).toBe(1); + expect(firstPayment.tanggalMentoring).toBe('28-01-2025'); + expect(firstPayment.sesi).toBe('Senin, 19:00 - 19:45'); + expect(firstPayment.namaMentee).toBe('Firdaus Wijaya'); + expect(firstPayment.jumlah).toBe('Rp.100.000'); + }); + + it('should sort payments by date descending (newest first)', () => { + const data = getMentorMockDashboardData(); + + for (let i = 0; i < data.payments.length - 1; i++) { + const current = new Date(data.payments[i].tanggalMentoring.split('-').reverse().join('-')); + const next = new Date(data.payments[i + 1].tanggalMentoring.split('-').reverse().join('-')); + expect(current.getTime()).toBeGreaterThanOrEqual(next.getTime()); + } + }); + }); + + describe('Mock data completeness', () => { + it('user dashboard should have consistent data', () => { + const data = mockUserDashboardData; + expect(data.articles.length).toBeGreaterThan(0); + expect(data.roadmap.length).toBeGreaterThan(0); + }); + + it('mentor dashboard should have consistent data', () => { + const data = mockMentorDashboardData; + expect(data.payments.length).toBeGreaterThan(0); + expect(data.topics.length).toBeGreaterThan(0); + }); + }); +}); + +describe('Persona Resolver', () => { + describe('resolvePersona', () => { + it('should respect query parameter override to mentor', () => { + const user: TUserItem = { + id: '1', + email: 'user@example.com', + fullname: 'Test User', + is_active: true, + role: { id: 'role-1', name: 'user', permissions: [] }, + }; + const searchParams = new URLSearchParams('persona=mentor'); + + const persona = resolvePersona(user, searchParams); + expect(persona).toBe('mentor'); + }); + + it('should respect query parameter override to user', () => { + const user: TUserItem = { + id: '1', + email: 'mentor@example.com', + fullname: 'Test Mentor', + is_active: true, + role: { id: 'role-2', name: 'mentor', permissions: [] }, + }; + const searchParams = new URLSearchParams('persona=user'); + + const persona = resolvePersona(user, searchParams); + expect(persona).toBe('user'); + }); + + it('should derive mentor from role name', () => { + const user: TUserItem = { + id: '1', + email: 'mentor@example.com', + fullname: 'Test Mentor', + is_active: true, + role: { id: 'role-2', name: 'mentor', permissions: [] }, + }; + + const persona = resolvePersona(user); + expect(persona).toBe('mentor'); + }); + + it('should handle case-insensitive role name', () => { + const user: TUserItem = { + id: '1', + email: 'mentor@example.com', + fullname: 'Test Mentor', + is_active: true, + role: { id: 'role-2', name: 'MENTOR', permissions: [] }, + }; + + const persona = resolvePersona(user); + expect(persona).toBe('mentor'); + }); + + it('should return user for non-mentor role', () => { + const user: TUserItem = { + id: '1', + email: 'user@example.com', + fullname: 'Test User', + is_active: true, + role: { id: 'role-1', name: 'user', permissions: [] }, + }; + + const persona = resolvePersona(user); + expect(persona).toBe('user'); + }); + + it('should default to user when user is undefined', () => { + const persona = resolvePersona(undefined); + expect(persona).toBe('user'); + }); + + it('should ignore invalid query parameters', () => { + const user: TUserItem = { + id: '1', + email: 'user@example.com', + fullname: 'Test User', + is_active: true, + role: { id: 'role-1', name: 'user', permissions: [] }, + }; + const searchParams = new URLSearchParams('persona=invalid'); + + const persona = resolvePersona(user, searchParams); + expect(persona).toBe('user'); + }); + + it('should ignore missing query parameter', () => { + const user: TUserItem = { + id: '1', + email: 'user@example.com', + fullname: 'Test User', + is_active: true, + role: { id: 'role-1', name: 'user', permissions: [] }, + }; + const searchParams = new URLSearchParams('other=value'); + + const persona = resolvePersona(user, searchParams); + expect(persona).toBe('user'); + }); + }); + + describe('parseSearchParams', () => { + it('should parse query string correctly', () => { + const result = parseSearchParams('?persona=mentor'); + expect(result.get('persona')).toBe('mentor'); + }); + + it('should handle multiple parameters', () => { + const result = parseSearchParams('?persona=user&other=value'); + expect(result.get('persona')).toBe('user'); + expect(result.get('other')).toBe('value'); + }); + + it('should handle empty string', () => { + const result = parseSearchParams(''); + expect(result.get('persona')).toBeNull(); + }); + }); +}); diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.ts b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.ts new file mode 100644 index 0000000..bd48a95 --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.ts @@ -0,0 +1,174 @@ +import type { + UserDashboardData, + MentorDashboardData, + MentorPaymentRecord, + MentorTopicChip, +} from './types'; + +/** + * Mock data for user dashboard. + * Used during development before API integration. + */ +export const mockUserDashboardData: UserDashboardData = { + mentoringSessions: 0, + articleSubmitted: 0, + articlePublished: 0, + roadmap: [ + { + id: 'roadmap-1', + name: 'Front End Basic', + completionPercentage: 50, + durationDays: 30, + }, + { + id: 'roadmap-2', + name: 'React Fundamentals', + completionPercentage: 25, + durationDays: 21, + }, + { + id: 'roadmap-3', + name: 'TypeScript Essentials', + completionPercentage: 10, + durationDays: 14, + }, + ], + articles: [ + { + id: 'article-1', + no: 1, + judul: 'How to install linux dist', + materi: 'Day 1', + status: 'Published', + submitDate: '2025-04-10', + }, + { + id: 'article-2', + no: 2, + judul: 'Mastering CSS Grid Layout', + materi: 'Day 2', + status: 'Published', + submitDate: '2025-04-08', + }, + { + id: 'article-3', + no: 3, + judul: 'React Hooks Deep Dive', + materi: 'Day 5', + status: 'Submitted', + submitDate: '2025-04-05', + }, + { + id: 'article-4', + no: 4, + judul: 'Understanding Async/Await', + materi: 'Day 3', + status: 'Draft', + submitDate: '2025-04-03', + }, + { + id: 'article-5', + no: 5, + judul: 'TypeScript Advanced Types', + materi: 'Day 7', + status: 'Rejected', + submitDate: '2025-04-01', + }, + ], +}; + +/** + * Mock mentor topics for chips display. + */ +const mockMentorTopics: MentorTopicChip[] = [ + { id: 'topic-1', label: 'Basic IT' }, + { id: 'topic-2', label: 'Career & Self Development' }, + { id: 'topic-3', label: 'PM & IT Tools' }, + { id: 'topic-4', label: 'Programming' }, + { id: 'topic-5', label: 'Industry Insight' }, + { id: 'topic-6', label: 'AI Tips' }, + { id: 'topic-7', label: 'Data & Database' }, +]; + +/** + * Mock payments history for mentor dashboard. + * Sorted by tanggalMentoring in descending order (newest first). + */ +const mockMentorPayments: MentorPaymentRecord[] = [ + { + id: 'payment-1', + no: 1, + tanggalMentoring: '28-01-2025', + sesi: 'Senin, 19:00 - 19:45', + namaMentee: 'Firdaus Wijaya', + jumlah: 'Rp.100.000', + }, + { + id: 'payment-2', + no: 2, + tanggalMentoring: '27-01-2025', + sesi: 'Minggu, 14:00 - 14:45', + namaMentee: 'Ahmad Rizki', + jumlah: 'Rp.100.000', + }, + { + id: 'payment-3', + no: 3, + tanggalMentoring: '25-01-2025', + sesi: 'Jumat, 19:00 - 19:45', + namaMentee: 'Siti Nurhaliza', + jumlah: 'Rp.150.000', + }, + { + id: 'payment-4', + no: 4, + tanggalMentoring: '24-01-2025', + sesi: 'Kamis, 18:00 - 18:45', + namaMentee: 'Budi Santoso', + jumlah: 'Rp.100.000', + }, + { + id: 'payment-5', + no: 5, + tanggalMentoring: '22-01-2025', + sesi: 'Selasa, 19:00 - 19:45', + namaMentee: 'Eka Suryanto', + jumlah: 'Rp.120.000', + }, +]; + +/** + * Mock data for mentor dashboard. + * Used during development before API integration. + */ +export const mockMentorDashboardData: MentorDashboardData = { + rating: 0, + sessionComplete: 0, + menteeImpacted: 0, + totalFeedback: 0, + topics: mockMentorTopics, + mentoringSetup: { + sessionRate: 'Rp.100.000/sesi', + availability: 'Senin-Jumat, 19:00-21:00', + expertise: ['JavaScript', 'React', 'TypeScript', 'Backend', 'System Design'], + experienceLevel: 'Senior', + status: 'Complete', + }, + payments: mockMentorPayments, +}; + +/** + * Get mock user dashboard data. + * Can be extended to support filtering/pagination. + */ +export function getUserMockDashboardData(): UserDashboardData { + return mockUserDashboardData; +} + +/** + * Get mock mentor dashboard data. + * Can be extended to support filtering/pagination. + */ +export function getMentorMockDashboardData(): MentorDashboardData { + return mockMentorDashboardData; +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/types.ts b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/types.ts new file mode 100644 index 0000000..3305ced --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/types.ts @@ -0,0 +1,57 @@ +/** User Dashboard Mock Data Types */ +export interface UserArticle { + id: string; + no: number; + judul: string; + materi: string; + status: 'Draft' | 'Submitted' | 'Published' | 'Rejected'; + submitDate: string; +} + +export interface UserRoadmapItem { + id: string; + name: string; + completionPercentage: number; + durationDays: number; +} + +export interface UserDashboardData { + mentoringSessions: number; + articleSubmitted: number; + articlePublished: number; + roadmap: UserRoadmapItem[]; + articles: UserArticle[]; +} + +/** Mentor Dashboard Mock Data Types */ +export interface MentorPaymentRecord { + id: string; + no: number; + tanggalMentoring: string; // DD-MM-YYYY + sesi: string; // Hari, HH:MM - HH:MM + namaMentee: string; + jumlah: string; // Rp.###.### +} + +export interface MentorMentoringSetup { + sessionRate: string; // e.g., "Rp.100.000/sesi" + availability: string; // e.g., "Senin-Jumat, 19:00-21:00" + expertise: string[]; + experienceLevel: string; // e.g., "Senior" + status: 'Incomplete' | 'Complete'; +} + +export interface MentorTopicChip { + id: string; + label: string; +} + +export interface MentorDashboardData { + rating: number; // 0-5 + sessionComplete: number; + menteeImpacted: number; + totalFeedback: number; + topics: MentorTopicChip[]; + mentoringSetup: MentorMentoringSetup; + payments: MentorPaymentRecord[]; +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/_data/persona-resolver.ts b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/persona-resolver.ts new file mode 100644 index 0000000..54db87e --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/_data/persona-resolver.ts @@ -0,0 +1,53 @@ +import type { TUserItem } from '@imphnen-frontend-service/service'; + +/** + * Persona type for dashboard rendering. + */ +export type Persona = 'user' | 'mentor'; + +/** + * Resolves the persona for the dashboard based on authentication and query parameters. + * + * Resolution priority: + * 1. Query parameter `?persona=user|mentor` (explicit override) + * 2. User role name contains 'mentor' (case-insensitive) + * 3. Fallback to 'user' persona + * + * @param user - The authenticated user object + * @param searchParams - URL search parameters + * @returns The resolved persona + */ +export function resolvePersona( + user: TUserItem | undefined, + searchParams?: URLSearchParams +): Persona { + // Check for explicit persona query parameter + if (searchParams) { + const param = searchParams.get('persona'); + if (param === 'mentor' || param === 'user') { + return param; + } + } + + // Derive from user role name + if (user?.role?.name) { + const roleName = user.role.name.toLocaleLowerCase(); + if (roleName.includes('mentor')) { + return 'mentor'; + } + } + + // Default to user persona + return 'user'; +} + +/** + * Parses search parameters from a URL search string. + * Utility for testing and usage without URLSearchParams API. + * + * @param search - URL search string (e.g., "?persona=mentor") + * @returns URLSearchParams instance + */ +export function parseSearchParams(search: string): URLSearchParams { + return new URLSearchParams(search); +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/index.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/index.tsx new file mode 100644 index 0000000..c65ac11 --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/index.tsx @@ -0,0 +1,28 @@ +import { createFileRoute, useLocation } from '@tanstack/react-router' +import { useAuthStore } from '@imphnen-frontend-service/service' +import { resolvePersona } from '../dashboard/_data/persona-resolver' +import { UserDashboard } from '../dashboard_/_components/user/user-dashboard' +import { MentorDashboard } from '../dashboard_/_components/mentor/mentor-dashboard' + +export const Route = createFileRoute('/_authenticated/dashboard/')({ + component: DashboardIndexPage, +}) + +/** + * Dashboard Index Page + * Routes to either user or mentor dashboard based on persona resolution. + * Persona resolved via query parameter or user role. + */ +function DashboardIndexPage() { + const { session } = useAuthStore() + const location = useLocation() + + const searchParams = new URLSearchParams(location.search); + const persona = resolvePersona(session?.user, searchParams); + + return ( +
+ {persona === 'mentor' ? : } +
+ ); +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/learning-path.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/learning-path.tsx new file mode 100644 index 0000000..c38df18 --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/learning-path.tsx @@ -0,0 +1,157 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useState } from 'react' + +export const Route = createFileRoute('/_authenticated/dashboard/learning-path')({ + component: LearningPathPage, +}) + +function LearningPathPage() { + const [activeTab, setActiveTab] = useState<'roadmap' | 'article'>('roadmap') + const [isSubmitArticlePopupOpen, setIsSubmitArticlePopupOpen] = useState(false) + + return ( +
+
+ + +
+ + {activeTab === 'roadmap' && ( +
+

Roadmap Kamu

+

Front-end Basic

+ +
+
+
+

Day 1 - Materi A

+ 1 / 3 diselesaikan +
+ +
+
+ 1. Submateri 1 + Done +
+
+ 2. Submateri 2 + To do +
+
+ 3. Tugas : Membuat Artikel + To do +
+
+
+ + {['Day 2 - Materi B', 'Day 3 - Materi C', 'Day 4 - Materi D', 'Day 5 - Materi E'].map((day) => ( +
+

{day}

+ Selesaikan materi sebelumnya +
+ ))} +
+
+ )} + + {activeTab === 'article' && ( +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
No.Judul ArtikelMateriStatusSubmit DateAction
1How to install linux dist..Day 1Done22 Maret 2025, 20:30 WIB + +
2.How to install linux dist..Day 2On Progress- + +
+
+
+ )} + + {isSubmitArticlePopupOpen && ( +
+
+
+

Apakah Kamu Sudah Yakin?

+

+ Pastikan isi artikel sudah sesuai dengan ketentuan^^, artikel yang sudah disubmit tidak dapat diedit +

+
+ +
+ + + +
+
+
+ )} +
+ ) +} diff --git a/apps/dimentorin/src/routes/_authenticated/dashboard/mentoring.tsx b/apps/dimentorin/src/routes/_authenticated/dashboard/mentoring.tsx new file mode 100644 index 0000000..03d046d --- /dev/null +++ b/apps/dimentorin/src/routes/_authenticated/dashboard/mentoring.tsx @@ -0,0 +1,339 @@ +import { createFileRoute } from '@tanstack/react-router' +import { useMemo, useState } from 'react' + +export const Route = createFileRoute('/_authenticated/dashboard/mentoring')({ + component: MentoringPage, +}) + +function MentoringPage() { + const [activeModal, setActiveModal] = useState(null) + const mentoringRows = useMemo( + () => + Array.from({ length: 12 }, (_, idx) => ({ + no: idx + 1, + mentorName: 'Muhammad Firdaus Oi...', + topic: 'Basic IT, Industry Ins...', + sessionTime: '22 Maret 2025, 20:00 - 20:30 WIB', + status: idx % 3 === 0 ? 'Done' : 'To do', + })), + [], + ) + + const rowsPerPage = 10 + const [currentPage, setCurrentPage] = useState(1) + const totalPages = Math.max(1, Math.ceil(mentoringRows.length / rowsPerPage)) + + const pagedRows = useMemo(() => { + const start = (currentPage - 1) * rowsPerPage + return mentoringRows.slice(start, start + rowsPerPage) + }, [currentPage, mentoringRows]) + + const goToPage = (page: number) => { + if (page < 1 || page > totalPages) return + setCurrentPage(page) + } + + return ( +
+
+
+ +
+ +
+ + + + + + + + + + + + + {pagedRows.map((row) => ( + + + + + + + + + ))} + +
No.Nama MentorTopikSesi MentoringStatusAction
{row.no}{row.mentorName}{row.topic}{row.sessionTime} + + {row.status} + + +
+ {row.status === 'Done' ? ( + + ) : ( + <> + + + + )} +
+
+
+ +
+

+ Menampilkan {(currentPage - 1) * rowsPerPage + 1} - {Math.min(currentPage * rowsPerPage, mentoringRows.length)} dari {mentoringRows.length} +

+ +
+ + + {Array.from({ length: totalPages }, (_, idx) => { + const page = idx + 1 + const isActive = page === currentPage + + return ( + + ) + })} + + +
+
+
+ + {activeModal !== null && ( +
+ {activeModal === 'detail' && ( +
+
+

Detail Sesi Mentoring

+ +
+ +
+
+

Your Senpai

+
+ Mentor +

+ Muhammad +
+ Firdaus Oi Oi Oi, S.H., M.H. +

+

UI Designer at Oray orayan Studios

+
+
+ +
+

Topics

+
+ + Industry Insight + + + Basic IT + +
+ +
+
+

Tanggal

+ +
+
+

Waktu

+ +
+
+

Lokasi

+ +
+
+ +
+

Pertanyaan Untuk Senpai

+