Added a Login and Register-Page

This commit is contained in:
CodingPhoenixx
2026-02-12 16:31:47 +01:00
parent 7a69e4a4ea
commit 61ebfec1c9
10 changed files with 311 additions and 15 deletions
+54
View File
@@ -0,0 +1,54 @@
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'QUERY';
export interface ApiOptions extends RequestInit {
method?: HttpMethod;
body?: any;
}
/**
* Zentraler API-Wrapper mit typisiertem Rückgabewert und Fehlerhandling.
* Beispiele:
* const tracks = await api<Track[]>('/api/tracks');
* await api<void>('/api/tracks/1', { method: 'DELETE' });
*/
export async function api<T = any>(
path: string,
options: ApiOptions = {}
): Promise<T> {
const config: RequestInit = {
...options,
headers: {
'Content-Type': 'application/json',
...(options.headers || {}),
},
};
if (options.body && typeof options.body !== 'string') {
config.body = JSON.stringify(options.body);
}
const response = await fetch(path, config);
if (!response.ok) {
let msg = `HTTP Error ${response.status}`;
try {
const text = await response.text();
msg += `: ${text}`;
} catch { }
throw new Error(msg);
}
if (response.status === 204) return undefined as T;
try {
return (await response.json()) as T;
} catch {
return undefined as T;
}
}
export const apiGet = <T>(path: string) => api<T>(path, { method: 'GET' });
export const apiPost = <T>(path: string, body: any) => api<T>(path, { method: 'POST', body });
export const apiPut = <T>(path: string, body: any) => api<T>(path, { method: 'PUT', body });
export const apiDelete = <T>(path: string) => api<T>(path, { method: 'DELETE' });
+28 -3
View File
@@ -1,21 +1,46 @@
import { LitElement, html } from 'lit';
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import './pages/not-found-page';
import './components/nav-bar';
import './components/footer-bar';
import { Router } from './router/router';
import './pages/home-page';
import './pages/competition-page';
import './pages/auth/login-page';
import './pages/auth/register-page';
@customElement('app-root')
export class AppRoot extends LitElement {
private router!: Router;
static styles = css`
:host {
display: flex;
flex-direction: column;
min-height: 100vh;
}
nav-bar {
flex: 0 0 auto;
}
main {
flex: 1 0 auto;
display: flex;
flex-direction: column;
}
footer-bar {
flex: 0 0 auto;
}
`
firstUpdated() {
const outlet = this.shadowRoot?.getElementById('outlet') as HTMLElement;
this.router = new Router(outlet, [
{ path: '/', view: () => document.createElement('home-page') },
{ path: '/tracks', view: () => document.createElement('tracks-page') },
{ path: '/about', view: () => document.createElement('about-page') },
{ path: '/competitions', view: () => document.createElement('competition-page') },
{ path: '/login', view: () => document.createElement('login-page') },
{ path: '/register', view: () => document.createElement('register-page') },
]);
this.router.resolve();
this.addEventListener('nav', (e: any) => this.router.navigate(e.detail.path));
+3 -5
View File
@@ -5,14 +5,12 @@ footer {
display: flex;
justify-content: space-around;
align-items: center;
padding: 0.8rem 1.5rem;
padding: 0.8rem 0;
font-size: 0.9rem;
margin-top: 3rem;
flex-wrap: wrap;
backdrop-filter: blur(10px);
position: fixed;
bottom: 0;
width: 100vw;
position: relative;
width: 100%;
}
a {
+2
View File
@@ -19,6 +19,8 @@ nav {
display: flex;
justify-content: center;
align-items: center;
padding-right: 1rem;
}
.brand img {
+1
View File
@@ -36,6 +36,7 @@ export class NavBar extends LitElement {
<div class="links">
<ui-link href="/">Home</ui-link>
<ui-link href="/competitions">Competitions</ui-link>
<ui-link href="/login">Login</ui-link>
<button @click=${this.toggleTheme}>
${this.theme === 'light' ? '🌙 Dark' : '☀️ Light'}
+97
View File
@@ -0,0 +1,97 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { apiPost } from '../../api/api';
import '../../components/ui-link';
@customElement('login-page')
export class LoginPage extends LitElement {
static styles = css`
:host {
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;
justify-content: center;
align-items: center;
}
.form {
width: 100%;
max-width: 380px;
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
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);
}
ui-button {
align-self: flex-end;
}
.error {
color: crimson;
font-size: 0.9rem;
}
`;
@state() email = '';
@state() password = '';
@state() error: string | null = null;
@state() loading = false;
async handleLogin() {
this.error = null;
this.loading = true;
try {
const res = await apiPost<{ token: string }>('/api/auth/login', {
email: this.email,
password: this.password,
});
localStorage.setItem('token', res.token);
window.dispatchEvent(new CustomEvent('auth-changed')); // for later reactive flows
} catch (e: any) {
this.error = e.message || 'Login failed';
} finally {
this.loading = false;
}
}
render() {
return html`
<div class="form">
<h2>Login</h2>
<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}
<ui-button ?disabled=${this.loading} @click=${this.handleLogin}>${this.loading ? 'Loading...' : 'Login'}</ui-button>
<p>
No account?
<ui-link href="/register">Register</ui-link>
</p>
</div>
`;
}
}
+115
View File
@@ -0,0 +1,115 @@
import { LitElement, html, css } from 'lit';
import { customElement, state } from 'lit/decorators.js';
import { apiPost } from '../../api/api';
@customElement('register-page')
export class RegisterPage extends LitElement {
static styles = css`
:host {
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;
justify-content: center;
align-items: center;
}
.form {
width: 100%;
max-width: 380px;
background: var(--color-bg-nav);
border: 1px solid var(--color-border);
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;
}
`;
@state() name = '';
@state() email = '';
@state() password = '';
@state() error: string | null = null;
@state() loading = false;
async handleRegister() {
this.error = null;
this.loading = true;
try {
const res = await apiPost<{ id: number; name: string; email: string }>(
'/api/auth/register',
{
name: this.name,
email: this.email,
password: this.password,
}
);
console.log('Registered user:', res);
window.dispatchEvent(
new CustomEvent('nav', {
detail: { path: '/login' },
bubbles: true,
composed: true,
})
);
} catch (e: any) {
this.error = e.message || 'Registration failed';
} finally {
this.loading = false;
}
}
render() {
return html`
<div class="form">
<h2>Register</h2>
<input
placeholder="Full name"
.value=${this.name}
@input=${(e: any) => (this.name = e.target.value)}
/>
<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}
<ui-button
?disabled=${this.loading}
@click=${this.handleRegister}
>${this.loading ? 'Loading...' : 'Register'}</ui-button
>
</div>
`;
}
}
+11
View File
@@ -0,0 +1,11 @@
import { LitElement, html } from 'lit';
import { customElement } from 'lit/decorators.js';
@customElement('competition-page')
export class CompetitionPage extends LitElement {
render() {
return html`
<h1>Welcome to the Competitions</h1>
`;
}
}
-7
View File
@@ -27,11 +27,4 @@ body {
font-family: 'Inter', system-ui, sans-serif;
}
a {
color: var(--color-accent);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}