- 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.
376 lines
19 KiB
TypeScript
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>
|
|
);
|
|
}
|