From 426b71f34166e7445af9ed59b86eb988bf5e1bc7 Mon Sep 17 00:00:00 2001 From: Asep Haryana Saputra <90584806+MythEclipse@users.noreply.github.com> Date: Mon, 13 Apr 2026 14:28:10 +0700 Subject: [PATCH] fix: update action versions in Nix build workflow (#78) --- .gitignore | 9 +- apps/backoffice/.env.example | 1 + apps/dimentorin/.env.example | 4 +- apps/dimentorin/e2e/dashboard.mock.spec.ts | 141 ++++++++ apps/dimentorin/e2e/screenshots.test.ts | 83 +++++ apps/dimentorin/playwright.config.ts | 43 +++ apps/dimentorin/public/image/mascot-1.png | Bin 0 -> 40653 bytes apps/dimentorin/src/index.css | 20 ++ apps/dimentorin/src/routeTree.gen.ts | 118 +++++- apps/dimentorin/src/routes/_authenticated.tsx | 7 + .../src/routes/_authenticated/dashboard.tsx | 207 ++++++++--- .../_data/mock/dashboard-mock.spec.ts | 284 +++++++++++++++ .../dashboard/_data/mock/dashboard-mock.ts | 174 +++++++++ .../dashboard/_data/mock/types.ts | 57 +++ .../dashboard/_data/persona-resolver.ts | 53 +++ .../routes/_authenticated/dashboard/index.tsx | 28 ++ .../dashboard/learning-path.tsx | 157 ++++++++ .../_authenticated/dashboard/mentoring.tsx | 339 ++++++++++++++++++ .../dashboard/roadmap-discovery.tsx | 60 ++++ .../_components/mentor/mentor-dashboard.tsx | 154 ++++++++ .../_components/shared/overview-cards.tsx | 59 +++ .../_components/user/user-dashboard.tsx | 117 ++++++ .../_authenticated/dashboard_/index.tsx | 177 --------- .../src/routes/_hooks/use-register.ts | 46 ++- .../src/routes/_public/auth/forgot.tsx | 165 ++++++++- .../src/routes/_public/auth/login.tsx | 123 +++---- .../src/routes/_public/auth/register.tsx | 181 ++++++---- .../dashboard/dashboard-route-render.spec.tsx | 142 ++++++++ apps/gacha/.env.example | 1 + apps/landing/.env.example | 1 + libs/ui/src/atoms/button/button.stories.tsx | 11 +- libs/ui/src/atoms/input/input.stories.tsx | 9 + .../src/molecules/forgot-step/forgot-step.tsx | 30 +- .../src/organisms/auth-banner/auth-banner.tsx | 52 ++- nx.json | 5 +- 35 files changed, 2613 insertions(+), 445 deletions(-) create mode 100644 apps/dimentorin/e2e/dashboard.mock.spec.ts create mode 100644 apps/dimentorin/e2e/screenshots.test.ts create mode 100644 apps/dimentorin/playwright.config.ts create mode 100644 apps/dimentorin/public/image/mascot-1.png create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.spec.ts create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/dashboard-mock.ts create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/_data/mock/types.ts create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/_data/persona-resolver.ts create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/index.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/learning-path.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/mentoring.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard/roadmap-discovery.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard_/_components/mentor/mentor-dashboard.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard_/_components/shared/overview-cards.tsx create mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard_/_components/user/user-dashboard.tsx delete mode 100644 apps/dimentorin/src/routes/_authenticated/dashboard_/index.tsx create mode 100644 apps/dimentorin/src/testing/dashboard/dashboard-route-render.spec.tsx 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 0000000000000000000000000000000000000000..7488c27fd2211ee0afc4d1c7a6b9d2b28f35f241 GIT binary patch literal 40653 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 JPDHLkV1k;va-IMH literal 0 HcmV?d00001 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

+