poprawka w trybie jasnym i logowaniu

This commit is contained in:
Mateusz Gruszczyński
2026-04-05 13:50:21 +02:00
parent 9a6e77a5fc
commit 1ba1a26291
2 changed files with 108 additions and 52 deletions

View File

@@ -1,5 +1,5 @@
import { CommonModule } from '@angular/common';
import { Component, inject, signal } from '@angular/core';
import { Component, computed, 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';
@@ -24,50 +24,26 @@ import { UiService } from '../../core/services/ui.service';
<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>
<div class="d-flex gap-2">
<button class="btn btn-icon btn-ghost-secondary"
type="button"
[attr.aria-label]="ui.t('theme.label')"
[attr.title]="ui.t('theme.label')"
(click)="ui.toggleTheme()">
@if (ui.theme() === 'dark') {
<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="M12 3l0 1"/><path d="M12 20l0 1"/><path d="M3 12l1 0"/><path d="M20 12l1 0"/><path d="M5.6 5.6l.7 .7"/><path d="M18.4 18.4l.7 .7"/><path d="M18.4 5.6l-.7 .7"/><path d="M5.6 18.4l-.7 .7"/><path d="M9 12a3 3 0 1 0 6 0a3 3 0 0 0 -6 0"/></svg>
} @else {
<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="M12 3c.132 0 .263 0 .393 .007a9 9 0 1 0 0 17.986a9 9 0 0 1 -.393 -17.993z"/></svg>
}
</button>
<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>
<button class="btn btn-icon btn-ghost-secondary"
type="button"
[attr.aria-label]="currentLanguageLabel()"
[attr.title]="currentLanguageLabel()"
(click)="toggleLanguage()">
<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="M4 5h7"/><path d="M7 4c0 4.846 0 7 .5 8"/><path d="M10 8l-3 4l-3 -4"/><path d="M19 22l0 -3"/><path d="M17 19h4"/><path d="M20 19l-3 -7l-3 7"/><path d="M11 19l4 0"/></svg>
</button>
</div>
</div>
@@ -89,16 +65,26 @@ import { UiService } from '../../core/services/ui.service';
<input class="form-control form-control-lg" type="password" formControlName="password" autocomplete="current-password" />
</div>
@if (errorMessage()) {
<div class="alert alert-danger py-2 mb-0">{{ errorMessage() }}</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>
<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="M15 15l6 6"/><path d="M4 11a7 7 0 1 1 14 0a7 7 0 0 1 -14 0"/></svg>
{{ loading() ? (mode() === 'login' ? ui.t('action.loggingIn') : ui.t('action.creatingAccount')) : (mode() === 'login' ? ui.t('action.login') : ui.t('action.createAccount')) }}
</button>
</form>
<div class="login-footer-note">
{{ mode() === 'login'
? (ui.language() === 'pl' ? 'Użyj swojego konta, aby zarządzać wydatkami, raportami i uprawnieniami.' : 'Use your account to manage expenses, reports and user permissions.')
: (ui.language() === 'pl' ? 'Po utworzeniu konta od razu wrócisz do logowania.' : 'After creating an account you will be taken back to sign in.') }}
</div>
@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')">
<button class="btn btn-ghost-primary btn-sm" type="button" (click)="switchMode()">
{{ mode() === 'login' ? ui.t('action.registerMode') : ui.t('action.loginMode') }}
</button>
</div>
@@ -121,6 +107,8 @@ export class LoginComponent {
readonly loading = signal(false);
readonly mode = signal<'login' | 'register'>('login');
readonly errorMessage = signal<string | null>(null);
readonly currentLanguageLabel = computed(() => this.ui.language() === 'pl' ? 'Polski' : 'English');
readonly form = this.fb.nonNullable.group({
email: ['', [Validators.required, Validators.email]],
@@ -134,6 +122,7 @@ export class LoginComponent {
submit() {
if (this.form.invalid) return;
this.errorMessage.set(null);
this.loading.set(true);
const raw = this.form.getRawValue();
@@ -144,8 +133,10 @@ export class LoginComponent {
this.router.navigate(['/']);
},
error: (error) => {
const message = error.error?.message ?? 'Nie udało się zalogować.';
this.loading.set(false);
this.toast.error(error.error?.message ?? 'Nie udało się zalogować.');
this.errorMessage.set(message);
this.toast.error(message);
}
});
return;
@@ -154,16 +145,28 @@ export class LoginComponent {
this.auth.register({ email: raw.email, password: raw.password, fullName: raw.fullName || raw.email }).subscribe({
next: () => {
this.loading.set(false);
this.errorMessage.set(null);
this.toast.success('Konto zostało utworzone.');
this.mode.set('login');
},
error: (error) => {
const message = error.error?.message ?? 'Nie udało się utworzyć konta.';
this.loading.set(false);
this.toast.error(error.error?.message ?? 'Nie udało się utworzyć konta.');
this.errorMessage.set(message);
this.toast.error(message);
}
});
}
toggleLanguage() {
this.ui.setLanguage(this.ui.language() === 'pl' ? 'en' : 'pl');
}
switchMode() {
this.errorMessage.set(null);
this.mode.set(this.mode() === 'login' ? 'register' : 'login');
}
loginSubtitle() {
return this.ui.language() === 'pl'
? 'Zaloguj się, aby zarządzać wydatkami, kontrahentami i raportami.'

View File

@@ -3,16 +3,18 @@
:root {
--tblr-primary: #111827;
--tblr-primary-rgb: 17, 24, 39;
--tblr-border-radius: 0.75rem;
--tblr-border-radius-lg: 0.9rem;
--tblr-border-radius-sm: 0.45rem;
--tblr-card-border-radius: 0.9rem;
--tblr-border-radius: 1rem;
--tblr-border-radius-lg: 1rem;
--tblr-border-radius-sm: 0.75rem;
--tblr-card-border-radius: 1rem;
--tblr-body-bg: #f4f6f8;
--ec-shell-bg: #f4f6f8;
--ec-card-shadow: 0 2px 8px rgba(15, 23, 42, 0.05);
--ec-card-border: rgba(15, 23, 42, 0.08);
--ec-navbar-bg: rgba(255, 255, 255, 0.95);
--ec-subnav-bg: rgba(255, 255, 255, 0.9);
--ec-light-radius-sm: 0.75rem;
--ec-light-radius-md: 1rem;
}
html,
@@ -24,6 +26,41 @@ body {
background: var(--ec-shell-bg);
}
[data-bs-theme="light"] {
--tblr-border-radius: var(--ec-light-radius-md);
--tblr-border-radius-lg: var(--ec-light-radius-md);
--tblr-border-radius-sm: var(--ec-light-radius-sm);
--tblr-card-border-radius: var(--ec-light-radius-md);
}
[data-bs-theme="light"] .card,
[data-bs-theme="light"] .pv-card,
[data-bs-theme="light"] .login-card,
[data-bs-theme="light"] .modal-content,
[data-bs-theme="light"] .alert,
[data-bs-theme="light"] .toast,
[data-bs-theme="light"] .dropdown-menu,
[data-bs-theme="light"] .table-responsive,
[data-bs-theme="light"] .form-control,
[data-bs-theme="light"] .form-select,
[data-bs-theme="light"] .form-check-input,
[data-bs-theme="light"] .input-group-text,
[data-bs-theme="light"] .btn,
[data-bs-theme="light"] .nav-segmented,
[data-bs-theme="light"] .nav-pills .nav-link,
[data-bs-theme="light"] .nav-tabs .nav-link,
[data-bs-theme="light"] .page-link {
border-radius: var(--ec-light-radius-md);
}
[data-bs-theme="light"] .rounded-3 {
border-radius: var(--ec-light-radius-md) !important;
}
[data-bs-theme="light"] .rounded-2 {
border-radius: var(--ec-light-radius-sm) !important;
}
[data-bs-theme="dark"] {
--tblr-primary: #0f172a;
--tblr-primary-rgb: 15, 23, 42;
@@ -158,7 +195,21 @@ body {
}
.login-card-enhanced {
position: relative;
overflow: hidden;
border-width: 1px;
}
.login-card-enhanced::before {
content: "";
position: absolute;
inset: 0 0 auto 0;
height: 5px;
background: linear-gradient(90deg, rgba(32, 107, 196, 0.95), rgba(47, 179, 68, 0.9));
}
.login-card-enhanced .card-body {
position: relative;
}
.login-input-stack {
@@ -175,6 +226,8 @@ body {
.login-footer-note {
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid rgba(148, 163, 184, 0.18);
color: var(--tblr-secondary);
font-size: 0.875rem;
}