/* ======================================== Admin Dashboard SPA - No Build Tools ======================================== */ /* Utilities */ const $ = (sel, ctx=document) => ctx.querySelector(sel); const $$ = (sel, ctx=document) => Array.from(ctx.querySelectorAll(sel)); const DB_KEY = 'admin_dashboard_db_v1'; function uid(prefix='id') { return `${prefix}_${Math.random().toString(36).slice(2,10)}_${Date.now().toString(36)}`; } function formatDate(ts) { const d = new Date(ts); return d.toLocaleDateString(); } function formatDateTime(ts) { const d = new Date(ts); return d.toLocaleString(); } function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); } /* Persist DB */ function loadDB() { const raw = localStorage.getItem(DB_KEY); if (raw) { try { return JSON.parse(raw); } catch {} } const seeded = seedDB(); saveDB(seeded); return seeded; } function saveDB(db) { localStorage.setItem(DB_KEY, JSON.stringify(db)); } /* Seed Demo Data */ function seedDB() { const now = Date.now(); const categories = ['Movies', 'Series', 'Anime', 'Documentaries', 'Shorts']; const genres = ['Action','Drama','Comedy','Sci-Fi','Fantasy','Romance','Thriller','Horror','Mystery','Adventure','Animation']; const studios = ['Studio A','Studio B','Studio C','Studio D','Studio E']; const producers = ['Producer X','Producer Y','Producer Z','Producer W']; const types = ['Movies','Series','Anime','Documentaries','Shorts']; const users = Array.from({length: 18}, (_,i) => ({ id: uid('usr'), name: `User ${i+1}`, email: `user${i+1}@example.com`, role: i % 9 === 0 ? 'Admin' : 'User', status: i % 11 === 0 ? 'Blocked' : 'Active', joinedAt: now - (i * 86400000 * 2), avatar: `https://i.pravatar.cc/100?img=${(i%70)+1}`, })); const items = Array.from({length: 72}, (_,i) => { const cat = categories[i % categories.length]; const type = types[i % types.length]; const g1 = genres[i % genres.length]; const g2 = genres[(i+3) % genres.length]; const studio = studios[i % studios.length]; const producer = producers[i % producers.length]; const year = 2016 + (i % 9); const views = 100 + (i * 37) % 100000; const likes = 10 + (i * 7) % 1500; const status = ['Active','Pending','Archived'][i % 3]; return { id: uid('itm'), title: `${cat} Item ${i+1}`, description: `This is a ${cat.toLowerCase()} item description. Lorem ipsum dolor sit amet, consectetur adipiscing elit.`, category: cat, type, year, genres: [g1, g2], studio, producer, themes: ['Trending','Featured','Top Pick'][i%3], views, likes, image: `https://picsum.photos/seed/item${i+1}/600/400`, status, createdAt: now - (i * 86400000), }; }); const polls = Array.from({length: 12}, (_,i) => ({ id: uid('pol'), title: `Poll Question ${i+1}`, category: categories[i % categories.length], year: 2020 + (i % 5), type: types[i % types.length], views: 200 + (i*19)%2000, totalVotes: 50 + (i*13)%800, status: ['Winner','Ongoing','Canceled'][i % 3], createdAt: now - (i * 86400000 * 4), })); const attributes = { categories: categories.map((c,i)=>({id: uid('cat'), name: c, count: items.filter(it=>it.category===c).length})), genres: genres.map((g,i)=>({id: uid('gen'), name: g, count: items.flatMap(it=>it.genres).filter(x=>x===g).length})), studios: studios.map((s,i)=>({id: uid('stu'), name: s, count: items.filter(it=>it.studio===s).length})), producers: producers.map((p,i)=>({id: uid('pro'), name: p, count: items.filter(it=>it.producer===p).length})), }; return { users, items, polls, attributes }; } /* Router */ const Router = { routes: {}, init() { window.addEventListener('hashchange', Router.resolve); Router.resolve(); }, add(path, handler) { Router.routes[path] = handler; }, resolve() { const hash = location.hash || '#/admin/dashboard'; const path = hash.replace('#',''); if (Router.routes[path]) Router.routes[path](); else if (path.startsWith('/admin')) Router.routes['/admin/dashboard'](); } }; /* Charts (destroy-safe) */ const Charts = { registry: new Map(), make(ctx, cfg) { if (Charts.registry.has(ctx)) { Charts.registry.get(ctx).destroy(); Charts.registry.delete(ctx); } const chart = new Chart(ctx, cfg); Charts.registry.set(ctx, chart); return chart; }, destroyAll() { for (const [ctx, c] of Charts.registry.entries()) { try { c.destroy(); } catch {} } Charts.registry.clear(); } }; /* Global Modal System */ const Modal = { root: null, init() { Modal.root = $('#modalRoot'); }, open({title='Modal', body='', footer='', size='max-w-2xl', closable=true}) { Modal.close(); const backdrop = document.createElement('div'); backdrop.className = 'modal-backdrop'; backdrop.innerHTML = ` `; Modal.root.appendChild(backdrop); feather.replace(); if (closable) { $('#modalClose', backdrop).onclick = Modal.close; backdrop.addEventListener('click', (e) => { if (e.target === backdrop) Modal.close(); }); } return backdrop; }, close() { const active = $('.modal-backdrop'); if (active) active.remove(); } }; /* Toast */ function toast(msg, type='info') { const t = document.createElement('div'); t.className = `fixed bottom-4 right-4 z-[70] px-4 py-3 rounded-lg shadow-lg text-white ${type==='success'?'bg-green-600':type==='error'?'bg-red-600':'bg-gray-800'}`; t.textContent = msg; document.body.appendChild(t); setTimeout(()=> t.remove(), 2400); } /* DB state */ let db = loadDB(); /* Header interactions */ (function initHeader() { const profileBtn = $('#profileBtn'); const dropdown = $('#profileDropdown'); profileBtn?.addEventListener('click', () => dropdown.classList.toggle('hidden')); document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && !profileBtn.contains(e.target)) dropdown.classList.add('hidden'); }); dropdown?.addEventListener('click', (e) => { const btn = e.target.closest('button[data-action]'); if (!btn) return; const action = btn.dataset.action; dropdown.classList.add('hidden'); if (action === 'logout') { location.hash = '#/login'; toast('Logged out. Redirecting...', 'info'); } else if (action === 'viewProfile') { showProfileModal('view'); } else if (action === 'editProfile') { showProfileModal('edit'); } else if (action === 'support') { window.open('https://example.com/support', '_blank'); } }); $('#openCreateItem')?.addEventListener('click', () => openItemModal()); })(); /* Profile Modal */ function showProfileModal(mode='view') { const admin = { name: 'Administrator', email: 'admin@example.com', avatar: 'https://i.pravatar.cc/200?img=5' }; const body = `
Profile
${admin.name}
${admin.email}
${mode==='view' ? 'Admin' : ''}
${mode==='edit' ? `
` : `
You can update your profile details or contact support for help.
`} `; const footer = ` ${mode==='edit' ? '' : ''} `; Modal.open({ title: 'Profile', body, footer }); } function saveProfile() { Modal.close(); toast('Profile updated', 'success'); } /* Item CRUD */ function openItemModal(item=null, options={}) { const isEdit = !!item; const title = isEdit ? 'Edit Item' : 'Create Item'; const body = `
`; const footer = ` `; const m = Modal.open({ title, body, footer, size: 'max-w-3xl' }); setTimeout(()=> { if ($('#fDesc')) { const q = new Quill('#fDesc', { theme: 'snow', placeholder: 'Write a description...' }); if (item?.description) q.setContents([]); q.root.innerHTML = item?.description || ''; m._quill = q; } }, 0); $('#fImage')?.addEventListener('change', (e)=> { const f = e.target.files[0]; if (f) { const url = URL.createObjectURL(f); $('#fImgPreview').src = url; } }); } function submitCreateItem() { const item = collectItemForm(); const confirmBody = `
Title: ${item.title}
Category: ${item.category} | Type: ${item.type}
Year: ${item.year}
Genres: ${item.genres.join(', ')}
Studio: ${item.studio} | Producer: ${item.producer}
Themes: ${item.themes}
`; Modal.open({ title: 'Confirm Item', body: confirmBody, footer: `` }); Modal._pendingItem = item; } function confirmCreateItem() { const item = Modal._pendingItem; Modal._pendingItem = null; Modal.close(); // close confirmation db.items.unshift({ id: uid('itm'), title: item.title, description: item.description, category: item.category, type: item.type, year: +item.year, genres: item.genres, studio: item.studio, producer: item.producer, themes: item.themes, views: 0, likes: 0, image: item.image, status: 'Active', createdAt: Date.now(), }); saveDB(db); toast('Item created', 'success'); Router.resolve(); } function collectItemForm() { const title = $('#fTitle').value.trim(); const year = $('#fYear').value; const category = $('#fCategory').value; const type = $('#fType').value; const genres = Array.from($('#fGenres').selectedOptions).map(o=>o.value); const studio = $('#fStudio').value.trim(); const producer = $('#fProducer').value.trim(); const themes = $('#fThemes').value.trim(); const desc = (Modal.open.lastModal?._quill?.getContents ? Modal.open.lastModal?._quill.root.innerHTML : ($('#fDesc')?.innerHTML || '')); let image = db.items.find(x=>x.id===Modal.open.currentItemId)?.image || 'https://picsum.photos/seed/newitem/600/400'; const fImg = $('#fImage').files[0]; if (fImg) image = URL.createObjectURL(fImg); return { title, year, category, type, genres, studio, producer, themes, description: desc, image }; } function submitEditItem(id) { const item = collectItemForm(); const confirmBody = `
Title: ${item.title}
Category: ${item.category} | Type: ${item.type}
Year: ${item.year}
Genres: ${item.genres.join(', ')}
Studio: ${item.studio} | Producer: ${item.producer}
Themes: ${item.themes}
`; Modal.open({ title: 'Confirm Changes', body: confirmBody, footer: `` }); Modal._pendingItem = item; } function confirmEditItem(id) { const item = Modal._pendingItem; Modal._pendingItem = null; Modal.close(); const idx = db.items.findIndex(x=>x.id===id); if (idx >= 0) { const prev = db.items[idx]; db.items[idx] = { ...prev, title: item.title, description: item.description, category: item.category, type: item.type, year: +item.year, genres: item.genres, studio: item.studio, producer: item.producer, themes: item.themes, image: item.image }; saveDB(db); toast('Item updated', 'success'); Router.resolve(); } } function previewItem(item) { const body = `
${item.title}
${item.category} • ${item.type} • ${item.year}
${item.genres.map(g=>`${g}`).join('')}
Studio: ${item.studio}   Producer: ${item.producer}
Themes: ${item.themes}
${item.views} ${item.likes} ${item.status}
Description
${item.description||'No description'}
`; const footer = ` `; Modal.open({ title: 'Preview Item', body, footer, size: 'max-w-3xl' }); feather.replace(); } function deleteItem(item) { Modal.open({ title: 'Delete Item', body: `
Are you sure you want to delete ${item.title}? This action cannot be undone.
`, footer: `` }); } function confirmDeleteItem(id) { const idx = db.items.findIndex(x=>x.id===id); if (idx >= 0) { db.items.splice(idx, 1); saveDB(db); Modal.close(); toast('Item deleted', 'success'); Router.resolve(); } } /* Items Table Component */ function renderItemsTable({container, items, pageSize=10, showActions=true, showStatus=true, onPage=()=>{}}) { const state = { page: 1, search: '', filterCat: 'All', filterYear: 'All', filterType: 'All', sortBy: 'dateCreated', sortDir: 'desc' }; function getFilteredSorted() { let arr = [...items]; if (state.search) { const q = state.search.toLowerCase(); arr = arr.filter(x => x.title.toLowerCase().includes(q)); } if (state.filterCat !== 'All') arr = arr.filter(x => x.category===state.filterCat); if (state.filterYear !== 'All') arr = arr.filter(x => String(x.year)===state.filterYear); if (state.filterType !== 'All') arr = arr.filter(x => x.type===state.filterType); arr.sort((a,b)=> { const dir = state.sortDir === 'asc' ? 1 : -1; if (state.sortBy==='title') return a.title.localeCompare(b.title) * dir; if (state.sortBy==='category') return a.category.localeCompare(b.category) * dir; if (state.sortBy==='year') return (a.year - b.year) * dir; if (state.sortBy==='type') return a.type.localeCompare(b.type) * dir; if (state.sortBy==='views') return (a.views - b.views) * dir; if (state.sortBy==='likes') return (a.likes - b.likes) * dir; if (state.sortBy==='dateCreated') return (a.createdAt - b.createdAt) * dir; return 0; }); return arr; } function render() { const wrap = typeof container === 'string' ? $(container) : container; const filtered = getFilteredSorted(); const total = filtered.length; const pages = Math.max(1, Math.ceil(total / pageSize)); state.page = clamp(state.page, 1, pages); const start = (state.page-1)*pageSize; const pageItems = filtered.slice(start, start+pageSize); const years = Array.from(new Set(items.map(x=>x.year))).sort((a,b)=>b-a); const categories = Array.from(new Set(items.map(x=>x.category))).sort(); const types = Array.from(new Set(items.map(x=>x.type))).sort(); wrap.innerHTML = `
${showStatus?'':''} ${showActions?'':''} ${pageItems.map(it=>` ${showStatus?``:''} ${showActions?` `:''} `).join('')} ${pageItems.length===0 ? ``:''}
Title Category Year TypeStatusViews Likes Date CreatedActions
${it.title} ${it.category} ${it.year} ${it.type}${it.status}${it.views} ${it.likes} ${formatDate(it.createdAt)}
No items found
Showing ${total===0?0:start+1}–${Math.min(start+pageSize,total)} of ${total}
${state.page} / ${Math.max(1,Math.ceil(total/pageSize))}
`; feather.replace(); // events $('#itemsSearch', wrap).addEventListener('input', (e)=>{ state.search = e.target.value; state.page = 1; render(); }); $('#fCat', wrap