diff --git a/public/auth/hero.jpeg b/public/auth/hero.jpeg new file mode 100644 index 0000000..e2d3e70 Binary files /dev/null and b/public/auth/hero.jpeg differ diff --git a/public/locales/de.json b/public/locales/de.json new file mode 100644 index 0000000..f297559 --- /dev/null +++ b/public/locales/de.json @@ -0,0 +1,14 @@ +{ + "login.eyebrow": "Wettbewerbsbüro", + "login.title": "Einloggen", + "login.subtitle": "Bitte gebe deine Zugangsdaten ein um dich im digitalen Wettbewerbsbüro anzumelden.", + "login.email": "E-MAIL", + "login.password": "PASSWORT", + "login.remember": "Auf diesem Gerät angemeldet bleiben", + "login.forgot": "Passwort vergessen?", + "login.submit": "Anmelden", + "login.loading": "Lädt…", + "login.error.credentials": "E-Mail oder Passwort falsch.", + "login.error.failed": "Login fehlgeschlagen.", + "login.error.network": "Netzwerkfehler. Bitte erneut versuchen." +} diff --git a/public/locales/en.json b/public/locales/en.json new file mode 100644 index 0000000..034c009 --- /dev/null +++ b/public/locales/en.json @@ -0,0 +1,14 @@ +{ + "login.eyebrow": "Competition Center", + "login.title": "Sign in", + "login.subtitle": "Please enter your login credentials to log in to the digital competition office.", + "login.email": "EMAIL", + "login.password": "PASSWORD", + "login.remember": "Keep me signed in on this device", + "login.forgot": "Forgot password?", + "login.submit": "Sign in", + "login.loading": "Loading…", + "login.error.credentials": "Incorrect email or password.", + "login.error.failed": "Login failed.", + "login.error.network": "Network error. Please try again." +} diff --git a/public/logo.svg b/public/logo.svg index 893eebc..9c85a67 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,25 +1,79 @@ - - - - - - - - - - - - - - + + + + + + + + + + + + - \ No newline at end of file + + + + + diff --git a/src/app-root.ts b/src/app-root.ts index b06b650..c507dde 100644 --- a/src/app-root.ts +++ b/src/app-root.ts @@ -3,9 +3,6 @@ import { customElement, state } from 'lit/decorators.js'; import { Router } from './router/router'; import { authService } from './services/auth.service'; import type { AuthLevel } from './router/router'; -import './components/footer-bar.js'; -import './components/nav-bar'; -import './components/cc/cc-nav-bar'; //import './components/tt/tt-nav-bar'; @customElement('app-root') @@ -33,22 +30,6 @@ export class AppRoot extends LitElement { return document.createElement('home-page'); }, }, - { - path: '/competitions', - auth: 'public', - view: async () => { - await import('./pages/competition/competition-page.js'); - return document.createElement('competition-page'); - }, - }, - { - path: '/dev', - auth: 'public', - view: async () => { - await import('./pages/dev-page.js'); - return document.createElement('dev-page'); - }, - }, { path: '/login', auth: 'public', @@ -66,16 +47,6 @@ export class AppRoot extends LitElement { }, }, - // ── Target Team (Code-geschützt) ───────────────────── - { - path: '/tt', - auth: 'code', - view: async () => { - await import('./pages/tt/tt-home-page.js'); - return document.createElement('tt-home-page'); - }, - }, - // ── Competition Center (User-Login) ────────────────── { path: '/cc', @@ -85,16 +56,6 @@ export class AppRoot extends LitElement { return document.createElement('cc-home-page'); }, }, - - // ── System Admin ───────────────────────────────────── - { - path: '/admin', - auth: 'admin', - view: async () => { - await import('./pages/admin/admin-home-page.js'); - return document.createElement('admin-home-page'); - }, - }, ], // Guard-Logik diff --git a/src/components/_components b/src/components/_components deleted file mode 100644 index 4fe818b..0000000 --- a/src/components/_components +++ /dev/null @@ -1,206 +0,0 @@ -# Component Library - -## ui-button - -| Attribute | Type | Description | -|------------|-----------|----------------------------| -| `disabled` | `boolean` | Disables the button | -| `full` | `boolean` | Full-width layout | - -| Slot | Description | -|-----------|---------------------| -| `default` | Button label text | -| `icon` | Leading SVG icon | - ---- - -## ui-button-secondary - -| Attribute | Type | Description | -|------------|-----------|----------------------------| -| `disabled` | `boolean` | Disables the button | -| `full` | `boolean` | Full-width layout | - -| Slot | Description | -|-----------|---------------------| -| `default` | Button label text | -| `icon` | Leading SVG icon | - ---- - -## ui-badge - -| Attribute | Type | Values | -|-----------|----------|------------------------------------------------------| -| `variant` | `string` | `accent` · `success` · `warning` · `error` · `muted` | - -| Slot | Description | -|-----------|-------------------| -| `default` | Badge label text | -| `icon` | Leading SVG icon | - ---- - -## notify-bar - -| Attribute | Type | Values / Description | -|---------------|-----------|-----------------------------------------| -| `type` | `string` | `success` · `warning` · `error` | -| `message` | `string` | Notification text | -| `dismissible` | `boolean` | Whether the bar can be dismissed (default `true`) | - ---- - -## ui-link - -| Attribute | Type | Description | -|-----------|-----------|--------------------------------| -| `href` | `string` | Navigation target | -| `active` | `boolean` | Renders in active/highlighted state | - -| Slot | Description | -|-----------|-------------| -| `default` | Link text | - ---- - -## form-input - -| Attribute | Type | Description | -|---------------|----------|------------------------------| -| `label` | `string` | Field label | -| `type` | `string` | `text` · `email` · `password` | -| `placeholder` | `string` | Placeholder text | -| `value` | `string` | Current input value | - -| Event | Detail | Description | -|-----------------|----------------|-------------------------| -| `value-changed` | `{ value: string }` | Fires on input change | - ---- - -## stat-card - -| Attribute | Type | Description | -|-----------|----------|----------------------| -| `value` | `string` | Displayed metric | -| `label` | `string` | Metric description | - ---- - -## icon-card - -| Attribute | Type | Description | -|---------------|----------|--------------------| -| `heading` | `string` | Card title | -| `description` | `string` | Card body text | - -| Slot | Description | -|--------|----------------| -| `icon` | Leading SVG icon | - ---- - -## ui-card - -| Attribute | Type | Description | -|------------|-----------|-----------------------------| -| `centered` | `boolean` | Centers all child content | - -| Slot | Description | -|-----------|----------------------| -| `default` | Arbitrary content | - ---- - -## card-header - -| Attribute | Type | Description | -|--------------|----------|---------------| -| `heading` | `string` | Title text | -| `subheading` | `string` | Subtitle text | - ---- - -## card-backdrop - -_No attributes or slots observed in usage._ - ---- - -## horizontal-divider - -_No attributes. Renders a visual separator line._ - ---- - -## loading-bar - -| Attribute | Type | Values / Description | -|-----------------|-----------|-----------------------------------------------| -| `label` | `string` | Text label next to the bar | -| `value` | `number` | Progress percentage (`0`–`100`) | -| `variant` | `string` | _(default)_ · `success` · `warning` · `error` | -| `size` | `string` | `sm` · `md` · `lg` | -| `indeterminate` | `boolean` | Infinite animation, no fixed value | -| `hideValue` | `boolean` | Hides the percentage label | - ---- - -## line-chart - -| Attribute | Type | Description | -|------------|-----------|------------------------------------| -| `heading` | `string` | Chart title | -| `subtitle` | `string` | Chart subtitle | -| `xLabel` | `string` | X-axis label | -| `yLabel` | `string` | Y-axis label | -| `showArea` | `boolean` | Fill area beneath lines | -| `series` | `Array` | Array of series objects (see below) | - -**Series object:** - -| Key | Type | Description | -|----------|----------|------------------------------------------| -| `label` | `string` | Legend label | -| `color` | `string` | CSS color value | -| `points` | `Array` | Array of `{ x: number, y: number }` | - ---- - -## circle-chart - -| Attribute | Type | Description | -|--------------|----------|--------------------------------------| -| `heading` | `string` | Chart title | -| `centerText` | `string` | Text rendered in the donut center | -| `segments` | `Array` | Array of segment objects (see below) | - -**Segment object:** - -| Key | Type | Description | -|---------|----------|--------------------| -| `label` | `string` | Legend label | -| `value` | `number` | Segment value | -| `color` | `string` | CSS color value | - ---- - -## ui-tab-bar - -| Attribute | Type | Description | -|------------|----------|------------------------------------| -| `tabs` | `Array` | Array of tab objects (see below) | -| `active` | `string` | The id of the currently active tab | - -**Tab object:** - -| Key | Type | Description | -|----------|----------|---------------------------| -| `id` | `string` | Tab identifier | -| `label` | `string` | Tab display text | -| `icon` | `string` | Optional icon identifier | - -| Event | Detail | Description | -|---------------|--------------------|-----------------------------| -| `tab-change` | `{ tab: string }` | Fired when tab is selected | \ No newline at end of file diff --git a/src/components/auth-card.ts b/src/components/auth-card.ts deleted file mode 100644 index 952221c..0000000 --- a/src/components/auth-card.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; -import './card-backdrop'; -import './ui-card'; -import './card-header'; - -@customElement('auth-card') -export class AuthCard extends LitElement { - static styles = css` - :host { - flex: 1; - display: flex; - } - `; - - @property() heading = ''; - @property() subheading = ''; - - render() { - return html` - - - - - - - `; - } -} \ No newline at end of file diff --git a/src/components/button/primary-button.ts b/src/components/button/primary-button.ts new file mode 100644 index 0000000..c261f8c --- /dev/null +++ b/src/components/button/primary-button.ts @@ -0,0 +1,84 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import "../hero-icon.js"; + +@customElement("primary-button") +export class PrimaryButton extends LitElement { + @property({ type: String }) label = "Button"; + @property({ type: String }) icon = ""; + @property({ type: String }) iconPosition: "left" | "right" = "right"; + @property({ type: Boolean }) disabled = false; + @property({ attribute: false }) onClick?: () => void; + + static styles = css` + :host { + display: inline-block; + } + + button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + padding: 0.625rem 1.25rem; + background-color: var(--color-accent, #2b6cb0); + color: var(--color-on-accent, #ffffff); + border: none; + border-radius: 0.375rem; + font-family: "Inter", system-ui, sans-serif; + font-size: var(--font-size-base, 0.9375rem); + font-weight: var(--font-weight-medium, 500); + cursor: pointer; + transition: filter 0.2s ease, background-color 0.25s ease; + } + + button:hover { + filter: brightness(1.1); + } + + button:active { + filter: brightness(0.92); + } + + button:focus-visible { + outline: 2px solid var(--color-accent, #2b6cb0); + outline-offset: 3px; + } + + button:disabled { + opacity: 0.45; + cursor: not-allowed; + filter: none; + } + + button:disabled:hover, + button:disabled:active { + filter: none; + } + + hero-icon { + width: 1.1rem; + height: 1.1rem; + flex-shrink: 0; + } + `; + + private _handleClick() { + this.onClick?.(); + } + + render() { + const iconEl = this.icon + ? html`` + : null; + + return html` + + `; + } +} diff --git a/src/components/button/secondary-button.ts b/src/components/button/secondary-button.ts new file mode 100644 index 0000000..89ae820 --- /dev/null +++ b/src/components/button/secondary-button.ts @@ -0,0 +1,84 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; + +@customElement("secondary-button") +export class SecondaryButton extends LitElement { + @property({ type: String }) label = "Button"; + @property({ type: String }) icon = ""; + @property({ type: String }) iconPosition: "left" | "right" = "right"; + @property({ type: Boolean }) disabled = false; + @property({ attribute: false }) onClick?: () => void; + + static styles = css` + :host { + display: inline-block; + } + + button { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + width: 100%; + padding: 0.625rem 1.25rem; + background-color: transparent; + color: var(--color-text, #111); + border: 1.5px solid var(--color-border-strong, #aaa); + border-radius: 0.375rem; + font-family: "Inter", system-ui, sans-serif; + font-size: var(--font-size-base, 0.9375rem); + font-weight: var(--font-weight-medium, 500); + cursor: pointer; + transition: background-color 0.2s ease, border-color 0.25s ease, color 0.25s ease; + } + + button:hover { + background-color: var(--color-border, #ddd); + } + + button:active { + background-color: var(--color-border, #ddd); + filter: brightness(0.95); + } + + button:focus-visible { + outline: 2px solid var(--color-accent, #2b6cb0); + outline-offset: 3px; + } + + button:disabled { + opacity: 0.45; + cursor: not-allowed; + filter: none; + } + + button:disabled:hover, + button:disabled:active { + background-color: transparent; + } + + hero-icon { + width: 1.1rem; + height: 1.1rem; + flex-shrink: 0; + } + `; + + private _handleClick() { + this.onClick?.(); + } + + render() { + const iconEl = this.icon + ? html`` + : null; + + return html` + + `; + } +} diff --git a/src/components/card-backdrop.ts b/src/components/card-backdrop.ts deleted file mode 100644 index 27eac12..0000000 --- a/src/components/card-backdrop.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -@customElement('card-backdrop') -export class CardBackdrop extends LitElement { - static styles = css` - :host { - flex: 1; - display: flex; - justify-content: center; - align-items: center; - background: - radial-gradient( - ellipse at 30% 20%, - color-mix(in srgb, var(--color-accent) 15%, transparent) 0%, - transparent 50% - ), - radial-gradient( - ellipse at 70% 80%, - color-mix(in srgb, var(--color-accent) 10%, transparent) 0%, - transparent 50% - ), - var(--color-bg); - padding: 1.5rem; - } - `; - - render() { - return html``; - } -} \ No newline at end of file diff --git a/src/components/card-header.ts b/src/components/card-header.ts deleted file mode 100644 index 3293f8b..0000000 --- a/src/components/card-header.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('card-header') -export class CardHeader extends LitElement { - static styles = css` - :host { - display: flex; - flex-direction: column; - gap: 0.35rem; - margin-bottom: 0.25rem; - } - - h2 { - margin: 0; - font-size: 1.5rem; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--color-text); - } - - p { - margin: 0; - font-size: 0.875rem; - color: color-mix(in srgb, var(--color-text) 60%, transparent); - } - `; - - @property() heading = ''; - @property() subheading = ''; - - render() { - return html` -

