Add terms of use translations, sync detection, and refactor book components

- Introduced new translations for terms of use in French and English locales.
- Added sync status detection logic for books in `BookList` and `BookCard` components.
- Refactored `BookCard` to handle additional props and improve layout flexibility.
- Enhanced `TermsOfUse` component with complete localization support and refuse functionality.
- Updated data decryption logic in Rust services to handle optional fields and additional metadata consistently.
- Improved offline/online synchronization workflows with extended context properties.
This commit is contained in:
natreex
2026-03-23 11:56:35 -04:00
parent 64ed90d993
commit a114592ac9
23 changed files with 588 additions and 438 deletions

View File

@@ -1,58 +1,65 @@
import {Link} from '@/lib/navigation';
import React from "react";
import {BookProps} from "@/lib/types/book";
import DeleteBook from "@/components/book/settings/DeleteBook";
import {useTranslations} from '@/lib/i18n';
export default function BookCard(
{
book,
onClickCallback,
index
}: {
book: BookProps,
onClickCallback: Function;
index: number;
}) {
const t = useTranslations();
return (
<Link href={`/book/${book.bookId}`}>
<div
className="group relative aspect-[2/3] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 hover:ring-1 hover:ring-text-primary/20">
{book.coverImage ? (
<img
src={book.coverImage}
alt={book.title || t("bookCard.noCoverAlt")}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-secondary flex items-center justify-center">
<span className="text-muted text-5xl font-['ADLaM_Display']">
{book.title.charAt(0).toUpperCase()}
</span>
</div>
)}
<div className="absolute inset-x-0 bottom-0 bg-darkest-background/70 p-3">
<h3 className="text-text-primary font-bold text-sm truncate">
{book.title}
</h3>
{book.subTitle && (
<p className="text-text-secondary text-xs truncate mt-0.5">
{book.subTitle}
</p>
)}
</div>
<div
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
onClick={(e: React.MouseEvent): void => e.preventDefault()}
{...(index === 0 && {'data-guide': 'bottom-book-card'})}
>
<DeleteBook bookId={book.bookId}/>
</div>
</div>
</Link>
)
}
import React from "react";
import {isDesktop} from '@/lib/configs';
import {BookProps} from "@/lib/types/book";
import DeleteBook from "@/components/book/settings/DeleteBook";
import SyncBook from "@/components/SyncBook";
import {SyncType} from "@/context/BooksSyncContext";
import {useTranslations} from '@/lib/i18n';
interface BookCardProps {
book: BookProps;
onClickCallback: (bookId: string) => void;
index: number;
syncStatus?: SyncType;
}
export default function BookCard({book, onClickCallback, index, syncStatus}: BookCardProps) {
const t = useTranslations();
return (
<div
className="group relative aspect-[2/3] rounded-xl overflow-hidden cursor-pointer transition-all duration-300 hover:ring-1 hover:ring-text-primary/20">
<button onClick={(): void => onClickCallback(book.bookId)} className="w-full h-full text-left block"
type="button">
{book.coverImage ? (
<img
src={book.coverImage}
alt={book.title || t("bookCard.noCoverAlt")}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full bg-secondary flex items-center justify-center">
<span className="text-muted text-5xl font-['ADLaM_Display']">
{book.title.charAt(0).toUpperCase()}
</span>
</div>
)}
</button>
{isDesktop && syncStatus && (
<div className="absolute top-2 left-2 cursor-default" onClick={(e: React.MouseEvent): void => e.stopPropagation()}>
<SyncBook status={syncStatus} bookId={book.bookId}/>
</div>
)}
<div className="absolute inset-x-0 bottom-0 bg-darkest-background/70 p-3">
<h3 className="text-text-primary font-bold text-sm truncate">
{book.title}
</h3>
{book.subTitle && (
<p className="text-text-secondary text-xs truncate mt-0.5">
{book.subTitle}
</p>
)}
</div>
<div
className="absolute top-2 right-2 opacity-0 group-hover:opacity-100 transition-opacity duration-200"
onClick={(e: React.MouseEvent): void => e.stopPropagation()}
{...(index === 0 && {'data-guide': 'bottom-book-card'})}
>
<DeleteBook bookId={book.bookId}/>
</div>
</div>
)
}

View File

