Remove unused components and models for improved maintainability

- 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.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

View File

@@ -1,23 +1,23 @@
import CollapsableArea from "@/components/CollapsableArea";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {useState} from "react";
import {faTrash} from "@fortawesome/free-solid-svg-icons";
import Collapse from "@/components/ui/Collapse";
import React, {ChangeEvent, useState} from "react";
import {LucideIcon, Trash2} from 'lucide-react';
import IconButton from "@/components/ui/IconButton";
import InputField from "@/components/form/InputField";
import TextInput from "@/components/form/TextInput";
import {Attribute, CharacterProps} from "@/lib/models/Character";
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
import {useTranslations} from "next-intl";
import {Attribute, CharacterAttributeSection, CharacterProps} from "@/lib/types/character";
import {useTranslations} from '@/lib/i18n';
interface CharacterSectionElementProps {
title: string;
section: keyof CharacterProps;
section: CharacterAttributeSection;
placeholder: string;
icon: IconDefinition;
icon: LucideIcon;
selectedCharacter: CharacterProps;
setSelectedCharacter: (character: CharacterProps) => void;
handleAddElement: (section: keyof CharacterProps, element: Attribute) => void;
handleAddElement: (section: CharacterAttributeSection, element: Attribute) => void;
handleRemoveElement: (
section: keyof CharacterProps,
section: CharacterAttributeSection,
index: number,
attrId: string,
) => void;
@@ -33,28 +33,26 @@ export default function CharacterSectionElement(
setSelectedCharacter,
handleAddElement,
handleRemoveElement,
}: CharacterSectionElementProps) {
}: CharacterSectionElementProps): React.JSX.Element {
const t = useTranslations();
const [element, setElement] = useState<string>('');
function handleAddNewElement() {
function handleAddNewElement(): void {
handleAddElement(section, {id: '', name: element});
setElement('');
}
return (
<CollapsableArea title={title} icon={icon}>
<div className="space-y-3 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
{Array.isArray(selectedCharacter?.[section]) &&
selectedCharacter?.[section].map((item, index: number) => (
<Collapse variant="card" title={title} icon={icon}>
{selectedCharacter[section].map((item: Attribute, index: number): React.JSX.Element => (
<div key={index}
className="flex items-center gap-2 bg-secondary/30 rounded-xl border-l-4 border-primary shadow-sm hover:shadow-md transition-all duration-200">
className="flex items-center gap-2 bg-secondary rounded-xl border-l-4 border-primary transition-colors duration-200">
<input
className="flex-1 bg-transparent text-text-primary px-3 py-2.5 focus:outline-none placeholder:text-muted/60"
value={item.name || item.type || item.description || item.history || ''}
onChange={(e) => {
const updatedSection = [...(selectedCharacter[section] as any[])];
updatedSection[index].name = e.target.value;
value={item.name || ''}
onChange={(e: ChangeEvent<HTMLInputElement>): void => {
const updatedSection: Attribute[] = [...selectedCharacter[section]];
updatedSection[index] = {...updatedSection[index], name: e.target.value};
setSelectedCharacter({
...selectedCharacter,
[section]: updatedSection,
@@ -62,28 +60,28 @@ export default function CharacterSectionElement(
}}
placeholder={placeholder}
/>
<button
onClick={() => handleRemoveElement(section, index, item.id)}
className="bg-error/90 hover:bg-error w-9 h-9 rounded-full flex items-center justify-center mr-2 shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200"
>
<FontAwesomeIcon icon={faTrash} className="text-white w-4 h-4"/>
</button>
<div className="mr-2">
<IconButton
icon={Trash2}
variant="danger"
onClick={(): void => handleRemoveElement(section, index, item.id)}
/>
</div>
</div>
))}
<div className="border-t border-secondary/50 mt-4 pt-4">
<div className="border-t border-secondary mt-4 pt-4">
<InputField
input={
<TextInput
value={element}
setValue={(e) => setElement(e.target.value)}
setValue={(e: ChangeEvent<HTMLInputElement>): void => setElement(e.target.value)}
placeholder={t("characterSectionElement.newItem", {item: title.toLowerCase()})}
/>
}
addButtonCallBack={async () => handleAddNewElement()}
addButtonCallBack={async (): Promise<void> => handleAddNewElement()}
/>
</div>
</div>
</CollapsableArea>
</Collapse>
)
}

View File

@@ -1,18 +1,15 @@
'use client';
import React, {useCallback, useContext, useMemo, useState} from 'react';
import {useCharacters, UseCharactersConfig} from '@/hooks/settings/useCharacters';
import {useTranslations} from 'next-intl';
import {CharacterProps} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import {BookContext} from '@/context/BookContext';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faSpinner, faToggleOn} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from '@/lib/i18n';
import {CharacterProps} from '@/lib/types/character';
import {SeriesCharacterProps} from '@/lib/types/series';
import {BookContext, BookContextProps} from '@/context/BookContext';
import PulseLoader from '@/components/ui/PulseLoader';
import ToolDetailHeader from '@/components/book/settings/ToolDetailHeader';
import InputField from '@/components/form/InputField';
import ToggleSwitch from '@/components/form/ToggleSwitch';
import SeriesImportSelector from '@/components/form/SeriesImportSelector';
import AlertBox from '@/components/AlertBox';
import AlertBox from '@/components/ui/AlertBox';
import CharacterEditorList from './CharacterEditorList';
import CharacterEditorDetail from './CharacterEditorDetail';
@@ -24,16 +21,16 @@ import CharacterEditorEdit from './CharacterEditorEdit';
*/
export default function CharacterEditor(): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const config: UseCharactersConfig = useMemo(function (): UseCharactersConfig {
return {
entityType: 'book',
entityId: book?.bookId || '',
};
}, [book?.bookId]);
const {
characters,
seriesCharacters,
@@ -58,7 +55,7 @@ export default function CharacterEditor(): React.JSX.Element {
backToList,
addNewCharacter,
} = useCharacters(config);
const availableSeriesCharacters = useMemo(function (): SeriesCharacterProps[] {
return seriesCharacters.filter(function (sc: SeriesCharacterProps): boolean {
return !characters.some(function (c: CharacterProps): boolean {
@@ -66,19 +63,19 @@ export default function CharacterEditor(): React.JSX.Element {
});
});
}, [seriesCharacters, characters]);
const handleCharacterChange = useCallback(function (key: keyof CharacterProps, value: string | number | null): void {
updateCharacterField(key, value);
}, [updateCharacterField]);
async function handleSave(): Promise<void> {
await exitEditMode(true);
}
function handleCancel(): void {
exitEditMode(false);
}
async function handleDelete(): Promise<void> {
if (selectedCharacter?.id) {
await deleteCharacter(selectedCharacter.id);
@@ -86,25 +83,21 @@ export default function CharacterEditor(): React.JSX.Element {
backToList();
}
}
function getSeriesCharacterForSelected(): SeriesCharacterProps | null {
if (!selectedCharacter?.seriesCharacterId) return null;
return seriesCharacters.find(function (sc: SeriesCharacterProps): boolean {
return sc.id === selectedCharacter.seriesCharacterId;
}) || null;
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<FontAwesomeIcon icon={faSpinner} className="w-6 h-6 text-primary animate-spin"/>
</div>
);
return <PulseLoader size="sm"/>;
}
const isNew: boolean = selectedCharacter?.id === null;
const canExport: boolean = Boolean(bookSeriesId && selectedCharacter?.id && !selectedCharacter.seriesCharacterId);
return (
<div className="flex flex-col h-full">
<ToolDetailHeader
@@ -116,56 +109,40 @@ export default function CharacterEditor(): React.JSX.Element {
onEdit={enterEditMode}
onSave={handleSave}
onCancel={handleCancel}
onDelete={function (): void { setShowDeleteConfirm(true); }}
onDelete={function (): void {
setShowDeleteConfirm(true);
}}
onExport={canExport ? exportToSeries : undefined}
showExport={canExport}
showDelete={Boolean(selectedCharacter?.id)}
/>
<div className="flex-1 overflow-y-auto">
{viewMode === 'list' && (
<div className="space-y-3 p-2">
{/* Toggle tool */}
<div className="bg-secondary/20 rounded-lg p-3 border border-secondary/30">
<InputField
icon={faToggleOn}
fieldName={t('characterComponent.enableTool')}
input={
<ToggleSwitch
checked={toolEnabled}
onChange={toggleTool}
/>
}
{/* Import from series */}
{bookSeriesId && availableSeriesCharacters.length > 0 && (
<SeriesImportSelector
availableItems={availableSeriesCharacters.map(function (sc: SeriesCharacterProps) {
return {
id: sc.id,
name: `${sc.name}${sc.lastName ? ' ' + sc.lastName : ''}`
};
})}
onImport={importFromSeries}
placeholder={t('seriesImport.selectElement')}
label={t('seriesImport.importFromSeries')}
/>
</div>
{toolEnabled && (
<>
{/* Import from series */}
{bookSeriesId && availableSeriesCharacters.length > 0 && (
<SeriesImportSelector
availableItems={availableSeriesCharacters.map(function (sc: SeriesCharacterProps) {
return {
id: sc.id,
name: `${sc.name}${sc.lastName ? ' ' + sc.lastName : ''}`
};
})}
onImport={importFromSeries}
placeholder={t('seriesImport.selectElement')}
label={t('seriesImport.importFromSeries')}
/>
)}
<CharacterEditorList
characters={characters}
onCharacterClick={enterDetailMode}
onAddCharacter={addNewCharacter}
/>
</>
)}
<CharacterEditorList
characters={characters}
onCharacterClick={enterDetailMode}
onAddCharacter={addNewCharacter}
/>
</div>
)}
{viewMode === 'detail' && selectedCharacter && (
<div className="p-4">
<CharacterEditorDetail
@@ -174,7 +151,7 @@ export default function CharacterEditor(): React.JSX.Element {
/>
</div>
)}
{viewMode === 'edit' && selectedCharacter && (
<div className="p-4">
<CharacterEditorEdit
@@ -189,7 +166,7 @@ export default function CharacterEditor(): React.JSX.Element {
</div>
)}
</div>
{showDeleteConfirm && selectedCharacter?.id && (
<AlertBox
title={t('characterDetail.deleteTitle')}
@@ -198,7 +175,9 @@ export default function CharacterEditor(): React.JSX.Element {
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}
onCancel={function (): void { setShowDeleteConfirm(false); }}
onCancel={function (): void {
setShowDeleteConfirm(false);
}}
/>
)}
</div>

View File

@@ -1,20 +1,16 @@
'use client';
import React, {useContext, useEffect} from 'react';
import {
Attribute,
CharacterAttribute,
characterCategories,
CharacterProps
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {LangContext} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext} from '@/context/BookContext';
import System from '@/lib/models/System';
import * as tauri from '@/lib/tauri';
import {Attribute, CharacterAttribute, CharacterProps} from '@/lib/types/character';
import {characterCategories} from '@/lib/constants/character';
import {SelectBoxProps} from '@/components/form/SelectBox';
import {SeriesCharacterProps} from '@/lib/types/series';
import {useTranslations} from '@/lib/i18n';
import DetailField from '@/components/ui/DetailField';
import AvatarIcon from '@/components/ui/AvatarIcon';
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[] }[];
@@ -30,36 +26,29 @@ interface CharacterEditorDetailProps {
* PAS de CollapsableArea, PAS de grids
*/
export default function CharacterEditorDetail({
character,
seriesCharacter,
onLoadAttributes,
}: CharacterEditorDetailProps): React.JSX.Element {
character,
seriesCharacter,
onLoadAttributes,
}: CharacterEditorDetailProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
useEffect(function (): void {
if (character?.id !== null) {
getAttributes().then();
}
}, [character?.id]);
async function getAttributes(): Promise<void> {
try {
let response: AttributeResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
const response: AttributeResponse = await apiGet<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
if (response && onLoadAttributes) {
const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void {
@@ -73,48 +62,34 @@ export default function CharacterEditorDetail({
}
}
}
function renderField(label: string, value: string | number | null | undefined): React.JSX.Element | null {
if (!value) return null;
return (
<div className="mb-3">
<span className="text-text-secondary text-xs block mb-1">{label}</span>
<p className="text-text-primary text-sm">{value}</p>
</div>
);
}
function getCategoryLabel(category: string | null | undefined): string {
if (!category) return '';
const found = characterCategories.find(function (c): boolean { return c.value === category; });
const found: SelectBoxProps | undefined = characterCategories.find(function (c: SelectBoxProps): boolean {
return c.value === category;
});
return found ? t(found.label) : category;
}
return (
<div>
{/* Image du personnage - version compacte */}
{character.image && (
<div className="flex justify-center mb-4">
<div className="w-16 h-16 rounded-full border-2 border-primary overflow-hidden">
<img
src={character.image}
alt={character.name}
className="w-full h-full object-cover"
/>
</div>
<AvatarIcon size="xl" image={character.image} alt={character.name}/>
</div>
)}
<h3 className="text-text-primary font-semibold text-base mb-4">
{character.name} {character.lastName}
</h3>
{renderField(t('characterDetail.role'), getCategoryLabel(character.category))}
{renderField(t('characterDetail.title'), character.title)}
{renderField(t('characterDetail.gender'), character.gender)}
{renderField(t('characterDetail.age'), character.age)}
{renderField(t('characterDetail.biography'), character.biography)}
{renderField(t('characterDetail.roleFull'), character.role)}
<DetailField variant="compact" label={t('characterDetail.role')}
value={getCategoryLabel(character.category)}/>
<DetailField variant="compact" label={t('characterDetail.title')} value={character.title}/>
<DetailField variant="compact" label={t('characterDetail.gender')} value={character.gender}/>
<DetailField variant="compact" label={t('characterDetail.age')} value={character.age}/>
<DetailField variant="compact" label={t('characterDetail.biography')} value={character.biography}/>
<DetailField variant="compact" label={t('characterDetail.roleFull')} value={character.role}/>
</div>
);
}

View File

@@ -1,34 +1,34 @@
'use client';
import React, {useContext, useEffect, useMemo, useState} from 'react';
import React, {Dispatch, SetStateAction, useContext, useEffect, useState} from 'react';
import {
advancedCharacterElements,
Attribute,
basicCharacterElements,
CharacterAttribute,
characterCategories,
CharacterAttributeSection,
CharacterElement,
CharacterProps,
isCharacterCategory,
isCharacterStatus,
} from '@/lib/types/character';
import {
advancedCharacterElements,
basicCharacterElements,
characterCategories,
characterStatus
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
} from '@/lib/constants/character';
import {SeriesCharacterProps} from '@/lib/types/series';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TexteAreaInput from '@/components/form/TexteAreaInput';
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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faSliders} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {LangContext} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext} from '@/context/BookContext';
import System from '@/lib/models/System';
import {Dispatch, SetStateAction} from 'react';
import * as tauri from '@/lib/tauri';
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[] }[];
@@ -36,8 +36,8 @@ interface CharacterEditorEditProps {
character: CharacterProps;
setCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
onCharacterChange: (key: keyof CharacterProps, value: string | number | null) => void;
onAddAttribute: (section: keyof CharacterProps, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: keyof CharacterProps, idx: number, id: string) => Promise<void>;
onAddAttribute: (section: CharacterAttributeSection, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: CharacterAttributeSection, idx: number, id: string) => Promise<void>;
seriesCharacter?: SeriesCharacterProps | null;
onSyncComplete?: () => void;
}
@@ -47,54 +47,34 @@ interface CharacterEditorEditProps {
* Mêmes fonctionnalités que CharacterSettingsEdit, layout linéaire
*/
export default function CharacterEditorEdit({
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterEditorEditProps): React.JSX.Element {
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterEditorEditProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
// Traduire les données des SelectBox
const translatedCharacterCategories = useMemo(() =>
characterCategories.map((item) => ({
...item,
label: t(item.label)
})), [t]);
const translatedCharacterStatus = useMemo(() =>
characterStatus.map((item) => ({
...item,
label: t(item.label)
})), [t]);
useEffect(function (): void {
if (character?.id !== null) {
getAttributes().then();
}
}, [character?.id]);
async function getAttributes(): Promise<void> {
try {
let response: AttributeResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
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 {
@@ -131,11 +111,11 @@ export default function CharacterEditorEdit({
}
}
}
return (
<div className="space-y-4">
{/* Informations de base */}
<div className="border-b border-secondary/30 pb-3">
<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
@@ -148,7 +128,9 @@ export default function CharacterEditorEdit({
bookElementId={character.id || ''}
field="name"
elementType="character"
onDownload={function (): void { onCharacterChange('name', seriesCharacter?.name || ''); }}
onDownload={function (): void {
onCharacterChange('name', seriesCharacter?.name || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -161,7 +143,7 @@ export default function CharacterEditorEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.lastName')}
input={
@@ -172,7 +154,9 @@ export default function CharacterEditorEdit({
bookElementId={character.id || ''}
field="lastName"
elementType="character"
onDownload={function (): void { onCharacterChange('lastName', seriesCharacter?.lastName || ''); }}
onDownload={function (): void {
onCharacterChange('lastName', seriesCharacter?.lastName || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -185,22 +169,27 @@ export default function CharacterEditorEdit({
</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: e.target.value as CharacterProps['category']} : prev;
return prev ? {
...prev,
category: isCharacterCategory(selectedCategory) ? selectedCategory : 'none'
} : prev;
});
}}
data={translatedCharacterCategories}
data={characterCategories}
translate
/>
}
/>
<InputField
fieldName={t('characterDetail.gender')}
input={
@@ -213,7 +202,7 @@ export default function CharacterEditorEdit({
/>
}
/>
<InputField
fieldName={t('characterDetail.age')}
input={
@@ -230,9 +219,9 @@ export default function CharacterEditorEdit({
/>
</div>
</div>
{/* Histoire */}
<div className="border-b border-secondary/30 pb-3">
<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
@@ -245,10 +234,12 @@ export default function CharacterEditorEdit({
bookElementId={character.id || ''}
field="biography"
elementType="character"
onDownload={function (): void { onCharacterChange('biography', seriesCharacter?.biography || ''); }}
onDownload={function (): void {
onCharacterChange('biography', seriesCharacter?.biography || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.biography || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('biography', e.target.value);
@@ -258,7 +249,7 @@ export default function CharacterEditorEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.roleFull')}
input={
@@ -269,10 +260,12 @@ export default function CharacterEditorEdit({
bookElementId={character.id || ''}
field="role"
elementType="character"
onDownload={function (): void { onCharacterChange('role', seriesCharacter?.role || ''); }}
onDownload={function (): void {
onCharacterChange('role', seriesCharacter?.role || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.role || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('role', e.target.value);
@@ -284,7 +277,7 @@ export default function CharacterEditorEdit({
/>
</div>
</div>
{/* Attributs de base */}
{basicCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
@@ -301,29 +294,32 @@ export default function CharacterEditorEdit({
/>
);
})}
{/* Toggle Mode Avancé */}
<div className="flex items-center justify-between p-3 bg-secondary/30 rounded-lg border border-secondary/50">
<div
className="flex items-center justify-between p-3 bg-secondary rounded-lg border border-secondary">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faSliders} className="text-primary w-4 h-4"/>
<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-all duration-200 ${
onClick={function (): void {
setShowAdvanced(!showAdvanced);
}}
className={`px-3 py-1.5 rounded-lg text-sm transition-colors duration-150 ${
showAdvanced
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-primary hover:bg-secondary'
? '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/30 pb-3">
<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
@@ -338,30 +334,35 @@ export default function CharacterEditorEdit({
/>
}
/>
<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: e.target.value as CharacterProps['status']} : prev;
return prev ? {
...prev,
status: isCharacterStatus(selectedStatus) ? selectedStatus : 'alive'
} : prev;
});
}}
data={translatedCharacterStatus}
data={characterStatus}
translate
/>
}
/>
</div>
</div>
<div className="border-b border-secondary/30 pb-3">
<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={
<TexteAreaInput
<TextAreaInput
value={character.notes || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('notes', e.target.value);
@@ -371,7 +372,7 @@ export default function CharacterEditorEdit({
}
/>
</div>
{advancedCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
<CharacterSectionElement

View File

@@ -1,11 +1,13 @@
'use client';
import React, {useState} from 'react';
import {CharacterProps} from '@/lib/models/Character';
import {CharacterProps} from '@/lib/types/character';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faChevronRight, faPlus, faUser} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {Plus, User} from 'lucide-react';
import EmptyState from '@/components/ui/EmptyState';
import EntityListItem from '@/components/ui/EntityListItem';
import AvatarIcon from '@/components/ui/AvatarIcon';
import {useTranslations} from '@/lib/i18n';
interface CharacterEditorListProps {
characters: CharacterProps[];
@@ -19,22 +21,22 @@ interface CharacterEditorListProps {
* PAS de scroll interne (géré par parent ComposerRightBar)
*/
export default function CharacterEditorList({
characters,
onCharacterClick,
onAddCharacter,
}: CharacterEditorListProps): React.JSX.Element {
characters,
onCharacterClick,
onAddCharacter,
}: CharacterEditorListProps): React.JSX.Element {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState<string>('');
function getFilteredCharacters(): CharacterProps[] {
return characters.filter(function (char: CharacterProps): boolean {
return char.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(char.lastName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);
});
}
const filteredCharacters: CharacterProps[] = getFilteredCharacters();
return (
<div className="space-y-3">
<div className="px-2">
@@ -48,65 +50,33 @@ export default function CharacterEditorList({
placeholder={t('characterList.search')}
/>
}
actionIcon={faPlus}
actionIcon={Plus}
actionLabel={t('characterList.add')}
addButtonCallBack={async function (): Promise<void> {
onAddCharacter();
}}
/>
</div>
<div className="px-2 space-y-2">
{filteredCharacters.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-3">
<FontAwesomeIcon icon={faUser} className="text-primary w-8 h-8"/>
</div>
<h3 className="text-text-primary font-semibold text-base mb-1">
{t('characterList.noCharacters')}
</h3>
<p className="text-muted text-sm max-w-xs">
{t('characterList.noCharactersDescription')}
</p>
</div>
<EmptyState icon={User} title={t('characterList.noCharacters')}
description={t('characterList.noCharactersDescription')}/>
) : (
filteredCharacters.map(function (char: CharacterProps): React.JSX.Element {
return (
<div
<EntityListItem
key={char.id}
onClick={function (): void { onCharacterClick(char); }}
className="group flex items-center p-3 bg-secondary/30 rounded-lg border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md transition-all duration-200 hover:border-primary/50"
>
<div className="w-10 h-10 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-sm group-hover:scale-110 transition-transform">
{char.image ? (
<img
src={char.image}
alt={char.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-primary/10 text-primary font-bold text-sm">
{char.name?.charAt(0)?.toUpperCase() || '?'}
</div>
)}
</div>
<div className="ml-3 flex-1 min-w-0">
<div className="text-text-primary font-semibold text-sm group-hover:text-primary transition-colors truncate">
{char.name || t('characterList.unknown')}
</div>
<div className="text-muted text-xs truncate">
{char.title || char.role || t('characterList.noRole')}
</div>
</div>
<div className="w-6 flex justify-center">
<FontAwesomeIcon
icon={faChevronRight}
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-3 h-3"
/>
</div>
</div>
size="sm"
onClick={function (): void {
onCharacterClick(char);
}}
avatar={<AvatarIcon size="sm" image={char.image}
initial={char.name?.charAt(0)?.toUpperCase() || '?'}
alt={char.name}/>}
title={char.name || t('characterList.unknown')}
subtitle={char.title || char.role || t('characterList.noRole')}
/>
);
})
)}

View File

@@ -1,18 +1,15 @@
'use client';
import React, {useCallback, useContext, useMemo, useState} from 'react';
import {useCharacters, UseCharactersConfig} from '@/hooks/settings/useCharacters';
import {useTranslations} from 'next-intl';
import {CharacterProps} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import {BookContext} from '@/context/BookContext';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faSpinner, faToggleOn} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from '@/lib/i18n';
import {CharacterProps} from '@/lib/types/character';
import {SeriesCharacterProps} from '@/lib/types/series';
import {BookContext, BookContextProps} from '@/context/BookContext';
import PulseLoader from '@/components/ui/PulseLoader';
import ToolDetailHeader from '@/components/book/settings/ToolDetailHeader';
import InputField from '@/components/form/InputField';
import ToggleSwitch from '@/components/form/ToggleSwitch';
import SeriesImportSelector from '@/components/form/SeriesImportSelector';
import AlertBox from '@/components/AlertBox';
import AlertBox from '@/components/ui/AlertBox';
import CharacterSettingsList from './CharacterSettingsList';
import CharacterSettingsDetail from './CharacterSettingsDetail';
@@ -21,7 +18,7 @@ import CharacterSettingsEdit from './CharacterSettingsEdit';
interface CharacterSettingsProps {
entityType?: 'book' | 'series';
entityId?: string;
showToggle?: boolean;
toolEnabled?: boolean;
}
/**
@@ -30,23 +27,23 @@ interface CharacterSettingsProps {
* Inclut: toggle tool, import from series, header avec actions
*/
export default function CharacterSettings({
entityType = 'book',
entityId,
showToggle = true,
}: CharacterSettingsProps): React.JSX.Element {
entityType = 'book',
entityId,
toolEnabled: parentToolEnabled,
}: CharacterSettingsProps): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const resolvedEntityId: string = entityId || book?.bookId || '';
const config: UseCharactersConfig = useMemo(function (): UseCharactersConfig {
return {
entityType,
entityId: resolvedEntityId,
};
}, [entityType, resolvedEntityId]);
const {
characters,
seriesCharacters,
@@ -72,7 +69,7 @@ export default function CharacterSettings({
backToList,
addNewCharacter,
} = useCharacters(config);
const availableSeriesCharacters = useMemo(function (): SeriesCharacterProps[] {
return seriesCharacters.filter(function (sc: SeriesCharacterProps): boolean {
return !characters.some(function (c: CharacterProps): boolean {
@@ -80,19 +77,19 @@ export default function CharacterSettings({
});
});
}, [seriesCharacters, characters]);
const handleCharacterChange = useCallback(function (key: keyof CharacterProps, value: string | number | null): void {
updateCharacterField(key, value);
}, [updateCharacterField]);
async function handleSave(): Promise<void> {
await exitEditMode(true);
}
function handleCancel(): void {
exitEditMode(false);
}
async function handleDelete(): Promise<void> {
if (selectedCharacter?.id) {
await deleteCharacter(selectedCharacter.id);
@@ -100,25 +97,21 @@ export default function CharacterSettings({
backToList();
}
}
function getSeriesCharacterForSelected(): SeriesCharacterProps | null {
if (!selectedCharacter?.seriesCharacterId) return null;
return seriesCharacters.find(function (sc: SeriesCharacterProps): boolean {
return sc.id === selectedCharacter.seriesCharacterId;
}) || null;
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<FontAwesomeIcon icon={faSpinner} className="w-8 h-8 text-primary animate-spin"/>
</div>
);
return <PulseLoader/>;
}
const isNew: boolean = selectedCharacter?.id === null;
const canExport: boolean = Boolean(bookSeriesId && selectedCharacter?.id && !selectedCharacter.seriesCharacterId);
return (
<div className="flex flex-col h-full">
{/* Header - uniquement pour detail/edit */}
@@ -131,37 +124,19 @@ export default function CharacterSettings({
onEdit={enterEditMode}
onSave={handleSave}
onCancel={handleCancel}
onDelete={function (): void { setShowDeleteConfirm(true); }}
onDelete={function (): void {
setShowDeleteConfirm(true);
}}
onExport={canExport ? exportToSeries : undefined}
showExport={canExport}
showDelete={Boolean(selectedCharacter?.id)}
/>
{/* Contenu principal */}
<div className="flex-1 overflow-y-auto">
{viewMode === 'list' && (
<div className="space-y-5 p-4">
{/* Toggle tool */}
{showToggle && !isSeriesMode && (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<InputField
icon={faToggleOn}
fieldName={t('characterComponent.enableTool')}
input={
<ToggleSwitch
checked={toolEnabled}
onChange={toggleTool}
/>
}
/>
<p className="text-muted text-sm mt-2">
{t('characterComponent.enableToolDescription')}
</p>
</div>
)}
{/* Contenu si outil activé */}
{(toolEnabled || isSeriesMode) && (
{((parentToolEnabled !== undefined ? parentToolEnabled : toolEnabled) || isSeriesMode) && (
<>
{/* Import from series */}
{!isSeriesMode && bookSeriesId && availableSeriesCharacters.length > 0 && (
@@ -177,7 +152,7 @@ export default function CharacterSettings({
label={t('seriesImport.importFromSeries')}
/>
)}
{/* Liste des personnages */}
<CharacterSettingsList
characters={characters}
@@ -188,7 +163,7 @@ export default function CharacterSettings({
)}
</div>
)}
{viewMode === 'detail' && selectedCharacter && (
<div className="p-4">
<CharacterSettingsDetail
@@ -197,7 +172,7 @@ export default function CharacterSettings({
/>
</div>
)}
{viewMode === 'edit' && selectedCharacter && (
<div className="p-4">
<CharacterSettingsEdit
@@ -212,7 +187,7 @@ export default function CharacterSettings({
</div>
)}
</div>
{/* Modal de confirmation de suppression */}
{showDeleteConfirm && selectedCharacter?.id && (
<AlertBox
@@ -222,7 +197,9 @@ export default function CharacterSettings({
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}
onCancel={function (): void { setShowDeleteConfirm(false); }}
onCancel={function (): void {
setShowDeleteConfirm(false);
}}
/>
)}
</div>

View File

@@ -1,45 +1,39 @@
'use client';
import React, {useContext, useEffect, useState} from 'react';
import {Attribute, CharacterAttribute, CharacterElement, CharacterProps} from '@/lib/types/character';
import {SelectBoxProps} from '@/components/form/SelectBox';
import {
advancedCharacterElements,
Attribute,
basicCharacterElements,
CharacterAttribute,
characterCategories,
CharacterElement,
CharacterProps,
characterStatus
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import CollapsableArea from '@/components/CollapsableArea';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
} from '@/lib/constants/character';
import {SeriesCharacterProps} from '@/lib/types/series';
import Badge from '@/components/ui/Badge';
import Collapse from '@/components/ui/Collapse';
import {
faBook,
faCommentDots,
faGlobe,
faSliders,
faStickyNote,
faUser,
faVenusMars,
faCakeCandles,
faTag,
faCrown,
faQuoteLeft,
faFlag,
faHouse,
faSkull,
faDna,
faPalette,
faNoteSticky
} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {LangContext} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext} from '@/context/BookContext';
import System from '@/lib/models/System';
import * as tauri from '@/lib/tauri';
Book,
Cake,
Dna,
Flag,
Globe,
Home,
MessageCircle,
Palette,
Quote,
Skull,
SlidersHorizontal,
StickyNote,
Tag,
User,
Users
} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import {SessionContext, SessionContextProps} from '@/context/SessionContext';
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
import {dynamicBg} from '@/lib/utils/dynamicStyles';
import {LangContext, LangContextProps} from '@/context/LangContext';
import {apiGet} from '@/lib/api/client';
type AttributeResponse = { type: string; values: Attribute[] }[];
@@ -50,37 +44,30 @@ interface CharacterSettingsDetailProps {
}
export default function CharacterSettingsDetail({
character,
seriesCharacter,
onLoadAttributes,
}: CharacterSettingsDetailProps): React.JSX.Element {
character,
seriesCharacter,
onLoadAttributes,
}: CharacterSettingsDetailProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
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 {
let response: AttributeResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
const response: AttributeResponse = await apiGet<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
if (response && onLoadAttributes) {
const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void {
@@ -94,53 +81,56 @@ export default function CharacterSettingsDetail({
}
}
}
function getCategoryLabel(): string {
const cat = characterCategories.find(c => c.value === character.category);
const cat: SelectBoxProps | undefined = characterCategories.find((c: SelectBoxProps): boolean => c.value === character.category);
return cat ? t(cat.label) : character.category || '—';
}
function getStatusLabel(): string {
const stat = characterStatus.find(s => s.value === character.status);
const stat: SelectBoxProps | undefined = characterStatus.find((s: SelectBoxProps): boolean => s.value === character.status);
return stat ? t(stat.label) : character.status || '—';
}
function renderAttributeSection(element: CharacterElement): React.JSX.Element | null {
const attributes: Attribute[] = character[element.section] as Attribute[] || [];
const attributes: Attribute[] = character[element.section] || [];
if (attributes.length === 0) return null;
return (
<CollapsableArea key={element.section} title={element.title} icon={element.icon}>
<div className="flex flex-wrap gap-2 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
{attributes.map(function (attr: Attribute, index: number): React.JSX.Element {
return (
<span key={index} className="px-3 py-1 bg-primary/20 text-primary rounded-full text-sm border border-primary/30">
{attr.name}
</span>
);
})}
</div>
</CollapsableArea>
<Collapse variant="card" key={element.section} title={element.title} icon={element.icon}>
<div className="flex flex-wrap gap-2">
{attributes.map(function (attr: Attribute, index: number): React.JSX.Element {
return (
<span key={index}
className="px-3 py-1 bg-primary/20 text-primary rounded-full text-sm border border-primary/30">
{attr.name}
</span>
);
})}
</div>
</Collapse>
);
}
return (
<div className="space-y-6 px-2 pb-4">
{/* Hero Section - Image + Infos principales */}
<div className="flex gap-6 p-6 bg-gradient-to-r from-secondary/30 to-transparent rounded-2xl border border-secondary/30">
<div
className="flex gap-6 p-6 bg-gradient-to-r from-secondary to-transparent rounded-2xl border border-secondary">
{/* Image */}
<div className="shrink-0">
{character.image ? (
<div className="w-32 h-32 rounded-2xl border-4 border-primary/50 overflow-hidden shadow-lg">
<div className="w-32 h-32 rounded-2xl border-4 border-primary overflow-hidden">
<img src={character.image} alt={character.name} className="w-full h-full object-cover"/>
</div>
) : (
<div className="w-32 h-32 rounded-2xl border-4 border-secondary/50 bg-secondary/30 flex items-center justify-center">
<FontAwesomeIcon icon={faUser} className="w-12 h-12 text-text-secondary/50"/>
<div
className="w-32 h-32 rounded-2xl border-4 border-secondary bg-secondary flex items-center justify-center">
<User className="w-12 h-12 text-text-secondary" strokeWidth={1.75}/>
</div>
)}
</div>
{/* Infos principales */}
<div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold text-text-primary">
@@ -152,170 +142,170 @@ export default function CharacterSettingsDetail({
{character.title && (
<p className="text-text-secondary mt-2">{character.title}</p>
)}
{/* Badges */}
<div className="flex flex-wrap gap-2 mt-4">
<span className="inline-flex items-center gap-2 px-3 py-1 bg-primary/20 text-primary rounded-lg text-sm border border-primary/30">
<FontAwesomeIcon icon={faTag} className="w-3 h-3"/>
{getCategoryLabel()}
</span>
<Badge variant="primary" icon={Tag} shape="rounded">{getCategoryLabel()}</Badge>
{character.gender && (
<span className="inline-flex items-center gap-2 px-3 py-1 bg-secondary/50 text-text-primary rounded-lg text-sm border border-secondary/50">
<FontAwesomeIcon icon={faVenusMars} className="w-3 h-3"/>
{character.gender}
</span>
<Badge variant="muted" icon={Users} shape="rounded">{character.gender}</Badge>
)}
{character.age && (
<span className="inline-flex items-center gap-2 px-3 py-1 bg-secondary/50 text-text-primary rounded-lg text-sm border border-secondary/50">
<FontAwesomeIcon icon={faCakeCandles} className="w-3 h-3"/>
{character.age} {t('characterDetail.yearsOld')}
</span>
<Badge variant="muted" icon={Cake}
shape="rounded">{character.age} {t('characterDetail.yearsOld')}</Badge>
)}
</div>
</div>
</div>
{/* Histoire & Biographie */}
<CollapsableArea title={t('characterDetail.historySection')} icon={faBook}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('characterDetail.historySection')} icon={Book}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="lg:col-span-2">
<h4 className="text-text-secondary text-xs uppercase tracking-wide mb-2">{t('characterDetail.biography')}</h4>
<p className={`${character.biography ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
<p className={`${character.biography ? 'text-text-primary' : 'text-text-dimmed italic'}`}>
{character.biography || '—'}
</p>
</div>
<div>
<h4 className="text-text-secondary text-xs uppercase tracking-wide mb-2">{t('characterDetail.history')}</h4>
<p className={`${character.history ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
<p className={`${character.history ? 'text-text-primary' : 'text-text-dimmed italic'}`}>
{character.history || '—'}
</p>
</div>
<div>
<h4 className="text-text-secondary text-xs uppercase tracking-wide mb-2">{t('characterDetail.roleFull')}</h4>
<p className={`${character.role ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
<p className={`${character.role ? 'text-text-primary' : 'text-text-dimmed italic'}`}>
{character.role || '—'}
</p>
</div>
</div>
</CollapsableArea>
</Collapse>
{/* Attributs de base */}
{basicCharacterElements.map(renderAttributeSection)}
{/* 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 justify-between p-4 bg-secondary rounded-xl border border-secondary">
<div className="flex items-center gap-3">
<FontAwesomeIcon icon={faSliders} className="text-primary w-5 h-5"/>
<SlidersHorizontal className="text-primary w-5 h-5" strokeWidth={1.75}/>
<span className="text-text-primary font-medium">{t('characterDetail.advancedMode')}</span>
</div>
<button
onClick={function (): void { setShowAdvanced(!showAdvanced); }}
onClick={function (): void {
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'
? 'bg-primary text-text-primary'
: 'bg-secondary text-text-primary hover:bg-gray-dark'
}`}
>
{showAdvanced ? t('characterDetail.hideAdvanced') : t('characterDetail.showAdvanced')}
</button>
</div>
{/* Sections avancées */}
{showAdvanced && (
<>
{/* Identité étendue */}
<CollapsableArea title={t('characterDetail.identitySection')} icon={faGlobe}>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="p-3 bg-dark-background/30 rounded-lg">
<Collapse variant="card" title={t('characterDetail.identitySection')} icon={Globe}>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
<div className="p-3 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faDna} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.species')}</span>
<Dna className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.species')}</span>
</div>
<p className={character.species ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
<p className={character.species ? 'text-text-primary' : 'text-text-dimmed italic'}>
{character.species || '—'}
</p>
</div>
<div className="p-3 bg-dark-background/30 rounded-lg">
<div className="p-3 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faFlag} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.nationality')}</span>
<Flag className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.nationality')}</span>
</div>
<p className={character.nationality ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
<p className={character.nationality ? 'text-text-primary' : 'text-text-dimmed italic'}>
{character.nationality || '—'}
</p>
</div>
<div className="p-3 bg-dark-background/30 rounded-lg">
<div className="p-3 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faSkull} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.status')}</span>
<Skull className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.status')}</span>
</div>
<p className={character.status ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
<p className={character.status ? 'text-text-primary' : 'text-text-dimmed italic'}>
{getStatusLabel()}
</p>
</div>
<div className="p-3 bg-dark-background/30 rounded-lg">
<div className="p-3 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faHouse} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.residence')}</span>
<Home className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.residence')}</span>
</div>
<p className={character.residence ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
<p className={character.residence ? 'text-text-primary' : 'text-text-dimmed italic'}>
{character.residence || '—'}
</p>
</div>
</div>
</CollapsableArea>
</Collapse>
{/* Voix du personnage */}
<CollapsableArea title={t('characterDetail.voiceSection')} icon={faCommentDots}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="p-4 bg-dark-background/30 rounded-lg">
<Collapse variant="card" title={t('characterDetail.voiceSection')} icon={MessageCircle}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="p-4 rounded-lg">
<h4 className="text-text-secondary text-xs uppercase tracking-wide mb-2">{t('characterDetail.speechPattern')}</h4>
<p className={`${character.speechPattern ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
<p className={`${character.speechPattern ? 'text-text-primary' : 'text-text-dimmed italic'}`}>
{character.speechPattern || '—'}
</p>
</div>
<div className="p-4 bg-dark-background/30 rounded-lg relative">
<FontAwesomeIcon icon={faQuoteLeft} className="absolute top-2 left-2 w-6 h-6 text-primary/20"/>
<div className="p-4 rounded-lg relative">
<Quote className="absolute top-2 left-2 w-6 h-6 text-primary/20" strokeWidth={1.75}/>
<h4 className="text-text-secondary text-xs uppercase tracking-wide mb-2">{t('characterDetail.catchphrase')}</h4>
<p className={`italic ${character.catchphrase ? 'text-text-primary' : 'text-text-secondary/50'}`}>
<p className={`italic ${character.catchphrase ? 'text-text-primary' : 'text-text-dimmed'}`}>
{character.catchphrase ? `« ${character.catchphrase} »` : '—'}
</p>
</div>
</div>
</CollapsableArea>
</Collapse>
{/* Notes de l'auteur */}
<CollapsableArea title={t('characterDetail.authorSection')} icon={faStickyNote}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="lg:col-span-2 p-4 bg-dark-background/30 rounded-lg">
<Collapse variant="card" title={t('characterDetail.authorSection')} icon={StickyNote}>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div className="lg:col-span-2 p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faNoteSticky} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.notes')}</span>
<StickyNote className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.notes')}</span>
</div>
<p className={`${character.notes ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
<p className={`${character.notes ? 'text-text-primary' : 'text-text-dimmed italic'}`}>
{character.notes || '—'}
</p>
</div>
<div className="p-4 bg-dark-background/30 rounded-lg">
<div className="p-4 rounded-lg">
<div className="flex items-center gap-2 mb-2">
<FontAwesomeIcon icon={faPalette} className="w-3 h-3 text-primary"/>
<span className="text-text-secondary text-xs uppercase">{t('characterDetail.colorLabel')}</span>
<Palette className="w-3 h-3 text-primary" strokeWidth={1.75}/>
<span
className="text-text-secondary text-xs uppercase">{t('characterDetail.colorLabel')}</span>
</div>
{character.color ? (
<div className="flex items-center gap-3">
<div
className="w-8 h-8 rounded-lg border-2 border-white/20"
style={{backgroundColor: character.color}}
className={`w-8 h-8 rounded-lg border-2 border-secondary ${character.color ? dynamicBg(character.color) : ''}`}
/>
<span className="text-text-primary font-mono">{character.color}</span>
</div>
) : (
<p className="text-text-secondary/50 italic"></p>
<p className="text-text-dimmed italic"></p>
)}
</div>
</div>
</CollapsableArea>
</Collapse>
{/* Attributs avancés */}
{advancedCharacterElements.map(renderAttributeSection)}
</>

View File

@@ -1,43 +1,35 @@
'use client';
import React, {useContext, useEffect, useMemo, useState} from 'react';
import React, {Dispatch, SetStateAction, useContext, useEffect, useState} from 'react';
import {
advancedCharacterElements,
Attribute,
basicCharacterElements,
CharacterAttribute,
characterCategories,
CharacterAttributeSection,
CharacterElement,
CharacterProps,
isCharacterCategory,
isCharacterStatus,
} from '@/lib/types/character';
import {
advancedCharacterElements,
basicCharacterElements,
characterCategories,
characterStatus
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import CollapsableArea from '@/components/CollapsableArea';
} from '@/lib/constants/character';
import {SeriesCharacterProps} from '@/lib/types/series';
import Collapse from '@/components/ui/Collapse';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TexteAreaInput from '@/components/form/TexteAreaInput';
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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faBook,
faCommentDots,
faGlobe,
faScroll,
faSliders,
faStickyNote,
faUser
} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {SessionContext} from '@/context/SessionContext';
import {AlertContext} from '@/context/AlertContext';
import {LangContext} from '@/context/LangContext';
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
import {BookContext} from '@/context/BookContext';
import System from '@/lib/models/System';
import {Dispatch, SetStateAction} from 'react';
import * as tauri from '@/lib/tauri';
import {Book, Globe, MessageCircle, ScrollText, SlidersHorizontal, StickyNote, User} 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[] }[];
@@ -45,8 +37,8 @@ interface CharacterSettingsEditProps {
character: CharacterProps;
setCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
onCharacterChange: (key: keyof CharacterProps, value: string | number | null) => void;
onAddAttribute: (section: keyof CharacterProps, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: keyof CharacterProps, idx: number, id: string) => Promise<void>;
onAddAttribute: (section: CharacterAttributeSection, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: CharacterAttributeSection, idx: number, id: string) => Promise<void>;
seriesCharacter?: SeriesCharacterProps | null;
onSyncComplete?: () => void;
}
@@ -57,54 +49,34 @@ interface CharacterSettingsEditProps {
* PAS de scroll interne (géré par parent)
*/
export default function CharacterSettingsEdit({
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterSettingsEditProps): React.JSX.Element {
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterSettingsEditProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext);
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const [showAdvanced, setShowAdvanced] = useState<boolean>(false);
// Traduire les données des SelectBox
const translatedCharacterCategories = useMemo(() =>
characterCategories.map((item) => ({
...item,
label: t(item.label)
})), [t]);
const translatedCharacterStatus = useMemo(() =>
characterStatus.map((item) => ({
...item,
label: t(item.label)
})), [t]);
useEffect(function (): void {
if (character?.id !== null) {
getAttributes().then();
}
}, [character?.id]);
async function getAttributes(): Promise<void> {
try {
let response: AttributeResponse;
if (isCurrentlyOffline() || book?.localBook) {
response = await tauri.getCharacterAttributes(character?.id!) as AttributeResponse;
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
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 {
@@ -141,12 +113,11 @@ export default function CharacterSettingsEdit({
}
}
}
return (
<div className="space-y-4 px-2 pb-4">
{/* Informations de base */}
<CollapsableArea title={t('characterDetail.basicInfo')} icon={faUser}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('characterDetail.basicInfo')} icon={User}>
<InputField
fieldName={t('characterDetail.name')}
input={
@@ -157,7 +128,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="name"
elementType="character"
onDownload={function (): void { onCharacterChange('name', seriesCharacter?.name || ''); }}
onDownload={function (): void {
onCharacterChange('name', seriesCharacter?.name || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -170,7 +143,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.lastName')}
input={
@@ -181,7 +154,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="lastName"
elementType="character"
onDownload={function (): void { onCharacterChange('lastName', seriesCharacter?.lastName || ''); }}
onDownload={function (): void {
onCharacterChange('lastName', seriesCharacter?.lastName || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -194,7 +169,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.nickname')}
input={
@@ -205,7 +180,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="nickname"
elementType="character"
onDownload={function (): void { onCharacterChange('nickname', seriesCharacter?.nickname || ''); }}
onDownload={function (): void {
onCharacterChange('nickname', seriesCharacter?.nickname || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -218,7 +195,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.role')}
input={
@@ -230,8 +207,12 @@ export default function CharacterSettingsEdit({
field="category"
elementType="character"
onDownload={function (): void {
const categoryValue: string = seriesCharacter?.category || 'none';
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, category: (seriesCharacter?.category || 'none') as CharacterProps['category']} : prev;
return prev ? {
...prev,
category: isCharacterCategory(categoryValue) ? categoryValue : 'none'
} : prev;
});
}}
onSyncComplete={onSyncComplete}
@@ -239,16 +220,21 @@ export default function CharacterSettingsEdit({
<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: e.target.value as CharacterProps['category']} : prev;
return prev ? {
...prev,
category: isCharacterCategory(selectedCategory) ? selectedCategory : 'none'
} : prev;
});
}}
data={translatedCharacterCategories}
data={characterCategories}
translate
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.title')}
input={
@@ -259,7 +245,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="title"
elementType="character"
onDownload={function (): void { onCharacterChange('title', seriesCharacter?.title || ''); }}
onDownload={function (): void {
onCharacterChange('title', seriesCharacter?.title || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -272,7 +260,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.gender')}
input={
@@ -283,7 +271,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="gender"
elementType="character"
onDownload={function (): void { onCharacterChange('gender', seriesCharacter?.gender || ''); }}
onDownload={function (): void {
onCharacterChange('gender', seriesCharacter?.gender || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -296,7 +286,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.age')}
input={
@@ -326,15 +316,13 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* Histoire */}
<CollapsableArea title={t('characterDetail.historySection')} icon={faBook}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('characterDetail.historySection')} icon={Book}>
<InputField
fieldName={t('characterDetail.biography')}
icon={faBook}
icon={Book}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
@@ -343,10 +331,12 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="biography"
elementType="character"
onDownload={function (): void { onCharacterChange('biography', seriesCharacter?.biography || ''); }}
onDownload={function (): void {
onCharacterChange('biography', seriesCharacter?.biography || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.biography || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('biography', e.target.value);
@@ -356,10 +346,10 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.history')}
icon={faScroll}
icon={ScrollText}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
@@ -368,10 +358,12 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="history"
elementType="character"
onDownload={function (): void { onCharacterChange('history', seriesCharacter?.history || ''); }}
onDownload={function (): void {
onCharacterChange('history', seriesCharacter?.history || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.history || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('history', e.target.value);
@@ -381,10 +373,10 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.roleFull')}
icon={faScroll}
icon={ScrollText}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
@@ -393,10 +385,12 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="role"
elementType="character"
onDownload={function (): void { onCharacterChange('role', seriesCharacter?.role || ''); }}
onDownload={function (): void {
onCharacterChange('role', seriesCharacter?.role || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.role || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('role', e.target.value);
@@ -406,8 +400,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* Attributs de base */}
{basicCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
@@ -425,31 +418,33 @@ export default function CharacterSettingsEdit({
/>
);
})}
{/* 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 justify-between p-4 bg-secondary rounded-xl border border-secondary">
<div className="flex items-center gap-3">
<FontAwesomeIcon icon={faSliders} className="text-primary w-5 h-5"/>
<SlidersHorizontal className="text-primary w-5 h-5" strokeWidth={1.75}/>
<span className="text-text-primary font-medium">{t('characterDetail.advancedMode')}</span>
</div>
<button
onClick={function (): void { setShowAdvanced(!showAdvanced); }}
className={`px-4 py-2 rounded-lg transition-all duration-200 ${
onClick={function (): void {
setShowAdvanced(!showAdvanced);
}}
className={`px-4 py-2 rounded-lg transition-colors duration-150 ${
showAdvanced
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-primary hover:bg-secondary'
? '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 && (
<>
{/* 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">
<Collapse variant="card" title={t('characterDetail.identitySection')} icon={Globe}>
<InputField
fieldName={t('characterDetail.species')}
input={
@@ -460,7 +455,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="species"
elementType="character"
onDownload={function (): void { onCharacterChange('species', seriesCharacter?.species || ''); }}
onDownload={function (): void {
onCharacterChange('species', seriesCharacter?.species || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -473,7 +470,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.nationality')}
input={
@@ -484,7 +481,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="nationality"
elementType="character"
onDownload={function (): void { onCharacterChange('nationality', seriesCharacter?.nationality || ''); }}
onDownload={function (): void {
onCharacterChange('nationality', seriesCharacter?.nationality || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -497,7 +496,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.status')}
input={
@@ -509,8 +508,12 @@ export default function CharacterSettingsEdit({
field="status"
elementType="character"
onDownload={function (): void {
const statusValue: string = seriesCharacter?.status || 'alive';
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, status: (seriesCharacter?.status || 'alive') as CharacterProps['status']} : prev;
return prev ? {
...prev,
status: isCharacterStatus(statusValue) ? statusValue : 'alive'
} : prev;
});
}}
onSyncComplete={onSyncComplete}
@@ -518,16 +521,21 @@ export default function CharacterSettingsEdit({
<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: e.target.value as CharacterProps['status']} : prev;
return prev ? {
...prev,
status: isCharacterStatus(selectedStatus) ? selectedStatus : 'alive'
} : prev;
});
}}
data={translatedCharacterStatus}
data={characterStatus}
translate
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.residence')}
input={
@@ -538,7 +546,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="residence"
elementType="character"
onDownload={function (): void { onCharacterChange('residence', seriesCharacter?.residence || ''); }}
onDownload={function (): void {
onCharacterChange('residence', seriesCharacter?.residence || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -551,12 +561,10 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* 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">
<Collapse variant="card" title={t('characterDetail.voiceSection')} icon={MessageCircle}>
<InputField
fieldName={t('characterDetail.speechPattern')}
input={
@@ -567,10 +575,12 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="speechPattern"
elementType="character"
onDownload={function (): void { onCharacterChange('speechPattern', seriesCharacter?.speechPattern || ''); }}
onDownload={function (): void {
onCharacterChange('speechPattern', seriesCharacter?.speechPattern || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.speechPattern || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('speechPattern', e.target.value);
@@ -580,7 +590,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.catchphrase')}
input={
@@ -591,7 +601,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="catchphrase"
elementType="character"
onDownload={function (): void { onCharacterChange('catchphrase', seriesCharacter?.catchphrase || ''); }}
onDownload={function (): void {
onCharacterChange('catchphrase', seriesCharacter?.catchphrase || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -604,12 +616,10 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* 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">
<Collapse variant="card" title={t('characterDetail.authorSection')} icon={StickyNote}>
<InputField
fieldName={t('characterDetail.notes')}
input={
@@ -620,10 +630,12 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="notes"
elementType="character"
onDownload={function (): void { onCharacterChange('notes', seriesCharacter?.notes || ''); }}
onDownload={function (): void {
onCharacterChange('notes', seriesCharacter?.notes || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={character.notes || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('notes', e.target.value);
@@ -633,7 +645,7 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.colorLabel')}
input={
@@ -644,7 +656,9 @@ export default function CharacterSettingsEdit({
bookElementId={character.id || ''}
field="color"
elementType="character"
onDownload={function (): void { onCharacterChange('color', seriesCharacter?.color || ''); }}
onDownload={function (): void {
onCharacterChange('color', seriesCharacter?.color || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -657,9 +671,8 @@ export default function CharacterSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* Attributs avancés */}
{advancedCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (

View File

@@ -1,13 +1,16 @@
'use client';
import React, {useState} from 'react';
import {characterCategories, CharacterProps} from '@/lib/models/Character';
import {CharacterProps} from '@/lib/types/character';
import {characterCategories} from '@/lib/constants/character';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import CollapsableArea from '@/components/CollapsableArea';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faChevronRight, faPlus, faUser} from '@fortawesome/free-solid-svg-icons';
import {SelectBoxProps} from '@/shared/interface';
import {useTranslations} from 'next-intl';
import Collapse from '@/components/ui/Collapse';
import {Plus, User} from 'lucide-react';
import EntityListItem from '@/components/ui/EntityListItem';
import AvatarIcon from '@/components/ui/AvatarIcon';
import {SelectBoxProps} from '@/components/form/SelectBox';
import {useTranslations} from '@/lib/i18n';
import EmptyState from '@/components/ui/EmptyState';
interface CharacterSettingsListProps {
characters: CharacterProps[];
@@ -21,22 +24,22 @@ interface CharacterSettingsListProps {
* PAS de scroll interne (géré par parent SettingsContainer)
*/
export default function CharacterSettingsList({
characters,
onCharacterClick,
onAddCharacter,
}: CharacterSettingsListProps): React.JSX.Element {
characters,
onCharacterClick,
onAddCharacter,
}: CharacterSettingsListProps): React.JSX.Element {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState<string>('');
function getFilteredCharacters(): CharacterProps[] {
return characters.filter(function (char: CharacterProps): boolean {
return char.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
(char.lastName?.toLowerCase().includes(searchQuery.toLowerCase()) ?? false);
});
}
const filteredCharacters: CharacterProps[] = getFilteredCharacters();
return (
<div className="space-y-4">
<div className="px-4 mb-4">
@@ -50,14 +53,14 @@ export default function CharacterSettingsList({
placeholder={t('characterList.search')}
/>
}
actionIcon={faPlus}
actionIcon={Plus}
actionLabel={t('characterList.add')}
addButtonCallBack={async function (): Promise<void> {
onAddCharacter();
}}
/>
</div>
<div className="px-2">
{characterCategories.map(function (category: SelectBoxProps): React.JSX.Element | null {
const categoryCharacters: CharacterProps[] = filteredCharacters.filter(
@@ -65,85 +68,57 @@ export default function CharacterSettingsList({
return char.category === category.value;
}
);
if (categoryCharacters.length === 0) {
return null;
}
return (
<CollapsableArea
<Collapse variant="card"
key={category.value}
title={t(category.label)}
icon={faUser}
icon={User}
>
<div className="space-y-2 p-2">
{categoryCharacters.map(function (char: CharacterProps): React.JSX.Element {
return (
<div
<EntityListItem
key={char.id}
variant="transparent"
onClick={function (): void {
onCharacterClick(char);
}}
className="group flex items-center p-4 bg-secondary/30 rounded-xl border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md hover:scale-102 transition-all duration-200 hover:border-primary/50"
>
<div className="w-14 h-14 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-md group-hover:scale-110 transition-transform">
{char.image ? (
<img
src={char.image}
alt={char.name}
className="w-full h-full object-cover"
/>
) : (
<div className="w-full h-full flex items-center justify-center bg-primary/10 text-primary font-bold text-lg">
{char.name?.charAt(0)?.toUpperCase() || '?'}
</div>
)}
</div>
<div className="ml-4 flex-1">
<div className="text-text-primary font-bold text-base group-hover:text-primary transition-colors">
{char.name || t('characterList.unknown')}
</div>
<div className="text-text-secondary text-sm mt-0.5">
{char.lastName || t('characterList.noLastName')}
</div>
</div>
<div className="w-28 px-3">
<div className="text-primary text-sm font-semibold truncate">
{char.title || t('characterList.noTitle')}
</div>
<div className="text-muted text-xs truncate mt-0.5">
{char.role || t('characterList.noRole')}
</div>
</div>
<div className="w-8 flex justify-center">
<FontAwesomeIcon
icon={faChevronRight}
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-4 h-4"
avatar={
<AvatarIcon
size="lg"
image={char.image}
initial={char.name?.charAt(0)?.toUpperCase() || '?'}
alt={char.name}
/>
</div>
</div>
}
title={char.name || t('characterList.unknown')}
subtitle={char.lastName || t('characterList.noLastName')}
extra={
<div className="w-28">
<div className="text-primary text-sm font-semibold truncate">
{char.title || t('characterList.noTitle')}
</div>
<div className="text-muted text-xs truncate mt-0.5">
{char.role || t('characterList.noRole')}
</div>
</div>
}
/>
);
})}
</div>
</CollapsableArea>
</Collapse>
);
})}
{filteredCharacters.length === 0 && (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-4">
<FontAwesomeIcon icon={faUser} className="text-primary w-10 h-10"/>
</div>
<h3 className="text-text-primary font-semibold text-lg mb-2">
{t('characterList.noCharacters')}
</h3>
<p className="text-muted text-sm max-w-xs">
{t('characterList.noCharactersDescription')}
</p>
</div>
<EmptyState icon={User} title={t('characterList.noCharacters')}
description={t('characterList.noCharactersDescription')}/>
)}
</div>
</div>