mirror of
https://github.com/booklore-app/booklore.git
synced 2026-02-18 00:17:53 +01:00
Improve login and initial user setup screen appearance (#1336)
This commit is contained in:
@@ -94,7 +94,7 @@
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 1.0rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
|
||||
|
||||
@@ -124,7 +124,7 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
@media (max-width: 768px) {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
<div class="ml-2 md:ml-5 relative w-full max-w-[500px]">
|
||||
<!-- Desktop view -->
|
||||
<p-iconfield class="w-full sm:flex">
|
||||
<p-inputicon styleClass="pi pi-search"/>
|
||||
<div class="book-searcher-container">
|
||||
<p-iconfield class="search-iconfield" [class.focused]="isSearchFocused" [class.has-results]="isDropdownOpen">
|
||||
<p-inputicon class="pi pi-search"/>
|
||||
<input
|
||||
type="text"
|
||||
pInputText
|
||||
[(ngModel)]="searchQuery"
|
||||
(input)="onSearchInputChange()"
|
||||
(focus)="isSearchFocused = true"
|
||||
(blur)="onSearchBlur()"
|
||||
placeholder="Title, Author, Series, Genre, or ISBN..."
|
||||
class="w-full pr-10"
|
||||
class="search-input"
|
||||
/>
|
||||
@if (searchQuery) {
|
||||
<p-button
|
||||
@@ -16,35 +17,45 @@
|
||||
(click)="clearSearch()"
|
||||
[text]="true"
|
||||
[rounded]="true"
|
||||
class="absolute right-2 top-1/2 -translate-y-1/2 hidden lg:inline-flex"
|
||||
class="clear-search-btn"
|
||||
aria-label="Clear Search">
|
||||
</p-button>
|
||||
}
|
||||
</p-iconfield>
|
||||
|
||||
<!-- Search results below small screen search input -->
|
||||
@if (books.length > 0 || searchQuery) {
|
||||
<div
|
||||
class="search-dropdown layout-menu w-[300px] sm:w-[600px]"
|
||||
class="search-dropdown layout-menu"
|
||||
[class.show]="(books.length > 0)"
|
||||
>
|
||||
@if (books.length > 0) {
|
||||
@for (book of books; track book) {
|
||||
<div class="search-dropdown-item flex flex-col" (click)="onBookClick(book)">
|
||||
<div class="flex flex-row w-full p-2">
|
||||
<div class="search-dropdown-item" (click)="onBookClick(book)">
|
||||
<div class="search-item-content">
|
||||
<img
|
||||
[attr.src]="urlHelper.getThumbnailUrl(book.id, book.metadata?.coverUpdatedOn)"
|
||||
alt="Book Cover"
|
||||
class="search-book-cover"
|
||||
/>
|
||||
<div class="search-book-details">
|
||||
<div class="flex items-center">
|
||||
<p class="search-book-name text font-medium truncate">
|
||||
<div class="search-book-title-container">
|
||||
<p class="search-book-name">
|
||||
{{ book.metadata?.title | slice: 0:70 }}
|
||||
</p>
|
||||
</div>
|
||||
@if (book.metadata?.authors?.length ?? 0 > 0) {
|
||||
<p class="italic text-gray-300">by: {{ getAuthorNames(book.metadata?.authors) }}</p>
|
||||
<div class="search-book-meta-line">
|
||||
@if (book.metadata?.authors?.length ?? 0 > 0) {
|
||||
<p class="search-book-authors">by {{ getAuthorNames(book.metadata?.authors) }}</p>
|
||||
}
|
||||
@if (getPublishedYear(book.metadata?.publishedDate)) {
|
||||
<span class="metadata-badge year-badge">{{ getPublishedYear(book.metadata?.publishedDate) }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (getSeriesInfo(book.metadata?.seriesName, book.metadata?.seriesNumber)) {
|
||||
<div class="search-book-series metadata-badge series-badge">
|
||||
<i class="pi pi-book"></i>
|
||||
<span>{{ getSeriesInfo(book.metadata?.seriesName, book.metadata?.seriesNumber) }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -52,7 +63,7 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<div class="search-dropdown-item px-4 py-2">
|
||||
<div class="search-dropdown-item no-results">
|
||||
<span>No results found</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -1,65 +1,265 @@
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 10px;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-color-secondary);
|
||||
font-size: 1.0rem;
|
||||
pointer-events: none;
|
||||
.book-searcher-container {
|
||||
margin-left: 0.5rem;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
margin-left: 1.25rem;
|
||||
max-width: 315px;
|
||||
transition: max-width 0.3s ease;
|
||||
}
|
||||
|
||||
&:has(.search-iconfield.focused),
|
||||
&:has(.search-iconfield.has-results) {
|
||||
@media (min-width: 768px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-iconfield {
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 100%;
|
||||
padding-left: 30px;
|
||||
padding-right: 2.5rem;
|
||||
height: 2.5rem;
|
||||
line-height: 2.5rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
color: var(--text-color-secondary);
|
||||
.clear-search-btn {
|
||||
position: absolute;
|
||||
right: 10px;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
display: none;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
display: inline-flex;
|
||||
}
|
||||
}
|
||||
|
||||
.search-dropdown {
|
||||
position: absolute;
|
||||
top: calc(100% + 0.5rem);
|
||||
left: 0;
|
||||
background-color: var(--code-background);
|
||||
width: 300px;
|
||||
background-color: var(--card-background);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--card-border);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -1px rgba(0, 0, 0, 0.06),
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
||||
z-index: 1000;
|
||||
max-height: 500px;
|
||||
max-height: 70vh;
|
||||
overflow-y: auto;
|
||||
opacity: 0;
|
||||
height: 0;
|
||||
transition: opacity 0.3s ease, height 0.3s ease;
|
||||
transform: translateY(-10px);
|
||||
transition: opacity 0.2s ease, transform 0.2s ease, height 0.2s ease;
|
||||
|
||||
@media (min-width: 640px) {
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: var(--border-color);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
.search-dropdown.show {
|
||||
opacity: 1;
|
||||
height: auto;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.search-dropdown-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
transition: background-color 0.2s ease;
|
||||
|
||||
&:first-child {
|
||||
border-radius: 8px 8px 0 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-radius: 0 0 8px 8px;
|
||||
|
||||
::ng-deep .p-divider {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: var(--surface-hover);
|
||||
}
|
||||
|
||||
&.no-results {
|
||||
padding: 2rem 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-secondary-color);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
::ng-deep .p-divider {
|
||||
margin: 0;
|
||||
|
||||
&::before {
|
||||
border-top-color: var(--border-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.search-item-content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
width: 100%;
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.search-book-cover {
|
||||
width: 35px;
|
||||
height: 47px;
|
||||
width: 50px;
|
||||
height: 67px;
|
||||
min-width: 50px;
|
||||
object-fit: cover;
|
||||
max-width: 100px;
|
||||
max-height: 150px;
|
||||
border-radius: 4px;
|
||||
margin-right: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
background-color: var(--surface-ground);
|
||||
}
|
||||
|
||||
.search-book-details {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.search-book-title-container {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.search-book-name {
|
||||
font-weight: 600;
|
||||
font-size: 0.95rem;
|
||||
line-height: 1.4;
|
||||
color: var(--text-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
word-break: break-word;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.search-book-meta-line {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-book-authors {
|
||||
font-size: 0.9rem;
|
||||
font-style: italic;
|
||||
color: var(--text-secondary-color);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.search-book-year {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-secondary-color);
|
||||
background-color: var(--surface-ground);
|
||||
padding: 0.125rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.search-book-series {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.8rem;
|
||||
color: var(--primary-color);
|
||||
background-color: rgba(var(--primary-color-rgb, 59, 130, 246), 0.1);
|
||||
padding: 0.075rem 0.125rem;
|
||||
border-radius: 4px;
|
||||
width: fit-content;
|
||||
|
||||
i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.search-book-metadata {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.metadata-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.1rem 0.25rem;
|
||||
border-radius: 4px;
|
||||
background-color: var(--surface-hover);
|
||||
color: var(--text-secondary-color);
|
||||
white-space: nowrap;
|
||||
|
||||
i {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
&.series-badge {
|
||||
background-color: rgba(59, 130, 246, 0.1);
|
||||
color: rgb(59, 130, 246);
|
||||
}
|
||||
|
||||
&.year-badge {
|
||||
background-color: rgba(34, 197, 94, 0.1);
|
||||
color: rgb(34, 197, 94);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {Component, inject, OnDestroy, OnInit} from '@angular/core';
|
||||
import {BehaviorSubject, of, Subject, Subscription} from 'rxjs';
|
||||
import {BehaviorSubject, of, Subscription} from 'rxjs';
|
||||
import {catchError, switchMap} from 'rxjs/operators';
|
||||
import {Book} from '../../model/book.model';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
@@ -34,6 +34,7 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
|
||||
books: Book[] = [];
|
||||
#searchSubject = new BehaviorSubject<string>('');
|
||||
#subscription!: Subscription;
|
||||
isSearchFocused = false;
|
||||
|
||||
private bookService = inject(BookService);
|
||||
private router = inject(Router);
|
||||
@@ -41,24 +42,16 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
|
||||
private headerFilter = new HeaderFilter(this.#searchSubject.asObservable());
|
||||
|
||||
ngOnInit(): void {
|
||||
this.initializeSearch();
|
||||
}
|
||||
|
||||
initializeSearch(): void {
|
||||
this.#subscription = this.bookService.bookState$.pipe(
|
||||
switchMap(bookState => this.headerFilter.filter(bookState)),
|
||||
catchError((error) => {
|
||||
console.error('Error while searching books:', error);
|
||||
return of({books: [], loaded: true, error: null});
|
||||
})
|
||||
catchError(() => of({books: [], loaded: true, error: null}))
|
||||
).subscribe({
|
||||
next: (filteredState) => {
|
||||
const term = this.searchQuery.trim();
|
||||
this.books = term.length >= 2
|
||||
? (filteredState.books || []).slice(0, 50)
|
||||
: [];
|
||||
},
|
||||
error: (error) => console.error('Subscription error:', error)
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -66,6 +59,20 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
|
||||
return authors?.join(', ') || 'Unknown Author';
|
||||
}
|
||||
|
||||
getPublishedYear(publishedDate: string | undefined): string | null {
|
||||
if (!publishedDate) return null;
|
||||
const year = publishedDate.split('-')[0];
|
||||
return year && year.length === 4 ? year : null;
|
||||
}
|
||||
|
||||
getSeriesInfo(seriesName: string | undefined, seriesNumber: number | null | undefined): string | null {
|
||||
if (!seriesName) return null;
|
||||
if (seriesNumber) {
|
||||
return `${seriesName} #${seriesNumber}`;
|
||||
}
|
||||
return seriesName;
|
||||
}
|
||||
|
||||
onSearchInputChange(): void {
|
||||
this.#searchSubject.next(this.searchQuery.trim());
|
||||
}
|
||||
@@ -82,6 +89,18 @@ export class BookSearcherComponent implements OnInit, OnDestroy {
|
||||
this.books = [];
|
||||
}
|
||||
|
||||
get isDropdownOpen(): boolean {
|
||||
return this.books.length > 0;
|
||||
}
|
||||
|
||||
onSearchBlur(): void {
|
||||
setTimeout(() => {
|
||||
if (!this.isDropdownOpen) {
|
||||
this.isSearchFocused = false;
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
if (this.#subscription) {
|
||||
this.#subscription.unsubscribe();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
@if (filteredBooks$ | async; as books) {
|
||||
<div class="rounded-xl overflow-hidden">
|
||||
<div class="rounded-xl overflow-hidden border-[1px] border-solid border-[var(--p-content-border-color)]">
|
||||
<p-tabs [value]="tab" lazy="true" scrollable>
|
||||
<p-tablist>
|
||||
<p-tab value="view">
|
||||
|
||||
@@ -102,7 +102,6 @@ export interface BookMetadata {
|
||||
providerBookId?: string;
|
||||
thumbnailUrl?: string | null;
|
||||
reviews?: BookReview[];
|
||||
|
||||
titleLocked?: boolean;
|
||||
subtitleLocked?: boolean;
|
||||
publisherLocked?: boolean;
|
||||
@@ -134,7 +133,6 @@ export interface BookMetadata {
|
||||
tagsLocked?: boolean;
|
||||
coverLocked?: boolean;
|
||||
reviewsLocked?: boolean;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -203,7 +201,6 @@ export interface BookSetting {
|
||||
epubSettings?: EpubViewerSetting;
|
||||
cbxSettings?: CbxViewerSetting;
|
||||
newPdfSettings?: NewPdfReaderSetting;
|
||||
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
padding: 3rem;
|
||||
background: var(--p-surface-900);
|
||||
border-radius: 24px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
backdrop-filter: blur(20px);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="rounded-xl overflow-hidden">
|
||||
<div class="rounded-xl overflow-hidden border-[1px] border-solid border-[var(--p-content-border-color)]">
|
||||
<p-tabs [value]="tab" lazy="true" scrollable>
|
||||
<p-tablist>
|
||||
<p-tab value="view">
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
p {
|
||||
margin-top: 1rem;
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 3rem;
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
background: var(--surface-card);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--surface-border);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.count {
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
@@ -107,7 +107,7 @@
|
||||
small {
|
||||
display: block;
|
||||
margin-top: 0.5rem;
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
padding: 25px;
|
||||
margin-bottom: 30px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -48,7 +48,7 @@
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
font-size: 0.95rem;
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
flex-wrap: wrap;
|
||||
|
||||
.total-item {
|
||||
@@ -67,7 +67,7 @@
|
||||
}
|
||||
|
||||
.total-label {
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.9rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -309,7 +309,7 @@
|
||||
}
|
||||
|
||||
label {
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
@@ -426,7 +426,7 @@
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
backdrop-filter: blur(10px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
@@ -472,7 +472,7 @@
|
||||
|
||||
.chart-description {
|
||||
text-align: center;
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.9rem;
|
||||
margin-bottom: 15px;
|
||||
line-height: 1.4;
|
||||
@@ -546,7 +546,7 @@
|
||||
.no-data-message {
|
||||
text-align: center;
|
||||
padding: 50px;
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@
|
||||
}
|
||||
|
||||
.insight-description {
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
@@ -702,7 +702,7 @@
|
||||
}
|
||||
|
||||
.insight-description {
|
||||
color: var(--text-color-secondary, #cccccc);
|
||||
color: var(--text-secondary-color);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
|
||||
@@ -1,37 +1,100 @@
|
||||
<div class="change-password-container">
|
||||
<div class="change-password-wrapper">
|
||||
<p-card class="change-password-card p-fluid">
|
||||
<div class="change-password-card-wrapper">
|
||||
<div class="change-password-card-border">
|
||||
<div class="change-password-card">
|
||||
<div class="change-password-header">
|
||||
<div class="logo-wrapper">
|
||||
<div class="icon-circle">
|
||||
<i class="pi pi-key"></i>
|
||||
</div>
|
||||
</div>
|
||||
<h1 class="change-password-title">Change Password</h1>
|
||||
<p class="change-password-subtitle">Update your password to keep your account secure</p>
|
||||
</div>
|
||||
|
||||
<ng-template pTemplate="content">
|
||||
<form #changePasswordForm="ngForm" (ngSubmit)="changePassword()">
|
||||
<div class="p-field">
|
||||
<label for="currentPassword">Current Password</label>
|
||||
<p-password id="currentPassword" [(ngModel)]="currentPassword" name="currentPassword" [feedback]="false" required fluid></p-password>
|
||||
<form class="change-password-form" #changePasswordForm="ngForm" (ngSubmit)="changePassword()">
|
||||
<div class="form-field">
|
||||
<label for="currentPassword" class="form-label">
|
||||
<i class="pi pi-lock"></i>
|
||||
<span>Current Password</span>
|
||||
</label>
|
||||
<p-password
|
||||
id="currentPassword"
|
||||
name="currentPassword"
|
||||
required
|
||||
[(ngModel)]="currentPassword"
|
||||
[feedback]="false"
|
||||
placeholder="Enter your current password"
|
||||
[toggleMask]="true"
|
||||
[fluid]="true"
|
||||
autocomplete="current-password"
|
||||
></p-password>
|
||||
</div>
|
||||
|
||||
<div class="p-field">
|
||||
<label for="newPassword">New Password</label>
|
||||
<p-password id="newPassword" [(ngModel)]="newPassword" name="newPassword" [feedback]="false" required fluid></p-password>
|
||||
<div class="form-field">
|
||||
<label for="newPassword" class="form-label">
|
||||
<i class="pi pi-lock-open"></i>
|
||||
<span>New Password</span>
|
||||
</label>
|
||||
<p-password
|
||||
id="newPassword"
|
||||
name="newPassword"
|
||||
required
|
||||
[(ngModel)]="newPassword"
|
||||
[feedback]="false"
|
||||
placeholder="Enter your new password"
|
||||
[toggleMask]="true"
|
||||
[fluid]="true"
|
||||
autocomplete="new-password"
|
||||
></p-password>
|
||||
</div>
|
||||
|
||||
<div class="p-field">
|
||||
<label for="confirmNewPassword">Confirm New Password</label>
|
||||
<p-password id="confirmNewPassword" [(ngModel)]="confirmNewPassword" name="confirmNewPassword" [feedback]="false" required fluid></p-password>
|
||||
<div class="form-field">
|
||||
<label for="confirmNewPassword" class="form-label">
|
||||
<i class="pi pi-check-circle"></i>
|
||||
<span>Confirm New Password</span>
|
||||
</label>
|
||||
<p-password
|
||||
id="confirmNewPassword"
|
||||
name="confirmNewPassword"
|
||||
required
|
||||
[(ngModel)]="confirmNewPassword"
|
||||
[feedback]="false"
|
||||
placeholder="Confirm your new password"
|
||||
[toggleMask]="true"
|
||||
[fluid]="true"
|
||||
autocomplete="new-password"
|
||||
></p-password>
|
||||
@if (!passwordsMatch && confirmNewPassword) {
|
||||
<p-message severity="error" text="New passwords do not match."></p-message>
|
||||
<small class="field-error">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
New passwords do not match
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<p-button type="submit" label="Change Password" icon="pi pi-key" class="p-fluid"></p-button>
|
||||
<p-button
|
||||
fluid
|
||||
type="submit"
|
||||
label="Update Password"
|
||||
icon="pi pi-check"
|
||||
[disabled]="!changePasswordForm.valid || !passwordsMatch"
|
||||
class="change-password-button"
|
||||
/>
|
||||
|
||||
@if (errorMessage) {
|
||||
<p-message severity="error" text="{{ errorMessage }}"></p-message>
|
||||
<div class="message-container error-message">
|
||||
<p-message severity="error">{{ errorMessage }}</p-message>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (successMessage) {
|
||||
<p-message severity="success" text="{{ successMessage }}"></p-message>
|
||||
<div class="message-container success-message">
|
||||
<p-message severity="success">{{ successMessage }}</p-message>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</ng-template>
|
||||
</p-card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,28 +1,90 @@
|
||||
@use '../../styles/auth-shared' as auth;
|
||||
|
||||
.change-password-container {
|
||||
@apply flex items-center justify-center min-h-screen bg-gradient-to-br from-blue-900 via-cyan-600 to-blue-400 relative;
|
||||
@include auth.auth-container;
|
||||
}
|
||||
|
||||
.change-password-wrapper {
|
||||
@apply flex flex-col items-center gap-6 absolute top-1/4 transform -translate-y-1/4;
|
||||
}
|
||||
|
||||
.change-password-card-wrapper {
|
||||
@include auth.auth-card-wrapper(500px, -6rem);
|
||||
}
|
||||
|
||||
.change-password-card-border {
|
||||
@include auth.auth-card-border;
|
||||
}
|
||||
|
||||
.change-password-card {
|
||||
@include auth.auth-card;
|
||||
}
|
||||
|
||||
.app-title {
|
||||
@apply text-5xl font-extrabold text-white drop-shadow-lg;
|
||||
text-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.change-password-card {
|
||||
@apply w-96 shadow-lg rounded-xl bg-opacity-90 backdrop-blur-lg;
|
||||
.change-password-header {
|
||||
@include auth.auth-header;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
@include auth.auth-logo;
|
||||
}
|
||||
|
||||
.icon-circle {
|
||||
@include auth.auth-icon-circle;
|
||||
}
|
||||
|
||||
.change-password-title {
|
||||
@include auth.auth-title;
|
||||
}
|
||||
|
||||
.change-password-subtitle {
|
||||
@include auth.auth-subtitle;
|
||||
}
|
||||
|
||||
form {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.change-password-form {
|
||||
@include auth.auth-form;
|
||||
}
|
||||
|
||||
.p-field {
|
||||
@apply flex flex-col gap-2;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@include auth.form-field;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@include auth.form-label;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
@include auth.field-error;
|
||||
}
|
||||
|
||||
p-button {
|
||||
@apply mt-4;
|
||||
}
|
||||
|
||||
.change-password-button {
|
||||
@include auth.auth-button;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
@include auth.message-container;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include auth.error-message;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
@include auth.success-message;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import {Component, inject} from '@angular/core';
|
||||
import {Button} from 'primeng/button';
|
||||
import {Card} from 'primeng/card';
|
||||
import {FormsModule, ReactiveFormsModule} from '@angular/forms';
|
||||
import {Message} from 'primeng/message';
|
||||
|
||||
import {Password} from 'primeng/password';
|
||||
import {MessageService, PrimeTemplate} from 'primeng/api';
|
||||
import {MessageService} from 'primeng/api';
|
||||
import {UserService} from '../../../features/settings/user-management/user.service';
|
||||
import {AuthService} from '../../service/auth.service';
|
||||
|
||||
@@ -14,13 +13,11 @@ import {AuthService} from '../../service/auth.service';
|
||||
standalone: true,
|
||||
imports: [
|
||||
Button,
|
||||
Card,
|
||||
FormsModule,
|
||||
Message,
|
||||
Password,
|
||||
PrimeTemplate,
|
||||
ReactiveFormsModule
|
||||
],
|
||||
],
|
||||
templateUrl: './change-password.component.html',
|
||||
styleUrl: './change-password.component.scss'
|
||||
})
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
|
||||
.hash-group {
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
margin: 0.5rem 0;
|
||||
@@ -120,7 +120,7 @@
|
||||
padding: 0.75rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid var(--p-content-border-color);
|
||||
transition: all 0.2s ease;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
font-size: 16px;
|
||||
border: 1px solid var(--text-color-secondary);
|
||||
border: 1px solid var(--text-secondary-color);
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
box-sizing: border-box;
|
||||
@@ -27,6 +27,6 @@
|
||||
}
|
||||
|
||||
.icon-search::placeholder {
|
||||
color: var(--text-color-secondary);
|
||||
color: var(--text-secondary-color);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
<div class="flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden"
|
||||
style="background: linear-gradient(60deg, var(--primary-color) 0%, #1e3a8a 100%);">
|
||||
<div class="flex flex-col items-center justify-center -mt-48 w-[90%] sm:w-auto sm:min-w-[400px] md:min-w-[500px]">
|
||||
<div class="w-full" style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-900 pt-8 pb-14 px-8 sm:px-12" style="border-radius: 53px">
|
||||
<div class="text-center mb-8">
|
||||
<svg class="mb-8 w-14 h-14 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
<div class="text-3xl font-medium mb-4">Welcome to Booklore</div>
|
||||
<span class="font-medium">Sign in to continue</span>
|
||||
<div class="login-container">
|
||||
<div class="login-card-wrapper">
|
||||
<div class="login-card-border">
|
||||
<div class="login-card">
|
||||
<div class="login-header">
|
||||
<div class="logo-wrapper">
|
||||
<svg class="logo-icon" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="login-title">Welcome Back</h1>
|
||||
<p class="login-subtitle">Sign in to continue your journey</p>
|
||||
</div>
|
||||
|
||||
@if (showOidcBypassInfo) {
|
||||
<div class="mb-4 p-3 rounded-md bg-yellow-900/30 border border-yellow-600/50 w-full">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="flex items-center gap-2 text-yellow-200 text-sm mb-2">
|
||||
<div class="oidc-warning">
|
||||
<div class="warning-content">
|
||||
<div class="warning-header">
|
||||
<i class="pi pi-exclamation-triangle"></i>
|
||||
<span class="font-medium">OIDC Authentication Issues Detected</span>
|
||||
<span>OIDC Authentication Issues</span>
|
||||
</div>
|
||||
<p class="text-xs text-yellow-300 mb-3 text-center">
|
||||
{{ oidcBypassMessage }}
|
||||
</p>
|
||||
<div class="flex gap-4">
|
||||
<p class="warning-message">{{ oidcBypassMessage }}</p>
|
||||
<div class="warning-actions">
|
||||
<p-button
|
||||
size="small"
|
||||
severity="warn"
|
||||
@@ -45,9 +44,12 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<form class="flex flex-col gap-4 w-full" #loginForm="ngForm" (ngSubmit)="login()">
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username" class="text-lg font-medium">Username</label>
|
||||
<form class="login-form" #loginForm="ngForm" (ngSubmit)="login()">
|
||||
<div class="form-field">
|
||||
<label for="username" class="form-label">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</label>
|
||||
<input
|
||||
fluid
|
||||
pInputText
|
||||
@@ -56,13 +58,16 @@
|
||||
required
|
||||
[(ngModel)]="username"
|
||||
placeholder="Enter your username"
|
||||
class="w-full"
|
||||
class="form-input"
|
||||
autocomplete="username"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password" class="text-lg font-medium">Password</label>
|
||||
<div class="form-field">
|
||||
<label for="password" class="form-label">
|
||||
<i class="pi pi-lock"></i>
|
||||
<span>Password</span>
|
||||
</label>
|
||||
<p-password
|
||||
id="password"
|
||||
name="password"
|
||||
@@ -71,7 +76,6 @@
|
||||
[feedback]="false"
|
||||
placeholder="Enter your password"
|
||||
[toggleMask]="true"
|
||||
class="mb-4"
|
||||
[fluid]="true"
|
||||
autocomplete="current-password"
|
||||
></p-password>
|
||||
@@ -80,47 +84,47 @@
|
||||
<p-button
|
||||
fluid
|
||||
type="submit"
|
||||
label="Login"
|
||||
label="Sign In"
|
||||
icon="pi pi-sign-in"
|
||||
[disabled]="!loginForm.valid || !username || !password"
|
||||
class="w-full">
|
||||
class="login-button">
|
||||
</p-button>
|
||||
|
||||
@if (errorMessage) {
|
||||
<p-message severity="error">{{ errorMessage }}</p-message>
|
||||
<div class="error-message">
|
||||
<p-message severity="error">{{ errorMessage }}</p-message>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (oidcEnabled && !isOidcBypassed) {
|
||||
<div class="flex items-center gap-2 my-2">
|
||||
<hr class="flex-grow" style="border: none; border-top: 1px solid var(--border-color);"/>
|
||||
<span class="text-gray-300">or</span>
|
||||
<hr class="flex-grow" style="border: none; border-top: 1px solid var(--border-color);"/>
|
||||
<div class="divider">
|
||||
<span>or continue with</span>
|
||||
</div>
|
||||
|
||||
<p-button
|
||||
fluid
|
||||
severity="info"
|
||||
[label]="isOidcLoginInProgress ? 'Redirecting to ' + oidcName + '...' : 'Login with ' + oidcName"
|
||||
severity="secondary"
|
||||
[label]="isOidcLoginInProgress ? 'Redirecting to ' + oidcName + '...' : oidcName"
|
||||
[icon]="isOidcLoginInProgress ? 'pi pi-spin pi-spinner' : 'pi pi-id-card'"
|
||||
[disabled]="isOidcLoginInProgress"
|
||||
(click)="loginWithOidc()"
|
||||
class="w-full bg-gray-800 text-white hover:bg-gray-700"/>
|
||||
class="oidc-button"/>
|
||||
|
||||
<div class="text-center">
|
||||
<div class="help-text">
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-gray-400 hover:text-gray-300 underline"
|
||||
class="text-link"
|
||||
(click)="bypassOidc()">
|
||||
Having trouble with {{ oidcName }}? Use local login only
|
||||
Having trouble with {{ oidcName }}? Use local login
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (oidcEnabled && isOidcBypassed) {
|
||||
<div class="text-center mt-4">
|
||||
<div class="help-text">
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs text-blue-400 hover:text-blue-300 underline"
|
||||
class="text-link primary"
|
||||
(click)="enableOidc()">
|
||||
Re-enable {{ oidcName }} login
|
||||
</button>
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
@use '../../styles/auth-shared' as auth;
|
||||
|
||||
.login-container {
|
||||
@include auth.auth-container;
|
||||
}
|
||||
|
||||
.login-card-wrapper {
|
||||
@include auth.auth-card-wrapper(460px, -8rem);
|
||||
}
|
||||
|
||||
.login-card-border {
|
||||
@include auth.auth-card-border;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
@include auth.auth-card;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
@include auth.auth-header;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
@include auth.auth-logo;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
@include auth.auth-logo-icon;
|
||||
}
|
||||
|
||||
.login-title {
|
||||
@include auth.auth-title;
|
||||
}
|
||||
|
||||
.login-subtitle {
|
||||
@include auth.auth-subtitle;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.oidc-warning {
|
||||
margin-bottom: 1.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
background: rgba(245, 158, 11, 0.1);
|
||||
border: 1px solid rgba(245, 158, 11, 0.3);
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
.warning-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.warning-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
color: #fbbf24;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
|
||||
i {
|
||||
font-size: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.warning-message {
|
||||
font-size: 0.75rem;
|
||||
color: #fcd34d;
|
||||
line-height: 1.5;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.warning-actions {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
@include auth.auth-form(1.5rem);
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@include auth.form-field;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@include auth.form-label;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@include auth.form-input;
|
||||
}
|
||||
|
||||
.login-button {
|
||||
@include auth.auth-button;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include auth.error-message;
|
||||
}
|
||||
|
||||
.divider {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
margin: 1.5rem 0;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
span {
|
||||
padding: 0 1rem;
|
||||
font-size: 0.875rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.oidc-button {
|
||||
::ng-deep button {
|
||||
height: 2.75rem;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.help-text {
|
||||
text-align: center;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.text-link {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 0.8rem;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s ease;
|
||||
padding: 0;
|
||||
|
||||
&:hover {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
color: var(--primary-color);
|
||||
|
||||
&:hover {
|
||||
color: #60a5fa;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,42 +1,50 @@
|
||||
<div class="flex items-center justify-center min-h-screen min-w-[100vw] overflow-hidden"
|
||||
style="background: linear-gradient(60deg, var(--primary-color) 0%, #1e3a8a 100%);">
|
||||
<div class="flex flex-col items-center justify-center -mt-48">
|
||||
<div style="border-radius: 56px; padding: 0.3rem; background: linear-gradient(180deg, var(--primary-color) 10%, rgba(33, 150, 243, 0) 30%)">
|
||||
<div class="w-full bg-surface-900 py-16 px-8 sm:px-16" style="border-radius: 53px">
|
||||
<div class="text-center mb-8">
|
||||
<svg class="mb-8 w-16 h-16 mx-auto" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
<div class="text-3xl font-medium mb-4">Welcome to Booklore</div>
|
||||
<span class="text-xl">Setup Initial Admin Account</span>
|
||||
<div class="setup-container">
|
||||
<div class="setup-card-wrapper">
|
||||
<div class="setup-card-border">
|
||||
<div class="setup-card">
|
||||
<div class="setup-header">
|
||||
<div class="logo-wrapper">
|
||||
<svg class="logo-icon" viewBox="0 0 126 126" fill="var(--primary-color)" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M59 4.79297C71.5051 11.5557 80 24.7854 80 40C80 40.5959 79.987 41.1888 79.9609 41.7783C79.8609 44.0406 81.7355 46 84 46C106.091 46 124 63.9086 124 86C124 108.091 106.091 126 84 126H10C4.47715 126 0 121.523 0 116V39.0068L0.0126953 38.9941C0.357624 25.0252 7.86506 12.8347 19 5.95215V63.832C19 64.8345 20.0676 65.4391 20.9121 64.9902L21.0771 64.8867L38.2227 52.3428C38.6819 52.0068 39.3064 52.0068 39.7656 52.3428L56.9229 64.8945L57.0879 64.998C57.9324 65.447 59 64.8423 59 63.8398V4.79297Z"/>
|
||||
<path
|
||||
d="M40 0C43.8745 0 47.6199 0.552381 51.1631 1.58008V50.9697L44.3926 46.0176L44.0879 45.8037C40.9061 43.6679 36.7098 43.7393 33.5957 46.0176L26.8369 50.9619V2.21875C30.9593 0.782634 35.3881 0 40 0Z"
|
||||
fill="white"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1 class="setup-title">Welcome to Booklore</h1>
|
||||
<p class="setup-subtitle">Setup your initial admin account to get started</p>
|
||||
</div>
|
||||
|
||||
<form class="flex flex-col gap-4" [formGroup]="setupForm" (ngSubmit)="onSubmit()">
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="username" class="text-lg font-medium">Username</label>
|
||||
<form class="setup-form" [formGroup]="setupForm" (ngSubmit)="onSubmit()">
|
||||
<div class="form-field">
|
||||
<label for="username" class="form-label">
|
||||
<i class="pi pi-user"></i>
|
||||
<span>Username</span>
|
||||
</label>
|
||||
<input
|
||||
pInputText
|
||||
id="username"
|
||||
name="username"
|
||||
required
|
||||
formControlName="username"
|
||||
placeholder="Enter your username"
|
||||
class="w-full md:w-[30rem]"
|
||||
placeholder="Choose a username"
|
||||
class="form-input"
|
||||
autocomplete="username"
|
||||
/>
|
||||
@if (setupForm.get('username')?.invalid && setupForm.get('username')?.touched) {
|
||||
<small class="text-red-500">
|
||||
Username is required.
|
||||
<small class="field-error">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
Username is required
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="name" class="text-lg font-medium">Name</label>
|
||||
<div class="form-field">
|
||||
<label for="name" class="form-label">
|
||||
<i class="pi pi-id-card"></i>
|
||||
<span>Full Name</span>
|
||||
</label>
|
||||
<input
|
||||
pInputText
|
||||
id="name"
|
||||
@@ -44,68 +52,85 @@
|
||||
required
|
||||
formControlName="name"
|
||||
placeholder="Enter your full name"
|
||||
class="w-full md:w-[30rem]"
|
||||
class="form-input"
|
||||
autocomplete="name"
|
||||
/>
|
||||
@if (setupForm.get('name')?.invalid && setupForm.get('name')?.touched) {
|
||||
<small class="text-red-500">
|
||||
Name is required.
|
||||
<small class="field-error">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
Name is required
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="email" class="text-lg font-medium">Email</label>
|
||||
<div class="form-field">
|
||||
<label for="email" class="form-label">
|
||||
<i class="pi pi-envelope"></i>
|
||||
<span>Email Address</span>
|
||||
</label>
|
||||
<input
|
||||
pInputText
|
||||
id="email"
|
||||
name="email"
|
||||
type="email"
|
||||
required
|
||||
formControlName="email"
|
||||
placeholder="Enter your email"
|
||||
class="w-full md:w-[30rem]"
|
||||
placeholder="Enter your email address"
|
||||
class="form-input"
|
||||
autocomplete="email"
|
||||
/>
|
||||
@if (setupForm.get('email')?.invalid && setupForm.get('email')?.touched) {
|
||||
<small class="text-red-500">
|
||||
Valid email is required.
|
||||
<small class="field-error">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
Valid email is required
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<label for="password" class="text-lg font-medium">Password</label>
|
||||
<div class="form-field">
|
||||
<label for="password" class="form-label">
|
||||
<i class="pi pi-lock"></i>
|
||||
<span>Password</span>
|
||||
</label>
|
||||
<input
|
||||
pInputText
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
required
|
||||
formControlName="password"
|
||||
placeholder="Enter new password"
|
||||
class="w-full md:w-[30rem]"
|
||||
placeholder="Create a secure password"
|
||||
class="form-input"
|
||||
autocomplete="new-password"
|
||||
/>
|
||||
@if (setupForm.get('password')?.invalid && setupForm.get('password')?.touched) {
|
||||
<small class="text-red-500">
|
||||
Minimum 6 characters required.
|
||||
<small class="field-error">
|
||||
<i class="pi pi-exclamation-circle"></i>
|
||||
Minimum 6 characters required
|
||||
</small>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-3 mt-4">
|
||||
<p-button
|
||||
fluid
|
||||
type="submit"
|
||||
[label]="loading ? 'Creating…' : 'Create Admin Account'"
|
||||
[disabled]="loading || setupForm.invalid"
|
||||
icon="pi pi-user-plus"
|
||||
>
|
||||
</p-button>
|
||||
<p-button
|
||||
fluid
|
||||
type="submit"
|
||||
[label]="loading ? 'Creating Account...' : 'Create Admin Account'"
|
||||
[icon]="loading ? 'pi pi-spin pi-spinner' : 'pi pi-user-plus'"
|
||||
[disabled]="loading || setupForm.invalid"
|
||||
class="setup-button"
|
||||
/>
|
||||
|
||||
@if (error) {
|
||||
<p-message severity="error" text="{{ error }}"></p-message>
|
||||
}
|
||||
@if (success) {
|
||||
<p-message severity="success" text="Admin account created! Redirecting…"></p-message>
|
||||
}
|
||||
</div>
|
||||
@if (error) {
|
||||
<div class="message-container error-message">
|
||||
<p-message severity="error">{{ error }}</p-message>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (success) {
|
||||
<div class="message-container success-message">
|
||||
<p-message severity="success">Admin account created successfully! Redirecting...</p-message>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
@use '../../styles/auth-shared' as auth;
|
||||
|
||||
.setup-container {
|
||||
@include auth.auth-container;
|
||||
}
|
||||
|
||||
.setup-card-wrapper {
|
||||
@include auth.auth-card-wrapper(540px, -6rem);
|
||||
}
|
||||
|
||||
.setup-card-border {
|
||||
@include auth.auth-card-border;
|
||||
}
|
||||
|
||||
.setup-card {
|
||||
@include auth.auth-card;
|
||||
}
|
||||
|
||||
.setup-header {
|
||||
@include auth.auth-header;
|
||||
}
|
||||
|
||||
.logo-wrapper {
|
||||
@include auth.auth-logo;
|
||||
}
|
||||
|
||||
.logo-icon {
|
||||
@include auth.auth-logo-icon;
|
||||
}
|
||||
|
||||
.setup-title {
|
||||
@include auth.auth-title;
|
||||
}
|
||||
|
||||
.setup-subtitle {
|
||||
@include auth.auth-subtitle;
|
||||
}
|
||||
|
||||
.setup-form {
|
||||
@include auth.auth-form;
|
||||
}
|
||||
|
||||
.form-field {
|
||||
@include auth.form-field;
|
||||
}
|
||||
|
||||
.form-label {
|
||||
@include auth.form-label;
|
||||
}
|
||||
|
||||
.form-input {
|
||||
@include auth.form-input;
|
||||
}
|
||||
|
||||
.field-error {
|
||||
@include auth.field-error;
|
||||
}
|
||||
|
||||
.setup-button {
|
||||
@include auth.auth-button;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
@include auth.message-container;
|
||||
}
|
||||
|
||||
.error-message {
|
||||
@include auth.error-message;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
@include auth.success-message;
|
||||
}
|
||||
|
||||
287
booklore-ui/src/app/shared/styles/_auth-shared.scss
Normal file
287
booklore-ui/src/app/shared/styles/_auth-shared.scss
Normal file
@@ -0,0 +1,287 @@
|
||||
@mixin auth-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #1e3a8a 50%, #0f172a 100%);
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(circle, rgba(59, 130, 246, 0.1) 0%, transparent 70%);
|
||||
animation: float 20s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-card-wrapper($max-width: 500px, $margin-top: -6rem) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: $margin-top;
|
||||
width: 90%;
|
||||
max-width: $max-width;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
animation: slideUp 0.6s ease-out;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
width: 95%;
|
||||
margin-top: calc($margin-top / 2);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-card-border {
|
||||
width: 100%;
|
||||
border-radius: 2rem;
|
||||
padding: 2px;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, rgba(59, 130, 246, 0.3) 50%, transparent 100%);
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 25px 70px rgba(0, 0, 0, 0.5);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-card {
|
||||
width: 100%;
|
||||
background: rgba(17, 24, 39, 0.95);
|
||||
backdrop-filter: blur(20px);
|
||||
border-radius: calc(2rem - 2px);
|
||||
padding: 3rem 2.5rem;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-header {
|
||||
text-align: center;
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
@mixin auth-logo {
|
||||
margin-bottom: 1.5rem;
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@mixin auth-logo-icon {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto;
|
||||
filter: drop-shadow(0 4px 12px rgba(59, 130, 246, 0.4));
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-icon-circle {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%);
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.4);
|
||||
transition: transform 0.3s ease;
|
||||
|
||||
i {
|
||||
font-size: 1.75rem;
|
||||
color: white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.1) rotate(5deg);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
background: linear-gradient(135deg, #ffffff 0%, var(--primary-color) 100%);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
background-clip: text;
|
||||
|
||||
@media (max-width: 640px) {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-subtitle {
|
||||
font-size: 0.95rem;
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
font-weight: 400;
|
||||
line-height: 1.5;
|
||||
max-width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@mixin auth-form($gap: 1.25rem) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
@mixin form-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
@mixin form-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
|
||||
i {
|
||||
color: var(--primary-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin form-input {
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
}
|
||||
|
||||
@mixin field-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
color: #ef4444;
|
||||
font-size: 0.8rem;
|
||||
margin-top: -0.25rem;
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
|
||||
i {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin auth-button {
|
||||
margin-top: 0.75rem;
|
||||
|
||||
::ng-deep button {
|
||||
height: 3rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s ease;
|
||||
background: linear-gradient(135deg, var(--primary-color) 0%, #2563eb 100%);
|
||||
border: none;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 8px 24px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@mixin message-container {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@mixin error-message {
|
||||
animation: shake 0.4s ease-in-out, fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@mixin success-message {
|
||||
animation: successPulse 0.5s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes float {
|
||||
0%, 100% {
|
||||
transform: translate(0, 0) rotate(0deg);
|
||||
}
|
||||
33% {
|
||||
transform: translate(30px, -30px) rotate(120deg);
|
||||
}
|
||||
66% {
|
||||
transform: translate(-20px, 20px) rotate(240deg);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes shake {
|
||||
0%, 100% {
|
||||
transform: translateX(0);
|
||||
}
|
||||
25% {
|
||||
transform: translateX(-10px);
|
||||
}
|
||||
75% {
|
||||
transform: translateX(10px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes successPulse {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: scale(0.95);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,41 @@
|
||||
@use "variables";
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
font-size: variables.$scale;
|
||||
height: 100%;
|
||||
font-size: variables.$scale;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--font-family);
|
||||
color: var(--text-color);
|
||||
background-color: var(--ground-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
font-family: var(--font-family), serif;
|
||||
color: var(--text-color);
|
||||
background-color: var(--ground-background);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
min-height: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
--glow-image: url(https://github.com/booklore-app/booklore-docs/blob/master/static/img/cdn/bg.png?raw=true), radial-gradient(50% 50% at center -25px, var(--p-primary-color) 0%, #000000 100%);
|
||||
--glow-blend: hard-light, color-dodge;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
text-decoration: none;
|
||||
color: var(--primary-color);
|
||||
}
|
||||
|
||||
.layout-wrapper {
|
||||
min-height: 100dvh;
|
||||
min-height: 100dvh;
|
||||
scroll-behavior: smooth;
|
||||
background-color: var(--ground-background);
|
||||
background-blend-mode: var(--glow-blend);
|
||||
background-image: var(--glow-image);
|
||||
background-position: top;
|
||||
background-repeat: no-repeat;
|
||||
background-size: auto 20rem;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
top: 0;
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
background-color: var(--card-background);
|
||||
background-color: color-mix(in srgb, var(--card-background) 25%, transparent);
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--p-content-border-color);
|
||||
transition: left variables.$transitionDuration;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
/* Content & Text */
|
||||
--text-color: var(--p-surface-0);
|
||||
--text-color-secondary: var(--p-surface-300);
|
||||
--text-secondary-color: var(--p-surface-400);
|
||||
--high-contrast-text-color: var(--p-surface-0);
|
||||
--p-text-muted-color: var(--p-surface-400);
|
||||
|
||||
Reference in New Issue
Block a user