Files
ERitors-Scribe-Desktop/components/spells/SpellComponent.tsx
natreex fd09a5531c Add comprehensive spell management functionality
- Introduced spell management with creation, editing, deletion, and tagging capabilities.
- Added `Spell`, `SpellList`, `SpellTagManager` models with corresponding IPC handlers for data operations.
- Implemented `SpellList` and `SpellTagChip` components for UI interactions with spells and tags.
- Localized spell-related strings for English (e.g., error messages, tooltips, and prompts).
- Enhanced database models and repositories with encryption and decryption workflows for secure data handling.
- Updated API to include filtering, searching, and tag-based spell management options.
2026-01-19 21:38:38 -05:00

564 lines
22 KiB
TypeScript

'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);