${this.heading}

- ${this.subheading ? html`

${this.subheading}

` : null} - `; - } -} \ No newline at end of file diff --git a/src/components/cc/cc-nav-bar.css b/src/components/cc/cc-nav-bar.css deleted file mode 100644 index bc9fb06..0000000 --- a/src/components/cc/cc-nav-bar.css +++ /dev/null @@ -1,76 +0,0 @@ -nav { - backdrop-filter: blur(10px); - background: var(--color-bg-nav); - border-bottom: 1px solid var(--color-border); - display: flex; - justify-content: space-between; - align-items: center; - padding: 0.75rem 1.5rem; - position: sticky; - top: 0; - z-index: 100; -} - -.brand { - font-weight: 700; - font-size: 1.1rem; - user-select: none; - width: fit-content; - display: flex; - justify-content: center; - align-items: center; - padding-right: 1rem; - -} - -.brand img { - width: auto; - height: 2rem; - padding-right: 1rem; -} - -.links { - display: flex; - gap: 1.25rem; - align-items: center; -} - -.links a { - text-decoration: none; -} - -a { - color: var(--color-text); - font-weight: 500; - position: relative; -} - -a::after { - content: ''; - position: absolute; - bottom: -6px; - left: 0; - width: 0%; - height: 2px; - background: var(--color-accent); - transition: 0.3s ease; -} - -a:hover::after { - width: 100%; -} - -button { - background: none; - border: 1px solid var(--color-border); - border-radius: 5px; - padding: 0.4rem 0.6rem; - color: var(--color-text); - cursor: pointer; - transition: all 0.3s ease; -} - -button:hover { - border-color: var(--color-accent); - color: var(--color-accent); -} \ No newline at end of file diff --git a/src/components/cc/cc-nav-bar.ts b/src/components/cc/cc-nav-bar.ts deleted file mode 100644 index 6b4a0ff..0000000 --- a/src/components/cc/cc-nav-bar.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { LitElement, html, css, unsafeCSS } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import styles from './cc-nav-bar.css?inline'; -import '../ui-link'; - -@customElement('cc-nav-bar') -export class CCNavBar extends LitElement { - static styles = css`${unsafeCSS(styles)}`; - - @state() theme: 'light' | 'dark' = - (localStorage.getItem('theme') as 'light' | 'dark') || - (window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'); - - firstUpdated() { - document.documentElement.setAttribute('data-theme', this.theme); - } - - toggleTheme() { - this.theme = this.theme === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-theme', this.theme); - localStorage.setItem('theme', this.theme); - } - - navigate(path: string) { - this.dispatchEvent( - new CustomEvent('nav', { detail: { path }, bubbles: true, composed: true }) - ); - } - - render() { - return html` - - `; - } -} \ No newline at end of file diff --git a/src/components/circle-chart.ts b/src/components/circle-chart.ts deleted file mode 100644 index f478cab..0000000 --- a/src/components/circle-chart.ts +++ /dev/null @@ -1,291 +0,0 @@ -import { LitElement, html, css, svg } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; - -export interface CircleSegment { - label: string; - value: number; - color: string; -} - -@customElement('circle-chart') -export class CircleChart extends LitElement { - static styles = css` - :host { - display: block; - } - - .chart-wrapper { - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 0.75rem; - padding: 1.75rem; - display: flex; - flex-direction: column; - gap: 1.5rem; - transition: border-color 0.25s ease; - } - - .chart-wrapper:hover { - border-color: color-mix( - in srgb, - var(--color-accent) 40%, - transparent - ); - } - - .title { - font-size: 1rem; - font-weight: 650; - color: var(--color-text); - margin: 0; - } - - .chart-body { - display: flex; - align-items: center; - gap: 2rem; - flex-wrap: wrap; - justify-content: center; - } - - .svg-container { - position: relative; - width: 160px; - height: 160px; - flex-shrink: 0; - } - - .center-label { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - pointer-events: none; - } - - .center-value { - font-size: 1.5rem; - font-weight: 800; - letter-spacing: -0.03em; - color: var(--color-text); - } - - .center-text { - font-size: 0.72rem; - font-weight: 500; - color: color-mix(in srgb, var(--color-text) 45%, transparent); - text-transform: uppercase; - letter-spacing: 0.04em; - } - - svg { - transform: rotate(-90deg); - width: 100%; - height: 100%; - } - - circle { - transition: - stroke-dasharray 0.6s cubic-bezier(0.22, 1, 0.36, 1), - opacity 0.25s ease; - cursor: pointer; - } - - circle:hover { - opacity: 0.8; - filter: brightness(1.15); - } - - .tooltip { - position: absolute; - pointer-events: none; - background: var(--color-bg-nav, #1a1a2e); - border: 1px solid var(--color-border, #333); - border-radius: 0.5rem; - padding: 0.35rem 0.65rem; - display: flex; - align-items: center; - gap: 0.4rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); - transform: translate(-50%, -120%); - z-index: 10; - white-space: nowrap; - opacity: 0; - transition: opacity 0.15s ease; - } - - .tooltip.visible { - opacity: 1; - } - - .tooltip-dot { - width: 0.45rem; - height: 0.45rem; - border-radius: 50%; - flex-shrink: 0; - } - - .tooltip-label { - font-size: 0.75rem; - color: color-mix(in srgb, var(--color-text) 65%, transparent); - } - - .tooltip-value { - font-size: 0.75rem; - font-weight: 700; - color: var(--color-text); - } - - .legend { - display: flex; - flex-direction: column; - gap: 0.6rem; - min-width: 120px; - } - - .legend-item { - display: flex; - align-items: center; - gap: 0.55rem; - cursor: default; - } - - .legend-dot { - width: 0.55rem; - height: 0.55rem; - border-radius: 50%; - flex-shrink: 0; - } - - .legend-label { - font-size: 0.82rem; - color: color-mix(in srgb, var(--color-text) 65%, transparent); - flex: 1; - } - - .legend-value { - font-size: 0.82rem; - font-weight: 700; - color: var(--color-text); - font-variant-numeric: tabular-nums; - } - `; - - @property() heading = ''; - @property() centerText = 'Total'; - @property({ type: Array }) segments: CircleSegment[] = []; - - @state() private _hoveredIndex = -1; - @state() private _tooltipX = 0; - @state() private _tooltipY = 0; - - private _onArcEnter(e: MouseEvent, index: number) { - this._hoveredIndex = index; - this._updateTooltipPos(e); - } - - private _onArcMove(e: MouseEvent) { - if (this._hoveredIndex < 0) return; - this._updateTooltipPos(e); - } - - private _onArcLeave() { - this._hoveredIndex = -1; - } - - private _updateTooltipPos(e: MouseEvent) { - const container = this.shadowRoot!.querySelector( - '.svg-container' - ) as HTMLElement; - if (!container) return; - const rect = container.getBoundingClientRect(); - this._tooltipX = e.clientX - rect.left; - this._tooltipY = e.clientY - rect.top; - } - - render() { - const total = this.segments.reduce((s, d) => s + d.value, 0); - const radius = 62; - const circumference = 2 * Math.PI * radius; - const gapAngle = 0.02; - const totalGap = gapAngle * this.segments.length; - const usable = Math.max(0, circumference * (1 - totalGap)); - const gapSize = circumference * gapAngle; - let offset = 0; - - const arcs = this.segments.map((seg, i) => { - const pct = total > 0 ? seg.value / total : 0; - const dash = pct * usable; - const gap = circumference - dash; - const currentOffset = offset; - offset += dash + gapSize; - - return svg` - this._onArcEnter(e, i)} - @mousemove=${(e: MouseEvent) => this._onArcMove(e)} - @mouseleave=${() => this._onArcLeave()} - /> - `; - }); - - const hovered = - this._hoveredIndex >= 0 - ? this.segments[this._hoveredIndex] - : null; - - return html` -
- ${this.heading - ? html`

${this.heading}

` - : null} -
-
- ${arcs} -
- ${total} - ${this.centerText} -
-
- ${hovered - ? html` - - ${hovered.label} - ${hovered.value} - ` - : null} -
-
-
- ${this.segments.map( - (seg) => html` -
- - ${seg.label} - ${seg.value} -
- ` - )} -
-
-
- `; - } -} \ No newline at end of file diff --git a/src/components/footer-bar.ts b/src/components/footer-bar.ts deleted file mode 100644 index a0779c3..0000000 --- a/src/components/footer-bar.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement } from 'lit/decorators.js'; -import './ui-link'; - -@customElement('footer-bar') -export class FooterBar extends LitElement { - static styles = css` - footer { - background: var(--color-bg-nav); - border-top: 1px solid var(--color-border); - color: var(--color-text); - display: flex; - justify-content: space-around; - align-items: center; - padding: 0.8rem 0; - font-size: 0.9rem; - flex-wrap: wrap; - backdrop-filter: blur(10px); - position: relative; - width: 100%; -} - -a { - color: var(--color-text); - text-decoration: none; - margin-left: 1.2rem; - font-weight: 500; -} - -a:hover { - color: var(--color-accent); -} - -.center { - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: 1rem; -} - -@media (max-width: 600px) { - footer { - flex-direction: column; - gap: 0.6rem; - text-align: center; - } - - a { - margin: 0; - } -} -`; - - render() { - const year = new Date().getFullYear(); - - return html` - - `; - } -} \ No newline at end of file diff --git a/src/components/form-input.ts b/src/components/form-input.ts deleted file mode 100644 index bba9cf5..0000000 --- a/src/components/form-input.ts +++ /dev/null @@ -1,73 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('form-input') -export class FormInput extends LitElement { - static styles = css` - :host { - display: flex; - flex-direction: column; - gap: 0.4rem; - } - - label { - font-size: 0.8rem; - font-weight: 600; - letter-spacing: 0.02em; - color: color-mix(in srgb, var(--color-text) 70%, transparent); - text-transform: uppercase; - } - - input { - border: 1px solid var(--color-border); - border-radius: 0.5rem; - padding: 0.65rem 0.75rem; - font-size: 0.95rem; - background: var(--color-bg); - color: var(--color-text); - outline: none; - transition: - border-color 0.25s ease, - box-shadow 0.25s ease; - } - - input::placeholder { - color: color-mix(in srgb, var(--color-text) 35%, transparent); - } - - input:focus { - border-color: var(--color-accent); - box-shadow: 0 0 0 3px - color-mix(in srgb, var(--color-accent) 15%, transparent); - } - `; - - @property() label = ''; - @property() type = 'text'; - @property() placeholder = ''; - @property() value = ''; - - private handleInput(e: InputEvent) { - const target = e.target as HTMLInputElement; - this.value = target.value; - this.dispatchEvent( - new CustomEvent('value-changed', { - detail: { value: target.value }, - bubbles: true, - composed: true, - }) - ); - } - - render() { - return html` - - - `; - } -} \ No newline at end of file diff --git a/src/components/hero-icon.ts b/src/components/hero-icon.ts new file mode 100644 index 0000000..901e776 --- /dev/null +++ b/src/components/hero-icon.ts @@ -0,0 +1,50 @@ +import { LitElement, html, css, type SVGTemplateResult } from "lit"; + +import { customElement, property, state } from "lit/decorators.js"; + +@customElement("hero-icon") +export class HeroIcon extends LitElement { + @property({ type: String }) + name = ""; + + @state() + private iconTemplate: SVGTemplateResult | null = null; + + static styles = css` + :host { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + } + svg { + width: 100%; + height: 100%; + fill: var(--icon-fill, none); + stroke: var(--icon-stroke, currentColor); + stroke-width: var(--icon-stroke-width, 1.5); + } + `; + + updated(changedProperties: Map) { + if (changedProperties.has("name")) { + this.loadIcon(); + } + } + + async loadIcon() { + try { + const module = await import(`./icons/${this.name}.ts`); + this.iconTemplate = module.icon; + } catch (e) { + this.iconTemplate = null; + } + } + + render() { + return html` + ${this.iconTemplate} + `; + } +} \ No newline at end of file diff --git a/src/components/horizontal-divider.ts b/src/components/horizontal-divider.ts deleted file mode 100644 index c7a2405..0000000 --- a/src/components/horizontal-divider.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -@customElement('horizontal-divider') -export class FormDivider extends LitElement { - static styles = css` - :host { - display: block; - width: 100%; - height: 1px; - background: var(--color-border); - margin: 0.25rem 0; - } - `; - - render() { - return html``; - } -} \ No newline at end of file diff --git a/src/components/icon-card.ts b/src/components/icon-card.ts deleted file mode 100644 index 8574865..0000000 --- a/src/components/icon-card.ts +++ /dev/null @@ -1,79 +0,0 @@ -// components/icon-card.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('icon-card') -export class IconCard extends LitElement { - static styles = css` - :host { - display: block; - } - - .card { - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 0.75rem; - padding: 1.75rem; - display: flex; - flex-direction: column; - gap: 0.75rem; - transition: border-color 0.25s ease; - } - - .card:hover { - border-color: color-mix( - in srgb, - var(--color-accent) 40%, - transparent - ); - } - - .icon { - width: 2.25rem; - height: 2.25rem; - display: grid; - place-items: center; - border-radius: 0.5rem; - background: color-mix( - in srgb, - var(--color-accent) 10%, - transparent - ); - color: var(--color-accent); - } - - ::slotted(svg) { - width: 1.15rem; - height: 1.15rem; - } - - h3 { - margin: 0; - font-size: 1rem; - font-weight: 650; - color: var(--color-text); - } - - p { - margin: 0; - font-size: 0.875rem; - line-height: 1.6; - color: color-mix(in srgb, var(--color-text) 55%, transparent); - } - `; - - @property() heading = ''; - @property() description = ''; - - render() { - return html` -
-
- -
-

${this.heading}

-

${this.description}

-
- `; - } -} \ No newline at end of file diff --git a/src/components/icons.ts b/src/components/icons.ts deleted file mode 100644 index 34b9ee6..0000000 --- a/src/components/icons.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { html, type TemplateResult } from "lit"; - -export class Icons { - public static icons: Record = { - calendar: html``, - globe: html``, - trophy: html``, - target: html``, - clipboard: html``, - users: html``, - user: html``, - home: html``, - mail: html``, - desktop: html``, - tools: html``, - map: html``, - map_pin: html``, - clock: html``, - table: html``, - calculator: html``, - chart_pie: html``, - gear: html``, - document: html``, - document_text: html``, - bin: html``, - stack: html``, - visible: html``, - hide: html``, - folder: html``, - warning: html``, - info: html``, - error: html``, - success: html``, - }; -} diff --git a/src/components/icons/arrow-long-right.ts b/src/components/icons/arrow-long-right.ts new file mode 100644 index 0000000..ec4d127 --- /dev/null +++ b/src/components/icons/arrow-long-right.ts @@ -0,0 +1,6 @@ +import { svg } from "lit"; +export const icon = svg` + + + +`; diff --git a/src/components/icons/arrow-right.ts b/src/components/icons/arrow-right.ts new file mode 100644 index 0000000..74f4770 --- /dev/null +++ b/src/components/icons/arrow-right.ts @@ -0,0 +1,6 @@ +import { svg } from "lit"; +export const icon = svg` + + + +`; diff --git a/src/components/icons/download.ts b/src/components/icons/download.ts new file mode 100644 index 0000000..af98d5c --- /dev/null +++ b/src/components/icons/download.ts @@ -0,0 +1,6 @@ +import { svg } from "lit"; +export const icon = svg` + + + +`; diff --git a/src/components/input/text-input.ts b/src/components/input/text-input.ts new file mode 100644 index 0000000..c6f590a --- /dev/null +++ b/src/components/input/text-input.ts @@ -0,0 +1,70 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators.js"; +import { ifDefined } from "lit/directives/if-defined.js"; + +@customElement("text-input") +export class TextInput extends LitElement { + @property({ type: String }) value = ""; + @property({ type: String }) placeholder = ""; + @property({ type: String }) name = ""; + @property({ type: String }) autocomplete = ""; + @property({ type: Boolean }) password = false; + @property({ type: Boolean }) disabled = false; + @property({ attribute: false }) onValueChange?: (value: string) => void; + + static styles = css` + :host { + display: block; + } + + input { + display: block; + width: 100%; + box-sizing: border-box; + padding: 0.625rem 0.875rem; + background-color: var(--color-bg, #fff); + color: var(--color-text, #111); + border: 1.5px solid var(--color-border-strong, #aaa); + border-radius: 0.375rem; + font-family: "Inter", system-ui, sans-serif; + font-size: var(--font-size-base, 0.9375rem); + font-weight: var(--font-weight-normal, 400); + outline: none; + transition: border-color 0.2s ease, box-shadow 0.2s ease; + } + + input::placeholder { + color: var(--color-text-muted, #999); + } + + input:focus { + border-color: var(--color-accent, #2b6cb0); + box-shadow: 0 0 0 3px color-mix(in srgb, var(--color-accent, #2b6cb0) 20%, transparent); + } + + input:disabled { + opacity: 0.45; + cursor: not-allowed; + } + `; + + private _handleInput(e: Event) { + const input = e.target as HTMLInputElement; + this.value = input.value; + this.onValueChange?.(this.value); + } + + render() { + return html` + + `; + } +} diff --git a/src/components/line-chart.ts b/src/components/line-chart.ts deleted file mode 100644 index 980059b..0000000 --- a/src/components/line-chart.ts +++ /dev/null @@ -1,557 +0,0 @@ -import { LitElement, html, css, svg, nothing } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; - -export interface LineSeries { - label: string; - color?: string; - points: { x: number; y: number }[]; -} - -interface SnapPoint { - label: string; - color: string; - x: number; - y: number; - screenX: number; - screenY: number; -} - -interface HoverState { - snapX: number; - pctX: number; - pctY: number; - points: SnapPoint[]; -} - -@customElement('line-chart') -export class LineChart extends LitElement { - static styles = css` - :host { - display: block; - width: 100%; - } - - .chart-wrapper { - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 0.75rem; - padding: 1.75rem; - display: flex; - flex-direction: column; - gap: 1.25rem; - transition: border-color 0.25s ease; - height: 100%; - box-sizing: border-box; - } - - .chart-wrapper:hover { - border-color: color-mix(in srgb, var(--color-accent) 40%, transparent); - } - - .header { - display: flex; - justify-content: space-between; - align-items: baseline; - flex-shrink: 0; - } - - .title { - font-size: 1rem; - font-weight: 650; - color: var(--color-text); - margin: 0; - } - - .subtitle { - font-size: 0.78rem; - color: color-mix(in srgb, var(--color-text) 45%, transparent); - margin: 0; - } - - .svg-container { - width: 100%; - min-height: 0; - flex: 1 1 auto; - aspect-ratio: 500 / 300; - overflow: visible; - position: relative; - } - - :host([style*='height']) .svg-container { - aspect-ratio: unset; - } - - svg { - width: 100%; - height: 100%; - overflow: visible; - display: block; - } - - .grid-line { - stroke: var(--color-border); - stroke-width: 0.5; - } - - .axis-label { - font-size: 9px; - fill: color-mix(in srgb, var(--color-text) 40%, transparent); - font-weight: 500; - font-family: inherit; - } - - .line-path { - fill: none; - stroke-width: 2; - stroke-linecap: round; - stroke-linejoin: round; - transition: opacity 0.25s ease; - } - - .area-path { - opacity: 0.08; - transition: opacity 0.25s ease; - } - - .dot-indicator { - pointer-events: none; - } - - .crosshair { - stroke-dasharray: 3 3; - pointer-events: none; - } - - .overlay { - fill: transparent; - cursor: crosshair; - } - - .tooltip { - position: absolute; - pointer-events: auto; - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 0.5rem; - padding: 0.5rem 0.65rem; - box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); - display: flex; - flex-direction: column; - gap: 0.15rem; - min-width: 120px; - z-index: 10; - } - - .tooltip-header { - font-size: 0.7rem; - font-weight: 700; - color: var(--color-text); - padding-bottom: 0.25rem; - border-bottom: 1px solid var(--color-border); - margin-bottom: 0.15rem; - } - - .tooltip-scroll { - display: flex; - flex-direction: column; - gap: 0.2rem; - max-height: 130px; - overflow-y: auto; - scrollbar-width: thin; - scrollbar-color: color-mix(in srgb, var(--color-text) 20%, transparent) - transparent; - } - - .tooltip-row { - display: flex; - align-items: center; - gap: 0.4rem; - font-size: 0.72rem; - white-space: nowrap; - } - - .tooltip-dot { - width: 8px; - height: 8px; - border-radius: 50%; - flex-shrink: 0; - } - - .tooltip-label { - color: color-mix(in srgb, var(--color-text) 65%, transparent); - font-weight: 500; - } - - .tooltip-value { - margin-left: auto; - font-weight: 700; - color: var(--color-text); - } - - .legend { - display: flex; - gap: 1rem; - flex-wrap: wrap; - flex-shrink: 0; - } - - .legend-item { - display: flex; - align-items: center; - gap: 0.4rem; - } - - .legend-line { - width: 1rem; - height: 2px; - border-radius: 1px; - flex-shrink: 0; - } - - .legend-label { - font-size: 0.78rem; - color: color-mix(in srgb, var(--color-text) 60%, transparent); - font-weight: 500; - } - `; - - @property() heading = ''; - @property() subtitle = ''; - @property() xLabel = 'X'; - @property() yLabel = 'Y'; - @property({ type: Array }) series: LineSeries[] = []; - @property({ type: Boolean }) showArea = false; - - @state() private _hover: HoverState | null = null; - - private defaultColors = [ - 'var(--color-accent)', - '#30a46c', - '#e79d13', - '#e5484d', - '#6e56cf', - '#0091ff', - ]; - - private padding = { top: 20, right: 25, bottom: 40, left: 45 }; - private width = 500; - private height = 300; - - private niceScale(min: number, max: number, ticks: number) { - const range = max - min || 1; - const rough = range / ticks; - const mag = Math.pow(10, Math.floor(Math.log10(rough))); - const norm = rough / mag; - let step: number; - if (norm <= 1.5) step = 1 * mag; - else if (norm <= 3) step = 2 * mag; - else if (norm <= 7) step = 5 * mag; - else step = 10 * mag; - const nMin = Math.floor(min / step) * step; - const nMax = Math.ceil(max / step) * step; - return { min: nMin, max: nMax, step }; - } - - private _formatVal(v: number): string { - return Number.isInteger(v) ? v.toString() : v.toFixed(2); - } - - private _onMouseMove( - e: MouseEvent, - sx: (v: number) => number, - sy: (v: number) => number, - xScale: { min: number; max: number } - ) { - const svgEl = this.renderRoot.querySelector('svg'); - if (!svgEl) return; - - const rect = svgEl.getBoundingClientRect(); - const ratioX = this.width / rect.width; - const ratioY = this.height / rect.height; - const mouseVpX = (e.clientX - rect.left) * ratioX; - const mouseVpY = (e.clientY - rect.top) * ratioY; - - const p = this.padding; - const w = this.width - p.left - p.right; - const xRange = xScale.max - xScale.min || 1; - const dataX = xScale.min + ((mouseVpX - p.left) / w) * xRange; - - let closestX = Infinity; - let closestDist = Infinity; - - for (const s of this.series) { - for (const pt of s.points) { - const dist = Math.abs(pt.x - dataX); - if (dist < closestDist) { - closestDist = dist; - closestX = pt.x; - } - } - } - - if (!isFinite(closestX)) { - this._hover = null; - return; - } - - const points: SnapPoint[] = []; - - this.series.forEach((s, i) => { - const color = - s.color || this.defaultColors[i % this.defaultColors.length]; - const sorted = [...s.points].sort((a, b) => a.x - b.x); - - const exact = sorted.find((pt) => pt.x === closestX); - if (exact) { - points.push({ - label: s.label, - color, - x: exact.x, - y: exact.y, - screenX: sx(exact.x), - screenY: sy(exact.y), - }); - return; - } - - let left = null; - let right = null; - for (let j = 0; j < sorted.length; j++) { - if (sorted[j].x <= closestX) left = sorted[j]; - if (sorted[j].x >= closestX && !right) right = sorted[j]; - } - - if (left && right && left !== right) { - const t = (closestX - left.x) / (right.x - left.x); - const interpY = left.y + t * (right.y - left.y); - points.push({ - label: s.label, - color, - x: closestX, - y: interpY, - screenX: sx(closestX), - screenY: sy(interpY), - }); - } - }); - - if (!points.length) { - this._hover = null; - return; - } - - this._hover = { - snapX: sx(closestX), - pctX: (sx(closestX) / this.width) * 100, - pctY: (mouseVpY / this.height) * 100, - points, - }; - } - - private _onMouseLeave() { - this._hover = null; - } - - render() { - const p = this.padding; - const w = this.width - p.left - p.right; - const h = this.height - p.top - p.bottom; - - const allPts = this.series.flatMap((s) => s.points); - const xs = allPts.map((d) => d.x); - const ys = allPts.map((d) => d.y); - const rawXMin = Math.min(...xs, 0); - const rawXMax = Math.max(...xs, 1); - const rawYMin = Math.min(...ys, 0); - const rawYMax = Math.max(...ys, 1); - - const xScale = this.niceScale(rawXMin, rawXMax, 5); - const yScale = this.niceScale(rawYMin, rawYMax, 5); - - const sx = (v: number) => - p.left + ((v - xScale.min) / (xScale.max - xScale.min)) * w; - const sy = (v: number) => - p.top + h - ((v - yScale.min) / (yScale.max - yScale.min)) * h; - - const gridLines = []; - for ( - let val = yScale.min; - val <= yScale.max + yScale.step * 0.01; - val += yScale.step - ) { - const y = sy(val); - gridLines.push(svg` - - ${Math.round(val)} - `); - } - for ( - let val = xScale.min; - val <= xScale.max + xScale.step * 0.01; - val += xScale.step - ) { - const x = sx(val); - gridLines.push(svg` - ${Math.round(val)} - `); - } - - const baseLine = sy(yScale.min); - - const lines = this.series.map((s, i) => { - const color = - s.color || this.defaultColors[i % this.defaultColors.length]; - const sorted = [...s.points].sort((a, b) => a.x - b.x); - if (!sorted.length) return svg``; - - const d = sorted - .map( - (pt, j) => `${j === 0 ? 'M' : 'L'}${sx(pt.x)},${sy(pt.y)}` - ) - .join(' '); - - const areaD = - `M${sx(sorted[0].x)},${baseLine} ` + - sorted.map((pt) => `L${sx(pt.x)},${sy(pt.y)}`).join(' ') + - ` L${sx(sorted[sorted.length - 1].x)},${baseLine} Z`; - - return svg` - ${this.showArea ? svg`` : null} - - `; - }); - - const hoverOverlay = svg` - - this._onMouseMove(e, sx, sy, xScale)} - /> - `; - - const hoverMarkers = this._hover - ? svg` - - ${this._hover.points.map( - (pt) => svg` - - ` - )} - ` - : nothing; - - const tooltipHtml = this._hover - ? (() => { - const hv = this._hover; - const onRight = hv.pctX <= 60; - const tooltipY = Math.max(0, Math.min(hv.pctY - 10, 70)); - - const posStyle = onRight - ? `left: calc(${hv.pctX}% + 16px);` - : `right: calc(${100 - hv.pctX}% + 16px);`; - - return html` -
-
- ${this.xLabel}: ${this._formatVal(hv.points[0].x)} -
-
- ${hv.points.map( - (pt) => html` -
- - ${pt.label} - - ${this._formatVal(pt.y)} - -
- ` - )} -
-
- `; - })() - : nothing; - - return html` -
- ${this.heading || this.subtitle - ? html` -
- ${this.heading - ? html`

${this.heading}

` - : nothing} - ${this.subtitle - ? html`

${this.subtitle}

` - : nothing} -
- ` - : nothing} -
this._onMouseLeave()} - > - - ${gridLines} ${lines} ${hoverMarkers} ${hoverOverlay} - - ${this.xLabel} - - - ${this.yLabel} - - - ${tooltipHtml} -
- ${this.series.length > 1 - ? html` -
- ${this.series.map( - (s, i) => html` -
- - ${s.label} -
- ` - )} -
- ` - : nothing} -
- `; - } -} \ No newline at end of file diff --git a/src/components/loading-bar.ts b/src/components/loading-bar.ts deleted file mode 100644 index 8c3dfda..0000000 --- a/src/components/loading-bar.ts +++ /dev/null @@ -1,139 +0,0 @@ -// components/loading-bar.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('loading-bar') -export class LoadingBar extends LitElement { - static styles = css` - :host { - display: block; - width: 100%; - } - - .wrapper { - display: flex; - flex-direction: column; - gap: 0.5rem; - } - - .meta { - display: flex; - justify-content: space-between; - align-items: baseline; - } - - .label { - font-size: 0.8rem; - font-weight: 600; - letter-spacing: 0.02em; - color: color-mix(in srgb, var(--color-text) 70%, transparent); - text-transform: uppercase; - } - - .value { - font-size: 0.85rem; - font-weight: 700; - color: var(--color-text); - font-variant-numeric: tabular-nums; - } - - .track { - width: 100%; - height: 0.5rem; - background: color-mix(in srgb, var(--color-text) 8%, transparent); - border-radius: 1rem; - overflow: hidden; - position: relative; - } - - :host([size='sm']) .track { - height: 0.3rem; - } - - :host([size='lg']) .track { - height: 0.75rem; - } - - .fill { - height: 100%; - border-radius: 1rem; - background: var(--color-accent); - transition: width 0.6s cubic-bezier(0.22, 1, 0.36, 1); - position: relative; - } - - .fill.success { - background: #30a46c; - } - - .fill.warning { - background: #e79d13; - } - - .fill.error { - background: #e5484d; - } - - :host([indeterminate]) .fill { - width: 40% !important; - animation: indeterminate 1.4s ease-in-out infinite; - } - - @keyframes indeterminate { - 0% { - transform: translateX(-100%); - } - 100% { - transform: translateX(350%); - } - } - - .fill::after { - content: ''; - position: absolute; - inset: 0; - background: linear-gradient( - 90deg, - transparent 0%, - rgba(255, 255, 255, 0.15) 50%, - transparent 100% - ); - border-radius: inherit; - } - `; - - @property({ type: Number }) value = 0; - @property() label = ''; - @property() variant: 'accent' | 'success' | 'warning' | 'error' = - 'accent'; - @property({ type: Boolean, reflect: true }) indeterminate = false; - @property({ type: Boolean }) hideValue = false; - @property({ reflect: true }) size: 'sm' | 'md' | 'lg' = 'md'; - - render() { - const clamped = Math.max(0, Math.min(100, this.value)); - - return html` -
- ${this.label || !this.hideValue - ? html` -
- ${this.label - ? html`${this.label}` - : html``} - ${!this.hideValue && !this.indeterminate - ? html`${clamped}%` - : null} -
- ` - : null} -
-
-
-
- `; - } -} \ No newline at end of file diff --git a/src/components/nav-bar.ts b/src/components/nav-bar.ts deleted file mode 100644 index fc8cc34..0000000 --- a/src/components/nav-bar.ts +++ /dev/null @@ -1,175 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, state } from 'lit/decorators.js'; -import './ui-link'; - -@customElement('nav-bar') -export class NavBar extends LitElement { - static styles = css` - nav { - backdrop-filter: blur(18px) saturate(180%); - -webkit-backdrop-filter: blur(18px) saturate(180%); - background: var(--color-bg-nav); - border-bottom: 1px solid var(--color-border); - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 2rem; - height: 3.5rem; - position: sticky; - top: 0; - z-index: 100; - transition: background 0.3s ease; -} - -.brand { - display: flex; - align-items: center; - gap: 0.65rem; - user-select: none; - cursor: default; -} - -.brand img { - width: auto; - height: 1.6rem; - flex-shrink: 0; -} - -.brand span { - font-weight: 700; - font-size: 1.05rem; - letter-spacing: -0.02em; - background: linear-gradient( - 135deg, - var(--color-text) 0%, - var(--color-accent) 100% - ); - -webkit-background-clip: text; - -webkit-text-fill-color: transparent; - background-clip: text; -} - -.links { - display: flex; - gap: 1.75rem; - align-items: center; -} - -.theme-toggle { - position: relative; - display: grid; - place-items: center; - width: 2.25rem; - height: 2.25rem; - padding: 0; - margin-left: 0.5rem; - background: transparent; - border: 1px solid var(--color-border); - border-radius: 0.5rem; - color: var(--color-text); - cursor: pointer; - transition: - border-color 0.25s ease, - color 0.25s ease, - background 0.25s ease, - transform 0.2s ease; - overflow: hidden; -} - -.theme-toggle:hover { - border-color: var(--color-accent); - color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 8%, transparent); - transform: scale(1.05); -} - -.theme-toggle:active { - transform: scale(0.95); -} - -.icon { - position: absolute; - width: 1.1rem; - height: 1.1rem; - opacity: 0; - transform: rotate(-90deg) scale(0.6); - transition: - opacity 0.35s ease, - transform 0.35s ease; - pointer-events: none; -} - -.icon.visible { - opacity: 1; - transform: rotate(0deg) scale(1); -} -`; - - @state() theme: 'light' | 'dark' = - (localStorage.getItem('theme') as 'light' | 'dark') || - (window.matchMedia('(prefers-color-scheme: dark)').matches - ? 'dark' - : 'light'); - - firstUpdated() { - document.documentElement.setAttribute('data-theme', this.theme); - } - - toggleTheme() { - this.theme = this.theme === 'light' ? 'dark' : 'light'; - document.documentElement.setAttribute('data-theme', this.theme); - localStorage.setItem('theme', this.theme); - } - - render() { - return html` - - `; - } -} \ No newline at end of file diff --git a/src/components/notify-bar.ts b/src/components/notify-bar.ts deleted file mode 100644 index 3676ae8..0000000 --- a/src/components/notify-bar.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { LitElement, html, css } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { Icons } from "./icons"; - -export type NotificationType = "success" | "warning" | "error" | "info"; - -@customElement("notify-bar") -export class NotifyBar extends LitElement { - static styles = css` - :host { - display: block; - } - - :host([hidden]) { - display: none; - } - - .bar { - display: flex; - align-items: center; - gap: 0.6rem; - padding: 0.6rem 0.85rem; - font-size: 0.85rem; - font-weight: 500; - border-radius: 0.5rem; - border: 1px solid transparent; - animation: slideIn 0.25s ease; - } - - @keyframes slideIn { - from { - opacity: 0; - transform: translateY(-4px); - } - to { - opacity: 1; - transform: translateY(0); - } - } - - .icon { - flex-shrink: 0; - width: 1.15rem; - height: 1.15rem; - } - - .content { - flex: 1; - line-height: 1.4; - } - - .close { - flex-shrink: 0; - background: none; - border: none; - padding: 0.15rem; - cursor: pointer; - color: inherit; - opacity: 0.5; - transition: opacity 0.2s ease; - display: grid; - place-items: center; - } - - .close:hover { - opacity: 1; - } - - .close svg { - width: 0.9rem; - height: 0.9rem; - } - - .bar.error { - color: #e5484d; - background: color-mix(in srgb, #e5484d 8%, transparent); - border-color: color-mix(in srgb, #e5484d 25%, transparent); - } - - .bar.warning { - color: #e79d13; - background: color-mix(in srgb, #e79d13 8%, transparent); - border-color: color-mix(in srgb, #e79d13 25%, transparent); - } - - .bar.success { - color: #30a46c; - background: color-mix(in srgb, #30a46c 8%, transparent); - border-color: color-mix(in srgb, #30a46c 25%, transparent); - } - - .bar.info { - color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 8%, transparent); - border-color: color-mix(in srgb, var(--color-accent) 25%, transparent); - } - `; - - @property() type: NotificationType = "error"; - @property() message: string | null = null; - @property({ type: Boolean }) dismissible = true; - - private dismiss() { - this.message = null; - this.dispatchEvent( - new CustomEvent("dismissed", { bubbles: true, composed: true }), - ); - this.requestUpdate(); - } - - private renderIcon() { - if (this.type === "error") { - return Icons.icons.error; - } - if (this.type === "warning") { - return Icons.icons.warning; - } - if (this.type === "info") { - return Icons.icons.info; - } - return Icons.icons.success; - } - - render() { - if (!this.message) return null; - return html` -
- ${this.renderIcon()} - ${this.message} - ${this.dismissible - ? html` - - ` - : null} -
- `; - } -} diff --git a/src/components/stat-card.ts b/src/components/stat-card.ts deleted file mode 100644 index 84dbd7f..0000000 --- a/src/components/stat-card.ts +++ /dev/null @@ -1,56 +0,0 @@ -// components/stat-card.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('stat-card') -export class StatCard extends LitElement { - static styles = css` - :host { - display: block; - } - - .card { - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 0.75rem; - padding: 1.5rem; - display: flex; - flex-direction: column; - gap: 0.25rem; - transition: border-color 0.25s ease; - } - - .card:hover { - border-color: color-mix( - in srgb, - var(--color-accent) 40%, - transparent - ); - } - - .value { - font-size: 2rem; - font-weight: 800; - letter-spacing: -0.03em; - color: var(--color-accent); - } - - .label { - font-size: 0.85rem; - color: color-mix(in srgb, var(--color-text) 55%, transparent); - font-weight: 500; - } - `; - - @property() value = ''; - @property() label = ''; - - render() { - return html` -
- ${this.value} - ${this.label} -
- `; - } -} \ No newline at end of file diff --git a/src/components/ui-badge.ts b/src/components/ui-badge.ts deleted file mode 100644 index 5b15ece..0000000 --- a/src/components/ui-badge.ts +++ /dev/null @@ -1,116 +0,0 @@ -// components/ui-badge.ts -import { LitElement, html, css } from "lit"; -import { customElement, property, state } from "lit/decorators.js"; - -export type BadgeVariant = "accent" | "success" | "warning" | "error" | "muted"; - -@customElement("ui-badge") -export class UiBadge extends LitElement { - static styles = css` - :host { - display: inline-flex; - } - - .badge { - display: inline-flex; - align-items: center; - gap: 0.4rem; - padding: 0.35rem 0.75rem; - font-size: 0.75rem; - font-weight: 600; - letter-spacing: 0.03em; - text-transform: uppercase; - border-radius: 2rem; - border: 1px solid transparent; - white-space: nowrap; - line-height: 1; - } - - .icon { - display: inline-flex; - align-items: center; - flex-shrink: 0; - } - - .icon ::slotted(*) { - display: inline-flex; - width: 0.85rem; - height: 0.85rem; - } - - .icon ::slotted(svg), - .icon ::slotted(* > svg) { - width: 0.85rem; - height: 0.85rem; - } - - .label { - display: inline-flex; - align-items: center; - padding-top: 0.05em; - } - - .badge.white { - color: #ffffff; - background: color-mix(in srgb, #ffffff 10%, transparent); - border-color: color-mix(in srgb, #ffffff 25%, transparent); - } - - .badge.accent { - color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 10%, transparent); - border-color: color-mix(in srgb, var(--color-accent) 25%, transparent); - } - - .badge.success { - color: #30a46c; - background: color-mix(in srgb, #30a46c 10%, transparent); - border-color: color-mix(in srgb, #30a46c 25%, transparent); - } - - .badge.warning { - color: #e79d13; - background: color-mix(in srgb, #e79d13 10%, transparent); - border-color: color-mix(in srgb, #e79d13 25%, transparent); - } - - .badge.error { - color: #e5484d; - background: color-mix(in srgb, #e5484d 10%, transparent); - border-color: color-mix(in srgb, #e5484d 25%, transparent); - } - - .badge.muted { - color: color-mix(in srgb, var(--color-text) 45%, transparent); - background: color-mix(in srgb, var(--color-text) 6%, transparent); - border-color: var(--color-border); - } - `; - - @property() variant: BadgeVariant = "accent"; - @state() private hasIcon = false; - - private handleSlotChange(e: Event) { - const slot = e.target as HTMLSlotElement; - this.hasIcon = slot.assignedNodes().length > 0; - } - - render() { - return html` - - ${this.hasIcon - ? html` - - ` - : html``} - - - - - `; - } -} diff --git a/src/components/ui-button-secondary.ts b/src/components/ui-button-secondary.ts deleted file mode 100644 index c543156..0000000 --- a/src/components/ui-button-secondary.ts +++ /dev/null @@ -1,91 +0,0 @@ -// components/ui-button-secondary.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; - -@customElement('ui-button-secondary') -export class UiButtonSecondary extends LitElement { - static styles = css` - :host { - display: inline-flex; - } - - :host([full]) { - display: flex; - } - - :host([full]) button { - width: 100%; - } - - button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.45rem; - padding: 0.65rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - color: var(--color-text); - background: transparent; - border: 1px solid var(--color-border); - border-radius: 0.5rem; - cursor: pointer; - transition: - border-color 0.25s ease, - color 0.25s ease, - background 0.25s ease, - transform 0.15s ease; - } - - button:hover:not(:disabled) { - border-color: var(--color-accent); - color: var(--color-accent); - background: color-mix(in srgb, var(--color-accent) 6%, transparent); - } - - button:active:not(:disabled) { - transform: scale(0.98); - } - - button:disabled { - opacity: 0.55; - cursor: not-allowed; - } - -.icon { - display: inline-flex; - align-items: center; -} - -.icon:not(:has(*)) { - display: none; -} - - .icon ::slotted(svg) { - width: 1rem; - height: 1rem; - flex-shrink: 0; - } - `; - - @property({ type: Boolean }) disabled = false; - @property({ type: Boolean, reflect: true }) full = false; - - @state() private hasIcon = false; - - private handleSlotChange(e: Event) { - const slot = e.target as HTMLSlotElement; - this.hasIcon = slot.assignedNodes().length > 0; - } - - render() { - return html` - - `; - } -} \ No newline at end of file diff --git a/src/components/ui-button.ts b/src/components/ui-button.ts deleted file mode 100644 index 919b08f..0000000 --- a/src/components/ui-button.ts +++ /dev/null @@ -1,90 +0,0 @@ -// components/ui-button.ts -import { LitElement, html, css } from 'lit'; -import { customElement, property, state } from 'lit/decorators.js'; - -@customElement('ui-button') -export class UiButton extends LitElement { - static styles = css` - :host { - display: inline-flex; - } - - :host([full]) { - display: flex; - } - - :host([full]) button { - width: 100%; - } - - button { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 0.45rem; - padding: 0.65rem 1.25rem; - font-size: 0.95rem; - font-weight: 600; - letter-spacing: 0.01em; - 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, - opacity 0.25s ease; - } - - button:hover:not(:disabled) { - background: color-mix(in srgb, var(--color-accent) 85%, black); - box-shadow: 0 4px 14px - color-mix(in srgb, var(--color-accent) 35%, transparent); - } - - button:active:not(:disabled) { - transform: scale(0.98); - } - - button:disabled { - opacity: 0.55; - cursor: not-allowed; - } -.icon { - display: inline-flex; - align-items: center; -} - -.icon:not(:has(*)) { - display: none; -} - - .icon ::slotted(svg) { - width: 1rem; - height: 1rem; - flex-shrink: 0; - } - `; - - @property({ type: Boolean }) disabled = false; - @property({ type: Boolean, reflect: true }) full = false; - - @state() private hasIcon = false; - - private handleSlotChange(e: Event) { - const slot = e.target as HTMLSlotElement; - this.hasIcon = slot.assignedNodes().length > 0; - } - render() { - return html` - - `; - } -} \ No newline at end of file diff --git a/src/components/ui-card.ts b/src/components/ui-card.ts deleted file mode 100644 index aaa1b95..0000000 --- a/src/components/ui-card.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('ui-card') -export class UiCard extends LitElement { - static styles = css` - :host { - display: block; - width: 100%; - max-width: 420px; - } - - .card { - width: 100%; - background: var(--color-bg-nav); - border: 1px solid var(--color-border); - border-radius: 1rem; - padding: 2.5rem 2rem 2rem; - display: flex; - flex-direction: column; - gap: 1.25rem; - box-shadow: - 0 1px 3px rgba(0, 0, 0, 0.08), - 0 12px 40px rgba(0, 0, 0, 0.12); - backdrop-filter: blur(12px); - } - - :host([centered]) .card { - align-items: center; - text-align: center; - } - `; - - @property({ type: Boolean, reflect: true }) centered = false; - - render() { - return html` -
- -
- `; - } -} \ No newline at end of file diff --git a/src/components/ui-link.ts b/src/components/ui-link.ts deleted file mode 100644 index b9d14a2..0000000 --- a/src/components/ui-link.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { LitElement, html, css } from 'lit'; -import { customElement, property } from 'lit/decorators.js'; - -@customElement('ui-link') -export class UiLink extends LitElement { - static styles = css` - a { - position: relative; - color: var(--color-text); - font-weight: 500; - font-size: 0.9rem; - letter-spacing: 0.01em; - text-decoration: none; - display: inline-block; - padding: 0.25rem 0; - transition: color 0.25s ease; -} - -a::after { - content: ""; - position: absolute; - left: 50%; - bottom: -1px; - width: 0; - height: 2px; - background: var(--color-accent); - border-radius: 1px; - transform: translateX(-50%); - transition: width 0.3s cubic-bezier(0.22, 1, 0.36, 1); -} - -a:hover { - color: var(--color-accent); -} - -a:hover::after, -a.active::after { - width: 100%; -} - -a.active { - color: var(--color-accent); -} -`; - - @property() href = '/'; - @property({ type: Boolean }) active = false; - - navigate(e: Event) { - e.preventDefault(); - this.dispatchEvent( - new CustomEvent('nav', { - detail: { path: this.href }, - bubbles: true, - composed: true, - }) - ); - } - - render() { - return html` - - - - `; - } -} \ No newline at end of file diff --git a/src/components/ui-tab-bar.ts b/src/components/ui-tab-bar.ts deleted file mode 100644 index e96fa17..0000000 --- a/src/components/ui-tab-bar.ts +++ /dev/null @@ -1,106 +0,0 @@ -// components/ui-tab-bar.ts -import { LitElement, html, css } from "lit"; -import { customElement, property } from "lit/decorators.js"; -import { Icons } from "./icons"; - -export interface Tab { - id: string; - label: string; - icon?: string; -} - -@customElement("ui-tab-bar") -export class UiTabBar extends LitElement { - static styles = css` - :host { - display: block; - background: var(--color-bg-nav); - border-bottom: 1px solid var(--color-border); - position: sticky; - top: 0; - z-index: 10; - } - - .tabs { - max-width: 1200px; - margin: 0 auto; - display: flex; - gap: 0; - overflow-x: auto; - scrollbar-width: none; - } - - .tabs::-webkit-scrollbar { - display: none; - } - - .tab { - display: flex; - align-items: center; - gap: 0.5rem; - padding: 1rem 1.25rem; - font-size: 0.875rem; - font-weight: 500; - color: color-mix(in srgb, var(--color-text) 55%, transparent); - border: none; - background: none; - cursor: pointer; - white-space: nowrap; - border-bottom: 2px solid transparent; - transition: all 0.15s ease; - font-family: inherit; - } - - .tab:hover { - color: var(--color-accent); - background: color-mix( - in srgb, - var(--color-accent) 8%, - transparent - ); - } - - .tab.active { - color: var(--color-accent); - border-bottom-color: var(--color-accent); - } - - .tab svg { - width: 16px; - height: 16px; - } - `; - - @property({ type: Array }) tabs: Tab[] = []; - @property() active = ""; - - - - private handleClick(id: string) { - this.dispatchEvent( - new CustomEvent("tab-change", { - detail: { tab: id }, - bubbles: true, - composed: true, - }) - ); - } - - render() { - return html` -
- ${this.tabs.map( - (t) => html` - - ` - )} -
- `; - } -} \ No newline at end of file diff --git a/src/i18n/keys.ts b/src/i18n/keys.ts new file mode 100644 index 0000000..4989437 --- /dev/null +++ b/src/i18n/keys.ts @@ -0,0 +1,13 @@ +export type TranslationKey = + | "login.eyebrow" + | "login.title" + | "login.subtitle" + | "login.email" + | "login.password" + | "login.remember" + | "login.forgot" + | "login.submit" + | "login.loading" + | "login.error.credentials" + | "login.error.failed" + | "login.error.network"; diff --git a/src/i18n/locale-controller.ts b/src/i18n/locale-controller.ts new file mode 100644 index 0000000..a120e2f --- /dev/null +++ b/src/i18n/locale-controller.ts @@ -0,0 +1,29 @@ +import type { ReactiveController, ReactiveControllerHost } from "lit"; +import { locale, type TranslationKey } from "./locale.js"; + +export class LocaleController implements ReactiveController { + private _host: ReactiveControllerHost; + private _unsub?: () => void; + + constructor(host: ReactiveControllerHost) { + this._host = host; + host.addController(this); + } + + hostConnected() { + this._unsub = locale.subscribe(() => this._host.requestUpdate()); + locale.init().then(() => this._host.requestUpdate()); + } + + hostDisconnected() { + this._unsub?.(); + } + + t(key: TranslationKey): string { + return locale.t(key); + } + + get current() { + return locale.current; + } +} diff --git a/src/i18n/locale.ts b/src/i18n/locale.ts new file mode 100644 index 0000000..772c395 --- /dev/null +++ b/src/i18n/locale.ts @@ -0,0 +1,44 @@ +import type { TranslationKey } from "./keys.js"; + +export type { TranslationKey }; +export type Locale = "de" | "en"; + +type TranslationMap = Record; + +let _current: Locale = (localStorage.getItem("locale") as Locale) ?? "de"; +const _cache = new Map(); +const _listeners = new Set<() => void>(); + +async function _load(l: Locale): Promise { + if (_cache.has(l)) return; + const res = await fetch(`/locales/${l}.json`); + if (!res.ok) throw new Error(`Failed to load locale "${l}"`); + _cache.set(l, await res.json()); +} + +export const locale = { + get current(): Locale { + return _current; + }, + + async init(): Promise { + await _load(_current); + _listeners.forEach((fn) => fn()); + }, + + async set(l: Locale): Promise { + await _load(l); + _current = l; + localStorage.setItem("locale", l); + _listeners.forEach((fn) => fn()); + }, + + t(key: TranslationKey): string { + return _cache.get(_current)?.[key] ?? key; + }, + + subscribe(fn: () => void): () => void { + _listeners.add(fn); + return () => _listeners.delete(fn); + }, +}; diff --git a/src/main.ts b/src/main.ts index 8728dc3..f5af323 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,2 +1,4 @@ -import './styles/theme.css'; -import './app-root'; \ No newline at end of file +import "./styles/theme.css"; +import { locale } from "./i18n/locale.js"; + +locale.init().then(() => import("./app-root.js")); diff --git a/src/pages/admin/admin-home-page.ts b/src/pages/admin/admin-home-page.ts deleted file mode 100644 index e4322ad..0000000 --- a/src/pages/admin/admin-home-page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { LitElement, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -@customElement('cc-home-page') -export class CCHomePage extends LitElement { - render() { - return html` -

Welcome to the Competition Center Homepage

-

Analyze your tracks visually.

- `; - } -} \ No newline at end of file diff --git a/src/pages/auth/login-page.ts b/src/pages/auth/login-page.ts index d376939..fab16f0 100644 --- a/src/pages/auth/login-page.ts +++ b/src/pages/auth/login-page.ts @@ -1,39 +1,192 @@ 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"; +import { authService } from "../../services/auth.service.js"; +import { LocaleController } from "../../i18n/locale-controller.js"; +import "../../components/button/primary-button.js"; +import "../../components/input/text-input.js"; @customElement("login-page") export class LoginPage extends LitElement { + private _t = new LocaleController(this); + + @state() private email = ""; + @state() private password = ""; + @state() private remember = false; + @state() private error: string | null = null; + @state() private loading = false; + static styles = css` :host { flex: 1; display: flex; } - .footer { - text-align: center; - font-size: 0.875rem; + .layout { + display: grid; + grid-template-columns: minmax(320px, 34rem) 1fr; + width: 100%; + min-height: 100%; + } + + /* ── Left panel ──────────────────────────── */ + .form-panel { + display: flex; + flex-direction: column; + justify-content: space-between; + padding: 3.5rem 4rem; + background: var(--color-bg, #f5f5f5); + } + + .form-content { + max-width: 26rem; + } + + .eyebrow { + margin: 0 0 1rem; + font-size: var(--font-size-xs, 0.75rem); + font-weight: var(--font-weight-medium, 500); + letter-spacing: 0.12em; + color: color-mix(in srgb, var(--color-text) 50%, transparent); + } + + h1 { + margin: 0 0 0.75rem; + font-size: 1.75rem; + font-weight: var(--font-weight-bold, 700); + color: var(--color-text, #111); + line-height: 1.2; + } + + .subtitle { + margin: 0 0 2.5rem; + font-size: var(--font-size-sm, 0.875rem); color: color-mix(in srgb, var(--color-text) 60%, transparent); + line-height: 1.55; + } + + form { + display: flex; + flex-direction: column; + gap: 1.25rem; + } + + .field { + display: flex; + flex-direction: column; + gap: 0.4rem; + } + + .field-label { + font-size: var(--font-size-xs, 0.75rem); + font-weight: var(--font-weight-medium, 500); + letter-spacing: 0.08em; + color: color-mix(in srgb, var(--color-text) 65%, transparent); + } + + .row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + } + + .remember { + display: flex; + align-items: center; + gap: 0.5rem; + font-size: var(--font-size-sm, 0.875rem); + color: color-mix(in srgb, var(--color-text) 70%, transparent); + cursor: pointer; + user-select: none; + } + + .remember input[type="checkbox"] { + width: 1rem; + height: 1rem; + cursor: pointer; + accent-color: var(--color-accent, #2b6cb0); + flex-shrink: 0; + } + + .forgot { + font-size: var(--font-size-sm, 0.875rem); + color: var(--color-accent, #2b6cb0); + text-decoration: none; + white-space: nowrap; + flex-shrink: 0; + } + + .forgot:hover { + text-decoration: underline; + } + + .error { + font-size: var(--font-size-sm, 0.875rem); + color: #c53030; margin: 0; } + + .footer { + margin-top: 2rem; + font-size: var(--font-size-xs, 0.75rem); + color: color-mix(in srgb, var(--color-text) 40%, transparent); + } + + /* ── Right panel ─────────────────────────── */ + .hero-panel { + position: relative; + background: var(--color-border, #ddd); + overflow: hidden; + } + + .hero-panel img { + position: absolute; + inset: 0; + width: 100%; + height: 100%; + object-fit: cover; + pointer-events: none; + user-select: none; + -webkit-user-drag: none; + } + + /* ── Responsive ──────────────────────────── */ + @media (max-width: 34rem) { + .layout { + grid-template-columns: 1fr; + } + + .hero-panel { + display: none; + } + + .form-panel { + max-width: 34rem; + margin: 0 auto; + padding: 2.5rem 1.5rem; + } + + .form-content { + width: 100%; + margin: 0 auto; + } + } `; - @state() email = ""; - @state() password = ""; - @state() error: string | null = null; - @state() loading = false; + private _navigate(path: string) { + window.dispatchEvent( + new CustomEvent("nav", { detail: { path }, bubbles: true, composed: true }), + ); + } + + private async _handleSubmit(e: Event) { + e.preventDefault(); + if (this.loading) return; - 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", @@ -44,8 +197,8 @@ export class LoginPage extends LitElement { if (!response.ok) { this.error = response.status === 401 - ? "E-Mail oder Passwort falsch." - : "Login fehlgeschlagen."; + ? this._t.t("login.error.credentials") + : this._t.t("login.error.failed"); return; } @@ -65,58 +218,84 @@ export class LoginPage extends LitElement { ); 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."; + this._navigate(authService.isAdmin ? "/admin" : "/cc"); + } catch { + this.error = this._t.t("login.error.network"); } finally { this.loading = false; } } render() { + const t = (k: Parameters[0]) => this._t.t(k); + return html` - - (this.email = e.detail.value)} - > +
+
+
+

${t("login.eyebrow")}

+

${t("login.title")}

+

${t("login.subtitle")}

- (this.password = e.detail.value)} - > +
+
+ ${t("login.email")} + { this.email = v; }} + > +
- +
+ ${t("login.password")} + { this.password = v; }} + > +
- - ${this.loading ? "Signing in..." : "Sign in"} - +
+ + { + e.preventDefault(); + this._navigate("/forgot-password"); + }}> + ${t("login.forgot")} + +
- + ${this.error ? html`

${this.error}

` : null} - - + {}} + icon="arrow-right" + > +
+
+ + +
+ +
+ +
+
`; } } diff --git a/src/pages/auth/register-page.ts b/src/pages/auth/register-page.ts index 0b6313b..31067ce 100644 --- a/src/pages/auth/register-page.ts +++ b/src/pages/auth/register-page.ts @@ -1,11 +1,5 @@ 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"; const COUNTRIES = [ { code: "AD", label: "Andorra" }, @@ -457,145 +451,10 @@ export class RegisterPage extends LitElement { } } - private renderDropdown() { - const selected = COUNTRIES.find((c) => c.code === this.country); - const filtered = COUNTRIES.filter((c) => - c.label.toLowerCase().includes(this.dropdownSearch.toLowerCase()), - ); - return html` - - `; - } render() { return html` - -
- - (this.firstname = e.detail.value)} - style="flex: 1; min-width: 0;" - > - - - (this.lastname = e.detail.value)} - style="flex: 1; min-width: 0;" - > -
- - (this.email = e.detail.value)} - > - - - (this.phonenumber = e.detail.value)} - > - - ${this.renderDropdown()} - - (this.password = e.detail.value)} - > - - - - - ${this.loading ? "Creating account..." : "Create account"} - - - - - -
`; } } diff --git a/src/pages/competition/competition-page.ts b/src/pages/competition/competition-page.ts deleted file mode 100644 index aa42352..0000000 --- a/src/pages/competition/competition-page.ts +++ /dev/null @@ -1,229 +0,0 @@ -// 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 Promise> = { - 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(); - @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` -
-
- Loading… -
- `; - } - - switch (this.activeTab) { - case "details": - return html``; - case "results": - return html``; - case "tasks": - return html``; - case "noticeboard": - return html``; - case "pilots": - return html``; - case "officials": - return html``; - default: - return nothing; - } - } - - render() { - return html` -
-
- - -

EVENT_NAME

- -
-
- ${Icons.icons.map_pin} COMPETITION_LOCATION -
-
- ${Icons.icons.calendar} DATE_FROM_UNTIL_SHORT -
- EVENT_TYPE - EVENT_STATUS -
-
-
- - - -
${this.renderTabContent()}
- `; - } -} diff --git a/src/pages/competition/tabs/competition-details.ts b/src/pages/competition/tabs/competition-details.ts deleted file mode 100644 index 55515ef..0000000 --- a/src/pages/competition/tabs/competition-details.ts +++ /dev/null @@ -1,190 +0,0 @@ -// 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` -
-
-
-
DATE_FROM – DATE_UNTIL
-
- COMPETITION_DETAILS -
-
-
- - -
- `; - } -} - -export default CompetitionDetails; \ No newline at end of file diff --git a/src/pages/competition/tabs/competition-noticeboard.ts b/src/pages/competition/tabs/competition-noticeboard.ts deleted file mode 100644 index 2c8fbd0..0000000 --- a/src/pages/competition/tabs/competition-noticeboard.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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` -
-

Noticeboard

-

Content will appear here once the competition is underway.

-
- `; - } -} - -export default CompetitionNoticeboard; \ No newline at end of file diff --git a/src/pages/competition/tabs/competition-officials.ts b/src/pages/competition/tabs/competition-officials.ts deleted file mode 100644 index f436b9a..0000000 --- a/src/pages/competition/tabs/competition-officials.ts +++ /dev/null @@ -1,41 +0,0 @@ -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` -
-

Officials

-

Content will appear here once the competition is underway.

-
- `; - } -} - -export default CompetitionOfficials; \ No newline at end of file diff --git a/src/pages/competition/tabs/competition-pilots.ts b/src/pages/competition/tabs/competition-pilots.ts deleted file mode 100644 index b87028d..0000000 --- a/src/pages/competition/tabs/competition-pilots.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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` -
-

Pilots

-

Content will appear here once the competition is underway.

-
- `; - } -} - -export default CompetitionPilots; \ No newline at end of file diff --git a/src/pages/competition/tabs/competition-results.ts b/src/pages/competition/tabs/competition-results.ts deleted file mode 100644 index e453ff1..0000000 --- a/src/pages/competition/tabs/competition-results.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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` -
-

Results

-

Content will appear here once the competition is underway.

-
- `; - } -} - -export default CompetitionResults; \ No newline at end of file diff --git a/src/pages/competition/tabs/competition-tasks.ts b/src/pages/competition/tabs/competition-tasks.ts deleted file mode 100644 index 1e65cc7..0000000 --- a/src/pages/competition/tabs/competition-tasks.ts +++ /dev/null @@ -1,42 +0,0 @@ -// 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` -
-

Task Data

-

Content will appear here once the competition is underway.

-
- `; - } -} - -export default CompetitionTasks; \ No newline at end of file diff --git a/src/pages/dev-page.ts b/src/pages/dev-page.ts deleted file mode 100644 index 857fde3..0000000 --- a/src/pages/dev-page.ts +++ /dev/null @@ -1,630 +0,0 @@ -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` -
- - -
-
-

Buttons

-

Primary and secondary action buttons

-
-
-
Primary
-
- Default - - - - - - With Icon - - Disabled -
-
Primary Full Width
-
- Full Width Button -
-
Secondary
-
- Default - - - - - - With Icon - - Disabled -
-
Secondary Full Width
-
- - Full Width Secondary - -
-
-
- -
-
-

Badges

-

Status and category indicators

-
-
-
Variants
-
- Accent - Success - Warning - Error - Muted -
-
With Icon
-
- - ${Icons.icons.stack} - Platform - - - ${Icons.icons.success} - Live - - - ${Icons.icons.error} - Offline - -
-
-
- -
-
-

Notification Bars

-

Inline feedback messages

-
-
-
Types
-
- - - - -
-
Non-Dismissible
-
- -
-
-
- -
-
-

Links

-

Navigation links with underline animation

-
-
-
States
-
- Default Link -
- Active Link -
-
-
- -
-
-

Form Inputs

-

Text input fields with labels

-
-
-
Types
-
-
- - (this.inputValue = e.detail.value)} - > - - -
-
-
Live Value
-
- - Current value: "${this.inputValue}" - -
-
-
- -
-
-

Stat Cards

-

Metric display cards

-
-
-
Grid
-
- - - - -
-
-
- -
-
-

