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 { customElement } from 'lit/decorators.js';
|
||||||
import './pages/not-found-page';
|
import './pages/not-found-page';
|
||||||
import './components/nav-bar';
|
import './components/nav-bar';
|
||||||
import './components/footer-bar';
|
import './components/footer-bar';
|
||||||
import { Router } from './router/router';
|
import { Router } from './router/router';
|
||||||
import './pages/home-page';
|
import './pages/home-page';
|
||||||
|
import './pages/competition-page';
|
||||||
|
import './pages/auth/login-page';
|
||||||
|
import './pages/auth/register-page';
|
||||||
|
|
||||||
@customElement('app-root')
|
@customElement('app-root')
|
||||||
export class AppRoot extends LitElement {
|
export class AppRoot extends LitElement {
|
||||||
private router!: Router;
|
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() {
|
firstUpdated() {
|
||||||
const outlet = this.shadowRoot?.getElementById('outlet') as HTMLElement;
|
const outlet = this.shadowRoot?.getElementById('outlet') as HTMLElement;
|
||||||
this.router = new Router(outlet, [
|
this.router = new Router(outlet, [
|
||||||
{ path: '/', view: () => document.createElement('home-page') },
|
{ path: '/', view: () => document.createElement('home-page') },
|
||||||
{ path: '/tracks', view: () => document.createElement('tracks-page') },
|
{ path: '/competitions', view: () => document.createElement('competition-page') },
|
||||||
{ path: '/about', view: () => document.createElement('about-page') },
|
{ path: '/login', view: () => document.createElement('login-page') },
|
||||||
|
{ path: '/register', view: () => document.createElement('register-page') },
|
||||||
]);
|
]);
|
||||||
this.router.resolve();
|
this.router.resolve();
|
||||||
this.addEventListener('nav', (e: any) => this.router.navigate(e.detail.path));
|
this.addEventListener('nav', (e: any) => this.router.navigate(e.detail.path));
|
||||||
|
|||||||
@@ -5,14 +5,12 @@ footer {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0.8rem 1.5rem;
|
padding: 0.8rem 0;
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
margin-top: 3rem;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
position: fixed;
|
position: relative;
|
||||||
bottom: 0;
|
width: 100%;
|
||||||
width: 100vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ nav {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding-right: 1rem;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand img {
|
.brand img {
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export class NavBar extends LitElement {
|
|||||||
<div class="links">
|
<div class="links">
|
||||||
<ui-link href="/">Home</ui-link>
|
<ui-link href="/">Home</ui-link>
|
||||||
<ui-link href="/competitions">Competitions</ui-link>
|
<ui-link href="/competitions">Competitions</ui-link>
|
||||||
|
<ui-link href="/login">Login</ui-link>
|
||||||
|
|
||||||
<button @click=${this.toggleTheme}>
|
<button @click=${this.toggleTheme}>
|
||||||
${this.theme === 'light' ? '🌙 Dark' : '☀️ Light'}
|
${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;
|
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