first commit
This commit is contained in:
186
web/src/app/features/auth/login.component.ts
Normal file
186
web/src/app/features/auth/login.component.ts
Normal file
@@ -0,0 +1,186 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, inject, signal } from '@angular/core';
|
||||
import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { AppSettingsService } from '../../core/services/app-settings.service';
|
||||
import { AuthService } from '../../core/services/auth.service';
|
||||
import { ToastService } from '../../core/services/toast.service';
|
||||
import { UiService } from '../../core/services/ui.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-login',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ReactiveFormsModule],
|
||||
template: `
|
||||
<div class="page page-center login-page-shell">
|
||||
<div class="container py-4">
|
||||
<div class="row justify-content-center align-items-stretch g-4 login-layout">
|
||||
<div class="col-12 col-md-10 col-lg-7 col-xl-5">
|
||||
<div class="card card-md login-card login-card-enhanced">
|
||||
<div class="card-body">
|
||||
<div class="d-flex justify-content-between align-items-start gap-3 mb-4 flex-wrap">
|
||||
<div>
|
||||
<h1 class="h2 mb-1">{{ appSettings.appName() }}</h1>
|
||||
<div class="text-secondary">{{ mode() === 'login' ? loginSubtitle() : registerSubtitle() }}</div>
|
||||
</div>
|
||||
|
||||
<div class="d-grid gap-2 login-toolbar-controls">
|
||||
<nav class="nav nav-segmented ec-segmented-control" role="tablist" [attr.aria-label]="ui.t('lang.label')">
|
||||
<button class="nav-link"
|
||||
type="button"
|
||||
role="tab"
|
||||
[class.active]="ui.language() === 'pl'"
|
||||
[attr.aria-selected]="ui.language() === 'pl'"
|
||||
[attr.aria-current]="ui.language() === 'pl' ? 'page' : null"
|
||||
(click)="ui.setLanguage('pl')">
|
||||
{{ ui.t('lang.pl') }}
|
||||
</button>
|
||||
<button class="nav-link"
|
||||
type="button"
|
||||
role="tab"
|
||||
[class.active]="ui.language() === 'en'"
|
||||
[attr.aria-selected]="ui.language() === 'en'"
|
||||
[attr.aria-current]="ui.language() === 'en' ? 'page' : null"
|
||||
(click)="ui.setLanguage('en')">
|
||||
{{ ui.t('lang.en') }}
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<nav class="nav nav-segmented ec-segmented-control" role="tablist" [attr.aria-label]="ui.t('theme.label')">
|
||||
<button class="nav-link d-inline-flex align-items-center gap-2"
|
||||
type="button"
|
||||
role="tab"
|
||||
[class.active]="ui.theme() === 'dark'"
|
||||
[attr.aria-selected]="ui.theme() === 'dark'"
|
||||
[attr.aria-current]="ui.theme() === 'dark' ? 'page' : null"
|
||||
(click)="ui.setTheme('dark')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 .007a8.5 8.5 0 0 0 0 16.986a9 9 0 1 1 -.393 -17z"/></svg>
|
||||
<span>{{ ui.t('theme.dark') }}</span>
|
||||
</button>
|
||||
<button class="nav-link d-inline-flex align-items-center gap-2"
|
||||
type="button"
|
||||
role="tab"
|
||||
[class.active]="ui.theme() === 'light'"
|
||||
[attr.aria-selected]="ui.theme() === 'light'"
|
||||
[attr.aria-current]="ui.theme() === 'light' ? 'page' : null"
|
||||
(click)="ui.setTheme('light')">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon icon-sm" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 3c.132 0 .263 0 .393 .007a9 9 0 1 0 0 17.986a9 9 0 0 0 -.393 -17.993z"/><path d="M12 3v1"/><path d="M12 20v1"/><path d="M3 12h1"/><path d="M20 12h1"/><path d="M5.6 5.6l.7 .7"/><path d="M17.7 17.7l.7 .7"/><path d="M17.7 6.3l.7 -.7"/><path d="M6.3 17.7l-.7 .7"/></svg>
|
||||
<span>{{ ui.t('theme.light') }}</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form [formGroup]="form" (ngSubmit)="submit()" class="login-input-stack">
|
||||
@if (mode() === 'register') {
|
||||
<div>
|
||||
<label class="form-label">{{ ui.t('login.fullName') }}</label>
|
||||
<input class="form-control form-control-lg" formControlName="fullName" autocomplete="name" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div>
|
||||
<label class="form-label">{{ ui.t('login.email') }}</label>
|
||||
<input class="form-control form-control-lg" formControlName="email" autocomplete="username" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="form-label">{{ ui.t('login.password') }}</label>
|
||||
<input class="form-control form-control-lg" type="password" formControlName="password" autocomplete="current-password" />
|
||||
</div>
|
||||
|
||||
<button class="btn btn-primary btn-lg w-100 login-submit-button" [disabled]="form.invalid || loading()">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M9 8l0 -2a2 2 0 1 1 4 0v2"/><path d="M5 8h14l0 12h-14z"/><path d="M12 12l0 .01"/></svg>
|
||||
{{ loading() ? (mode() === 'login' ? ui.t('action.loggingIn') : ui.t('action.creatingAccount')) : (mode() === 'login' ? ui.t('action.login') : ui.t('action.createAccount')) }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@if (appSettings.registrationEnabled()) {
|
||||
<div class="login-footer-note d-flex justify-content-between align-items-center gap-2 flex-wrap">
|
||||
<span>{{ mode() === 'login' ? switchToRegisterLabel() : switchToLoginLabel() }}</span>
|
||||
<button class="btn btn-ghost-primary btn-sm" type="button" (click)="mode.set(mode() === 'login' ? 'register' : 'login')">
|
||||
{{ mode() === 'login' ? ui.t('action.registerMode') : ui.t('action.loginMode') }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
})
|
||||
export class LoginComponent {
|
||||
private readonly fb = inject(FormBuilder);
|
||||
private readonly auth = inject(AuthService);
|
||||
private readonly router = inject(Router);
|
||||
private readonly toast = inject(ToastService);
|
||||
readonly ui = inject(UiService);
|
||||
readonly appSettings = inject(AppSettingsService);
|
||||
|
||||
readonly loading = signal(false);
|
||||
readonly mode = signal<'login' | 'register'>('login');
|
||||
|
||||
readonly form = this.fb.nonNullable.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
password: ['', [Validators.required, Validators.minLength(6)]],
|
||||
fullName: ['']
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.appSettings.loadPublic().subscribe({ error: () => undefined });
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.form.invalid) return;
|
||||
this.loading.set(true);
|
||||
const raw = this.form.getRawValue();
|
||||
|
||||
if (this.mode() === 'login') {
|
||||
this.auth.login({ email: raw.email, password: raw.password }).subscribe({
|
||||
next: () => {
|
||||
this.loading.set(false);
|
||||
this.router.navigate(['/']);
|
||||
},
|
||||
error: (error) => {
|
||||
this.loading.set(false);
|
||||
this.toast.error(error.error?.message ?? 'Nie udało się zalogować.');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.auth.register({ email: raw.email, password: raw.password, fullName: raw.fullName || raw.email }).subscribe({
|
||||
next: () => {
|
||||
this.loading.set(false);
|
||||
this.toast.success('Konto zostało utworzone.');
|
||||
this.mode.set('login');
|
||||
},
|
||||
error: (error) => {
|
||||
this.loading.set(false);
|
||||
this.toast.error(error.error?.message ?? 'Nie udało się utworzyć konta.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loginSubtitle() {
|
||||
return this.ui.language() === 'pl'
|
||||
? 'Zaloguj się, aby zarządzać wydatkami, kontrahentami i raportami.'
|
||||
: 'Sign in to manage expenses, merchants and reports.';
|
||||
}
|
||||
|
||||
registerSubtitle() {
|
||||
return this.ui.language() === 'pl'
|
||||
? 'Utwórz konto i zacznij zbierać potwierdzenia oraz statystyki.'
|
||||
: 'Create an account and start collecting proofs and analytics.';
|
||||
}
|
||||
|
||||
switchToRegisterLabel() {
|
||||
return this.ui.language() === 'pl' ? 'Nie masz konta?' : 'Need an account?';
|
||||
}
|
||||
|
||||
switchToLoginLabel() {
|
||||
return this.ui.language() === 'pl' ? 'Masz już konto?' : 'Already registered?';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user