import {ChapterListProps} from "@/lib/types/chapter"; import React, {useContext, useEffect, useRef, useState} from "react"; import {apiDelete, apiGet, apiPost} from '@/lib/api/client'; import {isDesktop} from '@/lib/configs'; import * as tauri from '@/lib/tauri'; import OfflineContext, {OfflineContextType} from '@/context/OfflineContext'; import {BooksSyncContext, BooksSyncContextProps} from '@/context/BooksSyncContext'; import {SyncedBook} from '@/lib/types/synced-book'; import {LocalSyncQueueContext, LocalSyncQueueContextProps} from '@/context/SyncQueueContext'; import {BookContext, BookContextProps} from "@/context/BookContext"; import {AlertContext, AlertContextProps} from "@/context/AlertContext"; import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext"; import {SessionContext, SessionContextProps} from "@/context/SessionContext"; import {useRouter} from "@/lib/navigation"; import {FileText} from "lucide-react"; import ListItem from "@/components/ui/ListItem"; import AlertBox from "@/components/ui/AlertBox"; import {useTranslations} from '@/lib/i18n'; import InlineAddInput from "@/components/form/InlineAddInput"; import {LangContext, LangContextProps} from "@/context/LangContext"; export default function ScribeChapterComponent() { const t = useTranslations(); const {lang}: LangContextProps = useContext(LangContext); const {book}: BookContextProps = useContext(BookContext); const {chapter}: ChapterContextProps = useContext(ChapterContext); const {errorMessage, successMessage}: AlertContextProps = useContext(AlertContext); const {session}: SessionContextProps = useContext(SessionContext); const userToken: string = session?.accessToken ? session?.accessToken : ''; const {isCurrentlyOffline}: OfflineContextType = useContext(OfflineContext); const {addToQueue}: LocalSyncQueueContextProps = useContext(LocalSyncQueueContext); const {localSyncedBooks}: BooksSyncContextProps = useContext(BooksSyncContext); const router = useRouter(); const [chapters, setChapters] = useState([]) const [newChapterName, setNewChapterName] = useState(''); const [newChapterOrder, setNewChapterOrder] = useState(1); const [deleteConfirmationMessage, setDeleteConfirmationMessage] = useState(false); const [removeChapterId, setRemoveChapterId] = useState(''); const chapterRefs: React.RefObject> = useRef>(new Map()); const scrollContainerRef: React.RefObject = useRef(null); useEffect((): void => { if (book) { getChapterList().then(); } }, [book]); useEffect((): void => { setNewChapterOrder(getNextChapterOrder()); }, [chapters]); useEffect((): void => { if (chapter?.chapterId && scrollContainerRef.current) { setTimeout((): void => { const element: HTMLDivElement | undefined = chapterRefs.current.get(chapter.chapterId); const container: HTMLUListElement | null = scrollContainerRef.current; if (element && container) { const containerRect: DOMRect = container.getBoundingClientRect(); const elementRect: DOMRect = element.getBoundingClientRect(); const relativeTop: number = elementRect.top - containerRect.top + container.scrollTop; const scrollPosition: number = relativeTop - (containerRect.height / 2) + (elementRect.height / 2); container.scrollTo({ top: Math.max(0, scrollPosition), behavior: 'smooth' }); } }, 100); } }, [chapter?.chapterId]); function getNextChapterOrder(): number { const maxOrder: number = Math.max(0, ...chapters.map((chap: ChapterListProps) => chap.chapterOrder ?? 0)); return maxOrder + 1; } async function getChapterList(): Promise { try { let response: ChapterListProps[]; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.getChapters(book?.bookId ?? '') as ChapterListProps[]; } else { response = await apiGet(`book/chapters?id=${book?.bookId}`, userToken, lang); } if (response) { setChapters(response); } } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("scribeChapterComponent.errorFetchChapters")); } } } function navigateToChapter(chapterId: string): void { router.push(`/book/${book?.bookId}/chapter/${chapterId}`); } async function handleChapterUpdate(chapterId: string, title: string, chapterOrder: number): Promise { try { let response: boolean; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.updateChapter(chapterId, title, chapterOrder); } else { const updateData = {chapterId, chapterOrder, title}; response = await apiPost('chapter/update', updateData, userToken, lang); if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { addToQueue('update_chapter', updateData); } } if (!response) { errorMessage(t("scribeChapterComponent.errorChapterUpdate")); return; } successMessage(t("scribeChapterComponent.successUpdate")); setChapters((prevState: ChapterListProps[]): ChapterListProps[] => { return prevState.map((chapter: ChapterListProps): ChapterListProps => { if (chapter.chapterId === chapterId) { chapter.chapterOrder = chapterOrder; chapter.title = title; } return chapter; }); }); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("scribeChapterComponent.errorChapterUpdateEn")); } } } async function handleDeleteConfirmation(chapterId: string): Promise { setDeleteConfirmationMessage(true); setRemoveChapterId(chapterId); } async function handleDeleteChapter(): Promise { try { setDeleteConfirmationMessage(false); let response: boolean; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { response = await tauri.removeChapter(removeChapterId, book?.bookId ?? '', Date.now()); } else { const deleteData = {bookId: book?.bookId, chapterId: removeChapterId}; response = await apiDelete('chapter/remove', deleteData, userToken, lang); if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { addToQueue('remove_chapter', {...deleteData, deletedAt: Date.now()}); } } if (!response) { errorMessage(t("scribeChapterComponent.errorChapterDelete")); return; } const updatedChapters: ChapterListProps[] = chapters.filter( (chapter: ChapterListProps): boolean => chapter.chapterId !== removeChapterId, ); setChapters(updatedChapters); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("scribeChapterComponent.unknownErrorChapterDelete")); } } } async function handleAddChapter(chapterOrder: number): Promise { if (!newChapterName && chapterOrder >= 0) { errorMessage(t("scribeChapterComponent.errorChapterNameRequired")); return; } const chapterTitle: string = chapterOrder >= 0 ? newChapterName : (book?.title ?? ''); try { let chapterId: string; if (isDesktop && (isCurrentlyOffline() || book?.localBook)) { chapterId = await tauri.addChapter({ bookId: book?.bookId ?? '', title: chapterTitle, chapterOrder: chapterOrder, }); } else { const addData = {bookId: book?.bookId, chapterOrder, title: chapterTitle}; chapterId = await apiPost('chapter/add', addData, userToken, lang); if (isDesktop && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === book?.bookId)) { addToQueue('add_chapter', addData); } } if (!chapterId) { errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName})); return; } const newChapter: ChapterListProps = { chapterId: chapterId, title: chapterTitle, chapterOrder: chapterOrder } setChapters((prevState: ChapterListProps[]): ChapterListProps[] => { return [newChapter, ...prevState] }) navigateToChapter(chapterId); } catch (e: unknown) { if (e instanceof Error) { errorMessage(e.message); } else { errorMessage(t("scribeChapterComponent.errorChapterSubmit", {chapterName: newChapterName})); } } } return (