@@ -17,7 +17,8 @@ import GuideTour, {GuideStep} from "@/components/GuideTour";
import {guideTourDone, setNewGuideTour} from "@/lib/utils/user";
import {useTranslations} from '@/lib/i18n';
import {LangContext, LangContextProps} from "@/context/LangContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {BooksSyncContext, BooksSyncContextProps, SyncType} from "@/context/BooksSyncContext";
import {BookSyncCompare, SyncedBook} from "@/lib/types/synced-book";
import {SeriesListItemProps} from "@/lib/types/series";
import SeriesCard, {SeriesCardProps} from "@/components/series/SeriesCard";
import SeriesSetting from "@/components/series/SeriesSetting";
@@ -35,8 +36,11 @@ export default function BookList() {
const router = useRouter();
const t = useTranslations();
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
const {serverSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext)
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const {
serverSyncedBooks, booksToSyncFromServer, booksToSyncToServer,
serverOnlyBooks, localOnlyBooks
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const {isCurrentlyOffline, offlineMode}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
const [searchQuery, setSearchQuery] = useState<string>('');
const [groupedItems, setGroupedItems] = useState<Record<string, CategoryItem[]>>({});
@@ -95,27 +99,38 @@ export default function BookList() {
]
useEffect((): void => {
if (groupedItems && Object.keys(groupedItems).length > 0 && guideTourDone(session.user?.guideTour || [], 'new-first-book')) {
setBookGuide(true);
if (groupedItems && Object.keys(groupedItems).length > 0) {
const notDone: boolean = isCurrentlyOffline()
? localStorage.getItem('guide-tour-new-first-book') !== 'true'
: guideTourDone(session.user?.guideTour || [], 'new-first-book');
if (notDone) setBookGuide(true);
}
}, [groupedItems]);
useEffect((): void => {
loadBooksAndSeries().then()
}, [serverSyncedBooks]);
const canLoad: boolean = !isDesktop ||
(!isCurrentlyOffline() || offlineMode.isDatabaseInitialized);
if (canLoad) loadBooksAndSeries().then();
}, [serverSyncedBooks, offlineMode.isDatabaseInitialized, booksToSyncFromServer, booksToSyncToServer, serverOnlyBooks, localOnlyBooks]);
useEffect((): void => {
if (accessToken) loadBooksAndSeries().then();
}, [accessToken]);
async function handleFirstBookGuide(): Promise<void> {
if (isCurrentlyOffline()) {
localStorage.setItem('guide-tour-new-first-book', 'true');
setBookGuide(false);
return;
}
try {
const response: boolean = await apiPost<boolean>(
'logs/tour',
{plateforme: 'web', tour: 'new-first-book'},
{plateforme: 'desktop', tour: 'new-first-book'},
session.accessToken, lang
);
if (response) {
localStorage.setItem('guide-tour-new-first-book', 'true');
setSession(setNewGuideTour(session, 'new-first-book'));
setBookGuide(false);
}
@@ -291,6 +306,23 @@ export default function BookList() {
}, 0);
}
function detectBookSyncStatus(bookId: string): SyncType {
if (!isDesktop || isCurrentlyOffline()) return 'synced';
if (serverOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId)) {
return 'server-only';
}
if (localOnlyBooks.find((book: SyncedBook): boolean => book.id === bookId)) {
return 'local-only';
}
if (booksToSyncFromServer.find((book: BookSyncCompare): boolean => book.id === bookId)) {
return 'to-sync-from-server';
}
if (booksToSyncToServer.find((book: BookSyncCompare): boolean => book.id === bookId)) {
return 'to-sync-to-server';
}
return 'synced';
}
function handleBookClick(bookId: string): void {
router.push(`/book/${bookId}`);
}
@@ -388,11 +420,12 @@ export default function BookList() {
return (
<div key={item.book.bookId}
{...(idx === 0 && {'data-guide': 'book-card'})}
className={`flex-shrink-0 w-64 sm:w-52 md:w-48 lg:w-56 xl:w-64 p-2 box-border ${guideTourDone(session.user?.guideTour || [], 'new-first-book') && 'mb-[200px]'}`}>
className={`flex-shrink-0 w-64 sm:w-52 md:w-48 lg:w-56 xl:w-64 p-2 box-border ${bookGuide && 'mb-[200px]'}`}>
<BookCard
book={item.book}
onClickCallback={handleBookClick}
index={idx}
syncStatus={detectBookSyncStatus(item.book.bookId)}
/>
</div>
);