Add components for Act management and integrate Electron setup
This commit is contained in:
291
components/book/BookList.tsx
Normal file
291
components/book/BookList.tsx
Normal file
@@ -0,0 +1,291 @@
|
||||
import React, {useContext, useEffect, useState} from "react";
|
||||
import System from "@/lib/models/System";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import SearchBook from "./SearchBook";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBook, faDownload, faGear, faTrash} from "@fortawesome/free-solid-svg-icons";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import Book, {BookListProps, BookProps} from "@/lib/models/Book";
|
||||
import BookCard from "@/components/book/BookCard";
|
||||
import BookCardSkeleton from "@/components/book/BookCardSkeleton";
|
||||
import GuideTour, {GuideStep} from "@/components/GuideTour";
|
||||
import User from "@/lib/models/User";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
export default function BookList() {
|
||||
const {session, setSession} = useContext(SessionContext);
|
||||
const accessToken: string = session?.accessToken || '';
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
const {setBook} = useContext(BookContext);
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [groupedBooks, setGroupedBooks] = useState<Record<string, BookProps[]>>({});
|
||||
const [isLoadingBooks, setIsLoadingBooks] = useState<boolean>(true);
|
||||
|
||||
const [bookGuide, setBookGuide] = useState<boolean>(false);
|
||||
|
||||
const bookGuideSteps: GuideStep[] = [
|
||||
{
|
||||
id: 0,
|
||||
targetSelector: '[data-guide="book-category"]',
|
||||
position: 'left',
|
||||
highlightRadius: -200,
|
||||
title: `${t("bookList.guideStep0Title")} ${session.user?.name}`,
|
||||
content: (
|
||||
<div>
|
||||
<p>{t("bookList.guideStep0Content")}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 1,
|
||||
targetSelector: '[data-guide="book-card"]',
|
||||
position: 'left',
|
||||
title: t("bookList.guideStep1Title"),
|
||||
content: (
|
||||
<div>
|
||||
<p>{t("bookList.guideStep1Content")}</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
targetSelector: '[data-guide="bottom-book-card"]',
|
||||
position: 'left',
|
||||
title: t("bookList.guideStep2Title"),
|
||||
content: (
|
||||
<div>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faGear} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentGear")}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faDownload} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentDownload")}
|
||||
</p>
|
||||
<p>
|
||||
<FontAwesomeIcon icon={faTrash} className="mr-2 text-primary w-5 h-5"/>
|
||||
{t("bookList.guideStep2ContentTrash")}
|
||||
</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
useEffect((): void => {
|
||||
if (groupedBooks && Object.keys(groupedBooks).length > 0 && User.guideTourDone(session.user?.guideTour || [], 'new-first-book')) {
|
||||
setBookGuide(true);
|
||||
}
|
||||
}, [groupedBooks]);
|
||||
|
||||
useEffect((): void => {
|
||||
getBooks().then()
|
||||
}, [session.user?.books]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (accessToken) getBooks().then();
|
||||
}, [accessToken]);
|
||||
|
||||
async function handleFirstBookGuide(): Promise<void> {
|
||||
try {
|
||||
const response: boolean = await System.authPostToServer<boolean>(
|
||||
'logs/tour',
|
||||
{plateforme: 'web', tour: 'new-first-book'},
|
||||
session.accessToken, lang
|
||||
);
|
||||
if (response) {
|
||||
setSession(User.setNewGuideTour(session, 'new-first-book'));
|
||||
setBookGuide(false);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorBookCreate"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getBooks(): Promise<void> {
|
||||
setIsLoadingBooks(true);
|
||||
try {
|
||||
const bookResponse: BookListProps[] = await System.authGetQueryToServer<BookListProps[]>('books', accessToken, lang);
|
||||
if (bookResponse) {
|
||||
const booksByType: Record<string, BookProps[]> = bookResponse.reduce((groups: Record<string, BookProps[]>, book: BookListProps): Record<string, BookProps[]> => {
|
||||
const imageDataUrl: string = book.coverImage ? 'data:image/jpeg;base64,' + book.coverImage : '';
|
||||
const categoryLabel: string = Book.getBookTypeLabel(book.type);
|
||||
const transformedBook: BookProps = {
|
||||
bookId: book.id,
|
||||
type: categoryLabel,
|
||||
title: book.title,
|
||||
subTitle: book.subTitle,
|
||||
summary: book.summary,
|
||||
serie: book.serieId,
|
||||
publicationDate: book.desiredReleaseDate,
|
||||
desiredWordCount: book.desiredWordCount,
|
||||
totalWordCount: 0,
|
||||
coverImage: imageDataUrl,
|
||||
};
|
||||
if (!groups[t(categoryLabel)]) {
|
||||
groups[t(categoryLabel)] = [];
|
||||
}
|
||||
groups[t(categoryLabel)].push(transformedBook);
|
||||
return groups;
|
||||
}, {});
|
||||
setGroupedBooks(booksByType);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorBooksFetch"));
|
||||
}
|
||||
} finally {
|
||||
setIsLoadingBooks(false);
|
||||
}
|
||||
}
|
||||
|
||||
const filteredGroupedBooks: Record<string, BookProps[]> = Object.entries(groupedBooks).reduce(
|
||||
(acc: Record<string, BookProps[]>, [category, books]: [string, BookProps[]]): Record<string, BookProps[]> => {
|
||||
const filteredBooks: BookProps[] = books.filter((book: BookProps): boolean =>
|
||||
book.title.toLowerCase().includes(searchQuery.toLowerCase())
|
||||
);
|
||||
|
||||
if (filteredBooks.length > 0) {
|
||||
acc[category] = filteredBooks;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
async function getBook(bookId: string): Promise<void> {
|
||||
try {
|
||||
const bookResponse: BookListProps = await System.authGetQueryToServer<BookListProps>(
|
||||
`book/basic-information`,
|
||||
accessToken,
|
||||
lang,
|
||||
{id: bookId}
|
||||
);
|
||||
if (!bookResponse) {
|
||||
errorMessage(t("bookList.errorBookDetails"));
|
||||
return;
|
||||
}
|
||||
if (setBook) {
|
||||
setBook({
|
||||
bookId: bookId,
|
||||
title: bookResponse?.title || '',
|
||||
subTitle: bookResponse?.subTitle || '',
|
||||
summary: bookResponse?.summary || '',
|
||||
type: bookResponse?.type || '',
|
||||
serie: bookResponse?.serieId,
|
||||
publicationDate: bookResponse?.desiredReleaseDate || '',
|
||||
desiredWordCount: bookResponse?.desiredWordCount || 0,
|
||||
totalWordCount: 0,
|
||||
coverImage: bookResponse?.coverImage ? 'data:image/jpeg;base64,' + bookResponse.coverImage : '',
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("bookList.errorUnknown"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex flex-col items-center h-full overflow-hidden w-full text-text-primary font-['Lora']">
|
||||
{session?.user && (
|
||||
<div data-guide="search-bar" className="w-full max-w-3xl px-4 pt-6 pb-4">
|
||||
<SearchBook searchQuery={searchQuery} setSearchQuery={setSearchQuery}/>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col w-full overflow-y-auto h-full min-h-0 flex-grow">
|
||||
{
|
||||
isLoadingBooks ? (
|
||||
<>
|
||||
<div className="text-center mb-8 px-6">
|
||||
<h1 className="font-['ADLaM_Display'] text-4xl mb-3 text-text-primary">{t("bookList.library")}</h1>
|
||||
<p className="text-muted italic text-lg">{t("bookList.booksAreMirrors")}</p>
|
||||
</div>
|
||||
|
||||
<div className="w-full mb-10">
|
||||
<div className="flex justify-between items-center w-full max-w-5xl mx-auto mb-4 px-6">
|
||||
<div className="h-8 bg-secondary/30 rounded-xl w-32 animate-pulse"></div>
|
||||
<div className="h-6 bg-secondary/20 rounded-lg w-24 animate-pulse"></div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-start w-full px-4">
|
||||
{Array.from({length: 6}).map((_, id: number) => (
|
||||
<div key={id}
|
||||
className="w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border">
|
||||
<BookCardSkeleton/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : Object.entries(filteredGroupedBooks).length > 0 ? (
|
||||
<>
|
||||
<div className="text-center mb-8 px-6">
|
||||
<h1 className="font-['ADLaM_Display'] text-4xl mb-3 text-text-primary">{t("bookList.library")}</h1>
|
||||
<p className="text-muted italic text-lg">{t("bookList.booksAreMirrors")}</p>
|
||||
</div>
|
||||
|
||||
{Object.entries(filteredGroupedBooks).map(([category, books], index) => (
|
||||
<div {...(index === 0 && {'data-guide': 'book-category'})} key={category}
|
||||
className="w-full mb-10">
|
||||
<div
|
||||
className="flex justify-between items-center w-full max-w-5xl mx-auto mb-6 px-6">
|
||||
<h2 className="text-3xl text-text-primary capitalize font-['ADLaM_Display'] flex items-center gap-3">
|
||||
<span className="w-1 h-8 bg-primary rounded-full"></span>
|
||||
{category}
|
||||
</h2>
|
||||
<span
|
||||
className="text-muted text-lg font-medium bg-secondary/30 px-4 py-1.5 rounded-full">{books.length} {t("bookList.works")}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-start w-full px-4">
|
||||
{
|
||||
books.map((book: BookProps, idx) => (
|
||||
<div key={book.bookId}
|
||||
{...(idx === 0 && {'data-guide': 'book-card'})}
|
||||
className={`w-full sm:w-1/3 md:w-1/4 lg:w-1/5 xl:w-1/6 p-2 box-border ${User.guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
|
||||
<BookCard book={book}
|
||||
onClickCallback={getBook}
|
||||
index={idx}/>
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-center p-8 max-w-lg">
|
||||
<div
|
||||
className="w-24 h-24 bg-primary/20 text-primary rounded-2xl flex items-center justify-center mx-auto mb-6 shadow-lg animate-pulse">
|
||||
<FontAwesomeIcon icon={faBook} className={'w-12 h-12'}/>
|
||||
</div>
|
||||
<h2 className="text-4xl font-['ADLaM_Display'] mb-4 text-text-primary">{t("bookList.welcomeWritingWorkshop")}</h2>
|
||||
<p className="text-muted mb-6 text-lg leading-relaxed">
|
||||
{t("bookList.whitePageText")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{
|
||||
bookGuide && <GuideTour stepId={0} steps={bookGuideSteps} onComplete={handleFirstBookGuide}
|
||||
onClose={() => setBookGuide(false)}/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user