Remove CharacterComponent and CharacterDetail components

- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
This commit is contained in:
natreex
2026-02-05 14:12:08 -05:00
parent cec5830360
commit 209dc6f85a
133 changed files with 17673 additions and 3110 deletions

View File

@@ -0,0 +1,206 @@
'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 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 CharacterEditorList from './CharacterEditorList';
import CharacterEditorDetail from './CharacterEditorDetail';
import CharacterEditorEdit from './CharacterEditorEdit';
/**
* CharacterEditor - Orchestrateur pour ComposerRightBar
* Mêmes fonctionnalités que CharacterSettings, layout condensé
*/
export default function CharacterEditor(): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const config: UseCharactersConfig = useMemo(function (): UseCharactersConfig {
return {
entityType: 'book',
entityId: book?.bookId || '',
};
}, [book?.bookId]);
const {
characters,
seriesCharacters,
selectedCharacter,
toolEnabled,
isLoading,
bookSeriesId,
viewMode,
saveCharacter,
deleteCharacter,
updateCharacterField,
addAttribute,
removeAttribute,
toggleTool,
importFromSeries,
exportToSeries,
refreshSeriesCharacters,
setSelectedCharacter,
enterDetailMode,
enterEditMode,
exitEditMode,
backToList,
addNewCharacter,
} = useCharacters(config);
const availableSeriesCharacters = useMemo(function (): SeriesCharacterProps[] {
return seriesCharacters.filter(function (sc: SeriesCharacterProps): boolean {
return !characters.some(function (c: CharacterProps): boolean {
return c.seriesCharacterId === sc.id;
});
});
}, [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);
setShowDeleteConfirm(false);
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>
);
}
const isNew: boolean = selectedCharacter?.id === null;
const canExport: boolean = Boolean(bookSeriesId && selectedCharacter?.id && !selectedCharacter.seriesCharacterId);
return (
<div className="flex flex-col h-full">
<ToolDetailHeader
title={selectedCharacter?.name || ''}
defaultTitle={t('characterDetail.newCharacter')}
viewMode={viewMode}
isNew={isNew}
onBack={backToList}
onEdit={enterEditMode}
onSave={handleSave}
onCancel={handleCancel}
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}
/>
}
/>
</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}
/>
</>
)}
</div>
)}
{viewMode === 'detail' && selectedCharacter && (
<div className="p-4">
<CharacterEditorDetail
character={selectedCharacter}
seriesCharacter={getSeriesCharacterForSelected()}
/>
</div>
)}
{viewMode === 'edit' && selectedCharacter && (
<div className="p-4">
<CharacterEditorEdit
character={selectedCharacter}
setCharacter={setSelectedCharacter}
onCharacterChange={handleCharacterChange}
onAddAttribute={addAttribute}
onRemoveAttribute={removeAttribute}
seriesCharacter={getSeriesCharacterForSelected()}
onSyncComplete={refreshSeriesCharacters}
/>
</div>
)}
</div>
{showDeleteConfirm && selectedCharacter?.id && (
<AlertBox
title={t('characterDetail.deleteTitle')}
message={t('characterDetail.deleteMessage', {name: selectedCharacter.name})}
type="danger"
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}
onCancel={function (): void { setShowDeleteConfirm(false); }}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,121 @@
'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';
type AttributeResponse = { type: string; values: Attribute[] }[];
interface CharacterEditorDetailProps {
character: CharacterProps;
seriesCharacter?: SeriesCharacterProps | null;
onLoadAttributes?: (attributes: CharacterAttribute) => void;
}
/**
* CharacterEditorDetail - Version sidebar lecture seule
* Layout linéaire simple, juste les infos essentielles empilées
* PAS de CollapsableArea, PAS de grids
*/
export default function CharacterEditorDetail({
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);
useEffect(function (): void {
if (character?.id !== null) {
getAttributes().then();
}
}, [character?.id]);
async function getAttributes(): Promise<void> {
try {
let response: AttributeResponse;
if (isCurrentlyOffline()) {
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
} else if (book?.localBook) {
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
if (response && onLoadAttributes) {
const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void {
attributes[item.type] = item.values;
});
onLoadAttributes(attributes);
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
}
}
}
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; });
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>
</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)}
</div>
);
}

View File

