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,8 +1,8 @@
'use client';
import React from 'react';
import {SpellTagProps} from "@/lib/models/Spell";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faX} from "@fortawesome/free-solid-svg-icons";
import {SpellTagProps} from "@/lib/types/spell";
import {X} from 'lucide-react';
import {dynamicBg, dynamicText} from "@/lib/utils/dynamicStyles";
interface SpellTagChipProps {
tag: SpellTagProps;
@@ -11,6 +11,16 @@ interface SpellTagChipProps {
size?: 'sm' | 'md';
}
function getContrastColor(hexColor: string | null): string {
if (!hexColor) return 'var(--color-text-primary)';
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? 'var(--color-darkest-background)' : 'var(--color-text-primary)';
}
export default function SpellTagChip(
{
tag,
@@ -19,34 +29,25 @@ export default function SpellTagChip(
size = 'md'
}: SpellTagChipProps) {
function getContrastColor(hexColor: string | null): string {
if (!hexColor) return '#FFFFFF';
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substring(0, 2), 16);
const g = parseInt(hex.substring(2, 4), 16);
const b = parseInt(hex.substring(4, 6), 16);
const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
return luminance > 0.5 ? '#1F2023' : '#FFFFFF';
}
const chipColor: string = tag.color || 'var(--color-primary)';
const bgClass: string = dynamicBg(chipColor);
const textClass: string = dynamicText(getContrastColor(tag.color));
const sizeClasses = size === 'sm'
? 'px-2 py-0.5 text-xs'
: 'px-3 py-1 text-sm';
const chipClasses = `
inline-flex items-center gap-1.5 rounded-full font-medium
transition-all duration-200
${sizeClasses}
${onClick ? 'cursor-pointer hover:scale-105 hover:shadow-md' : ''}
${onClick ? 'cursor-pointer hover:brightness-110' : ''}
${bgClass} ${textClass}
`;
return (
<span
className={chipClasses}
style={{
backgroundColor: tag.color || '#3B82F6',
color: getContrastColor(tag.color)
}}
onClick={onClick}
>
{tag.name}
@@ -56,9 +57,9 @@ export default function SpellTagChip(
e.stopPropagation();
onRemove();
}}
className="ml-0.5 hover:bg-white/20 rounded-full p-0.5 transition-all duration-200 hover:scale-110"
className="ml-0.5 hover:bg-text-primary/20 rounded-full p-0.5 transition-colors duration-200"
>
<FontAwesomeIcon icon={faX} className={size === 'sm' ? 'w-2 h-2' : 'w-2.5 h-2.5'}/>
<X className={size === 'sm' ? 'w-2 h-2' : 'w-2.5 h-2.5'} strokeWidth={1.75}/>
</button>
)}
</span>

View File

