- 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.
201 lines
8.9 KiB
TypeScript
201 lines
8.9 KiB
TypeScript
'use client';
|
|
import React, {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from "react";
|
|
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
|
import {apiPost} from '@/lib/api/client';
|
|
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
|
import {Book, Check, Layers, Pencil} from 'lucide-react';
|
|
import InputField from "@/components/form/InputField";
|
|
import TextInput from "@/components/form/TextInput";
|
|
import TextAreaInput from "@/components/form/TextAreaInput";
|
|
import Button from "@/components/ui/Button";
|
|
import Modal from "@/components/ui/Modal";
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
|
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
|
import {SyncedBook} from "@/lib/types/synced-book";
|
|
|
|
interface AddNewSeriesFormProps {
|
|
setCloseForm: Dispatch<SetStateAction<boolean>>;
|
|
onSeriesCreated?: (seriesId: string, name: string) => void;
|
|
}
|
|
|
|
export default function AddNewSeriesForm({setCloseForm, onSeriesCreated}: AddNewSeriesFormProps) {
|
|
const t = useTranslations();
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {
|
|
serverSyncedBooks,
|
|
setServerSyncedBooks
|
|
}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
|
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 ?? '';
|
|
|
|
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 response: string = await apiPost<string>(
|
|
'series/add',
|
|
{
|
|
name: name,
|
|
description: description || null,
|
|
bookIds: selectedBookIds,
|
|
},
|
|
token,
|
|
lang
|
|
);
|
|
if (!response) {
|
|
errorMessage(t('addNewSeriesForm.error.addingSeries'));
|
|
setIsAddingSeries(false);
|
|
return;
|
|
}
|
|
successMessage(t('addNewSeriesForm.success'));
|
|
|
|
if (selectedBookIds.length > 0) {
|
|
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 (
|
|
<Modal
|
|
icon={Layers}
|
|
title={t("addNewSeriesForm.title")}
|
|
onClose={(): void => setCloseForm(false)}
|
|
size="md"
|
|
footer={
|
|
<>
|
|
<Button variant="secondary" onClick={() => setCloseForm(false)}>{t("common.cancel")}</Button>
|
|
<Button
|
|
variant="primary"
|
|
onClick={handleAddSeries}
|
|
isLoading={isAddingSeries}
|
|
loadingText={t("addNewSeriesForm.adding")}
|
|
icon={Layers}
|
|
>{t("addNewSeriesForm.add")}</Button>
|
|
</>
|
|
}
|
|
>
|
|
<InputField icon={Pencil} fieldName={t("addNewSeriesForm.name")} input={
|
|
<TextInput
|
|
value={name}
|
|
setValue={(e: ChangeEvent<HTMLInputElement>): void => setName(e.target.value)}
|
|
placeholder={t("addNewSeriesForm.namePlaceholder")}
|
|
/>
|
|
}/>
|
|
|
|
<InputField
|
|
icon={Pencil}
|
|
fieldName={`${t("addNewSeriesForm.description")} (${t("addNewSeriesForm.optional")})`}
|
|
input={
|
|
<TextAreaInput
|
|
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">
|
|
<Book className="w-4 h-4 text-primary" strokeWidth={1.75}/>
|
|
<span className="font-medium">{t("addNewSeriesForm.selectBooks")}</span>
|
|
{selectedBookIds.length > 0 && (
|
|
<span className="text-sm text-muted">
|
|
({selectedBookIds.length} {t("addNewSeriesForm.selected")})
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
{serverSyncedBooks.length === 0 ? (
|
|
<div className="text-center py-6 text-muted">
|
|
<Book className="w-8 h-8 mb-2 opacity-50" strokeWidth={1.75}/>
|
|
<p>{t("addNewSeriesForm.noBooks")}</p>
|
|
</div>
|
|
) : (
|
|
<div
|
|
className="max-h-48 overflow-y-auto rounded-xl border border-secondary bg-tertiary">
|
|
{serverSyncedBooks.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-colors duration-150 border-b border-secondary last:border-b-0 ${
|
|
isSelected
|
|
? 'bg-secondary'
|
|
: 'hover:bg-secondary/50'
|
|
}`}
|
|
>
|
|
<div
|
|
className={`w-5 h-5 rounded border-2 flex items-center justify-center transition-colors ${
|
|
isSelected
|
|
? 'bg-primary/20 border-primary'
|
|
: 'border-secondary'
|
|
}`}>
|
|
{isSelected && (
|
|
<Check className="w-3 h-3 text-text-primary" strokeWidth={1.75}/>
|
|
)}
|
|
</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>
|
|
</Modal>
|
|
);
|
|
}
|