- Integrated sync queue mechanisms with `LocalSyncQueueContext` for offline data handling. - Updated key sync-related services (e.g., book, chapter, series) to support offline-first functionality. - Removed redundant database fetch methods to optimize repository logic and improve maintainability. - Enhanced Tauri IPC usage for sync operations and removed legacy methods in Rust services.
654 lines
28 KiB
TypeScript
654 lines
28 KiB
TypeScript
import React, {Dispatch, SetStateAction, useContext, useState} from 'react';
|
|
import {Flag, Flame, LucideIcon, Puzzle, Scale, Trophy} from 'lucide-react';
|
|
import {Act as ActType, Incident, PlotPoint} from '@/lib/types/book';
|
|
import {ActChapter, ChapterListProps} from '@/lib/types/chapter';
|
|
import {apiDelete, 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 {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
|
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
|
import Collapse from '@/components/ui/Collapse';
|
|
import ActDescription from '@/components/book/settings/story/act/ActDescription';
|
|
import ActChaptersSection from '@/components/book/settings/story/act/ActChaptersSection';
|
|
import ActIncidents from '@/components/book/settings/story/act/ActIncidents';
|
|
import ActPlotPoints from '@/components/book/settings/story/act/ActPlotPoints';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {LangContext, LangContextProps} from "@/context/LangContext";
|
|
|
|
interface ActProps {
|
|
acts: ActType[];
|
|
setActs: Dispatch<SetStateAction<ActType[]>>;
|
|
mainChapters: ChapterListProps[];
|
|
}
|
|
|
|
export default function Act({acts, setActs, mainChapters}: ActProps) {
|
|
const t = useTranslations('actComponent');
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
|
const {addToQueue}: LocalSyncQueueContextProps = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
|
const {localSyncedBooks}: BooksSyncContextProps = useContext<BooksSyncContextProps>(BooksSyncContext);
|
|
|
|
const bookId: string | undefined = book?.bookId;
|
|
const token: string = session.accessToken;
|
|
|
|
const [expandedSections, setExpandedSections] = useState<{
|
|
[key: string]: boolean;
|
|
}>({});
|
|
|
|
const [newIncidentTitle, setNewIncidentTitle] = useState<string>('');
|
|
const [newPlotPointTitle, setNewPlotPointTitle] = useState<string>('');
|
|
const [selectedIncidentId, setSelectedIncidentId] = useState<string>('');
|
|
|
|
function toggleSection(sectionKey: string): void {
|
|
setExpandedSections(prev => ({
|
|
...prev,
|
|
[sectionKey]: !prev[sectionKey],
|
|
}));
|
|
}
|
|
|
|
function updateActSummary(actId: number, summary: string): void {
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
return {...act, summary};
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
}
|
|
|
|
function getIncidents(): Incident[] {
|
|
const act2: ActType | undefined = acts.find((act: ActType): boolean => act.id === 2);
|
|
return act2?.incidents || [];
|
|
}
|
|
|
|
async function addIncident(actId: number): Promise<void> {
|
|
if (newIncidentTitle.trim() === '') return;
|
|
|
|
try {
|
|
let incidentId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
incidentId = await tauri.addIncident(bookId ?? '', newIncidentTitle);
|
|
} else {
|
|
const addData = {bookId, name: newIncidentTitle};
|
|
incidentId = await apiPost<string>('book/incident/new', addData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('add_incident', addData);
|
|
}
|
|
}
|
|
if (!incidentId) {
|
|
errorMessage(t('errorAddIncident'));
|
|
return;
|
|
}
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
const newIncident: Incident = {
|
|
incidentId: incidentId,
|
|
title: newIncidentTitle,
|
|
summary: '',
|
|
chapters: [],
|
|
};
|
|
|
|
return {
|
|
...act,
|
|
incidents: [...(act.incidents || []), newIncident],
|
|
};
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
setNewIncidentTitle('');
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(t('errorAddIncident'));
|
|
} else {
|
|
errorMessage(t('errorUnknownAddIncident'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deleteIncident(actId: number, incidentId: string): Promise<void> {
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.removeIncident(bookId ?? '', incidentId, Date.now());
|
|
} else {
|
|
const deleteData = {bookId, incidentId};
|
|
response = await apiDelete<boolean>('book/incident/remove', deleteData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('remove_incident', {...deleteData, deletedAt: Date.now()});
|
|
}
|
|
}
|
|
if (!response) {
|
|
errorMessage(t('errorDeleteIncident'));
|
|
return;
|
|
}
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
return {
|
|
...act,
|
|
incidents: (act.incidents || []).filter(
|
|
(inc: Incident): boolean => inc.incidentId !== incidentId,
|
|
),
|
|
};
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('errorUnknownDeleteIncident'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function addPlotPoint(actId: number): Promise<void> {
|
|
if (newPlotPointTitle.trim() === '') return;
|
|
try {
|
|
let plotId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
plotId = await tauri.addPlotPoint(bookId ?? '', newPlotPointTitle, selectedIncidentId);
|
|
} else {
|
|
const plotData = {bookId, name: newPlotPointTitle, incidentId: selectedIncidentId};
|
|
plotId = await apiPost<string>('book/plot/new', plotData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('add_plot_point', plotData);
|
|
}
|
|
}
|
|
if (!plotId) {
|
|
errorMessage(t('errorAddPlotPoint'));
|
|
return;
|
|
}
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
const newPlotPoint: PlotPoint = {
|
|
plotPointId: plotId,
|
|
title: newPlotPointTitle,
|
|
summary: '',
|
|
linkedIncidentId: selectedIncidentId,
|
|
chapters: [],
|
|
};
|
|
return {
|
|
...act,
|
|
plotPoints: [...(act.plotPoints || []), newPlotPoint],
|
|
};
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
setNewPlotPointTitle('');
|
|
setSelectedIncidentId('');
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(t('errorAddPlotPoint'));
|
|
} else {
|
|
errorMessage(t('errorUnknownAddPlotPoint'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function deletePlotPoint(actId: number, plotPointId: string): Promise<void> {
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.removePlotPoint(plotPointId, bookId ?? '', Date.now());
|
|
} else {
|
|
const deleteData = {plotId: plotPointId};
|
|
response = await apiDelete<boolean>('book/plot/remove', deleteData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('remove_plot_point', {...deleteData, bookId, deletedAt: Date.now()});
|
|
}
|
|
}
|
|
if (!response) {
|
|
errorMessage(t('errorDeletePlotPoint'));
|
|
return;
|
|
}
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
return {
|
|
...act,
|
|
plotPoints: (act.plotPoints || []).filter(
|
|
(pp: PlotPoint): boolean => pp.plotPointId !== plotPointId,
|
|
),
|
|
};
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('errorUnknownDeletePlotPoint'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function linkChapter(
|
|
actId: number,
|
|
chapterId: string,
|
|
destination: 'act' | 'incident' | 'plotPoint',
|
|
itemId?: string,
|
|
): Promise<void> {
|
|
const chapterToLink: ChapterListProps | undefined = mainChapters.find((chapter: ChapterListProps): boolean => chapter.chapterId === chapterId);
|
|
if (!chapterToLink) {
|
|
errorMessage(t('errorChapterNotFound'));
|
|
return;
|
|
}
|
|
try {
|
|
let linkId: string;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
linkId = await tauri.addChapterInformation({
|
|
chapterId: chapterId,
|
|
actId: actId,
|
|
bookId: bookId ?? '',
|
|
plotId: destination === 'plotPoint' ? itemId : undefined,
|
|
incidentId: destination === 'incident' ? itemId : undefined,
|
|
});
|
|
} else {
|
|
const linkData = {bookId, chapterId, actId, plotId: destination === 'plotPoint' ? itemId : null, incidentId: destination === 'incident' ? itemId : null};
|
|
linkId = await apiPost<string>('chapter/resume/add', linkData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('add_chapter_information', linkData);
|
|
}
|
|
}
|
|
if (!linkId) {
|
|
errorMessage(t('errorLinkChapter'));
|
|
return;
|
|
}
|
|
const newChapter: ActChapter = {
|
|
chapterInfoId: linkId,
|
|
chapterId: chapterId,
|
|
title: chapterToLink.title,
|
|
chapterOrder: chapterToLink.chapterOrder || 0,
|
|
actId: actId,
|
|
incidentId: destination === 'incident' ? itemId : '0',
|
|
plotPointId: destination === 'plotPoint' ? itemId : '0',
|
|
summary: '',
|
|
goal: '',
|
|
};
|
|
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
switch (destination) {
|
|
case 'act':
|
|
return {
|
|
...act,
|
|
chapters: [...(act.chapters || []), newChapter],
|
|
};
|
|
case 'incident':
|
|
return {
|
|
...act,
|
|
incidents:
|
|
act.incidents?.map((incident: Incident): Incident =>
|
|
incident.incidentId === itemId
|
|
? {
|
|
...incident,
|
|
chapters: [...(incident.chapters || []), newChapter],
|
|
}
|
|
: incident,
|
|
) || [],
|
|
};
|
|
case 'plotPoint':
|
|
return {
|
|
...act,
|
|
plotPoints:
|
|
act.plotPoints?.map(
|
|
(plotPoint: PlotPoint): PlotPoint =>
|
|
plotPoint.plotPointId === itemId
|
|
? {
|
|
...plotPoint,
|
|
chapters: [...(plotPoint.chapters || []), newChapter],
|
|
}
|
|
: plotPoint,
|
|
) || [],
|
|
};
|
|
}
|
|
}
|
|
return act;
|
|
});
|
|
|
|
setActs(updatedActs);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('errorUnknownLinkChapter'));
|
|
}
|
|
}
|
|
}
|
|
|
|
async function unlinkChapter(
|
|
chapterInfoId: string,
|
|
actId: number,
|
|
chapterId: string,
|
|
destination: 'act' | 'incident' | 'plotPoint',
|
|
itemId?: string,
|
|
): Promise<void> {
|
|
try {
|
|
let response: boolean;
|
|
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
|
response = await tauri.removeChapterInformation(chapterInfoId, bookId ?? '', Date.now());
|
|
} else {
|
|
const removeData = {chapterInfoId};
|
|
response = await apiDelete<boolean>('chapter/resume/remove', removeData, token, lang);
|
|
|
|
if (isDesktop && bookId && localSyncedBooks.find((sb: SyncedBook): boolean => sb.id === bookId)) {
|
|
addToQueue('remove_chapter_information', {...removeData, bookId, deletedAt: Date.now()});
|
|
}
|
|
}
|
|
if (!response) {
|
|
errorMessage(t('errorUnlinkChapter'));
|
|
return;
|
|
}
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
switch (destination) {
|
|
case 'act':
|
|
return {
|
|
...act,
|
|
chapters: (act.chapters || []).filter(
|
|
(ch: ActChapter): boolean => ch.chapterId !== chapterId,
|
|
),
|
|
};
|
|
|
|
case 'incident':
|
|
if (!itemId) return act;
|
|
|
|
return {
|
|
...act,
|
|
incidents:
|
|
act.incidents?.map((incident: Incident): Incident => {
|
|
if (incident.incidentId === itemId) {
|
|
return {
|
|
...incident,
|
|
chapters: (incident.chapters || []).filter(
|
|
(ch: ActChapter): boolean =>
|
|
ch.chapterId !== chapterId,
|
|
),
|
|
};
|
|
}
|
|
return incident;
|
|
}) || [],
|
|
};
|
|
|
|
case 'plotPoint':
|
|
if (!itemId) return act;
|
|
|
|
return {
|
|
...act,
|
|
plotPoints:
|
|
act.plotPoints?.map((plotPoint: PlotPoint): PlotPoint => {
|
|
if (plotPoint.plotPointId === itemId) {
|
|
return {
|
|
...plotPoint,
|
|
chapters: (plotPoint.chapters || []).filter((chapter: ActChapter): boolean => chapter.chapterId !== chapterId),
|
|
};
|
|
}
|
|
return plotPoint;
|
|
}) || [],
|
|
};
|
|
}
|
|
}
|
|
return act;
|
|
});
|
|
|
|
setActs(updatedActs);
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
} else {
|
|
errorMessage(t('errorUnknownUnlinkChapter'));
|
|
}
|
|
}
|
|
}
|
|
|
|
function updateLinkedChapterSummary(
|
|
actId: number,
|
|
chapterId: string,
|
|
summary: string,
|
|
destination: 'act' | 'incident' | 'plotPoint',
|
|
itemId?: string,
|
|
): void {
|
|
const updatedActs: ActType[] = acts.map((act: ActType): ActType => {
|
|
if (act.id === actId) {
|
|
switch (destination) {
|
|
case 'act':
|
|
return {
|
|
...act,
|
|
chapters: (act.chapters || []).map((chapter: ActChapter): ActChapter => {
|
|
if (chapter.chapterId === chapterId) {
|
|
return {...chapter, summary};
|
|
}
|
|
return chapter;
|
|
}),
|
|
};
|
|
|
|
case 'incident':
|
|
if (!itemId) return act;
|
|
|
|
return {
|
|
...act,
|
|
incidents:
|
|
act.incidents?.map((incident: Incident): Incident => {
|
|
if (incident.incidentId === itemId) {
|
|
return {
|
|
...incident,
|
|
chapters: (incident.chapters || []).map((chapter: ActChapter) => {
|
|
if (chapter.chapterId === chapterId) {
|
|
return {...chapter, summary};
|
|
}
|
|
return chapter;
|
|
}),
|
|
};
|
|
}
|
|
return incident;
|
|
}) || [],
|
|
};
|
|
|
|
case 'plotPoint':
|
|
if (!itemId) return act;
|
|
|
|
return {
|
|
...act,
|
|
plotPoints:
|
|
act.plotPoints?.map((plotPoint: PlotPoint): PlotPoint => {
|
|
if (plotPoint.plotPointId === itemId) {
|
|
return {
|
|
...plotPoint,
|
|
chapters: (plotPoint.chapters || []).map((chapter: ActChapter): ActChapter => {
|
|
if (chapter.chapterId === chapterId) {
|
|
return {...chapter, summary};
|
|
}
|
|
return chapter;
|
|
}),
|
|
};
|
|
}
|
|
return plotPoint;
|
|
}) || [],
|
|
};
|
|
}
|
|
}
|
|
return act;
|
|
});
|
|
setActs(updatedActs);
|
|
}
|
|
|
|
function getSectionKey(actId: number, section: string): string {
|
|
return `section_${actId}_${section}`;
|
|
}
|
|
|
|
|
|
function renderActChapters(act: ActType) {
|
|
if (act.id === 2 || act.id === 3) {
|
|
return null;
|
|
}
|
|
|
|
const sectionKey: string = getSectionKey(act.id, 'chapters');
|
|
const isExpanded: boolean = expandedSections[sectionKey];
|
|
|
|
return (
|
|
<ActChaptersSection
|
|
actId={act.id}
|
|
chapters={act.chapters || []}
|
|
mainChapters={mainChapters}
|
|
onLinkChapter={(actId, chapterId) => linkChapter(actId, chapterId, 'act')}
|
|
onUpdateChapterSummary={(chapterId, summary) =>
|
|
updateLinkedChapterSummary(act.id, chapterId, summary, 'act')
|
|
}
|
|
onUnlinkChapter={(chapterInfoId, chapterId) =>
|
|
unlinkChapter(chapterInfoId, act.id, chapterId, 'act')
|
|
}
|
|
sectionKey={sectionKey}
|
|
isExpanded={isExpanded}
|
|
onToggleSection={toggleSection}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderActDescription(act: ActType) {
|
|
if (act.id === 2 || act.id === 3) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<ActDescription
|
|
actId={act.id}
|
|
summary={act.summary || ''}
|
|
onUpdateSummary={updateActSummary}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderIncidents(act: ActType) {
|
|
if (act.id !== 2) return null;
|
|
|
|
const sectionKey: string = getSectionKey(act.id, 'incidents');
|
|
const isExpanded: boolean = expandedSections[sectionKey];
|
|
|
|
return (
|
|
<ActIncidents
|
|
incidents={act.incidents || []}
|
|
actId={act.id}
|
|
mainChapters={mainChapters}
|
|
newIncidentTitle={newIncidentTitle}
|
|
setNewIncidentTitle={setNewIncidentTitle}
|
|
onAddIncident={addIncident}
|
|
onDeleteIncident={deleteIncident}
|
|
onLinkChapter={(actId, chapterId, incidentId) =>
|
|
linkChapter(actId, chapterId, 'incident', incidentId)
|
|
}
|
|
onUpdateChapterSummary={(chapterId, summary, incidentId) =>
|
|
updateLinkedChapterSummary(act.id, chapterId, summary, 'incident', incidentId)
|
|
}
|
|
onUnlinkChapter={(chapterInfoId, chapterId, incidentId) =>
|
|
unlinkChapter(chapterInfoId, act.id, chapterId, 'incident', incidentId)
|
|
}
|
|
sectionKey={sectionKey}
|
|
isExpanded={isExpanded}
|
|
onToggleSection={toggleSection}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderPlotPoints(act: ActType) {
|
|
if (act.id !== 3) return null;
|
|
|
|
const sectionKey: string = getSectionKey(act.id, 'plotPoints');
|
|
const isExpanded: boolean = expandedSections[sectionKey];
|
|
|
|
return (
|
|
<ActPlotPoints
|
|
plotPoints={act.plotPoints || []}
|
|
incidents={getIncidents()}
|
|
actId={act.id}
|
|
mainChapters={mainChapters}
|
|
newPlotPointTitle={newPlotPointTitle}
|
|
setNewPlotPointTitle={setNewPlotPointTitle}
|
|
selectedIncidentId={selectedIncidentId}
|
|
setSelectedIncidentId={setSelectedIncidentId}
|
|
onAddPlotPoint={addPlotPoint}
|
|
onDeletePlotPoint={deletePlotPoint}
|
|
onLinkChapter={(actId, chapterId, plotPointId) =>
|
|
linkChapter(actId, chapterId, 'plotPoint', plotPointId)
|
|
}
|
|
onUpdateChapterSummary={(chapterId, summary, plotPointId) =>
|
|
updateLinkedChapterSummary(act.id, chapterId, summary, 'plotPoint', plotPointId)
|
|
}
|
|
onUnlinkChapter={(chapterInfoId, chapterId, plotPointId) =>
|
|
unlinkChapter(chapterInfoId, act.id, chapterId, 'plotPoint', plotPointId)
|
|
}
|
|
sectionKey={sectionKey}
|
|
isExpanded={isExpanded}
|
|
onToggleSection={toggleSection}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function renderActIcon(actId: number): LucideIcon {
|
|
switch (actId) {
|
|
case 1:
|
|
return Flag;
|
|
case 2:
|
|
return Flame;
|
|
case 3:
|
|
return Puzzle;
|
|
case 4:
|
|
return Scale;
|
|
case 5:
|
|
return Trophy;
|
|
default:
|
|
return Flag;
|
|
}
|
|
}
|
|
|
|
function renderActTitle(actId: number): string {
|
|
switch (actId) {
|
|
case 1:
|
|
return t('act1Title');
|
|
case 2:
|
|
return t('act2Title');
|
|
case 3:
|
|
return t('act3Title');
|
|
case 4:
|
|
return t('act4Title');
|
|
case 5:
|
|
return t('act5Title');
|
|
default:
|
|
return '';
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{acts.map((act: ActType) => (
|
|
<Collapse variant="card" key={`act-${act.id}`}
|
|
title={renderActTitle(act.id)}
|
|
icon={renderActIcon(act.id)}
|
|
children={
|
|
<>
|
|
{renderActDescription(act)}
|
|
{renderActChapters(act)}
|
|
{renderIncidents(act)}
|
|
{renderPlotPoints(act)}
|
|
</>
|
|
}
|
|
/>
|
|
))}
|
|
</div>
|
|
);
|
|
} |