Add spell management to book settings
- Moved spell-related components to the `book/settings/spells` directory for better organization. - Added "Spells" as a new tool in book settings and composer sidebar with localization support. - Integrated spell-related UI elements (`SpellComponent`, `SpellList`, `SpellTagManager`) into settings and sidebars. - Updated logic to handle enabling/disabling of the spells tool per book.
This commit is contained in:
563
components/book/settings/spells/SpellComponent.tsx
Normal file
563
components/book/settings/spells/SpellComponent.tsx
Normal file
@@ -0,0 +1,563 @@
|
||||
'use client';
|
||||
import React, {forwardRef, useContext, useEffect, useImperativeHandle, useState} from 'react';
|
||||
import {
|
||||
initialSpellState,
|
||||
SpellEditState,
|
||||
SpellListItem,
|
||||
SpellListResponse,
|
||||
SpellProps,
|
||||
SpellPropsPost,
|
||||
SpellTagProps
|
||||
} from "@/lib/models/Spell";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import System from '@/lib/models/System';
|
||||
import {useTranslations} from "next-intl";
|
||||
import ToggleSwitch from "@/components/form/ToggleSwitch";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import {faToggleOn} from "@fortawesome/free-solid-svg-icons";
|
||||
import SpellList from "@/components/book/settings/spells/SpellList";
|
||||
import SpellDetail from "@/components/book/settings/spells/SpellDetail";
|
||||
import SpellTagManager from "@/components/book/settings/spells/SpellTagManager";
|
||||
|
||||
interface SpellComponentProps {
|
||||
showToggle?: boolean;
|
||||
}
|
||||
|
||||
export function SpellComponent(props: SpellComponentProps, ref: React.Ref<{ handleSave: () => Promise<void> }>) {
|
||||
const {showToggle = true} = props;
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext);
|
||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {book, setBook} = useContext(BookContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
|
||||
const bookId: string | undefined = book?.bookId;
|
||||
const token: string = session.accessToken;
|
||||
|
||||
const [spells, setSpells] = useState<SpellListItem[]>([]);
|
||||
const [tags, setTags] = useState<SpellTagProps[]>([]);
|
||||
const [selectedSpell, setSelectedSpell] = useState<SpellEditState | null>(null);
|
||||
const [toolEnabled, setToolEnabled] = useState<boolean>(book?.tools?.spells ?? false);
|
||||
const [showTagManager, setShowTagManager] = useState<boolean>(false);
|
||||
|
||||
useImperativeHandle(ref, function () {
|
||||
return {
|
||||
handleSave: handleSaveSpell,
|
||||
};
|
||||
});
|
||||
|
||||
useEffect((): void => {
|
||||
getSpells().then();
|
||||
}, []);
|
||||
|
||||
async function handleToggleTool(enabled: boolean): Promise<void> {
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:book:tool:update', {
|
||||
bookId: bookId,
|
||||
toolName: 'spells',
|
||||
enabled: enabled
|
||||
});
|
||||
} else {
|
||||
response = await System.authPatchToServer<boolean>('book/tool-setting', {
|
||||
bookId: bookId,
|
||||
toolName: 'spells',
|
||||
enabled: enabled
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:book:tool:update', {
|
||||
bookId: bookId,
|
||||
toolName: 'spells',
|
||||
enabled: enabled
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response && setBook && book) {
|
||||
setToolEnabled(enabled);
|
||||
setBook({
|
||||
...book, tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: book.tools?.locations ?? false,
|
||||
spells: enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getSpells(): Promise<void> {
|
||||
try {
|
||||
let response: SpellListResponse;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: bookId});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellListResponse>('db:spell:list', {bookid: bookId});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellListResponse>('spell/list', token, lang, {
|
||||
bookid: bookId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setSpells(response.spells);
|
||||
setTags(response.tags);
|
||||
setToolEnabled(response.enabled);
|
||||
if (setBook && book) {
|
||||
setBook({
|
||||
...book, tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
locations: book.tools?.locations ?? false,
|
||||
spells: response.enabled
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSpellClick(spell: SpellListItem): Promise<void> {
|
||||
// Convertir les tags de SpellTagProps[] vers string[] (IDs)
|
||||
const tagIds: string[] = spell.tags.map((tag: SpellTagProps): string => tag.id);
|
||||
|
||||
// D'abord afficher avec les données de la liste
|
||||
setSelectedSpell({
|
||||
id: spell.id,
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: '',
|
||||
tags: tagIds,
|
||||
powerLevel: null,
|
||||
components: null,
|
||||
limitations: null,
|
||||
notes: null,
|
||||
});
|
||||
try {
|
||||
let response: SpellProps;
|
||||
if (isCurrentlyOffline()) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
} else {
|
||||
if (book?.localBook) {
|
||||
response = await window.electron.invoke<SpellProps>('db:spell:detail', {spellid: spell.id});
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<SpellProps>('spell/detail', token, lang, {
|
||||
spellid: spell.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setSelectedSpell((prev: SpellEditState | null): SpellEditState | null => {
|
||||
if (!prev) return null;
|
||||
return {
|
||||
...prev,
|
||||
appearance: response.appearance,
|
||||
powerLevel: response.powerLevel,
|
||||
components: response.components,
|
||||
limitations: response.limitations,
|
||||
notes: response.notes,
|
||||
// Garder les tags de la liste, pas ceux de l'API
|
||||
};
|
||||
});
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleAddSpell(): void {
|
||||
setSelectedSpell({...initialSpellState});
|
||||
}
|
||||
|
||||
function handleSpellChange(key: keyof SpellEditState, value: string | string[] | null): void {
|
||||
if (selectedSpell) {
|
||||
setSelectedSpell({...selectedSpell, [key]: value});
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSaveSpell(): Promise<void> {
|
||||
if (selectedSpell) {
|
||||
if (selectedSpell.id === null) {
|
||||
await addNewSpell(selectedSpell);
|
||||
} else {
|
||||
await updateSpell(selectedSpell);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function addNewSpell(spell: SpellEditState): Promise<void> {
|
||||
if (!spell.name) {
|
||||
errorMessage(t("spellComponent.errorNameRequired"));
|
||||
return;
|
||||
}
|
||||
if (!spell.description) {
|
||||
errorMessage(t("spellComponent.errorDescriptionRequired"));
|
||||
return;
|
||||
}
|
||||
if (!spell.appearance) {
|
||||
errorMessage(t("spellComponent.errorAppearanceRequired"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const spellPost: SpellPropsPost = {
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: spell.appearance,
|
||||
tags: spell.tags,
|
||||
powerLevel: spell.powerLevel,
|
||||
components: spell.components,
|
||||
limitations: spell.limitations,
|
||||
notes: spell.notes,
|
||||
};
|
||||
let spellId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
spellId = await window.electron.invoke<string>('db:spell:create', {
|
||||
bookId: bookId,
|
||||
spell: spellPost,
|
||||
});
|
||||
} else {
|
||||
const createdSpell: SpellProps = await System.authPostToServer<SpellProps>('spell/add', {
|
||||
bookId: bookId,
|
||||
spell: spellPost,
|
||||
}, token, lang);
|
||||
if (!createdSpell || !createdSpell.id) {
|
||||
errorMessage(t("spellComponent.errorAddSpell"));
|
||||
return;
|
||||
}
|
||||
spellId = createdSpell.id;
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:create', {
|
||||
bookId: bookId,
|
||||
spell: {...spellPost, id: spellId},
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!spellId) {
|
||||
errorMessage(t("spellComponent.errorAddSpell"));
|
||||
return;
|
||||
}
|
||||
// Ajouter à la liste avec les tags résolus
|
||||
const resolvedTags: SpellTagProps[] = tags.filter((tag: SpellTagProps) => spell.tags.includes(tag.id));
|
||||
const newSpellListItem: SpellListItem = {
|
||||
id: spellId,
|
||||
name: spell.name,
|
||||
description: spell.description.length > 150
|
||||
? spell.description.substring(0, 150) + '...'
|
||||
: spell.description,
|
||||
tags: resolvedTags,
|
||||
};
|
||||
setSpells([...spells, newSpellListItem]);
|
||||
setSelectedSpell(null);
|
||||
successMessage(t("spellComponent.successAdd"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSpell(spell: SpellEditState): Promise<void> {
|
||||
if (!spell.id) return;
|
||||
if (!spell.name) {
|
||||
errorMessage(t("spellComponent.errorNameRequired"));
|
||||
return;
|
||||
}
|
||||
if (!spell.description) {
|
||||
errorMessage(t("spellComponent.errorDescriptionRequired"));
|
||||
return;
|
||||
}
|
||||
if (!spell.appearance) {
|
||||
errorMessage(t("spellComponent.errorAppearanceRequired"));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const spellPost: SpellPropsPost = {
|
||||
name: spell.name,
|
||||
description: spell.description,
|
||||
appearance: spell.appearance,
|
||||
tags: spell.tags,
|
||||
powerLevel: spell.powerLevel,
|
||||
components: spell.components,
|
||||
limitations: spell.limitations,
|
||||
notes: spell.notes,
|
||||
};
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:spell:update', {
|
||||
spellId: spell.id,
|
||||
spell: spellPost,
|
||||
});
|
||||
} else {
|
||||
response = await System.authPutToServer<boolean>('spell/update', {
|
||||
spellId: spell.id,
|
||||
spell: spellPost,
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:update', {
|
||||
spellId: spell.id,
|
||||
spell: spellPost,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("spellComponent.errorUpdateSpell"));
|
||||
return;
|
||||
}
|
||||
// Mettre à jour la liste avec les tags résolus
|
||||
const resolvedTags: SpellTagProps[] = tags.filter((tag: SpellTagProps) => spell.tags.includes(tag.id));
|
||||
setSpells(spells.map((s: SpellListItem): SpellListItem =>
|
||||
s.id === spell.id ? {
|
||||
id: spell.id,
|
||||
name: spell.name,
|
||||
description: spell.description.length > 150
|
||||
? spell.description.substring(0, 150) + '...'
|
||||
: spell.description,
|
||||
tags: resolvedTags,
|
||||
} : s
|
||||
));
|
||||
setSelectedSpell(null);
|
||||
successMessage(t("spellComponent.successUpdate"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteSpell(spellId: string): Promise<void> {
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:spell:delete', {
|
||||
spellId: spellId,
|
||||
});
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('spell/delete', {
|
||||
spellId: spellId,
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:delete', {
|
||||
spellId: spellId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t("spellComponent.errorDeleteSpell"));
|
||||
return;
|
||||
}
|
||||
setSpells(spells.filter((s: SpellListItem) => s.id !== spellId));
|
||||
setSelectedSpell(null);
|
||||
successMessage(t("spellComponent.successDelete"));
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
} else {
|
||||
errorMessage(t("common.unknownError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleManageTags(): void {
|
||||
setShowTagManager(true);
|
||||
}
|
||||
|
||||
function handleBackFromTagManager(): void {
|
||||
setShowTagManager(false);
|
||||
}
|
||||
|
||||
async function handleCreateTag(name: string, color: string): Promise<SpellTagProps | null> {
|
||||
try {
|
||||
let tagId: string;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
tagId = await window.electron.invoke<string>('db:spell:tag:create', {
|
||||
bookId: bookId,
|
||||
name: name,
|
||||
color: color,
|
||||
});
|
||||
} else {
|
||||
const newTag: SpellTagProps = await System.authPostToServer<SpellTagProps>('spell/tag/add', {
|
||||
bookId: bookId,
|
||||
name: name,
|
||||
color: color,
|
||||
}, token, lang);
|
||||
if (!newTag || !newTag.id) {
|
||||
return null;
|
||||
}
|
||||
tagId = newTag.id;
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:tag:create', {
|
||||
bookId: bookId,
|
||||
name: name,
|
||||
color: color,
|
||||
tagId: tagId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (tagId) {
|
||||
const createdTag: SpellTagProps = {id: tagId, name: name, color: color};
|
||||
setTags([...tags, createdTag]);
|
||||
return createdTag;
|
||||
}
|
||||
return null;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateTag(tagId: string, name: string, color: string): Promise<boolean> {
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:spell:tag:update', {
|
||||
tagId: tagId,
|
||||
name: name,
|
||||
color: color,
|
||||
});
|
||||
} else {
|
||||
response = await System.authPutToServer<boolean>('spell/tag/update', {
|
||||
tagId: tagId,
|
||||
name: name,
|
||||
color: color,
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:tag:update', {
|
||||
tagId: tagId,
|
||||
name: name,
|
||||
color: color,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setTags(tags.map((tag: SpellTagProps): SpellTagProps =>
|
||||
tag.id === tagId ? {id: tagId, name: name, color: color} : tag
|
||||
));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteTag(tagId: string): Promise<boolean> {
|
||||
try {
|
||||
let response: boolean;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await window.electron.invoke<boolean>('db:spell:tag:delete', {
|
||||
tagId: tagId,
|
||||
bookId: bookId,
|
||||
});
|
||||
} else {
|
||||
response = await System.authDeleteToServer<boolean>('spell/tag/delete', {
|
||||
tagId: tagId,
|
||||
bookId: bookId,
|
||||
}, token, lang);
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === bookId)) {
|
||||
addToQueue('db:spell:tag:delete', {
|
||||
tagId: tagId,
|
||||
bookId: bookId,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (response) {
|
||||
setTags(tags.filter((tag: SpellTagProps): boolean => tag.id !== tagId));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(e.message);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{showToggle && (
|
||||
<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={async (checked: boolean): Promise<void> => handleToggleTool(checked)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<p className="text-muted text-sm mt-2">
|
||||
{t('spellComponent.enableToolDescription')}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{toolEnabled && (
|
||||
<>
|
||||
{showTagManager ? (
|
||||
<SpellTagManager
|
||||
tags={tags}
|
||||
onBack={handleBackFromTagManager}
|
||||
onCreateTag={handleCreateTag}
|
||||
onUpdateTag={handleUpdateTag}
|
||||
onDeleteTag={handleDeleteTag}
|
||||
/>
|
||||
) : selectedSpell ? (
|
||||
<SpellDetail
|
||||
selectedSpell={selectedSpell}
|
||||
setSelectedSpell={setSelectedSpell}
|
||||
availableTags={tags}
|
||||
handleSpellChange={handleSpellChange}
|
||||
handleSaveSpell={handleSaveSpell}
|
||||
handleDeleteSpell={handleDeleteSpell}
|
||||
handleCreateTagInline={handleCreateTag}
|
||||
/>
|
||||
) : (
|
||||
<SpellList
|
||||
spells={spells}
|
||||
tags={tags}
|
||||
handleSpellClick={handleSpellClick}
|
||||
handleAddSpell={handleAddSpell}
|
||||
handleManageTags={handleManageTags}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default forwardRef(SpellComponent);
|
||||
Reference in New Issue
Block a user