Expand character model with additional attributes and advanced customization options

- Added fields such as `nickname`, `age`, `gender`, `species`, `nationality`, `status`, and others to enhance character customization.
- Modified localization files to include new field labels and placeholders.
- Updated `CharacterComponent` and `CharacterDetail` components with UI elements for the newly added attributes.
- Introduced "Advanced Mode" toggle to manage visibility of extended customization options.
- Refactored database models and repository methods (`addNewCharacter`, `updateCharacter`, and `fetchCharacters`) to handle the extended schema.
- Improved data encryption and decryption workflows for secure storage of added attributes.
- Enhanced user experience by reorganizing character customization layouts.
This commit is contained in:
natreex
2026-01-23 20:49:57 -05:00
parent 57bf0c6ec3
commit 0fbd3743e7
11 changed files with 806 additions and 211 deletions

View File

@@ -10,23 +10,26 @@ import {
CharacterAttribute,
characterCategories,
CharacterElement,
characterElementCategory,
basicCharacterElements,
advancedCharacterElements,
CharacterProps,
characterTitle
characterStatus
} from "@/lib/models/Character";
import System from "@/lib/models/System";
import {
faAddressCard,
faArrowLeft,
faBook,
faLayerGroup,
faPlus,
faSave,
faScroll,
faUser
faUser,
faSliders,
faGlobe,
faCommentDots,
faStickyNote
} from "@fortawesome/free-solid-svg-icons";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {Dispatch, SetStateAction, useContext, useEffect} from "react";
import {Dispatch, SetStateAction, useContext, useEffect, useState} from "react";
import CharacterSectionElement from "@/components/book/settings/characters/CharacterSectionElement";
import DeleteButton from "@/components/form/DeleteButton";
import {useTranslations} from "next-intl";
@@ -67,6 +70,7 @@ export default function CharacterDetail(
const {book} = useContext(BookContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
useEffect((): void => {
if (selectedCharacter?.id !== null) {
@@ -104,13 +108,24 @@ export default function CharacterDetail(
setSelectedCharacter({
id: selectedCharacter?.id ?? '',
name: selectedCharacter?.name ?? '',
image: selectedCharacter?.image ?? '',
lastName: selectedCharacter?.lastName ?? '',
nickname: selectedCharacter?.nickname ?? '',
age: selectedCharacter?.age ?? '',
gender: selectedCharacter?.gender ?? '',
species: selectedCharacter?.species ?? '',
nationality: selectedCharacter?.nationality ?? '',
status: selectedCharacter?.status ?? 'alive',
category: selectedCharacter?.category ?? 'none',
title: selectedCharacter?.title ?? '',
image: selectedCharacter?.image ?? '',
role: selectedCharacter?.role ?? '',
biography: selectedCharacter?.biography,
history: selectedCharacter?.history,
role: selectedCharacter?.role ?? '',
speechPattern: selectedCharacter?.speechPattern,
catchphrase: selectedCharacter?.catchphrase,
residence: selectedCharacter?.residence,
notes: selectedCharacter?.notes,
color: selectedCharacter?.color,
physical: attributes.physical ?? [],
psychological: attributes.psychological ?? [],
relations: attributes.relations ?? [],
@@ -119,6 +134,16 @@ export default function CharacterDetail(
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) {
@@ -173,7 +198,7 @@ export default function CharacterDetail(
/>
}
/>
<InputField
fieldName={t("characterDetail.lastName")}
input={
@@ -184,7 +209,18 @@ export default function CharacterDetail(
/>
}
/>
<InputField
fieldName={t("characterDetail.nickname")}
input={
<TextInput
value={selectedCharacter?.nickname || ''}
setValue={(e) => handleCharacterChange('nickname', e.target.value)}
placeholder={t("characterDetail.nicknamePlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.role")}
input={
@@ -196,23 +232,43 @@ export default function CharacterDetail(
data={characterCategories}
/>
}
icon={faLayerGroup}
/>
<InputField
fieldName={t("characterDetail.title")}
input={
<SelectBox
defaultValue={selectedCharacter?.title || 'none'}
onChangeCallBack={(e) => handleCharacterChange('title', e.target.value)}
data={characterTitle}
<TextInput
value={selectedCharacter?.title || ''}
setValue={(e) => handleCharacterChange('title', e.target.value)}
placeholder={t("characterDetail.titlePlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.gender")}
input={
<TextInput
value={selectedCharacter?.gender || ''}
setValue={(e) => handleCharacterChange('gender', e.target.value)}
placeholder={t("characterDetail.genderPlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.age")}
input={
<TextInput
value={selectedCharacter?.age || ''}
setValue={(e) => handleCharacterChange('age', e.target.value)}
placeholder={t("characterDetail.agePlaceholder")}
/>
}
icon={faAddressCard}
/>
</div>
</CollapsableArea>
<CollapsableArea title={t("characterDetail.historySection")} icon={faUser}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
@@ -226,7 +282,7 @@ export default function CharacterDetail(
}
icon={faBook}
/>
<InputField
fieldName={t("characterDetail.history")}
input={
@@ -238,7 +294,7 @@ export default function CharacterDetail(
}
icon={faScroll}
/>
<InputField
fieldName={t("characterDetail.roleFull")}
input={
@@ -252,10 +308,11 @@ export default function CharacterDetail(
/>
</div>
</CollapsableArea>
{characterElementCategory.map((item: CharacterElement, index: number) => (
{/* Attributs de base - toujours visibles */}
{basicCharacterElements.map((item: CharacterElement, index: number) => (
<CharacterSectionElement
key={index}
key={`basic-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
@@ -266,6 +323,149 @@ export default function CharacterDetail(
handleRemoveElement={handleRemoveElement}
/>
))}
{/* Toggle Mode Avancé */}
<div className="flex items-center justify-between p-4 bg-secondary/30 rounded-xl border border-secondary/50">
<div className="flex items-center gap-3">
<FontAwesomeIcon icon={faSliders} className="text-primary w-5 h-5"/>
<span className="text-text-primary font-medium">{t("characterDetail.advancedMode")}</span>
</div>
<button
onClick={() => setShowAdvanced(!showAdvanced)}
className={`px-4 py-2 rounded-lg transition-all duration-200 ${
showAdvanced
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-primary hover:bg-secondary'
}`}
>
{showAdvanced ? t("characterDetail.hideAdvanced") : t("characterDetail.showAdvanced")}
</button>
</div>
{/* Sections avancées - visibles uniquement si showAdvanced est true */}
{showAdvanced && (
<>
{/* Identité étendue */}
<CollapsableArea title={t("characterDetail.identitySection")} icon={faGlobe}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t("characterDetail.species")}
input={
<TextInput
value={selectedCharacter?.species || ''}
setValue={(e) => handleCharacterChange('species', e.target.value)}
placeholder={t("characterDetail.speciesPlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.nationality")}
input={
<TextInput
value={selectedCharacter?.nationality || ''}
setValue={(e) => handleCharacterChange('nationality', e.target.value)}
placeholder={t("characterDetail.nationalityPlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.status")}
input={
<SelectBox
defaultValue={selectedCharacter?.status || 'alive'}
onChangeCallBack={(e) => setSelectedCharacter(prev =>
prev ? {...prev, status: e.target.value as CharacterProps['status']} : prev
)}
data={characterStatus}
/>
}
/>
<InputField
fieldName={t("characterDetail.residence")}
input={
<TextInput
value={selectedCharacter?.residence || ''}
setValue={(e) => handleCharacterChange('residence', e.target.value)}
placeholder={t("characterDetail.residencePlaceholder")}
/>
}
/>
</div>
</CollapsableArea>
{/* Voix du personnage */}
<CollapsableArea title={t("characterDetail.voiceSection")} icon={faCommentDots}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t("characterDetail.speechPattern")}
input={
<TexteAreaInput
value={selectedCharacter?.speechPattern || ''}
setValue={(e) => handleCharacterChange('speechPattern', e.target.value)}
placeholder={t("characterDetail.speechPatternPlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.catchphrase")}
input={
<TextInput
value={selectedCharacter?.catchphrase || ''}
setValue={(e) => handleCharacterChange('catchphrase', e.target.value)}
placeholder={t("characterDetail.catchphrasePlaceholder")}
/>
}
/>
</div>
</CollapsableArea>
{/* Notes de l'auteur */}
<CollapsableArea title={t("characterDetail.authorSection")} icon={faStickyNote}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<InputField
fieldName={t("characterDetail.notes")}
input={
<TexteAreaInput
value={selectedCharacter?.notes || ''}
setValue={(e) => handleCharacterChange('notes', e.target.value)}
placeholder={t("characterDetail.notesPlaceholder")}
/>
}
/>
<InputField
fieldName={t("characterDetail.colorLabel")}
input={
<TextInput
value={selectedCharacter?.color || ''}
setValue={(e) => handleCharacterChange('color', e.target.value)}
placeholder={t("characterDetail.colorPlaceholder")}
/>
}
/>
</div>
</CollapsableArea>
{/* Attributs avancés */}
{advancedCharacterElements.map((item: CharacterElement, index: number) => (
<CharacterSectionElement
key={`advanced-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
icon={item.icon}
selectedCharacter={selectedCharacter as CharacterProps}
setSelectedCharacter={setSelectedCharacter}
handleAddElement={handleAddElement}
handleRemoveElement={handleRemoveElement}
/>
))}
</>
)}
</div>
</div>
);