Added a competition page

This commit is contained in:
CodingPhoenixx
2026-02-16 15:34:56 +01:00
parent 67f7f4dd68
commit d8c12c70b6
13 changed files with 873 additions and 69 deletions
-18
View File
@@ -1,18 +0,0 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('competition-page')
export class CompetitionPage extends LitElement {
render() {
return html`
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
<h1>Welcome to the Competitions</h1>
`;
}
}
@@ -0,0 +1,262 @@
// pages/competition-page.ts
import { LitElement, html, css, nothing } from "lit";
import { customElement, state } from "lit/decorators.js";
import type { Tab } from "../../components/ui-tab-bar.js";
import "../../components/ui-tab-bar.js";
type TabModule = { default: () => unknown };
const TAB_LOADERS: Record<string, () => Promise<TabModule>> = {
details: () => import("./tabs/competition-details.js"),
results: () => import("./tabs/competition-results.js"),
tasks: () => import("./tabs/competition-tasks.js"),
noticeboard: () => import("./tabs/competition-noticeboard.js"),
pilots: () => import("./tabs/competition-pilots.js"),
officials: () => import("./tabs/competition-officials.js"),
};
@customElement("competition-page")
export class CompetitionPage extends LitElement {
@state() private activeTab = "details";
@state() private loadedTabs = new Set<string>();
@state() private loadingTab = "";
private tabs: Tab[] = [
{ id: "details", label: "Event Details", icon: "calendar" },
{ id: "results", label: "Results", icon: "trophy" },
{ id: "tasks", label: "Task Data", icon: "target" },
{ id: "noticeboard", label: "Noticeboard", icon: "clipboard" },
{ id: "pilots", label: "Pilots", icon: "users" },
{ id: "officials", label: "Officials", icon: "user" },
];
static styles = css`
:host {
display: block;
font-family: system-ui, -apple-system, sans-serif;
color: var(--color-text);
background: var(--color-bg);
min-height: 100vh;
}
.hero {
background: linear-gradient(135deg, #1e3a5f 0%, #2563eb 100%);
color: white;
padding: 3rem 1.5rem 2rem;
}
.hero-inner {
max-width: 1200px;
margin: 0 auto;
}
.breadcrumb {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
opacity: 0.8;
margin-bottom: 1.5rem;
flex-wrap: wrap;
}
.breadcrumb a {
color: white;
text-decoration: none;
}
.breadcrumb a:hover {
text-decoration: underline;
}
.breadcrumb span {
opacity: 0.6;
}
.hero h1 {
font-size: 1.75rem;
font-weight: 700;
margin: 0 0 1rem;
line-height: 1.3;
}
.hero-meta {
display: flex;
flex-wrap: wrap;
gap: 1.5rem;
align-items: center;
font-size: 0.9rem;
}
.hero-meta-item {
display: flex;
align-items: center;
gap: 0.4rem;
}
.hero-meta-item svg {
width: 16px;
height: 16px;
flex-shrink: 0;
}
.tab-content {
max-width: 1200px;
margin: 0 auto;
padding: 1.5rem;
}
.loading {
display: flex;
align-items: center;
justify-content: center;
padding: 4rem;
color: color-mix(in srgb, var(--color-text) 45%, transparent);
font-size: 0.9rem;
}
.spinner {
width: 1.25rem;
height: 1.25rem;
border: 2px solid var(--color-border);
border-top-color: var(--color-accent);
border-radius: 50%;
animation: spin 0.6s linear infinite;
margin-right: 0.75rem;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
`;
connectedCallback() {
super.connectedCallback();
this.loadTab(this.activeTab);
}
private async loadTab(id: string) {
if (this.loadedTabs.has(id)) return;
const loader = TAB_LOADERS[id];
if (!loader) return;
this.loadingTab = id;
try {
await loader();
this.loadedTabs = new Set([...this.loadedTabs, id]);
} catch (e) {
console.error(`Failed to load tab "${id}":`, e);
} finally {
this.loadingTab = "";
}
}
private handleTabChange(e: CustomEvent<{ tab: string }>) {
this.activeTab = e.detail.tab;
this.loadTab(e.detail.tab);
}
private renderTabContent() {
if (this.loadingTab === this.activeTab) {
return html`
<div class="loading">
<div class="spinner"></div>
Loading…
</div>
`;
}
switch (this.activeTab) {
case "details":
return html`<competition-details></competition-details>`;
case "results":
return html`<competition-results></competition-results>`;
case "tasks":
return html`<competition-tasks></competition-tasks>`;
case "noticeboard":
return html`<competition-noticeboard></competition-noticeboard>`;
case "pilots":
return html`<competition-pilots></competition-pilots>`;
case "officials":
return html`<competition-officials></competition-officials>`;
default:
return nothing;
}
}
render() {
return html`
<div class="hero">
<div class="hero-inner">
<div class="breadcrumb">
<a href="/">Home</a>
<span>/</span>
<a href="/competitions">Competitions</a>
<span>/</span>
<span
>22. Schweizermeisterschaften Heissluftballon / Swiss
Cup</span
>
</div>
<h1>
22. Schweizermeisterschaften Heissluftballon / Swiss Cup
</h1>
<div class="hero-meta">
<div class="hero-meta-item">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M15 10.5a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 10.5c0 7.142-7.5 11.25-7.5 11.25S4.5 17.642 4.5 10.5a7.5 7.5 0 1 1 15 0Z"
/>
</svg>
Langenthal / BE, Switzerland
</div>
<div class="hero-meta-item">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5"
/>
</svg>
13 17 May 2026
</div>
<ui-badge variant="accent">CAT2</ui-badge>
<ui-badge variant="success">Upcoming</ui-badge>
</div>
</div>
</div>
<ui-tab-bar
.tabs=${this.tabs}
.active=${this.activeTab}
@tab-change=${this.handleTabChange}
></ui-tab-bar>
<div class="tab-content">${this.renderTabContent()}</div>
`;
}
}
@@ -0,0 +1,213 @@
// tabs/competition-details.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-details")
export class CompetitionDetails extends LitElement {
static styles = css`
:host {
display: block;
}
.layout {
display: grid;
grid-template-columns: 1fr 380px;
gap: 1.5rem;
align-items: start;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
}
.main-card {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
overflow: hidden;
}
.card-date {
padding: 1rem 1.5rem;
font-weight: 600;
font-size: 0.95rem;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg);
color: var(--color-text);
}
.card-body {
padding: 1.5rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
font-size: 0.9rem;
line-height: 1.6;
color: color-mix(in srgb, var(--color-text) 65%, transparent);
}
.card-body a {
color: var(--color-accent);
text-decoration: none;
}
.card-body a:hover {
text-decoration: underline;
}
.stats-row {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.sidebar {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.sidebar-card {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
overflow: hidden;
}
.sidebar-card-header {
padding: 1rem 1.5rem;
font-weight: 600;
font-size: 0.95rem;
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--color-text);
}
.sidebar-card-header svg {
width: 18px;
height: 18px;
color: var(--color-accent);
}
.sidebar-card-body {
padding: 1.25rem 1.5rem;
display: flex;
flex-direction: column;
gap: 1rem;
}
.detail-row {
display: flex;
align-items: center;
gap: 0.75rem;
font-size: 0.875rem;
color: color-mix(in srgb, var(--color-text) 65%, transparent);
}
.detail-row svg {
width: 16px;
height: 16px;
color: var(--color-accent);
flex-shrink: 0;
}
.detail-row a {
color: var(--color-accent);
text-decoration: none;
}
.detail-row a:hover {
text-decoration: underline;
}
`;
render() {
return html`
<div class="layout">
<div>
<div class="main-card">
<div class="card-date">13 May 2026 17 May 2026</div>
<div class="card-body">
<span>Schweizermeisterschaften 2026</span>
<span>Open Swiss Nationals &amp; Swiss Cup</span>
<span
>Location: Berufsfachschule BZL Langenthal</span
>
<span
>Official announcement can be found in the
<a href="#">ENB</a></span
>
<span
>Registration via
<a
href="https://www.smhl.ch/de/registration"
target="_blank"
rel="noopener"
>https://www.smhl.ch/de/registration</a
></span
>
</div>
</div>
<div class="stats-row">
<stat-card value="32" label="Registered Pilots"></stat-card>
<stat-card value="12" label="Tasks Planned"></stat-card>
<stat-card value="5" label="Days"></stat-card>
<stat-card value="CAT2" label="FAI Category"></stat-card>
</div>
</div>
<div class="sidebar">
<div class="sidebar-card">
<div class="sidebar-card-header">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17 17.25 21A2.652 2.652 0 0 0 21 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 1 1-3.586-3.586l6.837-5.63m5.108-.233c.55-.164 1.163-.188 1.743-.14a4.5 4.5 0 0 0 4.486-6.336l-3.276 3.277a3.004 3.004 0 0 1-2.25-2.25l3.276-3.276a4.5 4.5 0 0 0-6.336 4.486c.049.58.025 1.193-.14 1.743"/></svg>
Competition Details
</div>
<div class="sidebar-card-body">
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/></svg>
Director: Claude Weber
</div>
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M12 21a9.004 9.004 0 0 0 8.716-6.747M12 21a9.004 9.004 0 0 1-8.716-6.747M12 21c2.485 0 4.5-4.03 4.5-9S14.485 3 12 3m0 18c-2.485 0-4.5-4.03-4.5-9S9.515 3 12 3m0 0a8.997 8.997 0 0 1 7.843 4.582M12 3a8.997 8.997 0 0 0-7.843 4.582m15.686 0A11.953 11.953 0 0 1 12 10.5c-2.998 0-5.74-1.1-7.843-2.918m15.686 0A8.959 8.959 0 0 1 21 12c0 .778-.099 1.533-.284 2.253m0 0A17.919 17.919 0 0 1 12 16.5a17.92 17.92 0 0 1-8.716-2.247m0 0A8.966 8.966 0 0 1 3 12c0-1.264.26-2.467.73-3.56"/></svg>
FAI sanctioned CAT2 / CIA Sporting event
</div>
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 0 1-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0 1 15 18.257V17.25m6-12V15a2.25 2.25 0 0 1-2.25 2.25H5.25A2.25 2.25 0 0 1 3 15V5.25A2.25 2.25 0 0 1 5.25 3h13.5A2.25 2.25 0 0 1 21 5.25Z"/></svg>
Combined Logger/Marker Event
</div>
</div>
</div>
<div class="sidebar-card">
<div class="sidebar-card-header">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>
Contact Details
</div>
<div class="sidebar-card-body">
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z"/></svg>
Werner BEYELER
</div>
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m2.25 12 8.954-8.955c.44-.439 1.152-.439 1.591 0L21.75 12M4.5 9.75v10.125c0 .621.504 1.125 1.125 1.125H9.75v-4.875c0-.621.504-1.125 1.125-1.125h2.25c.621 0 1.125.504 1.125 1.125V21h4.125c.621 0 1.125-.504 1.125-1.125V9.75M8.25 21h8.25"/></svg>
<a href="#" target="_blank" rel="noopener">Website</a>
</div>
<div class="detail-row">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"/></svg>
<a href="mailto:bewerner@bluewin.ch">bewerner@bluewin.ch</a>
</div>
</div>
</div>
</div>
</div>
`;
}
}
export default CompetitionDetails;
@@ -0,0 +1,42 @@
// tabs/competition-noticeboard.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-noticeboard")
export class CompetitionNoticeboard extends LitElement {
static styles = css`
:host {
display: block;
}
.placeholder {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 3rem;
text-align: center;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.placeholder p:first-child {
font-size: 1.1rem;
font-weight: 500;
}
.placeholder p:last-child {
font-size: 0.875rem;
margin-top: 0.5rem;
}
`;
render() {
return html`
<div class="placeholder">
<p>Noticeboard</p>
<p>Content will appear here once the competition is underway.</p>
</div>
`;
}
}
export default CompetitionNoticeboard;
@@ -0,0 +1,41 @@
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-officials")
export class CompetitionOfficials extends LitElement {
static styles = css`
:host {
display: block;
}
.placeholder {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 3rem;
text-align: center;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.placeholder p:first-child {
font-size: 1.1rem;
font-weight: 500;
}
.placeholder p:last-child {
font-size: 0.875rem;
margin-top: 0.5rem;
}
`;
render() {
return html`
<div class="placeholder">
<p>Officials</p>
<p>Content will appear here once the competition is underway.</p>
</div>
`;
}
}
export default CompetitionOfficials;
@@ -0,0 +1,42 @@
// tabs/competition-pilots.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-pilots")
export class CompetitionPilots extends LitElement {
static styles = css`
:host {
display: block;
}
.placeholder {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 3rem;
text-align: center;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.placeholder p:first-child {
font-size: 1.1rem;
font-weight: 500;
}
.placeholder p:last-child {
font-size: 0.875rem;
margin-top: 0.5rem;
}
`;
render() {
return html`
<div class="placeholder">
<p>Pilots</p>
<p>Content will appear here once the competition is underway.</p>
</div>
`;
}
}
export default CompetitionPilots;
@@ -0,0 +1,42 @@
// tabs/competition-results.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-results")
export class CompetitionResults extends LitElement {
static styles = css`
:host {
display: block;
}
.placeholder {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 3rem;
text-align: center;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.placeholder p:first-child {
font-size: 1.1rem;
font-weight: 500;
}
.placeholder p:last-child {
font-size: 0.875rem;
margin-top: 0.5rem;
}
`;
render() {
return html`
<div class="placeholder">
<p>Results</p>
<p>Content will appear here once the competition is underway.</p>
</div>
`;
}
}
export default CompetitionResults;
@@ -0,0 +1,42 @@
// tabs/competition-tasks.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
@customElement("competition-tasks")
export class CompetitionTasks extends LitElement {
static styles = css`
:host {
display: block;
}
.placeholder {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 3rem;
text-align: center;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.placeholder p:first-child {
font-size: 1.1rem;
font-weight: 500;
}
.placeholder p:last-child {
font-size: 0.875rem;
margin-top: 0.5rem;
}
`;
render() {
return html`
<div class="placeholder">
<p>Task Data</p>
<p>Content will appear here once the competition is underway.</p>
</div>
`;
}
}
export default CompetitionTasks;
+3 -3
View File
@@ -254,7 +254,7 @@ export class HomePage extends LitElement {
</svg>
Live Scoring Platform
</ui-badge>
<h1>Precision scoring for <span>balloon competitions</span></h1>
<h1>Transparent scoring for <span>balloon competitions</span></h1>
<p class="hero-sub">
Track tasks, manage participants, and deliver real-time
results for hot air balloon competitions worldwide.
@@ -292,12 +292,12 @@ export class HomePage extends LitElement {
<p class="section-label">Features</p>
<h2 class="section-title">Built for balloon events</h2>
<p class="section-desc">
Everything organizers, judges, and pilots need in one place.
Everything organizers, officials, juries and pilots need in one place.
</p>
<div class="features-grid">
<icon-card
heading="Real-Time Scoring"
description="Scores update live as judges submit results. Pilots and spectators always see the latest standings."
description="Scores update live as scorers submit results. Pilots and spectators always see the latest standings."
>
<svg slot="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />