Created some base components

This commit is contained in:
CodingPhoenixx
2026-02-13 16:20:24 +01:00
parent 0f043bda61
commit 8a8aecf557
17 changed files with 846 additions and 440 deletions
+32
View File
@@ -0,0 +1,32 @@
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`
<card-backdrop>
<ui-card>
<card-header
heading=${this.heading}
subheading=${this.subheading}
></card-header>
<slot></slot>
</ui-card>
</card-backdrop>
`;
}
}
@@ -0,0 +1,31 @@
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`<slot></slot>`;
}
}
+38
View File
@@ -0,0 +1,38 @@
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`
<h2>${this.heading}</h2>
${this.subheading ? html`<p>${this.subheading}</p>` : null}
`;
}
}
-44
View File
@@ -1,44 +0,0 @@
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;
}
}
+47 -3
View File
@@ -1,11 +1,55 @@
import { LitElement, html, css, unsafeCSS } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js'; import { customElement } from 'lit/decorators.js';
import styles from './footer-bar.css?inline';
import './ui-link'; import './ui-link';
@customElement('footer-bar') @customElement('footer-bar')
export class FooterBar extends LitElement { export class FooterBar extends LitElement {
static styles = css`${unsafeCSS(styles)}`; 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() { render() {
const year = new Date().getFullYear(); const year = new Date().getFullYear();
+73
View File
@@ -0,0 +1,73 @@
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`
<label>${this.label}</label>
<input
type=${this.type}
placeholder=${this.placeholder}
.value=${this.value}
@input=${this.handleInput}
/>
`;
}
}
@@ -0,0 +1,19 @@
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``;
}
}
-98
View File
@@ -1,98 +0,0 @@
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);
}
+101 -3
View File
@@ -1,11 +1,109 @@
import { LitElement, html, css, unsafeCSS } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import styles from './nav-bar.css?inline';
import './ui-link'; import './ui-link';
@customElement('nav-bar') @customElement('nav-bar')
export class NavBar extends LitElement { export class NavBar extends LitElement {
static styles = css`${unsafeCSS(styles)}`; 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' = @state() theme: 'light' | 'dark' =
(localStorage.getItem('theme') as 'light' | 'dark') || (localStorage.getItem('theme') as 'light' | 'dark') ||
+150
View File
@@ -0,0 +1,150 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js';
export type NotificationType = 'success' | 'warning' | 'error';
@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);
}
`;
@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 html`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
`;
}
if (this.type === 'warning') {
return html`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M10.29 3.86L1.82 18a2 2 0 001.71 3h16.94a2 2 0 001.71-3L13.71 3.86a2 2 0 00-3.42 0z" />
<line x1="12" y1="9" x2="12" y2="13" />
<line x1="12" y1="17" x2="12.01" y2="17" />
</svg>
`;
}
return html`
<svg class="icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 11.08V12a10 10 0 11-5.93-9.14" />
<polyline points="22 4 12 14.01 9 11.01" />
</svg>
`;
}
render() {
if (!this.message) return null;
return html`
<div class="bar ${this.type}">
${this.renderIcon()}
<span class="content">${this.message}</span>
${this.dismissible
? html`
<button class="close" @click=${this.dismiss} aria-label="Close">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
`
: null}
</div>
`;
}
}
+72
View File
@@ -0,0 +1,72 @@
import { LitElement, html, css } from 'lit';
import { customElement, property } 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;
}
::slotted(svg) {
width: 1rem;
height: 1rem;
flex-shrink: 0;
}
`;
@property({ type: Boolean }) disabled = false;
@property({ type: Boolean, reflect: true }) full = false;
render() {
return html`
<button ?disabled=${this.disabled}>
<slot></slot>
</button>
`;
}
}
+43
View File
@@ -0,0 +1,43 @@
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`
<div class="card">
<slot></slot>
</div>
`;
}
}
-37
View File
@@ -1,37 +0,0 @@
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);
}
+40 -3
View File
@@ -1,10 +1,47 @@
import { LitElement, html, css, unsafeCSS } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, property } from 'lit/decorators.js'; import { customElement, property } from 'lit/decorators.js';
import styles from './ui-link.css?inline';
@customElement('ui-link') @customElement('ui-link')
export class UiLink extends LitElement { export class UiLink extends LitElement {
static styles = css`${unsafeCSS(styles)}`; 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() href = '/';
@property({ type: Boolean }) active = false; @property({ type: Boolean }) active = false;
+31 -170
View File
@@ -1,6 +1,11 @@
import { LitElement, html, css } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import { apiPost } from '../../api/api'; import { apiPost } from '../../api/api';
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 '../../components/ui-link';
@customElement('login-page') @customElement('login-page')
@@ -9,139 +14,6 @@ export class LoginPage extends LitElement {
:host { :host {
flex: 1; flex: 1;
display: flex; 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;
}
.card {
width: 100%;
max-width: 400px;
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);
}
.header {
display: flex;
flex-direction: column;
gap: 0.35rem;
margin-bottom: 0.25rem;
}
.header h2 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-text);
}
.header p {
margin: 0;
font-size: 0.875rem;
color: color-mix(in srgb, var(--color-text) 60%, transparent);
}
.field {
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);
}
.error {
color: #e5484d;
font-size: 0.85rem;
font-weight: 500;
padding: 0.5rem 0.75rem;
background: color-mix(in srgb, #e5484d 8%, transparent);
border: 1px solid color-mix(in srgb, #e5484d 25%, transparent);
border-radius: 0.5rem;
}
button {
margin-top: 0.25rem;
width: 100%;
padding: 0.7rem 1rem;
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;
} }
.footer { .footer {
@@ -150,13 +22,6 @@ export class LoginPage extends LitElement {
color: color-mix(in srgb, var(--color-text) 60%, transparent); color: color-mix(in srgb, var(--color-text) 60%, transparent);
margin: 0; margin: 0;
} }
.divider {
width: 100%;
height: 1px;
background: var(--color-border);
margin: 0.25rem 0;
}
`; `;
@state() email = ''; @state() email = '';
@@ -188,52 +53,48 @@ export class LoginPage extends LitElement {
} }
} }
private handleKeydown(e: KeyboardEvent) {
if (e.key === 'Enter') this.handleLogin();
}
render() { render() {
return html` return html`
<div class="card"> <auth-card
<div class="header"> heading="Welcome back"
<h2>Welcome back</h2> subheading="Sign in to your FlightScore account"
<p>Sign in to your FlightScore account</p> >
</div> <form-input
label="Email"
<div class="field">
<label>Email</label>
<input
type="email" type="email"
placeholder="you@example.com" placeholder="you@example.com"
.value=${this.email} .value=${this.email}
@input=${(e: any) => (this.email = e.target.value)} @value-changed=${(e: CustomEvent) =>
@keydown=${this.handleKeydown} (this.email = e.detail.value)}
/> ></form-input>
</div>
<div class="field"> <form-input
<label>Password</label> label="Password"
<input
type="password" type="password"
placeholder="Enter your password" placeholder="Enter your password"
.value=${this.password} .value=${this.password}
@input=${(e: any) => (this.password = e.target.value)} @value-changed=${(e: CustomEvent) =>
@keydown=${this.handleKeydown} (this.password = e.detail.value)}
/> ></form-input>
</div>
${this.error ? html`<div class="error">${this.error}</div>` : null} <notify-bar type="error" .message=${this.error}></notify-bar>
<button ?disabled=${this.loading} @click=${this.handleLogin}> <ui-button
full
?disabled=${this.loading}
@click=${this.handleLogin}
>
${this.loading ? 'Signing in...' : 'Sign in'} ${this.loading ? 'Signing in...' : 'Sign in'}
</button> </ui-button>
<div class="divider"></div>
<horizontal-divider></horizontal-divider>
<p class="footer"> <p class="footer">
No account? <ui-link href="/register">Create one</ui-link> No account?
<ui-link href="/register">Create one</ui-link>
</p> </p>
</div> </auth-card>
`; `;
} }
} }
+52 -57
View File
@@ -1,49 +1,26 @@
import { LitElement, html, css } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js'; import { customElement, state } from 'lit/decorators.js';
import { apiPost } from '../../api/api'; import { apiPost } from '../../api/api';
import '../../components/auth-card.ts';
import '../../components/form-input.ts';
import '../../components/ui-button';
import '../../components/notify-bar.ts';
import '../../components/horizontal-divider';
import '../../components/ui-link';
@customElement('register-page') @customElement('register-page')
export class RegisterPage extends LitElement { export class RegisterPage extends LitElement {
static styles = css` static styles = css`
:host { :host {
flex: 1; flex: 1;
background: linear-gradient(
135deg,
var(--color-accent),
color-mix(in srgb, var(--color-accent) 30%, black)
);
background-size: cover;
background-position: center;
display: flex; display: flex;
justify-content: center;
align-items: center;
} }
.form { .footer {
width: 100%; text-align: center;
max-width: 380px; font-size: 0.875rem;
background: var(--color-bg-nav); color: color-mix(in srgb, var(--color-text) 60%, transparent);
border: 1px solid var(--color-border); margin: 0;
border-radius: 12px;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1rem;
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.25);
}
input {
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 0.6rem;
font-size: 1rem;
background: var(--color-bg);
color: var(--color-text);
}
.error {
color: crimson;
font-size: 0.9rem;
} }
`; `;
@@ -57,7 +34,7 @@ export class RegisterPage extends LitElement {
this.error = null; this.error = null;
this.loading = true; this.loading = true;
try { try {
const res = await apiPost<{ id: number; name: string; email: string }>( await apiPost<{ id: number; name: string; email: string }>(
'/api/auth/register', '/api/auth/register',
{ {
name: this.name, name: this.name,
@@ -65,7 +42,6 @@ export class RegisterPage extends LitElement {
password: this.password, password: this.password,
} }
); );
console.log('Registered user:', res);
window.dispatchEvent( window.dispatchEvent(
new CustomEvent('nav', { new CustomEvent('nav', {
detail: { path: '/login' }, detail: { path: '/login' },
@@ -82,34 +58,53 @@ export class RegisterPage extends LitElement {
render() { render() {
return html` return html`
<div class="form"> <auth-card
<h2>Register</h2> heading="Create account"
<input subheading="Get started with FlightScore"
placeholder="Full name" >
<form-input
label="Name"
placeholder="Your full name"
.value=${this.name} .value=${this.name}
@input=${(e: any) => (this.name = e.target.value)} @value-changed=${(e: CustomEvent) =>
/> (this.name = e.detail.value)}
<input ></form-input>
type="email"
placeholder="Email"
.value=${this.email}
@input=${(e: any) => (this.email = e.target.value)}
/>
<input
type="password"
placeholder="Password"
.value=${this.password}
@input=${(e: any) => (this.password = e.target.value)}
/>
${this.error ? html`<div class="error">${this.error}</div>` : null} <form-input
label="Email"
type="email"
placeholder="you@example.com"
.value=${this.email}
@value-changed=${(e: CustomEvent) =>
(this.email = e.detail.value)}
></form-input>
<form-input
label="Password"
type="password"
placeholder="Choose a password"
.value=${this.password}
@value-changed=${(e: CustomEvent) =>
(this.password = e.detail.value)}
></form-input>
<notify-bar type="error" .message=${this.error}></notify-bar>
<ui-button <ui-button
full
?disabled=${this.loading} ?disabled=${this.loading}
@click=${this.handleRegister} @click=${this.handleRegister}
>${this.loading ? 'Loading...' : 'Register'}</ui-button
> >
</div> ${this.loading ? 'Creating account...' : 'Create account'}
</ui-button>
<horizontal-divider></horizontal-divider>
<p class="footer">
Already have an account?
<ui-link href="/login">Sign in</ui-link>
</p>
</auth-card>
`; `;
} }
} }
+104 -12
View File
@@ -1,31 +1,123 @@
import { LitElement, html, css } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js'; 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') @customElement('not-found-page')
export class NotFoundPage extends LitElement { export class NotFoundPage extends LitElement {
static styles = css` static styles = css`
div { :host {
padding-top: 4rem; flex: 1;
* { display: flex;
width: fit-content;
margin: auto auto;
padding-bottom: 1rem;
} }
.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;
} }
`; `;
navigate(path: string) { private navigate(path: string) {
this.dispatchEvent( this.dispatchEvent(
new CustomEvent('nav', { detail: { path }, bubbles: true, composed: true }) new CustomEvent('nav', {
detail: { path },
bubbles: true,
composed: true,
})
); );
} }
render() { render() {
return html` return html`
<div> <card-backdrop>
<h1>404 Not Found</h1> <ui-card centered>
<p><ui-link href="/">Here</ui-link>you can get back.</p> <p class="code">404</p>
</div> <h2>Page not found</h2>
<p>
The page you are looking for does not exist or has been
moved.
</p>
<horizontal-divider></horizontal-divider>
<ui-button @click=${()=> this.navigate('/')}>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"></line>
<polyline points="12 19 5 12 12 5"></polyline>
</svg>
Back to home
</ui-button>
</ui-card>
</card-backdrop>
`; `;
} }
} }