···
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; form-action 'self'; base-uri 'self'; frame-ancestors 'none'; object-src 'none'">
<title>Admin - Thistle</title>
<link rel="apple-touch-icon" sizes="180x180" href="../../public/favicon/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="../../public/favicon/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="../../public/favicon/favicon-16x16.png">
<link rel="manifest" href="../../public/favicon/site.webmanifest">
<link rel="stylesheet" href="../styles/main.css">
21
-
margin-bottom: 2rem;
26
-
margin-bottom: 3rem;
33
-
margin-bottom: 1rem;
35
-
align-items: center;
42
-
border-bottom: 2px solid var(--secondary);
43
-
margin-bottom: 2rem;
47
-
padding: 0.75rem 1.5rem;
49
-
background: transparent;
54
-
font-family: inherit;
55
-
border-bottom: 2px solid transparent;
56
-
margin-bottom: -2px;
57
-
transition: all 0.2s;
61
-
color: var(--primary);
65
-
color: var(--primary);
66
-
border-bottom-color: var(--primary);
73
-
.tab-content.active {
91
-
background: #fee2e2;
95
-
margin-bottom: 1rem;
100
-
grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr));
102
-
margin-bottom: 2rem;
106
-
background: var(--background);
107
-
border: 2px solid var(--secondary);
108
-
border-radius: 8px;
115
-
color: var(--primary);
116
-
margin-bottom: 0.25rem;
120
-
color: var(--text);
122
-
font-size: 0.875rem;
126
-
color: var(--text);
128
-
font-size: 0.875rem;
14
+
<link rel="stylesheet" href="../styles/admin.css">
···
147
-
<div id="error-message" class="error" style="display: none;"></div>
31
+
<div id="error-message" class="error hidden"></div>
<div id="loading" class="loading">Loading...</div>
151
-
<div id="content" style="display: none;">
35
+
<div id="content" class="hidden">
<div class="stat-value" id="total-users">0</div>
···
<script type="module" src="../components/admin-classes.ts"></script>
<script type="module" src="../components/user-modal.ts"></script>
<script type="module" src="../components/transcript-view-modal.ts"></script>
214
-
<script type="module">
215
-
const transcriptionsComponent = document.getElementById('transcriptions-component');
216
-
const usersComponent = document.getElementById('users-component');
217
-
const userModal = document.getElementById('user-modal');
218
-
const transcriptModal = document.getElementById('transcript-modal');
219
-
const errorMessage = document.getElementById('error-message');
220
-
const loading = document.getElementById('loading');
221
-
const content = document.getElementById('content');
224
-
function openUserModal(userId) {
225
-
userModal.setAttribute('open', '');
226
-
userModal.userId = userId;
229
-
function closeUserModal() {
230
-
userModal.removeAttribute('open');
231
-
userModal.userId = null;
234
-
function openTranscriptModal(transcriptId) {
235
-
transcriptModal.setAttribute('open', '');
236
-
transcriptModal.transcriptId = transcriptId;
239
-
function closeTranscriptModal() {
240
-
transcriptModal.removeAttribute('open');
241
-
transcriptModal.transcriptId = null;
244
-
// Listen for component events
245
-
transcriptionsComponent.addEventListener('open-transcription', (e) => {
246
-
openTranscriptModal(e.detail.id);
249
-
usersComponent.addEventListener('open-user', (e) => {
250
-
openUserModal(e.detail.id);
253
-
// Listen for modal close events
254
-
userModal.addEventListener('close', closeUserModal);
255
-
userModal.addEventListener('user-updated', async () => {
258
-
userModal.addEventListener('click', (e) => {
259
-
if (e.target === userModal) closeUserModal();
262
-
transcriptModal.addEventListener('close', closeTranscriptModal);
263
-
transcriptModal.addEventListener('transcript-deleted', async () => {
266
-
transcriptModal.addEventListener('click', (e) => {
267
-
if (e.target === transcriptModal) closeTranscriptModal();
270
-
async function loadStats() {
272
-
const [transcriptionsRes, usersRes] = await Promise.all([
273
-
fetch('/api/admin/transcriptions'),
274
-
fetch('/api/admin/users')
277
-
if (!transcriptionsRes.ok || !usersRes.ok) {
278
-
if (transcriptionsRes.status === 403 || usersRes.status === 403) {
279
-
window.location.href = '/';
282
-
throw new Error('Failed to load admin data');
285
-
const transcriptions = await transcriptionsRes.json();
286
-
const users = await usersRes.json();
288
-
document.getElementById('total-users').textContent = users.length;
289
-
document.getElementById('total-transcriptions').textContent = transcriptions.length;
291
-
const failed = transcriptions.filter(t => t.status === 'failed');
292
-
document.getElementById('failed-transcriptions').textContent = failed.length;
294
-
loading.style.display = 'none';
295
-
content.style.display = 'block';
297
-
errorMessage.textContent = error.message;
298
-
errorMessage.style.display = 'block';
299
-
loading.style.display = 'none';
304
-
function switchTab(tabName) {
305
-
document.querySelectorAll('.tab').forEach(t => {
306
-
t.classList.remove('active');
308
-
document.querySelectorAll('.tab-content').forEach(c => {
309
-
c.classList.remove('active');
312
-
const tabButton = document.querySelector(`[data-tab="${tabName}"]`);
313
-
const tabContent = document.getElementById(`${tabName}-tab`);
315
-
if (tabButton && tabContent) {
316
-
tabButton.classList.add('active');
317
-
tabContent.classList.add('active');
319
-
// Update URL without reloading
320
-
const url = new URL(window.location.href);
321
-
url.searchParams.set('tab', tabName);
322
-
// Remove subtab param when leaving classes tab
323
-
if (tabName !== 'classes') {
324
-
url.searchParams.delete('subtab');
326
-
window.history.pushState({}, '', url);
330
-
document.querySelectorAll('.tab').forEach(tab => {
331
-
tab.addEventListener('click', () => {
332
-
switchTab(tab.dataset.tab);
336
-
// Check for tab query parameter on load
337
-
const params = new URLSearchParams(window.location.search);
338
-
const initialTab = params.get('tab');
339
-
const validTabs = ['pending', 'transcriptions', 'users', 'classes'];
341
-
if (initialTab && validTabs.includes(initialTab)) {
342
-
switchTab(initialTab);
98
+
<script type="module" src="./admin.ts"></script>