| import React, { useEffect, useState } from 'react'; |
| import { api } from '../services/api'; |
|
|
| type SlideItem = { |
| title: string; |
| file: string; |
| date?: string; |
| size?: string; |
| order?: number; |
| }; |
|
|
| const Slides: React.FC = () => { |
| const [allowed, setAllowed] = useState<boolean>(false); |
| const [items, setItems] = useState<SlideItem[]>([]); |
| const [error, setError] = useState<string>(''); |
| const [loading, setLoading] = useState<boolean>(true); |
| const [isAdmin, setIsAdmin] = useState<boolean>(false); |
| const [editing, setEditing] = useState<SlideItem & { id?: string } | null>(null); |
| const [deleting, setDeleting] = useState<string | null>(null); |
|
|
| useEffect(() => { |
| |
| let role = 'visitor'; |
| try { const u = localStorage.getItem('user'); role = u ? (JSON.parse(u).role || 'visitor') : 'visitor'; } catch {} |
| const ok = role === 'student' || role === 'admin'; |
| setAllowed(ok); |
| if (!ok) { |
| setLoading(false); |
| return; |
| } |
| let aborted = false; |
| (async () => { |
| const u = localStorage.getItem('user'); |
| try { |
| const parsed = u ? JSON.parse(u) : null; |
| const viewMode = (localStorage.getItem('viewMode') || 'auto'); |
| const admin = parsed?.role === 'admin'; |
| setIsAdmin(viewMode === 'student' ? false : admin); |
| } catch {} |
| try { |
| setLoading(true); setError(''); |
| const resp = await api.get('/api/slides', { headers: { 'Cache-Control': 'no-cache' } }); |
| if (aborted) return; |
| const list = Array.isArray((resp.data as any)?.slides) ? (resp.data as any).slides : []; |
| setItems(list as any); |
| } catch (e: any) { |
| if (!aborted) setError('Unable to load slides.'); |
| } finally { |
| if (!aborted) setLoading(false); |
| } |
| })(); |
| return () => { aborted = true; }; |
| }, []); |
|
|
| const handleDeleteSlide = async (slideId: string) => { |
| if (!window.confirm('Are you sure you want to delete this slide? This action cannot be undone.')) { |
| return; |
| } |
| |
| setDeleting(slideId); |
| try { |
| await api.delete(`/api/slides/${encodeURIComponent(slideId)}`); |
| setItems(items.filter(item => (item as any)._id !== slideId)); |
| } catch (e: any) { |
| const status = e?.response?.status; |
| const msg = e?.response?.data?.error || e?.message || 'Unknown error'; |
| console.error('Delete error:', status, msg); |
| window.alert(`Failed to delete slide: ${status ? status + ' ' : ''}${msg}`); |
| } finally { |
| setDeleting(null); |
| } |
| }; |
|
|
| if (!allowed) return null; |
| return ( |
| <div className="min-h-screen bg-white py-8"> |
| <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8"> |
| <div className="mb-8"> |
| <div className="flex items-center mb-3"> |
| <img src="/icons/slides.svg" alt="Slides" className="h-8 w-8 mr-3" /> |
| <h1 className="text-3xl font-bold text-ui-text">Slides</h1> |
| </div> |
| <p className="text-ui-text/70">Download tutorial slides.</p> |
| </div> |
| |
| {loading && <div className="text-sm text-gray-600">Loading…</div>} |
| {error && !loading && <div className="text-sm text-red-600">{error}</div>} |
| |
| {!loading && !error && ( |
| <div className="bg-ui-panel rounded-xl border border-ui-border p-6 shadow-sm"> |
| {items.length === 0 ? ( |
| <div className="text-sm text-ui-text/60">No slides available yet.</div> |
| ) : ( |
| <ul className="divide-y divide-ui-border/60"> |
| {items.map((s: any, idx) => ( |
| <li key={s._id || idx} className="py-3 grid grid-cols-12 items-center gap-3"> |
| <div className="col-span-8 min-w-0"> |
| <div className="text-ui-text font-medium truncate">{s.title}</div> |
| {s.date && (<div className="text-xs text-ui-text/60">{s.date}</div>)} |
| </div> |
| <div className="col-span-4 flex items-center justify-end gap-3"> |
| <a href={`/slides/${encodeURIComponent(s.file)}`} className="btn-secondary px-3 py-1 text-xs" rel="noopener noreferrer" download> |
| Download |
| </a> |
| {isAdmin && ( |
| <div className="flex items-center gap-2"> |
| <button className="text-xs text-ui-text/70 hover:text-ui-text" onClick={() => setEditing({ id: s._id, title: s.title, date: s.date, file: s.file, order: s.order })}>Edit</button> |
| <button className="text-xs text-red-600 hover:text-red-700" onClick={() => handleDeleteSlide(s._id)} disabled={deleting === s._id}> |
| {deleting === s._id ? 'Deleting...' : 'Delete'} |
| </button> |
| </div> |
| )} |
| </div> |
| </li> |
| ))} |
| </ul> |
| )} |
| </div> |
| )} |
| |
| {isAdmin && ( |
| <div className="mt-6 bg-ui-panel border border-ui-border rounded-xl p-6 shadow-sm"> |
| <div className="text-sm font-medium text-ui-text mb-2">Admin: Edit Slide</div> |
| <div className="grid gap-2"> |
| <input className="input-field" placeholder="Title" value={editing?.title || ''} onChange={(e) => setEditing({ ...(editing || {} as any), title: e.target.value })} /> |
| <input className="input-field" placeholder="Date (YYYY-MM-DD)" value={editing?.date || ''} onChange={(e) => setEditing({ ...(editing || {} as any), date: e.target.value })} /> |
| <input className="input-field" placeholder="File name (in /public/slides)" value={editing?.file || ''} onChange={(e) => setEditing({ ...(editing || {} as any), file: e.target.value })} /> |
| <input className="input-field" placeholder="Order (number)" value={String((editing as any)?.order || '')} onChange={(e) => setEditing({ ...(editing || {} as any), order: Number(e.target.value) })} /> |
| <div className="flex gap-2"> |
| <button className="btn-secondary" onClick={() => setEditing({ title: '', date: '', file: '', order: 0 })}>New</button> |
| <button className="btn-primary shadow-none hover:shadow-none" onClick={async () => { |
| if (!editing || !editing.title || !editing.file) return; |
| const resp = await api.post('/api/slides', editing); |
| if (resp.status >= 200 && resp.status < 300) { |
| setEditing(null); |
| // reload list |
| const listResp = await api.get('/api/slides'); |
| const listData = listResp.data; |
| setItems(Array.isArray(listData?.slides) ? listData.slides : []); |
| } |
| }}>Save</button> |
| </div> |
| </div> |
| </div> |
| )} |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default Slides; |
|
|
|
|
|
|