Moved duplicate folder

This commit is contained in:
2026-04-12 16:27:52 +02:00
parent bed5ee4179
commit a88ea8d931
50 changed files with 0 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('cc-home-page')
export class CCHomePage extends LitElement {
render() {
return html`
<h1>Welcome to the Competition Center Homepage</h1>
<p>Analyze your tracks visually.</p>
`;
}
}
+122
View File
@@ -0,0 +1,122 @@
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators.js";
import { authService, type UserSession } from "../../services/auth.service";
import "../../components/auth-card";
import "../../components/form-input";
import "../../components/ui-button";
import "../../components/notify-bar";
import "../../components/horizontal-divider";
import "../../components/ui-link";
@customElement("login-page")
export class LoginPage extends LitElement {
static styles = css`
:host {
flex: 1;
display: flex;
}
.footer {
text-align: center;
font-size: 0.875rem;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
margin: 0;
}
`;
@state() email = "";
@state() password = "";
@state() error: string | null = null;
@state() loading = false;
async handleLogin() {
this.error = null;
this.loading = true;
try {
//todo const response = await fetch('/api/auth/login', {
const response = await fetch("http://127.0.0.1:8080/auth/login", {
method: "POST",
credentials: "include",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email: this.email, password: this.password }),
});
if (!response.ok) {
this.error =
response.status === 401
? "E-Mail oder Passwort falsch."
: "Login fehlgeschlagen.";
return;
}
const raw = await response.json();
const base64url = raw.accessToken.split(".")[1];
const base64 = base64url.replace(/-/g, "+").replace(/_/g, "/");
const payload = JSON.parse(atob(base64));
authService.setSession(
{
id: payload.sub,
email: payload.email,
role: payload.role ?? null,
permissions: payload.permissions ?? [],
},
raw.accessToken,
);
window.dispatchEvent(new CustomEvent("auth-changed"));
const target = authService.isAdmin ? "/admin" : "/cc";
window.dispatchEvent(
new CustomEvent("nav", {
detail: { path: target },
bubbles: true,
composed: true,
}),
);
} catch (e) {
console.error("Fehler:", e);
this.error = "Netzwerkfehler. Bitte erneut versuchen.";
} finally {
this.loading = false;
}
}
render() {
return html`
<auth-card
heading="Welcome back"
subheading="Sign in to your FlightScore account"
>
<form-input
label="Email"
type="email"
placeholder="you@example.com"
.value=${this.email}
@value-changed=${(e: CustomEvent) => (this.email = e.detail.value)}
></form-input>
<form-input
label="Password"
type="password"
placeholder="Enter your password"
.value=${this.password}
@value-changed=${(e: CustomEvent) => (this.password = e.detail.value)}
></form-input>
<notify-bar type="error" .message=${this.error}></notify-bar>
<ui-button full ?disabled=${this.loading} @click=${this.handleLogin}>
${this.loading ? "Signing in..." : "Sign in"}
</ui-button>
<horizontal-divider></horizontal-divider>
<p class="footer">
No account?
<ui-link href="/register">Create one</ui-link>
</p>
</auth-card>
`;
}
}
+109
View File
@@ -0,0 +1,109 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import '../../components/auth-card.ts';
import '../../components/form-input.ts';
import '../../components/ui-button';
import '../../components/notify-bar.ts';
import '../../components/horizontal-divider';
import '../../components/ui-link';
@customElement('register-page')
export class RegisterPage extends LitElement {
static styles = css`
:host {
flex: 1;
display: flex;
}
.footer {
text-align: center;
font-size: 0.875rem;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
margin: 0;
}
`;
@state() name = '';
@state() email = '';
@state() password = '';
@state() error: string | null = null;
@state() loading = false;
async handleRegister() {
this.error = null;
this.loading = true;
try {
//const response = await fetch('/api/auth/register', {
const response = await fetch('http://127.0.0.1:8080/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: this.name,
email: this.email,
password: this.password,
}),
});
if (!response.ok) {
const err = await response.json().catch(() => null);
this.error = err?.message ?? 'Registration failed';
return;
}
window.dispatchEvent(new CustomEvent('nav', {
detail: { path: '/login' },
bubbles: true,
composed: true,
}));
} catch {
this.error = 'Netzwerkfehler. Bitte erneut versuchen.';
} finally {
this.loading = false;
}
}
render() {
return html`
<auth-card
heading="Create account"
subheading="Get started with FlightScore"
>
<form-input
label="Name"
placeholder="Your full name"
.value=${this.name}
@value-changed=${(e: CustomEvent) => (this.name = e.detail.value)}
></form-input>
<form-input
label="Email"
type="email"
placeholder="you@example.com"
.value=${this.email}
@value-changed=${(e: CustomEvent) => (this.email = e.detail.value)}
></form-input>
<form-input
label="Password"
type="password"
placeholder="Choose a password"
.value=${this.password}
@value-changed=${(e: CustomEvent) => (this.password = e.detail.value)}
></form-input>
<notify-bar type="error" .message=${this.error}></notify-bar>
<ui-button full ?disabled=${this.loading} @click=${this.handleRegister}>
${this.loading ? 'Creating account...' : 'Create account'}
</ui-button>
<horizontal-divider></horizontal-divider>
<p class="footer">
Already have an account?
<ui-link href="/login">Sign in</ui-link>
</p>
</auth-card>
`;
}
}
+12
View File
@@ -0,0 +1,12 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('cc-home-page')
export class CCHomePage extends LitElement {
render() {
return html`
<h1>Welcome to the Competition Center Homepage</h1>
<p>Analyze your tracks visually.</p>
`;
}
}
+229
View File
@@ -0,0 +1,229 @@
// 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";
import "../../components/ui-badge.js";
import { Icons } from "../../components/icons.js";
const TAB_LOADERS: Record<string, () => Promise<unknown>> = {
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>EVENT_NAME</span>
</div>
<h1>EVENT_NAME</h1>
<div class="hero-meta">
<div class="hero-meta-item">
${Icons.icons.map_pin} COMPETITION_LOCATION
</div>
<div class="hero-meta-item">
${Icons.icons.calendar} DATE_FROM_UNTIL_SHORT
</div>
<ui-badge variant="white">EVENT_TYPE</ui-badge>
<ui-badge variant="success">EVENT_STATUS</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,190 @@
// tabs/competition-details.ts
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
import { Icons } from "../../../components/icons";
@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">DATE_FROM DATE_UNTIL</div>
<div class="card-body">
COMPETITION_DETAILS
</div>
</div>
</div>
<div class="sidebar">
<div class="sidebar-card">
<div class="sidebar-card-header">
${Icons.icons.tools}
Competition Details
</div>
<div class="sidebar-card-body">
<div class="detail-row">
${Icons.icons.user}
Director: DIRECTOR_NAME
</div>
<div class="detail-row">
${Icons.icons.globe}
EVENT_TYPE [National, Continental] (CAT 1 | CAT 2)
</div>
<div class="detail-row">
${Icons.icons.desktop}
LOGGER_TYPE [Combined Logger, Marker Event]
</div>
</div>
</div>
<div class="sidebar-card">
<div class="sidebar-card-header">
${Icons.icons.mail}
Contact Details
</div>
<div class="sidebar-card-body">
<div class="detail-row">
${Icons.icons.user}
ORGANISATOR_NAME
</div>
<div class="detail-row">
${Icons.icons.home}
<a href="#" target="_blank" rel="noopener">COMPETITION_WEBSITE</a>
</div>
<div class="detail-row">
${Icons.icons.mail}
<a href="mailto:-">CONTACT_EMAIL</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;
+630
View File
@@ -0,0 +1,630 @@
import { LitElement, html, css } from "lit";
import { customElement, state } from "lit/decorators.js";
import "../components/ui-button";
import "../components/ui-button-secondary";
import "../components/ui-badge";
import "../components/ui-card";
import "../components/card-backdrop";
import "../components/card-header";
import "../components/auth-card";
import "../components/stat-card";
import "../components/icon-card";
import "../components/form-input";
import "../components/horizontal-divider";
import "../components/notify-bar";
import "../components/ui-link";
import "../components/circle-chart";
import "../components/loading-bar";
import "../components/line-chart";
import { Icons } from "../components/icons";
@customElement("dev-page")
export class DevPage extends LitElement {
static styles = css`
:host {
flex: 1;
display: flex;
flex-direction: column;
background: var(--color-bg);
}
.container {
max-width: 1100px;
width: 100%;
margin: 0 auto;
padding: 2.5rem 1.5rem 4rem;
display: flex;
flex-direction: column;
gap: 3rem;
}
.page-header {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding-bottom: 1.5rem;
border-bottom: 1px solid var(--color-border);
}
.page-header h1 {
margin: 0;
font-size: 2rem;
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-text);
}
.page-header p {
margin: 0;
font-size: 0.95rem;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.section {
display: flex;
flex-direction: column;
gap: 1.25rem;
}
.section-head {
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.section-head h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.01em;
color: var(--color-text);
}
.section-head p {
margin: 0;
font-size: 0.85rem;
color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
.component-group {
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
overflow: hidden;
}
.component-label {
padding: 0.6rem 1.25rem;
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
color: color-mix(in srgb, var(--color-text) 45%, transparent);
background: color-mix(in srgb, var(--color-text) 4%, transparent);
border-bottom: 1px solid var(--color-border);
}
.component-preview {
padding: 1.5rem 1.25rem;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem;
}
.component-preview.col {
flex-direction: column;
align-items: stretch;
}
.component-preview.grid-2 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
.component-preview.grid-4 {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
}
.component-preview + .component-label {
border-top: 1px solid var(--color-border);
}
.spacer {
width: 1px;
height: 1.5rem;
background: var(--color-border);
margin: 0 0.25rem;
}
.input-row {
max-width: 360px;
width: 100%;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.card-demo {
display: flex;
justify-content: center;
padding: 2rem;
background:
radial-gradient(
ellipse at 30% 20%,
color-mix(in srgb, var(--color-accent) 8%, transparent) 0%,
transparent 50%
),
var(--color-bg);
border-radius: 0 0 0.75rem 0.75rem;
}
.toggle-row {
display: flex;
align-items: center;
gap: 0.5rem;
}
.toggle-label {
font-size: 0.85rem;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
}
`;
@state() inputValue = "";
@state() notifyVisible = true;
render() {
return html`
<div class="container">
<div class="page-header">
<h1>Component Library</h1>
<p>All available UI components with variants and states</p>
</div>
<div class="section">
<div class="section-head">
<h2>Buttons</h2>
<p>Primary and secondary action buttons</p>
</div>
<div class="component-group">
<div class="component-label">Primary</div>
<div class="component-preview">
<ui-button>Default</ui-button>
<ui-button>
<svg
slot="icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
With Icon
</ui-button>
<ui-button disabled>Disabled</ui-button>
</div>
<div class="component-label">Primary Full Width</div>
<div class="component-preview col">
<ui-button full>Full Width Button</ui-button>
</div>
<div class="component-label">Secondary</div>
<div class="component-preview">
<ui-button-secondary>Default</ui-button-secondary>
<ui-button-secondary>
<svg
slot="icon"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="19" y1="12" x2="5" y2="12" />
<polyline points="12 19 5 12 12 5" />
</svg>
With Icon
</ui-button-secondary>
<ui-button-secondary disabled> Disabled </ui-button-secondary>
</div>
<div class="component-label">Secondary Full Width</div>
<div class="component-preview col">
<ui-button-secondary full>
Full Width Secondary
</ui-button-secondary>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Badges</h2>
<p>Status and category indicators</p>
</div>
<div class="component-group">
<div class="component-label">Variants</div>
<div class="component-preview">
<ui-badge variant="accent">Accent</ui-badge>
<ui-badge variant="success">Success</ui-badge>
<ui-badge variant="warning">Warning</ui-badge>
<ui-badge variant="error">Error</ui-badge>
<ui-badge variant="muted">Muted</ui-badge>
</div>
<div class="component-label">With Icon</div>
<div class="component-preview">
<ui-badge variant="accent">
<span slot="icon">${Icons.icons.stack}</span>
Platform
</ui-badge>
<ui-badge variant="success">
<span slot="icon">${Icons.icons.success}</span>
Live
</ui-badge>
<ui-badge variant="error">
<span slot="icon">${Icons.icons.error}</span>
Offline
</ui-badge>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Notification Bars</h2>
<p>Inline feedback messages</p>
</div>
<div class="component-group">
<div class="component-label">Types</div>
<div class="component-preview col">
<notify-bar
type="info"
message="Competition is currently active."
></notify-bar>
<notify-bar
type="success"
message="Competition successfully created."
></notify-bar>
<notify-bar
type="warning"
message="GPS data incomplete. Some scores may be inaccurate."
></notify-bar>
<notify-bar
type="error"
message="Login failed. Please check your credentials."
></notify-bar>
</div>
<div class="component-label">Non-Dismissible</div>
<div class="component-preview col">
<notify-bar
type="success"
message="This notification cannot be dismissed."
.dismissible=${false}
></notify-bar>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Links</h2>
<p>Navigation links with underline animation</p>
</div>
<div class="component-group">
<div class="component-label">States</div>
<div class="component-preview">
<ui-link href="#">Default Link</ui-link>
<div class="spacer"></div>
<ui-link href="#" active>Active Link</ui-link>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Form Inputs</h2>
<p>Text input fields with labels</p>
</div>
<div class="component-group">
<div class="component-label">Types</div>
<div class="component-preview col">
<div class="input-row">
<form-input
label="Text"
placeholder="Enter some text"
.value=${this.inputValue}
@value-changed=${(e: CustomEvent) =>
(this.inputValue = e.detail.value)}
></form-input>
<form-input
label="Email"
type="email"
placeholder="you@example.com"
></form-input>
<form-input
label="Password"
type="password"
placeholder="Enter your password"
></form-input>
</div>
</div>
<div class="component-label">Live Value</div>
<div class="component-preview">
<span
style="font-size:0.85rem;color:color-mix(in srgb,var(--color-text) 60%,transparent)"
>
Current value: "${this.inputValue}"
</span>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Stat Cards</h2>
<p>Metric display cards</p>
</div>
<div class="component-group">
<div class="component-label">Grid</div>
<div class="component-preview grid-4">
<stat-card value="42" label="Competitions"></stat-card>
<stat-card value="318" label="Pilots"></stat-card>
<stat-card value="1,240" label="Tasks"></stat-card>
<stat-card value="12" label="Countries"></stat-card>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Icon Cards</h2>
<p>Feature highlight cards</p>
</div>
<div class="component-group">
<div class="component-label">Grid</div>
<div class="component-preview grid-2">
<icon-card
heading="Real-Time Scoring"
description="Scores update live as judges submit results."
>
${Icons.icons.clock}
</icon-card>
<icon-card
heading="GPS Integration"
description="Import GPS tracks and calculate distances automatically."
>
${Icons.icons.map_pin}
</icon-card>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Cards</h2>
<p>Container cards and layout primitives</p>
</div>
<div class="component-group">
<div class="component-label">ui-card (default)</div>
<div class="card-demo">
<ui-card>
<card-header
heading="Card Title"
subheading="A subtitle goes here"
></card-header>
<p
style="margin:0;font-size:0.9rem;color:color-mix(in srgb,var(--color-text) 60%,transparent)"
>
This is the default card layout with a header component
inside.
</p>
<horizontal-divider></horizontal-divider>
<ui-button full>Action</ui-button>
</ui-card>
</div>
<div class="component-label">ui-card (centered)</div>
<div class="card-demo">
<ui-card centered>
<p
style="margin:0;font-size:3rem;font-weight:800;letter-spacing:-0.03em;color:var(--color-accent)"
>
404
</p>
<card-header
heading="Centered Content"
subheading="Used for error pages and similar layouts"
></card-header>
<horizontal-divider></horizontal-divider>
<ui-button>Action</ui-button>
</ui-card>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Divider</h2>
<p>Visual separator for content sections</p>
</div>
<div class="component-group">
<div class="component-label">horizontal-divider</div>
<div class="component-preview col">
<p
style="margin:0;font-size:0.85rem;color:color-mix(in srgb,var(--color-text) 60%,transparent)"
>
Content above
</p>
<horizontal-divider></horizontal-divider>
<p
style="margin:0;font-size:0.85rem;color:color-mix(in srgb,var(--color-text) 60%,transparent)"
>
Content below
</p>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Composed: Auth Card</h2>
<p>Pre-composed card for authentication flows</p>
</div>
<div class="component-group">
<div class="component-label">Login Example</div>
<div class="card-demo">
<ui-card>
<card-header
heading="Welcome back"
subheading="Sign in to your FlightScore account"
></card-header>
<form-input
label="Email"
type="email"
placeholder="you@example.com"
></form-input>
<form-input
label="Password"
type="password"
placeholder="Enter your password"
></form-input>
<notify-bar
type="error"
message="Invalid email or password."
></notify-bar>
<ui-button full>Sign in</ui-button>
<horizontal-divider></horizontal-divider>
<p
style="margin:0;text-align:center;font-size:0.875rem;color:color-mix(in srgb,var(--color-text) 60%,transparent)"
>
No account?
<ui-link href="#">Create one</ui-link>
</p>
</ui-card>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Loading Bars</h2>
<p>Progress indicators with variants and states</p>
</div>
<div class="component-group">
<div class="component-label">Variants</div>
<div class="component-preview col">
<loading-bar label="Accent" value="72"></loading-bar>
<loading-bar
label="Success"
value="100"
variant="success"
></loading-bar>
<loading-bar
label="Warning"
value="45"
variant="warning"
></loading-bar>
<loading-bar
label="Error"
value="18"
variant="error"
></loading-bar>
</div>
<div class="component-label">Sizes</div>
<div class="component-preview col">
<loading-bar label="Small" value="60" size="sm"></loading-bar>
<loading-bar label="Medium" value="60" size="md"></loading-bar>
<loading-bar label="Large" value="60" size="lg"></loading-bar>
</div>
<div class="component-label">Indeterminate</div>
<div class="component-preview col">
<loading-bar
label="Loading…"
indeterminate
hideValue
></loading-bar>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Line Chart</h2>
<p>Trend lines with optional area fill</p>
</div>
<div class="component-group">
<div class="component-label">Multi-Series</div>
<div class="component-preview col">
<line-chart
heading="Score Progression"
subtitle="Last 6 Rounds"
xLabel="Round"
yLabel="Score"
showArea
.series=${[
{
label: "Pilot A",
color: "var(--color-accent)",
points: [
{ x: 0, y: 20 },
{ x: 1, y: 420 },
{ x: 2, y: 580 },
{ x: 3, y: 540 },
{ x: 4, y: 710 },
{ x: 5, y: 690 },
{ x: 6, y: 820 },
],
},
{
label: "Pilot B",
color: "#30a46c",
points: [
{ x: 1, y: 380 },
{ x: 2, y: 450 },
{ x: 3, y: 620 },
{ x: 4, y: 590 },
{ x: 5, y: 750 },
{ x: 6, y: 780 },
{ x: 7, y: 780 },
{ x: 8, y: 780 },
{ x: 9, y: 780 },
{ x: 10, y: 780 },
{ x: 11, y: 780 },
{ x: 12, y: 780 },
],
},
]}
></line-chart>
</div>
</div>
</div>
<div class="section">
<div class="section-head">
<h2>Circle Chart</h2>
<p>Donut chart for proportional data</p>
</div>
<div class="component-group">
<div class="component-label">Example</div>
<div class="component-preview col">
<circle-chart
heading="Pilots by Category"
centerText="Pilots"
.segments=${[
{ label: "Sport", value: 124, color: "var(--color-accent)" },
{ label: "Serial", value: 89, color: "#30a46c" },
{ label: "Open", value: 47, color: "#e79d13" },
{ label: "Tandem", value: 18, color: "#e5484d" },
]}
></circle-chart>
</div>
</div>
</div>
</div>
`;
}
}
+399
View File
@@ -0,0 +1,399 @@
// pages/home/home-page.ts (updated)
import { LitElement, html, css } from "lit";
import { customElement } from "lit/decorators.js";
import "../components/ui-button";
import "../components/ui-button-secondary";
import "../components/ui-badge";
import "../components/stat-card";
import "../components/icon-card";
import "../components/ui-link";
import { Icons } from "../components/icons";
@customElement("home-page")
export class HomePage extends LitElement {
static styles = css`
:host {
flex: 1;
display: flex;
flex-direction: column;
}
.hero {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
padding: 5rem 1.5rem 4rem;
background:
radial-gradient(
ellipse at 40% 0%,
color-mix(in srgb, var(--color-accent) 18%, transparent) 0%,
transparent 55%
),
radial-gradient(
ellipse at 80% 60%,
color-mix(in srgb, var(--color-accent) 8%, transparent) 0%,
transparent 50%
),
var(--color-bg);
overflow: hidden;
}
.hero::after {
content: "";
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: var(--color-border);
}
.hero h1 {
margin: 1.5rem 0 0;
font-size: clamp(2.2rem, 5vw, 3.5rem);
font-weight: 800;
letter-spacing: -0.03em;
line-height: 1.1;
color: var(--color-text);
max-width: 700px;
}
.hero h1 span {
background: linear-gradient(
135deg,
var(--color-accent),
color-mix(in srgb, var(--color-accent) 60%, transparent)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-sub {
margin: 1.25rem 0 2rem;
font-size: 1.05rem;
line-height: 1.65;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
max-width: 520px;
}
.hero-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
justify-content: center;
}
section {
padding: 4rem 1.5rem;
max-width: 1100px;
margin: 0 auto;
width: 100%;
}
section + section {
border-top: 1px solid var(--color-border);
}
.section-label {
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.05em;
text-transform: uppercase;
color: var(--color-accent);
margin: 0 0 0.5rem;
}
.section-title {
margin: 0 0 0.5rem;
font-size: 1.75rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text);
}
.section-desc {
margin: 0 0 2.5rem;
font-size: 0.95rem;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
max-width: 520px;
line-height: 1.6;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 1rem;
}
.features-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 1.25rem;
}
.competitions-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.comp-row {
display: flex;
align-items: center;
gap: 1.25rem;
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
border-radius: 0.75rem;
padding: 1.25rem 1.5rem;
transition: border-color 0.25s ease;
cursor: pointer;
}
.comp-row:hover {
border-color: color-mix(in srgb, var(--color-accent) 40%, transparent);
}
.comp-date {
display: flex;
flex-direction: column;
align-items: center;
min-width: 3.5rem;
padding: 0.5rem 0.65rem;
background: color-mix(in srgb, var(--color-accent) 10%, transparent);
border-radius: 0.5rem;
flex-shrink: 0;
}
.comp-date-day {
font-size: 1.25rem;
font-weight: 800;
color: var(--color-accent);
line-height: 1;
}
.comp-date-month {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--color-accent);
margin-top: 0.15rem;
}
.comp-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.comp-name {
font-size: 1rem;
font-weight: 650;
color: var(--color-text);
}
.comp-location {
display: flex;
align-items: center;
gap: 0.3rem;
font-size: 0.82rem;
color: color-mix(in srgb, var(--color-text) 50%, transparent);
}
.comp-location svg {
width: 0.8rem;
height: 0.8rem;
flex-shrink: 0;
}
@media (max-width: 600px) {
.hero {
padding: 3.5rem 1.25rem 3rem;
}
section {
padding: 3rem 1.25rem;
}
.comp-row {
flex-wrap: wrap;
gap: 0.75rem;
}
}
`;
private navigate(path: string) {
this.dispatchEvent(
new CustomEvent("nav", {
detail: { path },
bubbles: true,
composed: true,
}),
);
}
render() {
return html`
<div class="hero">
<ui-badge variant="accent">
<span slot="icon">${Icons.icons.stack}</span>
Live Scoring Platform
</ui-badge>
<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.
</p>
<div class="hero-actions">
<ui-button @click=${() => this.navigate("/competitions")}>
Browse competitions
</ui-button>
<ui-button-secondary @click=${() => this.navigate("/register")}>
Create account
</ui-button-secondary>
</div>
</div>
<section>
<p class="section-label">At a glance</p>
<h2 class="section-title">Platform in numbers</h2>
<p class="section-desc">
FlightScore powers balloon competitions across the globe.
</p>
<div class="stats-grid">
<stat-card value="42" label="Competitions scored"></stat-card>
<stat-card value="318" label="Registered pilots"></stat-card>
<stat-card value="1,240" label="Tasks completed"></stat-card>
<stat-card value="12" label="Countries"></stat-card>
</div>
</section>
<section>
<p class="section-label">Features</p>
<h2 class="section-title">Built for balloon events</h2>
<p class="section-desc">
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."
>
${Icons.icons.clock}
</icon-card>
<icon-card
heading="Task Management"
description="Define and manage competition tasks with support for all standard ballooning task types."
>
${Icons.icons.document_text}
</icon-card>
<icon-card
heading="Pilot Profiles"
description="Every pilot gets a profile with competition history, rankings, and performance statistics."
>
${Icons.icons.users}
</icon-card>
<icon-card
heading="Live Leaderboard"
description="Public leaderboards let spectators follow the action from anywhere in the world."
>
${Icons.icons.desktop}
</icon-card>
<icon-card
heading="GPS Integration"
description="Import GPS tracks and calculate distances to targets automatically for precise scoring."
>
${Icons.icons.map_pin}
</icon-card>
<icon-card
heading="Result Export"
description="Export final standings and detailed score sheets as PDF or CSV for official records."
>
${Icons.icons.table}
</icon-card>
</div>
</section>
<section>
<p class="section-label">Competitions</p>
<h2 class="section-title">Upcoming and recent events</h2>
<p class="section-desc">
Discover balloon competitions scored with FlightScore.
</p>
<div class="competitions-list">
<div
class="comp-row"
@click=${() => this.navigate("/competitions/1")}
>
<div class="comp-date">
<span class="comp-date-day">18</span>
<span class="comp-date-month">Mar</span>
</div>
<div class="comp-info">
<span class="comp-name"> European Balloon Challenge 2026 </span>
<span class="comp-location">
${Icons.icons.map_pin} Salzburg, Austria
</span>
</div>
<ui-badge variant="accent">Upcoming</ui-badge>
</div>
<div
class="comp-row"
@click=${() => this.navigate("/competitions/2")}
>
<div class="comp-date">
<span class="comp-date-day">14</span>
<span class="comp-date-month">Feb</span>
</div>
<div class="comp-info">
<span class="comp-name">Cappadocia Open 2026</span>
<span class="comp-location">
${Icons.icons.map_pin} Goreme, Turkey
</span>
</div>
<ui-badge variant="success">Live</ui-badge>
</div>
<div
class="comp-row"
@click=${() => this.navigate("/competitions/3")}
>
<div class="comp-date">
<span class="comp-date-day">02</span>
<span class="comp-date-month">Feb</span>
</div>
<div class="comp-info">
<span class="comp-name"> Albuquerque Winter Fiesta </span>
<span class="comp-location">
${Icons.icons.map_pin} New Mexico, USA
</span>
</div>
<ui-badge variant="muted">Ended</ui-badge>
</div>
<div
class="comp-row"
@click=${() => this.navigate("/competitions/4")}
>
<div class="comp-date">
<span class="comp-date-day">11</span>
<span class="comp-date-month">Jan</span>
</div>
<div class="comp-info">
<span class="comp-name"> Swiss Alpine Balloon Trophy </span>
<span class="comp-location">
${Icons.icons.map_pin} Chateau-d'Oex, Switzerland
</span>
</div>
<ui-badge variant="error">Canceled</ui-badge>
</div>
</div>
</section>
`;
}
}
+123
View File
@@ -0,0 +1,123 @@
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import '../components/card-backdrop';
import '../components/ui-card';
import '../components/horizontal-divider';
import '../components/ui-button';
@customElement('not-found-page')
export class NotFoundPage extends LitElement {
static styles = css`
:host {
flex: 1;
display: flex;
}
.icon {
width: 3rem;
height: 3rem;
color: color-mix(in srgb, var(--color-text) 30%, transparent);
}
.code {
font-size: 5rem;
font-weight: 800;
letter-spacing: -0.04em;
line-height: 1;
margin: 0;
background: linear-gradient(
135deg,
var(--color-accent),
color-mix(in srgb, var(--color-accent) 50%, transparent)
);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
h2 {
margin: 0;
font-size: 1.35rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text);
}
p {
margin: 0;
font-size: 0.9rem;
line-height: 1.6;
color: color-mix(in srgb, var(--color-text) 55%, transparent);
}
.back-btn {
display: inline-flex;
align-items: center;
gap: 0.45rem;
padding: 0.6rem 1.25rem;
font-size: 0.9rem;
font-weight: 600;
color: #fff;
background: var(--color-accent);
border: none;
border-radius: 0.5rem;
cursor: pointer;
transition:
background 0.25s ease,
box-shadow 0.25s ease,
transform 0.15s ease;
}
.back-btn:hover {
background: color-mix(in srgb, var(--color-accent) 85%, black);
box-shadow: 0 4px 14px
color-mix(in srgb, var(--color-accent) 35%, transparent);
}
.back-btn:active {
transform: scale(0.98);
}
.back-btn svg {
width: 1rem;
height: 1rem;
}
`;
private navigate(path: string) {
this.dispatchEvent(
new CustomEvent('nav', {
detail: { path },
bubbles: true,
composed: true,
})
);
}
render() {
return html`
<card-backdrop>
<ui-card centered>
<p class="code">404</p>
<h2>Page not found</h2>
<p>
The page you are looking for does not exist or has been
moved.
</p>
<horizontal-divider></horizontal-divider>
<ui-button @click=${()=> this.navigate('/')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to home
</ui-button>
</ui-card>
</card-backdrop>
`;
}
}
+12
View File
@@ -0,0 +1,12 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('cc-home-page')
export class CCHomePage extends LitElement {
render() {
return html`
<h1>Welcome to the Target Team Homepage</h1>
<p>Analyze your tracks visually.</p>
`;
}
}