Skip to content

Commit af8ac65

Browse files
committed
chore: layout improvements
Co-Authored-by: Bertrand Zuchuat <bertrand.zuchuat@rero.ch>
1 parent 0b9cf95 commit af8ac65

File tree

20 files changed

+572
-190
lines changed

20 files changed

+572
-190
lines changed

public/images/logo.svg

Lines changed: 10 additions & 0 deletions
Loading

src/app/app.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,21 @@
1-
import { Component } from '@angular/core';
1+
import { Component, inject } from '@angular/core';
22
import { RouterOutlet } from '@angular/router';
33
import { Toast } from 'primeng/toast';
4-
import { Menu } from './features/menu/menu';
4+
import { Sidebar } from './features/sidebar/sidebar';
55
import { AppStateStore } from './shared/appSate/app-state-store';
6+
import { LayoutService } from './shared/layout/layout.service';
67

78
@Component({
89
selector: 'app-root',
9-
imports: [RouterOutlet, Menu, Toast],
10+
imports: [RouterOutlet, Sidebar, Toast],
1011
providers: [AppStateStore],
1112
template: `
12-
<div class="m-4">
13-
<app-menu />
14-
<div class="mt-4 mx-4 flex justify-center">
15-
<div class="w-[80%]">
13+
<div class="min-h-screen bg-gray-50">
14+
<app-sidebar />
15+
<div class="transition-all duration-300 p-4"
16+
[class.ml-64]="layoutService.sidebarExpanded()"
17+
[class.ml-16]="!layoutService.sidebarExpanded()">
18+
<div class="max-w-7xl mx-auto">
1619
<router-outlet />
1720
</div>
1821
<p-toast [showTransitionOptions]="'250ms'" [showTransformOptions]="'translateX(100%)'" [hideTransitionOptions]="'150ms'" [hideTransformOptions]="'translateX(100%)'" />
@@ -21,4 +24,5 @@ import { AppStateStore } from './shared/appSate/app-state-store';
2124
`
2225
})
2326
export class App {
27+
protected readonly layoutService = inject(LayoutService);
2428
}

src/app/features/books/book.ts

Lines changed: 99 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,111 @@
1-
import { Component, input } from '@angular/core';
1+
import { Component, computed, input } from '@angular/core';
22
import { OpenLibraryRecord } from './model/open-library.model';
33

44
@Component({
55
selector: 'app-book',
66
imports: [],
77
template: `
8-
<div class="flex flex-col">
9-
<h1>{{ book().title }}</h1>
10-
@if (book().author_name) {
11-
<div><span class="font-bold">Auteurs:</span> {{ book().author_name.join(', ') }}</div>
12-
}
13-
@if (book().language) {
14-
<div><span class="font-bold">Langues:</span> {{ book().language.join(', ') }}</div>
15-
}
8+
<div class="bg-white rounded-xl shadow-md border border-gray-100 overflow-hidden hover:shadow-xl hover:border-[#1EA1CF]/30 transition-all duration-300 group h-full flex flex-col">
9+
<!-- Book Cover -->
10+
<div class="relative bg-gradient-to-br from-gray-100 to-gray-200 h-64 flex items-center justify-center overflow-hidden">
11+
@if (coverUrl()) {
12+
<img
13+
[src]="coverUrl()"
14+
[alt]="book().title"
15+
class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
16+
(error)="onImageError($event)" />
17+
} @else {
18+
<div class="text-center p-6">
19+
<i class="pi pi-book text-6xl text-gray-400 mb-3"></i>
20+
<p class="text-sm text-gray-500">Couverture non disponible</p>
21+
</div>
22+
}
23+
<!-- Overlay badge for publication year -->
24+
@if (book().first_publish_year) {
25+
<div class="absolute top-3 right-3 bg-[#1765A2] text-white px-3 py-1 rounded-full text-sm font-medium shadow-lg">
26+
{{ book().first_publish_year }}
27+
</div>
28+
}
29+
</div>
30+
31+
<!-- Book Info -->
32+
<div class="p-5 flex-1 flex flex-col">
33+
<!-- Title -->
34+
<h3 class="text-lg font-bold text-gray-800 mb-2 line-clamp-2 group-hover:text-[#1765A2] transition-colors">
35+
{{ book().title }}
36+
</h3>
37+
38+
<!-- Authors -->
39+
@if (book().author_name && book().author_name.length > 0) {
40+
<div class="flex items-start gap-2 mb-3">
41+
<i class="pi pi-user text-[#1EA1CF] text-sm mt-1 flex-shrink-0"></i>
42+
<p class="text-sm text-gray-600 line-clamp-2">
43+
{{ book().author_name.join(', ') }}
44+
</p>
45+
</div>
46+
}
47+
48+
<!-- Metadata -->
49+
<div class="mt-auto space-y-2">
50+
<!-- Languages -->
51+
@if (book().language && book().language.length > 0) {
52+
<div class="flex items-center gap-2">
53+
<i class="pi pi-globe text-gray-400 text-xs"></i>
54+
<span class="text-xs text-gray-500">
55+
{{ book().language.slice(0, 3).join(', ') }}
56+
@if (book().language.length > 3) {
57+
<span class="text-gray-400">+{{ book().language.length - 3 }}</span>
58+
}
59+
</span>
60+
</div>
61+
}
62+
63+
<!-- Publisher -->
64+
@if (book().publisher?.length) {
65+
<div class="flex items-center gap-2">
66+
<i class="pi pi-building text-gray-400 text-xs"></i>
67+
<span class="text-xs text-gray-500 truncate">
68+
{{ book().publisher?.[0] }}
69+
</span>
70+
</div>
71+
}
72+
73+
<!-- Edition count -->
74+
@if (book().edition_count && book().edition_count! > 1) {
75+
<div class="flex items-center gap-2">
76+
<i class="pi pi-copy text-gray-400 text-xs"></i>
77+
<span class="text-xs text-gray-500">
78+
{{ book().edition_count }} édition{{ book().edition_count! > 1 ? 's' : '' }}
79+
</span>
80+
</div>
81+
}
82+
</div>
83+
</div>
1684
</div>
1785
`
1886
})
1987
export class Book {
2088
book = input.required<OpenLibraryRecord>();
89+
90+
// Compute cover URL from ISBN or cover_i
91+
coverUrl = computed(() => {
92+
const bookData = this.book();
93+
94+
// Try to get cover from cover_i (cover edition id)
95+
if (bookData.cover_i) {
96+
return `https://covers.openlibrary.org/b/id/${bookData.cover_i}-L.jpg`;
97+
}
98+
99+
// Try to get cover from ISBN
100+
if (bookData.isbn && bookData.isbn.length > 0) {
101+
return `https://covers.openlibrary.org/b/isbn/${bookData.isbn[0]}-L.jpg`;
102+
}
103+
104+
return null;
105+
});
106+
107+
onImageError(event: Event) {
108+
// Hide broken image
109+
(event.target as HTMLImageElement).style.display = 'none';
110+
}
21111
}

