feat(01-13): layout shell with header, tabs, FAB, toast, data table
- Persistent header: clock countdown, level, blinds, player count (red pulse <10s, PAUSED/BREAK badges) - Bottom tab bar (mobile): Overview, Players, Tables, Financials, More with 48px touch targets - Desktop sidebar (>=768px): vertical nav replacing bottom tabs - FAB: expandable quick actions (Bust, Buy In, Rebuy, Add-On, Pause/Resume) with backdrop - Toast notification system: success/info/warning/error with auto-dismiss and stacking - DataTable: sortable columns, sticky header, search/filter, mobile swipe actions, skeleton loading - Multi-tournament tabs: horizontal scrollable selector when 2+ tournaments active - Loading components: spinner (sm/md/lg), skeleton rows, full-page overlay - Root layout: auth guard, responsive shell (mobile bottom tabs / desktop sidebar) - Route pages: overview, players, tables, financials, more with placeholder content Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
51153df8dd
commit
7f91301efa
74 changed files with 2598 additions and 76 deletions
1
frontend/build/_app/immutable/assets/0.BcOVBfXh.css
Normal file
1
frontend/build/_app/immutable/assets/0.BcOVBfXh.css
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
[data-theme=mocha],:root{--ctp-rosewater: #f5e0dc;--ctp-flamingo: #f2cdcd;--ctp-pink: #f5c2e7;--ctp-mauve: #cba6f7;--ctp-red: #f38ba8;--ctp-maroon: #eba0ac;--ctp-peach: #fab387;--ctp-yellow: #f9e2af;--ctp-green: #a6e3a1;--ctp-teal: #94e2d5;--ctp-sky: #89dceb;--ctp-sapphire: #74c7ec;--ctp-blue: #89b4fa;--ctp-lavender: #b4befe;--ctp-text: #cdd6f4;--ctp-subtext1: #bac2de;--ctp-subtext0: #a6adc8;--ctp-overlay2: #9399b2;--ctp-overlay1: #7f849c;--ctp-overlay0: #6c7086;--ctp-surface2: #585b70;--ctp-surface1: #45475a;--ctp-surface0: #313244;--ctp-base: #1e1e2e;--ctp-mantle: #181825;--ctp-crust: #11111b;--color-bg: var(--ctp-base);--color-bg-elevated: var(--ctp-mantle);--color-bg-sunken: var(--ctp-crust);--color-surface: var(--ctp-surface0);--color-surface-hover: var(--ctp-surface1);--color-surface-active: var(--ctp-surface2);--color-text: var(--ctp-text);--color-text-secondary: var(--ctp-subtext1);--color-text-muted: var(--ctp-subtext0);--color-primary: var(--ctp-blue);--color-success: var(--ctp-green);--color-warning: var(--ctp-yellow);--color-error: var(--ctp-red);--color-accent: var(--ctp-mauve);--color-border: var(--ctp-surface1);--color-overlay: var(--ctp-overlay0);--color-felt: var(--ctp-green);--color-card: var(--ctp-text);--color-bounty: var(--ctp-pink);--color-prize: var(--ctp-yellow);--color-chip: var(--ctp-peach);--color-clock: var(--ctp-sapphire);--color-break: var(--ctp-teal);--color-elimination: var(--ctp-red)}[data-theme=latte]{--ctp-rosewater: #dc8a78;--ctp-flamingo: #dd7878;--ctp-pink: #ea76cb;--ctp-mauve: #8839ef;--ctp-red: #d20f39;--ctp-maroon: #e64553;--ctp-peach: #fe640b;--ctp-yellow: #df8e1d;--ctp-green: #40a02b;--ctp-teal: #179299;--ctp-sky: #04a5e5;--ctp-sapphire: #209fb5;--ctp-blue: #1e66f5;--ctp-lavender: #7287fd;--ctp-text: #4c4f69;--ctp-subtext1: #5c5f77;--ctp-subtext0: #6c6f85;--ctp-overlay2: #7c7f93;--ctp-overlay1: #8c8fa1;--ctp-overlay0: #9ca0b0;--ctp-surface2: #acb0be;--ctp-surface1: #bcc0cc;--ctp-surface0: #ccd0da;--ctp-base: #eff1f5;--ctp-mantle: #e6e9ef;--ctp-crust: #dce0e8}:root{--font-body: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;--font-mono: "JetBrains Mono", "Fira Code", ui-monospace, "Cascadia Code", "Source Code Pro", Menlo, Consolas, "DejaVu Sans Mono", monospace;--text-xs: .75rem;--text-sm: .875rem;--text-base: 1rem;--text-lg: 1.125rem;--text-xl: 1.25rem;--text-2xl: 1.5rem;--text-3xl: 1.875rem;--text-4xl: 2.25rem;--leading-tight: 1.25;--leading-normal: 1.5;--leading-relaxed: 1.75;--space-1: .25rem;--space-2: .5rem;--space-3: .75rem;--space-4: 1rem;--space-6: 1.5rem;--space-8: 2rem;--space-12: 3rem;--radius-sm: .25rem;--radius-md: .5rem;--radius-lg: .75rem;--radius-xl: 1rem;--radius-full: 9999px;--shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);--shadow-md: 0 4px 6px rgba(0, 0, 0, .3);--shadow-lg: 0 10px 15px rgba(0, 0, 0, .3);--transition-fast: .1s ease;--transition-normal: .2s ease;--transition-slow: .3s ease;--touch-target: 48px}*,*:before,*:after{box-sizing:border-box;margin:0;padding:0}html{font-family:var(--font-body);font-size:16px;line-height:var(--leading-normal);color:var(--color-text);background-color:var(--color-bg);-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;scrollbar-gutter:stable}body{min-height:100dvh;color:var(--color-text);background-color:var(--color-bg)}button,a,input,select,textarea,[role=button],[role=tab],[role=menuitem]{touch-action:manipulation}.touch-target,button,[role=button],[role=tab]{min-height:var(--touch-target);min-width:var(--touch-target)}button:active,[role=button]:active,[role=tab]:active,.touch-target:active{transform:scale(.97);opacity:.9;transition:transform var(--transition-fast),opacity var(--transition-fast)}:focus-visible{outline:2px solid var(--color-primary);outline-offset:2px}:focus:not(:focus-visible){outline:none}::-webkit-scrollbar{width:8px;height:8px}::-webkit-scrollbar-track{background:var(--color-bg)}::-webkit-scrollbar-thumb{background:var(--color-surface-active);border-radius:var(--radius-full)}::-webkit-scrollbar-thumb:hover{background:var(--color-overlay)}*{scrollbar-width:thin;scrollbar-color:var(--color-surface-active) var(--color-bg)}.font-mono{font-family:var(--font-mono)}.timer,.number,.blinds,.chips,.currency{font-family:var(--font-mono);font-variant-numeric:tabular-nums}.visually-hidden{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
||||
1
frontend/build/_app/immutable/assets/2.BmNlW7Gm.css
Normal file
1
frontend/build/_app/immutable/assets/2.BmNlW7Gm.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.redirect.svelte-1uha8ag{display:flex;align-items:center;justify-content:center;min-height:50dvh;color:var(--color-text-muted)}
|
||||
|
|
@ -1 +0,0 @@
|
|||
.container.svelte-1uha8ag{display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:100dvh;padding:1rem;gap:.5rem}h1.svelte-1uha8ag{font-size:2rem;font-weight:700;color:var(--color-primary)}.text-secondary.svelte-1uha8ag{color:var(--color-text-secondary)}
|
||||
1
frontend/build/_app/immutable/assets/3.BysT7-iU.css
Normal file
1
frontend/build/_app/immutable/assets/3.BysT7-iU.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.page-content.svelte-1ba4c5d{padding:var(--space-4)}h2.svelte-1ba4c5d{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-1ba4c5d{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.finance-grid.svelte-1ba4c5d{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-3)}@media(min-width:768px){.finance-grid.svelte-1ba4c5d{grid-template-columns:repeat(3,1fr)}}.finance-card.svelte-1ba4c5d{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-4);background-color:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border)}.finance-card.highlight.svelte-1ba4c5d{border-color:var(--color-prize)}.finance-label.svelte-1ba4c5d{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted)}.finance-value.svelte-1ba4c5d{font-size:var(--text-xl);font-weight:700;color:var(--color-text)}.finance-value.prize.svelte-1ba4c5d{color:var(--color-prize)}.empty-state.svelte-1ba4c5d{color:var(--color-text-muted);font-style:italic;padding:var(--space-8) 0;text-align:center}
|
||||
1
frontend/build/_app/immutable/assets/5.B34oOQk5.css
Normal file
1
frontend/build/_app/immutable/assets/5.B34oOQk5.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.page-content.svelte-hq0atu{padding:var(--space-4)}h2.svelte-hq0atu{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-hq0atu{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.menu-list.svelte-hq0atu{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.svelte-hq0atu{display:flex;align-items:center;justify-content:space-between;padding:var(--space-4);min-height:var(--touch-target);border-bottom:1px solid var(--color-border)}.menu-item.svelte-hq0atu:last-child{border-bottom:none}.menu-action.svelte-hq0atu{background:none;border:none;border-bottom:1px solid var(--color-border);cursor:pointer;width:100%;text-align:left;font-size:inherit;font-family:inherit}.menu-action.svelte-hq0atu:hover{background-color:var(--color-surface-hover)}.menu-label.svelte-hq0atu{font-size:var(--text-base);color:var(--color-text)}.menu-value.svelte-hq0atu{font-size:var(--text-sm);color:var(--color-text-secondary)}.danger.svelte-hq0atu .menu-label:where(.svelte-hq0atu){color:var(--color-error)}.divider.svelte-hq0atu{border:none;border-top:1px solid var(--color-border);margin:0}
|
||||
1
frontend/build/_app/immutable/assets/6.BcOWEnnB.css
Normal file
1
frontend/build/_app/immutable/assets/6.BcOWEnnB.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.page-content.svelte-14qseeg{padding:var(--space-4)}h2.svelte-14qseeg{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-14qseeg{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-6)}.stats-grid.svelte-14qseeg{display:grid;grid-template-columns:repeat(2,1fr);gap:var(--space-3)}@media(min-width:768px){.stats-grid.svelte-14qseeg{grid-template-columns:repeat(4,1fr)}}.stat-card.svelte-14qseeg{display:flex;flex-direction:column;gap:var(--space-1);padding:var(--space-4);background-color:var(--color-surface);border-radius:var(--radius-lg);border:1px solid var(--color-border)}.stat-label.svelte-14qseeg{font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted)}.stat-value.svelte-14qseeg{font-size:var(--text-2xl);font-weight:700;color:var(--color-text)}.empty-state.svelte-14qseeg{color:var(--color-text-muted);font-style:italic;padding:var(--space-8) 0;text-align:center}
|
||||
1
frontend/build/_app/immutable/assets/7.CLv6rscz.css
Normal file
1
frontend/build/_app/immutable/assets/7.CLv6rscz.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.page-content.svelte-wtkzqx{padding:var(--space-4)}h2.svelte-wtkzqx{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-wtkzqx{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)}
|
||||
1
frontend/build/_app/immutable/assets/8.WyH666g9.css
Normal file
1
frontend/build/_app/immutable/assets/8.WyH666g9.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
.page-content.svelte-bf0doe{padding:var(--space-4)}h2.svelte-bf0doe{font-size:var(--text-2xl);font-weight:700;color:var(--color-text);margin-bottom:var(--space-2)}.text-secondary.svelte-bf0doe{color:var(--color-text-secondary);font-size:var(--text-sm);margin-bottom:var(--space-4)}
|
||||
|
|
@ -0,0 +1 @@
|
|||
.data-table-wrapper.svelte-16k18c8{width:100%;overflow:hidden}.table-search.svelte-16k18c8{padding:var(--space-3) 0}.search-input.svelte-16k18c8{width:100%;padding:var(--space-2) var(--space-3);font-size:var(--text-sm);color:var(--color-text);background-color:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);outline:none;transition:border-color var(--transition-fast);min-height:var(--touch-target)}.search-input.svelte-16k18c8:focus{border-color:var(--color-primary)}.search-input.svelte-16k18c8::placeholder{color:var(--color-text-muted)}.table-scroll.svelte-16k18c8{overflow-x:auto;-webkit-overflow-scrolling:touch}.data-table.svelte-16k18c8{width:100%;border-collapse:collapse;font-size:var(--text-sm)}.data-table.svelte-16k18c8 thead:where(.svelte-16k18c8){position:sticky;top:0;z-index:2}.data-table.svelte-16k18c8 th:where(.svelte-16k18c8){padding:var(--space-2) var(--space-3);font-weight:600;font-size:var(--text-xs);text-transform:uppercase;letter-spacing:.05em;color:var(--color-text-muted);background-color:var(--color-bg-elevated);border-bottom:2px solid var(--color-border);white-space:nowrap;-webkit-user-select:none;user-select:none}.data-table.svelte-16k18c8 th.sortable:where(.svelte-16k18c8){cursor:pointer}.data-table.svelte-16k18c8 th.sortable:where(.svelte-16k18c8):hover{color:var(--color-text)}.th-content.svelte-16k18c8{display:inline-flex;align-items:center;gap:var(--space-1)}.sort-indicator.svelte-16k18c8{font-size:8px;color:var(--color-text-muted);opacity:.3}.sort-indicator.active.svelte-16k18c8{opacity:1;color:var(--color-primary)}.data-table.svelte-16k18c8 td:where(.svelte-16k18c8){padding:var(--space-2) var(--space-3);color:var(--color-text);border-bottom:1px solid var(--color-border);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;max-width:200px}.data-row.svelte-16k18c8{min-height:var(--touch-target);transition:background-color var(--transition-fast)}.data-row.svelte-16k18c8:hover{background-color:var(--color-surface)}.data-row.clickable.svelte-16k18c8{cursor:pointer}.data-row.clickable.svelte-16k18c8:active{background-color:var(--color-surface-hover)}.empty-state.svelte-16k18c8{text-align:center;padding:var(--space-12) var(--space-4);color:var(--color-text-muted);font-style:italic}.skeleton-row.svelte-16k18c8 td:where(.svelte-16k18c8){padding:var(--space-3)}.skeleton-cell.svelte-16k18c8{height:16px;background:linear-gradient(90deg,var(--color-surface) 25%,var(--color-surface-hover) 50%,var(--color-surface) 75%);background-size:200% 100%;animation:svelte-16k18c8-skeleton-shimmer 1.5s ease-in-out infinite;border-radius:var(--radius-sm)}@keyframes svelte-16k18c8-skeleton-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.swipe-actions-row.svelte-16k18c8 td:where(.svelte-16k18c8){padding:0;border-bottom:none}.swipe-actions.svelte-16k18c8{display:flex;justify-content:flex-end;gap:var(--space-1);padding:var(--space-1);background-color:var(--color-bg-sunken)}.swipe-action-btn.svelte-16k18c8{padding:var(--space-2) var(--space-4);font-size:var(--text-sm);font-weight:600;color:#fff;border:none;border-radius:var(--radius-md);cursor:pointer;white-space:nowrap}.hide-mobile.svelte-16k18c8{display:none}@media(min-width:768px){.hide-mobile.svelte-16k18c8{display:table-cell}.data-table.svelte-16k18c8 td:where(.svelte-16k18c8){max-width:300px}}
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{N as p,m as u,O as c,P as f,Q as E,T as g,R as w,h as d,j as s,S as N,c as y,U as M,f as x,V as A}from"./Ym0WvvUy.js";var l;const i=((l=globalThis==null?void 0:globalThis.window)==null?void 0:l.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(i==null?void 0:i.createHTML(t))??t}function O(t){var r=p("template");return r.innerHTML=L(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function b(t,r){var e=(r&g)!==0,m=(r&w)!==0,a,v=!t.startsWith("<!>");return()=>{if(d)return n(s,null),s;a===void 0&&(a=O(v?t:"<!>"+t),e||(a=f(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=f(o),h=o.lastChild;n(T,h)}else n(o,o);return o}}function C(t=""){if(!d){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==M?(e.before(e=u()),x(e)):A(e),n(e,e),e}function I(){if(d)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function S(t,r){if(d){var e=c;((e.f&N)===0||e.nodes.end===null)&&(e.nodes.end=s),y();return}t!==null&&t.before(r)}const P="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(P);export{S as a,n as b,I as c,b as f,C as t};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{b as T,h as o,c as b,E as v,r as p,H as A,d as E,e as R,f as g,i as l,j as m}from"./Ym0WvvUy.js";import{B as d}from"./Da6yQRl8.js";function H(_,u,c=!1){var i;o&&(i=m,b());var n=new d(_),h=c?v:0;function f(a,s){if(o){var r=p(i),e;if(r===A?e=0:r===E?e=!1:e=parseInt(r.substring(1)),a!==e){var t=R();g(t),n.anchor=t,l(!1),n.ensure(a,s),l(!0);return}}n.ensure(a,s)}T(()=>{var a=!1;u((s,r=0)=>{a=!0,f(r,s)}),a||f(!1,null)},h)}export{H as i};
|
||||
1
frontend/build/_app/immutable/chunks/BTkWS7xQ.js
Normal file
1
frontend/build/_app/immutable/chunks/BTkWS7xQ.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{t as b}from"./BeLKMLqR.js";import{h as c}from"./C4An0dnW.js";function A(i,u={},r,f){for(var a in r){var o=r[a];u[a]!==o&&(r[a]==null?i.style.removeProperty(a):i.style.setProperty(a,o,f))}}function t(i,u,r,f){var a=i.__style;if(c||a!==u){var o=b(u,f);(!c||o!==i.getAttribute("style"))&&(o==null?i.removeAttribute("style"):i.style.cssText=o),i.__style=u}else f&&(Array.isArray(f)?(A(i,r==null?void 0:r[0],f[0]),A(i,r==null?void 0:r[1],f[1],"important")):A(i,r,f));return f}export{t as s};
|
||||
1
frontend/build/_app/immutable/chunks/BViIIwgj.js
Normal file
1
frontend/build/_app/immutable/chunks/BViIIwgj.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{j as g,m as d,u as c,k as m,n as i,o as b,g as p,q as v,v as k,w as h}from"./C4An0dnW.js";function x(t=!1){const s=g,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(t){let o=0,n={};const _=k(()=>{let l=!1;const r=s.s;for(const a in r)r[a]!==n[a]&&(n[a]=r[a],l=!0);return l&&o++,o});f=()=>p(_)}e.b.length&&d(()=>{u(s,f),i(e.b)}),c(()=>{const o=m(()=>e.m.map(b));return()=>{for(const n of o)typeof n=="function"&&n()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(t,s){if(t.l.s)for(const e of t.l.s)p(e);s()}h();export{x as i};
|
||||
2
frontend/build/_app/immutable/chunks/BeLKMLqR.js
Normal file
2
frontend/build/_app/immutable/chunks/BeLKMLqR.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
import{K as U,T as tr,a5 as sr,h as A,_ as Y,a6 as vr,U as cr,g as j,W as dr,Y as gr,Z as x,$ as q,O as z,a7 as hr,a8 as pr,a9 as y,N as _r,aa as I,M as m,ab as Er,R as Ar,i as Tr,ac as Nr,ad as V,ae as Sr,af as Ir,ag as br,ah as rr,ai as Cr,H as ur,J as lr,aj as B,ak as or,al as Mr,am as Or,an as Lr,I as wr,ao as Hr,ap as Rr,aq as kr,ar as Dr,as as Fr,at as zr,au as Ur}from"./C4An0dnW.js";function Wr(r,e){return e}function Yr(r,e,f){for(var a=[],u=e.length,n,s=e.length,c=0;c<u;c++){let g=e[c];lr(g,()=>{if(n){if(n.pending.delete(g),n.done.add(g),n.pending.size===0){var t=r.outrogroups;G(V(n.done)),t.delete(n),t.size===0&&(r.outrogroups=null)}}else s-=1},!1)}if(s===0){var l=a.length===0&&f!==null;if(l){var d=f,o=d.parentNode;Lr(o),o.append(d),r.items.clear()}G(e,!l)}else n={pending:new Set(e),done:new Set},(r.outrogroups??(r.outrogroups=new Set)).add(n)}function G(r,e=!0){for(var f=0;f<r.length;f++)wr(r[f],e)}var er;function Zr(r,e,f,a,u,n=null){var s=r,c=new Map,l=(e&sr)!==0;if(l){var d=r;s=A?Y(vr(d)):d.appendChild(U())}A&&cr();var o=null,g=Tr(()=>{var v=f();return Nr(v)?v:v==null?[]:V(v)}),t,h=!0;function T(){i.fallback=o,qr(i,t,s,e,a),o!==null&&(t.length===0?(o.f&I)===0?ur(o):(o.f^=I,k(o,null,s)):lr(o,()=>{o=null}))}var N=tr(()=>{t=j(g);var v=t.length;let O=!1;if(A){var L=dr(s)===gr;L!==(v===0)&&(s=x(),Y(s),q(!1),O=!0)}for(var _=new Set,C=_r,w=Ar(),p=0;p<v;p+=1){A&&z.nodeType===hr&&z.data===pr&&(s=z,O=!0,q(!1));var M=t[p],H=a(M,p),E=h?null:c.get(H);E?(E.v&&y(E.v,M),E.i&&y(E.i,p),w&&C.unskip_effect(E.e)):(E=Br(c,h?s:er??(er=U()),M,H,p,u,e,f),h||(E.e.f|=I),c.set(H,E)),_.add(H)}if(v===0&&n&&!o&&(h?o=m(()=>n(s)):(o=m(()=>n(er??(er=U()))),o.f|=I)),v>_.size&&Er(),A&&v>0&&Y(x()),!h)if(w){for(const[D,F]of c)_.has(D)||C.skip_effect(F.e);C.oncommit(T),C.ondiscard(()=>{})}else T();O&&q(!0),j(g)}),i={effect:N,items:c,outrogroups:null,fallback:o};h=!1,A&&(s=z)}function R(r){for(;r!==null&&(r.f&Mr)===0;)r=r.next;return r}function qr(r,e,f,a,u){var E,D,F,X,J,P,W,Z,$;var n=(a&Or)!==0,s=e.length,c=r.items,l=R(r.effect.first),d,o=null,g,t=[],h=[],T,N,i,v;if(n)for(v=0;v<s;v+=1)T=e[v],N=u(T,v),i=c.get(N).e,(i.f&I)===0&&((D=(E=i.nodes)==null?void 0:E.a)==null||D.measure(),(g??(g=new Set)).add(i));for(v=0;v<s;v+=1){if(T=e[v],N=u(T,v),i=c.get(N).e,r.outrogroups!==null)for(const S of r.outrogroups)S.pending.delete(i),S.done.delete(i);if((i.f&I)!==0)if(i.f^=I,i===l)k(i,null,f);else{var O=o?o.next:l;i===r.effect.last&&(r.effect.last=i.prev),i.prev&&(i.prev.next=i.next),i.next&&(i.next.prev=i.prev),b(r,o,i),b(r,i,O),k(i,O,f),o=i,t=[],h=[],l=R(o.next);continue}if((i.f&B)!==0&&(ur(i),n&&((X=(F=i.nodes)==null?void 0:F.a)==null||X.unfix(),(g??(g=new Set)).delete(i))),i!==l){if(d!==void 0&&d.has(i)){if(t.length<h.length){var L=h[0],_;o=L.prev;var C=t[0],w=t[t.length-1];for(_=0;_<t.length;_+=1)k(t[_],L,f);for(_=0;_<h.length;_+=1)d.delete(h[_]);b(r,C.prev,w.next),b(r,o,C),b(r,w,L),l=L,o=w,v-=1,t=[],h=[]}else d.delete(i),k(i,l,f),b(r,i.prev,i.next),b(r,i,o===null?r.effect.first:o.next),b(r,o,i),o=i;continue}for(t=[],h=[];l!==null&&l!==i;)(d??(d=new Set)).add(l),h.push(l),l=R(l.next);if(l===null)continue}(i.f&I)===0&&t.push(i),o=i,l=R(i.next)}if(r.outrogroups!==null){for(const S of r.outrogroups)S.pending.size===0&&(G(V(S.done)),(J=r.outrogroups)==null||J.delete(S));r.outrogroups.size===0&&(r.outrogroups=null)}if(l!==null||d!==void 0){var p=[];if(d!==void 0)for(i of d)(i.f&B)===0&&p.push(i);for(;l!==null;)(l.f&B)===0&&l!==r.fallback&&p.push(l),l=R(l.next);var M=p.length;if(M>0){var H=(a&sr)!==0&&s===0?f:null;if(n){for(v=0;v<M;v+=1)(W=(P=p[v].nodes)==null?void 0:P.a)==null||W.measure();for(v=0;v<M;v+=1)($=(Z=p[v].nodes)==null?void 0:Z.a)==null||$.fix()}Yr(r,p,H)}}n&&or(()=>{var S,Q;if(g!==void 0)for(i of g)(Q=(S=i.nodes)==null?void 0:S.a)==null||Q.apply()})}function Br(r,e,f,a,u,n,s,c){var l=(s&Sr)!==0?(s&Ir)===0?br(f,!1,!1):rr(f):null,d=(s&Cr)!==0?rr(u):null;return{v:l,i:d,e:m(()=>(n(e,l??f,d??u,c),()=>{r.delete(a)}))}}function k(r,e,f){if(r.nodes)for(var a=r.nodes.start,u=r.nodes.end,n=e&&(e.f&I)===0?e.nodes.start:f;a!==null;){var s=Hr(a);if(n.before(a),a===u)return;a=s}}function b(r,e,f){e===null?r.effect.first=f:e.next=f,f===null?r.effect.last=e:f.prev=e}const fr=[...`
|
||||
\r\f \v\uFEFF`];function Kr(r,e,f){var a=r==null?"":""+r;if(e&&(a=a?a+" "+e:e),f){for(var u of Object.keys(f))if(f[u])a=a?a+" "+u:u;else if(a.length)for(var n=u.length,s=0;(s=a.indexOf(u,s))>=0;){var c=s+n;(s===0||fr.includes(a[s-1]))&&(c===a.length||fr.includes(a[c]))?a=(s===0?"":a.substring(0,s))+a.substring(c+1):s=c}}return a===""?null:a}function ar(r,e=!1){var f=e?" !important;":";",a="";for(var u of Object.keys(r)){var n=r[u];n!=null&&n!==""&&(a+=" "+u+": "+n+f)}return a}function K(r){return r[0]!=="-"||r[1]!=="-"?r.toLowerCase():r}function $r(r,e){if(e){var f="",a,u;if(Array.isArray(e)?(a=e[0],u=e[1]):a=e,r){r=String(r).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var n=!1,s=0,c=!1,l=[];a&&l.push(...Object.keys(a).map(K)),u&&l.push(...Object.keys(u).map(K));var d=0,o=-1;const N=r.length;for(var g=0;g<N;g++){var t=r[g];if(c?t==="/"&&r[g-1]==="*"&&(c=!1):n?n===t&&(n=!1):t==="/"&&r[g+1]==="*"?c=!0:t==='"'||t==="'"?n=t:t==="("?s++:t===")"&&s--,!c&&n===!1&&s===0){if(t===":"&&o===-1)o=g;else if(t===";"||g===N-1){if(o!==-1){var h=K(r.substring(d,o).trim());if(!l.includes(h)){t!==";"&&g++;var T=r.substring(d,g).trim();f+=" "+T+";"}}d=g+1,o=-1}}}}return a&&(f+=ar(a)),u&&(f+=ar(u,!0)),f=f.trim(),f===""?null:f}return r==null?null:String(r)}function Qr(r,e,f,a,u,n){var s=r.__className;if(A||s!==f||s===void 0){var c=Kr(f,a,n);(!A||c!==r.getAttribute("class"))&&(c==null?r.removeAttribute("class"):r.className=c),r.__className=f}else if(n&&u!==n)for(var l in n){var d=!!n[l];(u==null||d!==!!u[l])&&r.classList.toggle(l,d)}return n}const mr=Symbol("is custom element"),Gr=Symbol("is html"),Vr=Ur?"link":"LINK";function jr(r){if(A){var e=!1,f=()=>{if(!e){if(e=!0,r.hasAttribute("value")){var a=r.value;ir(r,"value",null),r.value=a}if(r.hasAttribute("checked")){var u=r.checked;ir(r,"checked",null),r.checked=u}}};r.__on_r=f,or(f),Fr()}}function ir(r,e,f,a){var u=Xr(r);A&&(u[e]=r.getAttribute(e),e==="src"||e==="srcset"||e==="href"&&r.nodeName===Vr)||u[e]!==(u[e]=f)&&(e==="loading"&&(r[zr]=f),f==null?r.removeAttribute(e):typeof f!="string"&&Jr(r).includes(e)?r[e]=f:r.setAttribute(e,f))}function Xr(r){return r.__attributes??(r.__attributes={[mr]:r.nodeName.includes("-"),[Gr]:r.namespaceURI===Rr})}var nr=new Map;function Jr(r){var e=r.getAttribute("is")||r.nodeName,f=nr.get(e);if(f)return f;nr.set(e,f=[]);for(var a,u=r,n=Element.prototype;n!==u;){a=Dr(u);for(var s in a)a[s].set&&f.push(s);u=kr(u)}return f}export{Qr as a,Zr as e,Wr as i,jr as r,ir as s,$r as t};
|
||||
1
frontend/build/_app/immutable/chunks/C48rM6KF.js
Normal file
1
frontend/build/_app/immutable/chunks/C48rM6KF.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{y as D,z as T,P as B,g,e as m,d as Y,A as y,B as M,D as N,C as U,k as h,l as x,E as z,F as C,v as G,i as $,G as q,S as w,L as F}from"./C4An0dnW.js";let S=!1;function Z(r){var n=S;try{return S=!1,[r(),S]}finally{S=n}}function H(r,n,t,d){var E;var f=!x||(t&z)!==0,v=(t&U)!==0,O=(t&q)!==0,a=d,c=!0,o=()=>(c&&(c=!1,a=O?h(d):d),a),u;if(v){var R=w in r||F in r;u=((E=D(r,n))==null?void 0:E.set)??(R&&n in r?e=>r[n]=e:void 0)}var _,I=!1;v?[_,I]=Z(()=>r[n]):_=r[n],_===void 0&&d!==void 0&&(_=o(),u&&(f&&T(),u(_)));var i;if(f?i=()=>{var e=r[n];return e===void 0?o():(c=!0,e)}:i=()=>{var e=r[n];return e!==void 0&&(a=void 0),e===void 0?a:e},f&&(t&B)===0)return i;if(u){var L=r.$$legacy;return(function(e,l){return arguments.length>0?((!f||!l||L||I)&&u(l?i():e),e):i()})}var P=!1,s=((t&C)!==0?G:$)(()=>(P=!1,i()));v&&g(s);var b=M;return(function(e,l){if(arguments.length>0){const A=l?g(s):f&&v?m(e):e;return Y(s,A),P=!0,a!==void 0&&(a=A),e}return y&&P||(b.f&N)!==0?s.v:g(s)})}export{H as p};
|
||||
1
frontend/build/_app/immutable/chunks/C4An0dnW.js
Normal file
1
frontend/build/_app/immutable/chunks/C4An0dnW.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/build/_app/immutable/chunks/C5aWxL5p.js
Normal file
1
frontend/build/_app/immutable/chunks/C5aWxL5p.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
var S=Object.defineProperty;var v=e=>{throw TypeError(e)};var O=(e,t,a)=>t in e?S(e,t,{enumerable:!0,configurable:!0,writable:!0,value:a}):e[t]=a;var g=(e,t,a)=>O(e,typeof t!="symbol"?t+"":t,a),U=(e,t,a)=>t.has(e)||v("Cannot "+a);var s=(e,t,a)=>(U(e,t,"read from private field"),a?a.call(e):t.get(e)),i=(e,t,a)=>t.has(e)?v("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,a);import{b as l,g as r,d as c,e as p}from"./C4An0dnW.js";var n,d,h,u,b,k,y,o;class x{constructor(){i(this,n,l(null));i(this,d,l(null));i(this,h,l(p([])));i(this,u,l(p([])));i(this,b,l(null));i(this,k,l(p([])));i(this,y,l(p([])));i(this,o,l(null));g(this,"maxActivityEntries",100)}get id(){return r(s(this,n))}set id(t){c(s(this,n),t,!0)}get clock(){return r(s(this,d))}set clock(t){c(s(this,d),t,!0)}get players(){return r(s(this,h))}set players(t){c(s(this,h),t,!0)}get tables(){return r(s(this,u))}set tables(t){c(s(this,u),t,!0)}get financials(){return r(s(this,b))}set financials(t){c(s(this,b),t,!0)}get activity(){return r(s(this,k))}set activity(t){c(s(this,k),t,!0)}get rankings(){return r(s(this,y))}set rankings(t){c(s(this,y),t,!0)}get balanceStatus(){return r(s(this,o))}set balanceStatus(t){c(s(this,o),t,!0)}get remainingPlayers(){return this.players.filter(t=>t.status==="active").length}get totalPlayers(){return this.players.length}get activeTables(){return this.tables.filter(t=>t.players.length>0).length}get isBalanced(){var t;return((t=this.balanceStatus)==null?void 0:t.is_balanced)??!0}handleMessage(t){switch(t.type){case"clock.tick":this.clock=t.data;break;case"clock.level_change":this.clock=t.data;break;case"clock.paused":this.clock&&(this.clock.is_paused=!0);break;case"clock.resumed":this.clock&&(this.clock.is_paused=!1);break;case"state.snapshot":this.loadFullState(t.data);break;case"player.registered":this.addOrUpdatePlayer(t.data);break;case"player.seated":this.addOrUpdatePlayer(t.data);break;case"player.bust":case"player.eliminated":this.addOrUpdatePlayer(t.data);break;case"player.rebuy":case"player.addon":this.addOrUpdatePlayer(t.data);break;case"player.moved":this.addOrUpdatePlayer(t.data);break;case"table.created":this.addOrUpdateTable(t.data);break;case"table.broken":this.removeTable(t.data.id);break;case"table.updated":this.addOrUpdateTable(t.data);break;case"financial.updated":this.financials=t.data;break;case"rankings.updated":this.rankings=t.data;break;case"balance.updated":this.balanceStatus=t.data;break;case"activity.new":this.addActivity(t.data);break;case"connected":console.log("tournament: connected to server");break;default:console.warn(`tournament: unknown message type: ${t.type}`)}}reset(){this.id=null,this.clock=null,this.players=[],this.tables=[],this.financials=null,this.activity=[],this.rankings=[],this.balanceStatus=null}loadFullState(t){this.id=t.id??this.id,this.clock=t.clock??null,this.players=t.players??[],this.tables=t.tables??[],this.financials=t.financials??null,this.activity=t.activity??[],this.rankings=t.rankings??[],this.balanceStatus=t.balance_status??null}addOrUpdatePlayer(t){const a=this.players.findIndex(f=>f.id===t.id);a>=0?this.players[a]=t:this.players.push(t)}addOrUpdateTable(t){const a=this.tables.findIndex(f=>f.id===t.id);a>=0?this.tables[a]=t:this.tables.push(t)}removeTable(t){this.tables=this.tables.filter(a=>a.id!==t)}addActivity(t){this.activity=[t,...this.activity].slice(0,this.maxActivityEntries)}}n=new WeakMap,d=new WeakMap,h=new WeakMap,u=new WeakMap,b=new WeakMap,k=new WeakMap,y=new WeakMap,o=new WeakMap;const w=new x;export{w as t};
|
||||
2
frontend/build/_app/immutable/chunks/CQQh_IlD.js
Normal file
2
frontend/build/_app/immutable/chunks/CQQh_IlD.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
var h=e=>{throw TypeError(e)};var m=(e,t,o)=>t.has(e)||h("Cannot "+o);var r=(e,t,o)=>(m(e,t,"read from private field"),o?o.call(e):t.get(e)),l=(e,t,o)=>t.has(e)?h("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o);import{s as c,g,a as u}from"./Ym0WvvUy.js";const n="felt_token",i="felt_operator";var a,s;class p{constructor(){l(this,a,c(null));l(this,s,c(null));typeof window<"u"&&this.loadFromStorage()}get token(){return g(r(this,a))}set token(t){u(r(this,a),t,!0)}get operator(){return g(r(this,s))}set operator(t){u(r(this,s),t,!0)}get isAuthenticated(){return this.token!==null}get isAdmin(){var t;return((t=this.operator)==null?void 0:t.role)==="admin"}get isFloor(){var t;return["admin","floor"].includes(((t=this.operator)==null?void 0:t.role)??"")}login(t,o){this.token=t,this.operator=o,this.saveToStorage()}logout(){this.token=null,this.operator=null,this.clearStorage()}loadFromStorage(){try{const t=localStorage.getItem(n),o=localStorage.getItem(i);t&&o&&(this.token=t,this.operator=JSON.parse(o))}catch(t){console.warn("auth: failed to load from storage:",t),this.clearStorage()}}saveToStorage(){try{this.token&&this.operator&&(localStorage.setItem(n,this.token),localStorage.setItem(i,JSON.stringify(this.operator)))}catch(t){console.warn("auth: failed to save to storage:",t)}}clearStorage(){try{localStorage.removeItem(n),localStorage.removeItem(i)}catch(t){console.warn("auth: failed to clear storage:",t)}}}a=new WeakMap,s=new WeakMap;const f=new p;export{f as a};
|
||||
var h=e=>{throw TypeError(e)};var m=(e,t,o)=>t.has(e)||h("Cannot "+o);var r=(e,t,o)=>(m(e,t,"read from private field"),o?o.call(e):t.get(e)),l=(e,t,o)=>t.has(e)?h("Cannot add the same private member more than once"):t instanceof WeakSet?t.add(e):t.set(e,o);import{b as c,g,d as u}from"./C4An0dnW.js";const n="felt_token",i="felt_operator";var a,s;class p{constructor(){l(this,a,c(null));l(this,s,c(null));typeof window<"u"&&this.loadFromStorage()}get token(){return g(r(this,a))}set token(t){u(r(this,a),t,!0)}get operator(){return g(r(this,s))}set operator(t){u(r(this,s),t,!0)}get isAuthenticated(){return this.token!==null}get isAdmin(){var t;return((t=this.operator)==null?void 0:t.role)==="admin"}get isFloor(){var t;return["admin","floor"].includes(((t=this.operator)==null?void 0:t.role)??"")}login(t,o){this.token=t,this.operator=o,this.saveToStorage()}logout(){this.token=null,this.operator=null,this.clearStorage()}loadFromStorage(){try{const t=localStorage.getItem(n),o=localStorage.getItem(i);t&&o&&(this.token=t,this.operator=JSON.parse(o))}catch(t){console.warn("auth: failed to load from storage:",t),this.clearStorage()}}saveToStorage(){try{this.token&&this.operator&&(localStorage.setItem(n,this.token),localStorage.setItem(i,JSON.stringify(this.operator)))}catch(t){console.warn("auth: failed to save to storage:",t)}}clearStorage(){try{localStorage.removeItem(n),localStorage.removeItem(i)}catch(t){console.warn("auth: failed to clear storage:",t)}}}a=new WeakMap,s=new WeakMap;const f=new p;export{f as a};
|
||||
1
frontend/build/_app/immutable/chunks/DQNCp18R.js
Normal file
1
frontend/build/_app/immutable/chunks/DQNCp18R.js
Normal file
File diff suppressed because one or more lines are too long
1
frontend/build/_app/immutable/chunks/D__6P984.js
Normal file
1
frontend/build/_app/immutable/chunks/D__6P984.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
var I=Object.defineProperty;var R=a=>{throw TypeError(a)};var x=(a,e,t)=>e in a?I(a,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):a[e]=t;var w=(a,e,t)=>x(a,typeof e!="symbol"?e+"":e,t),E=(a,e,t)=>e.has(a)||R("Cannot "+t);var s=(a,e,t)=>(E(a,e,"read from private field"),t?t.call(a):e.get(a)),_=(a,e,t)=>e.has(a)?R("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(a):e.set(a,t),M=(a,e,t,i)=>(E(a,e,"write to private field"),i?i.call(a,t):e.set(a,t),t);import{H,I as T,J as O,K as N,M as S,N as Y,h as A,O as F,Q as B,R as C,T as J,U as K,V as L,W as P,X as Q,Y as U,Z as V,_ as W,$ as D}from"./C4An0dnW.js";var l,u,h,p,v,m,g;class X{constructor(e,t=!0){w(this,"anchor");_(this,l,new Map);_(this,u,new Map);_(this,h,new Map);_(this,p,new Set);_(this,v,!0);_(this,m,e=>{if(s(this,l).has(e)){var t=s(this,l).get(e),i=s(this,u).get(t);if(i)H(i),s(this,p).delete(t);else{var n=s(this,h).get(t);n&&(s(this,u).set(t,n.effect),s(this,h).delete(t),n.fragment.lastChild.remove(),this.anchor.before(n.fragment),i=n.effect)}for(const[f,c]of s(this,l)){if(s(this,l).delete(f),f===e)break;const r=s(this,h).get(c);r&&(T(r.effect),s(this,h).delete(c))}for(const[f,c]of s(this,u)){if(f===t||s(this,p).has(f))continue;const r=()=>{if(Array.from(s(this,l).values()).includes(f)){var d=document.createDocumentFragment();B(c,d),d.append(N()),s(this,h).set(f,{effect:c,fragment:d})}else T(c);s(this,p).delete(f),s(this,u).delete(f)};s(this,v)||!i?(s(this,p).add(f),O(c,r,!1)):r()}}});_(this,g,e=>{s(this,l).delete(e);const t=Array.from(s(this,l).values());for(const[i,n]of s(this,h))t.includes(i)||(T(n.effect),s(this,h).delete(i))});this.anchor=e,M(this,v,t)}ensure(e,t){var i=Y,n=C();if(t&&!s(this,u).has(e)&&!s(this,h).has(e))if(n){var f=document.createDocumentFragment(),c=N();f.append(c),s(this,h).set(e,{effect:S(()=>t(c)),fragment:f})}else s(this,u).set(e,S(()=>t(this.anchor)));if(s(this,l).set(i,e),n){for(const[r,o]of s(this,u))r===e?i.unskip_effect(o):i.skip_effect(o);for(const[r,o]of s(this,h))r===e?i.unskip_effect(o.effect):i.skip_effect(o.effect);i.oncommit(s(this,m)),i.ondiscard(s(this,g))}else A&&(this.anchor=F),s(this,m).call(this,i)}}l=new WeakMap,u=new WeakMap,h=new WeakMap,p=new WeakMap,v=new WeakMap,m=new WeakMap,g=new WeakMap;function j(a,e,t=!1){var i;A&&(i=F,K());var n=new X(a),f=t?L:0;function c(r,o){if(A){var d=P(i),b;if(d===Q?b=0:d===U?b=!1:b=parseInt(d.substring(1)),r!==b){var k=V();W(k),n.anchor=k,D(!1),n.ensure(r,o),D(!0);return}}n.ensure(r,o)}J(()=>{var r=!1;e((o,d=0)=>{r=!0,c(d,o)}),r||c(!1,null)},f)}export{X as B,j as i};
|
||||
|
|
@ -1 +0,0 @@
|
|||
var B=Object.defineProperty;var g=i=>{throw TypeError(i)};var D=(i,e,s)=>e in i?B(i,e,{enumerable:!0,configurable:!0,writable:!0,value:s}):i[e]=s;var w=(i,e,s)=>D(i,typeof e!="symbol"?e+"":e,s),y=(i,e,s)=>e.has(i)||g("Cannot "+s);var t=(i,e,s)=>(y(i,e,"read from private field"),s?s.call(i):e.get(i)),l=(i,e,s)=>e.has(i)?g("Cannot add the same private member more than once"):e instanceof WeakSet?e.add(i):e.set(i,s),M=(i,e,s,a)=>(y(i,e,"write to private field"),a?a.call(i,s):e.set(i,s),s);import{k as F,l as b,p as j,m as x,n as A,o as q,h as C,j as S,q as z,t as E}from"./Ym0WvvUy.js";var h,n,r,u,p,_,v;class I{constructor(e,s=!0){w(this,"anchor");l(this,h,new Map);l(this,n,new Map);l(this,r,new Map);l(this,u,new Set);l(this,p,!0);l(this,_,e=>{if(t(this,h).has(e)){var s=t(this,h).get(e),a=t(this,n).get(s);if(a)F(a),t(this,u).delete(s);else{var c=t(this,r).get(s);c&&(t(this,n).set(s,c.effect),t(this,r).delete(s),c.fragment.lastChild.remove(),this.anchor.before(c.fragment),a=c.effect)}for(const[f,o]of t(this,h)){if(t(this,h).delete(f),f===e)break;const d=t(this,r).get(o);d&&(b(d.effect),t(this,r).delete(o))}for(const[f,o]of t(this,n)){if(f===s||t(this,u).has(f))continue;const d=()=>{if(Array.from(t(this,h).values()).includes(f)){var k=document.createDocumentFragment();z(o,k),k.append(x()),t(this,r).set(f,{effect:o,fragment:k})}else b(o);t(this,u).delete(f),t(this,n).delete(f)};t(this,p)||!a?(t(this,u).add(f),j(o,d,!1)):d()}}});l(this,v,e=>{t(this,h).delete(e);const s=Array.from(t(this,h).values());for(const[a,c]of t(this,r))s.includes(a)||(b(c.effect),t(this,r).delete(a))});this.anchor=e,M(this,p,s)}ensure(e,s){var a=q,c=E();if(s&&!t(this,n).has(e)&&!t(this,r).has(e))if(c){var f=document.createDocumentFragment(),o=x();f.append(o),t(this,r).set(e,{effect:A(()=>s(o)),fragment:f})}else t(this,n).set(e,A(()=>s(this.anchor)));if(t(this,h).set(a,e),c){for(const[d,m]of t(this,n))d===e?a.unskip_effect(m):a.skip_effect(m);for(const[d,m]of t(this,r))d===e?a.unskip_effect(m.effect):a.skip_effect(m.effect);a.oncommit(t(this,_)),a.ondiscard(t(this,v))}else C&&(this.anchor=S),t(this,_).call(this,a)}}h=new WeakMap,n=new WeakMap,r=new WeakMap,u=new WeakMap,p=new WeakMap,_=new WeakMap,v=new WeakMap;export{I as B};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{B as g,C as d,D as c,F as m,G as i,I as b,g as p,J as v,K as h,L as k}from"./Ym0WvvUy.js";function x(n=!1){const s=g,e=s.l.u;if(!e)return;let f=()=>v(s.s);if(n){let a=0,t={};const _=h(()=>{let l=!1;const r=s.s;for(const o in r)r[o]!==t[o]&&(t[o]=r[o],l=!0);return l&&a++,a});f=()=>p(_)}e.b.length&&d(()=>{u(s,f),i(e.b)}),c(()=>{const a=m(()=>e.m.map(b));return()=>{for(const t of a)typeof t=="function"&&t()}}),e.a.length&&c(()=>{u(s,f),i(e.a)})}function u(n,s){if(n.l.s)for(const e of n.l.s)p(e);s()}k();export{x as i};
|
||||
1
frontend/build/_app/immutable/chunks/DyXP65qD.js
Normal file
1
frontend/build/_app/immutable/chunks/DyXP65qD.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{s as e,p as r}from"./DQNCp18R.js";const t={get error(){return r.error},get status(){return r.status},get url(){return r.url}};e.updated.check;const a=t;export{a as p};
|
||||
1
frontend/build/_app/immutable/chunks/Q5CB4WY5.js
Normal file
1
frontend/build/_app/immutable/chunks/Q5CB4WY5.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{av as p,K as u,a6 as l,aw as E,B as c,ax as w,ay as g,h as d,O as s,az as y,U as N,aA as x,_ as A,aB as M}from"./C4An0dnW.js";var f;const i=((f=globalThis==null?void 0:globalThis.window)==null?void 0:f.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:t=>t});function L(t){return(i==null?void 0:i.createHTML(t))??t}function O(t){var r=p("template");return r.innerHTML=L(t.replaceAll("<!>","<!---->")),r.content}function n(t,r){var e=c;e.nodes===null&&(e.nodes={start:t,end:r,a:null,t:null})}function R(t,r){var e=(r&w)!==0,m=(r&g)!==0,a,v=!t.startsWith("<!>");return()=>{if(d)return n(s,null),s;a===void 0&&(a=O(v?t:"<!>"+t),e||(a=l(a)));var o=m||E?document.importNode(a,!0):a.cloneNode(!0);if(e){var T=l(o),h=o.lastChild;n(T,h)}else n(o,o);return o}}function C(t=""){if(!d){var r=u(t+"");return n(r,r),r}var e=s;return e.nodeType!==x?(e.before(e=u()),A(e)):M(e),n(e,e),e}function I(){if(d)return n(s,null),s;var t=document.createDocumentFragment(),r=document.createComment(""),e=u();return t.append(r,e),n(r,e),t}function B(t,r){if(d){var e=c;((e.f&y)===0||e.nodes.end===null)&&(e.nodes.end=s),N();return}t!==null&&t.before(r)}const b="5";var _;typeof window<"u"&&((_=window.__svelte??(window.__svelte={})).v??(_.v=new Set)).add(b);export{B as a,n as b,I as c,R as f,C as t};
|
||||
1
frontend/build/_app/immutable/chunks/WPMya0VZ.js
Normal file
1
frontend/build/_app/immutable/chunks/WPMya0VZ.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{D as o,B as t,M as c,F as u}from"./Ym0WvvUy.js";function l(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(n){t===null&&l(),c&&t.l!==null?a(t).m.push(n):o(()=>{const e=u(n);if(typeof e=="function")return e})}function a(n){var e=n.l;return e.u??(e.u={a:[],b:[],m:[]})}export{r as o};
|
||||
import{u as o,j as t,l as c,k as u}from"./C4An0dnW.js";function l(n){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function r(n){t===null&&l(),c&&t.l!==null?a(t).m.push(n):o(()=>{const e=u(n);if(typeof e=="function")return e})}function a(n){var e=n.l;return e.u??(e.u={a:[],b:[],m:[]})}export{r as o};
|
||||
File diff suppressed because one or more lines are too long
2
frontend/build/_app/immutable/entry/app.Dwn0pdp1.js
Normal file
2
frontend/build/_app/immutable/entry/app.Dwn0pdp1.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{l as o,a as r}from"../chunks/giww_vF6.js";export{o as load_css,r as start};
|
||||
1
frontend/build/_app/immutable/entry/start.Do4A91T6.js
Normal file
1
frontend/build/_app/immutable/entry/start.Do4A91T6.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{l as o,a as r}from"../chunks/DQNCp18R.js";export{o as load_css,r as start};
|
||||
1
frontend/build/_app/immutable/nodes/0.C1R4dMGA.js
Normal file
1
frontend/build/_app/immutable/nodes/0.C1R4dMGA.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +0,0 @@
|
|||
import{c as s,a as c}from"../chunks/B6M6q2Zo.js";import{b as l,E as p,v as i}from"../chunks/Ym0WvvUy.js";import{B as m}from"../chunks/Da6yQRl8.js";function u(n,r,...e){var o=new m(n);l(()=>{const t=r()??null;o.ensure(t,t&&(a=>t(a,...e)))},p)}const f=!0,_=!1,g=Object.freeze(Object.defineProperty({__proto__:null,prerender:f,ssr:_},Symbol.toStringTag,{value:"Module"}));function h(n,r){var e=s(),o=i(e);u(o,()=>r.children),c(n,e)}export{h as component,g as universal};
|
||||
1
frontend/build/_app/immutable/nodes/1.CNv_pgkw.js
Normal file
1
frontend/build/_app/immutable/nodes/1.CNv_pgkw.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as c,f as l}from"../chunks/Q5CB4WY5.js";import{i as v}from"../chunks/BViIIwgj.js";import{p as u,f as _,t as g,a as x,c as e,r as o,s as d}from"../chunks/C4An0dnW.js";import{s as p}from"../chunks/CQQh_IlD.js";import{p as m}from"../chunks/DyXP65qD.js";var b=l("<h1> </h1> <p> </p>",1);function y(f,i){u(i,!1),v();var t=b(),r=_(t),n=e(r,!0);o(r);var a=d(r,2),h=e(a,!0);o(a),g(()=>{var s;p(n,m.status),p(h,(s=m.error)==null?void 0:s.message)}),c(f,t),x()}export{y as component};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{a as h,f as g}from"../chunks/B6M6q2Zo.js";import{i as v}from"../chunks/De6rLmuB.js";import{u as l,v as d,A as x,w as _,y as e,z as o,x as $}from"../chunks/Ym0WvvUy.js";import{s as p}from"../chunks/dTRRgeF-.js";import{s as k,p as m}from"../chunks/giww_vF6.js";const b={get error(){return m.error},get status(){return m.status}};k.updated.check;const i=b;var w=g("<h1> </h1> <p> </p>",1);function q(f,n){l(n,!1),v();var t=w(),r=d(t),c=e(r,!0);o(r);var s=$(r,2),u=e(s,!0);o(s),x(()=>{var a;p(c,i.status),p(u,(a=i.error)==null?void 0:a.message)}),h(f,t),_()}export{q as component};
|
||||
1
frontend/build/_app/immutable/nodes/2.C9GK89sD.js
Normal file
1
frontend/build/_app/immutable/nodes/2.C9GK89sD.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as t,f as p}from"../chunks/Q5CB4WY5.js";import{i as e}from"../chunks/BViIIwgj.js";import{o as i}from"../chunks/nIaoZoCo.js";import{p as m,a as s}from"../chunks/C4An0dnW.js";import{g as f}from"../chunks/DQNCp18R.js";var n=p('<div class="redirect svelte-1uha8ag"><p>Loading...</p></div>');function u(o,a){m(a,!1),i(()=>{f("/overview",{replaceState:!0})}),e();var r=n();t(o,r),s()}export{u as component};
|
||||
|
|
@ -1 +0,0 @@
|
|||
import{c as u,a as e,f as p}from"../chunks/B6M6q2Zo.js";import{i as _}from"../chunks/De6rLmuB.js";import{o as d}from"../chunks/Bfwrz3i4.js";import{u as x,v as y,w as A,x as b,y as m,z as n,A as k}from"../chunks/Ym0WvvUy.js";import{s as w}from"../chunks/dTRRgeF-.js";import{i as z}from"../chunks/B9dvBo0E.js";import{a as o}from"../chunks/DMqvp7vx.js";import{g as F}from"../chunks/giww_vF6.js";var M=p('<main class="container svelte-1uha8ag"><h1 class="svelte-1uha8ag">Felt</h1> <p class="text-secondary svelte-1uha8ag">Tournament management system</p> <p> </p></main>'),O=p('<main class="container svelte-1uha8ag"><p>Redirecting to login...</p></main>');function E(l,f){x(f,!1),d(()=>{o.isAuthenticated||F("/login")}),_();var s=u(),c=y(s);{var v=a=>{var t=M(),r=b(m(t),4),h=m(r);n(r),n(t),k(()=>{var i;return w(h,`Welcome, ${((i=o.operator)==null?void 0:i.name)??"Operator"??""}`)}),e(a,t)},g=a=>{var t=O();e(a,t)};z(c,a=>{o.isAuthenticated?a(v):a(g,!1)})}e(l,s),A()}export{E as component};
|
||||
1
frontend/build/_app/immutable/nodes/3.BMSY6fJC.js
Normal file
1
frontend/build/_app/immutable/nodes/3.BMSY6fJC.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as b,f as _}from"../chunks/Q5CB4WY5.js";import{i as G}from"../chunks/BViIIwgj.js";import{p as I,a as J,s,c as a,r as e,t as K,g as c,i as M}from"../chunks/C4An0dnW.js";import{s as l}from"../chunks/CQQh_IlD.js";import{i as O}from"../chunks/D__6P984.js";import{t as P}from"../chunks/C5aWxL5p.js";var Q=_('<div class="finance-grid svelte-1ba4c5d"><div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Buy-ins</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Rebuys</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Total Add-ons</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card highlight svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Prize Pool</span> <span class="finance-value currency prize svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">House Fee</span> <span class="finance-value currency svelte-1ba4c5d"> </span></div> <div class="finance-card svelte-1ba4c5d"><span class="finance-label svelte-1ba4c5d">Paid Positions</span> <span class="finance-value number svelte-1ba4c5d"> </span></div></div>'),U=_('<p class="empty-state svelte-1ba4c5d">No financial data available yet.</p>'),V=_('<div class="page-content svelte-1ba4c5d"><h2 class="svelte-1ba4c5d">Financials</h2> <p class="text-secondary svelte-1ba4c5d">Prize pool and payout information.</p> <!></div>');function ea(S,z){I(z,!1),G();var i=V(),T=s(a(i),4);{var F=t=>{const n=M(()=>P.financials);var r=Q(),v=a(r),u=s(a(v),2),q=a(u,!0);e(u),e(v);var d=s(v,2),m=s(a(d),2),A=a(m,!0);e(m),e(d);var o=s(d,2),g=s(a(o),2),B=a(g,!0);e(g),e(o);var p=s(o,2),y=s(a(p),2),H=a(y,!0);e(y),e(p);var f=s(p,2),h=s(a(f),2),N=a(h,!0);e(h),e(f);var x=s(f,2),L=s(a(x),2),R=a(L,!0);e(L),e(x),e(r),K((j,w,C,D,E)=>{l(q,j),l(A,w),l(B,C),l(H,D),l(N,E),l(R,c(n).paid_positions)},[()=>c(n).total_buyin.toLocaleString(),()=>c(n).total_rebuys.toLocaleString(),()=>c(n).total_addons.toLocaleString(),()=>c(n).prize_pool.toLocaleString(),()=>c(n).house_fee.toLocaleString()]),b(t,r)},k=t=>{var n=U();b(t,n)};O(T,t=>{P.financials?t(F):t(k,!1)})}e(i),b(S,i),J()}export{ea as component};
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
import{a as P,f as K,t as ue}from"../chunks/B6M6q2Zo.js";import{o as ke}from"../chunks/Bfwrz3i4.js";import{m as V,b as Ne,av as we,h as I,f as X,P as ze,c as Ce,g as h,r as Se,d as Ie,e as ce,i as j,j as G,am as ye,at as Me,ac as de,o as De,aw as y,n as Z,ax as He,t as Le,ay as Oe,az as Pe,as as ae,aA as Re,aB as Fe,aC as Be,Y as ve,aD as Ue,k as Ae,p as Te,aE as Q,_ as $e,aF as qe,aG as Ge,aq as Ye,l as Je,an as Ke,aH as Ve,aI as Xe,aJ as je,u as Qe,A as Y,w as We,aK as Ze,y as R,x as L,z as O,s as W,a as w}from"../chunks/Ym0WvvUy.js";import{d as er,e as rr,a as B,s as pe}from"../chunks/dTRRgeF-.js";import{i as ge}from"../chunks/B9dvBo0E.js";import{a as J}from"../chunks/DMqvp7vx.js";import{g as ee}from"../chunks/giww_vF6.js";function he(e,t){return t}function tr(e,t,r){for(var a=[],i=t.length,l,s=t.length,d=0;d<i;d++){let m=t[d];Te(m,()=>{if(l){if(l.pending.delete(m),l.done.add(m),l.pending.size===0){var g=e.outrogroups;re(ae(l.done)),g.delete(l),g.size===0&&(e.outrogroups=null)}}else s-=1},!1)}if(s===0){var f=a.length===0&&r!==null;if(f){var p=r,c=p.parentNode;Ye(c),c.append(p),e.items.clear()}re(t,!f)}else l={pending:new Set(t),done:new Set},(e.outrogroups??(e.outrogroups=new Set)).add(l)}function re(e,t=!0){for(var r=0;r<e.length;r++)Je(e[r],t)}var _e;function me(e,t,r,a,i,l=null){var s=e,d=new Map,f=(t&we)!==0;if(f){var p=e;s=I?X(ze(p)):p.appendChild(V())}I&&Ce();var c=null,m=Oe(()=>{var u=r();return Pe(u)?u:u==null?[]:ae(u)}),g,_=!0;function T(){n.fallback=c,ar(n,g,s,t,a),c!==null&&(g.length===0?(c.f&y)===0?Ae(c):(c.f^=y,q(c,null,s)):Te(c,()=>{c=null}))}var M=Ne(()=>{g=h(m);var u=g.length;let k=!1;if(I){var N=Se(s)===Ie;N!==(u===0)&&(s=ce(),X(s),j(!1),k=!0)}for(var b=new Set,A=De,D=Le(),E=0;E<u;E+=1){I&&G.nodeType===ye&&G.data===Me&&(s=G,k=!0,j(!1));var z=g[E],o=a(z,E),v=_?null:d.get(o);v?(v.v&&de(v.v,z),v.i&&de(v.i,E),D&&A.unskip_effect(v.e)):(v=nr(d,_?s:_e??(_e=V()),z,o,E,i,t,r),_||(v.e.f|=y),d.set(o,v)),b.add(o)}if(u===0&&l&&!c&&(_?c=Z(()=>l(s)):(c=Z(()=>l(_e??(_e=V()))),c.f|=y)),u>b.size&&He(),I&&u>0&&X(ce()),!_)if(D){for(const[x,C]of d)b.has(x)||A.skip_effect(C.e);A.oncommit(T),A.ondiscard(()=>{})}else T();k&&j(!0),h(m)}),n={effect:M,items:d,outrogroups:null,fallback:c};_=!1,I&&(s=G)}function U(e){for(;e!==null&&(e.f&qe)===0;)e=e.next;return e}function ar(e,t,r,a,i){var v,x,C,F,ne,ie,se,oe,le;var l=(a&Ge)!==0,s=t.length,d=e.items,f=U(e.effect.first),p,c=null,m,g=[],_=[],T,M,n,u;if(l)for(u=0;u<s;u+=1)T=t[u],M=i(T,u),n=d.get(M).e,(n.f&y)===0&&((x=(v=n.nodes)==null?void 0:v.a)==null||x.measure(),(m??(m=new Set)).add(n));for(u=0;u<s;u+=1){if(T=t[u],M=i(T,u),n=d.get(M).e,e.outrogroups!==null)for(const S of e.outrogroups)S.pending.delete(n),S.done.delete(n);if((n.f&y)!==0)if(n.f^=y,n===f)q(n,null,r);else{var k=c?c.next:f;n===e.effect.last&&(e.effect.last=n.prev),n.prev&&(n.prev.next=n.next),n.next&&(n.next.prev=n.prev),H(e,c,n),H(e,n,k),q(n,k,r),c=n,g=[],_=[],f=U(c.next);continue}if((n.f&Q)!==0&&(Ae(n),l&&((F=(C=n.nodes)==null?void 0:C.a)==null||F.unfix(),(m??(m=new Set)).delete(n))),n!==f){if(p!==void 0&&p.has(n)){if(g.length<_.length){var N=_[0],b;c=N.prev;var A=g[0],D=g[g.length-1];for(b=0;b<g.length;b+=1)q(g[b],N,r);for(b=0;b<_.length;b+=1)p.delete(_[b]);H(e,A.prev,D.next),H(e,c,A),H(e,D,N),f=N,c=D,u-=1,g=[],_=[]}else p.delete(n),q(n,f,r),H(e,n.prev,n.next),H(e,n,c===null?e.effect.first:c.next),H(e,c,n),c=n;continue}for(g=[],_=[];f!==null&&f!==n;)(p??(p=new Set)).add(f),_.push(f),f=U(f.next);if(f===null)continue}(n.f&y)===0&&g.push(n),c=n,f=U(n.next)}if(e.outrogroups!==null){for(const S of e.outrogroups)S.pending.size===0&&(re(ae(S.done)),(ne=e.outrogroups)==null||ne.delete(S));e.outrogroups.size===0&&(e.outrogroups=null)}if(f!==null||p!==void 0){var E=[];if(p!==void 0)for(n of p)(n.f&Q)===0&&E.push(n);for(;f!==null;)(f.f&Q)===0&&f!==e.fallback&&E.push(f),f=U(f.next);var z=E.length;if(z>0){var o=(a&we)!==0&&s===0?r:null;if(l){for(u=0;u<z;u+=1)(se=(ie=E[u].nodes)==null?void 0:ie.a)==null||se.measure();for(u=0;u<z;u+=1)(le=(oe=E[u].nodes)==null?void 0:oe.a)==null||le.fix()}tr(e,E,o)}}l&&$e(()=>{var S,fe;if(m!==void 0)for(n of m)(fe=(S=n.nodes)==null?void 0:S.a)==null||fe.apply()})}function nr(e,t,r,a,i,l,s,d){var f=(s&Re)!==0?(s&Fe)===0?Be(r,!1,!1):ve(r):null,p=(s&Ue)!==0?ve(i):null;return{v:f,i:p,e:Z(()=>(l(t,f??r,p??i,d),()=>{e.delete(a)}))}}function q(e,t,r){if(e.nodes)for(var a=e.nodes.start,i=e.nodes.end,l=t&&(t.f&y)===0?t.nodes.start:r;a!==null;){var s=Ke(a);if(l.before(a),a===i)return;a=s}}function H(e,t,r){t===null?e.effect.first=r:t.next=r,r===null?e.effect.last=t:r.prev=t}const be=[...`
|
||||
\r\f \v\uFEFF`];function ir(e,t,r){var a=""+e;if(r){for(var i of Object.keys(r))if(r[i])a=a?a+" "+i:i;else if(a.length)for(var l=i.length,s=0;(s=a.indexOf(i,s))>=0;){var d=s+l;(s===0||be.includes(a[s-1]))&&(d===a.length||be.includes(a[d]))?a=(s===0?"":a.substring(0,s))+a.substring(d+1):s=d}}return a===""?null:a}function sr(e,t,r,a,i,l){var s=e.__className;if(I||s!==r||s===void 0){var d=ir(r,a,l);(!I||d!==e.getAttribute("class"))&&(d==null?e.removeAttribute("class"):e.className=d),e.__className=r}else if(l&&i!==l)for(var f in l){var p=!!l[f];(i==null||p!==!!i[f])&&e.classList.toggle(f,p)}return l}const or=Symbol("is custom element"),lr=Symbol("is html");function Ee(e,t,r,a){var i=fr(e);I&&(i[t]=e.getAttribute(t)),i[t]!==(i[t]=r)&&(r==null?e.removeAttribute(t):typeof r!="string"&&ur(e).includes(t)?e[t]=r:e.setAttribute(t,r))}function fr(e){return e.__attributes??(e.__attributes={[or]:e.nodeName.includes("-"),[lr]:e.namespaceURI===Ve})}var xe=new Map;function ur(e){var t=e.getAttribute("is")||e.nodeName,r=xe.get(t);if(r)return r;xe.set(t,r=[]);for(var a,i=e,l=Element.prototype;l!==i;){a=je(i);for(var s in a)a[s].set&&r.push(s);i=Xe(i)}return r}class te extends Error{constructor(t,r,a){const i=typeof a=="object"&&a!==null&&"error"in a?a.error:r;super(i),this.status=t,this.statusText=r,this.body=a,this.name="ApiError"}}function cr(){return`${window.location.origin}/api/v1`}function dr(e){const t={Accept:"application/json"};e&&(t["Content-Type"]="application/json");const r=J.token;return r&&(t.Authorization=`Bearer ${r}`),t}async function vr(e){if(e.status===401)throw J.logout(),await ee("/login"),new te(401,"Unauthorized",{error:"Session expired"});if(!e.ok){let t;try{t=await e.json()}catch{t={error:e.statusText}}throw new te(e.status,e.statusText,t)}if(e.status!==204)return e.json()}async function $(e,t,r){const a=`${cr()}${t}`,i={method:e,headers:dr(r!==void 0),credentials:"same-origin"};r!==void 0&&(i.body=JSON.stringify(r));const l=await fetch(a,i);return vr(l)}const pr={get(e){return $("GET",e)},post(e,t){return $("POST",e,t)},put(e,t){return $("PUT",e,t)},patch(e,t){return $("PATCH",e,t)},delete(e){return $("DELETE",e)}};var gr=K("<div></div>"),hr=K('<div class="error-message svelte-1x05zx6" role="alert"> </div>'),_r=K('<button class="numpad-btn touch-target svelte-1x05zx6"> </button>'),mr=K('<main class="login-container svelte-1x05zx6"><div class="login-card svelte-1x05zx6"><div class="logo svelte-1x05zx6"><h1 class="svelte-1x05zx6">Felt</h1> <p class="subtitle svelte-1x05zx6">Tournament Manager</p></div> <div class="pin-display svelte-1x05zx6" role="status"></div> <!> <div class="numpad svelte-1x05zx6"><!> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Clear PIN">CLR</button> <button class="numpad-btn touch-target svelte-1x05zx6" aria-label="Digit 0">0</button> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Delete last digit">DEL</button></div> <button class="submit-btn touch-target svelte-1x05zx6"><!></button></div></main>');function Nr(e,t){Qe(t,!0);let r=W(""),a=W(""),i=W(!1);const l=6;ke(()=>{J.isAuthenticated&&ee("/")});function s(o){h(r).length>=l||(w(r,h(r)+o),w(a,""))}function d(){w(r,h(r).slice(0,-1),!0),w(a,"")}function f(){w(r,""),w(a,"")}async function p(){if(!(h(r).length<4||h(i))){w(i,!0),w(a,"");try{const o=await pr.post("/auth/login",{pin:h(r)});J.login(o.token,{id:o.operator.id,name:o.operator.name,role:o.operator.role}),await ee("/")}catch(o){o instanceof te?o.status===429?w(a,"Too many attempts. Please wait."):o.status===401?w(a,"Invalid PIN. Try again."):w(a,o.message,!0):w(a,"Connection error. Check your network."),w(r,"")}finally{w(i,!1)}}}function c(o){o.key>="0"&&o.key<="9"?s(o.key):o.key==="Backspace"?d():o.key==="Enter"?p():o.key==="Escape"&&f()}var m=mr();rr("keydown",Ze,c);var g=R(m),_=L(R(g),2);me(_,21,()=>Array(l),he,(o,v,x)=>{var C=gr();let F;Y(()=>F=sr(C,1,"pin-dot svelte-1x05zx6",null,F,{filled:x<h(r).length})),P(o,C)}),O(_);var T=L(_,2);{var M=o=>{var v=hr(),x=R(v,!0);O(v),Y(()=>pe(x,h(a))),P(o,v)};ge(T,o=>{h(a)&&o(M)})}var n=L(T,2),u=R(n);me(u,16,()=>["1","2","3","4","5","6","7","8","9"],he,(o,v)=>{var x=_r(),C=R(x,!0);O(x),Y(()=>{x.disabled=h(i)||h(r).length>=l,Ee(x,"aria-label",`Digit ${v??""}`),pe(C,v)}),B("click",x,()=>s(v)),P(o,x)});var k=L(u,2),N=L(k,2),b=L(N,2);O(n);var A=L(n,2),D=R(A);{var E=o=>{var v=ue("Signing in...");P(o,v)},z=o=>{var v=ue("Sign In");P(o,v)};ge(D,o=>{h(i)?o(E):o(z,!1)})}O(A),O(g),O(m),Y(()=>{Ee(_,"aria-label",`PIN entered: ${h(r).length??""} digits`),k.disabled=h(i),N.disabled=h(i)||h(r).length>=l,b.disabled=h(i)||h(r).length===0,A.disabled=h(r).length<4||h(i)}),B("click",k,f),B("click",N,()=>s("0")),B("click",b,d),B("click",A,p),P(e,m),We()}er(["click"]);export{Nr as component};
|
||||
1
frontend/build/_app/immutable/nodes/4.Ct0ahWmg.js
Normal file
1
frontend/build/_app/immutable/nodes/4.Ct0ahWmg.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as f,f as y,t as M}from"../chunks/Q5CB4WY5.js";import{o as V}from"../chunks/nIaoZoCo.js";import{p as W,t as _,a as Y,a0 as Z,c as p,s as u,r as d,g as r,b as A,d as o}from"../chunks/C4An0dnW.js";import{d as tt,e as et,a as g,s as U}from"../chunks/CQQh_IlD.js";import{i as G}from"../chunks/D__6P984.js";import{e as O,i as R,s as q,a as at}from"../chunks/BeLKMLqR.js";import{a as k}from"../chunks/D3f6eoxz.js";import{g as D}from"../chunks/DQNCp18R.js";class C extends Error{constructor(s,a,i){const n=typeof i=="object"&&i!==null&&"error"in i?i.error:a;super(n),this.status=s,this.statusText=a,this.body=i,this.name="ApiError"}}function rt(){return`${window.location.origin}/api/v1`}function st(e){const s={Accept:"application/json"};e&&(s["Content-Type"]="application/json");const a=k.token;return a&&(s.Authorization=`Bearer ${a}`),s}async function it(e){if(e.status===401)throw k.logout(),await D("/login"),new C(401,"Unauthorized",{error:"Session expired"});if(!e.ok){let s;try{s=await e.json()}catch{s={error:e.statusText}}throw new C(e.status,e.statusText,s)}if(e.status!==204)return e.json()}async function m(e,s,a){const i=`${rt()}${s}`,n={method:e,headers:st(a!==void 0),credentials:"same-origin"};a!==void 0&&(n.body=JSON.stringify(a));const v=await fetch(i,n);return it(v)}const nt={get(e){return m("GET",e)},post(e,s){return m("POST",e,s)},put(e,s){return m("PUT",e,s)},patch(e,s){return m("PATCH",e,s)},delete(e){return m("DELETE",e)}};var ot=y("<div></div>"),lt=y('<div class="error-message svelte-1x05zx6" role="alert"> </div>'),ct=y('<button class="numpad-btn touch-target svelte-1x05zx6"> </button>'),ut=y('<main class="login-container svelte-1x05zx6"><div class="login-card svelte-1x05zx6"><div class="logo svelte-1x05zx6"><h1 class="svelte-1x05zx6">Felt</h1> <p class="subtitle svelte-1x05zx6">Tournament Manager</p></div> <div class="pin-display svelte-1x05zx6" role="status"></div> <!> <div class="numpad svelte-1x05zx6"><!> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Clear PIN">CLR</button> <button class="numpad-btn touch-target svelte-1x05zx6" aria-label="Digit 0">0</button> <button class="numpad-btn numpad-fn touch-target svelte-1x05zx6" aria-label="Delete last digit">DEL</button></div> <button class="submit-btn touch-target svelte-1x05zx6"><!></button></div></main>');function bt(e,s){W(s,!0);let a=A(""),i=A(""),n=A(!1);const v=6;V(()=>{k.isAuthenticated&&D("/")});function w(t){r(a).length>=v||(o(a,r(a)+t),o(i,""))}function I(){o(a,r(a).slice(0,-1),!0),o(i,"")}function N(){o(a,""),o(i,"")}async function j(){if(!(r(a).length<4||r(n))){o(n,!0),o(i,"");try{const t=await nt.post("/auth/login",{pin:r(a)});k.login(t.token,{id:t.operator.id,name:t.operator.name,role:t.operator.role}),await D("/")}catch(t){t instanceof C?t.status===429?o(i,"Too many attempts. Please wait."):t.status===401?o(i,"Invalid PIN. Try again."):o(i,t.message,!0):o(i,"Connection error. Check your network."),o(a,"")}finally{o(n,!1)}}}function F(t){t.key>="0"&&t.key<="9"?w(t.key):t.key==="Backspace"?I():t.key==="Enter"?j():t.key==="Escape"&&N()}var z=ut();et("keydown",Z,F);var S=p(z),x=u(p(S),2);O(x,21,()=>Array(v),R,(t,l,c)=>{var b=ot();let H;_(()=>H=at(b,1,"pin-dot svelte-1x05zx6",null,H,{filled:c<r(a).length})),f(t,b)}),d(x);var $=u(x,2);{var J=t=>{var l=lt(),c=p(l,!0);d(l),_(()=>U(c,r(i))),f(t,l)};G($,t=>{r(i)&&t(J)})}var T=u($,2),L=p(T);O(L,16,()=>["1","2","3","4","5","6","7","8","9"],R,(t,l)=>{var c=ct(),b=p(c,!0);d(c),_(()=>{c.disabled=r(n)||r(a).length>=v,q(c,"aria-label",`Digit ${l??""}`),U(b,l)}),g("click",c,()=>w(l)),f(t,c)});var E=u(L,2),P=u(E,2),B=u(P,2);d(T);var h=u(T,2),K=p(h);{var X=t=>{var l=M("Signing in...");f(t,l)},Q=t=>{var l=M("Sign In");f(t,l)};G(K,t=>{r(n)?t(X):t(Q,!1)})}d(h),d(S),d(z),_(()=>{q(x,"aria-label",`PIN entered: ${r(a).length??""} digits`),E.disabled=r(n),P.disabled=r(n)||r(a).length>=v,B.disabled=r(n)||r(a).length===0,h.disabled=r(a).length<4||r(n)}),g("click",E,N),g("click",P,()=>w("0")),g("click",B,I),g("click",h,j),f(e,z),Y()}tt(["click"]);export{bt as component};
|
||||
1
frontend/build/_app/immutable/nodes/5.CrKjY73y.js
Normal file
1
frontend/build/_app/immutable/nodes/5.CrKjY73y.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as _,f as b}from"../chunks/Q5CB4WY5.js";import{i as x}from"../chunks/BViIIwgj.js";import{p as k,t as w,a as O,s as e,c as a,r as t}from"../chunks/C4An0dnW.js";import{d as S,a as U,s as m}from"../chunks/CQQh_IlD.js";import{a as o}from"../chunks/D3f6eoxz.js";import{g as y}from"../chunks/DQNCp18R.js";var L=b('<div class="page-content svelte-hq0atu"><h2 class="svelte-hq0atu">More</h2> <p class="text-secondary svelte-hq0atu">Settings and additional options.</p> <div class="menu-list svelte-hq0atu"><div class="menu-item svelte-hq0atu"><span class="menu-label svelte-hq0atu">Operator</span> <span class="menu-value svelte-hq0atu"> </span></div> <div class="menu-item svelte-hq0atu"><span class="menu-label svelte-hq0atu">Role</span> <span class="menu-value svelte-hq0atu"> </span></div> <hr class="divider svelte-hq0atu"/> <button class="menu-item menu-action danger touch-target svelte-hq0atu"><span class="menu-label svelte-hq0atu">Sign Out</span></button></div></div>');function C(c,d){k(d,!1);function h(){o.logout(),y("/login")}x();var s=L(),r=e(a(s),4),n=a(r),i=e(a(n),2),g=a(i,!0);t(i),t(n);var l=e(n,2),u=e(a(l),2),f=a(u,!0);t(u),t(l);var q=e(l,4);t(r),t(s),w(()=>{var v,p;m(g,((v=o.operator)==null?void 0:v.name)??"Unknown"),m(f,((p=o.operator)==null?void 0:p.role)??"Unknown")}),U("click",q,h),_(c,s),O()}S(["click"]);export{C as component};
|
||||
1
frontend/build/_app/immutable/nodes/6.CMmeTvWv.js
Normal file
1
frontend/build/_app/immutable/nodes/6.CMmeTvWv.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as c,f as d}from"../chunks/Q5CB4WY5.js";import{i as N}from"../chunks/BViIIwgj.js";import{p as $,a as j,s as a,c as s,r as e,t as B}from"../chunks/C4An0dnW.js";import{s as r}from"../chunks/CQQh_IlD.js";import{i as L}from"../chunks/D__6P984.js";import{t}from"../chunks/C5aWxL5p.js";var O=d('<div class="stats-grid svelte-14qseeg"><div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Players</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Tables</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Level</span> <span class="stat-value number svelte-14qseeg"> </span></div> <div class="stat-card svelte-14qseeg"><span class="stat-label svelte-14qseeg">Blinds</span> <span class="stat-value blinds svelte-14qseeg"> </span></div></div>'),S=d('<p class="empty-state svelte-14qseeg">No active tournament. Start or join a tournament to see the overview.</p>'),z=d('<div class="page-content svelte-14qseeg"><h2 class="svelte-14qseeg">Overview</h2> <p class="text-secondary svelte-14qseeg">Tournament dashboard — detailed views coming in Plan N.</p> <!></div>');function H(f,u){$(u,!1),N();var i=z(),h=a(s(i),4);{var x=l=>{var v=O(),n=s(v),m=a(s(n),2),y=s(m);e(m),e(n);var o=a(n,2),g=a(s(o),2),P=s(g,!0);e(g),e(o);var p=a(o,2),_=a(s(p),2),w=s(_,!0);e(_),e(p);var q=a(p,2),b=a(s(q),2),T=s(b);e(b),e(q),e(v),B(()=>{r(y,`${t.remainingPlayers??""}/${t.totalPlayers??""}`),r(P,t.activeTables),r(w,t.clock.level),r(T,`${t.clock.small_blind??""}/${t.clock.big_blind??""}`)}),c(l,v)},k=l=>{var v=S();c(l,v)};L(h,l=>{t.clock?l(x):l(k,!1)})}e(i),c(f,i),j()}export{H as component};
|
||||
1
frontend/build/_app/immutable/nodes/7.0Z-UCw0W.js
Normal file
1
frontend/build/_app/immutable/nodes/7.0Z-UCw0W.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as o,f as i}from"../chunks/Q5CB4WY5.js";import{i as n}from"../chunks/BViIIwgj.js";import{p as b,a as p,s as u,c,r as d}from"../chunks/C4An0dnW.js";import{t as m}from"../chunks/C5aWxL5p.js";import{D as y}from"../chunks/WPMya0VZ.js";var h=i('<div class="page-content svelte-wtkzqx"><h2 class="svelte-wtkzqx">Players</h2> <p class="text-secondary svelte-wtkzqx">Registered players and chip counts.</p> <!></div>');function x(a,t){b(t,!1);const s=[{key:"name",label:"Name",sortable:!0},{key:"status",label:"Status",sortable:!0},{key:"chips",label:"Chips",sortable:!0,align:"right",render:r=>r.chips.toLocaleString()},{key:"table_id",label:"Table",hideMobile:!0,sortable:!0},{key:"seat",label:"Seat",hideMobile:!0,sortable:!0,align:"center"},{key:"rebuys",label:"Rebuys",hideMobile:!0,sortable:!0,align:"center"}];n();var e=h(),l=u(c(e),4);y(l,{get columns(){return s},get data(){return m.players},sortable:!0,searchable:!0,loading:!1,emptyMessage:"No players registered yet",rowKey:r=>String(r.id),swipeActions:[{id:"bust",label:"Bust",color:"var(--color-error)",handler:()=>{}},{id:"rebuy",label:"Rebuy",color:"var(--color-primary)",handler:()=>{}}]}),d(e),o(a,e),p()}export{x as component};
|
||||
1
frontend/build/_app/immutable/nodes/8.fn5hverG.js
Normal file
1
frontend/build/_app/immutable/nodes/8.fn5hverG.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
import{a as o,f as i}from"../chunks/Q5CB4WY5.js";import{p as b,a as p,g as c,x as u,s as d,c as m,r as g}from"../chunks/C4An0dnW.js";import{t as f}from"../chunks/C5aWxL5p.js";import{D as y}from"../chunks/WPMya0VZ.js";var v=i('<div class="page-content svelte-bf0doe"><h2 class="svelte-bf0doe">Tables</h2> <p class="text-secondary svelte-bf0doe">Active tables and seating.</p> <!></div>');function D(t,s){b(s,!0);const l=[{key:"number",label:"Table #",sortable:!0,align:"center"},{key:"seats",label:"Seats",sortable:!0,align:"center"},{key:"player_count",label:"Players",sortable:!0,align:"center"},{key:"is_final_table",label:"Final",hideMobile:!0,sortable:!0,align:"center",render:e=>e.is_final_table?"Yes":""}];let r=u(()=>f.tables.map(e=>({...e,player_count:e.players.length})));var a=v(),n=d(m(a),4);y(n,{get columns(){return l},get data(){return c(r)},sortable:!0,searchable:!1,loading:!1,emptyMessage:"No tables set up yet",rowKey:e=>String(e.id)}),g(a),o(t,a),p()}export{D as component};
|
||||
|
|
@ -1 +1 @@
|
|||
{"version":"1772333625386"}
|
||||
{"version":"1772334772507"}
|
||||
39
frontend/build/financials.html
Normal file
39
frontend/build/financials.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -5,30 +5,30 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="/favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="/_app/immutable/entry/start.Cw5np0_P.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/giww_vF6.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Ym0WvvUy.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Bfwrz3i4.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/entry/app.DWnDWHgs.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/dTRRgeF-.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/B6M6q2Zo.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/B9dvBo0E.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Da6yQRl8.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="/_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_1rgg0vt = {
|
||||
__sveltekit_og1wdu = {
|
||||
base: ""
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("/_app/immutable/entry/start.Cw5np0_P.js"),
|
||||
import("/_app/immutable/entry/app.DWnDWHgs.js")
|
||||
import("/_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("/_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -5,30 +5,30 @@
|
|||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Cw5np0_P.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/giww_vF6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Ym0WvvUy.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Bfwrz3i4.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.DWnDWHgs.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/dTRRgeF-.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/B6M6q2Zo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/B9dvBo0E.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Da6yQRl8.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_1rgg0vt = {
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Cw5np0_P.js"),
|
||||
import("./_app/immutable/entry/app.DWnDWHgs.js")
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
|
|
|
|||
39
frontend/build/more.html
Normal file
39
frontend/build/more.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
frontend/build/overview.html
Normal file
39
frontend/build/overview.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
frontend/build/players.html
Normal file
39
frontend/build/players.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
39
frontend/build/tables.html
Normal file
39
frontend/build/tables.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<!doctype html>
|
||||
<html lang="en" data-theme="mocha">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
|
||||
<link rel="icon" href="./favicon.png" />
|
||||
<title>Felt</title>
|
||||
<link href="./_app/immutable/entry/start.Do4A91T6.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/DQNCp18R.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C4An0dnW.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/nIaoZoCo.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/entry/app.Dwn0pdp1.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/CQQh_IlD.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/Q5CB4WY5.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/D__6P984.js" rel="modulepreload">
|
||||
<link href="./_app/immutable/chunks/C48rM6KF.js" rel="modulepreload">
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<div style="display: contents">
|
||||
<script>
|
||||
{
|
||||
__sveltekit_og1wdu = {
|
||||
base: new URL(".", location).pathname.slice(0, -1)
|
||||
};
|
||||
|
||||
const element = document.currentScript.parentElement;
|
||||
|
||||
Promise.all([
|
||||
import("./_app/immutable/entry/start.Do4A91T6.js"),
|
||||
import("./_app/immutable/entry/app.Dwn0pdp1.js")
|
||||
]).then(([kit, app]) => {
|
||||
kit.start(app, element);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
108
frontend/src/lib/components/BottomTabs.svelte
Normal file
108
frontend/src/lib/components/BottomTabs.svelte
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
|
||||
/**
|
||||
* Bottom tab bar for mobile navigation.
|
||||
* 5 tabs: Overview, Players, Tables, Financials, More.
|
||||
* Hidden on desktop (>= 768px) where sidebar shows instead.
|
||||
* 48px touch targets for poker room environment.
|
||||
*/
|
||||
|
||||
interface Tab {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const tabs: Tab[] = [
|
||||
{ label: 'Overview', href: '/overview', icon: '\u{1F3E0}' },
|
||||
{ label: 'Players', href: '/players', icon: '\u{1F465}' },
|
||||
{ label: 'Tables', href: '/tables', icon: '\u{1FA91}' },
|
||||
{ label: 'Financials', href: '/financials', icon: '\u{1F4B0}' },
|
||||
{ label: 'More', href: '/more', icon: '\u{2699}' }
|
||||
];
|
||||
|
||||
function isActive(tabHref: string): boolean {
|
||||
const path = page.url?.pathname ?? '/';
|
||||
if (tabHref === '/overview') {
|
||||
return path === '/' || path === '/overview' || path.startsWith('/overview/');
|
||||
}
|
||||
return path === tabHref || path.startsWith(tabHref + '/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bottom-tabs" role="tablist" aria-label="Main navigation">
|
||||
{#each tabs as tab}
|
||||
<a
|
||||
href={tab.href}
|
||||
class="tab-item"
|
||||
class:active={isActive(tab.href)}
|
||||
role="tab"
|
||||
aria-selected={isActive(tab.href)}
|
||||
aria-label={tab.label}
|
||||
>
|
||||
<span class="tab-icon" aria-hidden="true">{tab.icon}</span>
|
||||
<span class="tab-label">{tab.label}</span>
|
||||
</a>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.bottom-tabs {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 90;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: space-around;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-top: 1px solid var(--color-border);
|
||||
/* Safe area for phones with home indicator */
|
||||
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||
}
|
||||
|
||||
/* Hidden on desktop */
|
||||
@media (min-width: 768px) {
|
||||
.bottom-tabs {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 2px;
|
||||
flex: 1;
|
||||
min-height: var(--touch-target);
|
||||
padding: var(--space-1) var(--space-2);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-muted);
|
||||
transition: color var(--transition-fast);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.tab-item:hover:not(.active) {
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.tab-icon {
|
||||
font-size: var(--text-xl);
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.tab-label {
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
466
frontend/src/lib/components/DataTable.svelte
Normal file
466
frontend/src/lib/components/DataTable.svelte
Normal file
|
|
@ -0,0 +1,466 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Reusable data table component.
|
||||
*
|
||||
* Features: sort by column, sticky header, search/filter, row click,
|
||||
* mobile swipe actions, loading skeleton, empty state.
|
||||
* Responsive: configurable column visibility per breakpoint.
|
||||
* 48px row height for touch targets.
|
||||
*/
|
||||
|
||||
interface Column<T> {
|
||||
/** Unique key matching a property on data items. */
|
||||
key: string;
|
||||
/** Display label for column header. */
|
||||
label: string;
|
||||
/** Whether this column is sortable. Default: true if sortable prop is true. */
|
||||
sortable?: boolean;
|
||||
/** Hide on mobile (< 768px). */
|
||||
hideMobile?: boolean;
|
||||
/** Custom render function (returns string). */
|
||||
render?: (item: T) => string;
|
||||
/** Text alignment. */
|
||||
align?: 'left' | 'center' | 'right';
|
||||
/** Column width (CSS value). */
|
||||
width?: string;
|
||||
}
|
||||
|
||||
interface SwipeAction<T> {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
handler: (item: T) => void;
|
||||
}
|
||||
|
||||
interface Props<T> {
|
||||
columns: Column<T>[];
|
||||
data: T[];
|
||||
sortable?: boolean;
|
||||
searchable?: boolean;
|
||||
loading?: boolean;
|
||||
emptyMessage?: string;
|
||||
rowKey?: (item: T) => string;
|
||||
onrowclick?: (item: T) => void;
|
||||
swipeActions?: SwipeAction<T>[];
|
||||
}
|
||||
|
||||
let {
|
||||
columns,
|
||||
data,
|
||||
sortable = false,
|
||||
searchable = false,
|
||||
loading = false,
|
||||
emptyMessage = 'No data',
|
||||
rowKey = (item: Record<string, unknown>) => String(item['id'] ?? ''),
|
||||
onrowclick,
|
||||
swipeActions = []
|
||||
}: Props<Record<string, unknown>> = $props();
|
||||
|
||||
// Sort state
|
||||
let sortKey = $state<string | null>(null);
|
||||
let sortDir = $state<'asc' | 'desc'>('asc');
|
||||
|
||||
// Search state
|
||||
let searchQuery = $state('');
|
||||
|
||||
// Swipe state
|
||||
let swipedRowId = $state<string | null>(null);
|
||||
let touchStartX = 0;
|
||||
let touchCurrentX = 0;
|
||||
const SWIPE_THRESHOLD = 60;
|
||||
|
||||
// Filtered and sorted data
|
||||
let processedData = $derived.by(() => {
|
||||
let result = [...data];
|
||||
|
||||
// Filter by search query
|
||||
if (searchQuery.trim()) {
|
||||
const query = searchQuery.toLowerCase();
|
||||
result = result.filter((item) => {
|
||||
return columns.some((col) => {
|
||||
const value = col.render ? col.render(item) : String(item[col.key] ?? '');
|
||||
return value.toLowerCase().includes(query);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
if (sortKey) {
|
||||
const key = sortKey;
|
||||
const dir = sortDir;
|
||||
result.sort((a, b) => {
|
||||
const aVal = a[key];
|
||||
const bVal = b[key];
|
||||
|
||||
if (aVal == null && bVal == null) return 0;
|
||||
if (aVal == null) return 1;
|
||||
if (bVal == null) return -1;
|
||||
|
||||
let cmp: number;
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
cmp = aVal - bVal;
|
||||
} else {
|
||||
cmp = String(aVal).localeCompare(String(bVal));
|
||||
}
|
||||
|
||||
return dir === 'asc' ? cmp : -cmp;
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
});
|
||||
|
||||
function toggleSort(key: string): void {
|
||||
if (!sortable) return;
|
||||
if (sortKey === key) {
|
||||
sortDir = sortDir === 'asc' ? 'desc' : 'asc';
|
||||
} else {
|
||||
sortKey = key;
|
||||
sortDir = 'asc';
|
||||
}
|
||||
}
|
||||
|
||||
function getCellValue(item: Record<string, unknown>, col: Column<Record<string, unknown>>): string {
|
||||
if (col.render) return col.render(item);
|
||||
const val = item[col.key];
|
||||
if (val == null) return '';
|
||||
return String(val);
|
||||
}
|
||||
|
||||
// Swipe handlers for mobile
|
||||
function handleTouchStart(event: TouchEvent, id: string): void {
|
||||
if (swipeActions.length === 0) return;
|
||||
touchStartX = event.touches[0].clientX;
|
||||
touchCurrentX = touchStartX;
|
||||
// Close other swiped rows
|
||||
if (swipedRowId && swipedRowId !== id) {
|
||||
swipedRowId = null;
|
||||
}
|
||||
}
|
||||
|
||||
function handleTouchMove(event: TouchEvent): void {
|
||||
if (swipeActions.length === 0) return;
|
||||
touchCurrentX = event.touches[0].clientX;
|
||||
}
|
||||
|
||||
function handleTouchEnd(id: string): void {
|
||||
if (swipeActions.length === 0) return;
|
||||
const diff = touchStartX - touchCurrentX;
|
||||
if (diff > SWIPE_THRESHOLD) {
|
||||
// Swipe left: reveal actions
|
||||
swipedRowId = id;
|
||||
} else if (diff < -SWIPE_THRESHOLD) {
|
||||
// Swipe right: hide actions
|
||||
swipedRowId = null;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="data-table-wrapper">
|
||||
<!-- Search input -->
|
||||
{#if searchable}
|
||||
<div class="table-search">
|
||||
<input
|
||||
type="search"
|
||||
placeholder="Search..."
|
||||
bind:value={searchQuery}
|
||||
class="search-input"
|
||||
aria-label="Search table"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Table -->
|
||||
<div class="table-scroll">
|
||||
<table class="data-table" role="grid">
|
||||
<thead>
|
||||
<tr>
|
||||
{#each columns as col}
|
||||
<th
|
||||
class:hide-mobile={col.hideMobile}
|
||||
class:sortable={sortable && col.sortable !== false}
|
||||
style:text-align={col.align ?? 'left'}
|
||||
style:width={col.width}
|
||||
onclick={() => {
|
||||
if (sortable && col.sortable !== false) toggleSort(col.key);
|
||||
}}
|
||||
aria-sort={sortKey === col.key ? sortDir === 'asc' ? 'ascending' : 'descending' : undefined}
|
||||
>
|
||||
<span class="th-content">
|
||||
{col.label}
|
||||
{#if sortable && col.sortable !== false}
|
||||
<span class="sort-indicator" class:active={sortKey === col.key}>
|
||||
{#if sortKey === col.key}
|
||||
{sortDir === 'asc' ? '\u25B2' : '\u25BC'}
|
||||
{:else}
|
||||
\u25B4
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</th>
|
||||
{/each}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#if loading}
|
||||
<!-- Skeleton loading rows -->
|
||||
{#each Array(5) as _}
|
||||
<tr class="skeleton-row">
|
||||
{#each columns as col}
|
||||
<td class:hide-mobile={col.hideMobile}>
|
||||
<div class="skeleton-cell"></div>
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
{/each}
|
||||
{:else if processedData.length === 0}
|
||||
<!-- Empty state -->
|
||||
<tr>
|
||||
<td colspan={columns.length} class="empty-state">
|
||||
{emptyMessage}
|
||||
</td>
|
||||
</tr>
|
||||
{:else}
|
||||
<!-- Data rows -->
|
||||
{#each processedData as item (rowKey(item))}
|
||||
{@const id = rowKey(item)}
|
||||
<tr
|
||||
class="data-row"
|
||||
class:clickable={!!onrowclick}
|
||||
class:swiped={swipedRowId === id}
|
||||
onclick={() => onrowclick?.(item)}
|
||||
onkeydown={(e) => e.key === 'Enter' && onrowclick?.(item)}
|
||||
ontouchstart={(e) => handleTouchStart(e, id)}
|
||||
ontouchmove={handleTouchMove}
|
||||
ontouchend={() => handleTouchEnd(id)}
|
||||
role={onrowclick ? 'button' : undefined}
|
||||
tabindex={onrowclick ? 0 : undefined}
|
||||
>
|
||||
{#each columns as col}
|
||||
<td
|
||||
class:hide-mobile={col.hideMobile}
|
||||
style:text-align={col.align ?? 'left'}
|
||||
>
|
||||
{getCellValue(item, col)}
|
||||
</td>
|
||||
{/each}
|
||||
</tr>
|
||||
<!-- Swipe actions overlay -->
|
||||
{#if swipedRowId === id && swipeActions.length > 0}
|
||||
<tr class="swipe-actions-row">
|
||||
<td colspan={columns.length}>
|
||||
<div class="swipe-actions">
|
||||
{#each swipeActions as action}
|
||||
<button
|
||||
class="swipe-action-btn touch-target"
|
||||
style="background-color: {action.color}"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
action.handler(item);
|
||||
swipedRowId = null;
|
||||
}}
|
||||
>
|
||||
{action.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{/if}
|
||||
{/each}
|
||||
{/if}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.data-table-wrapper {
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.table-search {
|
||||
padding: var(--space-3) 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
outline: none;
|
||||
transition: border-color var(--transition-fast);
|
||||
min-height: var(--touch-target);
|
||||
}
|
||||
|
||||
.search-input:focus {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
.search-input::placeholder {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Table scroll wrapper */
|
||||
.table-scroll {
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
|
||||
/* Table */
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
/* Sticky header */
|
||||
.data-table thead {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
font-weight: 600;
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted);
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-bottom: 2px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.data-table th.sortable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-table th.sortable:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.th-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.sort-indicator {
|
||||
font-size: 8px;
|
||||
color: var(--color-text-muted);
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.sort-indicator.active {
|
||||
opacity: 1;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
/* Data rows */
|
||||
.data-table td {
|
||||
padding: var(--space-2) var(--space-3);
|
||||
color: var(--color-text);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 200px;
|
||||
}
|
||||
|
||||
.data-row {
|
||||
min-height: var(--touch-target);
|
||||
transition: background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.data-row:hover {
|
||||
background-color: var(--color-surface);
|
||||
}
|
||||
|
||||
.data-row.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.data-row.clickable:active {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: var(--space-12) var(--space-4);
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Skeleton loading */
|
||||
.skeleton-row td {
|
||||
padding: var(--space-3);
|
||||
}
|
||||
|
||||
.skeleton-cell {
|
||||
height: 16px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-surface) 25%,
|
||||
var(--color-surface-hover) 50%,
|
||||
var(--color-surface) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
|
||||
/* Swipe actions */
|
||||
.swipe-actions-row td {
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.swipe-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-1);
|
||||
background-color: var(--color-bg-sunken);
|
||||
}
|
||||
|
||||
.swipe-action-btn {
|
||||
padding: var(--space-2) var(--space-4);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Hide on mobile */
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.hide-mobile {
|
||||
display: table-cell;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
max-width: 300px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
257
frontend/src/lib/components/FAB.svelte
Normal file
257
frontend/src/lib/components/FAB.svelte
Normal file
|
|
@ -0,0 +1,257 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
|
||||
/**
|
||||
* Floating Action Button for quick tournament actions.
|
||||
* Positioned bottom-right, above the tab bar.
|
||||
* Expands to show action buttons: Bust, Buy In, Rebuy, Add-On, Pause/Resume.
|
||||
* Context-aware: only shows relevant actions.
|
||||
*/
|
||||
|
||||
interface FABAction {
|
||||
id: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
color: string;
|
||||
visible: () => boolean;
|
||||
}
|
||||
|
||||
let expanded = $state(false);
|
||||
|
||||
/** Event handler passed from parent. */
|
||||
interface Props {
|
||||
onaction?: (actionId: string) => void;
|
||||
}
|
||||
|
||||
let { onaction }: Props = $props();
|
||||
|
||||
const actions: FABAction[] = [
|
||||
{
|
||||
id: 'bust',
|
||||
label: 'Bust',
|
||||
icon: '\u{274C}',
|
||||
color: 'var(--color-error)',
|
||||
visible: () => tournament.remainingPlayers > 0
|
||||
},
|
||||
{
|
||||
id: 'buyin',
|
||||
label: 'Buy In',
|
||||
icon: '\u{2795}',
|
||||
color: 'var(--color-success)',
|
||||
visible: () => true
|
||||
},
|
||||
{
|
||||
id: 'rebuy',
|
||||
label: 'Rebuy',
|
||||
icon: '\u{1F504}',
|
||||
color: 'var(--color-primary)',
|
||||
visible: () => tournament.remainingPlayers > 0
|
||||
},
|
||||
{
|
||||
id: 'addon',
|
||||
label: 'Add-On',
|
||||
icon: '\u{2B06}',
|
||||
color: 'var(--color-warning)',
|
||||
visible: () => {
|
||||
// Show add-on only when relevant (simplified; real logic depends on tournament phase)
|
||||
return tournament.clock !== null;
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'pause-resume',
|
||||
label: tournament.clock?.is_paused ? 'Resume' : 'Pause',
|
||||
icon: tournament.clock?.is_paused ? '\u{25B6}' : '\u{23F8}',
|
||||
color: 'var(--ctp-peach)',
|
||||
visible: () => tournament.clock !== null
|
||||
}
|
||||
];
|
||||
|
||||
function toggle(): void {
|
||||
expanded = !expanded;
|
||||
}
|
||||
|
||||
function handleAction(actionId: string): void {
|
||||
expanded = false;
|
||||
onaction?.(actionId);
|
||||
}
|
||||
|
||||
function handleBackdrop(): void {
|
||||
expanded = false;
|
||||
}
|
||||
|
||||
function handleKeydown(event: KeyboardEvent): void {
|
||||
if (event.key === 'Escape' && expanded) {
|
||||
expanded = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={handleKeydown} />
|
||||
|
||||
{#if expanded}
|
||||
<!-- Backdrop -->
|
||||
<div
|
||||
class="fab-backdrop"
|
||||
onclick={handleBackdrop}
|
||||
onkeydown={(e) => e.key === 'Enter' && handleBackdrop()}
|
||||
role="presentation"
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<div class="fab-container" class:expanded>
|
||||
<!-- Action buttons (visible when expanded) -->
|
||||
{#if expanded}
|
||||
<div class="fab-actions">
|
||||
{#each actions.filter((a) => a.visible()) as action, i}
|
||||
<button
|
||||
class="fab-action touch-target"
|
||||
style="--action-color: {action.color}; --action-delay: {i * 40}ms"
|
||||
onclick={() => handleAction(action.id)}
|
||||
aria-label={action.label}
|
||||
>
|
||||
<span class="fab-action-icon" aria-hidden="true">{action.icon}</span>
|
||||
<span class="fab-action-label">{action.label}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Main FAB button -->
|
||||
<button
|
||||
class="fab-main touch-target"
|
||||
class:expanded
|
||||
onclick={toggle}
|
||||
aria-label={expanded ? 'Close actions' : 'Open quick actions'}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<span class="fab-icon" aria-hidden="true">{expanded ? '\u{2715}' : '\u{002B}'}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.fab-backdrop {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 94;
|
||||
background-color: rgba(0, 0, 0, 0.4);
|
||||
animation: fade-in 150ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.fab-container {
|
||||
position: fixed;
|
||||
/* Above tab bar on mobile (tab bar ~56px + safe area) */
|
||||
bottom: calc(var(--touch-target) + var(--space-6) + env(safe-area-inset-bottom, 0px));
|
||||
right: var(--space-4);
|
||||
z-index: 95;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
/* Desktop: no tab bar offset needed */
|
||||
@media (min-width: 768px) {
|
||||
.fab-container {
|
||||
bottom: var(--space-6);
|
||||
right: var(--space-6);
|
||||
}
|
||||
}
|
||||
|
||||
/* Action buttons container */
|
||||
.fab-actions {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.fab-action {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--action-color);
|
||||
border-radius: var(--radius-full);
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
box-shadow: var(--shadow-md);
|
||||
animation: fab-action-in 200ms ease-out backwards;
|
||||
animation-delay: var(--action-delay);
|
||||
transition:
|
||||
transform var(--transition-fast),
|
||||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.fab-action:hover {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
@keyframes fab-action-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.9);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
.fab-action-icon {
|
||||
font-size: var(--text-lg);
|
||||
color: var(--action-color);
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fab-action-label {
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Main FAB button */
|
||||
.fab-main {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
min-height: 56px;
|
||||
min-width: 56px;
|
||||
border-radius: var(--radius-full);
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-bg);
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: var(--shadow-lg);
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
transition:
|
||||
transform var(--transition-normal),
|
||||
background-color var(--transition-fast);
|
||||
}
|
||||
|
||||
.fab-main:hover {
|
||||
background-color: color-mix(in srgb, var(--color-primary) 90%, white);
|
||||
}
|
||||
|
||||
.fab-main.expanded {
|
||||
background-color: var(--color-surface-active);
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
|
||||
.fab-icon {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
258
frontend/src/lib/components/Header.svelte
Normal file
258
frontend/src/lib/components/Header.svelte
Normal file
|
|
@ -0,0 +1,258 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
|
||||
/**
|
||||
* Persistent header showing tournament clock, level, blinds, player count.
|
||||
* Fixed at top, always visible. Auto-updates from tournament state store.
|
||||
* Compact on mobile, expanded on desktop.
|
||||
*/
|
||||
|
||||
/** Format seconds into MM:SS. */
|
||||
function formatTime(seconds: number): string {
|
||||
if (seconds < 0) seconds = 0;
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = seconds % 60;
|
||||
return `${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
/** Format blind values (e.g., "100/200"). */
|
||||
function formatBlinds(sb: number, bb: number): string {
|
||||
return `${sb.toLocaleString()}/${bb.toLocaleString()}`;
|
||||
}
|
||||
</script>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-inner">
|
||||
{#if tournament.clock}
|
||||
{@const clock = tournament.clock}
|
||||
{@const isUrgent = clock.remaining_seconds <= 10 && !clock.is_break && !clock.is_paused}
|
||||
|
||||
<!-- Clock / Timer -->
|
||||
<div class="header-clock" class:urgent={isUrgent} class:paused={clock.is_paused} class:on-break={clock.is_break}>
|
||||
{#if clock.is_paused}
|
||||
<span class="status-badge paused-badge">PAUSED</span>
|
||||
{:else if clock.is_break}
|
||||
<span class="status-badge break-badge">BREAK</span>
|
||||
{/if}
|
||||
<span class="timer clock-time">{formatTime(clock.remaining_seconds)}</span>
|
||||
</div>
|
||||
|
||||
<!-- Level info -->
|
||||
<div class="header-level">
|
||||
<span class="level-number">L{clock.level}</span>
|
||||
<span class="level-name hide-mobile">{clock.name}</span>
|
||||
</div>
|
||||
|
||||
<!-- Blinds -->
|
||||
<div class="header-blinds">
|
||||
<span class="blinds-label hide-mobile">Blinds</span>
|
||||
<span class="blinds blinds-value">{formatBlinds(clock.small_blind, clock.big_blind)}</span>
|
||||
{#if clock.ante > 0}
|
||||
<span class="blinds ante-value hide-mobile">Ante {clock.ante.toLocaleString()}</span>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Player count -->
|
||||
<div class="header-players">
|
||||
<span class="number player-count">{tournament.remainingPlayers}/{tournament.totalPlayers}</span>
|
||||
<span class="player-label hide-mobile">remaining</span>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- No tournament data yet -->
|
||||
<div class="header-empty">
|
||||
<span class="brand">Felt</span>
|
||||
<span class="no-data">No active tournament</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<style>
|
||||
.header {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
min-height: var(--touch-target);
|
||||
}
|
||||
|
||||
/* Clock section */
|
||||
.header-clock {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.clock-time {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-clock);
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.header-clock.urgent .clock-time {
|
||||
color: var(--color-error);
|
||||
animation: pulse-urgent 1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.header-clock.paused .clock-time {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.header-clock.on-break .clock-time {
|
||||
color: var(--color-break);
|
||||
}
|
||||
|
||||
@keyframes pulse-urgent {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.status-badge {
|
||||
font-size: var(--text-xs);
|
||||
font-weight: 700;
|
||||
padding: 2px var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
.paused-badge {
|
||||
background-color: color-mix(in srgb, var(--ctp-peach) 20%, transparent);
|
||||
color: var(--ctp-peach);
|
||||
animation: pulse-paused 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-paused {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.5; }
|
||||
}
|
||||
|
||||
.break-badge {
|
||||
background-color: color-mix(in srgb, var(--color-break) 20%, transparent);
|
||||
color: var(--color-break);
|
||||
}
|
||||
|
||||
/* Level section */
|
||||
.header-level {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.level-number {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.level-name {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* Blinds section */
|
||||
.header-blinds {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.blinds-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.blinds-value {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.ante-value {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
/* Player count section */
|
||||
.header-players {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--space-1);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.player-count {
|
||||
font-size: var(--text-lg);
|
||||
font-weight: 600;
|
||||
color: var(--color-success);
|
||||
}
|
||||
|
||||
.player-label {
|
||||
font-size: var(--text-xs);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Empty state */
|
||||
.header-empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.brand {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.no-data {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Hide on mobile */
|
||||
.hide-mobile {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Desktop: show hidden elements, bigger clock */
|
||||
@media (min-width: 768px) {
|
||||
.hide-mobile {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.clock-time {
|
||||
font-size: var(--text-3xl);
|
||||
}
|
||||
|
||||
.header-inner {
|
||||
padding: var(--space-3) var(--space-6);
|
||||
gap: var(--space-6);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
145
frontend/src/lib/components/Loading.svelte
Normal file
145
frontend/src/lib/components/Loading.svelte
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
<script lang="ts">
|
||||
/**
|
||||
* Loading state components.
|
||||
*
|
||||
* - Skeleton: animated placeholder matching content shape
|
||||
* - Spinner: circular spinner for buttons and page load
|
||||
* - FullPage: full-page loading overlay for initial app load
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
/** Loading variant to render. */
|
||||
variant?: 'spinner' | 'skeleton' | 'full-page';
|
||||
/** Number of skeleton rows (for skeleton variant). */
|
||||
rows?: number;
|
||||
/** Text to show below spinner (for full-page and spinner variants). */
|
||||
text?: string;
|
||||
/** Size of the spinner. */
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
let {
|
||||
variant = 'spinner',
|
||||
rows = 3,
|
||||
text = '',
|
||||
size = 'md'
|
||||
}: Props = $props();
|
||||
</script>
|
||||
|
||||
{#if variant === 'full-page'}
|
||||
<div class="full-page-loading" role="status" aria-label="Loading">
|
||||
<div class="spinner spinner-lg"></div>
|
||||
{#if text}
|
||||
<p class="loading-text">{text}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else if variant === 'skeleton'}
|
||||
<div class="skeleton-container" role="status" aria-label="Loading content">
|
||||
{#each Array(rows) as _}
|
||||
<div class="skeleton-row">
|
||||
<div class="skeleton-line skeleton-short"></div>
|
||||
<div class="skeleton-line skeleton-long"></div>
|
||||
<div class="skeleton-line skeleton-medium"></div>
|
||||
</div>
|
||||
{/each}
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
{:else}
|
||||
<span class="spinner spinner-{size}" role="status" aria-label="Loading">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
border-radius: var(--radius-full);
|
||||
border-style: solid;
|
||||
border-color: var(--color-surface-active);
|
||||
border-top-color: var(--color-primary);
|
||||
animation: spin 0.6s linear infinite;
|
||||
}
|
||||
|
||||
.spinner-sm {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-width: 2px;
|
||||
}
|
||||
|
||||
.spinner-md {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-width: 3px;
|
||||
}
|
||||
|
||||
.spinner-lg {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-width: 4px;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Full-page loading */
|
||||
.full-page-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-4);
|
||||
min-height: 100dvh;
|
||||
background-color: var(--color-bg);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Skeleton */
|
||||
.skeleton-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-4);
|
||||
padding: var(--space-4);
|
||||
}
|
||||
|
||||
.skeleton-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.skeleton-line {
|
||||
height: 14px;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--color-surface) 25%,
|
||||
var(--color-surface-hover) 50%,
|
||||
var(--color-surface) 75%
|
||||
);
|
||||
background-size: 200% 100%;
|
||||
animation: skeleton-shimmer 1.5s ease-in-out infinite;
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.skeleton-short {
|
||||
width: 40%;
|
||||
}
|
||||
|
||||
.skeleton-medium {
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.skeleton-long {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
@keyframes skeleton-shimmer {
|
||||
0% { background-position: 200% 0; }
|
||||
100% { background-position: -200% 0; }
|
||||
}
|
||||
</style>
|
||||
135
frontend/src/lib/components/Sidebar.svelte
Normal file
135
frontend/src/lib/components/Sidebar.svelte
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/state';
|
||||
|
||||
/**
|
||||
* Desktop sidebar navigation.
|
||||
* Same 5 navigation items as bottom tabs but in vertical layout.
|
||||
* Renders only on desktop (>= 768px). Hidden on mobile.
|
||||
*/
|
||||
|
||||
interface NavItem {
|
||||
label: string;
|
||||
href: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
const navItems: NavItem[] = [
|
||||
{ label: 'Overview', href: '/overview', icon: '\u{1F3E0}' },
|
||||
{ label: 'Players', href: '/players', icon: '\u{1F465}' },
|
||||
{ label: 'Tables', href: '/tables', icon: '\u{1FA91}' },
|
||||
{ label: 'Financials', href: '/financials', icon: '\u{1F4B0}' },
|
||||
{ label: 'More', href: '/more', icon: '\u{2699}' }
|
||||
];
|
||||
|
||||
function isActive(itemHref: string): boolean {
|
||||
const path = page.url?.pathname ?? '/';
|
||||
if (itemHref === '/overview') {
|
||||
return path === '/' || path === '/overview' || path.startsWith('/overview/');
|
||||
}
|
||||
return path === itemHref || path.startsWith(itemHref + '/');
|
||||
}
|
||||
</script>
|
||||
|
||||
<nav class="sidebar" aria-label="Main navigation">
|
||||
<div class="sidebar-brand">
|
||||
<span class="brand-name">Felt</span>
|
||||
</div>
|
||||
|
||||
<ul class="sidebar-nav" role="list">
|
||||
{#each navItems as item}
|
||||
<li>
|
||||
<a
|
||||
href={item.href}
|
||||
class="nav-item"
|
||||
class:active={isActive(item.href)}
|
||||
aria-current={isActive(item.href) ? 'page' : undefined}
|
||||
>
|
||||
<span class="nav-icon" aria-hidden="true">{item.icon}</span>
|
||||
<span class="nav-label">{item.label}</span>
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
.sidebar {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 220px;
|
||||
background-color: var(--color-bg-elevated);
|
||||
border-right: 1px solid var(--color-border);
|
||||
z-index: 80;
|
||||
padding-top: var(--space-4);
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Only show on desktop */
|
||||
@media (min-width: 768px) {
|
||||
.sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-brand {
|
||||
padding: var(--space-4) var(--space-6);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
.sidebar-nav {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-6);
|
||||
min-height: var(--touch-target);
|
||||
text-decoration: none;
|
||||
color: var(--color-text-secondary);
|
||||
border-radius: 0;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
}
|
||||
|
||||
.nav-item:hover:not(.active) {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: var(--color-primary);
|
||||
background-color: color-mix(in srgb, var(--color-primary) 10%, transparent);
|
||||
border-right: 3px solid var(--color-primary);
|
||||
}
|
||||
|
||||
.nav-icon {
|
||||
font-size: var(--text-xl);
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nav-label {
|
||||
font-size: var(--text-base);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
156
frontend/src/lib/components/Toast.svelte
Normal file
156
frontend/src/lib/components/Toast.svelte
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<script lang="ts">
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
|
||||
/**
|
||||
* Toast notification container.
|
||||
* Renders all active toasts with slide-in/fade-out animations.
|
||||
* Positioned fixed at top-right on desktop, bottom-center on mobile.
|
||||
*/
|
||||
|
||||
function typeColor(type: string): string {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return 'var(--color-success)';
|
||||
case 'info':
|
||||
return 'var(--color-primary)';
|
||||
case 'warning':
|
||||
return 'var(--color-warning)';
|
||||
case 'error':
|
||||
return 'var(--color-error)';
|
||||
default:
|
||||
return 'var(--color-text)';
|
||||
}
|
||||
}
|
||||
|
||||
function typeIcon(type: string): string {
|
||||
switch (type) {
|
||||
case 'success':
|
||||
return '\u2713'; // checkmark
|
||||
case 'info':
|
||||
return '\u2139'; // info
|
||||
case 'warning':
|
||||
return '\u26A0'; // warning
|
||||
case 'error':
|
||||
return '\u2717'; // X
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if toast.toasts.length > 0}
|
||||
<div class="toast-container" role="region" aria-label="Notifications" aria-live="polite">
|
||||
{#each toast.toasts as t (t.id)}
|
||||
<div
|
||||
class="toast toast-{t.type}"
|
||||
role="alert"
|
||||
style="--toast-color: {typeColor(t.type)}"
|
||||
>
|
||||
<span class="toast-icon" aria-hidden="true">{typeIcon(t.type)}</span>
|
||||
<span class="toast-message">{t.message}</span>
|
||||
{#if t.dismissible}
|
||||
<button
|
||||
class="toast-dismiss"
|
||||
onclick={() => toast.dismiss(t.id)}
|
||||
aria-label="Dismiss notification"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.toast-container {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2);
|
||||
pointer-events: none;
|
||||
/* Mobile: bottom center */
|
||||
bottom: calc(var(--touch-target) + var(--space-8) + var(--space-4));
|
||||
left: var(--space-4);
|
||||
right: var(--space-4);
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Desktop: top-right */
|
||||
@media (min-width: 768px) {
|
||||
.toast-container {
|
||||
top: var(--space-4);
|
||||
right: var(--space-4);
|
||||
bottom: auto;
|
||||
left: auto;
|
||||
align-items: flex-end;
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
|
||||
.toast {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-3);
|
||||
padding: var(--space-3) var(--space-4);
|
||||
background-color: var(--color-surface);
|
||||
border: 1px solid var(--toast-color);
|
||||
border-left: 4px solid var(--toast-color);
|
||||
border-radius: var(--radius-md);
|
||||
box-shadow: var(--shadow-lg);
|
||||
pointer-events: auto;
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
animation: toast-slide-in 200ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes toast-slide-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
.toast-icon {
|
||||
flex-shrink: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--toast-color);
|
||||
width: 1.5em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.toast-message {
|
||||
flex: 1;
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text);
|
||||
line-height: var(--leading-normal);
|
||||
}
|
||||
|
||||
.toast-dismiss {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-height: 28px;
|
||||
min-width: 28px;
|
||||
padding: 0;
|
||||
font-size: var(--text-lg);
|
||||
color: var(--color-text-muted);
|
||||
background: none;
|
||||
border: none;
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.toast-dismiss:hover {
|
||||
color: var(--color-text);
|
||||
}
|
||||
</style>
|
||||
110
frontend/src/lib/components/TournamentTabs.svelte
Normal file
110
frontend/src/lib/components/TournamentTabs.svelte
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
<script lang="ts">
|
||||
import { multiTournament } from '$lib/stores/multi-tournament.svelte';
|
||||
|
||||
/**
|
||||
* Multi-tournament tab selector.
|
||||
* Shows when 2+ tournaments are active.
|
||||
* Horizontal scrollable tabs on mobile.
|
||||
* Each tab: tournament name + status indicator.
|
||||
*/
|
||||
|
||||
function statusColor(status: string): string {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return 'var(--color-success)';
|
||||
case 'paused':
|
||||
return 'var(--ctp-peach)';
|
||||
case 'break':
|
||||
return 'var(--color-break)';
|
||||
case 'completed':
|
||||
return 'var(--color-text-muted)';
|
||||
default:
|
||||
return 'var(--color-text-secondary)';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if multiTournament.isMulti}
|
||||
<div class="tournament-tabs" role="tablist" aria-label="Tournament selection">
|
||||
{#each multiTournament.tournaments as t}
|
||||
<button
|
||||
class="tournament-tab touch-target"
|
||||
class:active={t.id === multiTournament.activeId}
|
||||
role="tab"
|
||||
aria-selected={t.id === multiTournament.activeId}
|
||||
onclick={() => multiTournament.switchTo(t.id)}
|
||||
>
|
||||
<span
|
||||
class="status-dot"
|
||||
style="background-color: {statusColor(t.status)}"
|
||||
aria-hidden="true"
|
||||
></span>
|
||||
<span class="tab-name">{t.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.tournament-tabs {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background-color: var(--color-bg-sunken);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.tournament-tabs::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tournament-tab {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
padding: var(--space-2) var(--space-4);
|
||||
background: none;
|
||||
border: 1px solid transparent;
|
||||
border-radius: var(--radius-md);
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
font-weight: 500;
|
||||
transition:
|
||||
background-color var(--transition-fast),
|
||||
border-color var(--transition-fast),
|
||||
color var(--transition-fast);
|
||||
flex-shrink: 0;
|
||||
-webkit-tap-highlight-color: transparent;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tournament-tab:hover:not(.active) {
|
||||
background-color: var(--color-surface);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.tournament-tab.active {
|
||||
background-color: var(--color-surface);
|
||||
border-color: var(--color-primary);
|
||||
color: var(--color-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: var(--radius-full);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
</style>
|
||||
89
frontend/src/lib/stores/multi-tournament.svelte.ts
Normal file
89
frontend/src/lib/stores/multi-tournament.svelte.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
/**
|
||||
* Multi-tournament state for managing multiple active tournaments.
|
||||
*
|
||||
* Keeps all active tournament states in memory keyed by tournament ID
|
||||
* for fast switching. WebSocket subscribes to all active tournaments;
|
||||
* messages route to the correct state by tournament ID.
|
||||
*/
|
||||
|
||||
import { tournament, type WSMessage } from './tournament.svelte';
|
||||
|
||||
export interface TournamentInfo {
|
||||
id: string;
|
||||
name: string;
|
||||
status: 'active' | 'paused' | 'break' | 'completed';
|
||||
}
|
||||
|
||||
class MultiTournamentState {
|
||||
/** List of active tournaments. */
|
||||
tournaments = $state<TournamentInfo[]>([]);
|
||||
|
||||
/** Currently selected tournament ID. */
|
||||
activeId = $state<string | null>(null);
|
||||
|
||||
/** Whether multi-tournament mode is active (2+ tournaments). */
|
||||
get isMulti(): boolean {
|
||||
return this.tournaments.length >= 2;
|
||||
}
|
||||
|
||||
/** The currently active tournament info. */
|
||||
get activeTournament(): TournamentInfo | null {
|
||||
return this.tournaments.find((t) => t.id === this.activeId) ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the list of active tournaments.
|
||||
* If only one, auto-select it. If current selection is gone, select first.
|
||||
*/
|
||||
setTournaments(list: TournamentInfo[]): void {
|
||||
this.tournaments = list;
|
||||
|
||||
if (list.length === 0) {
|
||||
this.activeId = null;
|
||||
} else if (list.length === 1) {
|
||||
this.activeId = list[0].id;
|
||||
} else if (!this.activeId || !list.find((t) => t.id === this.activeId)) {
|
||||
this.activeId = list[0].id;
|
||||
}
|
||||
}
|
||||
|
||||
/** Switch to a different tournament. */
|
||||
switchTo(tournamentId: string): void {
|
||||
if (this.tournaments.find((t) => t.id === tournamentId)) {
|
||||
this.activeId = tournamentId;
|
||||
tournament.id = tournamentId;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Route a WebSocket message to the correct tournament state.
|
||||
* Only updates the singleton state if the message is for the active tournament.
|
||||
*/
|
||||
routeMessage(msg: WSMessage): void {
|
||||
const targetId = msg.tournament_id;
|
||||
|
||||
// Update tournament info from status messages
|
||||
if (msg.type === 'tournament.status' && targetId) {
|
||||
const data = msg.data as { name?: string; status?: string };
|
||||
const existing = this.tournaments.find((t) => t.id === targetId);
|
||||
if (existing) {
|
||||
if (data.name) existing.name = data.name;
|
||||
if (data.status) existing.status = data.status as TournamentInfo['status'];
|
||||
}
|
||||
}
|
||||
|
||||
// Route to singleton state only if it's for the active tournament
|
||||
if (!targetId || targetId === this.activeId) {
|
||||
tournament.handleMessage(msg);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reset state. */
|
||||
reset(): void {
|
||||
this.tournaments = [];
|
||||
this.activeId = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton multi-tournament state. */
|
||||
export const multiTournament = new MultiTournamentState();
|
||||
92
frontend/src/lib/stores/toast.svelte.ts
Normal file
92
frontend/src/lib/stores/toast.svelte.ts
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
/**
|
||||
* Toast notification state using Svelte 5 runes.
|
||||
*
|
||||
* Provides success/info/warning/error notifications with auto-dismiss.
|
||||
* Toasts stack vertically and animate in/out.
|
||||
*/
|
||||
|
||||
export type ToastType = 'success' | 'info' | 'warning' | 'error';
|
||||
|
||||
export interface Toast {
|
||||
id: string;
|
||||
type: ToastType;
|
||||
message: string;
|
||||
duration: number;
|
||||
dismissible: boolean;
|
||||
timer?: ReturnType<typeof setTimeout>;
|
||||
}
|
||||
|
||||
/** Default auto-dismiss durations by type (milliseconds). */
|
||||
const DEFAULT_DURATIONS: Record<ToastType, number> = {
|
||||
success: 3000,
|
||||
info: 4000,
|
||||
warning: 5000,
|
||||
error: 8000
|
||||
};
|
||||
|
||||
let nextId = 0;
|
||||
|
||||
class ToastState {
|
||||
toasts = $state<Toast[]>([]);
|
||||
|
||||
/** Add a success toast (green, 3s auto-dismiss). */
|
||||
success(message: string, duration?: number): string {
|
||||
return this.add('success', message, duration);
|
||||
}
|
||||
|
||||
/** Add an info toast (blue, 4s auto-dismiss). */
|
||||
info(message: string, duration?: number): string {
|
||||
return this.add('info', message, duration);
|
||||
}
|
||||
|
||||
/** Add a warning toast (yellow, 5s auto-dismiss). */
|
||||
warning(message: string, duration?: number): string {
|
||||
return this.add('warning', message, duration);
|
||||
}
|
||||
|
||||
/** Add an error toast (red, 8s auto-dismiss). */
|
||||
error(message: string, duration?: number): string {
|
||||
return this.add('error', message, duration);
|
||||
}
|
||||
|
||||
/** Dismiss a toast by ID. */
|
||||
dismiss(id: string): void {
|
||||
const toast = this.toasts.find((t) => t.id === id);
|
||||
if (toast?.timer) {
|
||||
clearTimeout(toast.timer);
|
||||
}
|
||||
this.toasts = this.toasts.filter((t) => t.id !== id);
|
||||
}
|
||||
|
||||
/** Dismiss all toasts. */
|
||||
dismissAll(): void {
|
||||
for (const t of this.toasts) {
|
||||
if (t.timer) clearTimeout(t.timer);
|
||||
}
|
||||
this.toasts = [];
|
||||
}
|
||||
|
||||
private add(type: ToastType, message: string, duration?: number): string {
|
||||
const id = `toast-${++nextId}`;
|
||||
const dur = duration ?? DEFAULT_DURATIONS[type];
|
||||
|
||||
const toast: Toast = {
|
||||
id,
|
||||
type,
|
||||
message,
|
||||
duration: dur,
|
||||
dismissible: type === 'error'
|
||||
};
|
||||
|
||||
// Auto-dismiss after duration
|
||||
toast.timer = setTimeout(() => {
|
||||
this.dismiss(id);
|
||||
}, dur);
|
||||
|
||||
this.toasts = [...this.toasts, toast];
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
||||
/** Singleton toast state instance. */
|
||||
export const toast = new ToastState();
|
||||
|
|
@ -1,7 +1,110 @@
|
|||
<script lang="ts">
|
||||
import '../app.css';
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import { onMount } from 'svelte';
|
||||
import Header from '$lib/components/Header.svelte';
|
||||
import BottomTabs from '$lib/components/BottomTabs.svelte';
|
||||
import Sidebar from '$lib/components/Sidebar.svelte';
|
||||
import FAB from '$lib/components/FAB.svelte';
|
||||
import Toast from '$lib/components/Toast.svelte';
|
||||
import TournamentTabs from '$lib/components/TournamentTabs.svelte';
|
||||
import { toast } from '$lib/stores/toast.svelte';
|
||||
|
||||
let { children } = $props();
|
||||
|
||||
/** Whether current route is the login page (no shell shown). */
|
||||
let isLoginPage = $derived((page.url?.pathname ?? '') === '/login');
|
||||
|
||||
/** Auth guard: redirect unauthenticated users to /login. */
|
||||
onMount(() => {
|
||||
if (!auth.isAuthenticated && !isLoginPage) {
|
||||
goto('/login');
|
||||
}
|
||||
});
|
||||
|
||||
/** Handle FAB action dispatches. */
|
||||
function handleFABAction(actionId: string): void {
|
||||
switch (actionId) {
|
||||
case 'bust':
|
||||
toast.info('Bust flow: coming in Plan N');
|
||||
break;
|
||||
case 'buyin':
|
||||
toast.info('Buy-in flow: coming in Plan N');
|
||||
break;
|
||||
case 'rebuy':
|
||||
toast.info('Rebuy flow: coming in Plan N');
|
||||
break;
|
||||
case 'addon':
|
||||
toast.info('Add-on flow: coming in Plan N');
|
||||
break;
|
||||
case 'pause-resume':
|
||||
toast.info('Pause/Resume: coming in Plan N');
|
||||
break;
|
||||
default:
|
||||
console.warn(`Unknown FAB action: ${actionId}`);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if isLoginPage}
|
||||
<!-- Login page: no layout shell -->
|
||||
{@render children()}
|
||||
{:else if auth.isAuthenticated}
|
||||
<!-- Authenticated layout shell -->
|
||||
<div class="app-shell">
|
||||
<Header />
|
||||
<Sidebar />
|
||||
<TournamentTabs />
|
||||
|
||||
<main class="main-content">
|
||||
{@render children()}
|
||||
</main>
|
||||
|
||||
<BottomTabs />
|
||||
<FAB onaction={handleFABAction} />
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Redirecting to login -->
|
||||
<div class="redirect-screen">
|
||||
<p>Redirecting to login...</p>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Toast notifications always visible -->
|
||||
<Toast />
|
||||
|
||||
<style>
|
||||
.app-shell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100dvh;
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
/* Offset for fixed header */
|
||||
padding-top: calc(var(--touch-target) + var(--space-2));
|
||||
/* Offset for bottom tabs on mobile */
|
||||
padding-bottom: calc(var(--touch-target) + var(--space-4) + env(safe-area-inset-bottom, 0px));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
/* Desktop: offset for sidebar */
|
||||
@media (min-width: 768px) {
|
||||
.main-content {
|
||||
margin-left: 220px;
|
||||
/* No bottom padding on desktop (no bottom tabs) */
|
||||
padding-bottom: var(--space-4);
|
||||
}
|
||||
}
|
||||
|
||||
.redirect-screen {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100dvh;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,45 +1,26 @@
|
|||
<script lang="ts">
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
/**
|
||||
* Root page redirects to /overview.
|
||||
* The layout shell handles auth guards.
|
||||
*/
|
||||
onMount(() => {
|
||||
if (!auth.isAuthenticated) {
|
||||
goto('/login');
|
||||
}
|
||||
goto('/overview', { replaceState: true });
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if auth.isAuthenticated}
|
||||
<main class="container">
|
||||
<h1>Felt</h1>
|
||||
<p class="text-secondary">Tournament management system</p>
|
||||
<p>Welcome, {auth.operator?.name ?? 'Operator'}</p>
|
||||
</main>
|
||||
{:else}
|
||||
<main class="container">
|
||||
<p>Redirecting to login...</p>
|
||||
</main>
|
||||
{/if}
|
||||
<div class="redirect">
|
||||
<p>Loading...</p>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
.redirect {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100dvh;
|
||||
padding: 1rem;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
min-height: 50dvh;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
109
frontend/src/routes/financials/+page.svelte
Normal file
109
frontend/src/routes/financials/+page.svelte
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
</script>
|
||||
|
||||
<div class="page-content">
|
||||
<h2>Financials</h2>
|
||||
<p class="text-secondary">Prize pool and payout information.</p>
|
||||
|
||||
{#if tournament.financials}
|
||||
{@const fin = tournament.financials}
|
||||
<div class="finance-grid">
|
||||
<div class="finance-card">
|
||||
<span class="finance-label">Total Buy-ins</span>
|
||||
<span class="finance-value currency">{fin.total_buyin.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="finance-card">
|
||||
<span class="finance-label">Total Rebuys</span>
|
||||
<span class="finance-value currency">{fin.total_rebuys.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="finance-card">
|
||||
<span class="finance-label">Total Add-ons</span>
|
||||
<span class="finance-value currency">{fin.total_addons.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="finance-card highlight">
|
||||
<span class="finance-label">Prize Pool</span>
|
||||
<span class="finance-value currency prize">{fin.prize_pool.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="finance-card">
|
||||
<span class="finance-label">House Fee</span>
|
||||
<span class="finance-value currency">{fin.house_fee.toLocaleString()}</span>
|
||||
</div>
|
||||
<div class="finance-card">
|
||||
<span class="finance-label">Paid Positions</span>
|
||||
<span class="finance-value number">{fin.paid_positions}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-state">No financial data available yet.</p>
|
||||
{/if}
|
||||
</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-2);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.finance-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.finance-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.finance-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4);
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.finance-card.highlight {
|
||||
border-color: var(--color-prize);
|
||||
}
|
||||
|
||||
.finance-label {
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.finance-value {
|
||||
font-size: var(--text-xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.finance-value.prize {
|
||||
color: var(--color-prize);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--space-8) 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
107
frontend/src/routes/more/+page.svelte
Normal file
107
frontend/src/routes/more/+page.svelte
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<script lang="ts">
|
||||
import { auth } from '$lib/stores/auth.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
|
||||
function handleLogout(): void {
|
||||
auth.logout();
|
||||
goto('/login');
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="page-content">
|
||||
<h2>More</h2>
|
||||
<p class="text-secondary">Settings and additional options.</p>
|
||||
|
||||
<div class="menu-list">
|
||||
<div class="menu-item">
|
||||
<span class="menu-label">Operator</span>
|
||||
<span class="menu-value">{auth.operator?.name ?? 'Unknown'}</span>
|
||||
</div>
|
||||
<div class="menu-item">
|
||||
<span class="menu-label">Role</span>
|
||||
<span class="menu-value">{auth.operator?.role ?? 'Unknown'}</span>
|
||||
</div>
|
||||
|
||||
<hr class="divider" />
|
||||
|
||||
<button class="menu-item menu-action danger touch-target" onclick={handleLogout}>
|
||||
<span class="menu-label">Sign Out</span>
|
||||
</button>
|
||||
</div>
|
||||
</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-2);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.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;
|
||||
justify-content: space-between;
|
||||
padding: var(--space-4);
|
||||
min-height: var(--touch-target);
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.menu-item:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.menu-action {
|
||||
background: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
font-size: inherit;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
.menu-action:hover {
|
||||
background-color: var(--color-surface-hover);
|
||||
}
|
||||
|
||||
.menu-label {
|
||||
font-size: var(--text-base);
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.menu-value {
|
||||
font-size: var(--text-sm);
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.danger .menu-label {
|
||||
color: var(--color-error);
|
||||
}
|
||||
|
||||
.divider {
|
||||
border: none;
|
||||
border-top: 1px solid var(--color-border);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
92
frontend/src/routes/overview/+page.svelte
Normal file
92
frontend/src/routes/overview/+page.svelte
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
</script>
|
||||
|
||||
<div class="page-content">
|
||||
<h2>Overview</h2>
|
||||
<p class="text-secondary">Tournament dashboard — detailed views coming in Plan N.</p>
|
||||
|
||||
{#if tournament.clock}
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Players</span>
|
||||
<span class="stat-value number">{tournament.remainingPlayers}/{tournament.totalPlayers}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Tables</span>
|
||||
<span class="stat-value number">{tournament.activeTables}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Level</span>
|
||||
<span class="stat-value number">{tournament.clock.level}</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-label">Blinds</span>
|
||||
<span class="stat-value blinds">{tournament.clock.small_blind}/{tournament.clock.big_blind}</span>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="empty-state">No active tournament. Start or join a tournament to see the overview.</p>
|
||||
{/if}
|
||||
</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-2);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-6);
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: var(--space-3);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-1);
|
||||
padding: var(--space-4);
|
||||
background-color: var(--color-surface);
|
||||
border-radius: var(--radius-lg);
|
||||
border: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
font-size: var(--text-xs);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: var(--text-2xl);
|
||||
font-weight: 700;
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
color: var(--color-text-muted);
|
||||
font-style: italic;
|
||||
padding: var(--space-8) 0;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
||||
51
frontend/src/routes/players/+page.svelte
Normal file
51
frontend/src/routes/players/+page.svelte
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
import DataTable from '$lib/components/DataTable.svelte';
|
||||
|
||||
const columns = [
|
||||
{ key: 'name', label: 'Name', sortable: true },
|
||||
{ key: 'status', label: 'Status', sortable: true },
|
||||
{ key: 'chips', label: 'Chips', sortable: true, align: 'right' as const, render: (p: Record<string, unknown>) => (p['chips'] as number).toLocaleString() },
|
||||
{ key: 'table_id', label: 'Table', hideMobile: true, sortable: true },
|
||||
{ key: 'seat', label: 'Seat', hideMobile: true, sortable: true, align: 'center' as const },
|
||||
{ key: 'rebuys', label: 'Rebuys', hideMobile: true, sortable: true, align: 'center' as const }
|
||||
];
|
||||
</script>
|
||||
|
||||
<div class="page-content">
|
||||
<h2>Players</h2>
|
||||
<p class="text-secondary">Registered players and chip counts.</p>
|
||||
|
||||
<DataTable
|
||||
{columns}
|
||||
data={tournament.players}
|
||||
sortable={true}
|
||||
searchable={true}
|
||||
loading={false}
|
||||
emptyMessage="No players registered yet"
|
||||
rowKey={(item) => String(item['id'])}
|
||||
swipeActions={[
|
||||
{ id: 'bust', label: 'Bust', color: 'var(--color-error)', handler: () => {} },
|
||||
{ id: 'rebuy', label: 'Rebuy', color: 'var(--color-primary)', handler: () => {} }
|
||||
]}
|
||||
/>
|
||||
</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-2);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
52
frontend/src/routes/tables/+page.svelte
Normal file
52
frontend/src/routes/tables/+page.svelte
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
<script lang="ts">
|
||||
import { tournament } from '$lib/stores/tournament.svelte';
|
||||
import DataTable from '$lib/components/DataTable.svelte';
|
||||
|
||||
const columns = [
|
||||
{ key: 'number', label: 'Table #', sortable: true, align: 'center' as const },
|
||||
{ key: 'seats', label: 'Seats', sortable: true, align: 'center' as const },
|
||||
{ key: 'player_count', label: 'Players', sortable: true, align: 'center' as const },
|
||||
{ key: 'is_final_table', label: 'Final', hideMobile: true, sortable: true, align: 'center' as const, render: (t: Record<string, unknown>) => t['is_final_table'] ? 'Yes' : '' }
|
||||
];
|
||||
|
||||
let tableData = $derived(
|
||||
tournament.tables.map((t) => ({
|
||||
...t,
|
||||
player_count: t.players.length
|
||||
}))
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="page-content">
|
||||
<h2>Tables</h2>
|
||||
<p class="text-secondary">Active tables and seating.</p>
|
||||
|
||||
<DataTable
|
||||
{columns}
|
||||
data={tableData}
|
||||
sortable={true}
|
||||
searchable={false}
|
||||
loading={false}
|
||||
emptyMessage="No tables set up yet"
|
||||
rowKey={(item) => String(item['id'])}
|
||||
/>
|
||||
</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-2);
|
||||
}
|
||||
|
||||
.text-secondary {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: var(--text-sm);
|
||||
margin-bottom: var(--space-4);
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Reference in a new issue