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:
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
})
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user