Added a Login and Register-Page
This commit is contained in:
@@ -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' });
|
||||
@@ -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));
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -19,6 +19,8 @@ nav {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding-right: 1rem;
|
||||
|
||||
}
|
||||
|
||||
.brand img {
|
||||
|
||||
@@ -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'}
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
`;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user