src/app/features/books/books.html

Lines changed: 43 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,50 @@
1-
<div class="flex flex-col gap-4">
2-
<h1>Livres</h1>
1+
<div class="max-w-7xl mx-auto p-6">
2+
<!-- Header -->
3+
<div class="mb-8">
4+
<h1 class="text-3xl font-bold text-gray-800 mb-2">Bibliothèque</h1>
5+
<p class="text-gray-500">Recherchez et découvrez des livres dans la collection Open Library</p>
6+
</div>
37

4-
<app-search />
8+
<!-- Search Section -->
9+
<div class="mb-6">
10+
<app-search />
11+
</div>
512

13+
<!-- Results Section -->
614
@if(store.total() > 0) {
7-
<div class="font-sm italic">Nombre de résultats: {{ store.total() }}</div>
8-
<div class="mt-4">
9-
@for(document of store.documents(); track document.key; let last = $last) {
10-
<app-book [book]="document" />
11-
@if (!last) {
12-
<p-divider />
13-
}
14-
}
15-
</div>
16-
<div class="mt-4">
17-
<shared-paginator [paginatorState]="paginatorConfig()" (pageChange)="store.setPaginator($event)" />
15+
<!-- Results Header -->
16+
<div class="mb-6 flex items-center justify-between">
17+
<div class="flex items-center gap-2 text-sm text-gray-600">
18+
<i class="pi pi-book"></i>
19+
<span class="font-medium">{{ store.total() }} résultat{{ store.total() > 1 ? 's' : '' }} trouvé{{ store.total() >
20+
1 ? 's' : '' }}</span>
1821
</div>
22+
</div>
23+
24+
<!-- Books Grid -->
25+
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 mb-8">
26+
@for(document of store.documents(); track document.key) {
27+
<app-book [book]="document" />
28+
}
29+
</div>
30+
31+
<!-- Pagination -->
32+
<div class="flex justify-center">
33+
<shared-paginator [paginatorState]="paginatorConfig()" (pageChange)="store.setPaginator($event)" />
34+
</div>
1935
} @else {
20-
<div class="font-sm italic">Aucun résultat…</div>
36+
<!-- Empty State -->
37+
<div class="text-center py-16">
38+
<div
39+
class="inline-flex items-center justify-center w-20 h-20 rounded-full bg-gradient-to-br from-[#1765A2]/10 to-[#1EA1CF]/10 mb-6">
40+
<i class="pi pi-search text-4xl text-[#1765A2]"></i>
41+
</div>
42+
<h3 class="text-xl font-semibold text-gray-700 mb-2">Aucun résultat trouvé</h3>
43+
<p class="text-gray-500 mb-6">Essayez une autre recherche ou modifiez vos critères</p>
44+
<div class="text-sm text-gray-400">
45+
<i class="pi pi-info-circle mr-1"></i>
46+
Utilisez la barre de recherche ci-dessus pour explorer la bibliothèque
47+
</div>
48+
</div>
2149
}
2250
</div>

