This commit is contained in:
Tomas Dvorak
2026-02-24 10:33:08 +01:00
parent b083dac3f0
commit 55d0284b2a
90 changed files with 27855 additions and 1940 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:9090/api/v1';
// Check if we're in demo mode
const isDemoMode = () => {
+49 -4
View File
@@ -1,6 +1,12 @@
import { createContext, useContext, type ParentComponent, onMount } from 'solid-js';
import { createStore } from 'solid-js/store';
import { isDemoMode } from './demo-mode';
// Check if we're in demo mode (same logic as api.ts)
const isDemoMode = () => {
return localStorage.getItem('demoMode') === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
};
// Types
export interface User {
@@ -182,6 +188,23 @@ export const AuthProvider: ParentComponent = (props) => {
const login = async (credentials: LoginRequest) => {
try {
// In demo mode, use mock login
if (isDemoMode()) {
const mockUser: User = {
id: 1,
email: 'demo@trackeep.com',
username: 'demo',
full_name: 'Demo User',
theme: 'dark',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString()
};
const mockToken = 'demo-token-' + Date.now();
setAuth(mockToken, mockUser);
return;
}
const response = await fetch(`${API_BASE_URL}/auth/login`, {
method: 'POST',
headers: {
@@ -191,8 +214,16 @@ export const AuthProvider: ParentComponent = (props) => {
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'Login failed');
let error;
try {
const errorData = await response.json();
error = errorData.error || 'Login failed';
} catch (jsonError) {
// Handle non-JSON error responses
const text = await response.text();
error = text || `Login failed with status ${response.status}`;
}
throw new Error(error);
}
const data: AuthResponse = await response.json();
@@ -369,7 +400,21 @@ export const useAuth = () => {
// Helper function to get auth headers for API requests
export const getAuthHeaders = () => {
const token = localStorage.getItem('token');
// Check if we're in demo mode first
const isDemo = localStorage.getItem('demoMode') === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
let token = null;
if (isDemo) {
// In demo mode, use a mock token
token = 'demo-token-' + Date.now();
} else {
// In normal mode, get token from localStorage
token = localStorage.getItem('token') || localStorage.getItem('trackeep_token');
}
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
+41 -19
View File
@@ -1,7 +1,32 @@
// Brave Search API integration
const BACKEND_API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api/v1';
const BRAVE_API_KEY = import.meta.env.VITE_BRAVE_API_KEY || 'BSAw0HNI1v3rKmXlSTr0C_UfZDjw7fT';
const BRAVE_WEB_API_BASE = 'https://api.search.brave.com/res/v1/web/search';
const BRAVE_NEWS_API_BASE = 'https://api.search.brave.com/res/v1/news/search';
// Use the variable to avoid unused warning
console.log('Brave API key available:', !!BRAVE_API_KEY);
// Helper function to get auth headers
const getAuthHeaders = () => {
// Check if we're in demo mode
const isDemo = import.meta.env.VITE_DEMO_MODE === 'true' ||
document.title.includes('Demo Mode') ||
window.location.search.includes('demo=true');
let token = null;
if (isDemo) {
// In demo mode, use a mock token
token = 'demo-token-' + Date.now();
} else {
// In normal mode, get token from localStorage
token = localStorage.getItem('token') || localStorage.getItem('trackeep_token');
}
return {
'Content-Type': 'application/json',
...(token && { 'Authorization': `Bearer ${token}` }),
};
};
export interface BraveSearchResult {
title: string;
@@ -32,29 +57,26 @@ export interface BraveSearchResponse {
export async function searchBrave(query: string, count: number = 10, type: 'web' | 'news' = 'web'): Promise<BraveSearchResult[]> {
try {
const apiBase = type === 'news' ? BRAVE_NEWS_API_BASE : BRAVE_WEB_API_BASE;
const response = await fetch(`${apiBase}?q=${encodeURIComponent(query)}&count=${count}`, {
method: 'GET',
headers: {
'Accept': 'application/json',
'Accept-Encoding': 'gzip',
'X-Subscription-Token': BRAVE_API_KEY,
},
// Use backend proxy to avoid CORS issues
const endpoint = type === 'news' ? '/search/news' : '/search/web';
const response = await fetch(`${BACKEND_API_URL}${endpoint}`, {
method: 'POST',
headers: getAuthHeaders(),
body: JSON.stringify({
query,
count,
}),
});
if (!response.ok) {
throw new Error(`Brave API error: ${response.status} ${response.statusText}`);
throw new Error(`Search API error: ${response.status} ${response.statusText}`);
}
const data: BraveSearchResponse = await response.json();
const data = await response.json();
// Return results from appropriate search type
if (type === 'news' && data.news?.results) {
return data.news.results;
} else if (data.web?.results) {
return data.web.results;
} else if (data.mixed?.results) {
return data.mixed.results;
// Return results from the backend response
if (data.results && Array.isArray(data.results)) {
return data.results;
}
return [];
+16 -3
View File
@@ -5,14 +5,21 @@ export const hasDatabaseCredentials = (): boolean => {
return !!(import.meta.env.VITE_DB_HOST &&
import.meta.env.VITE_DB_USER &&
import.meta.env.VITE_DB_PASSWORD &&
import.meta.env.VITE_DB_NAME);
import.meta.env.VITE_DB_NAME) ||
!!(import.meta.env.DB_HOST &&
import.meta.env.DB_USER &&
import.meta.env.DB_PASSWORD &&
import.meta.env.DB_NAME);
};
// Check if search API credentials are configured
export const hasSearchCredentials = (): boolean => {
return !!(import.meta.env.VITE_BRAVE_API_KEY ||
import.meta.env.VITE_SERPER_API_KEY ||
import.meta.env.VITE_SEARCH_API_PROVIDER);
import.meta.env.VITE_SEARCH_API_PROVIDER) ||
!!(import.meta.env.BRAVE_API_KEY ||
import.meta.env.SERPER_API_KEY ||
import.meta.env.SEARCH_API_PROVIDER);
};
// Check if AI service credentials are configured
@@ -22,7 +29,13 @@ export const hasAICredentials = (): boolean => {
import.meta.env.VITE_GROK_API_KEY ||
import.meta.env.VITE_DEEPSEEK_API_KEY ||
import.meta.env.VITE_OPENROUTER_API_KEY ||
import.meta.env.VITE_OLLAMA_BASE_URL);
import.meta.env.VITE_OLLAMA_BASE_URL) ||
!!(import.meta.env.LONGCAT_API_KEY ||
import.meta.env.MISTRAL_API_KEY ||
import.meta.env.GROK_API_KEY ||
import.meta.env.DEEPSEEK_API_KEY ||
import.meta.env.OPENROUTER_API_KEY ||
import.meta.env.OLLAMA_BASE_URL);
};
// Check if any credentials are configured
+34
View File
@@ -204,6 +204,40 @@ export class DemoModeApiClient {
} as T;
}
// Updates endpoint
if (endpoint.includes('/updates/check')) {
return {
updateAvailable: true,
currentVersion: '1.0.0',
latestVersion: '1.0.1',
updateInfo: {
version: '1.0.1',
releaseNotes: '• New AI features added\n• Performance improvements\n• Bug fixes and security patches\n• Enhanced user interface',
downloadUrl: 'https://github.com/trackeep/trackeep/releases/latest',
mandatory: false,
size: '~25MB'
}
} as T;
}
if (endpoint.includes('/updates/install')) {
return {
message: 'Update started',
version: '1.0.1'
} as T;
}
if (endpoint.includes('/updates/progress')) {
return {
available: true,
downloading: false,
installing: false,
completed: false,
error: '',
progress: 0
} as T;
}
// Auth endpoints
if (endpoint.includes('/auth/login-totp')) {
return {
+135 -8
View File
@@ -1,6 +1,6 @@
// Demo mode API interceptor to provide mock data instead of making real API calls
import { hasAnyCredentials, isBackendAvailable, isSearchAvailable } from './credentials';
import { hasAnyCredentials, isBackendAvailable, isSearchAvailable, hasSearchCredentials, hasDatabaseCredentials, hasAICredentials } from './credentials';
// Check if demo mode is enabled via environment variable
export const isEnvDemoMode = (): boolean => {
@@ -17,7 +17,18 @@ export const isDemoMode = (): boolean => {
// Check if we should use real APIs even in demo mode
export const shouldUseRealAPIs = (): boolean => {
return hasAnyCredentials();
const hasCredentials = hasAnyCredentials();
const hasBackend = shouldUseRealBackend();
const result = hasCredentials || hasBackend;
console.log('[Demo Mode] shouldUseRealAPIs:', {
hasCredentials,
hasBackend,
result,
searchCreds: hasSearchCredentials(),
dbCreds: hasDatabaseCredentials(),
aiCreds: hasAICredentials()
});
return result;
};
// Check if we should use real backend API
@@ -196,16 +207,60 @@ const generateMockAIProviders = () => [
}
];
// Store original fetch at module level
let originalFetch: typeof fetch | null = null;
// Request cache to prevent duplicate API calls
const requestCache = new Map<string, Promise<Response>>();
const CACHE_TTL = 2000; // 2 seconds
// Generate cache key for requests
const getCacheKey = (url: string, options?: RequestInit): string => {
const method = options?.method || 'GET';
const body = options?.body || '';
return `${method}:${url}:${body}`;
};
// Demo mode fetch interceptor
export const demoFetch = async (url: string, options?: RequestInit): Promise<Response> => {
// Check if we should use real APIs even in demo mode
if (shouldUseRealAPIs()) {
console.log('[Demo Mode] Real credentials detected, using real API for:', url);
return fetch(url, options);
const shouldUseReal = shouldUseRealAPIs();
console.log('[Demo Mode] demoFetch called:', { url, shouldUseReal });
if (shouldUseReal) {
// Only log YouTube API calls once every 50 calls to reduce spam
if (url.includes('youtube') && Math.random() < 0.02) {
console.log('[Demo Mode] Real credentials detected, using real API for:', url);
}
// Check cache for YouTube API calls to prevent duplicates
if (url.includes('youtube')) {
const cacheKey = getCacheKey(url, options);
const cachedRequest = requestCache.get(cacheKey);
if (cachedRequest) {
return cachedRequest;
}
// Create new request and cache it
const requestPromise = (originalFetch || window.fetch)(url, options);
requestCache.set(cacheKey, requestPromise);
// Clear cache after TTL
setTimeout(() => {
requestCache.delete(cacheKey);
}, CACHE_TTL);
return requestPromise;
}
// Use original fetch to avoid recursion
return (originalFetch || window.fetch)(url, options);
}
if (!isDemoMode()) {
return fetch(url, options);
console.log('[Demo Mode] Not in demo mode, using real fetch for:', url);
return (originalFetch || window.fetch)(url, options);
}
// Parse URL to determine which mock data to return
@@ -252,6 +307,27 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
});
}
if (path.includes('/api/v1/bookmarks') && (!options?.method || options.method === 'GET')) {
const { getMockBookmarks } = await import('./mockData');
const mockBookmarks = getMockBookmarks().map((bookmark, index) => ({
id: index + 1,
title: bookmark.title,
url: bookmark.url,
description: bookmark.description,
tags: bookmark.tags,
created_at: bookmark.createdAt,
is_favorite: bookmark.tags.some((tag) => tag.name === 'important' || tag.name === 'favorite'),
favicon: bookmark.favicon,
screenshot: bookmark.screenshot,
screenshot_medium: bookmark.screenshot,
}));
return new Response(JSON.stringify(mockBookmarks), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (path.includes('/api/v1/tasks') && (!options?.method || options.method === 'GET')) {
const { getMockTasks } = await import('./mockData');
const mockTasks = getMockTasks().map((task, index) => ({
@@ -423,6 +499,27 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
});
}
if (path.includes('/api/v1/bookmarks')) {
const body = options.body ? JSON.parse(options.body as string) : {};
const newBookmark = {
id: Date.now(),
title: body.title || 'Untitled bookmark',
url: body.url || '',
description: body.description || '',
tags: body.tags || [],
created_at: new Date().toISOString(),
is_favorite: body.is_favorite || false,
favicon: body.favicon || '',
screenshot: body.screenshot || '',
screenshot_medium: body.screenshot_medium || '',
};
return new Response(JSON.stringify(newBookmark), {
status: 201,
headers: { 'Content-Type': 'application/json' }
});
}
if (path.includes('/api/v1/tasks')) {
const body = options.body ? JSON.parse(options.body as string) : {};
const newTask = {
@@ -442,6 +539,29 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
}
}
if (options?.method === 'PUT' && path.includes('/api/v1/bookmarks')) {
const body = options.body ? JSON.parse(options.body as string) : {};
const pathParts = path.split('/');
const idFromPath = parseInt(pathParts[pathParts.length - 1] || '0', 10);
const updatedBookmark = {
id: idFromPath || body.id || Date.now(),
title: body.title || 'Untitled bookmark',
url: body.url || '',
description: body.description || '',
tags: body.tags || [],
created_at: body.created_at || new Date().toISOString(),
is_favorite: body.is_favorite ?? false,
favicon: body.favicon || '',
screenshot: body.screenshot || '',
screenshot_medium: body.screenshot_medium || '',
};
return new Response(JSON.stringify(updatedBookmark), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (options?.method === 'PUT' && path.includes('/api/v1/tasks')) {
const body = options.body ? JSON.parse(options.body as string) : {};
const pathParts = path.split('/');
@@ -462,6 +582,13 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
});
}
if (options?.method === 'DELETE' && path.includes('/api/v1/bookmarks')) {
return new Response(JSON.stringify({ message: 'Bookmark deleted (demo mode)' }), {
status: 200,
headers: { 'Content-Type': 'application/json' }
});
}
if (options?.method === 'DELETE' && path.includes('/api/v1/tasks')) {
return new Response(JSON.stringify({ message: 'Task deleted (demo mode)' }), {
status: 200,
@@ -590,8 +717,8 @@ export const demoFetch = async (url: string, options?: RequestInit): Promise<Res
// Override global fetch for demo mode
export const initializeDemoMode = () => {
if (isDemoMode()) {
// Store original fetch to restore later if needed
const originalFetch = window.fetch;
// Store original fetch to use for real API calls and restore later if needed
originalFetch = window.fetch;
window.fetch = demoFetch as typeof fetch;
console.log('[Demo Mode] API interceptor initialized');
return originalFetch;
+268 -2
View File
@@ -1107,7 +1107,7 @@ export const mockNotes: MockNote[] = [
{
id: 'note_4',
title: 'Shopping List',
content: 'Grocery shopping for this week:\n\n🥬 Vegetables:\n- Spinach\n- Bell peppers\n- Carrots\n- Broccoli\n\n🍎 Fruits:\n- Apples\n- Bananas\n- Oranges\n- Berries\n\n🥩 Proteins:\n- Chicken breast\n- Ground beef\n- Salmon\n- Eggs\n\n🥛 Dairy:\n- Milk\n- Greek yogurt\n- Cheese\n- Butter\n\n🍞 Pantry:\n- Bread\n- Rice\n- Pasta\n- Olive oil',
content: 'Grocery shopping for this week:\n\nVegetables:\n- Spinach\n- Bell peppers\n- Carrots\n- Broccoli\n\nFruits:\n- Apples\n- Bananas\n- Oranges\n- Berries\n\nProteins:\n- Chicken breast\n- Ground beef\n- Salmon\n- Eggs\n\nDairy:\n- Milk\n- Greek yogurt\n- Cheese\n- Butter\n\nPantry:\n- Bread\n- Rice\n- Pasta\n- Olive oil',
tags: [
{ name: 'personal', color: '#84cc16' },
{ name: 'shopping', color: '#10b981' }
@@ -1826,6 +1826,272 @@ export const mockLearningPaths: MockLearningPath[] = [
],
createdAt: '4 weeks ago',
enrolledAt: '3 weeks ago'
},
{
id: 'lp_9',
title: 'Blockchain Development',
description: 'Learn blockchain fundamentals, smart contracts, and decentralized application development',
category: 'Blockchain',
difficulty: 'advanced',
estimatedTime: '14 weeks',
progress: 20,
modules: [
{
id: 'mod_18',
title: 'Blockchain Fundamentals',
description: 'Understanding distributed ledgers and consensus mechanisms',
completed: true,
resources: [
{ type: 'video', title: 'Blockchain Basics', url: 'https://example.com/blockchain-basics' },
{ type: 'article', title: 'Consensus Algorithms', url: 'https://example.com/consensus' }
]
},
{
id: 'mod_19',
title: 'Smart Contract Development',
description: 'Building smart contracts with Solidity',
completed: false,
resources: [
{ type: 'video', title: 'Solidity Programming', url: 'https://example.com/solidity' },
{ type: 'project', title: 'DeFi Application', url: 'https://example.com/defi-project' }
]
}
],
tags: [
{ name: 'blockchain', color: '#8b5cf6' },
{ name: 'solidity', color: '#6366f1' },
{ name: 'web3', color: '#ec4899' }
],
createdAt: '5 days ago',
enrolledAt: undefined
},
{
id: 'lp_10',
title: 'Data Science with Python',
description: 'Master data analysis, visualization, and machine learning with Python',
category: 'Data Science',
difficulty: 'intermediate',
estimatedTime: '12 weeks',
progress: 40,
modules: [
{
id: 'mod_20',
title: 'Data Analysis Fundamentals',
description: 'Pandas, NumPy, and data manipulation',
completed: true,
resources: [
{ type: 'video', title: 'Data Analysis with Python', url: 'https://example.com/data-analysis' },
{ type: 'lab', title: 'Data Cleaning Exercises', url: 'https://example.com/data-cleaning' }
]
},
{
id: 'mod_21',
title: 'Machine Learning Basics',
description: 'Introduction to ML algorithms and scikit-learn',
completed: false,
resources: [
{ type: 'video', title: 'Machine Learning Intro', url: 'https://example.com/ml-intro' },
{ type: 'project', title: 'Predictive Model Project', url: 'https://example.com/predictive-model' }
]
}
],
tags: [
{ name: 'python', color: '#3776ab' },
{ name: 'data-science', color: '#10b981' },
{ name: 'pandas', color: '#f59e0b' }
],
createdAt: '1 week ago',
enrolledAt: '4 days ago'
},
{
id: 'lp_11',
title: 'Game Development with Unity',
description: 'Create immersive games using Unity engine and C# programming',
category: 'Game Development',
difficulty: 'intermediate',
estimatedTime: '16 weeks',
progress: 10,
modules: [
{
id: 'mod_22',
title: 'Unity Basics',
description: 'Interface, GameObjects, and basic scripting',
completed: true,
resources: [
{ type: 'video', title: 'Unity Interface Tutorial', url: 'https://example.com/unity-interface' },
{ type: 'lab', title: 'First Unity Project', url: 'https://example.com/unity-first' }
]
},
{
id: 'mod_23',
title: 'C# for Game Development',
description: 'Programming concepts and game logic',
completed: false,
resources: [
{ type: 'video', title: 'C# Game Programming', url: 'https://example.com/csharp-games' },
{ type: 'project', title: '2D Platformer Game', url: 'https://example.com/platformer' }
]
}
],
tags: [
{ name: 'gamedev', color: '#ef4444' },
{ name: 'unity', color: '#000000' },
{ name: 'csharp', color: '#8b5cf6' }
],
createdAt: '2 weeks ago',
enrolledAt: undefined
},
{
id: 'lp_12',
title: 'Cloud Architecture with AWS',
description: 'Design and deploy scalable cloud solutions using Amazon Web Services',
category: 'Cloud Computing',
difficulty: 'advanced',
estimatedTime: '10 weeks',
progress: 70,
modules: [
{
id: 'mod_24',
title: 'AWS Core Services',
description: 'EC2, S3, and fundamental AWS services',
completed: true,
resources: [
{ type: 'video', title: 'AWS Core Services Guide', url: 'https://example.com/aws-core' },
{ type: 'lab', title: 'AWS Hands-on Lab', url: 'https://example.com/aws-lab' }
]
},
{
id: 'mod_25',
title: 'Advanced Cloud Architecture',
description: 'Serverless, microservices, and cloud patterns',
completed: false,
resources: [
{ type: 'video', title: 'Advanced AWS Patterns', url: 'https://example.com/aws-advanced' },
{ type: 'case-study', title: 'Enterprise Cloud Migration', url: 'https://example.com/cloud-migration' }
]
}
],
tags: [
{ name: 'aws', color: '#ff9900' },
{ name: 'cloud', color: '#4ecdc4' },
{ name: 'architecture', color: '#3b82f6' }
],
createdAt: '3 weeks ago',
enrolledAt: '1 week ago'
},
{
id: 'lp_13',
title: 'React Native Advanced',
description: 'Master advanced React Native concepts for professional mobile app development',
category: 'Mobile Development',
difficulty: 'advanced',
estimatedTime: '8 weeks',
progress: 55,
modules: [
{
id: 'mod_26',
title: 'Advanced Navigation',
description: 'Complex navigation patterns and state management',
completed: true,
resources: [
{ type: 'video', title: 'Advanced React Native Navigation', url: 'https://example.com/advanced-nav' },
{ type: 'project', title: 'Multi-screen App', url: 'https://example.com/multi-screen' }
]
},
{
id: 'mod_27',
title: 'Performance Optimization',
description: 'Optimizing app performance and memory usage',
completed: false,
resources: [
{ type: 'video', title: 'React Native Performance', url: 'https://example.com/performance' },
{ type: 'article', title: 'Memory Management Tips', url: 'https://example.com/memory-tips' }
]
}
],
tags: [
{ name: 'react-native', color: '#61dafb' },
{ name: 'mobile', color: '#a855f7' },
{ name: 'performance', color: '#f59e0b' }
],
createdAt: '1 week ago',
enrolledAt: '3 days ago'
},
{
id: 'lp_14',
title: 'Vue.js 3 Complete Guide',
description: 'Learn Vue.js 3 from basics to advanced concepts including Composition API',
category: 'Web Development',
difficulty: 'beginner',
estimatedTime: '6 weeks',
progress: 85,
modules: [
{
id: 'mod_28',
title: 'Vue.js Fundamentals',
description: 'Components, directives, and reactivity',
completed: true,
resources: [
{ type: 'video', title: 'Vue.js 3 Basics', url: 'https://example.com/vue-basics' },
{ type: 'project', title: 'Todo App with Vue', url: 'https://example.com/vue-todo' }
]
},
{
id: 'mod_29',
title: 'Composition API',
description: 'Modern Vue.js development patterns',
completed: true,
resources: [
{ type: 'video', title: 'Composition API Guide', url: 'https://example.com/composition-api' },
{ type: 'article', title: 'Vue Best Practices', url: 'https://example.com/vue-best' }
]
}
],
tags: [
{ name: 'vue', color: '#4fc08d' },
{ name: 'javascript', color: '#f7df1e' },
{ name: 'frontend', color: '#61dafb' }
],
createdAt: '2 weeks ago',
enrolledAt: '1 week ago'
},
{
id: 'lp_15',
title: 'Kubernetes and Microservices',
description: 'Build and deploy microservices architecture with Kubernetes orchestration',
category: 'DevOps',
difficulty: 'advanced',
estimatedTime: '12 weeks',
progress: 30,
modules: [
{
id: 'mod_30',
title: 'Microservices Design',
description: 'Designing and implementing microservices',
completed: true,
resources: [
{ type: 'video', title: 'Microservices Architecture', url: 'https://example.com/microservices' },
{ type: 'article', title: 'Service Communication', url: 'https://example.com/service-comm' }
]
},
{
id: 'mod_31',
title: 'Kubernetes Production',
description: 'Production-ready Kubernetes deployments',
completed: false,
resources: [
{ type: 'video', title: 'K8s Production Guide', url: 'https://example.com/k8s-prod' },
{ type: 'lab', title: 'Production Deployment Lab', url: 'https://example.com/prod-lab' }
]
}
],
tags: [
{ name: 'kubernetes', color: '#326ce5' },
{ name: 'microservices', color: '#ff6b6b' },
{ name: 'devops', color: '#4ecdc4' }
],
createdAt: '4 days ago',
enrolledAt: undefined
}
];
@@ -2350,7 +2616,7 @@ export const getMockStats = () => ({
tasks: -5,
notes: 12
},
weeklyActivity: [12, 19, 8, 15, 22, 18, 25],
weeklyActivity: Array.from({length: 7}, () => Math.floor(Math.random() * 30) + 5), // Random values between 5-35
// Additional stats for enhanced dashboard
totalVideos: mockVideos.length,
totalLearningPaths: mockLearningPaths.length,
+29 -19
View File
@@ -11,36 +11,45 @@ export const WeeklyBarChart = (props: WeeklyBarChartProps) => {
return (
<div class="space-y-4">
<div class="relative h-32 md:h-36 px-6 weekly-activity-chart">
<div class="relative h-32 md:h-36 px-2 sm:px-4 lg:px-6 weekly-activity-chart">
{/* Grid lines */}
<div class="absolute inset-x-0 inset-y-2 pointer-events-none flex flex-col justify-between">
<div class="border-t border-border/60"></div>
<div class="border-t border-border/40"></div>
<div class="border-t border-border/30"></div>
<div class="border-t border-border/20"></div>
</div>
<div class="relative flex items-end justify-between h-full gap-3 md:gap-4">
{/* Bars container */}
<div class="relative flex items-end justify-between h-full w-full">
{['M', 'T', 'W', 'T', 'F', 'S', 'S'].map((day, index) => {
const activity = weeklyData()[index];
const maxActivity = Math.max(...weeklyData());
// Use dynamic scale based on actual data
const fixedMax = Math.max(maxActivity, 30); // Ensure minimum scale for better visualization
const containerHeight = 128; // h-32 = 128px (base), md:h-36 = 144px
const availableHeight = containerHeight * 0.75; // Use 75% of container height to leave room for labels
const heightPercent = (activity / fixedMax) * (availableHeight / containerHeight) * 100;
const minHeightPercent = (8 / containerHeight) * 100; // Minimum 8px height
const finalHeightPercent = Math.max(heightPercent, minHeightPercent);
const minActivity = Math.min(...weeklyData());
// Calculate responsive height with proper scaling
let heightPercent;
if (maxActivity === minActivity) {
// All values are the same, use 80% height for consistency
heightPercent = 80;
} else {
// Use the actual range for proportional scaling
const range = maxActivity - minActivity;
const normalizedValue = activity - minActivity;
// Scale to 20-90% range to ensure visibility while maintaining proportions
heightPercent = 20 + (normalizedValue / range) * 70;
}
// Ensure minimum height for very small values but maintain proportion
const finalHeightPercent = Math.max(heightPercent, 8);
return (
<div class="flex flex-col items-center flex-1 gap-2 group min-w-0 max-w-8">
<div class="relative w-full max-w-4 md:max-w-5 flex flex-col items-center">
<span
class="text-xs font-medium text-primary mb-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap absolute -top-5"
>
{activity}
</span>
<div class="flex flex-col items-center flex-1 gap-2 group min-w-0 max-w-4 h-full">
<div class="relative w-full max-w-2 md:max-w-3 flex flex-col items-center justify-end h-full">
<span class="text-xs font-medium text-primary mb-1 opacity-0 group-hover:opacity-100 transition-opacity whitespace-nowrap absolute -top-5 z-10">{activity}</span>
<div
class="w-full max-w-4 md:max-w-5 bg-primary rounded-t transition-all duration-500 hover:opacity-80 cursor-pointer hover:scale-105 weekly-bar"
style={`height: ${finalHeightPercent}%; background-color: hsl(199, 89%, 67%); min-height: 8px;`}
class="w-full max-w-2 md:max-w-3 bg-primary rounded-t transition-all duration-500 hover:opacity-80 cursor-pointer hover:scale-105 weekly-bar"
style={`height: ${finalHeightPercent}%; background-color: rgb(96, 198, 246); min-height: 4px;`}
title={`${day}: ${activity} ${chartType()}`}
></div>
</div>
@@ -51,7 +60,8 @@ export const WeeklyBarChart = (props: WeeklyBarChartProps) => {
</div>
</div>
<div class="flex justify-between text-xs text-muted-foreground pt-2 border-t border-border">
{/* Summary */}
<div class="flex justify-between text-xs text-muted-foreground pt-2 border-t border-border px-2 sm:px-4 lg:px-6">
<span>Total: {weeklyData().reduce((a, b) => a + b, 0)} {chartType()}</span>
<span>Avg: {Math.round(weeklyData().reduce((a, b) => a + b, 0) / 7)} per day</span>
</div>