first commit

This commit is contained in:
Mateusz Gruszczyński
2026-04-05 13:40:27 +02:00
commit 9a6e77a5fc
89 changed files with 18276 additions and 0 deletions

View 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?';
}
}