TransHub / client /src /pages /Slides.tsx
linguabot's picture
Upload folder using huggingface_hub
158a0ac verified
import React, { useEffect, useState } from 'react';
import { api } from '../services/api';
type SlideItem = {
title: string;
file: string; // relative to /slides/
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(() => {
// Access gate: allow only student/admin; do not redirect visitors
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;