Files
ERitors-Scribe-Desktop/components/series/AddNewSeriesForm.tsx
natreex ee4438834c Migrate from window.electron to tauri IPC functions across components
- Replaced `window.electron.invoke` calls with equivalent `tauri` function calls for all IPC interactions.
- Removed `electron.d.ts` TypeScript definitions as they are no longer needed.
- Updated related logic for offline/online state synchronization.
- Added `types.rs` and `shared/mod.rs` modules to support Tauri IPC integration with Rust enums and shared logic.
- Refactored IPC request queues to use updated handler names for consistency with Tauri.
2026-03-21 09:34:13 -04:00

260 lines
13 KiB
TypeScript

'use client';
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useEffect, useRef, useState} from "react";
import {AlertContext} from "@/context/AlertContext";
import System from "@/lib/models/System";
import {SessionContext} from "@/context/SessionContext";
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faBook, faCheck, faLayerGroup, faPencilAlt, faX} from "@fortawesome/free-solid-svg-icons";
import InputField from "@/components/form/InputField";
import TextInput from "@/components/form/TextInput";
import TexteAreaInput from "@/components/form/TexteAreaInput";
import CancelButton from "@/components/form/CancelButton";
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
import {useTranslations} from "next-intl";
import {LangContext, LangContextProps} from "@/context/LangContext";
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
import {SyncedBook} from "@/lib/models/SyncedBook";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {SeriesSyncContext, SeriesSyncContextProps} from "@/context/SeriesSyncContext";
import {SyncedSeries, SyncedSeriesBook} from "@/lib/models/SyncedSeries";
import * as tauri from '@/lib/tauri';
interface AddNewSeriesFormProps {
setCloseForm: Dispatch<SetStateAction<boolean>>;
onSeriesCreated?: (seriesId: string, name: string) => void;
}
export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNewSeriesFormProps) {
const t = useTranslations();
const {lang} = useContext<LangContextProps>(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage, successMessage} = useContext(AlertContext);
const {serverSyncedBooks, setServerSyncedBooks, localSyncedBooks, setLocalSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {serverSyncedSeries, localSyncedSeries} = useContext<SeriesSyncContextProps>(SeriesSyncContext);
// Get all bookIds already in a series
const booksAlreadyInSeries: Set<string> = new Set(
(isCurrentlyOffline() ? localSyncedSeries : serverSyncedSeries)
.flatMap((series: SyncedSeries): string[] =>
series.books.map((book: SyncedSeriesBook): string => book.bookId)
)
);
const modalRef: React.RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
const [name, setName] = useState<string>('');
const [description, setDescription] = useState<string>('');
const [selectedBookIds, setSelectedBookIds] = useState<string[]>([]);
const [isAddingSeries, setIsAddingSeries] = useState<boolean>(false);
const token: string = session?.accessToken ?? '';
useEffect((): () => void => {
document.body.style.overflow = 'hidden';
return (): void => {
document.body.style.overflow = 'auto';
};
}, []);
function toggleBookSelection(bookId: string): void {
setSelectedBookIds((prev: string[]): string[] => {
if (prev.includes(bookId)) {
return prev.filter((id: string): boolean => id !== bookId);
}
return [...prev, bookId];
});
}
async function handleAddSeries(): Promise<void> {
if (!name) {
errorMessage(t('addNewSeriesForm.error.nameMissing'));
return;
}
if (name.length < 2) {
errorMessage(t('addNewSeriesForm.error.nameTooShort'));
return;
}
if (name.length > 100) {
errorMessage(t('addNewSeriesForm.error.nameTooLong'));
return;
}
setIsAddingSeries(true);
try {
const createData = {
name: name,
description: description || null,
bookIds: selectedBookIds,
};
let response: string;
if (isCurrentlyOffline()) {
response = await tauri.createSeries(createData);
} else {
response = await System.authPostToServer<string>(
'series/add',
createData,
token,
lang
);
}
if (!response) {
errorMessage(t('addNewSeriesForm.error.addingSeries'));
setIsAddingSeries(false);
return;
}
successMessage(t('addNewSeriesForm.success'));
if (selectedBookIds.length > 0) {
if (isCurrentlyOffline()) {
setLocalSyncedBooks((prev: SyncedBook[]): SyncedBook[] =>
prev.map((book: SyncedBook): SyncedBook =>
selectedBookIds.includes(book.id)
? {...book, seriesId: response}
: book
)
);
} else {
setServerSyncedBooks((prev: SyncedBook[]): SyncedBook[] =>
prev.map((book: SyncedBook): SyncedBook =>
selectedBookIds.includes(book.id)
? {...book, seriesId: response}
: book
)
);
}
}
if (onSeriesCreated) {
onSeriesCreated(response, name);
}
setIsAddingSeries(false);
setCloseForm(false);
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
} else {
errorMessage(t('addNewSeriesForm.error.addingSeries'));
}
setIsAddingSeries(false);
}
}
return (
<div
className="fixed inset-0 flex items-center justify-center bg-black/60 z-50 backdrop-blur-md animate-fadeIn">
<div ref={modalRef}
className="bg-tertiary/95 backdrop-blur-sm text-text-primary rounded-2xl border border-secondary/50 shadow-2xl md:w-3/4 xl:w-2/5 lg:w-2/4 sm:w-11/12 max-h-[85vh] flex flex-col">
<div className="flex justify-between items-center bg-primary px-6 py-4 rounded-t-2xl shadow-lg">
<h2 className="flex items-center gap-3 font-['ADLaM_Display'] text-2xl text-text-primary">
<FontAwesomeIcon icon={faLayerGroup} className="w-6 h-6"/>
{t("addNewSeriesForm.title")}
</h2>
<button
className="text-background hover:text-background w-10 h-10 rounded-xl hover:bg-white/20 transition-all duration-200 flex items-center justify-center hover:scale-110"
onClick={(): void => setCloseForm(false)}
>
<FontAwesomeIcon icon={faX} className={'w-5 h-5'}/>
</button>
</div>
<div className="p-5 overflow-y-auto flex-grow custom-scrollbar">
<div className="space-y-6">
<InputField icon={faPencilAlt} fieldName={t("addNewSeriesForm.name")} input={
<TextInput
value={name}
setValue={(e: ChangeEvent<HTMLInputElement>): void => setName(e.target.value)}
placeholder={t("addNewSeriesForm.namePlaceholder")}
/>
}/>
<InputField
icon={faPencilAlt}
fieldName={`${t("addNewSeriesForm.description")} (${t("addNewSeriesForm.optional")})`}
input={
<TexteAreaInput
value={description}
setValue={(e: ChangeEvent<HTMLTextAreaElement>): void => setDescription(e.target.value)}
placeholder={t("addNewSeriesForm.descriptionPlaceholder")}
/>
}
/>
<div className="space-y-3">
<div className="flex items-center gap-2 text-text-primary">
<FontAwesomeIcon icon={faBook} className="w-4 h-4 text-primary"/>
<span className="font-medium">{t("addNewSeriesForm.selectBooks")}</span>
{selectedBookIds.length > 0 && (
<span className="text-sm text-muted">
({selectedBookIds.length} {t("addNewSeriesForm.selected")})
</span>
)}
</div>
{(isCurrentlyOffline() ? localSyncedBooks : serverSyncedBooks)
.filter((book: SyncedBook): boolean => !booksAlreadyInSeries.has(book.id)).length === 0 ? (
<div className="text-center py-6 text-muted">
<FontAwesomeIcon icon={faBook} className="w-8 h-8 mb-2 opacity-50"/>
<p>{t("addNewSeriesForm.noBooks")}</p>
</div>
) : (
<div className="max-h-48 overflow-y-auto rounded-xl border border-secondary/50 bg-secondary/20">
{(isCurrentlyOffline() ? localSyncedBooks : serverSyncedBooks)
.filter((book: SyncedBook): boolean => !booksAlreadyInSeries.has(book.id))
.map((book: SyncedBook) => {
const isSelected: boolean = selectedBookIds.includes(book.id);
return (
<button
key={book.id}
type="button"
onClick={(): void => toggleBookSelection(book.id)}
className={`w-full flex items-center gap-3 px-4 py-3 transition-all duration-200 border-b border-secondary/30 last:border-b-0 ${
isSelected
? 'bg-primary/20 hover:bg-primary/30'
: 'hover:bg-secondary/50'
}`}
>
<div className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-all ${
isSelected
? 'bg-primary border-primary'
: 'border-secondary/50'
}`}>
{isSelected && (
<FontAwesomeIcon icon={faCheck} className="w-3 h-3 text-white"/>
)}
</div>
<div className="flex-1 text-left">
<div className="text-text-primary font-medium">{book.title}</div>
{book.subTitle && (
<div className="text-sm text-muted">{book.subTitle}</div>
)}
</div>
</button>
);
})}
</div>
)}
</div>
</div>
</div>
<div
className="flex justify-between items-center p-5 border-t border-secondary/50 bg-secondary/20 rounded-b-2xl">
<div></div>
<div className="flex gap-3">
<CancelButton callBackFunction={() => setCloseForm(false)}/>
<SubmitButtonWLoading
callBackAction={handleAddSeries}
isLoading={isAddingSeries}
text={t("addNewSeriesForm.add")}
loadingText={t("addNewSeriesForm.adding")}
icon={faLayerGroup}
/>
</div>
</div>
</div>
</div>
);
}