Icon Cards

-

Feature highlight cards

-
-
-
Grid
-
- - ${Icons.icons.clock} - - - ${Icons.icons.map_pin} - -
-
-
- -
-
-

Cards

-

Container cards and layout primitives

-
-
-
ui-card (default)
-
- - -

- This is the default card layout with a header component - inside. -

- - Action -
-
-
ui-card (centered)
-
- -

- 404 -

- - - Action -
-
-
-
- -
-
-

Divider

-

Visual separator for content sections

-
-
-
horizontal-divider
-
-

- Content above -

- -

- Content below -

-
-
-
- -
-
-

Composed: Auth Card

-

Pre-composed card for authentication flows

-
-
-
Login Example
-
- - - - - - Sign in - -

- No account? - Create one -

-
-
-
-
- -
-
-

Loading Bars

-

Progress indicators with variants and states

-
-
-
Variants
-
- - - - -
-
Sizes
-
- - - -
-
Indeterminate
-
- -
-
-
- -
-
-

Line Chart

-

Trend lines with optional area fill

-
-
-
Multi-Series
-
- -
-
-
- -
-
-

Circle Chart

-

Donut chart for proportional data

-
-
-
Example
-
- -
-
-
-
- `; - } -} diff --git a/src/pages/home-page.ts b/src/pages/home-page.ts index c5395ba..f8be8c3 100644 --- a/src/pages/home-page.ts +++ b/src/pages/home-page.ts @@ -1,13 +1,10 @@ // 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"; +import "../components/hero-icon.ts"; +import "../components/button/primary-button.ts"; +import "../components/button/secondary-button.ts"; +import "../components/input/text-input.ts"; @customElement("home-page") export class HomePage extends LitElement { @@ -17,213 +14,6 @@ export class HomePage extends LitElement { 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) { @@ -238,162 +28,27 @@ export class HomePage extends LitElement { render() { return html` -
- - ${Icons.icons.stack} - Live Scoring Platform - -

