- 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.
382 lines
19 KiB
TypeScript
382 lines
19 KiB
TypeScript
'use client';
|
|
import React, {ChangeEvent, 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 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 {Plus} from 'lucide-react';
|
|
import {useTranslations} from '@/lib/i18n';
|
|
import {dynamicBg, dynamicBgWithOpacity, dynamicBorderWithOpacity, dynamicText} from '@/lib/utils/dynamicStyles';
|
|
import Button from '@/components/ui/Button';
|
|
|
|
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 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}
|
|
>
|
|
<TextAreaInput
|
|
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}
|
|
>
|
|
<TextAreaInput
|
|
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 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-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-2.5 h-2.5" strokeWidth={1.75}/>
|
|
{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 transition-colors text-left rounded-lg border border-secondary"
|
|
>
|
|
<Plus className="text-primary w-3 h-3" strokeWidth={1.75}/>
|
|
<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">
|
|
<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' : '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>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Niveau de puissance */}
|
|
<div className="border-b border-secondary 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 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}
|
|
>
|
|
<TextAreaInput
|
|
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 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}
|
|
>
|
|
<TextAreaInput
|
|
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}
|
|
>
|
|
<TextAreaInput
|
|
value={spell.notes || ''}
|
|
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
|
|
onSpellChange('notes', e.target.value || null);
|
|
}}
|
|
placeholder={t('spellDetail.notesPlaceholder')}
|
|
/>
|
|
</SyncFieldWrapper>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|