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>
|
||||
);
|
||||
}
|
||||
149
components/book/settings/story/Issue.tsx
Normal file
149
components/book/settings/story/Issue.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
import React, {ChangeEvent, useContext, useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faPlus, faTrash, faWarning,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Issue} from '@/lib/models/Book';
|
||||
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 {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
interface IssuesProps {
|
||||
issues: Issue[];
|
||||
setIssues: React.Dispatch<React.SetStateAction<Issue[]>>;
|
||||
}
|
||||
|
||||
export default function Issues({issues, setIssues}: IssuesProps) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage} = useContext(AlertContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [newIssueName, setNewIssueName] = useState<string>('');
|
||||
|
||||
async function addNewIssue(): Promise<void> {
|
||||
if (newIssueName.trim() === '') {
|
||||
errorMessage(t("issues.errorEmptyName"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const issueId: string = await System.authPostToServer<string>('book/issue/add', {
|
||||
bookId,
|
||||
name: newIssueName,
|
||||
}, token, lang);
|
||||
if (!issueId) {
|
||||
errorMessage(t("issues.errorAdd"));
|
||||
return;
|
||||
}
|
||||
const newIssue: Issue = {
|
||||
name: newIssueName,
|
||||
id: issueId,
|
||||
};
|
||||
|
||||
setIssues([...issues, newIssue]);
|
||||
setNewIssueName('');
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("issues.errorUnknownAdd"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function deleteIssue(issueId: string): Promise<void> {
|
||||
if (issueId === undefined) {
|
||||
errorMessage(t("issues.errorInvalidId"));
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(
|
||||
'book/issue/remove',
|
||||
{
|
||||
bookId,
|
||||
issueId,
|
||||
},
|
||||
token,
|
||||
lang
|
||||
);
|
||||
if (response) {
|
||||
const updatedIssues: Issue[] = issues.filter((issue: Issue): boolean => issue.id !== issueId,);
|
||||
setIssues(updatedIssues);
|
||||
} else {
|
||||
errorMessage(t("issues.errorDelete"));
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("issues.errorUnknownDelete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function updateIssueName(issueId: string, name: string): void {
|
||||
const updatedIssues: Issue[] = issues.map((issue: Issue): Issue => {
|
||||
if (issue.id === issueId) {
|
||||
return {...issue, name};
|
||||
}
|
||||
return issue;
|
||||
});
|
||||
setIssues(updatedIssues);
|
||||
}
|
||||
|
||||
return (
|
||||
<CollapsableArea title={t("issues.title")} children={
|
||||
<div className="p-1">
|
||||
{issues && issues.length > 0 ? (
|
||||
issues.map((item: Issue) => (
|
||||
<div
|
||||
className="mb-2 bg-secondary/30 rounded-xl p-3 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
key={`issue-${item.id}`}
|
||||
>
|
||||
<div className="flex justify-between items-center">
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={item.name}
|
||||
onChange={(e) => updateIssueName(item.id, e.target.value)}
|
||||
placeholder={t("issues.issueNamePlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="p-1.5 ml-2 rounded-lg text-error hover:bg-error/20 hover:scale-110 transition-all duration-200"
|
||||
onClick={() => deleteIssue(item.id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} size="sm"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center py-2 text-sm">
|
||||
{t("issues.noIssue")}
|
||||
</p>
|
||||
)}
|
||||
<div className="flex items-center mt-3 bg-secondary/30 p-3 rounded-xl shadow-sm">
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={newIssueName}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setNewIssueName(e.target.value)}
|
||||
placeholder={t("issues.newIssuePlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary w-9 h-9 rounded-full flex justify-center items-center ml-2 text-text-primary shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
onClick={addNewIssue}
|
||||
disabled={newIssueName.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} icon={faWarning}/>
|
||||
);
|
||||
}
|
||||
278
components/book/settings/story/MainChapter.tsx
Normal file
278
components/book/settings/story/MainChapter.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
'use client'
|
||||
|
||||
import React, {ChangeEvent, useContext, useEffect, useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faArrowDown, faArrowUp, faBookmark, faMinus, faPlus, faTrash,} from '@fortawesome/free-solid-svg-icons';
|
||||
import {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 AlertBox from "@/components/AlertBox";
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext} from "@/context/LangContext";
|
||||
|
||||
interface MainChapterProps {
|
||||
chapters: ChapterListProps[];
|
||||
setChapters: React.Dispatch<React.SetStateAction<ChapterListProps[]>>;
|
||||
}
|
||||
|
||||
export default function MainChapter({chapters, setChapters}: MainChapterProps) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext(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 [newChapterTitle, setNewChapterTitle] = useState<string>('');
|
||||
const [newChapterOrder, setNewChapterOrder] = useState<number>(0);
|
||||
|
||||
const [deleteConfirmMessage, setDeleteConfirmMessage] = useState<boolean>(false);
|
||||
const [chapterIdToRemove, setChapterIdToRemove] = useState<string>('');
|
||||
|
||||
function handleChapterTitleChange(chapterId: string, newTitle: string) {
|
||||
const updatedChapters: ChapterListProps[] = chapters.map((chapter: ChapterListProps): ChapterListProps => {
|
||||
if (chapter.chapterId === chapterId) {
|
||||
return {...chapter, title: newTitle};
|
||||
}
|
||||
return chapter;
|
||||
});
|
||||
setChapters(updatedChapters);
|
||||
}
|
||||
|
||||
function moveChapter(index: number, direction: number): void {
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number => (a.chapterOrder || 0) - (b.chapterOrder || 0));
|
||||
|
||||
const currentChapter: ChapterListProps = visibleChapters[index];
|
||||
const allChaptersIndex: number = chapters.findIndex(
|
||||
(chapter: ChapterListProps): boolean => chapter.chapterId === currentChapter.chapterId,
|
||||
);
|
||||
|
||||
const updatedChapters: ChapterListProps[] = [...chapters];
|
||||
|
||||
const currentOrder: number = updatedChapters[allChaptersIndex].chapterOrder || 0;
|
||||
const newOrder: number = Math.max(0, currentOrder + direction);
|
||||
|
||||
updatedChapters[allChaptersIndex] = {
|
||||
...updatedChapters[allChaptersIndex],
|
||||
chapterOrder: newOrder,
|
||||
};
|
||||
|
||||
setChapters(updatedChapters);
|
||||
}
|
||||
|
||||
function moveChapterUp(index: number): void {
|
||||
moveChapter(index, -1);
|
||||
}
|
||||
|
||||
function moveChapterDown(index: number): void {
|
||||
moveChapter(index, 1);
|
||||
}
|
||||
|
||||
async function deleteChapter(): Promise<void> {
|
||||
try {
|
||||
setDeleteConfirmMessage(false);
|
||||
const response: boolean = await System.authDeleteToServer<boolean>(
|
||||
'chapter/remove',
|
||||
{
|
||||
bookId,
|
||||
chapterId: chapterIdToRemove,
|
||||
},
|
||||
token,
|
||||
lang,
|
||||
);
|
||||
if (!response) {
|
||||
errorMessage(t("mainChapter.errorDelete"));
|
||||
}
|
||||
const updatedChapters: ChapterListProps[] = chapters.filter((chapter: ChapterListProps): boolean => chapter.chapterId !== chapterIdToRemove,);
|
||||
setChapters(updatedChapters);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message)
|
||||
} else {
|
||||
errorMessage(t("mainChapter.errorUnknownDelete"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewChapter(): Promise<void> {
|
||||
if (newChapterTitle.trim() === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const responseId: string = await System.authPostToServer<string>(
|
||||
'chapter/add',
|
||||
{
|
||||
bookId: bookId,
|
||||
wordsCount: 0,
|
||||
chapterOrder: newChapterOrder ? newChapterOrder : 0,
|
||||
title: newChapterTitle,
|
||||
},
|
||||
token,
|
||||
);
|
||||
if (!responseId) {
|
||||
errorMessage(t("mainChapter.errorAdd"));
|
||||
return;
|
||||
}
|
||||
const newChapter: ChapterListProps = {
|
||||
chapterId: responseId as string,
|
||||
title: newChapterTitle,
|
||||
chapterOrder: newChapterOrder,
|
||||
summary: '',
|
||||
goal: '',
|
||||
};
|
||||
setChapters([...chapters, newChapter]);
|
||||
setNewChapterTitle('');
|
||||
|
||||
setNewChapterOrder(newChapterOrder + 1);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message)
|
||||
} else {
|
||||
errorMessage(t("mainChapter.errorUnknownAdd"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decrementNewChapterOrder(): void {
|
||||
if (newChapterOrder > 0) {
|
||||
setNewChapterOrder(newChapterOrder - 1);
|
||||
}
|
||||
}
|
||||
|
||||
function incrementNewChapterOrder(): void {
|
||||
setNewChapterOrder(newChapterOrder + 1);
|
||||
}
|
||||
|
||||
useEffect((): void => {
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number =>
|
||||
(a.chapterOrder || 0) - (b.chapterOrder || 0),
|
||||
);
|
||||
|
||||
const nextOrder: number =
|
||||
visibleChapters.length > 0
|
||||
? (visibleChapters[visibleChapters.length - 1].chapterOrder || 0) + 1
|
||||
: 0;
|
||||
|
||||
setNewChapterOrder(nextOrder);
|
||||
}, [chapters]);
|
||||
|
||||
const visibleChapters: ChapterListProps[] = chapters
|
||||
.filter((chapter: ChapterListProps): boolean => chapter.chapterOrder !== -1)
|
||||
.sort((a: ChapterListProps, b: ChapterListProps): number => (a.chapterOrder || 0) - (b.chapterOrder || 0));
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CollapsableArea icon={faBookmark} title={t("mainChapter.chapters")} children={
|
||||
<div className="space-y-4">
|
||||
{visibleChapters.length > 0 ? (
|
||||
<div>
|
||||
{visibleChapters.map((item: ChapterListProps, index: number) => (
|
||||
<div key={item.chapterId}
|
||||
className="mb-2 bg-secondary/30 rounded-xl p-3 shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="bg-primary-dark text-white text-sm w-6 h-6 rounded-full text-center leading-6 mr-2">
|
||||
{item.chapterOrder !== undefined ? item.chapterOrder : index}
|
||||
</span>
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200"
|
||||
value={item.title}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => handleChapterTitleChange(item.chapterId, e.target.value)}
|
||||
placeholder={t("mainChapter.chapterTitlePlaceholder")}
|
||||
/>
|
||||
<div className="flex ml-1">
|
||||
<button
|
||||
className={`p-1.5 mx-0.5 rounded-lg hover:bg-secondary/50 transition-all duration-200 ${
|
||||
item.chapterOrder === 0 ? 'text-muted cursor-not-allowed' : 'text-primary hover:scale-110'
|
||||
}`}
|
||||
onClick={(): void => moveChapterUp(index)}
|
||||
disabled={item.chapterOrder === 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowUp} size="sm"/>
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 mx-0.5 rounded-lg text-primary hover:bg-secondary/50 hover:scale-110 transition-all duration-200"
|
||||
onClick={(): void => moveChapterDown(index)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faArrowDown} size="sm"/>
|
||||
</button>
|
||||
<button
|
||||
className="p-1.5 mx-0.5 rounded-lg text-error hover:bg-error/20 hover:scale-110 transition-all duration-200"
|
||||
onClick={(): void => {
|
||||
setChapterIdToRemove(item.chapterId);
|
||||
setDeleteConfirmMessage(true);
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} size="sm"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center py-2">
|
||||
{t("mainChapter.noChapter")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="bg-secondary/30 rounded-xl p-3 mt-3 shadow-sm">
|
||||
<div className="flex items-center">
|
||||
<div
|
||||
className="flex items-center gap-1 bg-secondary/50 rounded-lg mr-2 px-1 py-0.5 shadow-inner">
|
||||
<button
|
||||
className={`w-6 h-6 rounded-full bg-secondary flex justify-center items-center hover:scale-110 transition-all duration-200 ${
|
||||
newChapterOrder <= 0 ? 'text-muted cursor-not-allowed opacity-50' : 'text-primary hover:bg-secondary-dark'
|
||||
}`}
|
||||
onClick={decrementNewChapterOrder}
|
||||
disabled={newChapterOrder <= 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faMinus} size="xs"/>
|
||||
</button>
|
||||
<span
|
||||
className="bg-primary-dark text-white text-sm w-6 h-6 rounded-full text-center leading-6 mx-0.5">
|
||||
{newChapterOrder}
|
||||
</span>
|
||||
<button
|
||||
className="w-6 h-6 rounded-full bg-secondary flex justify-center items-center text-primary hover:bg-secondary-dark hover:scale-110 transition-all duration-200"
|
||||
onClick={incrementNewChapterOrder}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} size="xs"/>
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
className="flex-1 text-text-primary text-base px-2 py-1 bg-transparent border-b border-secondary/50 focus:outline-none focus:border-primary transition-colors duration-200 placeholder:text-muted/60"
|
||||
value={newChapterTitle}
|
||||
onChange={e => setNewChapterTitle(e.target.value)}
|
||||
placeholder={t("mainChapter.newChapterPlaceholder")}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary w-9 h-9 rounded-full flex justify-center items-center ml-2 text-text-primary shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:scale-100"
|
||||
onClick={addNewChapter}
|
||||
disabled={newChapterTitle.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className={'w-5 h-5'}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}/>
|
||||
{
|
||||
deleteConfirmMessage &&
|
||||
<AlertBox title={t("mainChapter.deleteTitle")} message={t("mainChapter.deleteMessage")}
|
||||
type={"danger"} onConfirm={deleteChapter}
|
||||
onCancel={(): void => setDeleteConfirmMessage(false)}/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
167
components/book/settings/story/StorySetting.tsx
Normal file
167
components/book/settings/story/StorySetting.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
'use client'
|
||||
|
||||
import React, {createContext, forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
import {SessionContext} from '@/context/SessionContext';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import System from '@/lib/models/System';
|
||||
import {Act as ActType, Issue} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import MainChapter from "@/components/book/settings/story/MainChapter";
|
||||
import Issues from "@/components/book/settings/story/Issue";
|
||||
import Act from "@/components/book/settings/story/Act";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
|
||||
export const StoryContext = createContext<{
|
||||
acts: ActType[];
|
||||
setActs: React.Dispatch<React.SetStateAction<ActType[]>>;
|
||||
mainChapters: ChapterListProps[];
|
||||
setMainChapters: React.Dispatch<React.SetStateAction<ChapterListProps[]>>;
|
||||
issues: Issue[];
|
||||
setIssues: React.Dispatch<React.SetStateAction<Issue[]>>;
|
||||
}>({
|
||||
acts: [],
|
||||
setActs: (): void => {
|
||||
},
|
||||
mainChapters: [],
|
||||
setMainChapters: (): void => {
|
||||
},
|
||||
issues: [],
|
||||
setIssues: (): void => {
|
||||
},
|
||||
});
|
||||
|
||||
interface StoryFetchData {
|
||||
mainChapter: ChapterListProps[];
|
||||
acts: ActType[];
|
||||
issues: Issue[];
|
||||
}
|
||||
|
||||
export function Story(props: any, ref: any) {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const bookId: string = book?.bookId ? book.bookId.toString() : '';
|
||||
const {session} = useContext(SessionContext);
|
||||
const userToken: string = session.accessToken;
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
const [acts, setActs] = useState<ActType[]>([]);
|
||||
const [issues, setIssues] = useState<Issue[]>([]);
|
||||
const [mainChapters, setMainChapters] = useState<ChapterListProps[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleSave
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getStoryData().then();
|
||||
}, [userToken]);
|
||||
|
||||
useEffect((): void => {
|
||||
cleanupDeletedChapters();
|
||||
}, [mainChapters]);
|
||||
|
||||
async function getStoryData(): Promise<void> {
|
||||
try {
|
||||
const response: StoryFetchData = await System.authGetQueryToServer<StoryFetchData>(`book/story`, userToken, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
if (response) {
|
||||
setActs(response.acts);
|
||||
setMainChapters(response.mainChapter);
|
||||
setIssues(response.issues);
|
||||
setIsLoading(false);
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
setIsLoading(false);
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("story.errorUnknownFetch"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function cleanupDeletedChapters(): void {
|
||||
const existingChapterIds: string[] = mainChapters.map(ch => ch.chapterId);
|
||||
|
||||
const updatedActs = acts.map((act: ActType) => {
|
||||
const filteredChapters: ActChapter[] = act.chapters?.filter((chapter: ActChapter): boolean =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || [];
|
||||
const updatedIncidents = act.incidents?.map(incident => {
|
||||
return {
|
||||
...incident,
|
||||
chapters: incident.chapters?.filter(chapter =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || []
|
||||
};
|
||||
}) || [];
|
||||
const updatedPlotPoints = act.plotPoints?.map(plotPoint => {
|
||||
return {
|
||||
...plotPoint,
|
||||
chapters: plotPoint.chapters?.filter(chapter =>
|
||||
existingChapterIds.includes(chapter.chapterId)) || []
|
||||
};
|
||||
}) || [];
|
||||
return {
|
||||
...act,
|
||||
chapters: filteredChapters,
|
||||
incidents: updatedIncidents,
|
||||
plotPoints: updatedPlotPoints,
|
||||
};
|
||||
});
|
||||
setActs(updatedActs);
|
||||
}
|
||||
|
||||
async function handleSave(): Promise<void> {
|
||||
try {
|
||||
const response: boolean =
|
||||
await System.authPostToServer<boolean>('book/story', {
|
||||
bookId,
|
||||
acts,
|
||||
mainChapters,
|
||||
issues,
|
||||
}, userToken, lang);
|
||||
if (!response) {
|
||||
errorMessage(t("story.errorSave"))
|
||||
}
|
||||
successMessage(t("story.successSave"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("story.errorUnknownSave"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<StoryContext.Provider
|
||||
value={{
|
||||
acts,
|
||||
setActs,
|
||||
mainChapters,
|
||||
setMainChapters,
|
||||
issues,
|
||||
setIssues,
|
||||
}}>
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="flex-grow overflow-auto py-4">
|
||||
<div className="space-y-6 px-4">
|
||||
<MainChapter chapters={mainChapters} setChapters={setMainChapters}/>
|
||||
<div className="space-y-4">
|
||||
<Act acts={acts} setActs={setActs} mainChapters={mainChapters}/>
|
||||
</div>
|
||||
<Issues issues={issues} setIssues={setIssues}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</StoryContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(Story);
|
||||
37
components/book/settings/story/act/ActChapter.tsx
Normal file
37
components/book/settings/story/act/ActChapter.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React, {ChangeEvent} from 'react';
|
||||
import {faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {ActChapter} from '@/lib/models/Chapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import TexteAreaInput from '@/components/form/TexteAreaInput';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActChapterItemProps {
|
||||
chapter: ActChapter;
|
||||
onUpdateSummary: (chapterId: string, summary: string) => void;
|
||||
onUnlink: (chapterInfoId: string, chapterId: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export default function ActChapterItem({chapter, onUpdateSummary, onUnlink}: ActChapterItemProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="bg-secondary/20 p-4 rounded-xl mb-3 border border-secondary/30 shadow-sm hover:shadow-md transition-all duration-200">
|
||||
<InputField
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={chapter.summary || ''}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onUpdateSummary(chapter.chapterId, e.target.value)
|
||||
}
|
||||
placeholder={t('chapterSummaryPlaceholder')}
|
||||
/>
|
||||
}
|
||||
actionIcon={faTrash}
|
||||
fieldName={chapter.title}
|
||||
action={(): Promise<void> => onUnlink(chapter.chapterInfoId, chapter.chapterId)}
|
||||
actionLabel={t('remove')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
93
components/book/settings/story/act/ActChaptersSection.tsx
Normal file
93
components/book/settings/story/act/ActChaptersSection.tsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp} from '@fortawesome/free-solid-svg-icons';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import {SelectBoxProps} from '@/shared/interface';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import SelectBox from '@/components/form/SelectBox';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActChaptersSectionProps {
|
||||
actId: number;
|
||||
chapters: ActChapter[];
|
||||
mainChapters: ChapterListProps[];
|
||||
onLinkChapter: (actId: number, chapterId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActChaptersSection({
|
||||
actId,
|
||||
chapters,
|
||||
mainChapters,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActChaptersSectionProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function mainChaptersData(): SelectBoxProps[] {
|
||||
return mainChapters.map((chapter: ChapterListProps): SelectBoxProps => ({
|
||||
value: chapter.chapterId,
|
||||
label: `${chapter.chapterOrder}. ${chapter.title}`,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('chapters')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{chapters && chapters.length > 0 ? (
|
||||
chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`chapter-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId)
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
<InputField
|
||||
addButtonCallBack={(): Promise<void> => onLinkChapter(actId, selectedChapterId)}
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={null}
|
||||
onChangeCallBack={(e) => setSelectedChapterId(e.target.value)}
|
||||
data={mainChaptersData()}
|
||||
placeholder={t('selectChapterPlaceholder')}
|
||||
/>
|
||||
}
|
||||
isAddButtonDisabled={!selectedChapterId}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
60
components/book/settings/story/act/ActDescription.tsx
Normal file
60
components/book/settings/story/act/ActDescription.tsx
Normal file
@@ -0,0 +1,60 @@
|
||||
import React, {ChangeEvent} from 'react';
|
||||
import {faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import TexteAreaInput from '@/components/form/TexteAreaInput';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActDescriptionProps {
|
||||
actId: number;
|
||||
summary: string;
|
||||
onUpdateSummary: (actId: number, summary: string) => void;
|
||||
}
|
||||
|
||||
export default function ActDescription({actId, summary, onUpdateSummary}: ActDescriptionProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
|
||||
function getActSummaryTitle(actId: number): string {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return t('act1Summary');
|
||||
case 4:
|
||||
return t('act4Summary');
|
||||
case 5:
|
||||
return t('act5Summary');
|
||||
default:
|
||||
return t('actSummary');
|
||||
}
|
||||
}
|
||||
|
||||
function getActSummaryPlaceholder(actId: number): string {
|
||||
switch (actId) {
|
||||
case 1:
|
||||
return t('act1SummaryPlaceholder');
|
||||
case 4:
|
||||
return t('act4SummaryPlaceholder');
|
||||
case 5:
|
||||
return t('act5SummaryPlaceholder');
|
||||
default:
|
||||
return t('actSummaryPlaceholder');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<InputField
|
||||
fieldName={getActSummaryTitle(actId)}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={summary || ''}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) =>
|
||||
onUpdateSummary(actId, e.target.value)
|
||||
}
|
||||
placeholder={getActSummaryPlaceholder(actId)}
|
||||
/>
|
||||
}
|
||||
actionIcon={faTrash}
|
||||
actionLabel={t('delete')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
176
components/book/settings/story/act/ActIncidents.tsx
Normal file
176
components/book/settings/story/act/ActIncidents.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Incident} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActIncidentsProps {
|
||||
incidents: Incident[];
|
||||
actId: number;
|
||||
mainChapters: ChapterListProps[];
|
||||
newIncidentTitle: string;
|
||||
setNewIncidentTitle: (title: string) => void;
|
||||
onAddIncident: (actId: number) => Promise<void>;
|
||||
onDeleteIncident: (actId: number, incidentId: string) => Promise<void>;
|
||||
onLinkChapter: (actId: number, chapterId: string, incidentId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string, incidentId: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string, incidentId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActIncidents({
|
||||
incidents,
|
||||
actId,
|
||||
mainChapters,
|
||||
newIncidentTitle,
|
||||
setNewIncidentTitle,
|
||||
onAddIncident,
|
||||
onDeleteIncident,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActIncidentsProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({});
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function toggleItem(itemKey: string): void {
|
||||
setExpandedItems(prev => ({
|
||||
...prev,
|
||||
[itemKey]: !prev[itemKey],
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('incidentsTitle')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{incidents && incidents.length > 0 ? (
|
||||
<>
|
||||
{incidents.map((item: Incident) => {
|
||||
const itemKey = `incident_${item.incidentId}`;
|
||||
const isItemExpanded: boolean = expandedItems[itemKey];
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`incident-${item.incidentId}`}
|
||||
className="bg-secondary/30 rounded-xl mb-3 overflow-hidden border border-secondary/40 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<button
|
||||
className="flex justify-between items-center w-full p-2 text-left"
|
||||
onClick={(): void => toggleItem(itemKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{item.title}</span>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={isItemExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5 mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
await onDeleteIncident(actId, item.incidentId);
|
||||
}}
|
||||
className="text-error hover:bg-error/20 p-1.5 rounded-lg transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isItemExpanded && (
|
||||
<div className="p-3 bg-secondary/20">
|
||||
{item.chapters && item.chapters.length > 0 ? (
|
||||
<>
|
||||
{item.chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`inc-chapter-${chapter.chapterId}-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary, item.incidentId)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId, item.incidentId)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<select
|
||||
onChange={(e) => setSelectedChapterId(e.target.value)}
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200"
|
||||
>
|
||||
<option value="">{t('selectChapterPlaceholder')}</option>
|
||||
{mainChapters.map((chapter: ChapterListProps) => (
|
||||
<option key={chapter.chapterId} value={chapter.chapterId}>
|
||||
{`${chapter.chapterOrder}. ${chapter.title}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={(): Promise<void> =>
|
||||
onLinkChapter(actId, selectedChapterId, item.incidentId)
|
||||
}
|
||||
disabled={selectedChapterId.length === 0}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noIncidentAdded')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200 placeholder:text-muted/60"
|
||||
value={newIncidentTitle}
|
||||
onChange={(e) => setNewIncidentTitle(e.target.value)}
|
||||
placeholder={t('newIncidentPlaceholder')}
|
||||
/>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={(): Promise<void> => onAddIncident(actId)}
|
||||
disabled={newIncidentTitle.trim() === ''}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
202
components/book/settings/story/act/ActPlotPoints.tsx
Normal file
202
components/book/settings/story/act/ActPlotPoints.tsx
Normal file
@@ -0,0 +1,202 @@
|
||||
import React, {useState} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faChevronDown, faChevronUp, faPlus, faTrash} from '@fortawesome/free-solid-svg-icons';
|
||||
import {Incident, PlotPoint} from '@/lib/models/Book';
|
||||
import {ActChapter, ChapterListProps} from '@/lib/models/Chapter';
|
||||
import {SelectBoxProps} from '@/shared/interface';
|
||||
import ActChapterItem from './ActChapter';
|
||||
import InputField from '@/components/form/InputField';
|
||||
import SelectBox from '@/components/form/SelectBox';
|
||||
import {useTranslations} from 'next-intl';
|
||||
|
||||
interface ActPlotPointsProps {
|
||||
plotPoints: PlotPoint[];
|
||||
incidents: Incident[];
|
||||
actId: number;
|
||||
mainChapters: ChapterListProps[];
|
||||
newPlotPointTitle: string;
|
||||
setNewPlotPointTitle: (title: string) => void;
|
||||
selectedIncidentId: string;
|
||||
setSelectedIncidentId: (id: string) => void;
|
||||
onAddPlotPoint: (actId: number) => Promise<void>;
|
||||
onDeletePlotPoint: (actId: number, plotPointId: string) => Promise<void>;
|
||||
onLinkChapter: (actId: number, chapterId: string, plotPointId: string) => Promise<void>;
|
||||
onUpdateChapterSummary: (chapterId: string, summary: string, plotPointId: string) => void;
|
||||
onUnlinkChapter: (chapterInfoId: string, chapterId: string, plotPointId: string) => Promise<void>;
|
||||
sectionKey: string;
|
||||
isExpanded: boolean;
|
||||
onToggleSection: (sectionKey: string) => void;
|
||||
}
|
||||
|
||||
export default function ActPlotPoints({
|
||||
plotPoints,
|
||||
incidents,
|
||||
actId,
|
||||
mainChapters,
|
||||
newPlotPointTitle,
|
||||
setNewPlotPointTitle,
|
||||
selectedIncidentId,
|
||||
setSelectedIncidentId,
|
||||
onAddPlotPoint,
|
||||
onDeletePlotPoint,
|
||||
onLinkChapter,
|
||||
onUpdateChapterSummary,
|
||||
onUnlinkChapter,
|
||||
sectionKey,
|
||||
isExpanded,
|
||||
onToggleSection,
|
||||
}: ActPlotPointsProps) {
|
||||
const t = useTranslations('actComponent');
|
||||
const [expandedItems, setExpandedItems] = useState<{ [key: string]: boolean }>({});
|
||||
const [selectedChapterId, setSelectedChapterId] = useState<string>('');
|
||||
|
||||
function toggleItem(itemKey: string): void {
|
||||
setExpandedItems(prev => ({
|
||||
...prev,
|
||||
[itemKey]: !prev[itemKey],
|
||||
}));
|
||||
}
|
||||
|
||||
function getIncidentData(): SelectBoxProps[] {
|
||||
return incidents.map((incident: Incident): SelectBoxProps => ({
|
||||
value: incident.incidentId,
|
||||
label: incident.title,
|
||||
}));
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<button
|
||||
className="flex justify-between items-center w-full bg-secondary/50 p-3 rounded-xl text-left hover:bg-secondary transition-all duration-200 shadow-sm"
|
||||
onClick={(): void => onToggleSection(sectionKey)}
|
||||
>
|
||||
<span className="font-bold text-text-primary">{t('plotPointsTitle')}</span>
|
||||
<FontAwesomeIcon
|
||||
icon={isExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5"
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isExpanded && (
|
||||
<div className="p-2">
|
||||
{plotPoints && plotPoints.length > 0 ? (
|
||||
plotPoints.map((item: PlotPoint) => {
|
||||
const itemKey = `plotpoint_${item.plotPointId}`;
|
||||
const isItemExpanded: boolean = expandedItems[itemKey];
|
||||
const linkedIncident: Incident | undefined = incidents.find(
|
||||
(inc: Incident): boolean => inc.incidentId === item.linkedIncidentId
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={`plot-point-${item.plotPointId}`}
|
||||
className="bg-secondary/30 rounded-xl mb-3 overflow-hidden border border-secondary/40 shadow-sm hover:shadow-md transition-all duration-200"
|
||||
>
|
||||
<button
|
||||
className="flex justify-between items-center w-full p-2 text-left"
|
||||
onClick={(): void => toggleItem(itemKey)}
|
||||
>
|
||||
<div>
|
||||
<p className="font-bold text-text-primary">{item.title}</p>
|
||||
{linkedIncident && (
|
||||
<p className="text-text-secondary text-sm italic">
|
||||
{t('linkedTo')}: {linkedIncident.title}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<FontAwesomeIcon
|
||||
icon={isItemExpanded ? faChevronUp : faChevronDown}
|
||||
className="text-text-primary w-3.5 h-3.5 mr-2"
|
||||
/>
|
||||
<button
|
||||
onClick={async (e): Promise<void> => {
|
||||
e.stopPropagation();
|
||||
await onDeletePlotPoint(actId, item.plotPointId);
|
||||
}}
|
||||
className="text-error hover:bg-error/20 p-1.5 rounded-lg transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{isItemExpanded && (
|
||||
<div className="p-3 bg-secondary/20">
|
||||
{item.chapters && item.chapters.length > 0 ? (
|
||||
item.chapters.map((chapter: ActChapter) => (
|
||||
<ActChapterItem
|
||||
key={`plot-chapter-${chapter.chapterId}-${chapter.chapterInfoId}`}
|
||||
chapter={chapter}
|
||||
onUpdateSummary={(chapterId, summary) =>
|
||||
onUpdateChapterSummary(chapterId, summary, item.plotPointId)
|
||||
}
|
||||
onUnlink={(chapterInfoId, chapterId) =>
|
||||
onUnlinkChapter(chapterInfoId, chapterId, item.plotPointId)
|
||||
}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noLinkedChapter')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<select
|
||||
onChange={(e) => setSelectedChapterId(e.target.value)}
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 mr-2 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200"
|
||||
>
|
||||
<option value="">{t('selectChapterPlaceholder')}</option>
|
||||
{mainChapters.map((chapter: ChapterListProps) => (
|
||||
<option key={chapter.chapterId} value={chapter.chapterId}>
|
||||
{`${chapter.chapterOrder}. ${chapter.title}`}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<button
|
||||
className="bg-primary text-text-primary w-9 h-9 rounded-full flex items-center justify-center disabled:opacity-50 disabled:cursor-not-allowed shadow-md hover:shadow-lg hover:scale-110 hover:bg-primary-dark transition-all duration-200"
|
||||
onClick={() => onLinkChapter(actId, selectedChapterId, item.plotPointId)}
|
||||
disabled={!selectedChapterId}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="w-3.5 h-3.5"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<p className="text-text-secondary text-center text-sm p-2">
|
||||
{t('noPlotPointAdded')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="mt-2 space-y-2">
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="text"
|
||||
className="flex-1 bg-secondary/50 text-text-primary rounded-xl px-4 py-2.5 border border-secondary/50 focus:outline-none focus:ring-4 focus:ring-primary/20 focus:border-primary hover:bg-secondary hover:border-secondary transition-all duration-200 placeholder:text-muted/60"
|
||||
value={newPlotPointTitle}
|
||||
onChange={(e) => setNewPlotPointTitle(e.target.value)}
|
||||
placeholder={t('newPlotPointPlaceholder')}
|
||||
/>
|
||||
</div>
|
||||
<InputField
|
||||
input={
|
||||
<SelectBox
|
||||
defaultValue={``}
|
||||
onChangeCallBack={(e) => setSelectedIncidentId(e.target.value)}
|
||||
data={getIncidentData()}
|
||||
/>
|
||||
}
|
||||
addButtonCallBack={(): Promise<void> => onAddPlotPoint(actId)}
|
||||
isAddButtonDisabled={newPlotPointTitle.trim() === ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user