{t("scribeChapterComponent.sheetHeading")}

    { chapters.filter((chap: ChapterListProps): boolean => { return chap.chapterOrder !== undefined && chap.chapterOrder < 0; }) .sort((a: ChapterListProps, b: ChapterListProps): number => { const aOrder: number = a.chapterOrder ?? 0; const bOrder: number = b.chapterOrder ?? 0; return aOrder - bOrder; }).map((chap: ChapterListProps): React.JSX.Element => (
    { if (el) { chapterRefs.current.set(chap.chapterId, el); } else { chapterRefs.current.delete(chap.chapterId); } }}> navigateToChapter(chap.chapterId)} selectedId={chapter?.chapterId ?? ''} id={chap.chapterId} text={chap.title}/>
    )) } { chapters.filter((chap: ChapterListProps): boolean => { return chap.chapterOrder !== undefined && chap.chapterOrder < 0; }).length === 0 &&
  • => handleAddChapter(-1)} className="group p-3 rounded-xl hover:bg-tertiary cursor-pointer transition-colors duration-150"> {t("scribeChapterComponent.createSheet")}
  • }

{t("scribeChapterComponent.chaptersHeading")}

    { chapters.filter((chap: ChapterListProps): boolean => { return !(chap.chapterOrder && chap.chapterOrder < 0); }) .sort((a: ChapterListProps, b: ChapterListProps): number => { const aOrder: number = a.chapterOrder ?? 0; const bOrder: number = b.chapterOrder ?? 0; return aOrder - bOrder; }).map((chap: ChapterListProps): React.JSX.Element => (
    { if (el) { chapterRefs.current.set(chap.chapterId, el); } else { chapterRefs.current.delete(chap.chapterId); } }}> navigateToChapter(chap.chapterId)} isEditable={true} handleUpdate={handleChapterUpdate} handleDelete={handleDeleteConfirmation} selectedId={chapter?.chapterId ?? ''} id={chap.chapterId} text={chap.title} numericalIdentifier={chap.chapterOrder} onReorder={(chapterId: string, newOrder: number): void => { setChapters((previousChapters: ChapterListProps[]): ChapterListProps[] => previousChapters.map((chapter: ChapterListProps): ChapterListProps => chapter.chapterId === chapterId ? { ...chapter, chapterOrder: newOrder } : chapter ) ); }}/>
    )) }
=> { await handleAddChapter(newChapterOrder); setNewChapterName(""); }} showNumericalInput={true} />
{ deleteConfirmationMessage && => handleDeleteChapter()} onCancel={(): void => setDeleteConfirmationMessage(false)}/> }
) }