mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-06-04 12:32:58 +00:00
uppdate
This commit is contained in:
@@ -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 = () => {
|
||||
|
||||
@@ -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}` }),
|
||||
|
||||
@@ -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 [];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user