@@ -0,0 +1,395 @@
'use client';
import React, {useContext, useEffect, useMemo, useState} from 'react';
import {
advancedCharacterElements,
Attribute,
basicCharacterElements,
CharacterAttribute,
characterCategories,
CharacterElement,
CharacterProps,
characterStatus
} from '@/lib/models/Character';
import {SeriesCharacterProps} from '@/lib/models/Series';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TexteAreaInput from '@/components/form/TexteAreaInput';
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';
type AttributeResponse = { type: string; values: Attribute[] }[];
interface CharacterEditorEditProps {
character: CharacterProps;
setCharacter: Dispatch<SetStateAction<CharacterProps | null>>;
onCharacterChange: (key: keyof CharacterProps, value: string | number | null) => void;
onAddAttribute: (section: keyof CharacterProps, attr: Attribute) => Promise<void>;
onRemoveAttribute: (section: keyof CharacterProps, idx: number, id: string) => Promise<void>;
seriesCharacter?: SeriesCharacterProps | null;
onSyncComplete?: () => void;
}
/**
* CharacterEditorEdit - Version sidebar édition
* Mêmes fonctionnalités que CharacterSettingsEdit, layout linéaire
*/
export default function CharacterEditorEdit({
character,
setCharacter,
onCharacterChange,
onAddAttribute,
onRemoveAttribute,
seriesCharacter,
onSyncComplete,
}: CharacterEditorEditProps): React.JSX.Element {
const t = useTranslations();
const {lang} = useContext(LangContext);
const {session} = useContext(SessionContext);
const {errorMessage} = useContext(AlertContext);
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
const {book} = useContext(BookContext);
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()) {
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
} else if (book?.localBook) {
response = await window.electron.invoke<AttributeResponse>('db:character:attributes', {characterId: character?.id});
} else {
response = await System.authGetQueryToServer<AttributeResponse>(
'character/attribute',
session.accessToken,
lang,
{characterId: character?.id}
);
}
if (response) {
const attributes: CharacterAttribute = {};
response.forEach(function (item: { type: string; values: Attribute[] }): void {
attributes[item.type] = item.values;
});
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
if (!prev) return null;
return {
...prev,
physical: attributes.physical ?? [],
psychological: attributes.psychological ?? [],
relations: attributes.relations ?? [],
skills: attributes.skills ?? [],
weaknesses: attributes.weaknesses ?? [],
strengths: attributes.strengths ?? [],
goals: attributes.goals ?? [],
motivations: attributes.motivations ?? [],
arc: attributes.arc ?? [],
secrets: attributes.secrets ?? [],
fears: attributes.fears ?? [],
flaws: attributes.flaws ?? [],
beliefs: attributes.beliefs ?? [],
conflicts: attributes.conflicts ?? [],
quotes: attributes.quotes ?? [],
distinguishingMarks: attributes.distinguishingMarks ?? [],
items: attributes.items ?? [],
affiliations: attributes.affiliations ?? [],
};
});
}
} catch (e: unknown) {
if (e instanceof Error) {
errorMessage(e.message);
}
}
}
return (
<div className="space-y-4">
{/* Informations de base */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.basicInfo')}</h4>
<div className="space-y-3">
<InputField
fieldName={t('characterDetail.name')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.name || ''}
currentValue={character.name || ''}
bookElementId={character.id || ''}
field="name"
elementType="character"
onDownload={function (): void { onCharacterChange('name', seriesCharacter?.name || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.name || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('name', e.target.value);
}}
placeholder={t('characterDetail.namePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.lastName')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.lastName || ''}
currentValue={character.lastName || ''}
bookElementId={character.id || ''}
field="lastName"
elementType="character"
onDownload={function (): void { onCharacterChange('lastName', seriesCharacter?.lastName || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={character.lastName || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('lastName', e.target.value);
}}
placeholder={t('characterDetail.lastNamePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.role')}
input={
<SelectBox
defaultValue={character.category || 'none'}
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, category: e.target.value as CharacterProps['category']} : prev;
});
}}
data={translatedCharacterCategories}
/>
}
/>
<InputField
fieldName={t('characterDetail.gender')}
input={
<TextInput
value={character.gender || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('gender', e.target.value);
}}
placeholder={t('characterDetail.genderPlaceholder')}
/>
}
/>
<InputField
fieldName={t('characterDetail.age')}
input={
<NumberInput
value={character.age ?? null}
onValueChange={function (val: number | null): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, age: val} : prev;
});
}}
placeholder={t('characterDetail.agePlaceholder')}
/>
}
/>
</div>
</div>
{/* Histoire */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.historySection')}</h4>
<div className="space-y-3">
<InputField
fieldName={t('characterDetail.biography')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.biography || ''}
currentValue={character.biography || ''}
bookElementId={character.id || ''}
field="biography"
elementType="character"
onDownload={function (): void { onCharacterChange('biography', seriesCharacter?.biography || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.biography || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('biography', e.target.value);
}}
placeholder={t('characterDetail.biographyPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('characterDetail.roleFull')}
input={
<SyncFieldWrapper
seriesElementId={character.seriesCharacterId}
seriesValue={seriesCharacter?.role || ''}
currentValue={character.role || ''}
bookElementId={character.id || ''}
field="role"
elementType="character"
onDownload={function (): void { onCharacterChange('role', seriesCharacter?.role || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={character.role || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('role', e.target.value);
}}
placeholder={t('characterDetail.roleFullPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</div>
{/* Attributs de base */}
{basicCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
<CharacterSectionElement
key={`basic-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
icon={item.icon}
selectedCharacter={character}
setSelectedCharacter={setCharacter}
handleAddElement={onAddAttribute}
handleRemoveElement={onRemoveAttribute}
/>
);
})}
{/* Toggle Mode Avancé */}
<div className="flex items-center justify-between p-3 bg-secondary/30 rounded-lg border border-secondary/50">
<div className="flex items-center gap-2">
<FontAwesomeIcon icon={faSliders} className="text-primary w-4 h-4"/>
<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 ${
showAdvanced
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-primary hover:bg-secondary'
}`}
>
{showAdvanced ? t('characterDetail.hideAdvanced') : t('characterDetail.showAdvanced')}
</button>
</div>
{/* Sections avancées */}
{showAdvanced && (
<>
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.identitySection')}</h4>
<div className="space-y-3">
<InputField
fieldName={t('characterDetail.species')}
input={
<TextInput
value={character.species || ''}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
onCharacterChange('species', e.target.value);
}}
placeholder={t('characterDetail.speciesPlaceholder')}
/>
}
/>
<InputField
fieldName={t('characterDetail.status')}
input={
<SelectBox
defaultValue={character.status || 'alive'}
onChangeCallBack={function (e: React.ChangeEvent<HTMLSelectElement>): void {
setCharacter(function (prev: CharacterProps | null): CharacterProps | null {
return prev ? {...prev, status: e.target.value as CharacterProps['status']} : prev;
});
}}
data={translatedCharacterStatus}
/>
}
/>
</div>
</div>
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('characterDetail.authorSection')}</h4>
<InputField
fieldName={t('characterDetail.notes')}
input={
<TexteAreaInput
value={character.notes || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onCharacterChange('notes', e.target.value);
}}
placeholder={t('characterDetail.notesPlaceholder')}
/>
}
/>
</div>
{advancedCharacterElements.map(function (item: CharacterElement, index: number): React.JSX.Element {
return (
<CharacterSectionElement
key={`advanced-${index}`}
title={item.title}
section={item.section}
placeholder={item.placeholder}
icon={item.icon}
selectedCharacter={character}
setSelectedCharacter={setCharacter}
handleAddElement={onAddAttribute}
handleRemoveElement={onRemoveAttribute}
/>
);
})}
</>
)}
</div>
);
}

View File

@@ -0,0 +1,116 @@
'use client';
import React, {useState} from 'react';
import {CharacterProps} from '@/lib/models/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';
interface CharacterEditorListProps {
characters: CharacterProps[];
onCharacterClick: (character: CharacterProps) => void;
onAddCharacter: () => void;
}
/**
* CharacterEditorList - Liste des personnages pour ComposerRightBar
* Version compacte sans groupage par catégorie
* PAS de scroll interne (géré par parent ComposerRightBar)
*/
export default function CharacterEditorList({
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">
<InputField
input={
<TextInput
value={searchQuery}
setValue={function (e: React.ChangeEvent<HTMLInputElement>): void {
setSearchQuery(e.target.value);
}}
placeholder={t('characterList.search')}
/>
}
actionIcon={faPlus}
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>
) : (
filteredCharacters.map(function (char: CharacterProps): React.JSX.Element {
return (
<div
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>
);
})
)}
</div>
</div>
);
}