- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files. - Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter. - Updated project structure and references to reflect these removals.
396 lines
20 KiB
TypeScript
396 lines
20 KiB
TypeScript
'use client';
|
|
import React, {Dispatch, SetStateAction, useContext, useEffect, useState} from 'react';
|
|
import {
|
|
Attribute,
|
|
CharacterAttribute,
|
|
CharacterAttributeSection,
|
|
CharacterElement,
|
|
CharacterProps,
|
|
isCharacterCategory,
|
|
isCharacterStatus,
|
|
} from '@/lib/types/character';
|
|
import {
|
|
advancedCharacterElements,
|
|
basicCharacterElements,
|
|
characterCategories,
|
|
characterStatus
|
|
} from '@/lib/constants/character';
|
|
import {SeriesCharacterProps} from '@/lib/types/series';
|
|
import InputField from '@/components/form/InputField';
|
|
import TextInput from '@/components/form/TextInput';
|
|
import TextAreaInput from '@/components/form/TextAreaInput';
|
|
import NumberInput from '@/components/form/NumberInput';
|
|
import SelectBox from '@/components/form/SelectBox';
|
|
import CharacterSectionElement from '@/components/book/settings/characters/CharacterSectionElement';
|
|
import SyncFieldWrapper from '@/components/form/SyncFieldWrapper';
|
|
import {SlidersHorizontal} from 'lucide-react';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
|
|
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
|
import {LangContext, LangContextProps} from '@/context/LangContext';
|
|
import {apiGet} from '@/lib/api/client';
|
|
|
|
type AttributeResponse = { type: string; values: Attribute[] }[];
|
|
|
|
interface CharacterEditorEditProps {
|
|
character: CharacterProps;
|
|
setCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
|
|
onCharacterChange: (key: keyof CharacterProps, value: string | number | null) => void;
|
|
onAddAttribute: (section: CharacterAttributeSection, attr: Attribute) => Promise<void>;
|
|
onRemoveAttribute: (section: CharacterAttributeSection, idx: number, id: string) => Promise<void>;
|
|
seriesCharacter?: SeriesCharacterProps | null;
|
|
onSyncComplete?: () => void;
|
|
}
|
|
|
|
/**
|
|
* CharacterEditorEdit - Version sidebar édition
|
|
* Mêmes fonctionnalités que CharacterSettingsEdit, layout linéaire
|
|
*/
|
|
export default function CharacterEditorEdit({
|
|
character,
|
|
setCharacter,
|
|
onCharacterChange,
|
|
onAddAttribute,
|
|
onRemoveAttribute,
|
|
seriesCharacter,
|
|
onSyncComplete,
|
|
}: CharacterEditorEditProps): React.JSX.Element {
|
|
const t = useTranslations();
|
|
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
|
|
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
|
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
|
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
|
|
|
|
useEffect(function (): void {
|
|
if (character?.id !== null) {
|
|
getAttributes().then();
|
|
}
|
|
}, [character?.id]);
|
|
|
|
async function getAttributes(): Promise<void> {
|
|
try {
|
|
const response: AttributeResponse = await apiGet<AttributeResponse>(
|
|
'character/attribute',
|
|
session.accessToken,
|
|
lang,
|
|
{characterId: character?.id}
|
|
);
|
|
if (response) {
|
|
const attributes: CharacterAttribute = {};
|
|
response.forEach(function (item: { type: string; values: Attribute[] }): void {
|
|
attributes[item.type] = item.values;
|
|
});
|
|
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
|
|
if (!prev) return null;
|
|
return {
|
|
...prev,
|
|
physical: attributes.physical ?? [],
|
|
psychological: attributes.psychological ?? [],
|
|
relations: attributes.relations ?? [],
|
|
skills: attributes.skills ?? [],
|
|
weaknesses: attributes.weaknesses ?? [],
|
|
strengths: attributes.strengths ?? [],
|
|
goals: attributes.goals ?? [],
|
|
motivations: attributes.motivations ?? [],
|
|
arc: attributes.arc ?? [],
|
|
secrets: attributes.secrets ?? [],
|
|
fears: attributes.fears ?? [],
|
|
flaws: attributes.flaws ?? [],
|
|
beliefs: attributes.beliefs ?? [],
|
|
conflicts: attributes.conflicts ?? [],
|
|
quotes: attributes.quotes ?? [],
|
|
distinguishingMarks: attributes.distinguishingMarks ?? [],
|
|
items: attributes.items ?? [],
|
|
affiliations: attributes.affiliations ?? [],
|
|
};
|
|
});
|
|
}
|
|
} catch (e: unknown) {
|
|
if (e instanceof Error) {
|
|
errorMessage(e.message);
|
|
}
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
{/* Informations de base */}
|
|
<div className="border-b border-secondary pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.basicInfo')}</h4>
|
|
<div className="space-y-3">
|
|
<InputField
|
|
fieldName={t('characterDetail.name')}
|
|
input={
|
|
<SyncFieldWrapper
|
|
seriesElementId={character.seriesCharacterId}
|
|
seriesValue={seriesCharacter?.name || ''}
|
|
currentValue={character.name || ''}
|
|
bookElementId={character.id || ''}
|
|
field="name"
|
|
elementType="character"
|
|
onDownload={function (): void {
|
|
onCharacterChange('name', seriesCharacter?.name || '');
|
|
}}
|
|
onSyncComplete={onSyncComplete}
|
|
>
|
|
<TextInput
|
|
value={character.name || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('name', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.namePlaceholder')}
|
|
/>
|
|
</SyncFieldWrapper>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.lastName')}
|
|
input={
|
|
<SyncFieldWrapper
|
|
seriesElementId={character.seriesCharacterId}
|
|
seriesValue={seriesCharacter?.lastName || ''}
|
|
currentValue={character.lastName || ''}
|
|
bookElementId={character.id || ''}
|
|
field="lastName"
|
|
elementType="character"
|
|
onDownload={function (): void {
|
|
onCharacterChange('lastName', seriesCharacter?.lastName || '');
|
|
}}
|
|
onSyncComplete={onSyncComplete}
|
|
>
|
|
<TextInput
|
|
value={character.lastName || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('lastName', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.lastNamePlaceholder')}
|
|
/>
|
|
</SyncFieldWrapper>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.role')}
|
|
input={
|
|
<SelectBox
|
|
defaultValue={character.category || 'none'}
|
|
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
|
|
const selectedCategory: string = e.target.value;
|
|
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
|
|
return prev ? {
|
|
...prev,
|
|
category: isCharacterCategory(selectedCategory) ? selectedCategory : 'none'
|
|
} : prev;
|
|
});
|
|
}}
|
|
data={characterCategories}
|
|
translate
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.gender')}
|
|
input={
|
|
<TextInput
|
|
value={character.gender || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('gender', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.genderPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.age')}
|
|
input={
|
|
<NumberInput
|
|
value={character.age ?? null}
|
|
onValueChange={function (val: number | null): void {
|
|
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
|
|
return prev ? {...prev, age: val} : prev;
|
|
});
|
|
}}
|
|
placeholder={t('characterDetail.agePlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Histoire */}
|
|
<div className="border-b border-secondary pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.historySection')}</h4>
|
|
<div className="space-y-3">
|
|
<InputField
|
|
fieldName={t('characterDetail.biography')}
|
|
input={
|
|
<SyncFieldWrapper
|
|
seriesElementId={character.seriesCharacterId}
|
|
seriesValue={seriesCharacter?.biography || ''}
|
|
currentValue={character.biography || ''}
|
|
bookElementId={character.id || ''}
|
|
field="biography"
|
|
elementType="character"
|
|
onDownload={function (): void {
|
|
onCharacterChange('biography', seriesCharacter?.biography || '');
|
|
}}
|
|
onSyncComplete={onSyncComplete}
|
|
>
|
|
<TextAreaInput
|
|
value={character.biography || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
|
|
onCharacterChange('biography', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.biographyPlaceholder')}
|
|
/>
|
|
</SyncFieldWrapper>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.roleFull')}
|
|
input={
|
|
<SyncFieldWrapper
|
|
seriesElementId={character.seriesCharacterId}
|
|
seriesValue={seriesCharacter?.role || ''}
|
|
currentValue={character.role || ''}
|
|
bookElementId={character.id || ''}
|
|
field="role"
|
|
elementType="character"
|
|
onDownload={function (): void {
|
|
onCharacterChange('role', seriesCharacter?.role || '');
|
|
}}
|
|
onSyncComplete={onSyncComplete}
|
|
>
|
|
<TextAreaInput
|
|
value={character.role || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
|
|
onCharacterChange('role', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.roleFullPlaceholder')}
|
|
/>
|
|
</SyncFieldWrapper>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Attributs de base */}
|
|
{basicCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
|
|
return (
|
|
<CharacterSectionElement
|
|
key={`basic-${index}`}
|
|
title={item.title}
|
|
section={item.section}
|
|
placeholder={item.placeholder}
|
|
icon={item.icon}
|
|
selectedCharacter={character}
|
|
setSelectedCharacter={setCharacter}
|
|
handleAddElement={onAddAttribute}
|
|
handleRemoveElement={onRemoveAttribute}
|
|
/>
|
|
);
|
|
})}
|
|
|
|
{/* Toggle Mode Avancé */}
|
|
<div
|
|
className="flex items-center justify-between p-3 bg-secondary rounded-lg border border-secondary">
|
|
<div className="flex items-center gap-2">
|
|
<SlidersHorizontal className="text-primary w-4 h-4" strokeWidth={1.75}/>
|
|
<span className="text-text-primary font-medium text-sm">{t('characterDetail.advancedMode')}</span>
|
|
</div>
|
|
<button
|
|
onClick={function (): void {
|
|
setShowAdvanced(!showAdvanced);
|
|
}}
|
|
className={`px-3 py-1.5 rounded-lg text-sm transition-colors duration-150 ${
|
|
showAdvanced
|
|
? 'bg-secondary text-primary'
|
|
: 'bg-secondary text-text-secondary hover:text-text-primary'
|
|
}`}
|
|
>
|
|
{showAdvanced ? t('characterDetail.hideAdvanced') : t('characterDetail.showAdvanced')}
|
|
</button>
|
|
</div>
|
|
|
|
{/* Sections avancées */}
|
|
{showAdvanced && (
|
|
<>
|
|
<div className="border-b border-secondary pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.identitySection')}</h4>
|
|
<div className="space-y-3">
|
|
<InputField
|
|
fieldName={t('characterDetail.species')}
|
|
input={
|
|
<TextInput
|
|
value={character.species || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
|
|
onCharacterChange('species', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.speciesPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
|
|
<InputField
|
|
fieldName={t('characterDetail.status')}
|
|
input={
|
|
<SelectBox
|
|
defaultValue={character.status || 'alive'}
|
|
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
|
|
const selectedStatus: string = e.target.value;
|
|
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
|
|
return prev ? {
|
|
...prev,
|
|
status: isCharacterStatus(selectedStatus) ? selectedStatus : 'alive'
|
|
} : prev;
|
|
});
|
|
}}
|
|
data={characterStatus}
|
|
translate
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="border-b border-secondary pb-3">
|
|
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.authorSection')}</h4>
|
|
<InputField
|
|
fieldName={t('characterDetail.notes')}
|
|
input={
|
|
<TextAreaInput
|
|
value={character.notes || ''}
|
|
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
|
|
onCharacterChange('notes', e.target.value);
|
|
}}
|
|
placeholder={t('characterDetail.notesPlaceholder')}
|
|
/>
|
|
}
|
|
/>
|
|
</div>
|
|
|
|
{advancedCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
|
|
return (
|
|
<CharacterSectionElement
|
|
key={`advanced-${index}`}
|
|
title={item.title}
|
|
section={item.section}
|
|
placeholder={item.placeholder}
|
|
icon={item.icon}
|
|
selectedCharacter={character}
|
|
setSelectedCharacter={setCharacter}
|
|
handleAddElement={onAddAttribute}
|
|
handleRemoveElement={onRemoveAttribute}
|
|
/>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|