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.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

View File

@@ -1,18 +1,15 @@
'use client';
import React, {useCallback, useContext, useMemo, useState} from 'react';
import {useSpells, UseSpellsConfig} from '@/hooks/settings/useSpells';
import {useTranslations} from 'next-intl';
import {SpellEditState, SpellListItem} from '@/lib/models/Spell';
import {SeriesSpellListItem} from '@/lib/models/Series';
import {BookContext} from '@/context/BookContext';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faSpinner, faToggleOn} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from '@/lib/i18n';
import {SpellEditState, SpellListItem} from '@/lib/types/spell';
import {SeriesSpellListItem} from '@/lib/types/series';
import {BookContext, BookContextProps} from '@/context/BookContext';
import PulseLoader from '@/components/ui/PulseLoader';
import ToolDetailHeader from '@/components/book/settings/ToolDetailHeader';
import InputField from '@/components/form/InputField';
import ToggleSwitch from '@/components/form/ToggleSwitch';
import SeriesImportSelector from '@/components/form/SeriesImportSelector';
import AlertBox from '@/components/AlertBox';
import AlertBox from '@/components/ui/AlertBox';
import SpellTagManager from '@/components/book/settings/spells/SpellTagManager';
import SpellEditorList from './SpellEditorList';
@@ -25,16 +22,16 @@ import SpellEditorEdit from './SpellEditorEdit';
*/
export default function SpellEditor(): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const config: UseSpellsConfig = useMemo(function (): UseSpellsConfig {
return {
entityType: 'book',
entityId: book?.bookId || '',
};
}, [book?.bookId]);
const {
spells,
seriesSpells,
@@ -64,7 +61,7 @@ export default function SpellEditor(): React.JSX.Element {
deleteTag,
handleSyncComplete,
} = useSpells(config);
const availableSeriesSpells = useMemo(function (): SeriesSpellListItem[] {
return seriesSpells.filter(function (ss: SeriesSpellListItem): boolean {
return !spells.some(function (s: SpellListItem): boolean {
@@ -72,19 +69,19 @@ export default function SpellEditor(): React.JSX.Element {
});
});
}, [seriesSpells, spells]);
const handleSpellChange = useCallback(function (key: keyof SpellEditState, value: string | string[] | null): void {
updateSpellField(key, value);
}, [updateSpellField]);
async function handleSave(): Promise<void> {
await exitEditMode(true);
}
function handleCancel(): void {
exitEditMode(false);
}
async function handleDelete(): Promise<void> {
if (selectedSpell?.id) {
await deleteSpell(selectedSpell.id);
@@ -92,82 +89,75 @@ export default function SpellEditor(): React.JSX.Element {
backToList();
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-8">
<FontAwesomeIcon icon={faSpinner} className="w-6 h-6 text-primary animate-spin"/>
</div>
);
return <PulseLoader size="sm"/>;
}
const isNew: boolean = selectedSpell?.id === null;
const canExport: boolean = Boolean(bookSeriesId && selectedSpell?.id && !selectedSpell.seriesSpellId);
return (
<div className="flex flex-col h-full">
<ToolDetailHeader
title={selectedSpell?.name || ''}
defaultTitle={t('spellDetail.newSpell')}
viewMode={viewMode}
title={showTagManager ? t('spellTagManager.title') : (selectedSpell?.name || '')}
defaultTitle={showTagManager ? t('spellTagManager.title') : t('spellDetail.newSpell')}
viewMode={showTagManager ? 'detail' : viewMode}
isNew={isNew}
onBack={backToList}
onEdit={enterEditMode}
onBack={showTagManager ? function (): void {
setShowTagManager(false);
} : backToList}
onEdit={showTagManager ? undefined : enterEditMode}
onSave={handleSave}
onCancel={handleCancel}
onDelete={function (): void { setShowDeleteConfirm(true); }}
onExport={canExport ? exportToSeries : undefined}
showExport={canExport}
showDelete={Boolean(selectedSpell?.id)}
onDelete={showTagManager ? undefined : function (): void {
setShowDeleteConfirm(true);
}}
onExport={canExport && !showTagManager ? exportToSeries : undefined}
showExport={canExport && !showTagManager}
showDelete={!showTagManager && Boolean(selectedSpell?.id)}
/>
<div className="flex-1 overflow-y-auto">
{viewMode === 'list' && (
{showTagManager && (
<SpellTagManager
tags={tags}
onCreateTag={createTag}
onUpdateTag={updateTag}
onDeleteTag={deleteTag}
/>
)}
{!showTagManager && viewMode === 'list' && (
<div className="space-y-3 p-2">
{/* Toggle tool */}
<div className="bg-secondary/20 rounded-lg p-3 border border-secondary/30">
<InputField
icon={faToggleOn}
fieldName={t('spellComponent.enableTool')}
input={
<ToggleSwitch
checked={toolEnabled}
onChange={toggleTool}
/>
}
{/* Import from series */}
{bookSeriesId && availableSeriesSpells.length > 0 && (
<SeriesImportSelector
availableItems={availableSeriesSpells.map(function (ss: SeriesSpellListItem) {
return {
id: ss.id,
name: ss.name
};
})}
onImport={importFromSeries}
placeholder={t('seriesImport.selectElement')}
label={t('seriesImport.importFromSeries')}
/>
</div>
{toolEnabled && (
<>
{/* Import from series */}
{bookSeriesId && availableSeriesSpells.length > 0 && (
<SeriesImportSelector
availableItems={availableSeriesSpells.map(function (ss: SeriesSpellListItem) {
return {
id: ss.id,
name: ss.name
};
})}
onImport={importFromSeries}
placeholder={t('seriesImport.selectElement')}
label={t('seriesImport.importFromSeries')}
/>
)}
<SpellEditorList
spells={spells}
tags={tags}
onSpellClick={enterDetailMode}
onAddSpell={addNewSpell}
onManageTags={function (): void { setShowTagManager(true); }}
/>
</>
)}
<SpellEditorList
spells={spells}
tags={tags}
onSpellClick={enterDetailMode}
onAddSpell={addNewSpell}
onManageTags={function (): void {
setShowTagManager(true);
}}
/>
</div>
)}
{viewMode === 'detail' && selectedSpell && (
{!showTagManager && viewMode === 'detail' && selectedSpell && (
<div className="p-4">
<SpellEditorDetail
spell={selectedSpell}
@@ -176,8 +166,8 @@ export default function SpellEditor(): React.JSX.Element {
/>
</div>
)}
{viewMode === 'edit' && selectedSpell && (
{!showTagManager && viewMode === 'edit' && selectedSpell && (
<div className="p-4">
<SpellEditorEdit
spell={selectedSpell}
@@ -190,7 +180,7 @@ export default function SpellEditor(): React.JSX.Element {
</div>
)}
</div>
{showDeleteConfirm && selectedSpell?.id && (
<AlertBox
title={t('spellDetail.deleteTitle')}
@@ -199,19 +189,12 @@ export default function SpellEditor(): React.JSX.Element {
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}
onCancel={function (): void { setShowDeleteConfirm(false); }}
/>
)}
{showTagManager && (
<SpellTagManager
tags={tags}
onBack={function (): void { setShowTagManager(false); }}
onCreateTag={createTag}
onUpdateTag={updateTag}
onDeleteTag={deleteTag}
onCancel={function (): void {
setShowDeleteConfirm(false);
}}
/>
)}
</div>
);
}

View File

@@ -1,10 +1,12 @@
'use client';
import React from 'react';
import {SpellEditState, spellPowerLevels, SpellTagProps} from '@/lib/models/Spell';
import {SeriesSpellDetailResponse} from '@/lib/models/Series';
import {SelectBoxProps} from '@/shared/interface';
import {SpellEditState, SpellTagProps} from '@/lib/types/spell';
import {spellPowerLevels} from '@/lib/constants/spell';
import {SeriesSpellDetailResponse} from '@/lib/types/series';
import {SelectBoxProps} from '@/components/form/SelectBox';
import SpellTagChip from '@/components/book/settings/spells/SpellTagChip';
import {useTranslations} from 'next-intl';
import DetailField from '@/components/ui/DetailField';
import {useTranslations} from '@/lib/i18n';
interface SpellEditorDetailProps {
spell: SpellEditState;
@@ -18,18 +20,18 @@ interface SpellEditorDetailProps {
* PAS de CollapsableArea, PAS de grids
*/
export default function SpellEditorDetail({
spell,
availableTags,
seriesSpell,
}: SpellEditorDetailProps): React.JSX.Element {
spell,
availableTags,
seriesSpell,
}: SpellEditorDetailProps): React.JSX.Element {
const t = useTranslations();
function getSelectedTags(): SpellTagProps[] {
return availableTags.filter(function (tag: SpellTagProps): boolean {
return spell.tags.includes(tag.id);
});
}
function getLocalizedPowerLevel(): string {
if (!spell.powerLevel || spell.powerLevel === 'none') {
return '';
@@ -39,31 +41,22 @@ export default function SpellEditorDetail({
});
return level ? t(level.label) : spell.powerLevel;
}
function renderField(label: string, value: string | null | undefined): React.JSX.Element | null {
if (!value) return null;
return (
<div className="mb-3">
<span className="text-text-secondary text-xs block mb-1">{label}</span>
<p className="text-text-primary text-sm whitespace-pre-wrap">{value}</p>
</div>
);
}
const selectedTags: SpellTagProps[] = getSelectedTags();
const powerLevelText: string = getLocalizedPowerLevel();
return (
<div>
<h3 className="text-text-primary font-semibold text-base mb-4">{spell.name}</h3>
{renderField(t('spellDetail.description'), spell.description)}
{renderField(t('spellDetail.appearance'), spell.appearance)}
{powerLevelText && renderField(t('spellDetail.powerLevel'), powerLevelText)}
{renderField(t('spellDetail.components'), spell.components)}
{renderField(t('spellDetail.limitations'), spell.limitations)}
{renderField(t('spellDetail.notes'), spell.notes)}
<DetailField variant="compact" label={t('spellDetail.description')} value={spell.description}/>
<DetailField variant="compact" label={t('spellDetail.appearance')} value={spell.appearance}/>
{powerLevelText &&
<DetailField variant="compact" label={t('spellDetail.powerLevel')} value={powerLevelText}/>}
<DetailField variant="compact" label={t('spellDetail.components')} value={spell.components}/>
<DetailField variant="compact" label={t('spellDetail.limitations')} value={spell.limitations}/>
<DetailField variant="compact" label={t('spellDetail.notes')} value={spell.notes}/>
{selectedTags.length > 0 && (
<div className="mb-3">
<span className="text-text-secondary text-xs block mb-1">{t('spellDetail.tags')}</span>

View File

@@ -1,17 +1,18 @@
'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 {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 TexteAreaInput from '@/components/form/TexteAreaInput';
import SelectBox from '@/components/form/SelectBox';
import TextAreaInput from '@/components/form/TextAreaInput';
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';
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;
@@ -28,32 +29,32 @@ interface SpellEditorEditProps {
* Gestion des tags, SyncFieldWrapper, tous les champs
*/
export default function SpellEditorEdit({
spell,
availableTags,
onSpellChange,
onCreateTag,
seriesSpell,
onSyncComplete,
}: SpellEditorEditProps): React.JSX.Element {
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);
@@ -61,13 +62,13 @@ export default function SpellEditorEdit({
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);
@@ -77,7 +78,7 @@ export default function SpellEditorEdit({
setNewTagColor(defaultTagColors[0]);
}
}
function getLocalizedPowerLevels(): SelectBoxProps[] {
return spellPowerLevels.map(function (level: SelectBoxProps): SelectBoxProps {
return {
@@ -86,7 +87,7 @@ export default function SpellEditorEdit({
};
});
}
const filteredTags: SpellTagProps[] = getFilteredAvailableTags();
const selectedTags: SpellTagProps[] = getSelectedTags();
const showCreateOption: boolean = Boolean(
@@ -95,11 +96,11 @@ export default function SpellEditorEdit({
return tag.name.toLowerCase() === tagSearchQuery.toLowerCase();
})
);
return (
<div className="space-y-4">
{/* Informations de base */}
<div className="border-b border-secondary/30 pb-3">
<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
@@ -112,7 +113,9 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="name"
elementType="spell"
onDownload={function (): void { onSpellChange('name', seriesSpell?.name || ''); }}
onDownload={function (): void {
onSpellChange('name', seriesSpell?.name || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -125,7 +128,7 @@ export default function SpellEditorEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.description')}
input={
@@ -136,10 +139,12 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="description"
elementType="spell"
onDownload={function (): void { onSpellChange('description', seriesSpell?.description || ''); }}
onDownload={function (): void {
onSpellChange('description', seriesSpell?.description || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={spell.description}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('description', e.target.value);
@@ -149,7 +154,7 @@ export default function SpellEditorEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.appearance')}
input={
@@ -160,10 +165,12 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="appearance"
elementType="spell"
onDownload={function (): void { onSpellChange('appearance', seriesSpell?.appearance || ''); }}
onDownload={function (): void {
onSpellChange('appearance', seriesSpell?.appearance || '');
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={spell.appearance}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('appearance', e.target.value);
@@ -175,9 +182,9 @@ export default function SpellEditorEdit({
/>
</div>
</div>
{/* Tags */}
<div className="border-b border-secondary/30 pb-3">
<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 && (
@@ -187,13 +194,15 @@ export default function SpellEditorEdit({
<SpellTagChip
key={tag.id}
tag={tag}
onRemove={function (): void { handleRemoveTag(tag.id); }}
onRemove={function (): void {
handleRemoveTag(tag.id);
}}
/>
);
})}
</div>
)}
<TextInput
value={tagSearchQuery}
setValue={function (e: ChangeEvent<HTMLInputElement>): void {
@@ -201,43 +210,42 @@ export default function SpellEditorEdit({
}}
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'
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)')}`}
>
<FontAwesomeIcon icon={faPlus} className="w-2.5 h-2.5"/>
<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/50 transition-colors text-left rounded-lg border border-secondary/50"
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"
>
<FontAwesomeIcon icon={faPlus} className="text-primary w-3 h-3"/>
<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/50">
<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>
@@ -246,34 +254,31 @@ export default function SpellEditorEdit({
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}}
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
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"
>
<Button variant="secondary" size="sm" onClick={function (): void {
setIsCreatingTag(false);
}}>
{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"
>
</Button>
<Button variant="primary" size="sm" onClick={handleCreateTag}>
{t('common.confirm')}
</button>
</Button>
</div>
</div>
)}
</div>
</div>
{/* Niveau de puissance */}
<div className="border-b border-secondary/30 pb-3">
<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}
@@ -282,7 +287,9 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="powerLevel"
elementType="spell"
onDownload={function (): void { onSpellChange('powerLevel', seriesSpell?.powerLevel || null); }}
onDownload={function (): void {
onSpellChange('powerLevel', seriesSpell?.powerLevel || null);
}}
onSyncComplete={onSyncComplete}
>
<SelectBox
@@ -294,9 +301,9 @@ export default function SpellEditorEdit({
/>
</SyncFieldWrapper>
</div>
{/* Composants */}
<div className="border-b border-secondary/30 pb-3">
<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}
@@ -305,10 +312,12 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="components"
elementType="spell"
onDownload={function (): void { onSpellChange('components', seriesSpell?.components || null); }}
onDownload={function (): void {
onSpellChange('components', seriesSpell?.components || null);
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={spell.components || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('components', e.target.value || null);
@@ -317,9 +326,9 @@ export default function SpellEditorEdit({
/>
</SyncFieldWrapper>
</div>
{/* Limitations */}
<div className="border-b border-secondary/30 pb-3">
<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}
@@ -328,10 +337,12 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="limitations"
elementType="spell"
onDownload={function (): void { onSpellChange('limitations', seriesSpell?.limitations || null); }}
onDownload={function (): void {
onSpellChange('limitations', seriesSpell?.limitations || null);
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={spell.limitations || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('limitations', e.target.value || null);
@@ -340,7 +351,7 @@ export default function SpellEditorEdit({
/>
</SyncFieldWrapper>
</div>
{/* Notes */}
<div className="pb-3">
<h4 className="text-text-primary font-medium text-sm mb-3">{t('spellDetail.notes')}</h4>
@@ -351,10 +362,12 @@ export default function SpellEditorEdit({
bookElementId={spell.id || ''}
field="notes"
elementType="spell"
onDownload={function (): void { onSpellChange('notes', seriesSpell?.notes || null); }}
onDownload={function (): void {
onSpellChange('notes', seriesSpell?.notes || null);
}}
onSyncComplete={onSyncComplete}
>
<TexteAreaInput
<TextAreaInput
value={spell.notes || ''}
setValue={function (e: ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('notes', e.target.value || null);

View File

@@ -1,12 +1,15 @@
'use client';
import React, {useState} from 'react';
import {SpellListItem, SpellTagProps} from '@/lib/models/Spell';
import {SpellListItem, SpellTagProps} from '@/lib/types/spell';
import InputField from '@/components/form/InputField';
import TextInput from '@/components/form/TextInput';
import SpellTagChip from '@/components/book/settings/spells/SpellTagChip';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faChevronRight, faHatWizard, faPlus, faTags} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {Plus, Tag, Wand2} from 'lucide-react';
import EntityListItem from '@/components/ui/EntityListItem';
import AvatarIcon from '@/components/ui/AvatarIcon';
import {useTranslations} from '@/lib/i18n';
import IconContainer from '@/components/ui/IconContainer';
import {dynamicBorderLeft} from '@/lib/utils/dynamicStyles';
interface SpellEditorListProps {
spells: SpellListItem[];
@@ -21,16 +24,16 @@ interface SpellEditorListProps {
* Mêmes fonctionnalités que SpellSettingsList, layout condensé
*/
export default function SpellEditorList({
spells,
tags,
onSpellClick,
onAddSpell,
onManageTags,
}: SpellEditorListProps): React.JSX.Element {
spells,
tags,
onSpellClick,
onAddSpell,
onManageTags,
}: SpellEditorListProps): React.JSX.Element {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState<string>('');
const [selectedTagId, setSelectedTagId] = useState<string | null>(null);
function getFilteredSpells(): SpellListItem[] {
return spells.filter(function (spell: SpellListItem): boolean {
const matchesSearch: boolean = spell.name.toLowerCase().includes(searchQuery.toLowerCase());
@@ -40,9 +43,9 @@ export default function SpellEditorList({
return matchesSearch && matchesTag;
});
}
const filteredSpells: SpellListItem[] = getFilteredSpells();
return (
<div className="space-y-3">
<div className="px-2">
@@ -56,22 +59,24 @@ export default function SpellEditorList({
placeholder={t('spellList.search')}
/>
}
actionIcon={faPlus}
actionIcon={Plus}
actionLabel={t('spellList.add')}
addButtonCallBack={async function (): Promise<void> {
onAddSpell();
}}
/>
</div>
{/* Tag filter + manage button */}
<div className="px-2 flex items-center gap-2 flex-wrap">
<button
onClick={function (): void { setSelectedTagId(null); }}
onClick={function (): void {
setSelectedTagId(null);
}}
className={`px-2 py-1 text-xs rounded-full transition-colors ${
selectedTagId === null
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-secondary hover:bg-secondary'
? 'bg-primary text-text-primary'
: 'bg-secondary text-text-secondary hover:bg-secondary'
}`}
>
{t('spellList.allTags')}
@@ -80,13 +85,14 @@ export default function SpellEditorList({
return (
<button
key={tag.id}
onClick={function (): void { setSelectedTagId(tag.id === selectedTagId ? null : tag.id); }}
onClick={function (): void {
setSelectedTagId(tag.id === selectedTagId ? null : tag.id);
}}
className={`px-2 py-1 text-xs rounded-full transition-colors ${
selectedTagId === tag.id
? 'bg-primary text-white'
: 'bg-secondary/50 text-text-secondary hover:bg-secondary'
}`}
style={selectedTagId === tag.id ? {} : {borderLeft: `3px solid ${tag.color}`}}
? 'bg-primary text-text-primary'
: 'bg-secondary text-text-secondary hover:bg-secondary'
} ${selectedTagId !== tag.id && tag.color ? dynamicBorderLeft(tag.color) : ''}`}
>
{tag.name}
</button>
@@ -94,19 +100,17 @@ export default function SpellEditorList({
})}
<button
onClick={onManageTags}
className="px-2 py-1 text-xs rounded-full bg-secondary/30 text-text-secondary hover:bg-secondary transition-colors flex items-center gap-1"
className="px-2 py-1 text-xs rounded-full bg-secondary text-text-secondary hover:bg-secondary transition-colors flex items-center gap-1"
>
<FontAwesomeIcon icon={faTags} className="w-3 h-3"/>
<Tag className="w-3 h-3" strokeWidth={1.75}/>
{t('spellList.manageTags')}
</button>
</div>
<div className="px-2 space-y-2">
{filteredSpells.length === 0 ? (
<div className="flex flex-col items-center justify-center py-8 text-center">
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-3">
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-8 h-8"/>
</div>
<IconContainer icon={Wand2} size="lg" shape="circle"/>
<h3 className="text-text-primary font-semibold text-base mb-1">
{t('spellList.noSpells')}
</h3>
@@ -117,43 +121,29 @@ export default function SpellEditorList({
) : (
filteredSpells.map(function (spell: SpellListItem): React.JSX.Element {
return (
<div
<EntityListItem
key={spell.id}
onClick={function (): void { onSpellClick(spell); }}
className="group flex items-center p-3 bg-secondary/30 rounded-lg border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md transition-all duration-200 hover:border-primary/50"
>
<div className="w-10 h-10 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-sm group-hover:scale-110 transition-transform flex items-center justify-center">
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-5 h-5"/>
</div>
<div className="ml-3 flex-1 min-w-0">
<div className="text-text-primary font-semibold text-sm group-hover:text-primary transition-colors truncate">
{spell.name}
size="sm"
onClick={function (): void {
onSpellClick(spell);
}}
avatar={<AvatarIcon size="sm" icon={Wand2}/>}
title={spell.name}
subtitle={spell.description}
extra={spell.tags.length > 0 ? (
<div className="flex flex-wrap gap-1">
{spell.tags.slice(0, 2).map(function (tag: SpellTagProps): React.JSX.Element {
return <SpellTagChip key={tag.id} tag={tag} size="sm"/>;
})}
{spell.tags.length > 2 && (
<span
className="text-muted text-xs px-1.5 py-0.5 bg-secondary rounded-full">
+{spell.tags.length - 2}
</span>
)}
</div>
<div className="text-muted text-xs truncate">
{spell.description}
</div>
{spell.tags.length > 0 && (
<div className="flex flex-wrap gap-1 mt-1">
{spell.tags.slice(0, 2).map(function (tag: SpellTagProps): React.JSX.Element {
return <SpellTagChip key={tag.id} tag={tag} size="sm"/>;
})}
{spell.tags.length > 2 && (
<span className="text-muted text-xs px-1.5 py-0.5 bg-secondary/50 rounded-full">
+{spell.tags.length - 2}
</span>
)}
</div>
)}
</div>
<div className="w-6 flex justify-center">
<FontAwesomeIcon
icon={faChevronRight}
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-3 h-3"
/>
</div>
</div>
) : undefined}
/>
);
})
)}