Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apiGateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ spring:
oauth2:
resourceserver:
jwt:
issuer-uri: http://keycloak:8080/realms/e-library
issuer-uri: http://localhost:8181/realms/e-library
jwk-set-uri: http://localhost:8181/realms/e-library/protocol/openid-connect/certs

services:
Expand Down
29 changes: 29 additions & 0 deletions book-bazaar/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions book-bazaar/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"@tailwindcss/postcss": "^4.1.16",
"keycloak-angular": "^20.0.0",
"keycloak-js": "^26.2.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
Expand Down
Binary file added book-bazaar/public/hero-books-illustration.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions book-bazaar/public/silent-check-sso.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<!doctype html>
<html>
<body>
<script>
parent.postMessage(location.href, location.origin);
</script>
</body>
</html>
41 changes: 37 additions & 4 deletions book-bazaar/src/app/app.config.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,47 @@
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import {provideRouter, withViewTransitions} from '@angular/router';
import { ApplicationConfig, inject, provideAppInitializer, provideBrowserGlobalErrorListeners, provideZonelessChangeDetection } from '@angular/core';
import { provideRouter, withViewTransitions } from '@angular/router';

import { routes } from './app.routes';
import { provideHttpClient } from '@angular/common/http';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import {
provideKeycloak,
INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
includeBearerTokenInterceptor,
createInterceptorCondition
} from 'keycloak-angular';

export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes, withViewTransitions()),
provideHttpClient(),

provideHttpClient(withInterceptors([includeBearerTokenInterceptor])),

provideKeycloak({
config: {
url: 'http://localhost:8181',
realm: 'e-library',
clientId: 'e-library-client'
},
initOptions: {
// 'check-sso': перевіряє сесію тихо. Якщо юзер залогінений - пускає, ні - лишає анонімом.
// 'login-required': відразу кидає на сторінку логіну (вам це НЕ підходить).
onLoad: 'check-sso',
silentCheckSsoRedirectUri: window.location.origin + '/silent-check-sso.html',
checkLoginIframe: false // Вимикаємо, щоб уникнути проблем з cookies у сучасних браузерах
},
// Автоматично додавати токен (Bearer) до запитів
}),
{
provide: INCLUDE_BEARER_TOKEN_INTERCEPTOR_CONFIG,
useValue: [
createInterceptorCondition({
urlPattern: /^(http:\/\/localhost:9000|http:\/\/localhost:8080)(\/.*)?$/i, // Регулярка для ваших сервісів
bearerPrefix: 'Bearer'
}),
// Можна додати інші умови, якщо є інші бекенди
]
}
]
};
44 changes: 26 additions & 18 deletions book-bazaar/src/app/components/book-card/book-card.html
Original file line number Diff line number Diff line change
@@ -1,43 +1,51 @@
<a [routerLink]="['/book-details', book().id]" class="block h-full no-underline text-inherit">
<a [routerLink]="['/book-details', book().id]" class="block h-full no-underline text-inherit group">
<div class="h-full">
<mat-card appearance="outlined" class="hover-shadow h-full flex flex-col w-72">
<mat-card appearance="outlined" class="h-full flex flex-col w-full border-gray-200 bg-white transition-all duration-300 group-hover:shadow-lg group-hover:border-primary/20 group-hover:-translate-y-1">

<div class="p-6 flex justify-center items-center bg-gray-50 rounded-t-lg border-b border-gray-100">
<img class="book-cover-image"
<div class="p-6 flex justify-center items-center bg-gray-50 rounded-t-lg border-b border-gray-100 aspect-[3/4] relative overflow-hidden">
<img class="book-cover-image transition-transform duration-500 group-hover:scale-105"
[src]="book().imageUrl || 'assets/default-book-cover.jpg'"
[alt]="book().title + ' cover'" />
</div>

