Files
ERitors-Scribe-Desktop/components/book/settings/spells/editor/SpellEditorEdit.tsx
natreex 209dc6f85a Remove CharacterComponent and CharacterDetail components
- Deleted `CharacterComponent` and `CharacterDetail` files from the project.
- Refactored related logic to improve code maintainability and reduce redundancy.
2026-02-05 14:12:08 -05:00

369 lines
18 KiB
TypeScript

'use client';
import React, {ChangeEvent, useState} from 'react';
import {defaultTagColors, SpellEditState, spellPowerLevels, SpellTagProps} from '@/lib/models/Spell';
import {SeriesSpellDetailResponse} from '@/lib/models/Series';
import {SelectBoxProps} from '@/shared/interface';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TexteAreaInput from '@/components/form/TexteAreaInput';
import SelectBox from '@/components/form/SelectBox';
import SpellTagChip from '@/components/book/settings/spells/SpellTagChip';
import SyncFieldWrapper from '@/components/form/SyncFieldWrapper';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faPlus} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
interface SpellEditorEditProps {
spell: SpellEditState;
availableTags: SpellTagProps[];
onSpellChange: (key: keyof SpellEditState, value: string | string[] | null) => void;
onCreateTag: (name: string, color: string) => Promise<SpellTagProps | null>;
seriesSpell?: SeriesSpellDetailResponse | null;
onSyncComplete?: () => void;
}
/**
* SpellEditorEdit - Version sidebar édition
* Mêmes fonctionnalités que SpellSettingsEdit, layout linéaire
* Gestion des tags, SyncFieldWrapper, tous les champs
*/
export default function SpellEditorEdit({
spell,
availableTags,
onSpellChange,
onCreateTag,
seriesSpell,
onSyncComplete,
}: SpellEditorEditProps): React.JSX.Element {
const t = useTranslations();
const [tagSearchQuery, setTagSearchQuery] = useState<string>('');
const [isCreatingTag, setIsCreatingTag] = useState<boolean>(false);
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
function handleAddTag(tagId: string): void {
if (!spell.tags.includes(tagId)) {
onSpellChange('tags', [...spell.tags, tagId]);
}
setTagSearchQuery('');
}
function handleRemoveTag(tagId: string): void {
onSpellChange('tags', spell.tags.filter(function (id: string): boolean {
return id !== tagId;
}));
}
function getFilteredAvailableTags(): SpellTagProps[] {
return availableTags.filter(function (tag: SpellTagProps): boolean {
const notAlreadyAdded: boolean = !spell.tags.includes(tag.id);
const matchesSearch: boolean = tag.name.toLowerCase().includes(tagSearchQuery.toLowerCase());
return notAlreadyAdded && matchesSearch;
});
}
function getSelectedTags(): SpellTagProps[] {
return availableTags.filter(function (tag: SpellTagProps): boolean {
return spell.tags.includes(tag.id);
});
}
async function handleCreateTag(): Promise<void> {
if (!tagSearchQuery.trim()) return;
const newTag: SpellTagProps | null = await onCreateTag(tagSearchQuery.trim(), newTagColor);
if (newTag) {
handleAddTag(newTag.id);
setIsCreatingTag(false);
setNewTagColor(defaultTagColors[0]);
}
}
function getLocalizedPowerLevels(): SelectBoxProps[] {
return spellPowerLevels.map(function (level: SelectBoxProps): SelectBoxProps {
return {
value: level.value,
label: t(level.label),
};
});
}
const filteredTags: SpellTagProps[] = getFilteredAvailableTags();
const selectedTags: SpellTagProps[] = getSelectedTags();
const showCreateOption: boolean = Boolean(
tagSearchQuery.trim() &&
!availableTags.some(function (tag: SpellTagProps): boolean {
return tag.name.toLowerCase() === tagSearchQuery.toLowerCase();
})
);
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('spellDetail.basicInfo')}</h4>
<div className="space-y-3">
<InputField
fieldName={t('spellDetail.name')}
input={
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.name || ''}
currentValue={spell.name}
bookElementId={spell.id || ''}
field="name"
elementType="spell"
onDownload={function (): void { onSpellChange('name', seriesSpell?.name || ''); }}
onSyncComplete={onSyncComplete}
>
<TextInput
value={spell.name}
setValue={function (e: ChangeEvent<HTMLInputElement>): void {
onSpellChange('name', e.target.value);
}}
placeholder={t('spellDetail.namePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.description')}
input={
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.description || ''}
currentValue={spell.description}
bookElementId={spell.id || ''}
field="description"
elementType="spell"
onDownload={function (): void { onSpellChange('description', seriesSpell?.description || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={spell.description}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('description', e.target.value);
}}
placeholder={t('spellDetail.descriptionPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.appearance')}
input={
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.appearance || ''}
currentValue={spell.appearance}
bookElementId={spell.id || ''}
field="appearance"
elementType="spell"
onDownload={function (): void { onSpellChange('appearance', seriesSpell?.appearance || ''); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={spell.appearance}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('appearance', e.target.value);
}}
placeholder={t('spellDetail.appearancePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</div>
</div>
{/* Tags */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.tags')}</h4>
<div className="space-y-3">
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedTags.map(function (tag: SpellTagProps): React.JSX.Element {
return (
<SpellTagChip
key={tag.id}
tag={tag}
onRemove={function (): void { handleRemoveTag(tag.id); }}
/>
);
})}
</div>
)}
<TextInput
value={tagSearchQuery}
setValue={function (e: ChangeEvent<HTMLInputElement>): void {
setTagSearchQuery(e.target.value);
}}
placeholder={t('spellDetail.addTag')}
/>
{filteredTags.length > 0 && (
<div className="flex flex-wrap gap-1.5">
{filteredTags.map(function (tag: SpellTagProps): React.JSX.Element {
return (
<button
key={tag.id}
onClick={function (): void { handleAddTag(tag.id); }}
className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs transition-all duration-200 hover:scale-105 border"
style={{
backgroundColor: `${tag.color || '#3B82F6'}20`,
borderColor: `${tag.color || '#3B82F6'}50`,
color: tag.color || '#3B82F6'
}}
>
<FontAwesomeIcon icon={faPlus} className="w-2.5 h-2.5"/>
{tag.name}
</button>
);
})}
</div>
)}
{showCreateOption && !isCreatingTag && (
<button
onClick={function (): void { setIsCreatingTag(true); }}
className="w-full flex items-center gap-2 px-3 py-2 bg-tertiary hover:bg-secondary/50 transition-colors text-left rounded-lg border border-secondary/50"
>
<FontAwesomeIcon icon={faPlus} className="text-primary w-3 h-3"/>
<span className="text-primary font-medium text-sm">
{t('spellDetail.createTag', {name: tagSearchQuery})}
</span>
</button>
)}
{isCreatingTag && (
<div className="p-3 bg-tertiary rounded-lg border border-secondary/50">
<p className="text-text-secondary text-xs mb-2">
{t('spellDetail.createTag', {name: tagSearchQuery})}
</p>
<div className="flex flex-wrap gap-1.5 mb-2">
{defaultTagColors.map(function (color: string): React.JSX.Element {
return (
<button
key={color}
onClick={function (): void { setNewTagColor(color); }}
className={`w-6 h-6 rounded-full transition-all duration-200 ${newTagColor === color ? 'ring-2 ring-offset-1 ring-primary scale-110' : 'hover:scale-110'}`}
style={{backgroundColor: color}}
/>
);
})}
</div>
<div className="flex gap-2">
<button
onClick={function (): void { setIsCreatingTag(false); }}
className="flex-1 py-1.5 px-2 bg-secondary/50 text-text-primary rounded-lg hover:bg-secondary transition-colors text-sm"
>
{t('common.cancel')}
</button>
<button
onClick={handleCreateTag}
className="flex-1 py-1.5 px-2 bg-primary text-text-primary rounded-lg hover:bg-primary-dark transition-colors text-sm"
>
{t('common.confirm')}
</button>
</div>
</div>
)}
</div>
</div>
{/* Niveau de puissance */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.powerLevel')}</h4>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.powerLevel || 'none'}
currentValue={spell.powerLevel || 'none'}
bookElementId={spell.id || ''}
field="powerLevel"
elementType="spell"
onDownload={function (): void { onSpellChange('powerLevel', seriesSpell?.powerLevel || null); }}
onSyncComplete={onSyncComplete}
>
<SelectBox
defaultValue={spell.powerLevel || 'none'}
onChangeCallBack={function (e: ChangeEvent<HTMLSelectElement>): void {
onSpellChange('powerLevel', e.target.value === 'none' ? null : e.target.value);
}}
data={getLocalizedPowerLevels()}
/>
</SyncFieldWrapper>
</div>
{/* Composants */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.components')}</h4>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.components || ''}
currentValue={spell.components || ''}
bookElementId={spell.id || ''}
field="components"
elementType="spell"
onDownload={function (): void { onSpellChange('components', seriesSpell?.components || null); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={spell.components || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('components', e.target.value || null);
}}
placeholder={t('spellDetail.componentsPlaceholder')}
/>
</SyncFieldWrapper>
</div>
{/* Limitations */}
<div className="border-b border-secondary/30 pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.limitations')}</h4>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.limitations || ''}
currentValue={spell.limitations || ''}
bookElementId={spell.id || ''}
field="limitations"
elementType="spell"
onDownload={function (): void { onSpellChange('limitations', seriesSpell?.limitations || null); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={spell.limitations || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('limitations', e.target.value || null);
}}
placeholder={t('spellDetail.limitationsPlaceholder')}
/>
</SyncFieldWrapper>
</div>
{/* Notes */}
<div className="pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.notes')}</h4>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.notes || ''}
currentValue={spell.notes || ''}
bookElementId={spell.id || ''}
field="notes"
elementType="spell"
onDownload={function (): void { onSpellChange('notes', seriesSpell?.notes || null); }}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
value={spell.notes || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('notes', e.target.value || null);
}}
placeholder={t('spellDetail.notesPlaceholder')}
/>
</SyncFieldWrapper>
</div>
</div>
);
}