feat(frontend): route migration - add /admin/login, /admin/students, /admin/live/[slotId], rooms/[roomId]

This commit is contained in:
2026-04-28 17:46:50 +02:00
parent 7e326153a8
commit bbccef4436
27 changed files with 403 additions and 269 deletions
+6 -44
View File
@@ -1,47 +1,9 @@
<script lang="ts">
import { token } from '$lib/auth';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { token } from '$lib/auth';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
onMount(() => {
if ($token) {
goto('/admin');
}
});
onMount(() => {
goto($token ? '/admin' : '/admin/login');
});
</script>
<div class="welcome">
<h1>FPTutor Attendance</h1>
<p>Efficiently tracking attendance and student observations.</p>
<div class="actions">
<a href="/login" class="btn">Tutor Login</a>
</div>
<div class="footer">
<p>Students: Please use the link provided by your tutor during the session.</p>
</div>
</div>
<style>
.welcome {
max-width: 600px;
margin: 100px auto;
text-align: center;
padding: 40px;
background: #f8f9fa;
border-radius: 12px;
}
h1 { font-size: 2.5em; color: #333; margin-bottom: 10px; }
p { color: #666; font-size: 1.2em; }
.actions { margin-top: 40px; }
.btn {
background: #007bff;
color: white;
padding: 12px 30px;
text-decoration: none;
border-radius: 6px;
font-weight: bold;
}
.footer { margin-top: 60px; font-size: 0.9em; color: #888; }
</style>
+2 -2
View File
@@ -5,13 +5,13 @@
onMount(() => {
if (!$token) {
goto('/login');
goto('/admin/login');
}
});
function handleLogout() {
logout();
goto('/login');
goto('/admin/login');
}
</script>
@@ -0,0 +1,10 @@
<script lang="ts">
import { page } from '$app/stores';
// Stub — full implementation in Phase 6
const slotId = $derived(parseInt(($page.params as Record<string, string>).slotId));
</script>
<div style="padding:28px 36px">
<span class="eyebrow">Tutor:innen-Ansicht · Live</span>
<h1 class="h1" style="font-family:var(--serif)">Slot {slotId}</h1>
</div>
@@ -0,0 +1,82 @@
<script lang="ts">
import { api } from '$lib/api';
import { token } from '$lib/auth';
import { goto } from '$app/navigation';
let email = '';
let password = '';
let error = '';
let loading = false;
async function login() {
loading = true;
error = '';
try {
const res = await api.auth.login(email, password);
token.set(res.token);
goto('/admin');
} catch (e: any) {
error = e.message || 'Invalid credentials';
} finally {
loading = false;
}
}
</script>
<div class="login-container">
<h1>Tutor Login</h1>
<form on:submit|preventDefault={login}>
<div class="field">
<label for="email">Email</label>
<input id="email" type="email" bind:value={email} required />
</div>
<div class="field">
<label for="password">Password</label>
<input id="password" type="password" bind:value={password} required />
</div>
{#if error}
<p class="error">{error}</p>
{/if}
<button type="submit" disabled={loading}>
{loading ? 'Logging in...' : 'Login'}
</button>
</form>
</div>
<style>
.login-container {
max-width: 400px;
margin: 100px auto;
padding: 20px;
border: 1px solid #ccc;
border-radius: 8px;
}
.field {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
.error {
color: red;
margin-bottom: 15px;
}
button {
width: 100%;
padding: 10px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
button:disabled {
background: #ccc;
}
</style>
+53 -195
View File
@@ -1,205 +1,63 @@
<script lang="ts">
import { onMount } from 'svelte';
import { api } from '$lib/api';
import RoomCanvas from '$lib/RoomCanvas.svelte';
import type { Room, LayoutElement } from '$lib/types';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import type { Room } from '$lib/types';
let rooms = $state<Room[]>([]);
let selectedRoomId = $state<number | null>(null);
let selectedRoom = $state<Room | null>(null);
let newRoomName = $state('');
let rooms = $state<Room[]>([]);
let newRoomName = $state('');
onMount(async () => {
await loadRooms();
});
onMount(async () => {
rooms = await api.admin.rooms.list();
});
async function loadRooms() {
rooms = await api.admin.rooms.list();
}
$effect(() => {
if (selectedRoomId) {
api.admin.rooms.get(selectedRoomId).then(r => selectedRoom = r);
}
});
async function createRoom() {
const defaultLayout: LayoutElement[] = [
{ id: 's1', label: '1', x: 2, y: 2, width: 1, height: 1, type: 'seat' }
];
try {
const room = await api.admin.rooms.create(newRoomName, defaultLayout);
newRoomName = '';
await loadRooms();
selectedRoomId = room.id;
} catch (e) {
alert(e);
}
}
async function saveLayout() {
if (!selectedRoom) return;
try {
await api.admin.rooms.updateLayout(selectedRoom.id, selectedRoom.layout);
alert('Layout saved');
} catch (e) {
alert(e);
}
}
function addElement(type: LayoutElement['type']) {
if (!selectedRoom) return;
const id = Math.random().toString(36).substr(2, 9);
const newEl: LayoutElement = {
id,
label: type === 'seat' ? (selectedRoom.layout.filter(e => e.type === 'seat').length + 1).toString() : '',
x: 0,
y: 0,
width: type === 'table' ? 2 : 1,
height: 1,
type
};
selectedRoom.layout = [...selectedRoom.layout, newEl];
}
let selectedElementId = $state<string | null>(null);
let selectedElement = $derived(selectedRoom?.layout.find(e => e.id === selectedElementId));
function deleteElement() {
if (!selectedRoom || !selectedElementId) return;
selectedRoom.layout = selectedRoom.layout.filter(e => e.id !== selectedElementId);
selectedElementId = null;
}
async function createRoom() {
if (!newRoomName.trim()) return;
try {
await api.admin.rooms.create(newRoomName, []);
newRoomName = '';
rooms = await api.admin.rooms.list();
} catch (_) {}
}
</script>
<h1>Room Layouts</h1>
<div style="padding:28px 36px;display:flex;flex-direction:column;gap:22px">
<div>
<span class="eyebrow">Räume</span>
<h1 class="h1" style="font-family:var(--serif)">Raumlayout-Editor</h1>
</div>
<div class="management-grid">
<div class="rooms-panel">
<h2>Rooms</h2>
<form onsubmit={createRoom}>
<input bind:value={newRoomName} placeholder="Room Name" required />
<button type="submit">Create</button>
</form>
<div class="room-list">
{#each rooms as room}
<div
class="room-item"
class:selected={selectedRoomId === room.id}
onclick={() => selectedRoomId = room.id}
>
{room.name}
</div>
{/each}
</div>
<div class="card" style="overflow:hidden">
<div style="padding:14px 18px;border-bottom:1px solid var(--rule);display:flex;align-items:center;justify-content:space-between">
<div class="serif" style="font-size:18px;font-weight:500">Räume</div>
<form onsubmit={(e) => { e.preventDefault(); createRoom(); }} style="display:flex;gap:8px">
<input class="input" bind:value={newRoomName} placeholder="Raumname" style="width:200px" />
<button class="btn" type="submit">+ Neuer Raum</button>
</form>
</div>
<div class="editor-panel">
{#if selectedRoom}
<div class="editor-header">
<h2>Editing: {selectedRoom.name}</h2>
<div class="toolbar">
<button onclick={() => addElement('seat')}>Add Seat</button>
<button onclick={() => addElement('table')}>Add Table</button>
<button onclick={() => addElement('door')}>Add Door</button>
<button class="save-btn" onclick={saveLayout}>Save Layout</button>
</div>
</div>
<div class="canvas-container">
<RoomCanvas
bind:elements={selectedRoom.layout}
editable={true}
selectedId={selectedElementId}
onElementClick={(el) => selectedElementId = el.id}
/>
<div class="properties-panel">
<h3>Properties</h3>
{#if selectedElement}
<div class="field">
<label>Label</label>
<input bind:value={selectedElement.label} />
</div>
<div class="field">
<label>Width</label>
<input type="number" step="0.5" bind:value={selectedElement.width} />
</div>
<div class="field">
<label>Height</label>
<input type="number" step="0.5" bind:value={selectedElement.height} />
</div>
<button class="delete-btn" onclick={deleteElement}>Delete Element</button>
{:else}
<p>Select an element to edit properties.</p>
{/if}
</div>
</div>
{:else}
<p>Select a room to edit its layout.</p>
{/if}
</div>
{#if rooms.length === 0}
<div style="padding:32px;text-align:center">
<span class="small" style="color:var(--ink-4)">Noch keine Räume angelegt.</span>
</div>
{:else}
<table style="width:100%;border-collapse:collapse;font-size:13px">
<thead>
<tr style="background:rgba(0,0,0,0.02);color:var(--ink-3);text-align:left">
<th style="padding:10px 14px;font-family:var(--mono);font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase">Name</th>
<th style="padding:10px 14px;font-family:var(--mono);font-size:10.5px;letter-spacing:0.1em;text-transform:uppercase;text-align:right">Aktionen</th>
</tr>
</thead>
<tbody>
{#each rooms as room, i}
<tr class="row-hover" style="border-top:{i === 0 ? 'none' : '1px solid var(--rule)'}">
<td style="padding:12px 14px">{room.name}</td>
<td style="padding:12px 14px;text-align:right">
<a href="/admin/rooms/{room.id}" class="btn ghost sm">Bearbeiten</a>
</td>
</tr>
{/each}
</tbody>
</table>
{/if}
</div>
</div>
<style>
.management-grid {
display: grid;
grid-template-columns: 200px 1fr;
gap: 20px;
}
.room-item {
padding: 10px;
border: 1px solid #eee;
margin-bottom: 5px;
cursor: pointer;
border-radius: 4px;
}
.room-item.selected {
background: #e7f1ff;
border-color: #007bff;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.toolbar button {
margin-right: 5px;
}
.save-btn {
background: #28a745;
color: white;
border: none;
padding: 5px 15px;
border-radius: 4px;
}
.canvas-container {
display: flex;
gap: 20px;
}
.properties-panel {
width: 200px;
padding: 15px;
background: #f8f9fa;
border-radius: 8px;
}
.field {
margin-bottom: 10px;
}
.field label {
display: block;
font-size: 0.8em;
color: #666;
}
.field input {
width: 100%;
padding: 4px;
}
.delete-btn {
margin-top: 10px;
color: red;
width: 100%;
}
</style>
@@ -0,0 +1,111 @@
<script lang="ts">
import { page } from '$app/stores';
import { onMount } from 'svelte';
import { api } from '$lib/api';
import RoomCanvas from '$lib/RoomCanvas.svelte';
import type { Room, LayoutElement } from '$lib/types';
const roomId = $derived(parseInt(($page.params as Record<string, string>).roomId));
let room = $state<Room | null>(null);
onMount(async () => {
room = await api.admin.rooms.get(roomId);
});
$effect(() => {
if (roomId) {
api.admin.rooms.get(roomId).then((r: Room) => { room = r; });
}
});
async function saveLayout() {
if (!room) return;
try {
await api.admin.rooms.updateLayout(room.id, room.layout);
} catch (_) {}
}
function addElement(type: LayoutElement['type']) {
if (!room) return;
const id = Math.random().toString(36).substr(2, 9);
const newEl: LayoutElement = {
id,
label: type === 'seat' ? (room.layout.filter((e: LayoutElement) => e.type === 'seat').length + 1).toString() : '',
x: 0, y: 0,
width: type === 'table' ? 2 : 1,
height: 1,
type,
};
room.layout = [...room.layout, newEl];
}
let selectedElementId = $state<string | null>(null);
const selectedElement = $derived(room?.layout.find((e: LayoutElement) => e.id === selectedElementId));
function deleteElement() {
if (!room || !selectedElementId) return;
room.layout = room.layout.filter((e: LayoutElement) => e.id !== selectedElementId);
selectedElementId = null;
}
</script>
{#if room}
<div style="padding:28px 36px;display:flex;flex-direction:column;gap:16px">
<div style="display:flex;align-items:center;justify-content:space-between">
<div>
<span class="eyebrow">Räume</span>
<h2 class="h2" style="font-family:var(--serif)">{room.name}</h2>
</div>
<div style="display:flex;gap:8px">
<button class="btn ghost" onclick={() => addElement('seat')}>+ Sitz</button>
<button class="btn ghost" onclick={() => addElement('table')}>+ Tisch</button>
<button class="btn ghost" onclick={() => addElement('door')}>+ Tür</button>
<button class="btn" onclick={saveLayout}>Speichern</button>
</div>
</div>
<div style="display:flex;gap:20px">
<div style="flex:1">
<RoomCanvas
bind:elements={room.layout}
editable={true}
selectedId={selectedElementId}
onElementClick={(el) => { selectedElementId = el.id; }}
/>
</div>
<div class="card" style="width:240px;padding:16px;display:flex;flex-direction:column;gap:12px">
<div class="eyebrow">Auswahl</div>
{#if selectedElement}
<div style="display:flex;flex-direction:column;gap:8px">
<div>
<div class="tiny" style="color:var(--ink-3)">Bezeichnung</div>
<input class="input" bind:value={selectedElement.label} />
</div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div>
<div class="tiny" style="color:var(--ink-3)">Breite</div>
<input class="input" type="number" step="0.5" bind:value={selectedElement.width} />
</div>
<div>
<div class="tiny" style="color:var(--ink-3)">Höhe</div>
<input class="input" type="number" step="0.5" bind:value={selectedElement.height} />
</div>
</div>
<button
style="color:var(--accent);background:none;border:none;cursor:pointer;text-align:left;font-family:var(--sans);font-size:13px;padding:4px 0"
onclick={deleteElement}
>Löschen ⌫</button>
</div>
{:else}
<span class="small" style="color:var(--ink-4)">Element auswählen</span>
{/if}
</div>
</div>
</div>
{:else}
<div style="padding:28px 36px">
<span class="small" style="color:var(--ink-4)">Raum wird geladen…</span>
</div>
{/if}
@@ -0,0 +1,8 @@
<script lang="ts">
// Stub — full implementation in Phase 6
</script>
<div style="padding:28px 36px">
<span class="eyebrow">Studierende</span>
<h1 class="h1" style="font-family:var(--serif)">Studierende</h1>
</div>