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