import { MaterialDTO, MaterialType, UserDTO, UserRole, CommentDTO, SystemConfig, PaginationResult } from '../types'; // --- ENTERPRISE DATA LAYER --- // This service acts as the Facade Pattern, shielding the UI from the complexity // of switching between a mock environment (for dev/demo) and the real backend. const STORAGE_KEY_MODE = 'NEXUS_DATA_MODE'; const getMode = (): 'MOCK' | 'REAL' => { // Default to MOCK for the browser demo environment return (localStorage.getItem(STORAGE_KEY_MODE) as 'MOCK' | 'REAL') || 'MOCK'; }; export const setApiMode = (mode: 'MOCK' | 'REAL') => { localStorage.setItem(STORAGE_KEY_MODE, mode); // Reload to apply changes cleanly across the app window.location.reload(); }; // --- MOCK DATABASE STATE --- const CURRENT_USER: UserDTO = { id: 'u_001', username: 'Neo_Architect', avatarUrl: 'https://picsum.photos/seed/neo/200/200', role: UserRole.CREATOR, status: 'ACTIVE', createdAt: new Date(2023, 10, 15).toISOString(), lastLogin: new Date().toISOString() }; const ALL_USERS: UserDTO[] = [ CURRENT_USER, { id: 'u_002', username: 'Visual_Drift', avatarUrl: 'https://picsum.photos/seed/drift/200/200', role: UserRole.CREATOR, status: 'ACTIVE', createdAt: new Date(2023, 11, 1).toISOString(), lastLogin: new Date().toISOString() }, { id: 'u_003', username: 'Poly_Master', avatarUrl: 'https://picsum.photos/seed/poly/200/200', role: UserRole.USER, status: 'FLAGGED', createdAt: new Date(2024, 0, 10).toISOString(), lastLogin: new Date().toISOString() }, { id: 'u_004', username: 'Script_Kiddie', avatarUrl: 'https://picsum.photos/seed/kiddie/200/200', role: UserRole.USER, status: 'BANNED', createdAt: new Date(2024, 1, 20).toISOString(), lastLogin: new Date(2024, 2, 1).toISOString() } ]; const MOCK_MATERIALS: MaterialDTO[] = [ { id: 'm_001', title: 'Cyberpunk HUD Interface React', description: 'A complete React component set for building sci-fi interfaces. Includes glitch effects and holographic transitions.', type: MaterialType.CODE, codeSnippet: `const CyberButton = ({ children }) => ( );`, language: 'tsx', author: CURRENT_USER, stats: { views: 1204, downloads: 450, favorites: 89 }, tags: ['react', 'ui', 'cyberpunk', 'animation'], createdAt: new Date(Date.now() - 86400000 * 2).toISOString(), comments: [] }, { id: 'm_002', title: 'Neon City Ambience Loop', description: '4K loop of a futuristic city rain scene. Perfect for background assets.', type: MaterialType.VIDEO, contentUrl: 'https://media.w3.org/2010/05/sintel/trailer.mp4', // Placeholder video author: ALL_USERS[1], stats: { views: 5320, downloads: 1200, favorites: 432 }, tags: ['video', 'loop', 'background', '4k'], createdAt: new Date(Date.now() - 86400000 * 5).toISOString(), comments: [ { id: 'c_1', content: 'The lighting on this is insane!', author: CURRENT_USER, createdAt: new Date().toISOString() } ] }, { id: 'm_003', title: 'Industrial 3D Assets Pack', description: 'Low-poly mechanical parts for game dev. GLB format + Textures.', type: MaterialType.ASSET_ZIP, contentUrl: '#', author: ALL_USERS[2], stats: { views: 800, downloads: 120, favorites: 45 }, tags: ['3d', 'blender', 'game-assets'], createdAt: new Date(Date.now() - 86400000 * 1).toISOString(), comments: [] } ]; // Generate additional code materials for testing (total 40) if (MOCK_MATERIALS.length < 40) { const base = MOCK_MATERIALS.length; const types = [MaterialType.CODE, MaterialType.ASSET_ZIP, MaterialType.VIDEO]; for (let i = base; i < 40; i++) { const id = `m_${(i + 1).toString().padStart(3, '0')}`; const author = ALL_USERS[i % ALL_USERS.length]; const t = types[i % types.length]; const createdAt = new Date(Date.now() - 86400000 * (i % 10)).toISOString(); const stats = { views: Math.floor(Math.random() * 1000), downloads: Math.floor(Math.random() * 200), favorites: Math.floor(Math.random() * 50) }; const common = { id, title: t === MaterialType.CODE ? `Test Code Snippet #${i + 1}` : t === MaterialType.ASSET_ZIP ? `Assets Pack #${i + 1}` : `Demo Video #${i + 1}`, description: 'Auto-generated demo material.', type: t, author, stats, tags: t === MaterialType.CODE ? ['test', 'code'] : t === MaterialType.ASSET_ZIP ? ['assets'] : ['video'], createdAt, comments: [] as any[] }; if (t === MaterialType.CODE) { MOCK_MATERIALS.push({ ...common, codeSnippet: `function test${i}(){return ${i};}`, language: 'ts' }); } else if (t === MaterialType.ASSET_ZIP) { MOCK_MATERIALS.push({ ...common, contentUrl: '#' }); } else { MOCK_MATERIALS.push({ ...common, contentUrl: 'https://media.w3.org/2010/05/sintel/trailer.mp4' }); } } } let SYSTEM_CONFIG: SystemConfig = { dbHost: '127.0.0.1', dbPort: '3306', dbUser: 'root', dbPass: 'admin123', dbName: 'nexus_db', maintenanceMode: false, maxUploadMB: 3 }; // Helper to call backend API in REAL mode async function apiCall(endpoint: string, options?: RequestInit): Promise { try { const res = await fetch(`/api/v1${endpoint}`, { ...options, headers: { 'Content-Type': 'application/json', ...options?.headers } }); const json = await res.json(); if (!res.ok) { // Return error as part of response structure throw new Error(json.error || res.statusText || 'Request failed'); } return json.data; // Assuming ApiResponse wrapper } catch (e) { console.error("API Call Failed:", e); throw e; // Re-throw to be caught by caller } } // --- SERVICE METHODS --- export const DataService = { getMode, setApiMode, // --- CLIENT SIDE --- getCurrentUser: async (): Promise => { if (getMode() === 'REAL') { try { return await apiCall('/auth/me'); } catch (error) { // User not logged in or token expired, return null console.log('User not authenticated'); return null; } } return new Promise(resolve => setTimeout(() => resolve(CURRENT_USER), 500)); }, updateUserProfile: async (userId: string, data: Partial): Promise => { if (getMode() === 'REAL') return apiCall('/users/me', { method: 'PATCH', body: JSON.stringify(data) }); return new Promise(resolve => { if (CURRENT_USER.id === userId) { if (data.username) CURRENT_USER.username = data.username; if (data.avatarUrl) CURRENT_USER.avatarUrl = data.avatarUrl; } setTimeout(() => resolve({ ...CURRENT_USER }), 800); }); }, getMaterials: async (page = 1, limit = 20, type?: MaterialType | 'ALL', search?: string): Promise> => { const params = new URLSearchParams({ page: String(page), limit: String(limit) }); if (type && type !== 'ALL') params.append('type', String(type)); if (search && search.trim()) params.append('q', search.trim()); if (getMode() === 'REAL') return apiCall>(`/materials?${params.toString()}`); let source = type && type !== 'ALL' ? MOCK_MATERIALS.filter(m => m.type === type) : MOCK_MATERIALS; if (search && search.trim()) { const q = search.trim().toLowerCase(); source = source.filter(m => { const inTitle = m.title.toLowerCase().includes(q); const inDesc = m.description.toLowerCase().includes(q); const inTags = m.tags.some(t => t.toLowerCase().includes(q)); return inTitle || inDesc || inTags; }); } const start = (page - 1) * limit; const end = start + limit; const items = source.slice(start, end); const total = source.length; const hasNext = page * limit < total; return new Promise(resolve => setTimeout(() => resolve({ items, total, page, limit, hasNext }), 400)); }, getUserMaterials: async (page = 1, limit = 20): Promise> => { if (getMode() === 'REAL') return apiCall>(`/users/me/materials?page=${page}&limit=${limit}`); const userItems = MOCK_MATERIALS.filter(m => m.author.id === CURRENT_USER.id); const start = (page - 1) * limit; const end = start + limit; const items = userItems.slice(start, end); const total = userItems.length; const hasNext = page * limit < total; return new Promise(resolve => setTimeout(() => resolve({ items, total, page, limit, hasNext }), 300)); }, getUserFavorites: async (page = 1, limit = 20): Promise> => { if (getMode() === 'REAL') return apiCall>(`/users/me/favorites?page=${page}&limit=${limit}`); const favItems = MOCK_MATERIALS.filter(m => m.stats.favorites > 0); const start = (page - 1) * limit; const end = start + limit; const items = favItems.slice(start, end); const total = favItems.length; const hasNext = page * limit < total; return new Promise(resolve => setTimeout(() => resolve({ items, total, page, limit, hasNext }), 300)); }, getMaterialById: async (id: string): Promise => { if (getMode() === 'REAL') return apiCall(`/materials/${id}`); return new Promise(resolve => { const item = MOCK_MATERIALS.find(m => m.id === id); setTimeout(() => resolve(item), 400); }); }, createMaterial: async (data: Partial): Promise => { if (getMode() === 'REAL') return apiCall('/materials', { method: 'POST', body: JSON.stringify(data) }); return new Promise(resolve => { const newMaterial: MaterialDTO = { id: `m_${Date.now()}`, title: data.title || 'Untitled', description: data.description || '', type: data.type || MaterialType.CODE, codeSnippet: data.codeSnippet, contentUrl: data.contentUrl, language: data.language || 'text', author: CURRENT_USER, stats: { views: 0, downloads: 0, favorites: 0 }, tags: data.tags || [], createdAt: new Date().toISOString(), comments: [] }; MOCK_MATERIALS.unshift(newMaterial); setTimeout(() => resolve(newMaterial), 1000); }); }, deleteMaterial: async (id: string): Promise => { if (getMode() === 'REAL') { await apiCall(`/materials/${id}`, { method: 'DELETE' }); return true; } const idx = MOCK_MATERIALS.findIndex(m => m.id === id); if (idx > -1) { MOCK_MATERIALS.splice(idx, 1); return true; } return false; }, addComment: async (materialId: string, content: string): Promise => { if (getMode() === 'REAL') return apiCall(`/materials/${materialId}/comments`, { method: 'POST', body: JSON.stringify({ content }) }); const material = MOCK_MATERIALS.find(m => m.id === materialId); if (!material) throw new Error('Material not found'); const newComment: CommentDTO = { id: `c_${Date.now()}`, content, author: CURRENT_USER, createdAt: new Date().toISOString() }; material.comments.push(newComment); return newComment; }, toggleFavorite: async (materialId: string): Promise => { if (getMode() === 'REAL') { const res = await apiCall<{ favorites: number }>(`/materials/${materialId}/favorite`, { method: 'POST' }); return res.favorites; } const material = MOCK_MATERIALS.find(m => m.id === materialId); if (material) { // Toggle logic simulation material.stats.favorites += 1; return material.stats.favorites; } return 0; }, uploadZip: async (file: File, meta?: { title?: string; description?: string; tags?: string[] }) => { const fd = new FormData(); fd.append('file', file); if (meta?.title) fd.append('title', meta.title); if (meta?.description) fd.append('description', meta.description); if (meta?.tags) fd.append('tags', meta.tags.join(',')); const res = await fetch('/api/v1/materials/upload-zip', { method: 'POST', body: fd }); const ct = res.headers.get('content-type') || ''; if (!ct.includes('application/json')) { const text = await res.text(); throw new Error(text || `Upload failed (${res.status})`); } const json = await res.json(); if (!res.ok || !json.success) throw new Error(json.error || `Upload failed (${res.status})`); return json.data as MaterialDTO; }, uploadVideo: async (file: File, meta?: { title?: string; description?: string; tags?: string[] }) => { const fd = new FormData(); fd.append('file', file); if (meta?.title) fd.append('title', meta.title); if (meta?.description) fd.append('description', meta.description); if (meta?.tags) fd.append('tags', meta.tags.join(',')); const res = await fetch('/api/v1/materials/upload-video', { method: 'POST', body: fd }); const ct = res.headers.get('content-type') || ''; if (!ct.includes('application/json')) { const text = await res.text(); throw new Error(text || `Upload failed (${res.status})`); } const json = await res.json(); if (!res.ok || !json.success) throw new Error(json.error || `Upload failed (${res.status})`); return json.data as MaterialDTO; }, // --- ADMIN SIDE --- verifyAdmin: async (u: string, p: string): Promise => { // Admin login is usually separate or special return new Promise(resolve => { setTimeout(() => { resolve(u === 'admin' && p === 'wx1998WX'); }, 1000); }); }, getAllUsers: async (): Promise => { if (getMode() === 'REAL') return apiCall('/admin/users'); return new Promise(resolve => setTimeout(() => resolve(ALL_USERS), 600)); }, updateUserRole: async (userId: string, role: UserRole): Promise => { if (getMode() === 'REAL') return apiCall(`/admin/users/${userId}/role`, { method: 'POST', body: JSON.stringify({ role }) }); const u = ALL_USERS.find(user => user.id === userId); if (!u) throw new Error('User not found'); u.role = role; return new Promise(resolve => setTimeout(() => resolve({ ...u }), 400)); }, getSystemConfig: async (): Promise => { if (getMode() === 'REAL') return apiCall('/admin/config'); return SYSTEM_CONFIG; }, updateSystemConfig: async (config: SystemConfig): Promise => { if (getMode() === 'REAL') { await apiCall('/admin/config', { method: 'PUT', body: JSON.stringify({ maxUploadMB: config.maxUploadMB, uploadDir: 'data', dbHost: config.dbHost, dbPort: config.dbPort, dbUser: config.dbUser, dbPass: config.dbPass, dbName: config.dbName, }) }); return; } SYSTEM_CONFIG = config; return new Promise(resolve => setTimeout(resolve, 1500)); }, toggleUserStatus: async (userId: string): Promise => { if (getMode() === 'REAL') return apiCall(`/admin/users/${userId}/toggle-status`, { method: 'POST' }); const u = ALL_USERS.find(user => user.id === userId); if (u) { u.status = u.status === 'ACTIVE' ? 'BANNED' : 'ACTIVE'; return u; } } };