Remove unused components and models for improved maintainability

- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files.
- Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter.
- Updated project structure and references to reflect these removals.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

View File

@@ -1,91 +1,64 @@
'use client'
import {forwardRef, useContext, useEffect, useImperativeHandle, useState} from "react";
import System from "@/lib/models/System";
import {AlertContext} from "@/context/AlertContext";
import {SessionContext} from "@/context/SessionContext";
import {useTranslations} from "next-intl";
import {apiDelete, apiGet, apiPost, apiPut} from '@/lib/api/client';
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
import {useTranslations} from '@/lib/i18n';
import {LangContext, LangContextProps} from "@/context/LangContext";
import {SeriesContext, SeriesContextProps} from "@/context/SeriesContext";
import {SeriesBookProps} from "@/lib/models/Series";
import {SeriesBookProps} from "@/lib/types/series";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faArrowDown, faArrowUp, faBook, faPlus, faSpinner, faTrash} from "@fortawesome/free-solid-svg-icons";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
import {SyncedSeries, SyncedSeriesBook} from "@/lib/models/SyncedSeries";
import * as tauri from '@/lib/tauri';
import {SyncedBook} from "@/lib/types/synced-book";
import {ArrowDown, ArrowUp, Book, Trash2} from 'lucide-react';
import PulseLoader from '@/components/ui/PulseLoader';
import InputField from '@/components/form/InputField';
import SelectBox from '@/components/form/SelectBox';
import IconButton from "@/components/ui/IconButton";
import EmptyState from "@/components/ui/EmptyState";
import Badge from "@/components/ui/Badge";
import EntityListItem from "@/components/ui/EntityListItem";
function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext);
const {session} = useContext(SessionContext);
const {seriesId, localSeries} = useContext<SeriesContextProps>(SeriesContext);
const {serverSyncedBooks, setServerSyncedBooks, localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {seriesId}: SeriesContextProps = useContext<SeriesContextProps>(SeriesContext);
const {
serverSyncedBooks,
setServerSyncedBooks
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
const userToken: string = session?.accessToken ? session?.accessToken : '';
const {errorMessage, successMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
const {localSyncedSeries, serverSyncedSeries} = useContext<SeriesSyncContextProps>(SeriesSyncContext);
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const [isLoading, setIsLoading] = useState<boolean>(true);
const [seriesBooks, setSeriesBooks] = useState<SeriesBookProps[]>([]);
const [selectedBookToAdd, setSelectedBookToAdd] = useState<string>('');
const [availableBooks, setAvailableBooks] = useState<SyncedBook[]>([]);
useEffect(function () {
if (seriesId) {
loadSeriesBooks();
}
}, [seriesId]);
useEffect(function () {
const booksInThisSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId);
let allBooks: SyncedBook[];
let allSeries: SyncedSeries[];
if (isCurrentlyOffline() || localSeries) {
allBooks = localSyncedBooks;
allSeries = localSyncedSeries;
} else {
allBooks = serverSyncedBooks;
allSeries = serverSyncedSeries;
}
// Get all bookIds in OTHER series (not this one)
const booksInOtherSeries: Set<string> = new Set(
allSeries
.filter((series: SyncedSeries): boolean => series.id !== seriesId)
.flatMap((series: SyncedSeries): string[] =>
series.books.map((book: SyncedSeriesBook): string => book.bookId)
)
);
// Filter out books already in this series AND books already in another series
const filteredBooks: SyncedBook[] = allBooks.filter(
(book: SyncedBook) => !booksInThisSeries.includes(book.id) && !booksInOtherSeries.has(book.id)
const booksInSeries: string[] = seriesBooks.map((book: SeriesBookProps) => book.bookId);
const filteredBooks: SyncedBook[] = serverSyncedBooks.filter(
(book: SyncedBook) => !booksInSeries.includes(book.id)
);
setAvailableBooks(filteredBooks);
}, [seriesBooks, serverSyncedBooks, localSyncedBooks, serverSyncedSeries, localSyncedSeries, isCurrentlyOffline, localSeries, seriesId]);
}, [seriesBooks, serverSyncedBooks]);
async function loadSeriesBooks(): Promise<void> {
setIsLoading(true);
try {
let response: SeriesBookProps[];
if (isCurrentlyOffline() || localSeries) {
response = await tauri.getSeriesBooks(seriesId);
} else {
response = await System.authGetQueryToServer<SeriesBookProps[]>(
'series/book/list',
userToken,
lang,
{seriesid: seriesId}
);
}
const response: SeriesBookProps[] = await apiGet<SeriesBookProps[]>(
'series/book/list',
userToken,
lang,
{seriesid: seriesId}
);
if (response) {
setSeriesBooks(response);
}
@@ -99,48 +72,36 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
setIsLoading(false);
}
}
useImperativeHandle(ref, function () {
return {
handleSave: handleSave
};
});
async function handleSave(): Promise<void> {
successMessage(t('seriesBooks.success.saved'));
}
async function handleAddBook(): Promise<void> {
if (!selectedBookToAdd) {
errorMessage(t('seriesBooks.error.selectBook'));
return;
}
try {
const addData = {
seriesId: seriesId,
bookId: selectedBookToAdd
};
let response: boolean;
if (isCurrentlyOffline() || localSeries) {
response = await tauri.addBookToSeries(addData.seriesId, addData.bookId);
} else {
response = await System.authPostToServer<boolean>(
'series/book/add',
addData,
userToken,
lang
);
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
addToQueue('add_book_to_series', {data: addData});
}
}
const response: boolean = await apiPost<boolean>(
'series/book/add',
{
seriesId: seriesId,
bookId: selectedBookToAdd
},
userToken,
lang
);
if (response) {
const allBooks: SyncedBook[] = isCurrentlyOffline() || localSeries ? localSyncedBooks : serverSyncedBooks;
const addedBook: SyncedBook | undefined = allBooks.find(
const addedBook: SyncedBook | undefined = serverSyncedBooks.find(
(book: SyncedBook) => book.id === selectedBookToAdd
);
if (addedBook) {
@@ -151,7 +112,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
coverImage: null
};
setSeriesBooks([...seriesBooks, newSeriesBook]);
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] =>
prev.map((book: SyncedBook): SyncedBook =>
book.id === selectedBookToAdd
@@ -170,31 +131,19 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
}
}
}
async function handleRemoveBook(bookId: string): Promise<void> {
try {
const removeData = {
seriesId: seriesId,
bookId: bookId,
deletedAt: System.timeStampInSeconds(),
};
let response: boolean;
if (isCurrentlyOffline() || localSeries) {
response = await tauri.removeBookFromSeries(removeData.seriesId, removeData.bookId, removeData.deletedAt);
} else {
response = await System.authDeleteToServer<boolean>(
'series/book/remove',
removeData,
userToken,
lang
);
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
addToQueue('remove_book_from_series', {data: removeData});
}
}
const response: boolean = await apiDelete<boolean>(
'series/book/remove',
{
seriesId: seriesId,
bookId: bookId
},
userToken,
lang
);
if (response) {
const updatedBooks: SeriesBookProps[] = seriesBooks
.filter((book: SeriesBookProps) => book.bookId !== bookId)
@@ -203,7 +152,7 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
order: index + 1
}));
setSeriesBooks(updatedBooks);
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] =>
prev.map((book: SyncedBook): SyncedBook =>
book.id === bookId
@@ -220,48 +169,37 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
}
}
}
async function handleMoveBook(bookId: string, direction: 'up' | 'down'): Promise<void> {
const currentIndex: number = seriesBooks.findIndex((book: SeriesBookProps) => book.bookId === bookId);
if (currentIndex === -1) return;
const newIndex: number = direction === 'up' ? currentIndex - 1 : currentIndex + 1;
if (newIndex < 0 || newIndex >= seriesBooks.length) return;
const reorderedBooks: SeriesBookProps[] = [...seriesBooks];
const [movedBook] = reorderedBooks.splice(currentIndex, 1);
reorderedBooks.splice(newIndex, 0, movedBook);
const updatedBooks: SeriesBookProps[] = reorderedBooks.map((book: SeriesBookProps, index: number) => ({
...book,
order: index + 1
}));
try {
const reorderData = {
seriesId: seriesId,
booksOrder: updatedBooks.map((book: SeriesBookProps) => ({
bookId: book.bookId,
order: book.order
}))
};
let response: boolean;
if (isCurrentlyOffline() || localSeries) {
response = await tauri.reorderSeriesBooks(reorderData.seriesId, reorderData.booksOrder);
} else {
response = await System.authPutToServer<boolean>(
'series/book/reorder',
reorderData,
userToken,
lang
);
if (localSyncedSeries.find((s: SyncedSeries): boolean => s.id === seriesId)) {
addToQueue('reorder_series_books', {data: reorderData});
}
}
const response: boolean = await apiPut<boolean>(
'series/book/reorder',
{
seriesId: seriesId,
booksOrder: updatedBooks.map((book: SeriesBookProps) => ({
bookId: book.bookId,
order: book.order
}))
},
userToken,
lang
);
if (response) {
setSeriesBooks(updatedBooks);
}
@@ -273,96 +211,74 @@ function SeriesBooksManager(props: object, ref: React.Ref<{ handleSave: () => Pr
}
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<FontAwesomeIcon icon={faSpinner} className="w-8 h-8 text-primary animate-spin"/>
</div>
);
return <PulseLoader/>;
}
return (
<div className="space-y-6">
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
<h3 className="text-lg font-semibold text-text-primary mb-4">
{t('seriesBooks.addBook')}
</h3>
<div className="flex gap-3">
<select
value={selectedBookToAdd}
onChange={(e) => setSelectedBookToAdd(e.target.value)}
className="flex-1 bg-secondary/50 border border-secondary/50 rounded-lg px-4 py-2 text-text-primary focus:outline-none focus:border-primary"
>
<option value="">{t('seriesBooks.selectBookPlaceholder')}</option>
{availableBooks.map((book: SyncedBook) => (
<option key={book.id} value={book.id}>
{book.title}
</option>
))}
</select>
<button
onClick={handleAddBook}
disabled={!selectedBookToAdd}
className="bg-primary hover:bg-primary-dark disabled:bg-secondary disabled:cursor-not-allowed text-white px-4 py-2 rounded-lg transition-colors duration-200 flex items-center gap-2"
>
<FontAwesomeIcon icon={faPlus} className="w-4 h-4"/>
{t('seriesBooks.add')}
</button>
</div>
</div>
<div className="bg-tertiary/90 backdrop-blur-sm rounded-xl p-5 border border-secondary/50 shadow-md">
<h3 className="text-lg font-semibold text-text-primary mb-4">
<InputField
fieldName={t('seriesBooks.addBook')}
input={
<SelectBox
onChangeCallBack={(e) => setSelectedBookToAdd(e.target.value)}
data={availableBooks.map((book: SyncedBook) => ({label: book.title, value: book.id}))}
defaultValue={selectedBookToAdd}
placeholder={t('seriesBooks.selectBookPlaceholder')}
/>
}
addButtonCallBack={handleAddBook}
isAddButtonDisabled={!selectedBookToAdd}
/>
<div>
<p className="text-text-secondary text-sm font-medium mb-3">
{t('seriesBooks.booksInSeries')} ({seriesBooks.length})
</h3>
</p>
{seriesBooks.length === 0 ? (
<div className="text-center py-8 text-text-secondary">
<FontAwesomeIcon icon={faBook} className="w-12 h-12 mb-4 opacity-50"/>
<p>{t('seriesBooks.noBooks')}</p>
</div>
<EmptyState icon={Book} title={t('seriesBooks.noBooks')}/>
) : (
<div className="space-y-2">
{seriesBooks
.sort((a: SeriesBookProps, b: SeriesBookProps) => a.order - b.order)
.map((book: SeriesBookProps, index: number) => (
<div
<EntityListItem
key={book.bookId}
className="flex items-center justify-between bg-secondary/30 rounded-lg p-3 border border-secondary/30 hover:border-primary/30 transition-colors duration-200"
>
<div className="flex items-center gap-3">
<span className="bg-primary/20 text-primary font-bold w-8 h-8 rounded-full flex items-center justify-center text-sm">
{book.order}
</span>
<span className="text-text-primary font-medium">{book.title}</span>
</div>
<div className="flex items-center gap-2">
<button
onClick={() => handleMoveBook(book.bookId, 'up')}
disabled={index === 0}
className="p-2 rounded-lg hover:bg-secondary/50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors duration-200"
title={t('seriesBooks.moveUp')}
>
<FontAwesomeIcon icon={faArrowUp} className="w-4 h-4 text-text-secondary"/>
</button>
<button
onClick={() => handleMoveBook(book.bookId, 'down')}
disabled={index === seriesBooks.length - 1}
className="p-2 rounded-lg hover:bg-secondary/50 disabled:opacity-30 disabled:cursor-not-allowed transition-colors duration-200"
title={t('seriesBooks.moveDown')}
>
<FontAwesomeIcon icon={faArrowDown} className="w-4 h-4 text-text-secondary"/>
</button>
<button
onClick={() => handleRemoveBook(book.bookId)}
className="p-2 rounded-lg hover:bg-error/20 text-error transition-colors duration-200"
title={t('seriesBooks.removeBook')}
>
<FontAwesomeIcon icon={faTrash} className="w-4 h-4"/>
</button>
</div>
</div>
onClick={function (): void {
}}
size="sm"
avatar={<Badge variant="primary" size="sm">{book.order}</Badge>}
title={book.title}
extra={
<div className="flex items-center gap-1">
<IconButton
icon={ArrowUp}
variant="ghost"
size="sm"
onClick={() => handleMoveBook(book.bookId, 'up')}
disabled={index === 0}
tooltip={t('seriesBooks.moveUp')}
/>
<IconButton
icon={ArrowDown}
variant="ghost"
size="sm"
onClick={() => handleMoveBook(book.bookId, 'down')}
disabled={index === seriesBooks.length - 1}
tooltip={t('seriesBooks.moveDown')}
/>
<IconButton
icon={Trash2}
variant="danger"
size="sm"
onClick={() => handleRemoveBook(book.bookId)}
tooltip={t('seriesBooks.removeBook')}
/>
</div>
}
/>
))}
</div>
)}