Moved duplicate folder
This commit is contained in:
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user