<div class="flex flex-col flex-1 p-4">
<mat-card-title-group class="mb-2">
<div class="flex flex-col flex-1 p-5">
<mat-card-title-group class="mb-3 block">
<mat-card-title
class="title mb-1"
class="title mb-2 text-lg font-bold leading-tight text-gray-900 line-clamp-1"
[matTooltip]="book().title || ''"
matTooltipShowDelay="500">
{{ book().title }}
</mat-card-title>
<mat-card-subtitle class="subtitle">
<mat-card-subtitle class="subtitle text-sm font-medium text-gray-500">
{{ book().author?.name }}
</mat-card-subtitle>
</mat-card-title-group>

<div class="mt-auto pt-2">
<mat-chip-set>
<div class="mt-auto pt-3 border-t border-dashed border-gray-100 flex flex-col gap-3">

@if(book().price) {
<div class="text-primary font-bold text-lg">
{{ book().price | currency }}
</div>
}

<div class="flex flex-wrap gap-2">
@if(book().category) {
<mat-chip
class="category-chip"
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-50 text-purple-700 cursor-pointer hover:bg-purple-100 transition-colors border border-purple-100"
(click)="searchByCategory($event, book().category!)">
{{ book().category?.name }}
</mat-chip>
</span>
}
@for (genre of book().bookGenres; track genre.id) {
<mat-chip
class="genre-chip"

@for (genre of book().bookGenres?.slice(0, 2); track genre.id) {
<span
class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-50 text-blue-700 cursor-pointer hover:bg-blue-100 transition-colors border border-blue-100"
(click)="searchByGenre($event, genre)">
{{ genre.name }}
</mat-chip>
</span>
}
</mat-chip-set>
</div>
</div>
</div>
</mat-card>
Expand Down
3 changes: 2 additions & 1 deletion book-bazaar/src/app/components/book-card/book-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ import { MatChipSet, MatChip } from "@angular/material/chips";
import { MatTooltipModule } from '@angular/material/tooltip';
import { Category } from '../../model/category';
import { Genre } from '../../model/genre';
import { CurrencyPipe } from '@angular/common';

@Component({
selector: 'app-book-card',
imports: [MatCard, RouterModule, MatCardTitleGroup, MatCardTitle, MatCardSubtitle, MatChipSet, MatChip, MatTooltipModule],
imports: [MatCard, RouterModule, MatCardTitleGroup, MatCardTitle, MatCardSubtitle, MatChipSet, MatChip, MatTooltipModule, CurrencyPipe],
templateUrl: './book-card.html',
styleUrl: './book-card.css',
})
Expand Down
10 changes: 10 additions & 0 deletions book-bazaar/src/app/components/filter-panel/filter-panel.css
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,14 @@
.custom-scrollbar {
scrollbar-width: thin;
scrollbar-color: #e5e7eb transparent;
}

/* Зменшуємо відступи у чекбоксів, щоб вони виглядали компактніше */
::ng-deep .mat-mdc-checkbox .mdc-form-field {
padding-right: 0 !important;
}

::ng-deep .mat-mdc-checkbox-touch-target {
width: 32px !important;
height: 32px !important;
}
79 changes: 53 additions & 26 deletions book-bazaar/src/app/components/filter-panel/filter-panel.html
Original file line number Diff line number Diff line change
@@ -1,33 +1,60 @@
<div class="flex flex-col gap-6 text-book-gray w-full">
<div class="flex flex-col w-full bg-white rounded-xl border border-gray-100 p-5 shadow-sm">

<div class="flex justify-between items-center mb-2"> <h3 class="text-2xl text-book-dark-gray font-bold font-serif">Filters</h3>
<button matButton color="primary" (click)="clearFilters()">Clear all</button>
<div class="flex justify-between items-center mb-6 pb-4 border-b border-gray-100">
<h3 class="text-lg font-bold text-gray-900 flex items-center gap-2">
<mat-icon class="text-primary scale-90">tune</mat-icon>
Filters
</h3>
@if (hasSelectedFilters()) {
<button
mat-button
color="warn"
class="!min-w-0 !px-2 !h-8 text-xs font-medium uppercase tracking-wide opacity-80 hover:opacity-100"
(click)="clearFilters()">
Clear all
</button>
}
</div>

@for (filter of filters(); track filter.name) {
<div class="flex flex-col border-b border-gray-100 pb-4 last:border-0"> <h3 class="text-lg font-medium mb-3 text-dark">{{ filter.label }}</h3>
<div class="flex flex-col gap-8">
@for (filter of filters(); track filter.name) {
<div class="flex flex-col group">

<h3 class="text-sm font-bold text-gray-900 uppercase tracking-wider mb-3 flex justify-between items-center">
{{ filter.label }}
<span class="text-xs text-gray-400 font-normal bg-gray-50 px-2 py-0.5 rounded-full border border-gray-100">
{{ filter.options.length }}
</span>
</h3>

<div class="flex flex-col gap-1.5">
@for (option of visibleOptions(filter); track option) {
<label class="flex items-center gap-3 cursor-pointer p-1.5 -ml-1.5 rounded-lg transition-colors hover:bg-gray-50 group/item">

<mat-checkbox
color="primary"
class="custom-checkbox scale-90"
[checked]="isSelected(filter.name, option)"
(change)="toggleOption(filter.name, option)"
(click)="$event.stopPropagation()">
</mat-checkbox>

<span class="text-sm text-gray-600 group-hover/item:text-primary transition-colors select-none">
{{ option }}
</span>
</label>
}
</div>

<div class="flex flex-col gap-2">
@for (option of visibleOptions(filter); track option) {
<label class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors">
<mat-checkbox
color="primary"
[checked]="isSelected(filter.name, option)"
(change)="toggleOption(filter.name, option)"
>
<span class="text-base">{{ option }}</span>
</mat-checkbox>
</label>
@if (filter.options.length > filter.defaultVisibleCount) {
<button
class="text-xs font-semibold text-primary mt-3 self-start hover:underline flex items-center gap-1 opacity-90 hover:opacity-100 transition-opacity"
(click)="toggleExpanded(filter.name)">
{{ filter.expanded ? 'Show less' : 'Show all (' + filter.options.length + ')' }}
<mat-icon class="!w-3 !h-3 !text-[12px] leading-3">{{ filter.expanded ? 'expand_less' : 'expand_more' }}</mat-icon>
</button>
}
</div>

<button
*ngIf="filter.options.length > filter.defaultVisibleCount"
class="text-sm text-primary font-medium hover:underline mt-2 self-start"
(click)="toggleExpanded(filter.name)"
>
{{ filter.expanded ? 'Show less' : 'Show all' }}
</button>
</div>
}
}
</div>
</div>
11 changes: 9 additions & 2 deletions book-bazaar/src/app/components/filter-panel/filter-panel.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { Component, effect, input, output, signal, SimpleChange, SimpleChanges } from '@angular/core';
import { Component, computed, effect, input, output, signal, SimpleChange, SimpleChanges } from '@angular/core';
import {Filter} from '../../utils/filter';
import {NgIf} from '@angular/common';
import {MatCheckbox} from '@angular/material/checkbox';
import {MatButton} from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';

@Component({
selector: 'app-filter-panel',
imports: [
NgIf,
MatCheckbox,
MatButton
MatButton,
MatIcon
],
templateUrl: './filter-panel.html',
styleUrl: './filter-panel.css',
Expand All @@ -24,6 +26,11 @@ export class FilterPanel {

changed = output<Record<string, string[]>>();

hasSelectedFilters = computed(() => {
const selected = this.currentSelected();
return Object.values(selected).some(arr => arr && arr.length > 0);
});

constructor() {
effect(() => {
if (this.selectedFilters) {
Expand Down
Loading