felt/frontend/src/routes/more/+page.svelte
Mikkel Georgsen 59badcbfe8 feat(01-14): implement More tab with templates, blind editor, wizard, settings, audit
- TemplateManager with LEGO-style building block composition (5 block types)
- BlindStructureEditor with full level fields, mixed game, reorder, add/delete
- StructureWizard generates structures from player count, chips, duration params
- More page with navigable menu to all sub-pages (admin-gated operators section)
- Templates page with DataTable list, create/edit/duplicate/delete actions
- Structures page with DataTable list, wizard integration, and editor
- Settings page with venue config, currency, receipts, theme toggle (Mocha/Latte)
- Audit log page with filterable DataTable, detail panel, and undo capability

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 08:26:12 +01:00

202 lines
4.9 KiB
Svelte

<script lang="ts">
/**
* More tab page.
*
* Navigation list to sub-pages: templates, blind structures, chip sets,
* payout structures, buy-in configs, venue settings, operators,
* audit log, and about/version.
*/
import { auth } from '$lib/stores/auth.svelte';
import { goto } from '$app/navigation';
interface MenuItem {
label: string;
description: string;
href: string;
icon: string;
adminOnly?: boolean;
}
const menuItems: MenuItem[] = [
{ label: 'Tournament Templates', description: 'LEGO-style template builder', href: '/more/templates', icon: '\u{1F4CB}' },
{ label: 'Blind Structures', description: 'Level timing and blinds', href: '/more/structures', icon: '\u{23F1}' },
{ label: 'Chip Sets', description: 'Denomination configurations', href: '/more/templates', icon: '\u{1FA99}' },
{ label: 'Payout Structures', description: 'Prize distribution brackets', href: '/more/templates', icon: '\u{1F4B0}' },
{ label: 'Buy-in Configs', description: 'Entry fees and rake', href: '/more/templates', icon: '\u{1F3AB}' },
{ label: 'Venue Settings', description: 'Currency, receipts, theme', href: '/more/settings', icon: '\u{2699}' },
{ label: 'Operators', description: 'Manage floor staff', href: '/more/settings', icon: '\u{1F464}', adminOnly: true },
{ label: 'Audit Log', description: 'Action history and undo', href: '/more/audit', icon: '\u{1F4DC}' },
{ label: 'About / Version', description: 'Felt v1.0 - Phase 1', href: '/more/settings', icon: '\u{2139}' }
];
let visibleItems = $derived(
menuItems.filter((item) => !item.adminOnly || auth.isAdmin)
);
function handleLogout(): void {
auth.logout();
goto('/login');
}
</script>
<div class="page-content">
<h2>More</h2>
<p class="text-secondary">Settings, templates, and administration.</p>
<!-- Current operator info -->
<div class="operator-card">
<div class="operator-info">
<span class="operator-name">{auth.operator?.name ?? 'Unknown'}</span>
<span class="operator-role">{auth.operator?.role ?? 'Unknown'}</span>
</div>
<button class="logout-btn touch-target" onclick={handleLogout}>
Sign Out
</button>
</div>
<!-- Menu list -->
<nav class="menu-list" aria-label="More options">
{#each visibleItems as item}
<a href={item.href} class="menu-item touch-target">
<span class="menu-icon" aria-hidden="true">{item.icon}</span>
<div class="menu-text">
<span class="menu-label">{item.label}</span>
<span class="menu-desc">{item.description}</span>
</div>
<span class="menu-arrow" aria-hidden="true">&#8250;</span>
</a>
{/each}
</nav>
</div>
<style>
.page-content {
padding: var(--space-4);
}
h2 {
font-size: var(--text-2xl);
font-weight: 700;
color: var(--color-text);
margin-bottom: var(--space-1);
}
.text-secondary {
color: var(--color-text-secondary);
font-size: var(--text-sm);
margin-bottom: var(--space-6);
}
/* Operator card */
.operator-card {
display: flex;
align-items: center;
justify-content: space-between;
padding: var(--space-4);
background-color: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-lg);
margin-bottom: var(--space-6);
}
.operator-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.operator-name {
font-size: var(--text-base);
font-weight: 600;
color: var(--color-text);
}
.operator-role {
font-size: var(--text-xs);
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--color-text-muted);
}
.logout-btn {
padding: var(--space-2) var(--space-3);
font-size: var(--text-sm);
font-weight: 600;
color: var(--color-error);
background: none;
border: 1px solid var(--color-error);
border-radius: var(--radius-md);
cursor: pointer;
}
.logout-btn:hover {
background-color: var(--color-error);
color: white;
}
/* Menu list */
.menu-list {
display: flex;
flex-direction: column;
background-color: var(--color-surface);
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
overflow: hidden;
}
.menu-item {
display: flex;
align-items: center;
gap: var(--space-3);
padding: var(--space-3) var(--space-4);
min-height: var(--touch-target);
text-decoration: none;
color: var(--color-text);
border-bottom: 1px solid var(--color-border);
transition: background-color var(--transition-fast);
}
.menu-item:last-child {
border-bottom: none;
}
.menu-item:hover {
background-color: var(--color-surface-hover);
}
.menu-item:active {
background-color: var(--color-surface-active);
}
.menu-icon {
font-size: var(--text-xl);
width: 32px;
text-align: center;
flex-shrink: 0;
}
.menu-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 1px;
}
.menu-label {
font-size: var(--text-base);
font-weight: 500;
color: var(--color-text);
}
.menu-desc {
font-size: var(--text-xs);
color: var(--color-text-muted);
}
.menu-arrow {
font-size: var(--text-xl);
color: var(--color-text-muted);
flex-shrink: 0;
}
</style>