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.
This commit is contained in:
@@ -670,7 +670,7 @@ export default function Scribe() {
|
||||
|
||||
return (
|
||||
<LangContext.Provider value={{lang: locale, setLang: setLocale}}>
|
||||
<NextIntlClientProvider locale={locale} messages={messages}>
|
||||
<NextIntlClientProvider locale={locale} messages={messages} timeZone="America/Montreal">
|
||||
<OfflineProvider>
|
||||
<AlertProvider>
|
||||
<ScribeContent/>
|
||||
|
||||
@@ -98,6 +98,7 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
||||
setBook({...book, tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
spells: book.tools?.spells ?? false,
|
||||
locations: enabled
|
||||
}});
|
||||
}
|
||||
@@ -129,6 +130,7 @@ export function LocationComponent({showToggle = true}: {showToggle?: boolean}, r
|
||||
setBook({...book, tools: {
|
||||
characters: book.tools?.characters ?? false,
|
||||
worlds: book.tools?.worlds ?? false,
|
||||
spells: book.tools?.spells ?? false,
|
||||
locations: response.enabled
|
||||
}});
|
||||
}
|
||||
|
||||
563
components/spells/SpellComponent.tsx
Normal file
563
components/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);
|
||||
325
components/spells/SpellDetail.tsx
Normal file
325
components/spells/SpellDetail.tsx
Normal file
@@ -0,0 +1,325 @@
|
||||
'use client';
|
||||
import React, {Dispatch, SetStateAction, useState} from 'react';
|
||||
import {defaultTagColors, SpellEditState, spellPowerLevels, SpellTagProps} from "@/lib/models/Spell";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import CollapsableArea from "@/components/CollapsableArea";
|
||||
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 SpellTagChip from "@/components/book/settings/spells/SpellTagChip";
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
faArrowLeft,
|
||||
faBolt,
|
||||
faBook,
|
||||
faEye,
|
||||
faHatWizard,
|
||||
faPlus,
|
||||
faPuzzlePiece,
|
||||
faSave,
|
||||
faStickyNote,
|
||||
faTags,
|
||||
faTrash,
|
||||
faTriangleExclamation
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslations} from "next-intl";
|
||||
import AlertBox from "@/components/AlertBox";
|
||||
|
||||
interface SpellDetailProps {
|
||||
selectedSpell: SpellEditState;
|
||||
setSelectedSpell: Dispatch<SetStateAction<SpellEditState | null>>;
|
||||
availableTags: SpellTagProps[];
|
||||
handleSpellChange: (key: keyof SpellEditState, value: string | string[] | null) => void;
|
||||
handleSaveSpell: () => Promise<void>;
|
||||
handleDeleteSpell: (spellId: string) => Promise<void>;
|
||||
handleCreateTagInline: (name: string, color: string) => Promise<SpellTagProps | null>;
|
||||
}
|
||||
|
||||
export default function SpellDetail(
|
||||
{
|
||||
selectedSpell,
|
||||
setSelectedSpell,
|
||||
availableTags,
|
||||
handleSpellChange,
|
||||
handleSaveSpell,
|
||||
handleDeleteSpell,
|
||||
handleCreateTagInline,
|
||||
}: SpellDetailProps) {
|
||||
const t = useTranslations();
|
||||
|
||||
const [tagSearchQuery, setTagSearchQuery] = useState<string>('');
|
||||
const [showTagDropdown, setShowTagDropdown] = useState<boolean>(false);
|
||||
const [isCreatingTag, setIsCreatingTag] = useState<boolean>(false);
|
||||
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||
|
||||
function handleAddTag(tagId: string): void {
|
||||
if (!selectedSpell.tags.includes(tagId)) {
|
||||
handleSpellChange('tags', [...selectedSpell.tags, tagId]);
|
||||
}
|
||||
setTagSearchQuery('');
|
||||
setShowTagDropdown(false);
|
||||
}
|
||||
|
||||
function handleRemoveTag(tagId: string): void {
|
||||
handleSpellChange('tags', selectedSpell.tags.filter((id: string) => id !== tagId));
|
||||
}
|
||||
|
||||
function getFilteredAvailableTags(): SpellTagProps[] {
|
||||
return availableTags.filter((tag: SpellTagProps) => {
|
||||
const notAlreadyAdded = !selectedSpell.tags.includes(tag.id);
|
||||
const matchesSearch = tag.name.toLowerCase().includes(tagSearchQuery.toLowerCase());
|
||||
return notAlreadyAdded && matchesSearch;
|
||||
});
|
||||
}
|
||||
|
||||
function getSelectedTags(): SpellTagProps[] {
|
||||
return availableTags.filter((tag: SpellTagProps) => selectedSpell.tags.includes(tag.id));
|
||||
}
|
||||
|
||||
async function handleCreateTag(): Promise<void> {
|
||||
if (!tagSearchQuery.trim()) {
|
||||
return;
|
||||
}
|
||||
const newTag: SpellTagProps | null = await handleCreateTagInline(tagSearchQuery.trim(), newTagColor);
|
||||
if (newTag) {
|
||||
handleAddTag(newTag.id);
|
||||
setIsCreatingTag(false);
|
||||
setNewTagColor(defaultTagColors[0]);
|
||||
}
|
||||
}
|
||||
|
||||
function getLocalizedPowerLevels(): SelectBoxProps[] {
|
||||
return spellPowerLevels.map((level: SelectBoxProps): SelectBoxProps => ({
|
||||
value: level.value,
|
||||
label: t(level.label),
|
||||
}));
|
||||
}
|
||||
|
||||
const filteredTags = getFilteredAvailableTags();
|
||||
const selectedTags = getSelectedTags();
|
||||
const showCreateOption = tagSearchQuery.trim() && !availableTags.some((tag: SpellTagProps) => tag.name.toLowerCase() === tagSearchQuery.toLowerCase());
|
||||
|
||||
|
||||
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={() => setSelectedSpell(null)}
|
||||
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("spellDetail.back")}</span>
|
||||
</button>
|
||||
<span className="text-text-primary font-semibold text-lg">
|
||||
{selectedSpell.name || t("spellDetail.newSpell")}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{selectedSpell.id && (
|
||||
<button
|
||||
onClick={() => setShowDeleteConfirm(true)}
|
||||
className="flex items-center justify-center bg-error/90 hover:bg-error w-10 h-10 rounded-xl border border-error shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrash} className="text-text-primary w-5 h-5"/>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={handleSaveSpell}
|
||||
className="flex items-center justify-center bg-primary w-10 h-10 rounded-xl border border-primary-dark shadow-md hover:shadow-lg hover:scale-110 transition-all duration-200"
|
||||
>
|
||||
<FontAwesomeIcon icon={selectedSpell.id ? faSave : faPlus}
|
||||
className="text-text-primary w-5 h-5"/>
|
||||
</button>
|
||||
</div>
|
||||
{showDeleteConfirm && selectedSpell.id && (
|
||||
<AlertBox
|
||||
title={t("spellDetail.deleteTitle")}
|
||||
message={t("spellDetail.deleteMessage", {name: selectedSpell.name})}
|
||||
type="danger"
|
||||
confirmText={t("common.delete")}
|
||||
cancelText={t("common.cancel")}
|
||||
onConfirm={async (): Promise<void> => {
|
||||
await handleDeleteSpell(selectedSpell.id as string);
|
||||
setShowDeleteConfirm(false);
|
||||
}}
|
||||
onCancel={(): void => setShowDeleteConfirm(false)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-350px)] space-y-4 px-2 pb-4">
|
||||
<CollapsableArea title={t("spellDetail.basicInfo")} icon={faHatWizard}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<InputField
|
||||
fieldName={t("spellDetail.name")}
|
||||
input={
|
||||
<TextInput
|
||||
value={selectedSpell.name}
|
||||
setValue={(e) => handleSpellChange('name', e.target.value)}
|
||||
placeholder={t("spellDetail.namePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("spellDetail.description")}
|
||||
icon={faBook}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={selectedSpell.description}
|
||||
setValue={(e) => handleSpellChange('description', e.target.value)}
|
||||
placeholder={t("spellDetail.descriptionPlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<InputField
|
||||
fieldName={t("spellDetail.appearance")}
|
||||
icon={faEye}
|
||||
input={
|
||||
<TexteAreaInput
|
||||
value={selectedSpell.appearance}
|
||||
setValue={(e) => handleSpellChange('appearance', e.target.value)}
|
||||
placeholder={t("spellDetail.appearancePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("spellDetail.tags")} icon={faTags}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
{selectedTags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{selectedTags.map((tag: SpellTagProps) => (
|
||||
<SpellTagChip
|
||||
key={tag.id}
|
||||
tag={tag}
|
||||
onRemove={() => handleRemoveTag(tag.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="relative">
|
||||
<TextInput
|
||||
value={tagSearchQuery}
|
||||
setValue={(e) => {
|
||||
setTagSearchQuery(e.target.value);
|
||||
setShowTagDropdown(true);
|
||||
}}
|
||||
placeholder={t("spellDetail.addTag")}
|
||||
onFocus={() => setShowTagDropdown(true)}
|
||||
/>
|
||||
|
||||
{showTagDropdown && (tagSearchQuery || filteredTags.length > 0) && (
|
||||
<div
|
||||
className="absolute z-10 w-full mt-1 bg-tertiary border border-secondary/50 rounded-xl shadow-lg max-h-48 overflow-y-auto">
|
||||
{filteredTags.map((tag: SpellTagProps) => (
|
||||
<button
|
||||
key={tag.id}
|
||||
onClick={() => handleAddTag(tag.id)}
|
||||
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-secondary/50 transition-colors text-left"
|
||||
>
|
||||
<span
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{backgroundColor: tag.color || '#3B82F6'}}
|
||||
/>
|
||||
<span className="text-text-primary">{tag.name}</span>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{showCreateOption && !isCreatingTag && (
|
||||
<button
|
||||
onClick={() => setIsCreatingTag(true)}
|
||||
className="w-full flex items-center gap-2 px-4 py-2 hover:bg-secondary/50 transition-colors text-left border-t border-secondary/30"
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlus} className="text-primary w-3 h-3"/>
|
||||
<span className="text-primary font-medium">
|
||||
{t("spellDetail.createTag", {name: tagSearchQuery})}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
{isCreatingTag && (
|
||||
<div className="p-4 border-t border-secondary/30">
|
||||
<p className="text-text-secondary text-sm mb-3">
|
||||
{t("spellDetail.createTag", {name: tagSearchQuery})}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mb-3">
|
||||
{defaultTagColors.map((color: string) => (
|
||||
<button
|
||||
key={color}
|
||||
onClick={() => 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}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={() => setIsCreatingTag(false)}
|
||||
className="flex-1 py-2 px-3 bg-secondary/50 text-text-primary rounded-lg hover:bg-secondary transition-colors"
|
||||
>
|
||||
{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"
|
||||
>
|
||||
{t("common.confirm")}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("spellDetail.powerLevel")} icon={faBolt}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<SelectBox
|
||||
defaultValue={selectedSpell.powerLevel || 'none'}
|
||||
onChangeCallBack={(e) => handleSpellChange('powerLevel', e.target.value === 'none' ? null : e.target.value)}
|
||||
data={getLocalizedPowerLevels()}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("spellDetail.components")} icon={faPuzzlePiece}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<TexteAreaInput
|
||||
value={selectedSpell.components || ''}
|
||||
setValue={(e) => handleSpellChange('components', e.target.value || null)}
|
||||
placeholder={t("spellDetail.componentsPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("spellDetail.limitations")} icon={faTriangleExclamation}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<TexteAreaInput
|
||||
value={selectedSpell.limitations || ''}
|
||||
setValue={(e) => handleSpellChange('limitations', e.target.value || null)}
|
||||
placeholder={t("spellDetail.limitationsPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
|
||||
<CollapsableArea title={t("spellDetail.notes")} icon={faStickyNote}>
|
||||
<div className="space-y-4 p-4 bg-secondary/20 rounded-xl border border-secondary/30">
|
||||
<TexteAreaInput
|
||||
value={selectedSpell.notes || ''}
|
||||
setValue={(e) => handleSpellChange('notes', e.target.value || null)}
|
||||
placeholder={t("spellDetail.notesPlaceholder")}
|
||||
/>
|
||||
</div>
|
||||
</CollapsableArea>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
141
components/spells/SpellList.tsx
Normal file
141
components/spells/SpellList.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
'use client';
|
||||
import React, {useState} from 'react';
|
||||
import {SpellListItem, SpellTagProps} from "@/lib/models/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";
|
||||
|
||||
interface SpellListProps {
|
||||
spells: SpellListItem[];
|
||||
tags: SpellTagProps[];
|
||||
handleSpellClick: (spell: SpellListItem) => void;
|
||||
handleAddSpell: () => void;
|
||||
handleManageTags: () => void;
|
||||
}
|
||||
|
||||
export default function SpellList(
|
||||
{
|
||||
spells,
|
||||
tags,
|
||||
handleSpellClick,
|
||||
handleAddSpell,
|
||||
handleManageTags,
|
||||
}: SpellListProps) {
|
||||
const t = useTranslations();
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [filterTag, setFilterTag] = useState<string>('all');
|
||||
|
||||
function getFilteredSpells(): SpellListItem[] {
|
||||
return spells.filter((spell: SpellListItem) => {
|
||||
const matchesSearch = spell.name.toLowerCase().includes(searchQuery.toLowerCase());
|
||||
const matchesTag = filterTag === 'all' || spell.tags.some((tag: SpellTagProps) => tag.id === filterTag);
|
||||
return matchesSearch && matchesTag;
|
||||
});
|
||||
}
|
||||
|
||||
const filteredSpells = getFilteredSpells();
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="px-4 space-y-3">
|
||||
<InputField
|
||||
input={
|
||||
<TextInput
|
||||
value={searchQuery}
|
||||
setValue={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder={t("spellList.search")}
|
||||
/>
|
||||
}
|
||||
actionIcon={faPlus}
|
||||
actionLabel={t("spellList.add")}
|
||||
addButtonCallBack={async () => handleAddSpell()}
|
||||
/>
|
||||
|
||||
<div className="flex flex-wrap gap-3 items-center">
|
||||
<div className="flex-1 min-w-[150px]">
|
||||
<select
|
||||
value={filterTag}
|
||||
onChange={(e) => 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"
|
||||
>
|
||||
<option value="all"
|
||||
className="bg-tertiary text-text-primary">{t("spellList.allTags")}</option>
|
||||
{tags.map((tag: SpellTagProps) => (
|
||||
<option key={tag.id} value={tag.id} className="bg-tertiary text-text-primary">
|
||||
{tag.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleManageTags}
|
||||
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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-y-auto max-h-[calc(100vh-450px)] 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>
|
||||
) : (
|
||||
<div className="space-y-2 p-2">
|
||||
{filteredSpells.map((spell: SpellListItem) => (
|
||||
<div
|
||||
key={spell.id}
|
||||
onClick={() => handleSpellClick(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">
|
||||
{spell.tags.slice(0, 3).map((tag: SpellTagProps) => (
|
||||
<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">
|
||||
+{spell.tags.length - 3}
|
||||
</span>
|
||||
)}
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
66
components/spells/SpellTagChip.tsx
Normal file
66
components/spells/SpellTagChip.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
'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";
|
||||
|
||||
interface SpellTagChipProps {
|
||||
tag: SpellTagProps;
|
||||
onRemove?: () => void;
|
||||
onClick?: () => void;
|
||||
size?: 'sm' | 'md';
|
||||
}
|
||||
|
||||
export default function SpellTagChip(
|
||||
{
|
||||
tag,
|
||||
onRemove,
|
||||
onClick,
|
||||
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 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' : ''}
|
||||
`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={chipClasses}
|
||||
style={{
|
||||
backgroundColor: tag.color || '#3B82F6',
|
||||
color: getContrastColor(tag.color)
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
{tag.name}
|
||||
{onRemove && (
|
||||
<button
|
||||
onClick={(e: React.MouseEvent): void => {
|
||||
e.stopPropagation();
|
||||
onRemove();
|
||||
}}
|
||||
className="ml-0.5 hover:bg-white/20 rounded-full p-0.5 transition-all duration-200 hover:scale-110"
|
||||
>
|
||||
<FontAwesomeIcon icon={faX} className={size === 'sm' ? 'w-2 h-2' : 'w-2.5 h-2.5'}/>
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
250
components/spells/SpellTagManager.tsx
Normal file
250
components/spells/SpellTagManager.tsx
Normal file
@@ -0,0 +1,250 @@
|
||||
'use client';
|
||||
import React, {useContext, useState} from 'react';
|
||||
import {defaultTagColors, SpellTagProps} from "@/lib/models/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";
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export default function SpellTagManager(
|
||||
{
|
||||
tags,
|
||||
onBack,
|
||||
onCreateTag,
|
||||
onUpdateTag,
|
||||
onDeleteTag,
|
||||
}: SpellTagManagerProps) {
|
||||
const t = useTranslations();
|
||||
const {successMessage} = useContext(AlertContext);
|
||||
|
||||
const [newTagName, setNewTagName] = useState<string>('');
|
||||
const [newTagColor, setNewTagColor] = useState<string>(defaultTagColors[0]);
|
||||
const [editingTag, setEditingTag] = useState<SpellTagProps | null>(null);
|
||||
const [editTagName, setEditTagName] = useState<string>('');
|
||||
const [editTagColor, setEditTagColor] = useState<string>('');
|
||||
const [showEditModal, setShowEditModal] = useState<boolean>(false);
|
||||
const [tagToDelete, setTagToDelete] = useState<SpellTagProps | null>(null);
|
||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState<boolean>(false);
|
||||
|
||||
async function handleAddTag(): Promise<void> {
|
||||
if (!newTagName.trim()) {
|
||||
return;
|
||||
}
|
||||
const newTag: SpellTagProps | null = await onCreateTag(newTagName.trim(), newTagColor);
|
||||
if (newTag) {
|
||||
setNewTagName('');
|
||||
setNewTagColor(defaultTagColors[0]);
|
||||
successMessage(t("spellTagManager.successAdd"));
|
||||
}
|
||||
}
|
||||
|
||||
function handleEditClick(tag: SpellTagProps): void {
|
||||
setEditingTag(tag);
|
||||
setEditTagName(tag.name);
|
||||
setEditTagColor(tag.color || defaultTagColors[0]);
|
||||
setShowEditModal(true);
|
||||
}
|
||||
|
||||
async function handleUpdateTag(): Promise<void> {
|
||||
if (!editingTag || !editTagName.trim()) {
|
||||
return;
|
||||
}
|
||||
const success: boolean = await onUpdateTag(editingTag.id, editTagName.trim(), editTagColor);
|
||||
if (success) {
|
||||
setShowEditModal(false);
|
||||
setEditingTag(null);
|
||||
successMessage(t("spellTagManager.successUpdate"));
|
||||
}
|
||||
}
|
||||
|
||||
function handleDeleteClick(tag: SpellTagProps): void {
|
||||
setTagToDelete(tag);
|
||||
setShowDeleteConfirm(true);
|
||||
}
|
||||
|
||||
async function handleDeleteTag(): Promise<void> {
|
||||
if (!tagToDelete) {
|
||||
return;
|
||||
}
|
||||
const success: boolean = await onDeleteTag(tagToDelete.id);
|
||||
if (success) {
|
||||
setShowDeleteConfirm(false);
|
||||
setTagToDelete(null);
|
||||
successMessage(t("spellTagManager.successDelete"));
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<h3 className="text-text-primary font-semibold mb-3">{t("spellTagManager.addTag")}</h3>
|
||||
<div className="space-y-3">
|
||||
<InputField
|
||||
fieldName={t("spellTagManager.tagName")}
|
||||
input={
|
||||
<TextInput
|
||||
value={newTagName}
|
||||
setValue={(e) => setNewTagName(e.target.value)}
|
||||
placeholder={t("spellTagManager.tagNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
className="text-text-primary text-sm font-medium mb-2 block">{t("spellTagManager.tagColor")}</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{defaultTagColors.map((color: string) => (
|
||||
<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}}
|
||||
/>
|
||||
))}
|
||||
</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"/>
|
||||
{t("spellTagManager.addTag")}
|
||||
</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>
|
||||
<p className="text-muted text-sm">{t("spellTagManager.noTags")}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{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"
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showEditModal && editingTag && (
|
||||
<Modal
|
||||
title={t("spellTagManager.editTag")}
|
||||
size="small"
|
||||
onClose={() => setShowEditModal(false)}
|
||||
onConfirm={handleUpdateTag}
|
||||
confirmText={t("common.confirm")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<div className="p-6 space-y-4">
|
||||
<InputField
|
||||
fieldName={t("spellTagManager.tagName")}
|
||||
input={
|
||||
<TextInput
|
||||
value={editTagName}
|
||||
setValue={(e) => setEditTagName(e.target.value)}
|
||||
placeholder={t("spellTagManager.tagNamePlaceholder")}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<div>
|
||||
<span
|
||||
className="text-text-primary text-sm font-medium mb-2 block">{t("spellTagManager.tagColor")}</span>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{defaultTagColors.map((color: string) => (
|
||||
<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}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-2">
|
||||
<span className="text-text-secondary text-sm">{t("spellTagManager.preview")}</span>
|
||||
<div className="mt-2">
|
||||
<SpellTagChip tag={{
|
||||
id: editingTag.id,
|
||||
name: editTagName || editingTag.name,
|
||||
color: editTagColor
|
||||
}}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
{showDeleteConfirm && tagToDelete && (
|
||||
<Modal
|
||||
title={t("spellTagManager.deleteTagTitle")}
|
||||
size="small"
|
||||
onClose={() => setShowDeleteConfirm(false)}
|
||||
onConfirm={handleDeleteTag}
|
||||
confirmText={t("spellTagManager.delete")}
|
||||
cancelText={t("common.cancel")}
|
||||
>
|
||||
<div className="p-6">
|
||||
<p className="text-text-secondary">{t("spellTagManager.confirmDelete")}</p>
|
||||
<div className="mt-4">
|
||||
<SpellTagChip tag={tagToDelete}/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
359
electron/database/models/Spell.ts
Normal file
359
electron/database/models/Spell.ts
Normal file
@@ -0,0 +1,359 @@
|
||||
import SpellRepo, { SpellResult } from '../repositories/spell.repo.js';
|
||||
import SpellTagRepo, { SpellTagResult } from '../repositories/spelltag.repo.js';
|
||||
import BookRepo, { BookToolsTable } from '../repositories/book.repository.js';
|
||||
import System from '../System.js';
|
||||
import { getUserEncryptionKey } from '../keyManager.js';
|
||||
|
||||
export interface SpellTagProps {
|
||||
id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export interface SpellProps {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string[];
|
||||
powerLevel: string | null;
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface SpellListItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
tags: SpellTagProps[];
|
||||
}
|
||||
|
||||
export interface SpellListResponse {
|
||||
enabled: boolean;
|
||||
spells: SpellListItem[];
|
||||
tags: SpellTagProps[];
|
||||
}
|
||||
|
||||
export interface SyncedSpell {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export interface SyncedSpellTag {
|
||||
id: string;
|
||||
name: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
export default class Spell {
|
||||
/**
|
||||
* Retrieves all spell tags for a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of spell tag props
|
||||
*/
|
||||
static getSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps[] {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
|
||||
|
||||
return spellTags.map((tag: SpellTagResult): SpellTagProps => ({
|
||||
id: tag.tag_id,
|
||||
name: System.decryptDataWithUserKey(tag.name, userKey),
|
||||
color: tag.color,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new spell tag to a book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param name - The name of the tag
|
||||
* @param color - The optional color hex code
|
||||
* @param existingTagId - Optional existing tag ID for sync
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The created spell tag props
|
||||
*/
|
||||
static addSpellTag(userId: string, bookId: string, name: string, color: string | null, existingTagId?: string, lang: 'fr' | 'en' = 'fr'): SpellTagProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const tagId: string = existingTagId || System.createUniqueId();
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
|
||||
SpellTagRepo.insertSpellTag(tagId, bookId, userId, encryptedName, nameHash, color, lang);
|
||||
|
||||
return {
|
||||
id: tagId,
|
||||
name: name,
|
||||
color: color,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing spell tag.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param name - The new name of the tag
|
||||
* @param color - The new optional color hex code
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpellTag(userId: string, tagId: string, name: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
|
||||
return SpellTagRepo.updateSpellTag(userId, tagId, encryptedName, nameHash, color, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a spell tag and removes its references from all spells in the book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag to delete
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
static deleteSpellTag(userId: string, tagId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const spells: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
|
||||
|
||||
for (const spell of spells) {
|
||||
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||
let tagsArray: string[] = [];
|
||||
try {
|
||||
tagsArray = JSON.parse(decryptedTags) as string[];
|
||||
} catch {
|
||||
tagsArray = [];
|
||||
}
|
||||
|
||||
if (tagsArray.includes(tagId)) {
|
||||
const updatedTags: string[] = tagsArray.filter((t: string): boolean => t !== tagId);
|
||||
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(updatedTags), userKey);
|
||||
SpellRepo.updateSpellTags(userId, spell.spell_id, encryptedTags, lang);
|
||||
}
|
||||
}
|
||||
|
||||
// Then delete the tag
|
||||
return SpellTagRepo.deleteSpellTag(userId, tagId, lang);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the spell list with tags for a specific book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell list response with enabled status, spells, and tags
|
||||
*/
|
||||
static getSpellList(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellListResponse {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const bookTools: BookToolsTable | null = BookRepo.fetchBookTools(userId, bookId, lang);
|
||||
const enabled: boolean = bookTools ? bookTools.spells_enabled === 1 : false;
|
||||
|
||||
const spellTags: SpellTagResult[] = SpellTagRepo.fetchSpellTags(userId, bookId, lang);
|
||||
const tags: SpellTagProps[] = spellTags.map((tag: SpellTagResult): SpellTagProps => ({
|
||||
id: tag.tag_id,
|
||||
name: System.decryptDataWithUserKey(tag.name, userKey),
|
||||
color: tag.color,
|
||||
}));
|
||||
|
||||
const tagMap: Map<string, SpellTagProps> = new Map();
|
||||
for (const tag of tags) {
|
||||
tagMap.set(tag.id, tag);
|
||||
}
|
||||
|
||||
const spellResults: SpellResult[] = SpellRepo.fetchSpells(userId, bookId, lang);
|
||||
|
||||
const spells: SpellListItem[] = spellResults.map((spell: SpellResult): SpellListItem => {
|
||||
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
|
||||
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
|
||||
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||
|
||||
let tagIds: string[];
|
||||
try {
|
||||
tagIds = JSON.parse(decryptedTags) as string[];
|
||||
} catch {
|
||||
tagIds = [];
|
||||
}
|
||||
|
||||
const resolvedTags: SpellTagProps[] = tagIds
|
||||
.map((tagId: string): SpellTagProps | undefined => tagMap.get(tagId))
|
||||
.filter((tag: SpellTagProps | undefined): tag is SpellTagProps => tag !== undefined);
|
||||
|
||||
const truncatedDescription: string = decryptedDescription.length > 150
|
||||
? decryptedDescription.substring(0, 150) + '...'
|
||||
: decryptedDescription;
|
||||
|
||||
return {
|
||||
id: spell.spell_id,
|
||||
name: decryptedName,
|
||||
description: truncatedDescription,
|
||||
tags: resolvedTags,
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
enabled,
|
||||
spells,
|
||||
tags,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the full details of a specific spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell props with all details
|
||||
*/
|
||||
static getSpellDetail(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const spell: SpellResult | null = SpellRepo.fetchSpellById(userId, spellId, lang);
|
||||
if (!spell) {
|
||||
throw new Error(lang === 'fr' ? 'Sort non trouvé.' : 'Spell not found.');
|
||||
}
|
||||
|
||||
const decryptedName: string = System.decryptDataWithUserKey(spell.name, userKey);
|
||||
const decryptedDescription: string = System.decryptDataWithUserKey(spell.description, userKey);
|
||||
const decryptedAppearance: string = System.decryptDataWithUserKey(spell.appearance, userKey);
|
||||
const decryptedTags: string = System.decryptDataWithUserKey(spell.tags, userKey);
|
||||
|
||||
let tagIds: string[];
|
||||
try {
|
||||
tagIds = JSON.parse(decryptedTags) as string[];
|
||||
} catch {
|
||||
tagIds = [];
|
||||
}
|
||||
|
||||
return {
|
||||
id: spell.spell_id,
|
||||
name: decryptedName,
|
||||
description: decryptedDescription,
|
||||
appearance: decryptedAppearance,
|
||||
tags: tagIds,
|
||||
powerLevel: spell.power_level ? System.decryptDataWithUserKey(spell.power_level, userKey) : null,
|
||||
components: spell.components ? System.decryptDataWithUserKey(spell.components, userKey) : null,
|
||||
limitations: spell.limitations ? System.decryptDataWithUserKey(spell.limitations, userKey) : null,
|
||||
notes: spell.notes ? System.decryptDataWithUserKey(spell.notes, userKey) : null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new spell to a book.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param name - The name of the spell
|
||||
* @param description - The description of the spell
|
||||
* @param appearance - The appearance of the spell
|
||||
* @param tags - The tag IDs array
|
||||
* @param powerLevel - The optional power level
|
||||
* @param components - The optional components
|
||||
* @param limitations - The optional limitations
|
||||
* @param notes - The optional notes
|
||||
* @param existingSpellId - Optional existing spell ID for sync
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The created spell props
|
||||
*/
|
||||
static addSpell(userId: string, bookId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, existingSpellId?: string, lang: 'fr' | 'en' = 'fr'): SpellProps {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
const spellId: string = existingSpellId || System.createUniqueId();
|
||||
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
|
||||
const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
|
||||
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
|
||||
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||
|
||||
SpellRepo.insertSpell(
|
||||
spellId,
|
||||
bookId,
|
||||
userId,
|
||||
encryptedName,
|
||||
nameHash,
|
||||
encryptedDescription,
|
||||
encryptedAppearance,
|
||||
encryptedTags,
|
||||
encryptedPowerLevel,
|
||||
encryptedComponents,
|
||||
encryptedLimitations,
|
||||
encryptedNotes,
|
||||
lang,
|
||||
);
|
||||
|
||||
return {
|
||||
id: spellId,
|
||||
name,
|
||||
description,
|
||||
appearance,
|
||||
tags,
|
||||
powerLevel,
|
||||
components,
|
||||
limitations,
|
||||
notes,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param name - The name of the spell
|
||||
* @param description - The description of the spell
|
||||
* @param appearance - The appearance of the spell
|
||||
* @param tags - The tag IDs array
|
||||
* @param powerLevel - The optional power level
|
||||
* @param components - The optional components
|
||||
* @param limitations - The optional limitations
|
||||
* @param notes - The optional notes
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpell(userId: string, spellId: string, name: string, description: string, appearance: string, tags: string[], powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
const userKey: string = getUserEncryptionKey(userId);
|
||||
|
||||
const encryptedName: string = System.encryptDataWithUserKey(name, userKey);
|
||||
const nameHash: string = System.hashElement(name);
|
||||
const encryptedDescription: string = System.encryptDataWithUserKey(description, userKey);
|
||||
const encryptedAppearance: string = System.encryptDataWithUserKey(appearance, userKey);
|
||||
const encryptedTags: string = System.encryptDataWithUserKey(JSON.stringify(tags), userKey);
|
||||
const encryptedPowerLevel: string | null = powerLevel ? System.encryptDataWithUserKey(powerLevel, userKey) : null;
|
||||
const encryptedComponents: string | null = components ? System.encryptDataWithUserKey(components, userKey) : null;
|
||||
const encryptedLimitations: string | null = limitations ? System.encryptDataWithUserKey(limitations, userKey) : null;
|
||||
const encryptedNotes: string | null = notes ? System.encryptDataWithUserKey(notes, userKey) : null;
|
||||
|
||||
return SpellRepo.updateSpell(
|
||||
userId,
|
||||
spellId,
|
||||
encryptedName,
|
||||
nameHash,
|
||||
encryptedDescription,
|
||||
encryptedAppearance,
|
||||
encryptedTags,
|
||||
encryptedPowerLevel,
|
||||
encryptedComponents,
|
||||
encryptedLimitations,
|
||||
encryptedNotes,
|
||||
lang,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
return SpellRepo.deleteSpell(userId, spellId, lang);
|
||||
}
|
||||
}
|
||||
368
electron/database/repositories/spell.repo.ts
Normal file
368
electron/database/repositories/spell.repo.ts
Normal file
@@ -0,0 +1,368 @@
|
||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
||||
import System from '../System.js';
|
||||
|
||||
export interface SpellResult extends Record<string, SQLiteValue> {
|
||||
spell_id: string;
|
||||
book_id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string;
|
||||
power_level: string | null;
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
}
|
||||
|
||||
export interface BookSpellsTable extends Record<string, SQLiteValue> {
|
||||
spell_id: string;
|
||||
book_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
name_hash: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string;
|
||||
power_level: string | null;
|
||||
components: string | null;
|
||||
limitations: string | null;
|
||||
notes: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SyncedSpellResult extends Record<string, SQLiteValue> {
|
||||
spell_id: string;
|
||||
book_id: string;
|
||||
name: string;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export default class SpellRepo {
|
||||
/**
|
||||
* Fetches all spells for a specific book owned by the user.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of spell results
|
||||
*/
|
||||
static fetchSpells(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellResult[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND book_id=?';
|
||||
const params: SQLiteValue[] = [userId, bookId];
|
||||
return db.all(query, params) as SpellResult[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a single spell by its ID.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell result or null if not found
|
||||
*/
|
||||
static fetchSpellById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): SpellResult | null {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT spell_id, book_id, name, description, appearance, tags, power_level, components, limitations, notes FROM book_spells WHERE user_id=? AND spell_id=?';
|
||||
const params: SQLiteValue[] = [userId, spellId];
|
||||
const spells: SpellResult[] = db.all(query, params) as SpellResult[];
|
||||
return spells.length > 0 ? spells[0] : null;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new spell.
|
||||
* @param spellId - The unique identifier for the new spell
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name for duplicate detection
|
||||
* @param description - The encrypted description
|
||||
* @param appearance - The encrypted appearance
|
||||
* @param tags - The encrypted JSON tags array
|
||||
* @param powerLevel - The encrypted power level (nullable)
|
||||
* @param components - The encrypted components (nullable)
|
||||
* @param limitations - The encrypted limitations (nullable)
|
||||
* @param notes - The encrypted notes (nullable)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell ID if successful
|
||||
*/
|
||||
static insertSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): string {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'INSERT INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds()];
|
||||
const result: RunResult = db.run(query, params);
|
||||
if (!result || result.changes === 0) {
|
||||
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du sort.` : `Error adding spell.`);
|
||||
}
|
||||
return spellId;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible d'ajouter le sort.` : `Unable to add spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name
|
||||
* @param description - The encrypted description
|
||||
* @param appearance - The encrypted appearance
|
||||
* @param tags - The encrypted JSON tags array
|
||||
* @param powerLevel - The encrypted power level (nullable)
|
||||
* @param components - The encrypted components (nullable)
|
||||
* @param limitations - The encrypted limitations (nullable)
|
||||
* @param notes - The encrypted notes (nullable)
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, System.timeStampInSeconds(), spellId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
static deleteSpell(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'DELETE FROM book_spells WHERE spell_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [spellId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de supprimer le sort.` : `Unable to delete spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the tags field of a spell.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param tags - The new encrypted JSON tags array
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpellTags(userId: string, spellId: string, tags: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'UPDATE book_spells SET tags=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [tags, System.timeStampInSeconds(), spellId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de mettre à jour les tags du sort.` : `Unable to update spell tags.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all spells for a book with full table data for sync.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of book spells table records
|
||||
*/
|
||||
static fetchBookSpellsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND book_id=?';
|
||||
const params: SQLiteValue[] = [userId, bookId];
|
||||
return db.all(query, params) as BookSpellsTable[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts.` : `Unable to retrieve spells.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a complete spell record by its ID.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell table record or null
|
||||
*/
|
||||
static fetchSpellTableById(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): BookSpellsTable | null {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update FROM book_spells WHERE user_id=? AND spell_id=?';
|
||||
const params: SQLiteValue[] = [userId, spellId];
|
||||
const spells: BookSpellsTable[] = db.all(query, params) as BookSpellsTable[];
|
||||
return spells.length > 0 ? spells[0] : null;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer le sort.` : `Unable to retrieve spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all synced spells for a user.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of synced spell results
|
||||
*/
|
||||
static fetchSyncedSpells(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellResult[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT spell_id, book_id, name, last_update FROM book_spells WHERE user_id=?';
|
||||
const params: SQLiteValue[] = [userId];
|
||||
return db.all(query, params) as SyncedSpellResult[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les sorts synchronisés.` : `Unable to retrieve synced spells.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates a spell from synchronization data.
|
||||
* @param spellId - The unique identifier for the spell
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name
|
||||
* @param description - The encrypted description
|
||||
* @param appearance - The encrypted appearance
|
||||
* @param tags - The encrypted JSON tags array
|
||||
* @param powerLevel - The encrypted power level (nullable)
|
||||
* @param components - The encrypted components (nullable)
|
||||
* @param limitations - The encrypted limitations (nullable)
|
||||
* @param notes - The encrypted notes (nullable)
|
||||
* @param lastUpdate - The timestamp of the last update
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the insertion was successful
|
||||
*/
|
||||
static insertSyncSpell(spellId: string, bookId: string, userId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'INSERT OR REPLACE INTO book_spells (spell_id, book_id, user_id, name, name_hash, description, appearance, tags, power_level, components, limitations, notes, last_update) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)';
|
||||
const params: SQLiteValue[] = [spellId, bookId, userId, name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible d'insérer le sort.` : `Unable to insert spell.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a spell exists.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the spell exists
|
||||
*/
|
||||
static isSpellExist(userId: string, spellId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT 1 FROM book_spells WHERE spell_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [spellId, userId];
|
||||
const existenceCheck = db.all(query, params);
|
||||
return existenceCheck.length > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du sort.` : `Unable to check spell existence.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a spell with timestamp for sync.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param spellId - The unique identifier of the spell
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name
|
||||
* @param description - The encrypted description
|
||||
* @param appearance - The encrypted appearance
|
||||
* @param tags - The encrypted JSON tags array
|
||||
* @param powerLevel - The encrypted power level (nullable)
|
||||
* @param components - The encrypted components (nullable)
|
||||
* @param limitations - The encrypted limitations (nullable)
|
||||
* @param notes - The encrypted notes (nullable)
|
||||
* @param lastUpdate - The timestamp of the last update
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSyncSpell(userId: string, spellId: string, name: string, nameHash: string, description: string, appearance: string, tags: string, powerLevel: string | null, components: string | null, limitations: string | null, notes: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'UPDATE book_spells SET name=?, name_hash=?, description=?, appearance=?, tags=?, power_level=?, components=?, limitations=?, notes=?, last_update=? WHERE spell_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [name, nameHash, description, appearance, tags, powerLevel, components, limitations, notes, lastUpdate, spellId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le sort.` : `Unable to update spell.`);
|
||||
}
|
||||
}
|
||||
}
|
||||
292
electron/database/repositories/spelltag.repo.ts
Normal file
292
electron/database/repositories/spelltag.repo.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
import { Database, RunResult, SQLiteValue } from 'node-sqlite3-wasm';
|
||||
import System from '../System.js';
|
||||
|
||||
export interface SpellTagResult extends Record<string, SQLiteValue> {
|
||||
tag_id: string;
|
||||
book_id: string;
|
||||
name: string;
|
||||
color: string | null;
|
||||
}
|
||||
|
||||
export interface BookSpellTagsTable extends Record<string, SQLiteValue> {
|
||||
tag_id: string;
|
||||
book_id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
name_hash: string;
|
||||
color: string | null;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export interface SyncedSpellTagResult extends Record<string, SQLiteValue> {
|
||||
tag_id: string;
|
||||
book_id: string;
|
||||
name: string;
|
||||
last_update: number;
|
||||
}
|
||||
|
||||
export default class SpellTagRepo {
|
||||
/**
|
||||
* Fetches all spell tags for a specific book owned by the user.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of spell tag results
|
||||
*/
|
||||
static fetchSpellTags(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): SpellTagResult[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT tag_id, book_id, name, color FROM book_spell_tags WHERE user_id=? AND book_id=?';
|
||||
const params: SQLiteValue[] = [userId, bookId];
|
||||
return db.all(query, params) as SpellTagResult[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a new spell tag.
|
||||
* @param tagId - The unique identifier for the new tag
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param name - The encrypted name of the tag
|
||||
* @param nameHash - The hashed name for duplicate detection
|
||||
* @param color - The optional color hex code
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The tag ID if successful
|
||||
*/
|
||||
static insertSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): string {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'INSERT INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
|
||||
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, System.timeStampInSeconds()];
|
||||
const result: RunResult = db.run(query, params);
|
||||
if (!result || result.changes === 0) {
|
||||
throw new Error(lang === 'fr' ? `Une erreur s'est produite lors de l'ajout du tag.` : `Error adding tag.`);
|
||||
}
|
||||
return tagId;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
}
|
||||
throw new Error(lang === 'fr' ? `Impossible d'ajouter le tag de sort.` : `Unable to add spell tag.`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing spell tag.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param name - The encrypted name of the tag
|
||||
* @param nameHash - The hashed name
|
||||
* @param color - The optional color hex code
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [name, nameHash, color, System.timeStampInSeconds(), tagId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a spell tag.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the deletion was successful
|
||||
*/
|
||||
static deleteSpellTag(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'DELETE FROM book_spell_tags WHERE tag_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [tagId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de supprimer le tag de sort.` : `Unable to delete spell tag.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all spell tags for a book with full table data for sync.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of book spell tags table records
|
||||
*/
|
||||
static fetchBookSpellTagsTable(userId: string, bookId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND book_id=?';
|
||||
const params: SQLiteValue[] = [userId, bookId];
|
||||
return db.all(query, params) as BookSpellTagsTable[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts.` : `Unable to retrieve spell tags.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches a complete spell tag record by its ID.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns The spell tag table record or null
|
||||
*/
|
||||
static fetchSpellTagTableById(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): BookSpellTagsTable | null {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT tag_id, book_id, user_id, name, name_hash, color, last_update FROM book_spell_tags WHERE user_id=? AND tag_id=?';
|
||||
const params: SQLiteValue[] = [userId, tagId];
|
||||
const spellTags: BookSpellTagsTable[] = db.all(query, params) as BookSpellTagsTable[];
|
||||
return spellTags.length > 0 ? spellTags[0] : null;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer le tag de sort.` : `Unable to retrieve spell tag.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches all synced spell tags for a user.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns An array of synced spell tag results
|
||||
*/
|
||||
static fetchSyncedSpellTags(userId: string, lang: 'fr' | 'en' = 'fr'): SyncedSpellTagResult[] {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT tag_id, book_id, name, last_update FROM book_spell_tags WHERE user_id=?';
|
||||
const params: SQLiteValue[] = [userId];
|
||||
return db.all(query, params) as SyncedSpellTagResult[];
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de récupérer les tags de sorts synchronisés.` : `Unable to retrieve synced spell tags.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts or updates a spell tag from synchronization data.
|
||||
* @param tagId - The unique identifier for the tag
|
||||
* @param bookId - The unique identifier of the book
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name
|
||||
* @param color - The optional color hex code
|
||||
* @param lastUpdate - The timestamp of the last update
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the insertion was successful
|
||||
*/
|
||||
static insertSyncSpellTag(tagId: string, bookId: string, userId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'INSERT OR REPLACE INTO book_spell_tags (tag_id, book_id, user_id, name, name_hash, color, last_update) VALUES (?,?,?,?,?,?,?)';
|
||||
const params: SQLiteValue[] = [tagId, bookId, userId, name, nameHash, color, lastUpdate];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible d'insérer le tag de sort.` : `Unable to insert spell tag.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a spell tag exists.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the tag exists
|
||||
*/
|
||||
static isSpellTagExist(userId: string, tagId: string, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'SELECT 1 FROM book_spell_tags WHERE tag_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [tagId, userId];
|
||||
const existenceCheck = db.all(query, params);
|
||||
return existenceCheck.length > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de vérifier l'existence du tag.` : `Unable to check tag existence.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates a spell tag with timestamp for sync.
|
||||
* @param userId - The unique identifier of the user
|
||||
* @param tagId - The unique identifier of the tag
|
||||
* @param name - The encrypted name
|
||||
* @param nameHash - The hashed name
|
||||
* @param color - The optional color hex code
|
||||
* @param lastUpdate - The timestamp of the last update
|
||||
* @param lang - The language for error messages ('fr' or 'en')
|
||||
* @returns True if the update was successful
|
||||
*/
|
||||
static updateSyncSpellTag(userId: string, tagId: string, name: string, nameHash: string, color: string | null, lastUpdate: number, lang: 'fr' | 'en' = 'fr'): boolean {
|
||||
try {
|
||||
const db: Database = System.getDb();
|
||||
const query: string = 'UPDATE book_spell_tags SET name=?, name_hash=?, color=?, last_update=? WHERE tag_id=? AND user_id=?';
|
||||
const params: SQLiteValue[] = [name, nameHash, color, lastUpdate, tagId, userId];
|
||||
const result: RunResult = db.run(query, params);
|
||||
return result.changes > 0;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
console.error(`[SpellTagRepo] DB Error: ${error.message}`);
|
||||
throw new Error(lang === 'fr' ? `Impossible de mettre à jour le tag de sort.` : `Unable to update spell tag.`);
|
||||
} else {
|
||||
console.error('[SpellTagRepo] An unknown error occurred.');
|
||||
throw new Error(lang === 'fr' ? "Une erreur inconnue s'est produite." : 'An unknown error occurred.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
201
electron/ipc/spell.ipc.ts
Normal file
201
electron/ipc/spell.ipc.ts
Normal file
@@ -0,0 +1,201 @@
|
||||
import { ipcMain } from 'electron';
|
||||
import { createHandler } from '../database/LocalSystem.js';
|
||||
import Spell from '../database/models/Spell.js';
|
||||
import type {
|
||||
SpellProps,
|
||||
SpellListResponse,
|
||||
SpellTagProps,
|
||||
} from '../database/models/Spell.js';
|
||||
|
||||
// ==================== INTERFACES ====================
|
||||
|
||||
interface SpellPost {
|
||||
id?: string;
|
||||
name: string;
|
||||
description: string;
|
||||
appearance: string;
|
||||
tags: string[];
|
||||
powerLevel?: string | null;
|
||||
components?: string | null;
|
||||
limitations?: string | null;
|
||||
notes?: string | null;
|
||||
}
|
||||
|
||||
interface GetSpellListData {
|
||||
bookid: string;
|
||||
}
|
||||
|
||||
interface GetSpellTagsData {
|
||||
bookid: string;
|
||||
}
|
||||
|
||||
interface GetSpellDetailData {
|
||||
spellid: string;
|
||||
}
|
||||
|
||||
interface CreateSpellData {
|
||||
bookId: string;
|
||||
spell: SpellPost;
|
||||
}
|
||||
|
||||
interface UpdateSpellData {
|
||||
spellId: string;
|
||||
spell: SpellPost;
|
||||
}
|
||||
|
||||
interface DeleteSpellData {
|
||||
spellId: string;
|
||||
}
|
||||
|
||||
interface CreateTagData {
|
||||
bookId: string;
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
interface UpdateTagData {
|
||||
tagId: string;
|
||||
name: string;
|
||||
color?: string | null;
|
||||
}
|
||||
|
||||
interface DeleteTagData {
|
||||
tagId: string;
|
||||
bookId: string;
|
||||
}
|
||||
|
||||
// ==================== SPELL HANDLERS ====================
|
||||
|
||||
// GET /spell/list
|
||||
ipcMain.handle(
|
||||
'db:spell:list',
|
||||
createHandler<GetSpellListData, SpellListResponse>(
|
||||
function (userId: string, data: GetSpellListData, lang: 'fr' | 'en'): SpellListResponse {
|
||||
return Spell.getSpellList(userId, data.bookid, lang);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// GET /spell/tags
|
||||
ipcMain.handle(
|
||||
'db:spell:tags',
|
||||
createHandler<GetSpellTagsData, SpellTagProps[]>(
|
||||
function (userId: string, data: GetSpellTagsData, lang: 'fr' | 'en'): SpellTagProps[] {
|
||||
return Spell.getSpellTags(userId, data.bookid, lang);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// GET /spell/detail
|
||||
ipcMain.handle(
|
||||
'db:spell:detail',
|
||||
createHandler<GetSpellDetailData, SpellProps>(
|
||||
function (userId: string, data: GetSpellDetailData, lang: 'fr' | 'en'): SpellProps {
|
||||
return Spell.getSpellDetail(userId, data.spellid, lang);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// POST /spell/add
|
||||
ipcMain.handle(
|
||||
'db:spell:create',
|
||||
createHandler<CreateSpellData, string>(
|
||||
function (userId: string, data: CreateSpellData, lang: 'fr' | 'en'): string {
|
||||
const spell: SpellPost = data.spell;
|
||||
const result: SpellProps = Spell.addSpell(
|
||||
userId,
|
||||
data.bookId,
|
||||
spell.name,
|
||||
spell.description,
|
||||
spell.appearance,
|
||||
spell.tags || [],
|
||||
spell.powerLevel || null,
|
||||
spell.components || null,
|
||||
spell.limitations || null,
|
||||
spell.notes || null,
|
||||
spell.id,
|
||||
lang,
|
||||
);
|
||||
return result.id;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// PUT /spell/update
|
||||
ipcMain.handle(
|
||||
'db:spell:update',
|
||||
createHandler<UpdateSpellData, boolean>(
|
||||
function (userId: string, data: UpdateSpellData, lang: 'fr' | 'en'): boolean {
|
||||
const spell: SpellPost = data.spell;
|
||||
return Spell.updateSpell(
|
||||
userId,
|
||||
data.spellId,
|
||||
spell.name,
|
||||
spell.description,
|
||||
spell.appearance,
|
||||
spell.tags || [],
|
||||
spell.powerLevel || null,
|
||||
spell.components || null,
|
||||
spell.limitations || null,
|
||||
spell.notes || null,
|
||||
lang,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// DELETE /spell/delete
|
||||
ipcMain.handle(
|
||||
'db:spell:delete',
|
||||
createHandler<DeleteSpellData, boolean>(
|
||||
function (userId: string, data: DeleteSpellData, lang: 'fr' | 'en'): boolean {
|
||||
return Spell.deleteSpell(userId, data.spellId, lang);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// ==================== SPELL TAG HANDLERS ====================
|
||||
|
||||
// POST /spell/tag/add
|
||||
ipcMain.handle(
|
||||
'db:spell:tag:create',
|
||||
createHandler<CreateTagData, string>(
|
||||
function (userId: string, data: CreateTagData, lang: 'fr' | 'en'): string {
|
||||
const result: SpellTagProps = Spell.addSpellTag(
|
||||
userId,
|
||||
data.bookId,
|
||||
data.name,
|
||||
data.color || null,
|
||||
undefined,
|
||||
lang,
|
||||
);
|
||||
return result.id;
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// PUT /spell/tag/update
|
||||
ipcMain.handle(
|
||||
'db:spell:tag:update',
|
||||
createHandler<UpdateTagData, boolean>(
|
||||
function (userId: string, data: UpdateTagData, lang: 'fr' | 'en'): boolean {
|
||||
return Spell.updateSpellTag(
|
||||
userId,
|
||||
data.tagId,
|
||||
data.name,
|
||||
data.color || null,
|
||||
lang,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
// DELETE /spell/tag/delete
|
||||
ipcMain.handle(
|
||||
'db:spell:tag:delete',
|
||||
createHandler<DeleteTagData, boolean>(
|
||||
function (userId: string, data: DeleteTagData, lang: 'fr' | 'en'): boolean {
|
||||
return Spell.deleteSpellTag(userId, data.tagId, data.bookId, lang);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -456,6 +456,80 @@
|
||||
"characterSectionElement": {
|
||||
"newItem": "New {item}"
|
||||
},
|
||||
"spellComponent": {
|
||||
"enableTool": "Enable spell book",
|
||||
"enableToolDescription": "Manage the spells and magic of your universe.",
|
||||
"errorNameRequired": "Spell name is required.",
|
||||
"errorDescriptionRequired": "Spell description is required.",
|
||||
"errorAppearanceRequired": "Spell appearance is required.",
|
||||
"errorAddSpell": "Error adding spell.",
|
||||
"errorUpdateSpell": "Error updating spell.",
|
||||
"errorDeleteSpell": "Error deleting spell.",
|
||||
"successAdd": "Spell added successfully.",
|
||||
"successUpdate": "Spell updated successfully.",
|
||||
"successDelete": "Spell deleted successfully."
|
||||
},
|
||||
"spellList": {
|
||||
"search": "Search for a spell...",
|
||||
"add": "Add a spell",
|
||||
"manageTags": "Manage tags",
|
||||
"filterByTag": "Filter by tag",
|
||||
"filterByLevel": "Filter by level",
|
||||
"allTags": "All tags",
|
||||
"allLevels": "All levels",
|
||||
"noSpells": "No spells created",
|
||||
"noSpellsDescription": "Add your first spell to get started."
|
||||
},
|
||||
"spellDetail": {
|
||||
"back": "Back",
|
||||
"newSpell": "New spell",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"deleteTitle": "Delete spell",
|
||||
"deleteMessage": "You are about to permanently delete the spell \"{name}\".",
|
||||
"basicInfo": "Basic information",
|
||||
"name": "Spell name",
|
||||
"namePlaceholder": "Enter spell name",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Describe the effects and nature of the spell",
|
||||
"appearance": "Appearance",
|
||||
"appearancePlaceholder": "Describe the visual appearance of the spell",
|
||||
"tags": "Tags",
|
||||
"addTag": "Add a tag...",
|
||||
"createTag": "Create \"{name}\"",
|
||||
"powerLevel": "Power level",
|
||||
"components": "Components",
|
||||
"componentsPlaceholder": "Ingredients, gestures, incantations required...",
|
||||
"limitations": "Limitations",
|
||||
"limitationsPlaceholder": "Restrictions, side effects, conditions of use...",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Additional notes about the spell..."
|
||||
},
|
||||
"spellTagManager": {
|
||||
"title": "Tag management",
|
||||
"back": "Back",
|
||||
"addTag": "Add a tag",
|
||||
"tagName": "Tag name",
|
||||
"tagNamePlaceholder": "Tag name...",
|
||||
"tagColor": "Color",
|
||||
"editTag": "Edit tag",
|
||||
"preview": "Preview",
|
||||
"deleteTagTitle": "Delete tag",
|
||||
"delete": "Delete",
|
||||
"confirmDelete": "Delete this tag? It will be removed from all spells.",
|
||||
"noTags": "No tags created",
|
||||
"successAdd": "Tag added successfully.",
|
||||
"successUpdate": "Tag updated successfully.",
|
||||
"successDelete": "Tag deleted successfully."
|
||||
},
|
||||
"spellPowerLevels": {
|
||||
"none": "None",
|
||||
"minor": "Minor",
|
||||
"moderate": "Moderate",
|
||||
"major": "Major",
|
||||
"legendary": "Legendary",
|
||||
"divine": "Divine"
|
||||
},
|
||||
"aboutEditors": {
|
||||
"title": "About Scribe",
|
||||
"version": "Version",
|
||||
|
||||
@@ -456,6 +456,80 @@
|
||||
"characterSectionElement": {
|
||||
"newItem": "Nouveau {item}"
|
||||
},
|
||||
"spellComponent": {
|
||||
"enableTool": "Activer le grimoire de sorts",
|
||||
"enableToolDescription": "Gérez les sorts et la magie de votre univers.",
|
||||
"errorNameRequired": "Le nom du sort est requis.",
|
||||
"errorDescriptionRequired": "La description du sort est requise.",
|
||||
"errorAppearanceRequired": "L'apparence du sort est requise.",
|
||||
"errorAddSpell": "Erreur lors de l'ajout du sort.",
|
||||
"errorUpdateSpell": "Erreur lors de la mise à jour du sort.",
|
||||
"errorDeleteSpell": "Erreur lors de la suppression du sort.",
|
||||
"successAdd": "Sort ajouté avec succès.",
|
||||
"successUpdate": "Sort mis à jour avec succès.",
|
||||
"successDelete": "Sort supprimé avec succès."
|
||||
},
|
||||
"spellList": {
|
||||
"search": "Rechercher un sort...",
|
||||
"add": "Ajouter un sort",
|
||||
"manageTags": "Gérer les tags",
|
||||
"filterByTag": "Filtrer par tag",
|
||||
"filterByLevel": "Filtrer par niveau",
|
||||
"allTags": "Tous les tags",
|
||||
"allLevels": "Tous les niveaux",
|
||||
"noSpells": "Aucun sort créé",
|
||||
"noSpellsDescription": "Ajoutez votre premier sort pour commencer."
|
||||
},
|
||||
"spellDetail": {
|
||||
"back": "Retour",
|
||||
"newSpell": "Nouveau sort",
|
||||
"save": "Enregistrer",
|
||||
"delete": "Supprimer",
|
||||
"deleteTitle": "Supprimer le sort",
|
||||
"deleteMessage": "Vous êtes sur le point de supprimer le sort « {name} » définitivement.",
|
||||
"basicInfo": "Informations de base",
|
||||
"name": "Nom du sort",
|
||||
"namePlaceholder": "Entrez le nom du sort",
|
||||
"description": "Description",
|
||||
"descriptionPlaceholder": "Décrivez les effets et la nature du sort",
|
||||
"appearance": "Apparence",
|
||||
"appearancePlaceholder": "Décrivez l'apparence visuelle du sort",
|
||||
"tags": "Tags",
|
||||
"addTag": "Ajouter un tag...",
|
||||
"createTag": "Créer \"{name}\"",
|
||||
"powerLevel": "Niveau de puissance",
|
||||
"components": "Composantes",
|
||||
"componentsPlaceholder": "Ingrédients, gestes, incantations nécessaires...",
|
||||
"limitations": "Limitations",
|
||||
"limitationsPlaceholder": "Restrictions, effets secondaires, conditions d'utilisation...",
|
||||
"notes": "Notes",
|
||||
"notesPlaceholder": "Notes supplémentaires sur le sort..."
|
||||
},
|
||||
"spellTagManager": {
|
||||
"title": "Gestion des tags",
|
||||
"back": "Retour",
|
||||
"addTag": "Ajouter un tag",
|
||||
"tagName": "Nom du tag",
|
||||
"tagNamePlaceholder": "Nom du tag...",
|
||||
"tagColor": "Couleur",
|
||||
"editTag": "Modifier le tag",
|
||||
"preview": "Aperçu",
|
||||
"deleteTagTitle": "Supprimer le tag",
|
||||
"delete": "Supprimer",
|
||||
"confirmDelete": "Supprimer ce tag? Il sera retiré de tous les sorts.",
|
||||
"noTags": "Aucun tag créé",
|
||||
"successAdd": "Tag ajouté avec succès.",
|
||||
"successUpdate": "Tag mis à jour avec succès.",
|
||||
"successDelete": "Tag supprimé avec succès."
|
||||
},
|
||||
"spellPowerLevels": {
|
||||
"none": "Aucun",
|
||||
"minor": "Mineur",
|
||||
"moderate": "Modéré",
|
||||
"major": "Majeur",
|
||||
"legendary": "Légendaire",
|
||||
"divine": "Divin"
|
||||
},
|
||||
"aboutEditors": {
|
||||
"title": "À propos de Scribe",
|
||||
"version": "Version",
|
||||
|
||||
@@ -61,6 +61,7 @@ export interface BookToolsSettings {
|
||||
characters: boolean;
|
||||
worlds: boolean;
|
||||
locations: boolean;
|
||||
spells: boolean;
|
||||
}
|
||||
|
||||
export interface BookProps {
|
||||
|
||||
Reference in New Issue
Block a user