src/app/features/books/books.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
11
import { Component, inject, Signal } from '@angular/core';
22
import { deepComputed } from '@ngrx/signals';
3-
import { Divider } from 'primeng/divider';
43
import { PaginatorComponent, PaginatorConfig } from '../../shared/component/paginator';
54
import { Book } from './book';
65
import { Search } from './search';
76
import { OpenLibraryStore } from './store/open-library.store';
87

98
@Component({
109
selector: 'app-books',
11-
imports: [Search, Book, Divider, PaginatorComponent],
10+
imports: [Search, Book, PaginatorComponent],
1211
templateUrl: './books.html',
1312
providers: [OpenLibraryStore]
1413
})

src/app/features/books/model/open-library.model.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ export type OpenLibraryRecord = {
1111
language: string[];
1212
key: string;
1313
title: string;
14+
cover_i?: number;
15+
isbn?: string[];
16+
first_publish_year?: number;
17+
publisher?: string[];
18+
edition_count?: number;
1419
}
1520

1621
export const availableFields = [
@@ -19,4 +24,9 @@ export const availableFields = [
1924
'language',
2025
'key',
2126
'title',
27+
'cover_i',
28+
'isbn',
29+
'first_publish_year',
30+
'publisher',
31+
'edition_count',
2232
];

src/app/features/books/search.ts

Lines changed: 38 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,48 @@
11
import { Component, inject } from '@angular/core';
2-
import { Button } from 'primeng/button';
3-
import { InputGroup } from 'primeng/inputgroup';
4-
import { InputGroupAddon } from 'primeng/inputgroupaddon';
52
import { InputText } from "primeng/inputtext";
63
import { OpenLibraryStore } from './store/open-library.store';
74

85
@Component({
96
selector: 'app-search',
10-
imports: [InputText, InputGroup, InputGroupAddon, Button],
7+
imports: [InputText],
118
template: `
12-
<p-inputgroup>
13-
<input
14-
fluid="true"
15-
#input
16-
type="text"
17-
pInputText
18-
placeholder="Rechercher…"
19-
[value]="store.filter()"
20-
(keyup.enter)="store.search(input.value)"
21-
/>
22-
<p-inputgroup-addon>
23-
<p-button icon="pi pi-times" severity="secondary" (onClick)="store.reset()" />
24-
</p-inputgroup-addon>
25-
<p-inputgroup-addon>
26-
<i
27-
class="pi"
28-
[class]="store.isPending() ? 'pi-spin pi-spinner text-blue-500' : 'pi-search'"
29-
></i>
30-
</p-inputgroup-addon>
31-
</p-inputgroup>
9+
<div class="bg-white rounded-xl shadow-md border border-gray-100 p-6">
10+
<div class="flex gap-3">
11+
<div class="flex-1 relative">
12+
<input
13+
#input
14+
type="text"
15+
pInputText
16+
placeholder="Rechercher un livre, un auteur..."
17+
[value]="store.filter()"
18+
(keyup.enter)="store.search(input.value)"
19+
class="w-full pl-4 pr-4 py-3 border-2 border-gray-200 rounded-lg focus:border-[#1EA1CF] focus:ring-2 focus:ring-[#1EA1CF]/20 transition-all"
20+
/>
21+
</div>
22+
23+
@if (store.filter()) {
24+
<button
25+
(click)="store.reset()"
26+
class="px-4 py-3 bg-gray-100 hover:bg-gray-200 text-gray-600 rounded-lg transition-all flex items-center gap-2">
27+
<i class="pi pi-times"></i>
28+
<span class="hidden sm:inline">Effacer</span>
29+
</button>
30+
}
31+
32+
<button
33+
(click)="store.search(input.value)"
34+
[disabled]="store.isPending()"
35+
class="px-6 py-3 bg-[#1765A2] hover:bg-[#1765A2]/90 disabled:bg-gray-300 text-white rounded-lg font-medium transition-all hover:shadow-md flex items-center gap-2">
36+
@if (store.isPending()) {
37+
<i class="pi pi-spin pi-spinner"></i>
38+
<span class="hidden sm:inline">Recherche...</span>
39+
} @else {
40+
<i class="pi pi-search"></i>
41+
<span class="hidden sm:inline">Rechercher</span>
42+
}
43+
</button>
44+
</div>
45+
</div>
3246
`,
3347
})
3448
export class Search {

src/app/features/connected-user/connected-user.html

Lines changed: 0 additions & 8 deletions
This file was deleted.

src/app/features/connected-user/connected-user.ts

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/app/features/home/home.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<div class="flex flex-col gap-6">
2-
<h1 class="flex justify-center">Signal Store</h1>
2+
<h1 class="flex justify-center">Angular POC</h1>
33

4-
<p class="flex justify-center">Projet de démonstration pour tester Angular POC.</p>
4+
<p class="flex justify-center">Projet de démonstration pour tester les bonnes pratiques et les implémentations.</p>
55
</div>

0 commit comments

Comments
 (0)