Transparent scoring for balloon competitions

-

- Track tasks, manage participants, and deliver real-time results for - hot air balloon competitions worldwide. -

-
- this.navigate("/competitions")}> - Browse competitions - - this.navigate("/register")}> - Create account - -
-
+ + + + + + + + console.log(v)} + > + -
- -

Platform in numbers

-

- FlightScore powers balloon competitions across the globe. -

-
- - - - -
-
- -
- -

Built for balloon events

-

- Everything organizers, officials, juries and pilots need in one place. -

-
- - ${Icons.icons.clock} - - - ${Icons.icons.document_text} - - - ${Icons.icons.users} - - - ${Icons.icons.desktop} - - - ${Icons.icons.map_pin} - - - ${Icons.icons.table} - -
-
- -
- -

Upcoming and recent events

-

- Discover balloon competitions scored with FlightScore. -

-
-
this.navigate("/competitions/1")} - > -
- 18 - Mar -
-
- European Balloon Challenge 2026 - - ${Icons.icons.map_pin} Salzburg, Austria - -
- Upcoming -
- -
this.navigate("/competitions/2")} - > -
- 14 - Feb -
-
- Cappadocia Open 2026 - - ${Icons.icons.map_pin} Goreme, Turkey - -
- Live -
- -
this.navigate("/competitions/3")} - > -
- 02 - Feb -
-
- Albuquerque Winter Fiesta - - ${Icons.icons.map_pin} New Mexico, USA - -
- Ended -
- -
this.navigate("/competitions/4")} - > -
- 11 - Jan -
-
- Swiss Alpine Balloon Trophy - - ${Icons.icons.map_pin} Chateau-d'Oex, Switzerland - -
- Canceled -
-
-
`; } } diff --git a/src/pages/not-found-page.ts b/src/pages/not-found-page.ts deleted file mode 100644 index e5aaa89..0000000 --- a/src/pages/not-found-page.ts +++ /dev/null @@ -1,123 +0,0 @@ -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` - - -

