feat: add brand mark component and dashboard page
- Introduced a new `BrandMark` component for branding purposes. - Created a `DashboardPage` component with a responsive layout, including sections for user profile, apartment management, and API demo. - Added styles for the dashboard page to enhance user experience. - Implemented a settings page with theme preferences and session management features. - Developed a sign-in page with branding and user-friendly design. - Established a workspace navigation component for easy access to different sections of the application. - Implemented an `ApartmentService` for managing apartment data with corresponding unit tests. - Created a `ConfigService` for loading application configuration. - Added a `ThemeService` to manage user theme preferences dynamically.
This commit is contained in:
1 parent
3e552cad90
commit
26ab037084
37 files changed
+1877
-1650
No files matched your search
+30
-446
@@ -1,152 +1,15 @@
|
||||
@if (!isLoggedIn) {
|
||||
<main class="sign-in-shell">
|
||||
<section class="sign-in-panel">
|
||||
<div class="brand-lockup">
|
||||
<app-brand-mark class="brand-mark" />
|
||||
<div>
|
||||
<h1>LMNP Organizer</h1>
|
||||
<p>Gestion locative & fiscalité</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="sign-in-copy">
|
||||
<span class="context-label">Espace bailleur</span>
|
||||
<h2>Votre gestion locative, enfin lisible.</h2>
|
||||
<p>
|
||||
Pilotez vos appartements et gardez vos démarches LMNP au même endroit, sur mobile comme
|
||||
sur ordinateur.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button mat-flat-button class="primary-action sign-in-action" (click)="login()">
|
||||
Se connecter avec OIDC
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M5 12h14M13 6l6 6-6 6" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<p class="security-note">
|
||||
<span aria-hidden="true">●</span>
|
||||
Connexion sécurisée via OpenID Connect
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<aside class="sign-in-story" aria-label="Aperçu du produit">
|
||||
<div class="story-card">
|
||||
<p class="story-kicker">Pensé pour aller à l'essentiel</p>
|
||||
<h2>Un espace calme pour vos décisions de bailleur.</h2>
|
||||
<div class="story-list">
|
||||
<div>
|
||||
<span>01</span>
|
||||
<p>Vos biens accessibles en quelques gestes.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>02</span>
|
||||
<p>Une interface adaptée à chaque écran.</p>
|
||||
</div>
|
||||
<div>
|
||||
<span>03</span>
|
||||
<p>Vos outils LMNP réunis progressivement.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</main>
|
||||
<app-sign-in-page (loginRequested)="login()" />
|
||||
} @else {
|
||||
<div class="workspace-shell">
|
||||
<aside class="app-navigation" data-testid="primary-navigation">
|
||||
<div class="sidebar-brand">
|
||||
<app-brand-mark class="brand-mark" />
|
||||
<div>
|
||||
<strong>LMNP</strong>
|
||||
<span>Organizer</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="navigation-items" aria-label="Navigation principale">
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="navigation-item"
|
||||
[class.active]="activeTab() === 'dashboard'"
|
||||
[attr.aria-current]="activeTab() === 'dashboard' ? 'page' : null"
|
||||
(click)="activeTab.set('dashboard')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<rect x="3" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="3" width="7" height="7" rx="2" />
|
||||
<rect x="3" y="14" width="7" height="7" rx="2" />
|
||||
<rect x="14" y="14" width="7" height="7" rx="2" />
|
||||
</svg>
|
||||
<span>Tableau de bord</span>
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="navigation-item"
|
||||
[class.active]="activeTab() === 'apartments'"
|
||||
[attr.aria-current]="activeTab() === 'apartments' ? 'page' : null"
|
||||
(click)="activeTab.set('apartments')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 21V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16" />
|
||||
<path d="M2 21h20M8 7h2M14 7h2M8 11h2M14 11h2M8 15h2M14 15h2" />
|
||||
</svg>
|
||||
<span>Appartements</span>
|
||||
</button>
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="navigation-item"
|
||||
[class.active]="activeTab() === 'settings'"
|
||||
[attr.aria-current]="activeTab() === 'settings' ? 'page' : null"
|
||||
(click)="activeTab.set('settings')"
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path
|
||||
d="M19.4 15a1.7 1.7 0 0 0 .34 1.88l.06.06-2.83 2.83-.06-.06A1.7 1.7 0 0 0 15 19.4a1.7 1.7 0 0 0-1 .6 1.7 1.7 0 0 0-.4 1.1V21h-4v-.09A1.7 1.7 0 0 0 8.6 19.4a1.7 1.7 0 0 0-1.88.34l-.06.06-2.83-2.83.06-.06A1.7 1.7 0 0 0 4.6 15a1.7 1.7 0 0 0-.6-1 1.7 1.7 0 0 0-1.1-.4H3v-4h.09A1.7 1.7 0 0 0 4.6 8.6a1.7 1.7 0 0 0-.34-1.88l-.06-.06 2.83-2.83.06.06A1.7 1.7 0 0 0 9 4.6a1.7 1.7 0 0 0 1-.6 1.7 1.7 0 0 0 .4-1.1V3h4v.09A1.7 1.7 0 0 0 15.4 4.6a1.7 1.7 0 0 0 1.88-.34l.06-.06 2.83 2.83-.06.06A1.7 1.7 0 0 0 19.4 9c.16.38.37.72.6 1 .3.3.7.45 1.1.45h.09v4h-.09c-.4 0-.8.15-1.1.45-.23.28-.44.62-.6 1Z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Réglages</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-user">
|
||||
<div class="compact-avatar">{{ userInitial() }}</div>
|
||||
<div>
|
||||
<strong>{{ username }}</strong>
|
||||
<span>{{ email }}</span>
|
||||
</div>
|
||||
<button mat-button type="button" class="logout-action" (click)="logout()">
|
||||
Déconnexion
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
<app-workspace-navigation
|
||||
[activeTab]="activeTab()"
|
||||
[username]="username"
|
||||
[email]="email"
|
||||
[userInitial]="userInitial()"
|
||||
(tabSelected)="activeTab.set($event)"
|
||||
(logoutRequested)="logout()"
|
||||
/>
|
||||
|
||||
<main class="workspace-main">
|
||||
<header class="mobile-header">
|
||||
@@ -159,310 +22,31 @@
|
||||
|
||||
<div class="page-container">
|
||||
@if (activeTab() === 'dashboard') {
|
||||
<section class="page-section dashboard-page">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Bonjour {{ username }}</p>
|
||||
<h1>Tableau de bord</h1>
|
||||
<p>Retrouvez rapidement les actions utiles à votre gestion locative.</p>
|
||||
</div>
|
||||
<span class="status-pill"><span></span>Espace sécurisé</span>
|
||||
</header>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<article class="feature-card feature-card-primary">
|
||||
<div class="feature-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M4 21V5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v16M2 21h20" />
|
||||
<path d="M8 8h8M8 12h8M8 16h4" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="eyebrow">Votre portefeuille</p>
|
||||
<h2>
|
||||
{{ apartments().length }} appartement{{ apartments().length > 1 ? 's' : '' }}
|
||||
</h2>
|
||||
<p>Centralisez les informations essentielles de vos biens.</p>
|
||||
</div>
|
||||
<button mat-button type="button" (click)="activeTab.set('apartments')">
|
||||
Gérer mes appartements
|
||||
</button>
|
||||
</article>
|
||||
|
||||
<article class="feature-card user-profile">
|
||||
<div class="large-avatar">{{ userInitial() }}</div>
|
||||
<div class="user-details">
|
||||
<p class="eyebrow">Compte actif</p>
|
||||
<h3>{{ username }}</h3>
|
||||
<p class="user-email">{{ email }}</p>
|
||||
<span class="role-chip">Bailleur connecté</span>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="feature-card api-demo-section">
|
||||
<div>
|
||||
<p class="eyebrow">État du service</p>
|
||||
<h2>Connexion au serveur</h2>
|
||||
<p>Vérifiez que votre espace communique correctement avec le service sécurisé.</p>
|
||||
</div>
|
||||
<div class="api-actions">
|
||||
<button mat-stroked-button type="button" (click)="callHelloApi()">
|
||||
Tester la connexion
|
||||
</button>
|
||||
</div>
|
||||
@if (helloMessage()) {
|
||||
<div class="api-response-wrapper">
|
||||
<span>Réponse du serveur</span>
|
||||
<code class="api-response-code">{{ helloMessage() }}</code>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
<app-dashboard-page
|
||||
[username]="username"
|
||||
[email]="email"
|
||||
[userInitial]="userInitial()"
|
||||
[apartmentCount]="apartments().length"
|
||||
[helloMessage]="helloMessage()"
|
||||
(apartmentsRequested)="activeTab.set('apartments')"
|
||||
(connectionCheckRequested)="callHelloApi()"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (activeTab() === 'apartments') {
|
||||
<section class="page-section apartments-page">
|
||||
@if (!showForm()) {
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Portefeuille</p>
|
||||
<h1>Mes appartements</h1>
|
||||
<p>Les biens rattachés à votre activité LMNP.</p>
|
||||
</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
type="button"
|
||||
class="primary-action"
|
||||
(click)="openAddForm()"
|
||||
>
|
||||
<span class="button-plus" aria-hidden="true">+</span>
|
||||
Ajouter un appartement
|
||||
</button>
|
||||
</header>
|
||||
|
||||
@if (apartments().length === 0) {
|
||||
<article class="empty-state">
|
||||
<div class="empty-illustration" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
|
||||
<path d="m4 10 8-6 8 6v10H4z" />
|
||||
<path d="M9 20v-6h6v6" />
|
||||
</svg>
|
||||
</div>
|
||||
<p class="eyebrow">Commencez ici</p>
|
||||
<h2>Aucun appartement enregistré</h2>
|
||||
<p>Ajoutez votre premier bien pour construire votre portefeuille.</p>
|
||||
<button mat-stroked-button type="button" (click)="openAddForm()">
|
||||
Ajouter un appartement
|
||||
</button>
|
||||
</article>
|
||||
} @else {
|
||||
<div class="apartments-grid">
|
||||
@for (apt of apartments(); track apt.id) {
|
||||
<article class="apartment-card">
|
||||
<div class="apartment-card-topline">
|
||||
<span class="property-label">Bien immobilier</span>
|
||||
<span class="property-value"
|
||||
>{{ apt.purchasePrice.toLocaleString('fr-FR') }} €</span
|
||||
>
|
||||
</div>
|
||||
<div class="apt-info">
|
||||
<h2>{{ apt.name }}</h2>
|
||||
<p class="apt-address">
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.8"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M20 10c0 5-8 11-8 11S4 15 4 10a8 8 0 1 1 16 0Z" />
|
||||
<circle cx="12" cy="10" r="2.5" />
|
||||
</svg>
|
||||
{{ apt.address }}
|
||||
</p>
|
||||
@if (apt.description) {
|
||||
<p class="apt-desc">{{ apt.description }}</p>
|
||||
}
|
||||
</div>
|
||||
<div class="apt-actions">
|
||||
<button mat-button type="button" (click)="openEditForm(apt)">
|
||||
Modifier
|
||||
</button>
|
||||
@if (apt.id) {
|
||||
<button
|
||||
mat-button
|
||||
type="button"
|
||||
class="danger-action"
|
||||
(click)="deleteApartment(apt.id)"
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="form-page">
|
||||
<header class="page-heading form-heading">
|
||||
<div>
|
||||
<button mat-button type="button" class="back-action" (click)="closeForm()">
|
||||
← Retour
|
||||
</button>
|
||||
<p class="eyebrow">Portefeuille</p>
|
||||
<h1>
|
||||
{{ editingApartment() ? "Modifier l'appartement" : 'Ajouter un appartement' }}
|
||||
</h1>
|
||||
<p>Renseignez les informations principales du bien.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<form (submit)="saveApartment(); $event.preventDefault()" class="apartment-form">
|
||||
@if (formError) {
|
||||
<div class="alert-error" role="alert">{{ formError }}</div>
|
||||
}
|
||||
|
||||
<div class="form-grid">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Nom de l'appartement</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
name="name"
|
||||
[(ngModel)]="formName"
|
||||
placeholder="T2 Paris 15"
|
||||
required
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>Prix d'acquisition</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="number"
|
||||
name="purchasePrice"
|
||||
[(ngModel)]="formPurchasePrice"
|
||||
min="1"
|
||||
required
|
||||
/>
|
||||
<span matTextSuffix>€</span>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-field">
|
||||
<mat-label>Adresse complète</mat-label>
|
||||
<input
|
||||
matInput
|
||||
type="text"
|
||||
name="address"
|
||||
[(ngModel)]="formAddress"
|
||||
placeholder="45 rue des Entrepreneurs, 75015 Paris"
|
||||
required
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="outline" class="full-field">
|
||||
<mat-label>Description ou notes</mat-label>
|
||||
<textarea
|
||||
matInput
|
||||
name="description"
|
||||
[(ngModel)]="formDescription"
|
||||
rows="4"
|
||||
placeholder="Étage, agencement, équipements…"
|
||||
></textarea>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="form-actions">
|
||||
<button mat-button type="button" (click)="closeForm()">Annuler</button>
|
||||
<button mat-flat-button type="submit" class="primary-action">
|
||||
Enregistrer
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
<app-apartments-page
|
||||
[apartments]="apartments()"
|
||||
(deleteRequested)="deleteApartment($event)"
|
||||
(apartmentSaved)="saveApartment($event)"
|
||||
/>
|
||||
}
|
||||
|
||||
@if (activeTab() === 'settings') {
|
||||
<section class="page-section settings-page">
|
||||
<header class="page-heading">
|
||||
<div>
|
||||
<p class="eyebrow">Préférences</p>
|
||||
<h1>Réglages</h1>
|
||||
<p>Adaptez LMNP Organizer à votre façon de travailler.</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<article class="settings-card" aria-labelledby="appearance-heading">
|
||||
<div class="settings-copy">
|
||||
<span class="settings-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<path d="M12 3a9 9 0 1 0 9 9c0-.5-.04-1-.12-1.47A7 7 0 0 1 12 3Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<p class="eyebrow">Interface</p>
|
||||
<h2 id="appearance-heading">Apparence</h2>
|
||||
<p>Le réglage est mémorisé uniquement sur cet appareil.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-radio-group
|
||||
class="theme-options"
|
||||
aria-label="Thème de l'application"
|
||||
[value]="themePreference()"
|
||||
(change)="themeService.setPreference($event.value)"
|
||||
>
|
||||
<mat-radio-button value="light">
|
||||
<span class="theme-option-copy"
|
||||
><strong>Clair</strong><small>Une interface lumineuse et douce.</small></span
|
||||
>
|
||||
</mat-radio-button>
|
||||
<mat-radio-button value="dark">
|
||||
<span class="theme-option-copy"
|
||||
><strong>Sombre</strong
|
||||
><small>Un contraste réduit en faible lumière.</small></span
|
||||
>
|
||||
</mat-radio-button>
|
||||
<mat-radio-button value="system">
|
||||
<span class="theme-option-copy"
|
||||
><strong>Système</strong
|
||||
><small>Suit automatiquement votre appareil.</small></span
|
||||
>
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
</article>
|
||||
|
||||
<article
|
||||
class="settings-card session-settings"
|
||||
data-testid="session-settings"
|
||||
aria-labelledby="session-heading"
|
||||
>
|
||||
<div class="settings-copy">
|
||||
<span class="settings-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
||||
<circle cx="12" cy="8" r="4" />
|
||||
<path d="M4 21a8 8 0 0 1 16 0" />
|
||||
</svg>
|
||||
</span>
|
||||
<div>
|
||||
<p class="eyebrow">Compte</p>
|
||||
<h2 id="session-heading">Session</h2>
|
||||
<p>Gérez la session ouverte sur cet appareil.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="session-action">
|
||||
<div>
|
||||
<strong>{{ username }}</strong>
|
||||
<span>{{ email }}</span>
|
||||
</div>
|
||||
<button mat-stroked-button type="button" (click)="logout()">Déconnexion</button>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
<app-settings-page
|
||||
[username]="username"
|
||||
[email]="email"
|
||||
[themePreference]="themePreference()"
|
||||
(themePreferenceChanged)="themeService.setPreference($event)"
|
||||
(logoutRequested)="logout()"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
Reference in new issue
Block a user