Add components for Act management and integrate Electron setup

This commit is contained in:
natreex
2025-11-16 11:00:04 -05:00
parent e192b6dcc2
commit 8167948881
97 changed files with 25378 additions and 3 deletions

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}

View 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>
);
}