404

-

Page not found

-

- The page you are looking for does not exist or has been - moved. -

- - - - - this.navigate('/')}> - - - - - Back to home - -
-
- `; - } -} \ No newline at end of file diff --git a/src/pages/tt/tt-home-page.ts b/src/pages/tt/tt-home-page.ts deleted file mode 100644 index 6505157..0000000 --- a/src/pages/tt/tt-home-page.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { LitElement, html } from 'lit'; -import { customElement } from 'lit/decorators.js'; - -@customElement('cc-home-page') -export class CCHomePage extends LitElement { - render() { - return html` -

Welcome to the Target Team Homepage

-

Analyze your tracks visually.

- `; - } -} \ No newline at end of file diff --git a/src/styles/theme.css b/src/styles/theme.css index 2ff70b8..8a11555 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -2,20 +2,44 @@ --color-bg: #f5f5f5; --color-bg-nav: #ffffffcc; --color-text: #111; + --color-text-primary: #2b6cb0; --color-accent: #2b6cb0; + --color-on-accent: #ffffff; --color-border: #ddd; + --color-border-strong: #aaa; + --color-text-muted: #999; + + --font-size-xs: 0.75rem; + --font-size-sm: 0.875rem; + --font-size-base: 0.9375rem; + --font-size-lg: 1.125rem; + --font-size-xl: 1.25rem; + + --font-weight-normal: 400; + --font-weight-medium: 500; + --font-weight-bold: 700; } -:root[data-theme='dark'] { +:root[data-theme="dark"] { --color-bg: #1a1a1a; --color-bg-nav: #222; --color-text: #f5f5f5; --color-accent: #63b3ed; + --color-on-accent: #1a1a1a; --color-border: #333; + --color-border-strong: #666; + --color-text-muted: #777; +} + +.text-primary { + color: var(--color-text-primary); } * { - transition: background-color 0.25s ease, color 0.25s ease, border-color 0.25s ease; + transition: + background-color 0.25s ease, + color 0.25s ease, + border-color 0.25s ease; } html, @@ -24,7 +48,5 @@ body { padding: 0; background: var(--color-bg); color: var(--color-text); - font-family: 'Inter', system-ui, sans-serif; + font-family: "Inter", system-ui, sans-serif; } - -