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;
}
}
};