Files
ERitors-Scribe-Desktop/components/book/settings/spells/settings/SpellSettingsEdit.tsx
natreex 64ed90d993 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.
2026-03-22 22:37:31 -04:00

376 lines
19 KiB
TypeScript

'use client';
import React, {useState} from 'react';
import {SpellEditState, SpellTagProps} from '@/lib/types/spell';
import {defaultTagColors, spellPowerLevels} from '@/lib/constants/spell';
import {SeriesSpellDetailResponse} from '@/lib/types/series';
import SelectBox, {SelectBoxProps} from '@/components/form/SelectBox';
import Collapse from '@/components/ui/Collapse';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import TextAreaInput from '@/components/form/TextAreaInput';
import SpellTagChip from '@/components/book/settings/spells/SpellTagChip';
import SyncFieldWrapper from '@/components/form/SyncFieldWrapper';
import {AlertTriangle, Book, Eye, Plus, Puzzle, StickyNote, Tag, Wand2, Zap} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import {dynamicBg, dynamicBgWithOpacity, dynamicBorderWithOpacity, dynamicText} from '@/lib/utils/dynamicStyles';
import Button from '@/components/ui/Button';
interface SpellSettingsEditProps {
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;
}
/**
* SpellSettingsEdit - Vue édition pour BookSetting/SerieSetting
* Tous les champs avec SyncFieldWrapper
* Gestion des tags inline
* PAS de scroll interne (géré par parent)
*/
export default function SpellSettingsEdit({
spell,
availableTags,
onSpellChange,
onCreateTag,
seriesSpell,
onSyncComplete,
}: SpellSettingsEditProps): 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 px-2 pb-4">
{/* Informations de base */}
<Collapse variant="card" title={t('spellDetail.basicInfo')} icon={Wand2}>
<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: React.ChangeEvent<HTMLInputElement>): void {
onSpellChange('name', e.target.value);
}}
placeholder={t('spellDetail.namePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.description')}
icon={Book}
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}
>
<TextAreaInput
value={spell.description}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('description', e.target.value);
}}
placeholder={t('spellDetail.descriptionPlaceholder')}
/>
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.appearance')}
icon={Eye}
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}
>
<TextAreaInput
value={spell.appearance}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('appearance', e.target.value);
}}
placeholder={t('spellDetail.appearancePlaceholder')}
/>
</SyncFieldWrapper>
}
/>
</Collapse>
{/* Tags */}
<Collapse variant="card" title={t('spellDetail.tags')} icon={Tag}>
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-2 mb-3">
{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: React.ChangeEvent<HTMLInputElement>): void {
setTagSearchQuery(e.target.value);
}}
placeholder={t('spellDetail.addTag')}
/>
{filteredTags.length > 0 && (
<div className="flex flex-wrap gap-2">
{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.5 px-3 py-1.5 rounded-full text-sm transition-colors duration-200 hover:brightness-110 border ${dynamicBgWithOpacity(tag.color || '#51AE84', '20')} ${dynamicBorderWithOpacity(tag.color || '#51AE84', '50')} ${dynamicText(tag.color || 'var(--color-primary)')}`}
>
<Plus className="w-3 h-3" strokeWidth={1.75}/>
{tag.name}
</button>
);
})}
</div>
)}
{showCreateOption && !isCreatingTag && (
<button
onClick={function (): void {
setIsCreatingTag(true);
}}
className="w-full flex items-center gap-2 px-4 py-2.5 bg-tertiary hover:bg-secondary transition-colors text-left rounded-xl border border-secondary"
>
<Plus className="text-primary w-3 h-3" strokeWidth={1.75}/>
<span className="text-primary font-medium">
{t('spellDetail.createTag', {name: tagSearchQuery})}
</span>
</button>
)}
{isCreatingTag && (
<div className="p-4 bg-tertiary rounded-xl border border-secondary">
<p className="text-text-secondary text-sm mb-3">
{t('spellDetail.createTag', {name: tagSearchQuery})}
</p>
<div className="flex flex-wrap gap-2 mb-3">
{defaultTagColors.map(function (color: string): React.JSX.Element {
return (
<button
key={color}
onClick={function (): void {
setNewTagColor(color);
}}
className={`w-8 h-8 rounded-full transition-all duration-200 ${newTagColor === color ? 'ring-2 ring-offset-2 ring-primary' : 'hover:ring-1 hover:ring-primary/50'} ${dynamicBg(color)}`}
/>
);
})}
</div>
<div className="flex gap-2">
<Button variant="secondary" size="sm" onClick={function (): void {
setIsCreatingTag(false);
}}>
{t('common.cancel')}
</Button>
<Button variant="primary" size="sm" onClick={handleCreateTag}>
{t('common.confirm')}
</Button>
</div>
</div>
)}
</Collapse>
{/* Niveau de puissance */}
<Collapse variant="card" title={t('spellDetail.powerLevel')} icon={Zap}>
<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: React.ChangeEvent<HTMLSelectElement>): void {
onSpellChange('powerLevel', e.target.value === 'none' ? null : e.target.value);
}}
data={getLocalizedPowerLevels()}
/>
</SyncFieldWrapper>
</Collapse>
{/* Composants */}
<Collapse variant="card" title={t('spellDetail.components')} icon={Puzzle}>
<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}
>
<TextAreaInput
value={spell.components || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('components', e.target.value || null);
}}
placeholder={t('spellDetail.componentsPlaceholder')}
/>
</SyncFieldWrapper>
</Collapse>
{/* Limitations */}
<Collapse variant="card" title={t('spellDetail.limitations')} icon={AlertTriangle}>
<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}
>
<TextAreaInput
value={spell.limitations || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('limitations', e.target.value || null);
}}
placeholder={t('spellDetail.limitationsPlaceholder')}
/>
</SyncFieldWrapper>
</Collapse>
{/* Notes */}
<Collapse variant="card" title={t('spellDetail.notes')} icon={StickyNote}>
<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}
>
<TextAreaInput
value={spell.notes || ''}
setValue={function (e: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('notes', e.target.value || null);
}}
placeholder={t('spellDetail.notesPlaceholder')}
/>
</SyncFieldWrapper>
</Collapse>
</div>
);
}