Add components for Act management and integrate Electron setup
This commit is contained in:
608
components/book/settings/story/Act.tsx
Normal file
608
components/book/settings/story/Act.tsx
Normal file
@@ -0,0 +1,608 @@
|
||||
import React, {Dispatch, SetStateAction, useContext, useState} from 'react';
|
||||
import {
|
||||
faFire,
|
||||
faFlag,
|
||||
faPuzzlePiece,
|
||||
faScaleBalanced,
|
||||
faTrophy,
|
||||
IconDefinition,
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Act as ActType, Incident, PlotPoint} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import System from '@/lib/models/System';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import CollapsableArea from '@/components/CollapsableArea';
|
||||
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 'next-intl';
|
||||
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} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
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 {
|
||||
const incidentId: string =
|
||||
await System.authPostToServer<string>('book/incident/new', {
|
||||
bookId,
|
||||
name: newIncidentTitle,
|
||||
}, token, lang);
|
||||
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 {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('book/incident/remove', {
|
||||
bookId,
|
||||
incidentId,
|
||||
}, token, lang);
|
||||
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 {
|
||||
const plotId: string = await System.authPostToServer<string>('book/plot/new', {
|
||||
bookId,
|
||||
name: newPlotPointTitle,
|
||||
incidentId: selectedIncidentId,
|
||||
}, token, lang);
|
||||
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 {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('book/plot/remove', {
|
||||
plotId: plotPointId,
|
||||
}, token, lang);
|
||||
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 {
|
||||
const linkId: string =
|
||||
await System.authPostToServer<string>('chapter/resume/add', {
|
||||
bookId,
|
||||
chapterId: chapterId,
|
||||
actId: actId,
|
||||
plotId: destination === 'plotPoint' ? itemId : null,
|
||||
incidentId: destination === 'incident' ? itemId : null,
|
||||
}, token, lang);
|
||||
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 {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>('chapter/resume/remove', {
|
||||
chapterInfoId,
|
||||
}, token, lang);
|
||||
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): IconDefinition {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return faFlag;
|
||||
case 2:
|
||||
return faFire;
|
||||
case 3:
|
||||
return faPuzzlePiece;
|
||||
case 4:
|
||||
return faScaleBalanced;
|
||||
case 5:
|
||||
return faTrophy;
|
||||
default:
|
||||
return faFlag;
|
||||
}
|
||||
}
|
||||
|
||||
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) => (
|
||||
<CollapsableArea key={`act-${act.id}`}
|
||||
title={renderActTitle(act.id)}
|
||||
icon={renderActIcon(act.id)}
|
||||
children={
|
||||
<>
|
||||
{renderActDescription(act)}
|
||||
{renderActChapters(act)}
|
||||
{renderIncidents(act)}
|
||||
{renderPlotPoints(act)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user