@@ -1,18 +1,21 @@
'use client';
import React, {useContext, useState} from 'react';
import {defaultTagColors, SpellTagProps} from "@/lib/models/Spell";
import {SpellTagProps} from "@/lib/types/spell";
import {defaultTagColors} from "@/lib/constants/spell";
import SpellTagChip from "@/components/book/settings/spells/SpellTagChip";
import InputField from "@/components/form/InputField";
import TextInput from "@/components/form/TextInput";
import Modal from "@/components/Modal";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faArrowLeft, faEdit, faPlus, faTags, faTrash} from "@fortawesome/free-solid-svg-icons";
import {useTranslations} from "next-intl";
import {AlertContext} from "@/context/AlertContext";
import Modal from "@/components/ui/Modal";
import {Pencil, Plus, Tag, Trash2} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import IconContainer from '@/components/ui/IconContainer';
import Button from '@/components/ui/Button';
import IconButton from '@/components/ui/IconButton';
import {dynamicBg} from '@/lib/utils/dynamicStyles';
interface SpellTagManagerProps {
tags: SpellTagProps[];
onBack: () => void;
onCreateTag: (name: string, color: string) => Promise<SpellTagProps | null>;
onUpdateTag: (tagId: string, name: string, color: string) => Promise<boolean>;
onDeleteTag: (tagId: string) => Promise<boolean>;
@@ -21,13 +24,12 @@ interface SpellTagManagerProps {
export default function SpellTagManager(
{
tags,
onBack,
onCreateTag,
onUpdateTag,
onDeleteTag,
}: SpellTagManagerProps) {
const t = useTranslations();
const {successMessage} = useContext(AlertContext);
const {successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
const [newTagName, setNewTagName] = useState<string>('');
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
@@ -87,25 +89,9 @@ export default function SpellTagManager(
}
return (
<div className="space-y-4">
<div
className="flex justify-between items-center p-4 border-b border-secondary/50 bg-tertiary/50 backdrop-blur-sm">
<button
onClick={onBack}
className="flex items-center gap-2 bg-secondary/50 py-2 px-4 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200"
>
<FontAwesomeIcon icon={faArrowLeft} className="text-primary w-4 h-4"/>
<span className="text-text-primary font-medium">{t("spellTagManager.back")}</span>
</button>
<span className="text-text-primary font-semibold text-lg flex items-center gap-2">
<FontAwesomeIcon icon={faTags} className="text-primary w-5 h-5"/>
{t("spellTagManager.title")}
</span>
<div className="w-24"/>
</div>
<div className="px-4 space-y-4">
<div className="bg-secondary/20 rounded-xl p-4 border border-secondary/30">
<div className="space-y-4 p-4">
<div className="space-y-4">
<div className="bg-tertiary rounded-xl p-4">
<h3 className="text-text-primary font-semibold mb-3">{t("spellTagManager.addTag")}</h3>
<div className="space-y-3">
<InputField
@@ -126,29 +112,22 @@ export default function SpellTagManager(
<button
key={color}
onClick={() => setNewTagColor(color)}
className={`w-10 h-10 rounded-full transition-all duration-200 ${newTagColor === color ? 'ring-2 ring-offset-2 ring-primary scale-110' : 'hover:scale-110'}`}
style={{backgroundColor: color}}
className={`w-10 h-10 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>
<button
onClick={handleAddTag}
disabled={!newTagName.trim()}
className="w-full flex items-center justify-center gap-2 py-2.5 bg-primary text-text-primary rounded-xl hover:bg-primary-dark transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed"
>
<FontAwesomeIcon icon={faPlus} className="w-4 h-4"/>
<Button variant="primary" icon={Plus} onClick={handleAddTag} disabled={!newTagName.trim()}
fullWidth>
{t("spellTagManager.addTag")}
</button>
</Button>
</div>
</div>
<div className="overflow-y-auto max-h-[calc(100vh-500px)]">
{tags.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-16 h-16 rounded-full bg-primary/10 flex items-center justify-center mb-4">
<FontAwesomeIcon icon={faTags} className="text-primary w-8 h-8"/>
</div>
<IconContainer icon={Tag} size="lg" shape="circle"/>
<p className="text-muted text-sm">{t("spellTagManager.noTags")}</p>
</div>
) : (
@@ -156,22 +135,14 @@ export default function SpellTagManager(
{tags.map((tag: SpellTagProps) => (
<div
key={tag.id}
className="flex items-center justify-between p-3 bg-secondary/30 rounded-xl border border-secondary/50"
className="flex items-center justify-between p-3 bg-secondary rounded-xl"
>
<SpellTagChip tag={tag}/>
<div className="flex items-center gap-2">
<button
onClick={() => handleEditClick(tag)}
className="p-2 bg-secondary/50 rounded-lg hover:bg-secondary hover:scale-110 transition-all duration-200"
>
<FontAwesomeIcon icon={faEdit} className="text-primary w-4 h-4"/>
</button>
<button
onClick={() => handleDeleteClick(tag)}
className="p-2 bg-error/10 rounded-lg hover:bg-error/20 hover:scale-110 transition-all duration-200"
>
<FontAwesomeIcon icon={faTrash} className="text-error w-4 h-4"/>
</button>
<IconButton icon={Pencil} variant="ghost" size="sm"
onClick={() => handleEditClick(tag)}/>
<IconButton icon={Trash2} variant="danger" size="sm"
onClick={() => handleDeleteClick(tag)}/>
</div>
</div>
))}
@@ -183,7 +154,7 @@ export default function SpellTagManager(
{showEditModal && editingTag && (
<Modal
title={t("spellTagManager.editTag")}
size="small"
size="sm"
onClose={() => setShowEditModal(false)}
onConfirm={handleUpdateTag}
confirmText={t("common.confirm")}
@@ -208,8 +179,7 @@ export default function SpellTagManager(
<button
key={color}
onClick={() => setEditTagColor(color)}
className={`w-10 h-10 rounded-full transition-all duration-200 ${editTagColor === color ? 'ring-2 ring-offset-2 ring-primary scale-110' : 'hover:scale-110'}`}
style={{backgroundColor: color}}
className={`w-10 h-10 rounded-full transition-all duration-200 ${editTagColor === color ? 'ring-2 ring-offset-2 ring-primary' : 'hover:ring-1 hover:ring-primary/50'} ${dynamicBg(color)}`}
/>
))}
</div>
@@ -231,7 +201,7 @@ export default function SpellTagManager(
{showDeleteConfirm && tagToDelete && (
<Modal
title={t("spellTagManager.deleteTagTitle")}
size="small"
size="sm"
onClose={() => setShowDeleteConfirm(false)}
onConfirm={handleDeleteTag}
confirmText={t("spellTagManager.delete")}

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}
/>
);
})
)}

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, SpellTagProps} 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 SpellSettingsList from './SpellSettingsList';
@@ -22,7 +19,7 @@ import SpellSettingsEdit from './SpellSettingsEdit';
interface SpellSettingsProps {
entityType?: 'book' | 'series';
entityId?: string;
showToggle?: boolean;
toolEnabled?: boolean;
}
/**
@@ -31,23 +28,23 @@ interface SpellSettingsProps {
* Inclut: toggle tool, import from series, tag manager
*/
export default function SpellSettings({
entityType = 'book',
entityId,
showToggle = true,
}: SpellSettingsProps): React.JSX.Element {
entityType = 'book',
entityId,
toolEnabled: parentToolEnabled,
}: SpellSettingsProps): React.JSX.Element {
const t = useTranslations();
const {book} = useContext(BookContext);
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
const resolvedEntityId: string = entityId || book?.bookId || '';
const config: UseSpellsConfig = useMemo(function (): UseSpellsConfig {
return {
entityType,
entityId: resolvedEntityId,
};
}, [entityType, resolvedEntityId]);
const {
spells,
seriesSpells,
@@ -79,7 +76,7 @@ export default function SpellSettings({
deleteTag,
handleSyncComplete,
} = useSpells(config);
const availableSeriesSpells = useMemo(function (): SeriesSpellListItem[] {
return seriesSpells.filter(function (ss: SeriesSpellListItem): boolean {
return !spells.some(function (s: SpellListItem): boolean {
@@ -87,19 +84,19 @@ export default function SpellSettings({
});
});
}, [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);
@@ -107,61 +104,50 @@ export default function SpellSettings({
backToList();
}
}
if (isLoading) {
return (
<div className="flex items-center justify-center py-12">
<FontAwesomeIcon icon={faSpinner} className="w-8 h-8 text-primary animate-spin"/>
</div>
);
return <PulseLoader/>;
}
const isNew: boolean = selectedSpell?.id === null;
const canExport: boolean = Boolean(bookSeriesId && selectedSpell?.id && !selectedSpell.seriesSpellId);
return (
<div className="flex flex-col h-full">
{/* Header - uniquement pour detail/edit */}
{/* Header */}
<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)}
/>
{/* Contenu principal */}
<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-5 p-4">
{/* Toggle tool */}
{showToggle && !isSeriesMode && (
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
<InputField
icon={faToggleOn}
fieldName={t('spellComponent.enableTool')}
input={
<ToggleSwitch
checked={toolEnabled}
onChange={toggleTool}
/>
}
/>
<p className="text-muted text-sm mt-2">
{t('spellComponent.enableToolDescription')}
</p>
</div>
)}
{/* Contenu si outil activé */}
{(toolEnabled || isSeriesMode) && (
{((parentToolEnabled !== undefined ? parentToolEnabled : toolEnabled) || isSeriesMode) && (
<>
{/* Import from series */}
{!isSeriesMode && bookSeriesId && availableSeriesSpells.length > 0 && (
@@ -177,21 +163,23 @@ export default function SpellSettings({
label={t('seriesImport.importFromSeries')}
/>
)}
{/* Liste des sorts */}
<SpellSettingsList
spells={spells}
tags={tags}
onSpellClick={enterDetailMode}
onAddSpell={addNewSpell}
onManageTags={function (): void { setShowTagManager(true); }}
onManageTags={function (): void {
setShowTagManager(true);
}}
/>
</>
)}
</div>
)}
{viewMode === 'detail' && selectedSpell && (
{!showTagManager && viewMode === 'detail' && selectedSpell && (
<div className="p-4">
<SpellSettingsDetail
spell={selectedSpell}
@@ -200,8 +188,8 @@ export default function SpellSettings({
/>
</div>
)}
{viewMode === 'edit' && selectedSpell && (
{!showTagManager && viewMode === 'edit' && selectedSpell && (
<div className="p-4">
<SpellSettingsEdit
spell={selectedSpell}
@@ -214,7 +202,7 @@ export default function SpellSettings({
</div>
)}
</div>
{/* Modal de confirmation de suppression */}
{showDeleteConfirm && selectedSpell?.id && (
<AlertBox
@@ -224,20 +212,12 @@ export default function SpellSettings({
confirmText={t('common.delete')}
cancelText={t('common.cancel')}
onConfirm={handleDelete}
onCancel={function (): void { setShowDeleteConfirm(false); }}
/>
)}
{/* Tag Manager Modal */}
{showTagManager && (
<SpellTagManager
tags={tags}
onBack={function (): void { setShowTagManager(false); }}
onCreateTag={createTag}
onUpdateTag={updateTag}
onDeleteTag={deleteTag}
onCancel={function (): void {
setShowDeleteConfirm(false);
}}
/>
)}
</div>
);
}

View File

@@ -1,21 +1,14 @@
'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 {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {
faBolt,
faEye,
faHatWizard,
faPuzzlePiece,
faStickyNote,
faTags,
faTriangleExclamation,
faWandMagicSparkles
} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {AlertTriangle, Eye, Puzzle, StickyNote, Wand2, Zap} from 'lucide-react';
import {useTranslations} from '@/lib/i18n';
import DetailHeroSection from '@/components/ui/DetailHeroSection';
import DetailField from '@/components/ui/DetailField';
interface SpellSettingsDetailProps {
spell: SpellEditState;
@@ -24,18 +17,18 @@ interface SpellSettingsDetailProps {
}
export default function SpellSettingsDetail({
spell,
availableTags,
seriesSpell,
}: SpellSettingsDetailProps): React.JSX.Element {
spell,
availableTags,
seriesSpell,
}: SpellSettingsDetailProps): 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 t('spellPowerLevels.none');
@@ -45,88 +38,57 @@ export default function SpellSettingsDetail({
});
return level ? t(level.label) : spell.powerLevel;
}
function getPowerLevelColor(): string {
switch (spell.powerLevel) {
case 'weak': return 'bg-green-500/20 text-green-400 border-green-500/30';
case 'moderate': return 'bg-blue-500/20 text-blue-400 border-blue-500/30';
case 'strong': return 'bg-orange-500/20 text-orange-400 border-orange-500/30';
case 'legendary': return 'bg-purple-500/20 text-purple-400 border-purple-500/30';
default: return 'bg-secondary/50 text-text-secondary border-secondary/50';
case 'weak':
return 'bg-accent-green/20 text-accent-green border-accent-green/30';
case 'moderate':
return 'bg-accent-blue/20 text-accent-blue border-accent-blue/30';
case 'strong':
return 'bg-accent-orange/20 text-accent-orange border-accent-orange/30';
case 'legendary':
return 'bg-accent-purple/20 text-accent-purple border-accent-purple/30';
default:
return 'bg-secondary text-text-secondary border-secondary';
}
}
const selectedTags: SpellTagProps[] = getSelectedTags();
return (
<div className="space-y-6 px-2 pb-4">
{/* Hero Section */}
<div className="p-6 bg-gradient-to-r from-primary/10 via-secondary/20 to-transparent rounded-2xl border border-secondary/30">
<div className="flex items-start gap-4">
<div className="w-16 h-16 rounded-xl bg-primary/20 flex items-center justify-center shrink-0">
<FontAwesomeIcon icon={faWandMagicSparkles} className="w-8 h-8 text-primary"/>
</div>
<div className="flex-1 min-w-0">
<h2 className="text-2xl font-bold text-text-primary">{spell.name || '—'}</h2>
{/* Power Level Badge */}
<div className="flex items-center gap-3 mt-3">
<span className={`inline-flex items-center gap-2 px-3 py-1 rounded-lg text-sm border ${getPowerLevelColor()}`}>
<FontAwesomeIcon icon={faBolt} className="w-3 h-3"/>
{getLocalizedPowerLevel()}
</span>
</div>
{/* Tags */}
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{selectedTags.map(function (tag: SpellTagProps): React.JSX.Element {
return <SpellTagChip key={tag.id} tag={tag}/>;
})}
</div>
)}
</div>
<DetailHeroSection icon={Wand2} title={spell.name || '—'}>
<div className="flex items-center gap-3 mt-3">
<span
className={`inline-flex items-center gap-2 px-3 py-1 rounded-lg text-sm border ${getPowerLevelColor()}`}>
<Zap className="w-3 h-3" strokeWidth={1.75}/>
{getLocalizedPowerLevel()}
</span>
</div>
</div>
{selectedTags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-4">
{selectedTags.map(function (tag: SpellTagProps): React.JSX.Element {
return <SpellTagChip key={tag.id} tag={tag}/>;
})}
</div>
)}
</DetailHeroSection>
{/* Description & Appearance - Side by side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="p-5 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="flex items-center gap-2 mb-3">
<FontAwesomeIcon icon={faHatWizard} className="w-4 h-4 text-primary"/>
<h3 className="text-text-primary font-semibold">{t('spellDetail.description')}</h3>
</div>
<p className={`whitespace-pre-wrap ${spell.description ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
{spell.description || '—'}
</p>
</div>
<div className="p-5 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="flex items-center gap-2 mb-3">
<FontAwesomeIcon icon={faEye} className="w-4 h-4 text-primary"/>
<h3 className="text-text-primary font-semibold">{t('spellDetail.appearance')}</h3>
</div>
<p className={`whitespace-pre-wrap ${spell.appearance ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
{spell.appearance || '—'}
</p>
</div>
<DetailField icon={Wand2} label={t('spellDetail.description')} value={spell.description}/>
<DetailField icon={Eye} label={t('spellDetail.appearance')} value={spell.appearance}/>
</div>
{/* Components & Limitations - Side by side */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<div className="p-5 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="flex items-center gap-2 mb-3">
<FontAwesomeIcon icon={faPuzzlePiece} className="w-4 h-4 text-primary"/>
<h3 className="text-text-primary font-semibold">{t('spellDetail.components')}</h3>
</div>
<p className={`whitespace-pre-wrap ${spell.components ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
{spell.components || '—'}
</p>
</div>
<DetailField icon={Puzzle} label={t('spellDetail.components')} value={spell.components}/>
<div className="p-5 bg-error/10 rounded-xl border border-error/30">
<div className="flex items-center gap-2 mb-3">
<FontAwesomeIcon icon={faTriangleExclamation} className="w-4 h-4 text-error"/>
<AlertTriangle className="w-4 h-4 text-error" strokeWidth={1.75}/>
<h3 className="text-text-primary font-semibold">{t('spellDetail.limitations')}</h3>
</div>
<p className={`whitespace-pre-wrap ${spell.limitations ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
@@ -134,17 +96,9 @@ export default function SpellSettingsDetail({
</p>
</div>
</div>
{/* Notes - Full width */}
<div className="p-5 bg-secondary/20 rounded-xl border border-secondary/30">
<div className="flex items-center gap-2 mb-3">
<FontAwesomeIcon icon={faStickyNote} className="w-4 h-4 text-primary"/>
<h3 className="text-text-primary font-semibold">{t('spellDetail.notes')}</h3>
</div>
<p className={`whitespace-pre-wrap ${spell.notes ? 'text-text-primary' : 'text-text-secondary/50 italic'}`}>
{spell.notes || '—'}
</p>
</div>
<DetailField icon={StickyNote} label={t('spellDetail.notes')} value={spell.notes}/>
</div>
);
}

View File

@@ -1,28 +1,19 @@
'use client';
import React, {useState} from 'react';
import {defaultTagColors, SpellEditState, spellPowerLevels, SpellTagProps} from '@/lib/models/Spell';
import {SeriesSpellDetailResponse} from '@/lib/models/Series';
import {SelectBoxProps} from '@/shared/interface';
import CollapsableArea from '@/components/CollapsableArea';
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 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 {
faBolt,
faBook,
faEye,
faHatWizard,
faPlus,
faPuzzlePiece,
faStickyNote,
faTags,
faTriangleExclamation
} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
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;
@@ -40,32 +31,32 @@ interface SpellSettingsEditProps {
* PAS de scroll interne (géré par parent)
*/
export default function SpellSettingsEdit({
spell,
availableTags,
onSpellChange,
onCreateTag,
seriesSpell,
onSyncComplete,
}: SpellSettingsEditProps): React.JSX.Element {
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);
@@ -73,13 +64,13 @@ export default function SpellSettingsEdit({
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);
@@ -89,7 +80,7 @@ export default function SpellSettingsEdit({
setNewTagColor(defaultTagColors[0]);
}
}
function getLocalizedPowerLevels(): SelectBoxProps[] {
return spellPowerLevels.map(function (level: SelectBoxProps): SelectBoxProps {
return {
@@ -98,7 +89,7 @@ export default function SpellSettingsEdit({
};
});
}
const filteredTags: SpellTagProps[] = getFilteredAvailableTags();
const selectedTags: SpellTagProps[] = getSelectedTags();
const showCreateOption: boolean = Boolean(
@@ -107,12 +98,11 @@ export default function SpellSettingsEdit({
return tag.name.toLowerCase() === tagSearchQuery.toLowerCase();
})
);
return (
<div className="space-y-4 px-2 pb-4">
{/* Informations de base */}
<CollapsableArea title={t('spellDetail.basicInfo')} icon={faHatWizard}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('spellDetail.basicInfo')} icon={Wand2}>
<InputField
fieldName={t('spellDetail.name')}
input={
@@ -123,7 +113,9 @@ export default function SpellSettingsEdit({
bookElementId={spell.id || ''}
field="name"
elementType="spell"
onDownload={function (): void { onSpellChange('name', seriesSpell?.name || ''); }}
onDownload={function (): void {
onSpellChange('name', seriesSpell?.name || '');
}}
onSyncComplete={onSyncComplete}
>
<TextInput
@@ -136,10 +128,10 @@ export default function SpellSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.description')}
icon={faBook}
icon={Book}
input={
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
@@ -148,10 +140,12 @@ export default function SpellSettingsEdit({
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: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('description', e.target.value);
@@ -161,10 +155,10 @@ export default function SpellSettingsEdit({
</SyncFieldWrapper>
}
/>
<InputField
fieldName={t('spellDetail.appearance')}
icon={faEye}
icon={Eye}
input={
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
@@ -173,10 +167,12 @@ export default function SpellSettingsEdit({
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: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('appearance', e.target.value);
@@ -186,12 +182,10 @@ export default function SpellSettingsEdit({
</SyncFieldWrapper>
}
/>
</div>
</CollapsableArea>
</Collapse>
{/* Tags */}
<CollapsableArea title={t('spellDetail.tags')} icon={faTags}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<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 {
@@ -199,13 +193,15 @@ export default function SpellSettingsEdit({
<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: React.ChangeEvent<HTMLInputElement>): void {
@@ -213,43 +209,42 @@ export default function SpellSettingsEdit({
}}
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-all duration-200 hover:scale-105 hover:shadow-md 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.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)')}`}
>
<FontAwesomeIcon icon={faPlus} className="w-3 h-3"/>
<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/50 transition-colors text-left rounded-xl border border-secondary/50"
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"
>
<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">
{t('spellDetail.createTag', {name: tagSearchQuery})}
</span>
</button>
)}
{isCreatingTag && (
<div className="p-4 bg-tertiary rounded-xl border border-secondary/50">
<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>
@@ -258,35 +253,30 @@ export default function SpellSettingsEdit({
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 scale-110' : 'hover:scale-110'}`}
style={{backgroundColor: 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
onClick={function (): void { setIsCreatingTag(false); }}
className="flex-1 py-2 px-3 bg-secondary/50 text-text-primary rounded-lg hover:bg-secondary transition-colors"
>
<Button variant="secondary" size="sm" onClick={function (): void {
setIsCreatingTag(false);
}}>
{t('common.cancel')}
</button>
<button
onClick={handleCreateTag}
className="flex-1 py-2 px-3 bg-primary text-text-primary rounded-lg hover:bg-primary-dark transition-colors"
>
</Button>
<Button variant="primary" size="sm" onClick={handleCreateTag}>
{t('common.confirm')}
</button>
</Button>
</div>
</div>
)}
</div>
</CollapsableArea>
</Collapse>
{/* Niveau de puissance */}
<CollapsableArea title={t('spellDetail.powerLevel')} icon={faBolt}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('spellDetail.powerLevel')} icon={Zap}>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.powerLevel || 'none'}
@@ -294,7 +284,9 @@ export default function SpellSettingsEdit({
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
@@ -305,12 +297,10 @@ export default function SpellSettingsEdit({
data={getLocalizedPowerLevels()}
/>
</SyncFieldWrapper>
</div>
</CollapsableArea>
</Collapse>
{/* Composants */}
<CollapsableArea title={t('spellDetail.components')} icon={faPuzzlePiece}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('spellDetail.components')} icon={Puzzle}>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.components || ''}
@@ -318,10 +308,12 @@ export default function SpellSettingsEdit({
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: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('components', e.target.value || null);
@@ -329,12 +321,10 @@ export default function SpellSettingsEdit({
placeholder={t('spellDetail.componentsPlaceholder')}
/>
</SyncFieldWrapper>
</div>
</CollapsableArea>
</Collapse>
{/* Limitations */}
<CollapsableArea title={t('spellDetail.limitations')} icon={faTriangleExclamation}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('spellDetail.limitations')} icon={AlertTriangle}>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.limitations || ''}
@@ -342,10 +332,12 @@ export default function SpellSettingsEdit({
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: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('limitations', e.target.value || null);
@@ -353,12 +345,10 @@ export default function SpellSettingsEdit({
placeholder={t('spellDetail.limitationsPlaceholder')}
/>
</SyncFieldWrapper>
</div>
</CollapsableArea>
</Collapse>
{/* Notes */}
<CollapsableArea title={t('spellDetail.notes')} icon={faStickyNote}>
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
<Collapse variant="card" title={t('spellDetail.notes')} icon={StickyNote}>
<SyncFieldWrapper
seriesElementId={spell.seriesSpellId}
seriesValue={seriesSpell?.notes || ''}
@@ -366,10 +356,12 @@ export default function SpellSettingsEdit({
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: React.ChangeEvent<HTMLTextAreaElement>): void {
onSpellChange('notes', e.target.value || null);
@@ -377,8 +369,7 @@ export default function SpellSettingsEdit({
placeholder={t('spellDetail.notesPlaceholder')}
/>
</SyncFieldWrapper>
</div>
</CollapsableArea>
</Collapse>
</div>
);
}

View File

@@ -1,12 +1,16 @@
'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, faCog, faHatWizard, faPlus} from '@fortawesome/free-solid-svg-icons';
import {useTranslations} from 'next-intl';
import {Plus, Settings, Wand2} from 'lucide-react';
import EntityListItem from '@/components/ui/EntityListItem';
import AvatarIcon from '@/components/ui/AvatarIcon';
import {useTranslations} from '@/lib/i18n';
import EmptyState from '@/components/ui/EmptyState';
import Button from '@/components/ui/Button';
import Badge from '@/components/ui/Badge';
interface SpellSettingsListProps {
spells: SpellListItem[];
@@ -22,16 +26,16 @@ interface SpellSettingsListProps {
* PAS de scroll interne (géré par parent)
*/
export default function SpellSettingsList({
spells,
tags,
onSpellClick,
onAddSpell,
onManageTags,
}: SpellSettingsListProps): React.JSX.Element {
spells,
tags,
onSpellClick,
onAddSpell,
onManageTags,
}: SpellSettingsListProps): React.JSX.Element {
const t = useTranslations();
const [searchQuery, setSearchQuery] = useState<string>('');
const [filterTag, setFilterTag] = useState<string>('all');
function getFilteredSpells(): SpellListItem[] {
return spells.filter(function (spell: SpellListItem): boolean {
const matchesSearch: boolean = spell.name.toLowerCase().includes(searchQuery.toLowerCase());
@@ -41,9 +45,9 @@ export default function SpellSettingsList({
return matchesSearch && matchesTag;
});
}
const filteredSpells: SpellListItem[] = getFilteredSpells();
return (
<div className="space-y-4">
<div className="px-4 space-y-3">
@@ -57,13 +61,13 @@ export default function SpellSettingsList({
placeholder={t('spellList.search')}
/>
}
actionIcon={faPlus}
actionIcon={Plus}
actionLabel={t('spellList.add')}
addButtonCallBack={async function (): Promise<void> {
onAddSpell();
}}
/>
<div className="flex flex-wrap gap-3 items-center">
<div className="flex-1 min-w-[150px]">
<select
@@ -71,7 +75,7 @@ export default function SpellSettingsList({
onChange={function (e: React.ChangeEvent<HTMLSelectElement>): void {
setFilterTag(e.target.value);
}}
className="w-full text-text-primary bg-secondary/50 hover:bg-secondary px-4 py-2.5 rounded-xl border border-secondary/50 focus:border-primary focus:ring-4 focus:ring-primary/20 focus:bg-secondary hover:border-secondary outline-none transition-all duration-200 cursor-pointer"
className="input-base cursor-pointer"
>
<option value="all" className="bg-tertiary text-text-primary">
{t('spellList.allTags')}
@@ -85,70 +89,43 @@ export default function SpellSettingsList({
})}
</select>
</div>
<button
onClick={onManageTags}
className="flex items-center gap-2 px-4 py-2.5 bg-secondary/50 rounded-xl border border-secondary/50 hover:bg-secondary hover:border-secondary hover:shadow-md hover:scale-105 transition-all duration-200"
>
<FontAwesomeIcon icon={faCog} className="text-primary w-4 h-4"/>
<span className="text-text-primary text-sm font-medium">{t('spellList.manageTags')}</span>
</button>
<Button variant="secondary" size="sm" icon={Settings} onClick={onManageTags}>
{t('spellList.manageTags')}
</Button>
</div>
</div>
<div className="px-2">
{filteredSpells.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center">
<div className="w-20 h-20 rounded-full bg-primary/10 flex items-center justify-center mb-4">
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-10 h-10"/>
</div>
<h3 className="text-text-primary font-semibold text-lg mb-2">
{t('spellList.noSpells')}
</h3>
<p className="text-muted text-sm max-w-xs">
{t('spellList.noSpellsDescription')}
</p>
</div>
<EmptyState icon={Wand2} title={t('spellList.noSpells')}
description={t('spellList.noSpellsDescription')}/>
) : (
<div className="space-y-2 p-2">
{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-4 bg-secondary/30 rounded-xl border-l-4 border-primary border border-secondary/50 cursor-pointer hover:bg-secondary hover:shadow-md hover:scale-102 transition-all duration-200 hover:border-primary/50"
>
<div className="w-12 h-12 rounded-full border-2 border-primary overflow-hidden bg-secondary shadow-md group-hover:scale-110 transition-transform flex items-center justify-center">
<FontAwesomeIcon icon={faHatWizard} className="text-primary w-6 h-6"/>
</div>
<div className="ml-4 flex-1 min-w-0">
<div className="text-text-primary font-bold text-base group-hover:text-primary transition-colors">
{spell.name}
</div>
<div className="text-text-secondary text-sm mt-0.5 truncate">
{spell.description}
</div>
{spell.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5 mt-2">
onClick={function (): void {
onSpellClick(spell);
}}
avatar={<AvatarIcon icon={Wand2}/>}
title={spell.name}
subtitle={spell.description}
extra={
spell.tags.length > 0 ? (
<div className="flex flex-wrap gap-1.5">
{spell.tags.slice(0, 3).map(function (tag: SpellTagProps): React.JSX.Element {
return <SpellTagChip key={tag.id} tag={tag} size="sm"/>;
})}
{spell.tags.length > 3 && (
<span className="text-muted text-xs px-2 py-0.5 bg-secondary/50 rounded-full">
<Badge variant="muted" size="sm">
+{spell.tags.length - 3}
</span>
</Badge>
)}
</div>
)}
</div>
<div className="w-8 flex justify-center">
<FontAwesomeIcon
icon={faChevronRight}
className="text-muted group-hover:text-primary group-hover:translate-x-1 transition-all w-4 h-4"
/>
</div>
</div>
) : undefined
}
/>
);
})}
</div>