- Deleted `CharacterComponent` and `CharacterDetail` files from the project. - Refactored related logic to improve code maintainability and reduce redundancy.
327 lines
17 KiB
TypeScript
327 lines
17 KiB
TypeScript
'use client';
|
|
import React, {useContext, useEffect, useState} from 'react';
|
|
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';
|
|
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';
|
|
|
|
type AttributeResponse = { type: string; values: Attribute[] }[];
|
|
|
|
interface CharacterSettingsDetailProps {
|
|
character: CharacterProps;
|
|
seriesCharacter?: SeriesCharacterProps | null;
|
|
onLoadAttributes?: (attributes: CharacterAttribute) => void;
|
|
}
|
|
|
|
export default function CharacterSettingsDetail({
|
|
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 [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()) {
|
|
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 getCategoryLabel(): string {
|
|
const cat = characterCategories.find(c => c.value === character.category);
|
|
return cat ? t(cat.label) : character.category || '—';
|
|
}
|
|
|
|
function getStatusLabel(): string {
|
|
const stat = characterStatus.find(s => 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[] || [];
|
|
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>
|
|
);
|
|
}
|
|
|
|
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">
|
|
{/* Image */}
|
|
<div className="shrink-0">
|
|
{character.image ? (
|
|
<div className="w-32 h-32 rounded-2xl border-4 border-primary/50 overflow-hidden shadow-lg">
|
|
<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>
|
|
)}
|
|
</div>
|
|
|
|
{/* Infos principales */}
|
|
<div className="flex-1 min-w-0">
|
|
<h2 className="text-2xl font-bold text-text-primary">
|
|
{character.name} {character.lastName}
|
|
</h2>
|
|
{character.nickname && (
|
|
<p className="text-primary italic mt-1">« {character.nickname} »</p>
|
|
)}
|
|
{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>
|
|
{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>
|
|
)}
|
|
{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>
|
|
)}
|
|
</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">
|
|
<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'}`}>
|
|
{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'}`}>
|
|
{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'}`}>
|
|
{character.role || '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CollapsableArea>
|
|
|
|
{/* 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 gap-3">
|
|
<FontAwesomeIcon icon={faSliders} className="text-primary w-5 h-5"/>
|
|
<span className="text-text-primary font-medium">{t('characterDetail.advancedMode')}</span>
|
|
</div>
|
|
<button
|
|
onClick={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'
|
|
}`}
|
|
>
|
|
{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">
|
|
<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>
|
|
</div>
|
|
<p className={character.species ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
|
|
{character.species || '—'}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-dark-background/30 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>
|
|
</div>
|
|
<p className={character.nationality ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
|
|
{character.nationality || '—'}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-dark-background/30 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>
|
|
</div>
|
|
<p className={character.status ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
|
|
{getStatusLabel()}
|
|
</p>
|
|
</div>
|
|
<div className="p-3 bg-dark-background/30 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>
|
|
</div>
|
|
<p className={character.residence ? 'text-text-primary' : 'text-text-secondary/50 italic'}>
|
|
{character.residence || '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CollapsableArea>
|
|
|
|
{/* 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">
|
|
<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'}`}>
|
|
{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"/>
|
|
<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'}`}>
|
|
{character.catchphrase ? `« ${character.catchphrase} »` : '—'}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</CollapsableArea>
|
|
|
|
{/* 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">
|
|
<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>
|
|
</div>
|
|
<p className={`${character.notes ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
|
|
{character.notes || '—'}
|
|
</p>
|
|
</div>
|
|
<div className="p-4 bg-dark-background/30 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>
|
|
</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}}
|
|
/>
|
|
<span className="text-text-primary font-mono">{character.color}</span>
|
|
</div>
|
|
) : (
|
|
<p className="text-text-secondary/50 italic">—</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CollapsableArea>
|
|
|
|
{/* Attributs avancés */}
|
|
{advancedCharacterElements.map(renderAttributeSection)}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|