Download script.js from ericsnfabro/admin-dashboard-builder: direct link, hf CLI and curl.
- Browser
- Download file 24.9 kB
-
https://huggingface.co/spaces/ericsnfabro/admin-dashboard-builder/resolve/main/script.js
- Command line
-
hf download hf://spaces/ericsnfabro/admin-dashboard-builder/script.js
-
curl -L -o script.js https://huggingface.co/spaces/ericsnfabro/admin-dashboard-builder/resolve/main/script.js
24.9 kB
| /* ======================================== | |
| 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 = ` | |
| <div class="modal ${size}"> | |
| <div class="modal-header"> | |
| <div class="font-semibold text-lg">${title}</div> | |
| ${closable ? '<button id="modalClose" class="p-1 hover:bg-gray-100 rounded-lg"><i data-feather="x"></i></button>' : ''} | |
| </div> | |
| <div class="modal-body">${body}</div> | |
| ${footer ? `<div class="modal-footer">${footer}</div>` : ''} | |
| </div> | |
| `; | |
| 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 = ` | |
| <div class="flex items-center gap-4"> | |
| <img src="${admin.avatar}" class="w-16 h-16 rounded-full object-cover ring-2 ring-primary/20" alt="Profile"/> | |
| <div> | |
| <div class="text-lg font-semibold">${admin.name}</div> | |
| <div class="text-sm text-gray-500">${admin.email}</div> | |
| <div class="mt-2 flex gap-2"> | |
| ${mode==='view' ? '<span class="badge badge-blue">Admin</span>' : ''} | |
| </div> | |
| </div> | |
| </div> | |
| ${mode==='edit' ? ` | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="text-sm text-gray-600">Full Name</label> | |
| <input class="w-full mt-1 px-3 py-2 border rounded-lg" value="${admin.name}"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Email</label> | |
| <input class="w-full mt-1 px-3 py-2 border rounded-lg" value="${admin.email}"> | |
| </div> | |
| </div>` : ` | |
| <div class="text-sm text-gray-700"> | |
| You can update your profile details or contact support for help. | |
| </div> | |
| `} | |
| `; | |
| const footer = ` | |
| <button class="btn" onclick="Modal.close()">Close</button> | |
| ${mode==='edit' ? '<button class="btn btn-primary" onclick="saveProfile()">Save Changes</button>' : ''} | |
| `; | |
| 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 = ` | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <div> | |
| <label class="text-sm text-gray-600">Title</label> | |
| <input id="fTitle" class="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="Item title" value="${item?.title||''}"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Year</label> | |
| <input id="fYear" type="number" min="1900" max="2100" class="w-full mt-1 px-3 py-2 border rounded-lg" value="${item?.year||new Date().getFullYear()}"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Category</label> | |
| <select id="fCategory" class="w-full mt-1 px-3 py-2 border rounded-lg"> | |
| ${['Movies','Series','Anime','Documentaries','Shorts'].map(c=>`<option ${item?.category===c?'selected':''}>${c}</option>`).join('')} | |
| </select> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Type</label> | |
| <select id="fType" class="w-full mt-1 px-3 py-2 border rounded-lg"> | |
| ${['Movies','Series','Anime','Documentaries','Shorts'].map(c=>`<option ${item?.type===c?'selected':''}>${c}</option>`).join('')} | |
| </select> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Genres</label> | |
| <select id="fGenres" class="w-full mt-1 px-3 py-2 border rounded-lg" multiple> | |
| ${['Action','Drama','Comedy','Sci-Fi','Fantasy','Romance','Thriller','Horror','Mystery','Adventure','Animation'].map(g=>`<option ${item?.genres?.includes(g)?'selected':''}>${g}</option>`).join('')} | |
| </select> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Studio</label> | |
| <input id="fStudio" class="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="Studio" value="${item?.studio||''}"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Producer</label> | |
| <input id="fProducer" class="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="Producer" value="${item?.producer||''}"> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Themes</label> | |
| <input id="fThemes" class="w-full mt-1 px-3 py-2 border rounded-lg" placeholder="Comma separated e.g., Trending,Top Pick" value="${item?.themes||''}"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Description</label> | |
| <div id="fDesc" class="mt-1 bg-white border rounded-lg min-h-[140px]"></div> | |
| </div> | |
| <div> | |
| <label class="text-sm text-gray-600">Image</label> | |
| <div class="mt-1 flex items-center gap-4"> | |
| <img id="fImgPreview" class="w-40 h-28 object-cover rounded-lg border" src="${item?.image||'https://picsum.photos/seed/placeholder/600/400'}" alt=""> | |
| <input id="fImage" type="file" accept="image/*" class="block w-full text-sm"> | |
| </div> | |
| </div> | |
| `; | |
| const footer = ` | |
| <button class="btn" onclick="Modal.close()">Cancel</button> | |
| <button class="btn btn-primary" onclick="${isEdit?'submitEditItem('+JSON.stringify(item?.id||'')+')':'submitCreateItem()' }">${isEdit?'Update Item':'Create Item'}</button> | |
| `; | |
| 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 = ` | |
| <div class="space-y-2"> | |
| <div><span class="font-semibold">Title:</span> ${item.title}</div> | |
| <div><span class="font-semibold">Category:</span> ${item.category} | <span class="font-semibold">Type:</span> ${item.type}</div> | |
| <div><span class="font-semibold">Year:</span> ${item.year}</div> | |
| <div><span class="font-semibold">Genres:</span> ${item.genres.join(', ')}</div> | |
| <div><span class="font-semibold">Studio:</span> ${item.studio} | <span class="font-semibold">Producer:</span> ${item.producer}</div> | |
| <div><span class="font-semibold">Themes:</span> ${item.themes}</div> | |
| </div> | |
| `; | |
| Modal.open({ | |
| title: 'Confirm Item', | |
| body: confirmBody, | |
| footer: `<button class="btn" onclick="Modal.close()">No, I will update</button><button class="btn btn-primary" onclick="confirmCreateItem()">Yes, Correct</button>` | |
| }); | |
| 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 = ` | |
| <div class="space-y-2"> | |
| <div><span class="font-semibold">Title:</span> ${item.title}</div> | |
| <div><span class="font-semibold">Category:</span> ${item.category} | <span class="font-semibold">Type:</span> ${item.type}</div> | |
| <div><span class="font-semibold">Year:</span> ${item.year}</div> | |
| <div><span class="font-semibold">Genres:</span> ${item.genres.join(', ')}</div> | |
| <div><span class="font-semibold">Studio:</span> ${item.studio} | <span class="font-semibold">Producer:</span> ${item.producer}</div> | |
| <div><span class="font-semibold">Themes:</span> ${item.themes}</div> | |
| </div> | |
| `; | |
| Modal.open({ | |
| title: 'Confirm Changes', | |
| body: confirmBody, | |
| footer: `<button class="btn" onclick="Modal.close()">No, I will update</button><button class="btn btn-primary" onclick="confirmEditItem('${id}')">Yes, Correct</button>` | |
| }); | |
| 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 = ` | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> | |
| <img src="${item.image}" class="w-full h-48 object-cover rounded-lg border" alt=""> | |
| <div> | |
| <div class="text-xl font-semibold">${item.title}</div> | |
| <div class="text-sm text-gray-500 mb-2">${item.category} • ${item.type} • ${item.year}</div> | |
| <div class="flex gap-2 flex-wrap mb-2">${item.genres.map(g=>`<span class="chip">${g}</span>`).join('')}</div> | |
| <div class="text-sm"><span class="font-semibold">Studio:</span> ${item.studio} <span class="font-semibold">Producer:</span> ${item.producer}</div> | |
| <div class="text-sm"><span class="font-semibold">Themes:</span> ${item.themes}</div> | |
| <div class="mt-3 flex gap-3 text-sm text-gray-600"> | |
| <span><i data-feather="eye" class="w-4 h-4 inline"></i> ${item.views}</span> | |
| <span><i data-feather="thumbs-up" class="w-4 h-4 inline"></i> ${item.likes}</span> | |
| <span class="badge badge-blue">${item.status}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-3"> | |
| <div class="font-semibold mb-1">Description</div> | |
| <div class="prose max-w-none">${item.description||'<em>No description</em>'}</div> | |
| </div> | |
| `; | |
| const footer = ` | |
| <button class="btn" onclick="Modal.close()">Close</button> | |
| <button class="btn btn-primary" onclick="Modal.close(); openItemModal(db.items.find(x=>x.id==='${item.id}'), {edit:true})">Edit</button> | |
| `; | |
| Modal.open({ title: 'Preview Item', body, footer, size: 'max-w-3xl' }); | |
| feather.replace(); | |
| } | |
| function deleteItem(item) { | |
| Modal.open({ | |
| title: 'Delete Item', | |
| body: `<div>Are you sure you want to delete <span class="font-semibold">${item.title}</span>? This action cannot be undone.</div>`, | |
| footer: `<button class="btn" onclick="Modal.close()">Cancel</button><button class="btn btn-danger" onclick="confirmDeleteItem('${item.id}')">Delete</button>` | |
| }); | |
| } | |
| 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 = ` | |
| <div class="card p-4 mb-4"> | |
| <div class="flex flex-col md:flex-row md:items-center gap-3 justify-between"> | |
| <div class="flex items-center gap-2 w-full md:w-auto"> | |
| <div class="relative w-full md:w-80"> | |
| <i data-feather="search" class="w-4 h-4 absolute left-3 top-3 text-gray-400"></i> | |
| <input id="itemsSearch" class="pl-10 pr-3 py-2 w-full border rounded-lg" placeholder="Search by Item Name" value="${state.search}"> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap items-center gap-2"> | |
| <select id="fCat" class="px-3 py-2 border rounded-lg"> | |
| <option>All</option> | |
| ${categories.map(c=>`<option ${state.filterCat===c?'selected':''}>${c}</option>`).join('')} | |
| </select> | |
| <select id="fYear" class="px-3 py-2 border rounded-lg"> | |
| <option>All</option> | |
| ${years.map(y=>`<option ${state.filterYear===String(y)?'selected':''}>${y}</option>`).join('')} | |
| </select> | |
| <select id="fType" class="px-3 py-2 border rounded-lg"> | |
| <option>All</option> | |
| ${types.map(t=>`<option ${state.filterType===t?'selected':''}>${t}</option>`).join('')} | |
| </select> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="table-container"> | |
| <table class="table"> | |
| <thead> | |
| <tr> | |
| <th data-sort="title">Title</th> | |
| <th data-sort="category">Category</th> | |
| <th data-sort="year">Year</th> | |
| <th data-sort="type">Type</th> | |
| ${showStatus?'<th>Status</th>':''} | |
| <th data-sort="views">Views</th> | |
| <th data-sort="likes">Likes</th> | |
| <th data-sort="dateCreated">Date Created</th> | |
| ${showActions?'<th>Actions</th>':''} | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${pageItems.map(it=>` | |
| <tr> | |
| <td class="font-medium">${it.title}</td> | |
| <td>${it.category}</td> | |
| <td>${it.year}</td> | |
| <td>${it.type}</td> | |
| ${showStatus?`<td><span class="badge badge-${it.status==='Active'?'green':it.status==='Pending'?'yellow':'gray'}">${it.status}</span></td>`:''} | |
| <td>${it.views}</td> | |
| <td>${it.likes}</td> | |
| <td>${formatDate(it.createdAt)}</td> | |
| ${showActions?` | |
| <td> | |
| <div class="flex gap-2"> | |
| <button class="btn" onclick="previewItem(db.items.find(x=>x.id==='${it.id}'))"><i data-feather="eye" class="w-4 h-4"></i> Preview</button> | |
| <button class="btn" onclick="openItemModal(db.items.find(x=>x.id==='${it.id}'), {edit:true})"><i data-feather="edit-2" class="w-4 h-4"></i> Edit</button> | |
| <button class="btn btn-danger" onclick="deleteItem(db.items.find(x=>x.id==='${it.id}'))"><i data-feather="trash-2" class="w-4 h-4"></i> Delete</button> | |
| </div> | |
| </td>`:''} | |
| </tr> | |
| `).join('')} | |
| ${pageItems.length===0 ? `<tr><td colspan="${8+(showActions?1:0)}" class="text-center text-gray-500 py-6">No items found</td></tr>`:''} | |
| </tbody> | |
| </table> | |
| </div> | |
| <div class="mt-4 flex items-center justify-between"> | |
| <div class="text-sm text-gray-600">Showing ${total===0?0:start+1}–${Math.min(start+pageSize,total)} of ${total}</div> | |
| <div class="flex items-center gap-2"> | |
| <button class="btn" ${state.page===1?'disabled':''} onclick="gotoPage(${state.page-1})"><i data-feather="chevron-left"></i></button> | |
| <span class="px-3 py-1 border rounded-lg">${state.page} / ${Math.max(1,Math.ceil(total/pageSize))}</span> | |
| <button class="btn" ${state.page===Math.ceil(total/pageSize)?'disabled':''} onclick="gotoPage(${state.page+1})"><i data-feather="chevron-right"></i></button> | |
| </div> | |
| </div> | |
| `; | |
| feather.replace(); | |
| // events | |
| $('#itemsSearch', wrap).addEventListener('input', (e)=>{ state.search = e.target.value; state.page = 1; render(); }); | |
| $('#fCat', wrap |