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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user