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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user