Remove unused components and models for improved maintainability

- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files.
- Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter.
- Updated project structure and references to reflect these removals.
This commit is contained in:
natreex
2026-03-22 22:37:31 -04:00
parent e8aaef108b
commit 64ed90d993
229 changed files with 15091 additions and 21289 deletions

94
lib/api/client.ts Normal file
View File

@@ -0,0 +1,94 @@
import axios, {AxiosResponse, Method} from "axios";
import {configs, isDesktop} from "@/lib/configs";
type ContentType = 'application/json' | 'multipart/form-data';
interface ApiRequestConfig {
method: Method;
url: string;
auth: string;
lang?: string;
params?: Record<string, unknown>;
data?: unknown;
contentType?: ContentType;
}
function handleApiError(error: unknown): never {
if (axios.isAxiosError(error)) {
const serverMessage: string = error.response?.data?.message || error.response?.data || error.message;
throw new Error(serverMessage);
} else if (error instanceof Error) {
throw new Error(error.message);
}
throw new Error('An unexpected error occurred');
}
async function apiRequest<T>(config: ApiRequestConfig): Promise<T> {
try {
const headers: Record<string, string> = {
'Authorization': `Bearer ${config.auth}`
};
if (config.contentType) {
headers['Content-Type'] = config.contentType;
}
const response: AxiosResponse<T> = await axios({
method: config.method,
headers,
params: {
lang: config.lang ?? 'fr',
plateforme: isDesktop ? 'desktop' : 'web',
...config.params
},
url: configs.apiUrl + config.url,
data: config.data
});
return response.data;
} catch (error: unknown) {
handleApiError(error);
}
}
export async function apiGet<T>(url: string, auth: string, lang: string = "fr", params: Record<string, unknown> = {}): Promise<T> {
return apiRequest<T>({method: 'GET', url, auth, lang, params});
}
export async function apiPost<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'POST', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiPut<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'PUT', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiPatch<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'PATCH', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiDelete<T>(url: string, data: object, auth: string, lang: string = "fr"): Promise<T> {
return apiRequest<T>({method: 'DELETE', url, auth, lang, data, contentType: 'application/json'});
}
export async function apiPostPublic<T>(url: string, data: object, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {'Content-Type': 'application/json'},
params: {lang, plateforme: isDesktop ? 'desktop' : 'web'},
url: configs.apiUrl + url,
data
});
return response.data;
} catch (error: unknown) {
handleApiError(error);
}
}
export async function apiUpload<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
const formData: FormData = new FormData();
formData.append('file', file);
return apiRequest<T>({method: 'POST', url, auth, lang, data: formData, contentType: 'multipart/form-data'});
}

View File

@@ -9,11 +9,12 @@ export interface Configs {
}
const isProduction: boolean = false;
export const isDesktop: boolean = true;
export const configs: Configs = {
apiUrl: isProduction ? 'https://api.eritors.com/' : 'http://localhost:3001/',
baseUrl: isProduction ? 'https://scribe.eritors.com/' : 'http://localhost:3000/',
appName: 'ERitors Scribe',
appDescription: 'ERitors Scribe est une application de prise de notes et d\'écriture collaborative.',
appName: 'Eritors Scribe',
appDescription: 'Eritors Scribe est une application de prise de notes et d\'écriture collaborative.',
appVersion: packageJson.version,
};

9
lib/constants/book.ts Normal file
View File

@@ -0,0 +1,9 @@
import {SelectBoxProps} from "@/components/form/SelectBox";
export const bookTypes: SelectBoxProps[] = [
{label: 'bookTypes.short', value: 'short'},
{label: 'bookTypes.novelette', value: 'novelette'},
{label: 'bookTypes.novella', value: 'long'},
{label: 'bookTypes.chapbook', value: 'chapbook'},
{label: 'bookTypes.novel', value: 'novel'},
];

9
lib/constants/chapter.ts Normal file
View File

@@ -0,0 +1,9 @@
import {SelectBoxProps} from "@/components/form/SelectBox";
export const chapterVersions: SelectBoxProps[] = [
{value: '1', label: 'chapterVersions.prompt'},
{value: '2', label: 'chapterVersions.draft'},
{value: '3', label: 'chapterVersions.refine'},
{value: '4', label: 'chapterVersions.review'},
{value: '5', label: 'chapterVersions.final'},
];

153
lib/constants/character.ts Normal file
View File

@@ -0,0 +1,153 @@
import {
Zap,
Box,
Brain,
Target,
AlertTriangle,
Fingerprint,
Flame,
Ghost,
Heart,
HeartCrack,
Users,
Quote,
Route,
Ruler,
Shield,
UserX,
Wrench
} from 'lucide-react';
import {SelectBoxProps} from "@/components/form/SelectBox";
import {CharacterElement} from "@/lib/types/character";
export const characterCategories: SelectBoxProps[] = [
{value: 'none', label: 'characterCategories.none'},
{value: 'main', label: 'characterCategories.main'},
{value: 'secondary', label: 'characterCategories.secondary'},
{value: 'recurring', label: 'characterCategories.recurring'},
];
export const characterStatus: SelectBoxProps[] = [
{value: 'alive', label: 'characterStatus.alive'},
{value: 'dead', label: 'characterStatus.dead'},
{value: 'unknown', label: 'characterStatus.unknown'},
];
export const basicCharacterElements: CharacterElement[] = [
{
title: 'Descriptions physiques',
section: 'physical',
placeholder: 'Nouvelle Description Physique',
icon: Ruler,
},
{
title: 'Descriptions psychologiques',
section: 'psychological',
placeholder: 'Nouvelle Description Psychologique',
icon: Brain,
},
];
export const advancedCharacterElements: CharacterElement[] = [
{
title: 'Signes distinctifs',
section: 'distinguishingMarks',
placeholder: 'Nouveau signe distinctif',
icon: Fingerprint,
},
{
title: 'Arc du personnage',
section: 'arc',
placeholder: 'Nouvelle étape de l\'arc',
icon: Route,
},
{
title: 'Secrets',
section: 'secrets',
placeholder: 'Nouveau secret',
icon: UserX,
},
{
title: 'Peurs',
section: 'fears',
placeholder: 'Nouvelle peur',
icon: Ghost,
},
{
title: 'Défauts',
section: 'flaws',
placeholder: 'Nouveau défaut',
icon: HeartCrack,
},
{
title: 'Croyances',
section: 'beliefs',
placeholder: 'Nouvelle croyance',
icon: Heart,
},
{
title: 'Conflits internes',
section: 'conflicts',
placeholder: 'Nouveau conflit',
icon: Zap,
},
{
title: 'Citations',
section: 'quotes',
placeholder: 'Nouvelle citation',
icon: Quote,
},
{
title: 'Relations',
section: 'relations',
placeholder: 'Nouveau Nom de Relation',
icon: Users,
},
{
title: 'Compétences',
section: 'skills',
placeholder: 'Nouvelle Compétence',
icon: Wrench,
},
{
title: 'Faiblesses',
section: 'weaknesses',
placeholder: 'Nouvelle Faiblesse',
icon: AlertTriangle,
},
{
title: 'Forces',
section: 'strengths',
placeholder: 'Nouvelle Force',
icon: Shield,
},
{
title: 'Objectifs',
section: 'goals',
placeholder: 'Nouvel Objectif',
icon: Target,
},
{
title: 'Motivations',
section: 'motivations',
placeholder: 'Nouvelle Motivation',
icon: Flame,
},
{
title: 'Objets importants',
section: 'items',
placeholder: 'Nouvel objet',
icon: Box,
},
{
title: 'Affiliations',
section: 'affiliations',
placeholder: 'Nouvelle affiliation',
icon: Users,
},
];
export const characterElementCategory: CharacterElement[] = [
...basicCharacterElements,
...advancedCharacterElements,
];

35
lib/constants/spell.ts Normal file
View File

@@ -0,0 +1,35 @@
import {SelectBoxProps} from "@/components/form/SelectBox";
import {SpellEditState} from "@/lib/types/spell";
export const initialSpellState: SpellEditState = {
id: null,
name: '',
description: '',
appearance: '',
tags: [],
powerLevel: null,
components: null,
limitations: null,
notes: null,
seriesSpellId: null,
};
export const spellPowerLevels: SelectBoxProps[] = [
{value: 'none', label: 'spellPowerLevels.none'},
{value: 'cantrip', label: 'spellPowerLevels.cantrip'},
{value: 'novice', label: 'spellPowerLevels.novice'},
{value: 'apprentice', label: 'spellPowerLevels.apprentice'},
{value: 'journeyman', label: 'spellPowerLevels.journeyman'},
{value: 'expert', label: 'spellPowerLevels.expert'},
{value: 'master', label: 'spellPowerLevels.master'},
{value: 'grandmaster', label: 'spellPowerLevels.grandmaster'},
{value: 'legendary', label: 'spellPowerLevels.legendary'},
{value: 'divine', label: 'spellPowerLevels.divine'},
];
export const defaultTagColors: string[] = [
'#51AE84', '#3A8B69', '#2196F3', '#1976D2',
'#FFA726', '#FF9800', '#EF5350', '#E53935',
'#AB47BC', '#9C27B0', '#26A69A', '#00897B',
'#5C6BC0', '#3F51B5', '#EC407A', '#D81B60',
];

135
lib/constants/story.ts Normal file
View File

@@ -0,0 +1,135 @@
import {RadioBoxValue} from "@/components/form/RadioBox";
import {SelectBoxProps} from "@/components/form/SelectBox";
export const storyStates: RadioBoxValue[] = [
{label: 'Suite', value: 0},
{label: 'Début', value: 1},
{label: 'Fin', value: 2},
{label: 'Introduction', value: 3},
{label: 'Final', value: 4},
];
export const beginnerPredefinedType: SelectBoxProps[] = [
{label: `Thème défini disponible.`, value: '0'},
{label: `Féerique`, value: '3'},
{label: 'Romance historique', value: '10'},
{label: 'Conte de fées moderne', value: '13'},
{label: 'Romance contemporaine', value: '17'},
{label: 'Conte moral', value: '20'},
];
export const intermediatePredefinedType: SelectBoxProps[] = [
{label: `Thème défini disponible.`, value: '0'},
{label: `Histoire d'horreur jeune adulte`, value: '1'},
{label: 'Féerique', value: '3'},
{label: 'Romance dramatique', value: '5'},
{label: 'Fantastique sombre', value: '9'},
{label: 'Romance historique', value: '10'},
{label: 'Science-fiction utopique', value: '12'},
{label: 'Conte de fées moderne', value: '13'},
{label: 'Drame familial', value: '14'},
{label: 'Romance contemporaine', value: '17'},
{label: 'Science-fiction post-apocalyptique', value: '19'},
{label: 'Conte moral', value: '20'},
];
export const advancedPredefinedType: SelectBoxProps[] = [
{label: `Thème défini disponible.`, value: '0'},
{label: `Histoire d'horreur jeune adulte`, value: '1'},
{label: `Horreur adulte`, value: '2'},
{label: 'Féerique', value: '3'},
{label: 'Science-fiction dystopique', value: '4'},
{label: 'Romance dramatique', value: '5'},
{label: 'Aventure épique', value: '6'},
{label: 'Conte philosophique', value: '7'},
{label: 'Thriller psychologique', value: '8'},
{label: 'Fantastique sombre', value: '9'},
{label: 'Romance historique', value: '10'},
{label: 'Polar noir', value: '11'},
{label: 'Science-fiction utopique', value: '12'},
{label: 'Conte de fées moderne', value: '13'},
{label: 'Drame familial', value: '14'},
{label: 'Aventure maritime', value: '15'},
{label: 'Fantaisie épique', value: '16'},
{label: 'Romance contemporaine', value: '17'},
{label: "Thriller d'espionnage", value: '18'},
{label: 'Science-fiction post-apocalyptique', value: '19'},
{label: 'Conte moral', value: '20'},
];
export const beginnerNarrativePersons: SelectBoxProps[] = [
{label: 'Sélectionner un type narrative.', value: '0'},
{label: 'Première personne (Je acteur) - Implication émotionnelle', value: '1'},
{label: 'Troisième omnisciente - Narration divine, savoir total', value: '3'},
];
export const intermediateNarrativePersons: SelectBoxProps[] = [
{label: 'Sélectionner un type narrative.', value: '0'},
{label: 'Première personne (Je acteur) - Implication émotionnelle', value: '1'},
{label: 'Première personne (Je témoin) - Observation extérieure', value: '2'},
{label: 'Troisième omnisciente - Narration divine, savoir total', value: '3'},
{label: 'Troisième limitée - Focus sur 1 personnage', value: '4'},
];
export const advancedNarrativePersons: SelectBoxProps[] = [
{label: 'Sélectionner un type narrative.', value: '0'},
{label: 'Première personne (Je acteur) - Implication émotionnelle', value: '1'},
{label: 'Première personne (Je témoin) - Observation extérieure', value: '2'},
{label: 'Troisième omnisciente - Narration divine, savoir total', value: '3'},
{label: 'Troisième limitée - Focus sur 1 personnage', value: '4'},
{label: 'Deuxième personne (Tu) - Immersion/confrontation', value: '5'},
{label: 'Nous collectif - Voix chorale, destin partagé', value: '6'},
];
export const langues: SelectBoxProps[] = [
{label: 'Sélectionner une langue.', value: '0'},
{label: 'Français Canada', value: '1'},
{label: 'Français France', value: '2'},
{label: 'Français Québécois', value: '3'},
{label: 'English Canada', value: '4'},
];
export const beginnerDialogueTypes: SelectBoxProps[] = [
{label: 'Sélectionner un type de dialogue.', value: '0'},
{label: 'Dialogue direct - Paroles exactes des personnages', value: '1'},
{label: 'Dialogue indirect - Paroles résumées par le narrateur', value: '2'},
];
export const intermediateDialogueTypes: SelectBoxProps[] = [
{label: 'Sélectionner un type de dialogue.', value: '0'},
{label: 'Dialogue direct - Paroles exactes des personnages', value: '1'},
{label: 'Dialogue indirect - Paroles résumées par le narrateur', value: '2'},
{label: 'Dialogue mixte - Mélange de dialogue direct et indirect', value: '3'},
];
export const advancedDialogueTypes: SelectBoxProps[] = [
{label: 'Sélectionner un type de dialogue.', value: '0'},
{label: 'Dialogue direct - Paroles exactes des personnages', value: '1'},
{label: 'Dialogue indirect - Paroles résumées par le narrateur', value: '2'},
{label: 'Dialogue mixte - Mélange de dialogue direct et indirect', value: '3'},
{label: 'Monologue intérieur - Interaction avec soi-même', value: '4'},
];
export const verbalTime: SelectBoxProps[] = [
{label: 'Sélectionner un temps verbal.', value: '0'},
{label: 'Passé Simple', value: '1'},
{label: 'Passé Immédiat → Témoignages, récits autobiographiques', value: '2'},
{label: 'Passé Profond → Flashbacks littéraires, tragédies', value: '3'},
{label: 'Présent Brut → Urgence, immersion totale', value: '4'},
{label: 'Présent Réflexif → Méditations philosophiques', value: '5'},
{label: 'Futur Projeté → Prophéties, plans stratégiques', value: '6'},
{label: 'Futur Catastrophe → Dystopies, récits post-apocalyptiques', value: '7'},
{label: 'Imparfait Onirique → Rêves, souvenirs déformés', value: '8'},
{label: 'Conditionnel Hypothétique → Uchronies, réalités alternatives', value: '9'},
{label: 'Subjonctif Angoissé → Drames psychologiques, dilemmes', value: '10'},
{label: 'Mélancolie Composée → Regrets, introspection nostalgique', value: '11'},
{label: 'Urgence Narrative → Urgences', value: '12'},
{label: 'Présent Émotionnel → Émotions intenses', value: '13'},
{label: 'Présent Introspectif → Réflexions profondes', value: '14'},
{label: 'Présent Historique → Histoires historiques', value: '15'},
{label: 'Passé Réflexif → Récits introspectifs', value: '16'},
{label: 'Futur Prophétique → Prophéties, visions apocalyptiques', value: '17'},
{label: 'Conditionnel Visionnaire → Mondes parallèles', value: '18'},
{label: 'Imparfait Poétique → Lyrisme, poésie narrative', value: '19'},
{label: 'Second Person Narrative → Immersion totale', value: '20'},
];

8
lib/constants/user.ts Normal file
View File

@@ -0,0 +1,8 @@
import {SelectBoxProps} from "@/components/form/SelectBox";
export const writingLevel: SelectBoxProps[] = [
{value: '0', label: 'Sélectionner un niveau d\'écriture'},
{value: '1', label: 'Je suis débutant'},
{value: '2', label: 'Je suis intermédiaire'},
{value: '3', label: 'Je suis avancé'},
];

30
lib/constants/world.ts Normal file
View File

@@ -0,0 +1,30 @@
import {
Crown,
AlertTriangle,
Flag,
Gavel,
Factory,
Leaf,
Mountain,
Music,
ArrowLeftRight,
Snowflake,
UserCog,
Users
} from 'lucide-react';
import {ElementSection} from "@/lib/types/world";
export const elementSections: ElementSection[] = [
{title: 'Lois', section: 'laws', icon: Gavel},
{title: 'Biomes', section: 'biomes', icon: Mountain},
{title: 'Problèmes', section: 'issues', icon: AlertTriangle},
{title: 'Coutumes', section: 'customs', icon: ArrowLeftRight},
{title: 'Royaumes', section: 'kingdoms', icon: Flag},
{title: 'Climat', section: 'climate', icon: Snowflake},
{title: 'Ressources', section: 'resources', icon: Factory},
{title: 'Faune', section: 'wildlife', icon: Leaf},
{title: 'Arts', section: 'arts', icon: Music},
{title: 'Groupes ethniques', section: 'ethnicGroups', icon: Users},
{title: 'Classes sociales', section: 'socialClasses', icon: UserCog},
{title: 'Personnages importants', section: 'importantCharacters', icon: Crown},
];

30
lib/i18n.ts Normal file
View File

@@ -0,0 +1,30 @@
import i18n from 'i18next';
import {initReactI18next, useTranslation} from 'react-i18next';
import fr from '@/lib/locales/fr.json';
import en from '@/lib/locales/en.json';
i18n.use(initReactI18next).init({
resources: {
fr: {translation: fr},
en: {translation: en},
},
lng: 'fr',
fallbackLng: 'fr',
interpolation: {escapeValue: false},
});
export function useTranslations(namespace?: string) {
const {t} = useTranslation();
if (namespace) {
return (key: string, params?: Record<string, unknown>) =>
t(`${namespace}.${key}`, params as Record<string, string>);
}
return (key: string, params?: Record<string, unknown>) =>
t(key, params as Record<string, string>);
}
export function changeLanguage(lang: string) {
i18n.changeLanguage(lang);
}
export {i18n};

1
lib/image.ts Normal file
View File

@@ -0,0 +1 @@
export const Image = 'img';

View File

@@ -13,121 +13,6 @@
"activate": "Activate",
"cancel": "Cancel"
},
"loginPage": {
"title": "Login",
"welcome": "Welcome to ERitors",
"orSocial": "or continue with",
"noAccount": "Don't have an account yet?",
"createAccount": "Create one here",
"backToLogin": "Back to login",
"offlineWarning": {
"title": "First sync required",
"message": "An Internet connection is required for your first login to sync your data."
}
},
"loginForm": {
"error": {
"emailRequired": "Your email must be filled in.",
"passwordRequired": "Your password must be filled in.",
"emailLength": "Your email must have at least 3 characters.",
"emailInvalidChars": "Your email contains invalid characters.",
"connection": "An error occurred during login.",
"server": "A server error occurred during login.",
"unknown": "An unknown error occurred during login."
},
"fields": {
"email": {
"label": "Email address",
"placeholder": "your.email@example.com"
},
"password": {
"label": "Password",
"placeholder": "••••••••",
"forgot": "Forgot password?"
}
},
"loading": "Logging in...",
"submit": "Log in"
},
"registerPage": {
"title": "Create account",
"subtitle": "Create a free account and start your adventure with us.",
"progress": {
"infos": "Information",
"verif": "Verification"
},
"backToLogin": "Back to login"
},
"registerStepOne": {
"fields": {
"firstName": {
"label": "First Name",
"placeholder": "Your first name"
},
"lastName": {
"label": "Last Name",
"placeholder": "Your last name"
},
"username": {
"label": "Username",
"placeholder": "Choose a username",
"note": "Username must be at least 3 characters"
},
"email": {
"label": "Email Address",
"placeholder": "your.email@example.com"
},
"password": {
"label": "Password",
"placeholder": "••••••••"
},
"repeatPassword": {
"label": "Confirm Password",
"placeholder": "••••••••"
}
},
"next": "Next"
},
"resetPassword": {
"title": "Forgot password",
"subtitle": "Reset your password in a few simple steps",
"progress": {
"email": "Email",
"verification": "Verification",
"final": "Finalization"
},
"fields": {
"email": {
"label": "Email address",
"placeholder": "your.email@example.com"
},
"code": {
"label": "Verification code",
"placeholder": "Enter the code received by email"
},
"newPassword": {
"label": "New password",
"placeholder": "••••••••"
}
},
"verify": "Verify",
"confirm": "Confirm",
"changePassword": "Change password",
"back": "Back",
"success": "Your password has been successfully updated!",
"goToLogin": "Go to login page",
"backToLogin": "Back to login",
"error": {
"codeServer": "An error occurred while verifying the code on the server.",
"codeUnknown": "An unknown error occurred while verifying the code.",
"emailInvalid": "Your email is invalid. Please enter a valid email address.",
"emailFormat": "Your input is not a valid email address.",
"emailServer": "An error occurred while verifying the email on the server.",
"emailUnknown": "An unknown error occurred while verifying the email.",
"passwordServer": "An error occurred while changing the password on the server.",
"passwordUnknown": "An unknown error occurred while changing the password."
}
},
"controllerBar": {
"bookNotFound": "No book found",
"errorGettingBook": "Error while trying to get the book information",
@@ -137,6 +22,10 @@
"unknownBookError": "Unknown error while retrieving book",
"unknownChapterError": "Unknown error while retrieving chapter"
},
"userMenu": {
"settings": "Settings",
"logout": "Logout"
},
"bookList": {
"library": "Library",
"booksAreMirrors": "\"Books are the mirrors of the soul\"",
@@ -161,18 +50,7 @@
"bookCard": {
"noCoverAlt": "No cover",
"initialsSeparator": ".",
"subtitlePlaceholder": "No subtitle",
"synced": "Synced",
"localOnly": "Local only",
"serverOnly": "Server only",
"toSyncFromServer": "Download from server",
"toSyncToServer": "Upload to server",
"sync": "Sync",
"uploadError": "Error uploading book.",
"downloadError": "Error downloading book.",
"syncFromServerError": "Error syncing from server.",
"syncToServerError": "Error syncing to server.",
"refreshError": "Error refreshing books."
"subtitlePlaceholder": "No subtitle"
},
"scribeTopBar": {
"logoAlt": "Logo",
@@ -210,11 +88,41 @@
},
"importBook": {
"title": "Import a book",
"description": "Import a book from a DOCX file.",
"badge": "IMPORT"
"description": "Import a DOCX file to create a book.",
"badge": "DOCX"
}
}
},
"importBook": {
"header": "Import a book",
"pickFile": "Select a DOCX file",
"parsing": "Parsing file...",
"fields": {
"type": "Book type",
"title": "Title",
"subTitle": "Subtitle",
"summary": "Summary",
"version": "Chapter version"
},
"chapters": {
"title": "Chapters",
"detected": "{count} chapter(s) detected",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"words": "words"
},
"submit": "Import",
"importing": "Importing...",
"success": "Book imported successfully.",
"error": {
"invalidFormat": "Invalid file format. Please select a DOCX file.",
"parseFailed": "Failed to parse the file.",
"titleRequired": "Title is required.",
"typeRequired": "Book type is required.",
"noChaptersSelected": "Please select at least one chapter.",
"importFailed": "An error occurred while importing the book."
}
},
"scribeChapterComponent": {
"sheetHeading": "Sheet",
"createSheet": "Create your sheet",
@@ -255,7 +163,7 @@
},
"spells": {
"title": "Spell Book",
"description": "Create and manage spells, magic systems, and supernatural abilities.",
"description": "Manage the spells and magic of your universe.",
"badge": "SPELL"
},
"items": {
@@ -303,7 +211,12 @@
"exampleHeading": "Example",
"literaryUsageHeading": "Literary usage",
"description": "Search for a word to get its definition, usage examples, and literary tips.",
"errorUnknown": "An unknown error occurred while searching for the word."
"errorUnknown": "An unknown error occurred while searching for the word.",
"errorNoResponse": "No response received from the server.",
"locked": {
"title": "Access required",
"description": "A QuillSense basic subscription or an API key is required to enable the smart dictionary."
}
},
"synonyms": {
"heading": "Lexical search",
@@ -319,7 +232,12 @@
"emptyAntonymsTitle": "Antonym Search",
"emptySynonymsDescription": "Enter a word to find synonyms suitable for different writing contexts.",
"emptyAntonymsDescription": "Enter a word to find antonyms suitable for different writing contexts.",
"errorUnknown": "An unknown error occurred while searching for the word."
"errorUnknown": "An unknown error occurred while searching for the word.",
"errorNoResponse": "No response received from the server.",
"locked": {
"title": "Access required",
"description": "A QuillSense basic subscription or an API key is required to enable synonym search."
}
},
"inspireMe": {
"fieldName": "Find inspiration",
@@ -332,7 +250,17 @@
"emptyHeading": "Inspire me",
"emptyDescription": "Search for ideas to enrich your writing. Enter a prompt and let AI inspire you with creative suggestions based on your current content.",
"emptyPromptError": "Please enter a prompt to get inspired.",
"errorUnknown": "An unknown error occurred while trying to fetch inspiration."
"error": {
"contentRetrieval": "Error retrieving content.",
"contentRetrievalUnknown": "Unknown error retrieving content.",
"noBook": "No book selected.",
"noChapter": "No chapter selected.",
"unknown": "An unknown error occurred during generation."
},
"locked": {
"title": "Access required",
"description": "A QuillSense basic subscription or an API key is required to enable the Inspire Me mode."
}
},
"conjugator": {
"locked": {
@@ -467,82 +395,17 @@
"errorCategoryRequired": "Character role is required.",
"successAdd": "Character added successfully.",
"successUpdate": "Character updated successfully.",
"successDelete": "Character deleted successfully.",
"errorAddCharacter": "Error adding character.",
"errorUpdateCharacter": "Error updating character.",
"errorDeleteCharacter": "Error deleting character.",
"errorAddAttribute": "Error adding attribute.",
"errorRemoveAttribute": "Error removing attribute.",
"errorDeleteCharacter": "Error deleting character.",
"successDelete": "Character deleted successfully.",
"enableTool": "Enable characters",
"enableToolDescription": "Enable character management for this book.",
"toolEnabled": "Character management enabled.",
"toolDisabled": "Character management disabled."
},
"characterDetail": {
"back": "Back",
"newCharacter": "New character",
"exportToSeries": "Export to series",
"deleteTitle": "Delete character",
"deleteMessage": "You are about to permanently delete the character \"{name}\".",
"basicInfo": "Basic information",
"name": "Name",
"namePlaceholder": "Enter a name",
"lastName": "Last name",
"lastNamePlaceholder": "Example: Smith",
"nickname": "Nickname",
"nicknamePlaceholder": "Alias or nickname",
"role": "Role",
"title": "Title",
"titlePlaceholder": "Ex: King, Captain, Doctor...",
"gender": "Gender",
"genderPlaceholder": "Ex: Male, Female, Non-binary",
"age": "Age",
"agePlaceholder": "Ex: 25",
"yearsOld": "years old",
"species": "Species",
"speciesPlaceholder": "Ex: Human, Elf, Vampire",
"nationality": "Nationality/Origin",
"nationalityPlaceholder": "Ex: French, Elven",
"status": "Status",
"residence": "Place of residence",
"residencePlaceholder": "Where the character lives",
"speechPattern": "Speech pattern",
"speechPatternPlaceholder": "Verbal tics, accent, vocabulary...",
"catchphrase": "Catchphrase",
"catchphrasePlaceholder": "Character's recurring quote",
"notes": "Author notes",
"notesPlaceholder": "Personal notes, reminders...",
"colorLabel": "Associated color",
"colorPlaceholder": "Ex: #51AE84 or green",
"advancedMode": "Advanced mode",
"showAdvanced": "Show",
"hideAdvanced": "Hide",
"identitySection": "Extended identity",
"voiceSection": "Character voice",
"authorSection": "Author notes",
"historySection": "Background",
"biography": "Biography",
"biographyPlaceholder": "Character biography.",
"history": "History",
"historyPlaceholder": "Character history...",
"roleFull": "Role",
"roleFullPlaceholder": "Role of the character in the story",
"fetchAttributesError": "Error fetching attributes."
},
"characterList": {
"search": "Search for a character...",
"add": "Add a character",
"unknownImage": "?",
"unknown": "Unknown",
"noLastName": "No last name",
"noTitle": "No title",
"noRole": "No role",
"noCharacters": "No characters",
"noCharactersDescription": "Add your first character to get started."
},
"characterSectionElement": {
"newItem": "New {item}"
},
"spellComponent": {
"exportSuccess": "Spell exported to series successfully.",
"enableTool": "Enable spell book",
@@ -613,16 +476,77 @@
},
"spellPowerLevels": {
"none": "None",
"cantrip": "Cantrip",
"novice": "Novice",
"apprentice": "Apprentice",
"journeyman": "Journeyman",
"expert": "Expert",
"master": "Master",
"grandmaster": "Grandmaster",
"minor": "Minor",
"moderate": "Moderate",
"major": "Major",
"legendary": "Legendary",
"divine": "Divine"
},
"characterDetail": {
"back": "Back",
"newCharacter": "New character",
"exportToSeries": "Export to series",
"deleteTitle": "Delete character",
"deleteMessage": "You are about to permanently delete the character \"{name}\".",
"basicInfo": "Basic information",
"name": "Name",
"namePlaceholder": "Enter a name",
"lastName": "Last name",
"lastNamePlaceholder": "Example: Smith",
"nickname": "Nickname",
"nicknamePlaceholder": "Alias or nickname",
"role": "Role",
"title": "Title",
"titlePlaceholder": "Ex: King, Captain, Doctor...",
"gender": "Gender",
"genderPlaceholder": "Ex: Male, Female, Non-binary",
"age": "Age",
"agePlaceholder": "Ex: 25",
"yearsOld": "years old",
"species": "Species",
"speciesPlaceholder": "Ex: Human, Elf, Vampire",
"nationality": "Nationality/Origin",
"nationalityPlaceholder": "Ex: French, Elven",
"status": "Status",
"residence": "Place of residence",
"residencePlaceholder": "Where the character lives",
"speechPattern": "Speech pattern",
"speechPatternPlaceholder": "Verbal tics, accent, vocabulary...",
"catchphrase": "Catchphrase",
"catchphrasePlaceholder": "Character's recurring quote",
"notes": "Author notes",
"notesPlaceholder": "Personal notes, reminders...",
"colorLabel": "Associated color",
"colorPlaceholder": "Ex: #51AE84 or green",
"advancedMode": "Advanced mode",
"showAdvanced": "Show",
"hideAdvanced": "Hide",
"identitySection": "Extended identity",
"voiceSection": "Character voice",
"authorSection": "Author notes",
"historySection": "Background",
"biography": "Biography",
"biographyPlaceholder": "Character biography.",
"history": "History",
"historyPlaceholder": "Character history...",
"roleFull": "Role",
"roleFullPlaceholder": "Role of the character in the story",
"fetchAttributesError": "Error fetching attributes."
},
"characterList": {
"search": "Search for a character...",
"add": "Add a character",
"unknownImage": "?",
"unknown": "Unknown",
"noLastName": "No last name",
"noTitle": "No title",
"noRole": "No role",
"noCharacters": "No characters",
"noCharactersDescription": "Add your first character to get started."
},
"characterSectionElement": {
"newItem": "New {item}"
},
"aboutEditors": {
"title": "About Scribe",
"version": "Version",
@@ -642,29 +566,13 @@
"yourLocations": "Your locations",
"characters": "Characters",
"spells": "Spell Book",
"quillsense": "QuillSense Settings",
"export": "Export your book",
"objectsList": "Objects list",
"bookGoals": "Book goals",
"quillsense": "QuillSense Settings",
"export": "Export Book",
"save": "Save",
"notAvailable": "Option not available"
},
"exportOption": {
"title": "Export Your Book",
"description": "Choose the format and chapters to export.",
"format": "Format",
"selectFormat": "Select a format",
"chapters": "Chapters",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"version": "Version",
"export": "Export",
"exporting": "Exporting...",
"noChapters": "No chapters available for export.",
"success": "Book exported successfully!",
"cancelled": "Export cancelled.",
"error": "Error exporting the book.",
"loadingChapters": "Loading chapters..."
"notAvailable": "Option not available",
"unknownError": "An unknown error occurred."
},
"noBookHome": {
"title": "Your work is waiting for its first words",
@@ -675,14 +583,7 @@
"preferences": "Preferences",
"ghostWriter": "Ghost Writer",
"draftCompanion": "Draft Companion",
"save": "Save",
"saving": "Saving...",
"close": "Close",
"toolbar": {
"1": "1",
"2": "2",
"3": "3"
}
"save": "Save"
},
"draftCompanion": {
"noPreviousVersion": "No previous version of this chapter",
@@ -696,13 +597,13 @@
"words": "Words",
"refine": "Refine",
"refining": "Refining...",
"abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh."
"abortSuccess": "Generation stopped. Token and cost totals will be available on next page refresh.",
"sseParsingError": "Error reading generation data"
},
"ghostWriter": {
"title": "Ghost Writer",
"description": "Turn your ideas into captivating prose",
"subscriptionRequired": "You must be subscribed to Quill Sense to use Ghost Writer.",
"quillsenseDisabled": "QuillSense is disabled for this book. You can enable it in the book settings.",
"subscriptionRequired": "You must be subscribed to QuillSense Pro to use Ghost Writer.",
"subscribe": "Subscribe",
"length": "Text length",
"minimum": "Minimum",
@@ -719,6 +620,7 @@
"cancel": "Cancel",
"generate": "Generate",
"generating": "Generating...",
"stop": "Stop",
"successGenerate": "Text generated successfully.",
"successInsert": "Excerpt successfully inserted",
"successImport": "Content imported successfully",
@@ -740,6 +642,7 @@
"unknownError": "Unknown error saving settings."
},
"tags": {
"addTagPlaceholder": "Tags",
"unknownError": "Unknown error managing tags."
}
},
@@ -776,12 +679,16 @@
"sépia": "Sepia"
},
"focusMode": "Focus mode (hide distractions)",
"reset": "Reset preferences"
"reset": "Reset preferences",
"saveError": "Error saving preferences.",
"unknownError": "An unknown error occurred while saving preferences."
},
"scribeFooterBar": {
"sheet": "Sheet : ",
"madeWith": "Scribe Editor made with",
"words": "Words",
"words": "words",
"pages": "pages",
"paragraphs": "paragraphs",
"books": "Books"
},
"addNewBookForm": {
@@ -1082,17 +989,7 @@
"book": "book",
"books": "books",
"series": "Series",
"settings": "Series settings",
"synced": "Synced",
"localOnly": "Local only",
"serverOnly": "Server only",
"toSyncFromServer": "Download from server",
"toSyncToServer": "Upload to server",
"uploadError": "Error uploading series.",
"downloadError": "Error downloading series.",
"syncFromServerError": "Error syncing from server.",
"syncToServerError": "Error syncing to server.",
"refreshError": "Error refreshing series."
"settings": "Series settings"
},
"basicInformationSetting": {
"error": {
@@ -1120,6 +1017,12 @@
"generateWithQuillSense": "Generate with QuillSense"
}
},
"quillList": {
"untitled": "Untitled",
"error": {
"unknown": "An unknown error occurred while loading conversations."
}
},
"quillConversation": {
"emptyMessageError": "Please enter a message before sending it.",
"inputPlaceholder": "What's on your mind?",
@@ -1160,23 +1063,25 @@
"edit": "Edit",
"exportToSeries": "Export to series",
"save": "Save",
"unknownError": "An unknown error occurred",
"loading": "Loading..."
"unknownError": "An unknown error occurred"
},
"syncField": {
"uploadSuccess": "{count} element(s) updated successfully.",
"uploadTooltip": "Push to series",
"downloadTooltip": "Pull from series"
},
"seriesImport": {
"importButton": "Import",
"importFromSeries": "Import from series",
"selectElement": "Select an element"
"spellPowerLevels": {
"none": "None",
"cantrip": "Cantrip",
"novice": "Novice",
"apprentice": "Apprentice",
"journeyman": "Journeyman",
"expert": "Expert",
"master": "Master",
"grandmaster": "Grandmaster",
"legendary": "Legendary",
"divine": "Divine"
},
"editor": {
"error": {
"savedFailed": "Save failed",
"unknownError": "An unknown error occurred"
"unknownError": "An unknown error occurred",
"parsingContent": "Error loading chapter content"
},
"success": {
"saved": "Saved successfully"
@@ -1217,18 +1122,22 @@
"userNotFound": "User not found",
"authenticationError": "Error during authentication",
"termsAcceptError": "Error accepting terms of service",
"lastChapterError": "Error retrieving last chapter",
"localDataError": "Unable to load local data",
"encryptionKeyError": "Encryption key not found",
"offlineModeError": "Error initializing offline mode",
"offlineInitError": "Error initializing offline mode",
"syncError": "Error syncing data",
"dbInitError": "Error initializing local database",
"offlineError": "Error checking offline mode",
"fetchBooksError": "Error fetching books",
"fetchSeriesError": "Error fetching series"
"lastChapterError": "Error retrieving last chapter"
}
},
"quillsenseSetting": {
"enableQuillsense": "Enable QuillSense",
"enableDescription": "When disabled, all AI features will be hidden for this book.",
"advancedPrompt": "Advanced prompt for Ghost Writer",
"advancedPromptPlaceholder": "Enter custom instructions to guide the AI when generating text...",
"advancedPromptDescription": "This prompt will be used as a priority directive when generating text with Ghost Writer.",
"saveSuccess": "QuillSense settings saved successfully.",
"saveError": "Error saving settings.",
"unknownError": "An unknown error occurred.",
"enable_characters": "Enable characters",
"enable_worlds": "Enable worlds",
"enable_locations": "Enable locations"
},
"shortStoryGenerator": {
"title": "Short Story Generator",
"tabs": {
@@ -1285,123 +1194,221 @@
"close": "Close"
}
},
"userMenu": {
"settings": "Settings",
"logout": "Logout"
"syncField": {
"uploadSuccess": "{count} element(s) updated successfully.",
"uploadTooltip": "Push to series",
"downloadTooltip": "Pull from series"
},
"exportOption": {
"formatLabel": "Export format",
"chapters": "Chapters",
"loadingChapters": "Loading chapters...",
"noChaptersAvailable": "No chapters available for export.",
"selectAll": "Select all",
"deselectAll": "Deselect all",
"exportButton": "Export",
"exporting": "Exporting...",
"noBookSelected": "No book selected.",
"noChaptersSelected": "Please select at least one chapter.",
"downloadSuccess": "Your {format} file has been downloaded successfully.",
"downloadError": "Download failed.",
"serverError": "Server error during export.",
"unknownError": "An unknown error occurred."
},
"seriesImport": {
"importButton": "Import",
"importFromSeries": "Import from series",
"selectElement": "Select an element"
},
"loginPage": {
"title": "Login",
"welcome": "Welcome to ERitors Scribe",
"orSocial": "or sign in with",
"offlineWarning": {
"title": "Offline mode required",
"message": "You are not connected to the Internet. To use the app offline, you must first sign in at least once online."
}
},
"loginForm": {
"submit": "Sign in",
"fields": {
"email": {
"label": "Email address",
"placeholder": "Enter your email address"
},
"password": {
"label": "Password",
"placeholder": "Enter your password",
"forgot": "Forgot password?"
}
},
"error": {
"emailRequired": "Please enter your email address.",
"passwordRequired": "Please enter your password.",
"emailLength": "Email address must be between 5 and 100 characters.",
"emailInvalidChars": "Email address contains invalid characters.",
"connection": "Connection error. Check your credentials.",
"server": "Server error. Please try again later.",
"unknown": "An unknown error occurred."
}
},
"socialForm": {
"error": {
"connection": "Connection error with the provider."
}
},
"registerPage": {
"title": "Register",
"subtitle": "Create your ERitors account",
"backToLogin": "Back to login",
"progress": {
"infos": "Information",
"verif": "Verification"
}
},
"registerStepOne": {
"next": "Continue",
"fields": {
"firstName": {
"label": "First name",
"placeholder": "Enter your first name"
},
"lastName": {
"label": "Last name",
"placeholder": "Enter your last name"
},
"username": {
"label": "Username",
"placeholder": "Choose a username",
"note": "Username must be between 3 and 50 characters."
},
"email": {
"label": "Email address",
"placeholder": "Enter your email address"
},
"password": {
"label": "Password",
"placeholder": "Choose a password"
},
"repeatPassword": {
"label": "Confirm password",
"placeholder": "Confirm your password"
}
},
"error": {
"requiredFields": "All fields are required.",
"firstNameLength": "First name must be between 2 and 50 characters.",
"lastNameLength": "Last name must be between 2 and 50 characters.",
"usernameLength": "Username must be between 3 and 50 characters.",
"invalidInput": "One or more fields contain invalid characters.",
"passwordMismatch": "Passwords do not match.",
"preRegister": "Error during pre-registration.",
"unknown": "An unknown error occurred."
},
"success": {
"preRegister": "A verification code has been sent to your email address."
}
},
"registerStepTwo": {
"verify": "Verify",
"back": "Back",
"confirmed": "Your account has been verified successfully!",
"start": "Get started",
"instructions": {
"sent": "A verification code has been sent to your email address.",
"checkInbox": "Check your inbox and spam folder."
},
"fields": {
"code": {
"label": "Verification code",
"placeholder": "Enter the code received by email"
}
},
"error": {
"codeIncorrect": "The verification code is incorrect.",
"unknown": "An unknown error occurred."
},
"success": {
"verified": "Your account has been verified successfully!"
}
},
"resetPassword": {
"title": "Reset Password",
"subtitle": "Reset your password",
"verify": "Verify email",
"confirm": "Confirm code",
"changePassword": "Change password",
"back": "Back",
"backToLogin": "Back to login",
"success": "Your password has been reset successfully!",
"goToLogin": "Sign in",
"progress": {
"email": "Email",
"verification": "Verification",
"final": "New password"
},
"fields": {
"email": {
"label": "Email address",
"placeholder": "Enter your email address"
},
"code": {
"label": "Verification code",
"placeholder": "Enter the code received"
},
"newPassword": {
"label": "New password",
"placeholder": "Enter your new password"
}
},
"error": {
"emailInvalid": "Please enter a valid email address.",
"emailFormat": "The email address format is invalid.",
"emailServer": "Server error while verifying email.",
"emailUnknown": "An unknown error occurred.",
"codeServer": "Server error while verifying code.",
"codeUnknown": "An unknown error occurred.",
"passwordServer": "Server error while changing password.",
"passwordUnknown": "An unknown error occurred."
}
},
"offline": {
"mode": {
"title": "Offline Mode",
"backToOnline": "Back to online login"
"title": "Offline mode",
"backToOnline": "Back online"
},
"pin": {
"errors": {
"tooShort": "PIN must be at least 4 digits.",
"tooLong": "PIN cannot exceed 8 digits.",
"digitsOnly": "PIN must contain only digits.",
"mismatch": "PINs do not match.",
"setupFailed": "Error while setting up PIN."
},
"setup": {
"title": "Configure PIN",
"titleFirstLogin": "Secure your offline access",
"subtitle": "Protect your local data",
"description": "This PIN will allow you to access your works even without an internet connection",
"pinLabel": "PIN Code (4-8 digits)",
"confirmPinLabel": "Confirm PIN",
"title": "Set up offline PIN",
"titleFirstLogin": "Set up a PIN for offline mode",
"subtitle": "This PIN will allow you to access your data without an Internet connection.",
"description": "Choose a 4 to 8 digit PIN to secure offline access to your data.",
"pinLabel": "PIN code",
"confirmPinLabel": "Confirm PIN code",
"laterButton": "Later",
"configureButton": "Configure PIN",
"configuringButton": "Configuring...",
"footer": "Your PIN is stored securely on your device"
"configuringButton": "Setting up...",
"configureButton": "Set up PIN",
"footer": "You can change this PIN in settings."
},
"verify": {
"title": "Offline Mode",
"subtitle": "Enter your PIN to access your local works",
"placeholder": "Enter your PIN",
"enterPin": "Please enter your PIN",
"incorrect": "Incorrect PIN",
"tooManyAttempts": "Too many failed attempts. Please reconnect online.",
"error": "Error verifying PIN",
"title": "PIN Verification",
"subtitle": "Enter your PIN to access your offline data.",
"placeholder": "PIN code",
"enterPin": "Please enter your PIN.",
"incorrect": "Incorrect PIN.",
"tooManyAttempts": "Too many attempts. Please sign in online.",
"error": "Error while verifying PIN.",
"cancelButton": "Cancel",
"unlockButton": "Unlock",
"verifyingButton": "Verifying...",
"attemptsRemaining": "{{count}} attempt(s) remaining"
},
"errors": {
"tooShort": "PIN must be at least 4 digits",
"tooLong": "PIN cannot exceed 8 digits",
"digitsOnly": "PIN must contain only digits",
"mismatch": "PINs do not match",
"setupFailed": "Error configuring PIN"
"unlockButton": "Unlock"
}
}
},
"deleteBook": {
"title": "Delete book",
"message": "You are about to permanently delete your book.",
"confirm": "Delete",
"cancel": "Cancel",
"deleteLocalToo": "Also delete local version",
"deleteLocalWarning": "Warning: This action will delete the book from the server AND your device. This action is irreversible.",
"errorUnknown": "An unknown error occurred while deleting the book."
},
"quillSenseSetting": {
"title": "QuillSense Settings",
"description": "Manage AI features for this book.",
"enableSection": "Enable/Disable QuillSense",
"enableLabel": "QuillSense enabled",
"enableDescription": "When enabled, AI features like Ghost Writer and QuillSense will be available for this book.",
"advancedSection": "Advanced Settings",
"advancedPromptLabel": "Advanced Prompt",
"advancedPromptPlaceholder": "Enter custom instructions for the AI...",
"advancedPromptHint": "These instructions will be included in every text generation for this book.",
"disabledWarning": "QuillSense is disabled. AI features will not be available for this book.",
"errorFetch": "Error fetching QuillSense settings.",
"errorSave": "Error saving settings.",
"errorUnknown": "An unknown error occurred.",
"successSave": "QuillSense settings saved successfully.",
"noBookSelected": "No book selected.",
"enable_characters": "Enable character management for this book",
"enable_worlds": "Enable world management for this book",
"enable_locations": "Enable location management for this book"
},
"importBook": {
"header": {
"title": "Import a Book"
},
"pickFile": "Choose a DOCX file",
"parsing": "Analyzing file...",
"chaptersDetected": "{count} chapters detected",
"noChaptersDetected": "No chapters detected in the file",
"fields": {
"title": {
"label": "Book Title",
"placeholder": "Enter the title"
},
"subTitle": {
"label": "Subtitle",
"placeholder": "Enter the subtitle"
},
"summary": {
"label": "Summary",
"placeholder": "Enter a summary"
},
"type": {
"label": "Book Type"
},
"version": {
"label": "Chapter Version"
}
},
"chapters": {
"title": "Chapters to import",
"words": "{count} words",
"selectAll": "Select all",
"deselectAll": "Deselect all"
},
"submit": "Import",
"importing": "Importing...",
"success": "Book imported successfully",
"error": {
"titleRequired": "Book title is required",
"typeRequired": "Book type is required",
"noChaptersSelected": "Select at least one chapter",
"parseFailed": "Error analyzing the file",
"importFailed": "Error during import",
"invalidFormat": "Invalid format. Only DOCX files are accepted"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,187 +0,0 @@
import {Author} from './User';
import {ActChapter, ChapterProps} from "@/lib/models/Chapter";
import {SelectBoxProps} from "@/shared/interface";
import {
BookActSummariesTable,
BookAIGuideLineTable,
BookChapterContentTable, BookChapterInfosTable,
BookChaptersTable, BookCharactersAttributesTable, BookCharactersTable, BookGuideLineTable, BookIncidentsTable,
BookIssuesTable, BookLocationTable, BookPlotPointsTable, BookWorldElementsTable, BookWorldTable,
EritBooksTable, LocationElementTable, LocationSubElementTable
} from "@/lib/models/BookTables";
import {
SyncedActSummary, SyncedAIGuideLine,
SyncedChapter,
SyncedCharacter, SyncedGuideLine,
SyncedIncident, SyncedIssue,
SyncedLocation,
SyncedPlotPoint,
SyncedWorld
} from "@/lib/models/SyncedBook";
export interface CompleteBook {
eritBooks: EritBooksTable[];
actSummaries: BookActSummariesTable[];
aiGuideLine: BookAIGuideLineTable[];
chapters: BookChaptersTable[];
chapterContents: BookChapterContentTable[];
chapterInfos: BookChapterInfosTable[];
characters: BookCharactersTable[];
characterAttributes: BookCharactersAttributesTable[];
guideLine: BookGuideLineTable[];
incidents: BookIncidentsTable[];
issues: BookIssuesTable[];
locations: BookLocationTable[];
plotPoints: BookPlotPointsTable[];
worlds: BookWorldTable[];
worldElements: BookWorldElementsTable[];
locationElements: LocationElementTable[];
locationSubElements: LocationSubElementTable[];
}
export interface SyncedBook {
id: string;
type: string;
title: string;
subTitle: string | null;
lastUpdate: number;
chapters: SyncedChapter[];
characters: SyncedCharacter[];
locations: SyncedLocation[];
worlds: SyncedWorld[];
incidents: SyncedIncident[];
plotPoints: SyncedPlotPoint[];
issues: SyncedIssue[];
actSummaries: SyncedActSummary[];
guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null;
}
export interface BookToolsSettings {
characters: boolean;
worlds: boolean;
locations: boolean;
spells: boolean;
}
export interface BookProps {
bookId: string;
type: string;
title: string;
author?: Author;
serie?: number;
seriesId?: string | null;
subTitle?: string;
summary?: string;
publicationDate?: string;
desiredWordCount?: number;
totalWordCount?: number;
coverImage?: string;
localBook?: boolean;
chapters?: ChapterProps[];
quillsenseEnabled?: boolean;
tools?: BookToolsSettings;
}
export interface GuideLine {
tone: string;
atmosphere: string;
writingStyle: string;
themes: string;
symbolism: string;
motifs: string;
narrativeVoice: string;
pacing: string;
intendedAudience: string;
keyMessages: string;
}
export interface GuideLineAI {
narrativeType: number|null;
dialogueType: number|null;
globalResume: string|null;
atmosphere: string|null;
verbeTense: number|null;
langue: number|null;
currentResume: string|null;
themes: string|null;
}
export interface PlotPoint {
plotPointId: string;
title: string;
summary: string;
linkedIncidentId: string;
chapters?: ActChapter[];
}
export interface Incident {
incidentId: string;
title: string;
summary: string;
chapters?: ActChapter[];
}
export interface Issue {
name: string;
id: string;
}
export interface Act {
id: number;
summary: string | null;
incidents?: Incident[];
plotPoints?: PlotPoint[];
chapters?: ActChapter[];
}
export interface Tag {
label: string,
value: string,
}
export interface BookTags {
characters: Tag[];
locations: Tag[];
objects: Tag[];
worldElements: Tag[];
}
export const bookTypes: SelectBoxProps[] = [
{label: 'bookTypes.short', value: 'short'},
{label: 'bookTypes.novelette', value: 'novelette'},
{label: 'bookTypes.novella', value: 'long'},
{label: 'bookTypes.chapbook', value: 'chapbook'},
{label: 'bookTypes.novel', value: 'novel'},
]
export default class Book {
constructor() {
}
static booksToSelectBox(books: SyncedBook[]): SelectBoxProps[] {
return books.map((book: SyncedBook): SelectBoxProps => {
return {
label: book.title,
value: book.id,
}
});
}
static getBookTypeLabel(value: string): string {
switch (value) {
case 'short':
return 'bookTypes.short';
case 'novelette':
return 'bookTypes.novelette';
case 'long':
return 'bookTypes.novella';
case 'chapbook':
return 'bookTypes.chapbook';
case 'novel':
return 'bookTypes.novel';
default:
return 'bookTypes.novel';
}
}
}

View File

@@ -1,3 +0,0 @@
export default class BookSerie{
}

View File

@@ -1,211 +0,0 @@
import {SelectBoxProps} from "@/shared/interface";
export interface ActChapter {
chapterInfoId: string;
chapterId: string;
title: string;
chapterOrder: number;
actId: number;
incidentId?: string;
plotPointId?: string;
summary: string;
goal: string;
}
export interface ChapterListProps {
chapterId: string;
title: string;
summary?: string;
chapterOrder?: number;
goal?: string;
}
export interface ChapterProps {
chapterId: string;
chapterOrder: number;
title: string;
chapterContent: ChapterContent;
}
export interface ChapterContent {
version: number;
content: string;
wordsCount: number;
}
export interface ChapterVersion {
value: number;
label: 'Invite' | 'Brouillon' | 'Perfectionnement' | 'Révision' | 'Finale';
}
export type TiptapNode = {
type: string;
content?: TiptapNode[];
text?: string;
attrs?: {
[key: string]: any;
};
};
export type ExportFormat = 'epub' | 'pdf' | 'docx';
export interface ChapterExportInfo {
chapterId: string;
title: string;
chapterOrder: number;
availableVersions: number[];
}
export interface ChapterExportSelection {
chapterId: string;
version: number;
selected: boolean;
}
export const chapterVersions: SelectBoxProps[] = [
{value: '1', label: 'chapterVersions.prompt'},
{value: '2', label: 'chapterVersions.draft'},
{value: '3', label: 'chapterVersions.refine'},
{value: '4', label: 'chapterVersions.review'},
{value: '5', label: 'chapterVersions.final'},
];
export default class Chapter {
public static getPageCount(text: string): number {
const charactersPerLine = 90;
const linesPerPage = 40;
const lines: string[] = text.split('\n');
let totalLines: number = 0;
lines.forEach((line: string) => {
const lineLength: number = line.length;
const estimatedLines: number = Math.ceil(lineLength / charactersPerLine);
totalLines += estimatedLines;
});
// Calcul du nombre de pages
return Math.ceil(totalLines / linesPerPage);
}
static convertTiptapToHTML(node: TiptapNode): string {
let html = '';
switch (node.type) {
case 'doc':
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
break;
case 'paragraph':
html += '<p>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</p>';
break;
case 'text':
let textContent = node.text || '';
// Apply attributes like bold, italic, etc.
if (node.attrs) {
if (node.attrs.bold) {
textContent = `<strong>${textContent}</strong>`;
}
if (node.attrs.italic) {
textContent = `<em>${textContent}</em>`;
}
if (node.attrs.underline) {
textContent = `<u>${textContent}</u>`;
}
if (node.attrs.strike) {
textContent = `<s>${textContent}</s>`;
}
if (node.attrs.link) {
textContent = `<a href="${node.attrs.link.href}">${textContent}</a>`;
}
}
html += textContent;
break;
case 'heading':
const level = node.attrs?.level || 1;
html += `<h${level}>`;
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += `</h${level}>`;
break;
case 'bulletList':
html += '<ul>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</ul>';
break;
case 'orderedList':
html += '<ol>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</ol>';
break;
case 'listItem':
html += '<li>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</li>';
break;
case 'blockquote':
html += '<blockquote>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</blockquote>';
break;
case 'codeBlock':
html += '<pre><code>';
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
html += '</code></pre>';
break;
default:
console.warn(`Unhandled node type: ${node.type}`);
if (node.content) {
node.content.forEach(childNode => {
html += this.convertTiptapToHTML(childNode);
});
}
break;
}
return html;
}
}

View File

@@ -1,228 +0,0 @@
import {
faBrain,
faBullseye,
faExclamationTriangle,
faFire,
faRuler,
faShieldAlt,
faUsers,
faWrench,
faRoute,
faUserSecret,
faGhost,
faHeartBroken,
faHandHoldingHeart,
faBolt,
faQuoteLeft,
faFingerprint,
faBox,
faPeopleGroup,
} from '@fortawesome/free-solid-svg-icons';
import {SelectBoxProps} from "@/shared/interface";
type CharacterCategory = 'main' | 'secondary' | 'recurring' | 'none';
export const characterCategories: SelectBoxProps[] = [
{value: 'none', label: 'characterCategories.none'},
{value: 'main', label: 'characterCategories.main'},
{value: 'secondary', label: 'characterCategories.secondary'},
{value: 'recurring', label: 'characterCategories.recurring'},
];
export const characterStatus: SelectBoxProps[] = [
{value: 'alive', label: 'characterStatus.alive'},
{value: 'dead', label: 'characterStatus.dead'},
{value: 'unknown', label: 'characterStatus.unknown'},
];
export interface Relation {
name: string;
type: string;
description: string;
history: string;
}
export interface Attribute {
id: string;
name: string;
}
export interface CharacterAttribute {
[key: string]: Array<Attribute>;
}
export interface CharacterProps {
id: string | null;
name: string;
lastName: string;
nickname: string;
age: number | null;
gender: string;
species: string;
nationality: string;
status: 'alive' | 'dead' | 'unknown';
category: CharacterCategory;
title: string;
image: string;
physical: Attribute[];
psychological: Attribute[];
relations: Attribute[];
skills: Attribute[];
weaknesses: Attribute[];
strengths: Attribute[];
goals: Attribute[];
motivations: Attribute[];
arc: Attribute[];
secrets: Attribute[];
fears: Attribute[];
flaws: Attribute[];
beliefs: Attribute[];
conflicts: Attribute[];
quotes: Attribute[];
distinguishingMarks: Attribute[];
items: Attribute[];
affiliations: Attribute[];
role: string;
biography?: string;
history?: string;
speechPattern?: string;
catchphrase?: string;
residence?: string;
notes?: string;
color?: string;
seriesCharacterId?: string | null;
}
export interface CharacterListResponse {
characters: CharacterProps[];
enabled: boolean;
}
export interface CharacterElement {
title: string;
section: keyof CharacterProps;
placeholder: string;
icon: any; // Replace `any` with an appropriate type if you have a specific icon type.
}
// Attributs de base (toujours visibles)
export const basicCharacterElements: CharacterElement[] = [
{
title: 'Descriptions physiques',
section: 'physical',
placeholder: 'Nouvelle Description Physique',
icon: faRuler,
},
{
title: 'Descriptions psychologiques',
section: 'psychological',
placeholder: 'Nouvelle Description Psychologique',
icon: faBrain,
},
];
// Attributs avancés (visibles en mode avancé)
export const advancedCharacterElements: CharacterElement[] = [
{
title: 'Signes distinctifs',
section: 'distinguishingMarks',
placeholder: 'Nouveau signe distinctif',
icon: faFingerprint,
},
{
title: 'Arc du personnage',
section: 'arc',
placeholder: 'Nouvelle étape de l\'arc',
icon: faRoute,
},
{
title: 'Secrets',
section: 'secrets',
placeholder: 'Nouveau secret',
icon: faUserSecret,
},
{
title: 'Peurs',
section: 'fears',
placeholder: 'Nouvelle peur',
icon: faGhost,
},
{
title: 'Défauts',
section: 'flaws',
placeholder: 'Nouveau défaut',
icon: faHeartBroken,
},
{
title: 'Croyances',
section: 'beliefs',
placeholder: 'Nouvelle croyance',
icon: faHandHoldingHeart,
},
{
title: 'Conflits internes',
section: 'conflicts',
placeholder: 'Nouveau conflit',
icon: faBolt,
},
{
title: 'Citations',
section: 'quotes',
placeholder: 'Nouvelle citation',
icon: faQuoteLeft,
},
{
title: 'Relations',
section: 'relations',
placeholder: 'Nouveau Nom de Relation',
icon: faUsers,
},
{
title: 'Compétences',
section: 'skills',
placeholder: 'Nouvelle Compétence',
icon: faWrench,
},
{
title: 'Faiblesses',
section: 'weaknesses',
placeholder: 'Nouvelle Faiblesse',
icon: faExclamationTriangle,
},
{
title: 'Forces',
section: 'strengths',
placeholder: 'Nouvelle Force',
icon: faShieldAlt,
},
{
title: 'Objectifs',
section: 'goals',
placeholder: 'Nouvel Objectif',
icon: faBullseye,
},
{
title: 'Motivations',
section: 'motivations',
placeholder: 'Nouvelle Motivation',
icon: faFire,
},
{
title: 'Objets importants',
section: 'items',
placeholder: 'Nouvel objet',
icon: faBox,
},
{
title: 'Affiliations',
section: 'affiliations',
placeholder: 'Nouvelle affiliation',
icon: faPeopleGroup,
},
];
// Pour rétro-compatibilité, on garde characterElementCategory qui combine les deux
export const characterElementCategory: CharacterElement[] = [
...basicCharacterElements,
...advancedCharacterElements,
];

View File

@@ -1,20 +0,0 @@
import {IconDefinition} from "@fortawesome/free-solid-svg-icons";
export interface PanelComponent {
id: number,
title: string,
badge: string,
description: string,
icon: IconDefinition,
action?: () => void,
}
export default class Editor {
public static convertToHtml(text: string): string {
return text
.split(/\n\s*\n/)
.map((paragraph: string): string => `<p>${paragraph.trim()}</p>`)
.join('');
}
}

View File

@@ -1,134 +0,0 @@
import User, {Subscription} from "@/lib/models/User";
import {SessionProps} from "@/lib/models/Session";
export type MessageType = "user" | "model";
export type QSView = 'list' | 'chat' | 'ghostwritter' | 'dictionary' | 'synonyms' | 'conjugator' | 'inspiration'
export type ConversationType = 'dictionary' | 'synonyms' | 'conjugator' | 'chatbot' | 'inspire';
export interface Message {
id: number;
type: MessageType;
message: string;
date: string;
}
export interface Conversation {
id: string;
title?: string;
date?: string;
type?: ConversationType;
messages: Message[];
status: number;
totalPrice?: number
useYourKey?: boolean;
}
export interface AIGeneratedText {
totalTokens: number;
totalPrice: number;
response: string;
}
export interface AIResponseWithCredits<T> {
useYourKey: boolean;
totalPrice: number;
data: T;
}
export interface AIDictionary extends AIResponseWithCredits<DictionaryAIResponse> {
}
export interface AIGeneratedTextData {
totalCost: number;
response: string;
}
export interface AIGeneratedText extends AIResponseWithCredits<AIGeneratedTextData> {
}
export interface AIInspire extends AIResponseWithCredits<InspireAIResponse> {
}
export interface AISynonyms extends AIResponseWithCredits<SynonymsAIResponse> {
}
export interface AIVerbConjugation extends AIResponseWithCredits<unknown> {
}
interface InspireAIResponse {
ideas: {
idea: string,
reason: string;
relatedTo: string;
}[]
}
export interface DictionaryAIResponse {
word: string;
definition: string;
example: string;
literaryUsage: string
}
export interface SynonymAI {
word: string;
context: string;
}
export interface SynonymsAIResponse {
words: SynonymAI[];
}
export interface InspirationAIIdea {
idea: string;
reason: string;
relatedTo: string;
}
export interface ConversationProps {
id: string;
mode: string;
title: string;
startDate: string;
status: number;
}
export default class QuillSense {
static getSubLevel(session: SessionProps): number {
let currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'quill-sense');
if (!currentSub) {
currentSub = User.getCurrentSubscription(session?.user, 'quill-trial');
if (!currentSub) {
return 0;
}
}
switch (currentSub?.subTier) {
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
default:
return 0;
}
};
static isBringYourKeys(session: SessionProps): boolean {
if (!session?.user) return false;
const currentSub: Subscription | null = User.getCurrentSubscription(session?.user, 'use-your-keys');
return currentSub?.status || session.user.groupId <= 4;
}
static isGeminiEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.gemini || false;
}
static isAnthropicEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.anthropic || false;
}
static isOpenAIEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.openai || false;
}
}

View File

@@ -1,4 +0,0 @@
export interface QuillSenseSettingsProps {
quillsenseEnabled: boolean;
advancedPrompt: string | null;
}

View File

@@ -1,19 +0,0 @@
import {UserProps} from "@/lib/models/User";
export interface SessionProps {
isConnected: boolean,
accessToken: string;
user: UserProps | null;
}
export interface LoginResponse {
valid: boolean,
message?: string,
token?: string,
userid?: string
}
export default class Session {
constructor() {
}
}

View File

@@ -1,114 +0,0 @@
import {SelectBoxProps} from "@/shared/interface";
// ==================== SPELL TAG INTERFACES ====================
export interface SpellTagProps {
id: string;
name: string;
color: string | null;
}
// ==================== SPELL INTERFACES ====================
// Réponse de GET /spell/detail et POST /spell/add
export interface SpellProps {
id: string;
name: string;
description: string;
appearance: string;
tags: string[]; // IDs des tags
powerLevel: string | null;
components: string | null;
limitations: string | null;
notes: string | null;
seriesSpellId?: string | null;
}
// Pour POST /spell/add et PUT /spell/update
export interface SpellPropsPost {
id?: string;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel?: string | null;
components?: string | null;
limitations?: string | null;
notes?: string | null;
seriesSpellId?: string | null;
}
// Item dans la liste (GET /spell/list)
export interface SpellListItem {
id: string;
name: string;
description: string;
tags: SpellTagProps[]; // Tags résolus (pas les IDs)
seriesSpellId?: string | null;
}
// Réponse de GET /spell/list
export interface SpellListResponse {
enabled: boolean;
spells: SpellListItem[];
tags: SpellTagProps[];
}
// État local pour l'édition (avec id nullable pour création)
export interface SpellEditState {
id: string | null;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel: string | null;
components: string | null;
limitations: string | null;
notes: string | null;
seriesSpellId?: string | null;
}
export const initialSpellState: SpellEditState = {
id: null,
name: '',
description: '',
appearance: '',
tags: [],
powerLevel: null,
components: null,
limitations: null,
notes: null,
seriesSpellId: null,
};
export const spellPowerLevels: SelectBoxProps[] = [
{value: 'none', label: 'spellPowerLevels.none'},
{value: 'cantrip', label: 'spellPowerLevels.cantrip'},
{value: 'novice', label: 'spellPowerLevels.novice'},
{value: 'apprentice', label: 'spellPowerLevels.apprentice'},
{value: 'journeyman', label: 'spellPowerLevels.journeyman'},
{value: 'expert', label: 'spellPowerLevels.expert'},
{value: 'master', label: 'spellPowerLevels.master'},
{value: 'grandmaster', label: 'spellPowerLevels.grandmaster'},
{value: 'legendary', label: 'spellPowerLevels.legendary'},
{value: 'divine', label: 'spellPowerLevels.divine'},
];
export const defaultTagColors: string[] = [
'#51AE84',
'#3A8B69',
'#2196F3',
'#1976D2',
'#FFA726',
'#FF9800',
'#EF5350',
'#E53935',
'#AB47BC',
'#9C27B0',
'#26A69A',
'#00897B',
'#5C6BC0',
'#3F51B5',
'#EC407A',
'#D81B60',
];

File diff suppressed because it is too large Load Diff

View File

@@ -1,274 +0,0 @@
import axios, {AxiosResponse} from "axios";
import {configs} from "@/lib/configs";
export default class System{
static verifyInput(input: string): boolean {
let pattern: RegExp = new RegExp('(<.*?>)|(&.*?;)|({.*?})', 'gmi');
return pattern.test(input);
}
public static timeStampInSeconds(): number {
const date: number = new Date().getTime();
return Math.floor(date / 1000);
}
public static formatHTMLContent(htmlContent: string): string {
return htmlContent
.replace(/<h1>/g, '<h1 style="color: #FFFFFF; text-indent: 5px; font-size: 28px; font-weight: bold; text-align: left; margin-vertical: 10px;">')
.replace(/<p>/g, '<p style="color: #d0d0d0; text-indent: 30px; font-size: 16px; line-height: 22px; margin-vertical: 5px;">')
.replace(/<blockquote>/g, '<blockquote style="border-left-width: 4px; border-left-color: #ccc; padding-left: 10px; font-style: italic; color: #555;">');
}
public static textContentToHtml(content: string): string {
const paragraphs: string[] = content
.split(/\n+/)
.map((paragraph: string) => paragraph.trim())
.filter((paragraph: string) => paragraph.length > 0);
return paragraphs
.map((paragraph: string) => `<p>${paragraph}</p>`)
.join('');
}
public static async authGetQueryToServer<T>(url: string, auth: string, lang: string = "fr", params: Record<string, any> = {}): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'GET',
headers: {
'Authorization': `Bearer ${auth}`
},
params: {
lang: lang,
plateforme: 'desktop',
...params
},
url: configs.apiUrl + url,
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static setCookie(name: string, value: string, days: number): void {
const date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires = `expires=${date.toUTCString()}`;
let domain: string = '';
if (!/localhost|127\.0\.0\.1/.test(window.location.hostname)) {
domain = `domain=${window.location.hostname};`;
}
const secure = 'Secure;';
const sameSite = 'SameSite=Strict;';
document.cookie = `${name}=${value}; ${expires}; ${domain} path=/; ${secure} ${sameSite}`;
}
public static async authPutToServer<T>(url: string, data: {}, auth: string, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth}`
},
params: {
lang: lang,
plateforme: 'desktop',
},
url: configs.apiUrl + url,
data: data
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static async authPatchToServer<T>(url: string, data: {}, auth: string, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'patch',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth}`
},
params: {
lang: lang,
plateforme: 'desktop',
},
url: configs.apiUrl + url,
data: data
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static async postToServer<T>(url: string, data: {}, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
url: configs.apiUrl + url,
params: {
lang: lang,
plateforme: 'desktop',
},
data: data
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static async authPostToServer<T>(url: string, data: {}, auth: string, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth}`
},
url: configs.apiUrl + url,
params: {
lang: lang,
plateforme: 'desktop',
},
data: data
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
static htmlToText(html: string) {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n')
.replace(/<\/?[^>]+(>|$)/g, '')
.replace(/(\n\s*){2,}/g, '\n\n')
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '')
.trim();
}
public static getCookie(name: string): string | null {
const nameEQ = `${name}=`;
const allCookies: string[] = document.cookie.split(';');
for (let i: number = 0; i < allCookies.length; i++) {
let cookie: string = allCookies[i];
while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length);
if (cookie.indexOf(nameEQ) === 0) return cookie.substring(nameEQ.length, cookie.length);
}
return null;
}
public static removeCookie(name: string): void {
let domain: string = '';
if (!/localhost|127\.0\.0\.1/.test(window.location.hostname)) {
domain = `domain=${window.location.hostname};`;
}
const secure = 'Secure;';
const sameSite = 'SameSite=Strict;';
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${domain} path=/; ${secure} ${sameSite}`;
}
public static async authUploadFileToServer<T>(url: string, file: File, auth: string, lang: string = "fr"): Promise<T> {
try {
const formData: FormData = new FormData();
formData.append('file', file);
formData.append('lang', lang);
formData.append('plateforme', 'desktop');
const response: AxiosResponse<T> = await axios({
method: 'POST',
headers: {
'Authorization': `Bearer ${auth}`,
},
url: configs.apiUrl + url,
params: {
lang: lang,
plateforme: 'desktop',
},
data: formData,
});
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
public static async authDeleteToServer<T>(url: string, data: {}, auth: string, lang: string = "fr"): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${auth}`
},
url: configs.apiUrl + url,
params: {
lang: lang,
plateforme: 'desktop',
},
data: data
})
return response.data;
} catch (e: unknown) {
if (axios.isAxiosError(e)) {
const serverMessage: string = e.response?.data?.message || e.response?.data || e.message;
throw new Error(serverMessage as string);
} else if (e instanceof Error) {
throw new Error(e.message);
} else {
throw new Error('An unexpected error occurred');
}
}
}
}

View File

@@ -1,104 +0,0 @@
import {SelectBoxProps} from "@/shared/interface";
import {BookProps} from "@/lib/models/Book";
import {SessionProps} from "@/lib/models/Session";
export interface Author {
id: string;
name: string;
}
export interface UserProps {
id: string;
name: string;
lastName: string;
username: string;
authorName?: string;
email?: string;
accountVerified?: boolean;
termsAccepted?: boolean;
aiUsage: number,
apiKeys: {
gemini: boolean
openai: boolean,
anthropic: boolean,
},
guideTour?: GuideTour[];
subscription?: Subscription[];
writingLang: number;
writingLevel: number;
ritePoints: number;
creditsBalance:number;
groupId: number;
}
export interface GuideTour {
[key: string]: boolean;
}
export interface Subscription {
subType: string;
subTier: number;
status: boolean;
}
export const writingLevel: SelectBoxProps[] = [
{value: '0', label: 'Sélectionner un niveau d\'écriture'},
{value: '1', label: 'Je suis débutant'},
{value: '2', label: 'Je suis intermédiaire'},
{value: '3', label: 'Je suis avancé'},
];
export default class User {
static getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys" | "quill-trial"): Subscription | null {
if (!user || !user.subscription || user.subscription.length === 0) {
return null;
}
return user.subscription.find((sub: Subscription): boolean => {
return sub.subType === type && sub.status;
}) || null;
}
static getWritingLevel(level: number): string {
switch (level) {
case 1:
return 'Débutant';
case 2:
return 'Intermédiaire';
case 3:
return 'Avancé';
default:
return 'Débutant';
}
}
static guideTourDone(guide: GuideTour[], tour: string): boolean {
if (!tour) return false;
if (guide && guide.find((guide: GuideTour): boolean => guide[tour]) !== undefined) {
return false;
}
// Vérifier ensuite dans localStorage pour le mode offline
if (typeof window !== 'undefined' && window.localStorage) {
const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
if (completedGuides.includes(tour)) {
return false;
}
}
return true;
}
static setNewGuideTour(session: SessionProps, tour: string): SessionProps {
const newGuideTour: { [key: string]: boolean }[] = [
...(session?.user?.guideTour ?? []),
{[tour]: true}
];
return {
...session,
user: {
...session?.user as UserProps,
guideTour: newGuideTour
}
}
}
}

View File

@@ -1,123 +0,0 @@
import {
faCrown,
faExclamationTriangle,
faFlag,
faGavel,
faIndustry,
faLeaf,
faMountain,
faMusic,
faPeopleArrows,
faSnowflake,
faUserCog,
faUserFriends,
IconDefinition,
} from '@fortawesome/free-solid-svg-icons';
export interface ElementSection {
title: string;
section: keyof WorldProps;
icon: IconDefinition;
}
export interface WorldElement {
id: string;
name: string;
description: string;
}
export interface WorldProps {
id: string;
name: string;
history: string;
politics: string;
economy: string;
religion: string;
languages: string;
laws: WorldElement[];
biomes: WorldElement[];
issues: WorldElement[];
customs: WorldElement[];
kingdoms: WorldElement[];
climate: WorldElement[];
resources: WorldElement[];
wildlife: WorldElement[];
arts: WorldElement[];
ethnicGroups: WorldElement[];
socialClasses: WorldElement[];
importantCharacters: WorldElement[];
seriesWorldId?: string | null;
}
export interface WorldListResponse {
worlds: WorldProps[];
enabled: boolean;
}
export const elementSections: ElementSection[] = [
{
title: 'Lois',
section: 'laws',
icon: faGavel,
},
{
title: 'Biomes',
section: 'biomes',
icon: faMountain,
},
{
title: 'Problèmes',
section: 'issues',
icon: faExclamationTriangle,
},
{
title: 'Coutumes',
section: 'customs',
icon: faPeopleArrows,
},
{
title: 'Royaumes',
section: 'kingdoms',
icon: faFlag,
},
{
title: 'Climat',
section: 'climate',
icon: faSnowflake,
},
{
title: 'Ressources',
section: 'resources',
icon: faIndustry,
},
{
title: 'Faune',
section: 'wildlife',
icon: faLeaf,
},
{
title: 'Arts',
section: 'arts',
icon: faMusic,
},
{
title: 'Groupes ethniques',
section: 'ethnicGroups',
icon: faUserFriends,
},
{
title: 'Classes sociales',
section: 'socialClasses',
icon: faUserCog,
},
{
title: 'Personnages importants',
section: 'importantCharacters',
icon: faCrown,
},
];
export default class World {
constructor() {
}
}

20
lib/navigation.ts Normal file
View File

@@ -0,0 +1,20 @@
import {useNavigate, useParams as useRouterParams, Link, useLocation} from 'react-router-dom';
function useRouter() {
const navigate = useNavigate();
return {
push: (path: string) => navigate(path),
replace: (path: string) => navigate(path, {replace: true}),
back: () => navigate(-1),
};
}
function useParams<T extends Record<string, string>>(): T {
return useRouterParams() as T;
}
function usePathname(): string {
return useLocation().pathname;
}
export {Link, useRouter, useParams, usePathname};

735
lib/tauri.ts Normal file
View File

@@ -0,0 +1,735 @@
import {invoke as tauriInvoke} from '@tauri-apps/api/core';
async function invoke<T>(command: string, args?: Record<string, unknown>): Promise<T> {
try {
return await tauriInvoke<T>(command, args);
} catch (e: unknown) {
if (e instanceof Error) throw e;
if (typeof e === 'string') throw new Error(e);
if (typeof e === 'object' && e !== null && 'message' in e) throw new Error(String((e as {message: string}).message));
throw new Error(String(e));
}
}
export {invoke};
// ─── Types ─────────────────────────────────────────────────
import {BookProps, GuideLine, GuideLineAI} from '@/lib/types/book';
import {ChapterProps} from '@/lib/types/chapter';
import {UserProps} from '@/lib/types/user';
export interface InitUserResult {
success: boolean;
error: string | null;
}
export interface OfflineResult {
success: boolean;
error: string | null;
userId: string | null;
}
export interface OfflineModeStatus {
enabled: boolean;
syncInterval: number;
hasPin: boolean;
lastUserId: string | null;
}
export interface SyncCheckResult {
shouldSync: boolean;
daysSinceSync: number | null;
syncInterval: number | null;
}
export interface TombstoneRecord {
tableName: string;
entityId: string;
bookId: string | null;
deletedAt: number;
}
// ─── User & Auth ───────────────────────────────────────────
export async function initUser(userId: string): Promise<InitUserResult> {
return invoke<InitUserResult>('init_user', {data: {userId}});
}
export async function dbInitialize(userId: string, encryptionKey: string): Promise<boolean> {
return invoke<boolean>('db_initialize', {userId, encryptionKey});
}
export async function getToken(): Promise<string | null> {
return invoke<string | null>('get_token');
}
export async function setToken(token: string): Promise<void> {
return invoke<void>('set_token', {token});
}
export async function removeToken(): Promise<void> {
return invoke<void>('remove_token');
}
export async function getUserEncryptionKey(userId: string): Promise<string | null> {
return invoke<string | null>('get_user_encryption_key', {userId});
}
export async function getPlatform(): Promise<string> {
return invoke<string>('get_platform');
}
export async function getUserInfo(): Promise<UserProps> {
return invoke<UserProps>('get_user_info');
}
export async function syncUser(data: {
userId: string;
username: string;
email: string;
}): Promise<boolean> {
return invoke<boolean>('sync_user', {data});
}
// ─── Dev ──────────────────────────────────────────────────
export async function devResetAll(): Promise<boolean> {
return invoke<boolean>('dev_reset_all');
}
// ─── Offline ───────────────────────────────────────────────
export async function offlinePinSet(pin: string): Promise<OfflineResult> {
return invoke<OfflineResult>('offline_pin_set', {data: {pin}});
}
export async function offlinePinVerify(pin: string): Promise<OfflineResult> {
return invoke<OfflineResult>('offline_pin_verify', {data: {pin}});
}
export async function offlineModeGet(): Promise<OfflineModeStatus> {
return invoke<OfflineModeStatus>('offline_mode_get');
}
export async function offlineModeSet(enabled: boolean, syncIntervalDays: number): Promise<boolean> {
return invoke<boolean>('offline_mode_set', {data: {enabled, syncIntervalDays}});
}
export async function offlineSyncCheck(): Promise<SyncCheckResult> {
return invoke<SyncCheckResult>('offline_sync_check');
}
// ─── Book ──────────────────────────────────────────────────
export async function getBooks(): Promise<BookProps[]> {
return invoke<BookProps[]>('get_books');
}
export async function getBook(bookId: string): Promise<BookProps> {
return invoke<BookProps>('get_book', {bookId});
}
export async function createBook(data: {
title: string;
subTitle?: string;
summary?: string;
type: string;
serieId?: number;
desiredReleaseDate?: string;
desiredWordCount?: number;
}): Promise<string> {
return invoke<string>('create_book', {data});
}
export async function updateBookBasicInfo(data: {
bookId: string;
title: string;
subTitle: string;
summary: string;
publicationDate: string;
wordCount: number;
}): Promise<boolean> {
return invoke<boolean>('update_book_basic_info', {data});
}
export async function deleteBook(id: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_book', {data: {id, deletedAt}});
}
export async function updateBookToolSetting(bookId: string, toolName: string, enabled: boolean): Promise<boolean> {
return invoke<boolean>('update_book_tool_setting', {data: {bookId, toolName, enabled}});
}
export async function getBookStory(bookId: string): Promise<unknown> {
return invoke<unknown>('get_book_story', {data: {bookId}});
}
export async function updateBookStory(data: {
bookId: string;
acts: unknown[];
mainChapters: unknown[];
issues: unknown[];
}): Promise<boolean> {
return invoke<boolean>('update_book_story', {data});
}
export async function addIncident(bookId: string, name: string, incidentId?: string): Promise<string> {
return invoke<string>('add_incident', {data: {bookId, name, incidentId}});
}
export async function removeIncident(bookId: string, incidentId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_incident', {data: {bookId, incidentId, deletedAt}});
}
export async function addPlotPoint(bookId: string, name: string, incidentId: string, plotId?: string): Promise<string> {
return invoke<string>('add_plot_point', {data: {bookId, name, incidentId, plotId}});
}
export async function removePlotPoint(plotId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_plot_point', {data: {plotId, bookId, deletedAt}});
}
export async function addIssue(bookId: string, name: string, issueId?: string): Promise<string> {
return invoke<string>('add_issue', {data: {bookId, name, issueId}});
}
export async function removeIssue(bookId: string, issueId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_issue', {data: {bookId, issueId, deletedAt}});
}
export async function getBookBasicInformation(bookId: string): Promise<BookProps> {
return invoke<BookProps>('get_book_basic_information', {bookId});
}
export async function getBookExportInfo(bookId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_book_export_info', {data: {bookId}});
}
export async function exportBook(data: {
bookId: string;
format: string;
selections: unknown[] | null;
}): Promise<boolean> {
return invoke<boolean>('export_book', {data});
}
// ─── Book Guidelines ────────────────────────────────────────
export async function getGuideLine(bookId: string): Promise<GuideLine> {
return invoke<GuideLine>('get_guideline', {data: {id: bookId}});
}
export async function getAIGuideLine(bookId: string): Promise<GuideLineAI> {
return invoke<GuideLineAI>('get_ai_guideline', {data: {id: bookId}});
}
export async function updateGuideLine(data: {
bookId: string;
tone: string;
atmosphere: string;
writingStyle: string;
themes: string;
symbolism: string;
motifs: string;
narrativeVoice: string;
pacing: string;
intendedAudience: string;
keyMessages: string;
}): Promise<boolean> {
return invoke<boolean>('update_guideline', {data});
}
export async function updateAIGuideLine(data: {
bookId: string;
plotSummary: string;
verbTense: string;
narrativeType: string;
dialogueType: string;
toneAtmosphere: string;
language: string;
themes: string;
}): Promise<boolean> {
return invoke<boolean>('update_ai_guideline', {data});
}
// ─── Chapter ───────────────────────────────────────────────
export async function getChapters(bookId: string): Promise<ChapterProps[]> {
return invoke<ChapterProps[]>('get_chapters', {bookId});
}
export async function getWholeChapter(id: string, version: number, bookId: string): Promise<ChapterProps> {
return invoke<ChapterProps>('get_whole_chapter', {data: {id, version, bookId}});
}
export async function getChapterStory(chapterId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_chapter_story', {chapterId});
}
export async function getCompanionContent(chapterId: string, version: number): Promise<unknown> {
return invoke<unknown>('get_companion_content', {data: {chapterId, version}});
}
export async function getChapterContent(chapterId: string, version: number): Promise<string> {
return invoke<string>('get_chapter_content', {data: {chapterId, version}});
}
export async function saveChapterContent(data: {
chapterId: string;
version: number;
content: unknown;
totalWordCount: number;
contentId: string;
}): Promise<boolean> {
return invoke<boolean>('save_chapter_content', {data});
}
export async function getLastChapter(bookId: string): Promise<ChapterProps | null> {
return invoke<ChapterProps | null>('get_last_chapter', {bookId});
}
export async function addChapter(data: {
bookId: string;
title: string;
chapterOrder: number;
chapterId?: string;
}): Promise<string> {
return invoke<string>('add_chapter', {data});
}
export async function removeChapter(chapterId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_chapter', {data: {chapterId, bookId, deletedAt}});
}
export async function updateChapter(chapterId: string, title: string, chapterOrder: number): Promise<boolean> {
return invoke<boolean>('update_chapter', {data: {chapterId, title, chapterOrder}});
}
export async function addChapterInformation(data: {
chapterId: string;
actId: number;
bookId: string;
plotId?: string;
incidentId?: string;
chapterInfoId?: string;
}): Promise<string> {
return invoke<string>('add_chapter_information', {data});
}
export async function removeChapterInformation(chapterInfoId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_chapter_information', {data: {chapterInfoId, bookId, deletedAt}});
}
export async function getBookTags(bookId: string): Promise<unknown> {
return invoke<unknown>('get_book_tags', {bookId});
}
// ─── Character ─────────────────────────────────────────────
export async function getCharacterList(bookId: string, enabled: boolean): Promise<unknown> {
return invoke<unknown>('get_character_list', {data: {bookId, enabled}});
}
export async function getCharacterAttributes(characterId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_character_attributes', {data: {characterId}});
}
export async function createCharacter(character: unknown, bookId: string, id?: string): Promise<string> {
return invoke<string>('create_character', {data: {character, bookId, id}});
}
export async function addCharacterAttribute(characterId: string, type: string, name: string, id?: string): Promise<string> {
return invoke<string>('add_character_attribute', {data: {characterId, type, name, id}});
}
export async function deleteCharacterAttribute(attributeId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_character_attribute', {data: {attributeId, bookId, deletedAt}});
}
export async function updateCharacter(character: unknown): Promise<boolean> {
return invoke<boolean>('update_character', {data: {character}});
}
export async function deleteCharacter(characterId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_character', {data: {characterId, bookId, deletedAt}});
}
// ─── Location ──────────────────────────────────────────────
export async function getAllLocations(bookId: string, enabled: boolean): Promise<unknown> {
return invoke<unknown>('get_all_locations', {data: {bookId, enabled}});
}
export async function addLocationSection(locationName: string, bookId: string, id?: string, seriesLocationId?: string): Promise<string> {
return invoke<string>('add_location_section', {data: {locationName, bookId, id, seriesLocationId}});
}
export async function addLocationElement(locationId: string, elementName: string, id?: string): Promise<string> {
return invoke<string>('add_location_element', {data: {locationId, elementName, id}});
}
export async function addLocationSubElement(elementId: string, subElementName: string, id?: string): Promise<string> {
return invoke<string>('add_location_sub_element', {data: {elementId, subElementName, id}});
}
export async function updateLocations(locations: unknown[]): Promise<unknown> {
return invoke<unknown>('update_locations', {data: {locations}});
}
export async function updateLocationSectionWithSeriesLink(sectionId: string, sectionName?: string, seriesLocationId?: string): Promise<boolean> {
return invoke<boolean>('update_location_section_with_series_link', {data: {sectionId, sectionName, seriesLocationId}});
}
export async function deleteLocationSection(locationId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_location_section', {data: {locationId, bookId, deletedAt}});
}
export async function deleteLocationElement(elementId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_location_element', {data: {elementId, bookId, deletedAt}});
}
export async function deleteLocationSubElement(subElementId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_location_sub_element', {data: {subElementId, bookId, deletedAt}});
}
// ─── Spell ─────────────────────────────────────────────────
export async function getSpellList(bookId: string, enabled: boolean): Promise<unknown> {
return invoke<unknown>('get_spell_list', {data: {bookId, enabled}});
}
export async function getSpellTags(bookId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_spell_tags', {data: {bookId}});
}
export async function getSpellDetail(spellId: string): Promise<unknown> {
return invoke<unknown>('get_spell_detail', {data: {spellId}});
}
export async function createSpell(bookId: string, spell: unknown): Promise<unknown> {
return invoke<unknown>('create_spell', {data: {bookId, spell}});
}
export async function updateSpell(spellId: string, spell: unknown): Promise<boolean> {
return invoke<boolean>('update_spell', {data: {spellId, spell}});
}
export async function deleteSpell(spellId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_spell', {data: {spellId, bookId, deletedAt}});
}
export async function createSpellTag(bookId: string, name: string, color?: string, id?: string): Promise<unknown> {
return invoke<unknown>('create_spell_tag', {data: {bookId, name, color, id}});
}
export async function updateSpellTag(tagId: string, name: string, color?: string): Promise<boolean> {
return invoke<boolean>('update_spell_tag', {data: {tagId, name, color}});
}
export async function deleteSpellTag(tagId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_spell_tag', {data: {tagId, bookId, deletedAt}});
}
// ─── World ─────────────────────────────────────────────────
export async function getWorlds(bookId: string, enabled: boolean): Promise<unknown> {
return invoke<unknown>('get_worlds', {data: {bookId, enabled}});
}
export async function addWorld(bookId: string, worldName: string, id?: string, seriesWorldId?: string): Promise<string> {
return invoke<string>('add_world', {data: {bookId, worldName, id, seriesWorldId}});
}
export async function addWorldElement(worldId: string, elementName: string, elementType: string, id?: string): Promise<string> {
return invoke<string>('add_world_element', {data: {worldId, elementName, elementType, id}});
}
export async function removeWorldElement(elementId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_world_element', {data: {elementId, bookId, deletedAt}});
}
export async function updateWorld(world: unknown): Promise<boolean> {
return invoke<boolean>('update_world', {data: {world}});
}
// ─── Series ────────────────────────────────────────────────
export async function getSeriesList(): Promise<unknown[]> {
return invoke<unknown[]>('get_series_list');
}
export async function getSeriesDetail(seriesId: string): Promise<unknown> {
return invoke<unknown>('get_series_detail', {data: {seriesId}});
}
export async function createSeries(data: unknown): Promise<string> {
return invoke<string>('create_series', {data});
}
export async function updateSeries(data: unknown): Promise<boolean> {
return invoke<boolean>('update_series', {data});
}
export async function deleteSeries(seriesId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series', {data: {seriesId, deletedAt}});
}
export async function getSeriesBooks(seriesId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_series_books', {data: {seriesId}});
}
export async function addBookToSeries(seriesId: string, bookId: string): Promise<boolean> {
return invoke<boolean>('add_book_to_series', {data: {seriesId, bookId}});
}
export async function removeBookFromSeries(seriesId: string, bookId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('remove_book_from_series', {data: {seriesId, bookId, deletedAt}});
}
export async function reorderSeriesBooks(seriesId: string, bookIds: string[]): Promise<boolean> {
return invoke<boolean>('reorder_series_books', {data: {seriesId, bookIds}});
}
export async function getSeriesForBook(bookId: string): Promise<string | null> {
return invoke<string | null>('get_series_for_book', {data: {bookId}});
}
// ─── Series Characters ─────────────────────────────────────
export async function getSeriesCharacterList(seriesId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_series_character_list', {data: {seriesId}});
}
export async function getSeriesCharacterAttributes(characterId: string): Promise<unknown> {
return invoke<unknown>('get_series_character_attributes', {data: {characterId}});
}
export async function addSeriesCharacter(data: unknown): Promise<string> {
return invoke<string>('add_series_character', {data});
}
export async function updateSeriesCharacter(data: unknown): Promise<boolean> {
return invoke<boolean>('update_series_character', {data});
}
export async function deleteSeriesCharacter(characterId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_character', {data: {characterId, deletedAt}});
}
export async function addSeriesCharacterAttribute(data: unknown): Promise<string> {
return invoke<string>('add_series_character_attribute', {data});
}
export async function deleteSeriesCharacterAttribute(attributeId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_character_attribute', {data: {attributeId, deletedAt}});
}
// ─── Series Locations ──────────────────────────────────────
export async function getSeriesLocationList(seriesId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_series_location_list', {data: {seriesId}});
}
export async function addSeriesLocationSection(data: unknown): Promise<string> {
return invoke<string>('add_series_location_section', {data});
}
export async function addSeriesLocationElement(data: unknown): Promise<string> {
return invoke<string>('add_series_location_element', {data});
}
export async function addSeriesLocationSubElement(data: unknown): Promise<string> {
return invoke<string>('add_series_location_sub_element', {data});
}
export async function deleteSeriesLocation(locationId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_location', {data: {locationId, deletedAt}});
}
export async function deleteSeriesLocationElement(elementId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_location_element', {data: {elementId, deletedAt}});
}
export async function deleteSeriesLocationSubElement(subElementId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_location_sub_element', {data: {subElementId, deletedAt}});
}
// ─── Series Worlds ─────────────────────────────────────────
export async function getSeriesWorldList(seriesId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_series_world_list', {data: {seriesId}});
}
export async function addSeriesWorld(data: unknown): Promise<string> {
return invoke<string>('add_series_world', {data});
}
export async function updateSeriesWorld(data: unknown): Promise<boolean> {
return invoke<boolean>('update_series_world', {data});
}
export async function addSeriesWorldElement(data: unknown): Promise<string> {
return invoke<string>('add_series_world_element', {data});
}
export async function deleteSeriesWorldElement(elementId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_world_element', {data: {elementId, deletedAt}});
}
// ─── Series Spells ─────────────────────────────────────────
export async function getSeriesSpellList(seriesId: string): Promise<unknown[]> {
return invoke<unknown[]>('get_series_spell_list', {data: {seriesId}});
}
export async function getSeriesSpellDetail(spellId: string): Promise<unknown> {
return invoke<unknown>('get_series_spell_detail', {data: {spellId}});
}
export async function addSeriesSpell(data: unknown): Promise<string> {
return invoke<string>('add_series_spell', {data});
}
export async function updateSeriesSpell(data: unknown): Promise<boolean> {
return invoke<boolean>('update_series_spell', {data});
}
export async function deleteSeriesSpell(spellId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_spell', {data: {spellId, deletedAt}});
}
export async function addSeriesSpellTag(data: unknown): Promise<string> {
return invoke<string>('add_series_spell_tag', {data});
}
export async function updateSeriesSpellTag(data: unknown): Promise<boolean> {
return invoke<boolean>('update_series_spell_tag', {data});
}
export async function deleteSeriesSpellTag(tagId: string, deletedAt: number): Promise<boolean> {
return invoke<boolean>('delete_series_spell_tag', {data: {tagId, deletedAt}});
}
// ─── Sync (books/series) ───────────────────────────────────
export async function getSyncedBooks(): Promise<unknown[]> {
return invoke<unknown[]>('get_synced_books');
}
export async function getSyncedSeries(): Promise<unknown[]> {
return invoke<unknown[]>('get_synced_series');
}
export async function uploadBookToServer(bookId: string): Promise<unknown> {
return invoke<unknown>('upload_book_to_server', {bookId});
}
export async function syncSaveBook(book: unknown): Promise<boolean> {
return invoke<boolean>('sync_save_book', {data: book});
}
export async function syncBookToClient(book: unknown): Promise<boolean> {
return invoke<boolean>('sync_book_to_client', {data: book});
}
export async function syncBookToServer(bookToSync: unknown): Promise<unknown> {
return invoke<unknown>('sync_book_to_server', {data: bookToSync});
}
export async function uploadSeriesToServer(seriesId: string): Promise<unknown> {
return invoke<unknown>('upload_series_to_server', {seriesId});
}
export async function syncSaveSeries(series: unknown): Promise<boolean> {
return invoke<boolean>('sync_save_series', {data: series});
}
export async function syncSeriesToClient(series: unknown): Promise<boolean> {
return invoke<boolean>('sync_series_to_client', {data: series});
}
export async function syncSeriesToServer(seriesToSync: unknown): Promise<unknown> {
return invoke<unknown>('sync_series_to_server', {data: seriesToSync});
}
export async function seriesSyncUpload(data: {
type: string;
bookElementId: string;
field: string;
value: string;
}): Promise<unknown> {
return invoke<unknown>('series_sync_upload', {data});
}
// ─── Tombstones ────────────────────────────────────────────
export async function getTombstonesSince(since: number): Promise<unknown[]> {
return invoke<unknown[]>('get_tombstones_since', {since});
}
export async function applyBookTombstones(tombstones: TombstoneRecord[]): Promise<void> {
return invoke<void>('apply_book_tombstones', {tombstones});
}
export async function applySeriesTombstones(tombstones: TombstoneRecord[]): Promise<void> {
return invoke<void>('apply_series_tombstones', {tombstones});
}
// ─── Window Management ──────────────────────────────────────
export async function openLoginWindow(): Promise<void> {
const {WebviewWindow} = await import('@tauri-apps/api/webviewWindow');
const {getCurrentWindow} = await import('@tauri-apps/api/window');
const existing = await WebviewWindow.getByLabel('login');
if (existing) {
await existing.setFocus();
await getCurrentWindow().hide();
return;
}
const loginWindow = new WebviewWindow('login', {
url: '/login/login/',
title: 'ERitors - Connexion',
width: 500,
height: 900,
resizable: false,
center: true,
decorations: true,
});
loginWindow.once('tauri://created', async function () {
await getCurrentWindow().hide();
});
}
export async function loginSuccess(): Promise<void> {
const {WebviewWindow} = await import('@tauri-apps/api/webviewWindow');
const {getCurrentWindow} = await import('@tauri-apps/api/window');
const currentLabel = getCurrentWindow().label;
if (currentLabel === 'login') {
const {emit} = await import('@tauri-apps/api/event');
await emit('auth-success');
const mainWindow = await WebviewWindow.getByLabel('main');
if (mainWindow) {
await mainWindow.show();
await mainWindow.setFocus();
}
await getCurrentWindow().close();
} else {
window.location.reload();
}
}
export async function logout(): Promise<void> {
await openLoginWindow();
}
export async function openExternal(url: string): Promise<void> {
const {open} = await import('@tauri-apps/plugin-shell');
return open(url);
}

View File

@@ -1,18 +1,18 @@
export interface EritBooksTable {
book_id:string;
type:string;
author_id:string;
title:string;
hashed_title:string;
sub_title:string|null;
hashed_sub_title:string|null;
summary:string|null;
serie_id:number|null;
desired_release_date:string|null;
desired_word_count:number|null;
words_count:number|null;
cover_image:string|null;
last_update:number;
book_id: string;
type: string;
author_id: string;
title: string;
hashed_title: string;
sub_title: string | null;
hashed_sub_title: string | null;
summary: string | null;
serie_id: number | null;
desired_release_date: string | null;
desired_word_count: number | null;
words_count: number | null;
cover_image: string | null;
last_update: number;
}
export interface BookActSummariesTable {
@@ -140,6 +140,26 @@ export interface BookLocationTable {
last_update: number;
}
export interface LocationElementTable {
element_id: string;
location: string;
user_id: string;
element_name: string;
original_name: string;
element_description: string | null;
last_update: number;
}
export interface LocationSubElementTable {
sub_element_id: string;
element_id: string;
user_id: string;
sub_elem_name: string;
original_name: string;
sub_elem_description: string | null;
last_update: number;
}
export interface BookPlotPointsTable {
plot_point_id: string;
title: string;
@@ -176,29 +196,29 @@ export interface BookWorldElementsTable {
last_update: number;
}
export interface LocationElementTable {
element_id: string;
location: string;
user_id: string;
element_name: string;
original_name: string;
element_description: string | null;
last_update: number;
}
export interface LocationSubElementTable {
sub_element_id: string;
element_id: string;
user_id: string;
sub_elem_name: string;
original_name: string;
sub_elem_description: string | null;
last_update: number;
}
export interface GhostWriterSettingsTable {
book_id: string;
user_id: string;
advanced_prompt: string | null;
quillsense_enabled: number;
}
}
export interface CompleteBook {
eritBooks: EritBooksTable[];
actSummaries: BookActSummariesTable[];
aiGuideLine: BookAIGuideLineTable[];
chapters: BookChaptersTable[];
chapterContents: BookChapterContentTable[];
chapterInfos: BookChapterInfosTable[];
characters: BookCharactersTable[];
characterAttributes: BookCharactersAttributesTable[];
guideLine: BookGuideLineTable[];
incidents: BookIncidentsTable[];
issues: BookIssuesTable[];
locations: BookLocationTable[];
plotPoints: BookPlotPointsTable[];
worlds: BookWorldTable[];
worldElements: BookWorldElementsTable[];
locationElements: LocationElementTable[];
locationSubElements: LocationSubElementTable[];
}

108
lib/types/book.ts Normal file
View File

@@ -0,0 +1,108 @@
import {Author} from "@/lib/types/user";
import {ActChapter, ChapterProps} from "@/lib/types/chapter";
export interface BookToolsSettings {
characters: boolean;
worlds: boolean;
locations: boolean;
spells: boolean;
}
export interface BookProps {
bookId: string;
type: string;
title: string;
author?: Author;
serie?: number;
seriesId?: string | null;
subTitle?: string;
summary?: string;
publicationDate?: string;
desiredWordCount?: number;
totalWordCount?: number;
coverImage?: string;
chapters?: ChapterProps[];
quillsenseEnabled?: boolean;
tools?: BookToolsSettings;
localBook?: boolean;
}
export interface BookListProps {
id: string;
type: string;
authorId: string;
title: string;
subTitle?: string;
summary?: string;
serieId?: number;
desiredReleaseDate?: string;
desiredWordCount?: number;
wordCount?: number;
coverImage?: string;
bookMeta?: string;
quillsenseEnabled?: boolean;
}
export interface GuideLine {
tone: string;
atmosphere: string;
writingStyle: string;
themes: string;
symbolism: string;
motifs: string;
narrativeVoice: string;
pacing: string;
intendedAudience: string;
keyMessages: string;
}
export interface GuideLineAI {
narrativeType: number | null;
dialogueType: number | null;
globalResume: string | null;
atmosphere: string | null;
verbeTense: number | null;
langue: number | null;
currentResume: string | null;
themes: string | null;
}
export interface PlotPoint {
plotPointId: string;
title: string;
summary: string;
linkedIncidentId: string;
chapters?: ActChapter[];
}
export interface Incident {
incidentId: string;
title: string;
summary: string;
chapters?: ActChapter[];
}
export interface Issue {
name: string;
id: string;
}
export interface Act {
id: number;
summary: string | null;
incidents?: Incident[];
plotPoints?: PlotPoint[];
chapters?: ActChapter[];
}
export interface Tag {
label: string;
value: string;
}
export interface BookTags {
characters: Tag[];
locations: Tag[];
objects: Tag[];
worldElements: Tag[];
}

80
lib/types/chapter.ts Normal file
View File

@@ -0,0 +1,80 @@
export interface ActChapter {
chapterInfoId: string;
chapterId: string;
title: string;
chapterOrder: number;
actId: number;
incidentId?: string;
plotPointId?: string;
summary: string;
goal: string;
}
export interface ChapterListProps {
chapterId: string;
title: string;
summary?: string;
chapterOrder?: number;
goal?: string;
}
export interface ChapterProps {
chapterId: string;
chapterOrder: number;
title: string;
chapterContent: ChapterContent;
}
export interface ChapterContent {
version: number;
content: string;
wordsCount: number;
}
export interface ChapterVersion {
value: number;
label: 'Invite' | 'Brouillon' | 'Perfectionnement' | 'Révision' | 'Finale';
}
export interface TiptapLinkAttrs {
href: string;
target?: string;
}
export type TiptapAttrValue = string | number | boolean | null | TiptapLinkAttrs;
export type TiptapNode = {
type: string;
content?: TiptapNode[];
text?: string;
attrs?: {
[key: string]: TiptapAttrValue;
};
};
export interface CompanionContent {
version: number;
content: string;
wordsCount: number;
}
export type ExportFormat = 'epub' | 'pdf' | 'docx';
export interface ChapterExportInfo {
chapterId: string;
title: string;
chapterOrder: number;
availableVersions: number[];
}
export interface ChapterExportSelection {
chapterId: string;
version: number;
selected: boolean;
}
export interface ExportRequestBody {
bookId: string;
format: ExportFormat;
chapters: { chapterId: string; version: number }[];
}

105
lib/types/character.ts Normal file
View File

@@ -0,0 +1,105 @@
import {LucideIcon} from 'lucide-react';
export type CharacterCategory = 'main' | 'secondary' | 'recurring' | 'none';
export type CharacterStatus = 'alive' | 'dead' | 'unknown';
export type CharacterAttributeSection =
'physical'
| 'psychological'
| 'relations'
| 'skills'
| 'weaknesses'
| 'strengths'
| 'goals'
| 'motivations'
| 'arc'
| 'secrets'
| 'fears'
| 'flaws'
| 'beliefs'
| 'conflicts'
| 'quotes'
| 'distinguishingMarks'
| 'items'
| 'affiliations';
export function isCharacterCategory(value: string): value is CharacterCategory {
return value === 'main' || value === 'secondary' || value === 'recurring' || value === 'none';
}
export function isCharacterStatus(value: string): value is CharacterStatus {
return value === 'alive' || value === 'dead' || value === 'unknown';
}
export interface Relation {
name: string;
type: string;
description: string;
history: string;
}
export interface Attribute {
id: string;
name: string;
}
export interface CharacterAttribute {
[key: string]: Array<Attribute>;
}
export interface CharacterProps {
id: string | null;
name: string;
lastName: string;
nickname: string;
age: number | null;
gender: string;
species: string;
nationality: string;
status: CharacterStatus;
category: CharacterCategory;
title: string;
image: string;
physical: Attribute[];
psychological: Attribute[];
relations: Attribute[];
skills: Attribute[];
weaknesses: Attribute[];
strengths: Attribute[];
goals: Attribute[];
motivations: Attribute[];
arc: Attribute[];
secrets: Attribute[];
fears: Attribute[];
flaws: Attribute[];
beliefs: Attribute[];
conflicts: Attribute[];
quotes: Attribute[];
distinguishingMarks: Attribute[];
items: Attribute[];
affiliations: Attribute[];
role: string;
biography?: string;
history?: string;
speechPattern?: string;
catchphrase?: string;
residence?: string;
notes?: string;
color?: string;
seriesCharacterId?: string | null;
}
export interface CharacterListResponse {
characters: CharacterProps[];
enabled: boolean;
}
export interface CharacterElement {
title: string;
section: CharacterAttributeSection;
placeholder: string;
icon: LucideIcon;
}
export interface AttributeResponse {
attributes: Attribute[];
}

10
lib/types/editor.ts Normal file
View File

@@ -0,0 +1,10 @@
import {LucideIcon} from 'lucide-react';
export interface PanelComponent {
id: number;
title: string;
badge: string;
description: string;
icon: LucideIcon;
action?: () => void;
}

View File

@@ -15,3 +15,13 @@ export interface ImportChapterSelection {
wordCount: number;
selected: boolean;
}
export interface ImportConfirmBody {
importId: string;
title: string;
subTitle: string;
summary: string;
type: string;
version: number;
selectedChapterIndexes: number[];
}

109
lib/types/quillsense.ts Normal file
View File

@@ -0,0 +1,109 @@
export type MessageType = "user" | "model";
export type QSView = 'list' | 'chat' | 'ghostwritter' | 'dictionary' | 'synonyms' | 'conjugator' | 'inspiration';
export type ConversationType = 'dictionary' | 'synonyms' | 'conjugator' | 'chatbot' | 'inspire';
export interface Message {
id: number;
type: MessageType;
message: string;
date: string;
}
export interface Conversation {
id: string;
title?: string;
date?: string;
type?: ConversationType;
messages: Message[];
status: number;
totalPrice?: number;
useYourKey?: boolean;
}
export interface AIGeneratedTextData {
totalCost: number;
response: string;
}
export interface AIResponseWithCredits<T> {
useYourKey: boolean;
totalPrice: number;
data: T;
}
export interface AIDictionary extends AIResponseWithCredits<DictionaryAIResponse> {
}
export interface AIGeneratedText extends AIResponseWithCredits<AIGeneratedTextData> {
}
export interface AIInspire extends AIResponseWithCredits<InspireAIResponse> {
}
export interface AISynonyms extends AIResponseWithCredits<SynonymsAIResponse> {
}
export interface ConjugationTenses {
[tense: string]: {
firstPersonSingular?: string;
secondPersonSingular?: string;
thirdPersonSingular?: string;
firstPersonPlural?: string;
secondPersonPlural?: string;
thirdPersonPlural?: string;
présent?: string;
passé?: string;
} | string;
}
export interface ConjugationResponse {
conjugations: {
[mode: string]: ConjugationTenses;
};
}
export interface AIVerbConjugation extends AIResponseWithCredits<ConjugationResponse> {
}
export interface InspireAIResponse {
ideas: {
idea: string;
reason: string;
relatedTo: string;
}[];
}
export interface DictionaryAIResponse {
word: string;
definition: string;
example: string;
literaryUsage: string;
}
export interface SynonymAI {
word: string;
context: string;
}
export interface SynonymsAIResponse {
words: SynonymAI[];
}
export interface InspirationAIIdea {
idea: string;
reason: string;
relatedTo: string;
}
export interface ConversationProps {
id: string;
mode: string;
title: string;
startDate: string;
status: number;
}
export interface QuillSenseSettingsProps {
quillsenseEnabled: boolean;
advancedPrompt: string | null;
}

View File

@@ -37,7 +37,6 @@ export interface SeriesListItemProps {
bookIds: string[];
}
// Personnages de série
export interface SeriesCharacterListItem {
id: string;
name: string;
@@ -80,7 +79,6 @@ export interface SeriesCharacterAttribute {
export type SeriesCharacterProps = SeriesCharacterDetailResponse;
// Mondes de série
export interface SeriesWorldElementItem {
id: string;
name: string;
@@ -118,7 +116,6 @@ export interface SeriesWorldElement {
description: string;
}
// Lieux de série
export interface SeriesLocationSubElement {
id: string;
name: string;
@@ -138,7 +135,6 @@ export interface SeriesLocationItem {
elements: SeriesLocationElement[];
}
// Sorts de série (Grimoire)
export interface SeriesSpellTag {
id: string;
name: string;

14
lib/types/session.ts Normal file
View File

@@ -0,0 +1,14 @@
import {UserProps} from "@/lib/types/user";
export interface SessionProps {
isConnected: boolean;
accessToken: string;
user: UserProps | null;
}
export interface LoginResponse {
valid: boolean;
message?: string;
token?: string;
userid?: string;
}

5
lib/types/settings.ts Normal file
View File

@@ -0,0 +1,5 @@
export type ViewMode = 'list' | 'detail' | 'edit';
export interface SettingRef {
handleSave: () => Promise<void>;
}

58
lib/types/spell.ts Normal file
View File

@@ -0,0 +1,58 @@
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;
seriesSpellId?: string | null;
}
export interface SpellPropsPost {
id?: string;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel?: string | null;
components?: string | null;
limitations?: string | null;
notes?: string | null;
seriesSpellId?: string | null;
}
export interface SpellListItem {
id: string;
name: string;
description: string;
tags: SpellTagProps[];
seriesSpellId?: string | null;
}
export interface SpellListResponse {
enabled: boolean;
spells: SpellListItem[];
tags: SpellTagProps[];
}
export interface SpellEditState {
id: string | null;
name: string;
description: string;
appearance: string;
tags: string[];
powerLevel: string | null;
components: string | null;
limitations: string | null;
notes: string | null;
seriesSpellId?: string | null;
}

29
lib/types/story.ts Normal file
View File

@@ -0,0 +1,29 @@
import {ChapterListProps} from "@/lib/types/chapter";
import {Act, Issue} from "@/lib/types/book";
export interface StoryProps {
mainChapter: ChapterListProps[];
acts: Act[];
issues: Issue[];
}
export interface VerbalTimeProps {
actions: string;
descriptions: string;
dialogues: string;
thoughts: string;
summary: string;
}
export interface DialogueProps {
description: string;
example: string;
}
export interface GeneratedShortStory {
title: string;
short: string;
resume: string;
totalPrice: number;
totalTokens: number;
}

View File

@@ -1,392 +1,338 @@
export interface SyncedBookTools {
lastUpdate: number;
}
export interface SyncedBook {
id: string;
type: string;
title: string;
subTitle: string | null;
seriesId: string | null;
lastUpdate: number;
chapters: SyncedChapter[];
characters: SyncedCharacter[];
locations: SyncedLocation[];
worlds: SyncedWorld[];
incidents: SyncedIncident[];
plotPoints: SyncedPlotPoint[];
issues: SyncedIssue[];
actSummaries: SyncedActSummary[];
guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null;
bookTools: SyncedBookTools | null;
spells: SyncedSpell[];
spellTags: SyncedSpellTag[];
}
export interface SyncedChapter {
id: string;
name: string;
lastUpdate: number;
contents: SyncedChapterContent[];
info: SyncedChapterInfo | null;
}
export interface SyncedChapterContent {
id: string;
lastUpdate: number;
}
export interface SyncedChapterInfo {
id: string;
lastUpdate: number;
}
export interface SyncedCharacter {
id: string;
name: string;
lastUpdate: number;
attributes: SyncedCharacterAttribute[];
}
export interface SyncedCharacterAttribute {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedLocation {
id: string;
name: string;
lastUpdate: number;
elements: SyncedLocationElement[];
}
export interface SyncedLocationElement {
id: string;
name: string;
lastUpdate: number;
subElements: SyncedLocationSubElement[];
}
export interface SyncedLocationSubElement {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedWorld {
id: string;
name: string;
lastUpdate: number;
elements: SyncedWorldElement[];
}
export interface SyncedWorldElement {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedIncident {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedPlotPoint {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedIssue {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedActSummary {
id: string;
lastUpdate: number;
}
export interface SyncedGuideLine {
lastUpdate: number;
}
export interface SyncedAIGuideLine {
lastUpdate: number;
}
export interface SyncedSpell {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedSpellTag {
id: string;
name: string;
lastUpdate: number;
}
export interface BookSyncCompare {
id: string;
chapters: string[];
chapterContents: string[];
chapterInfos: string[];
characters: string[];
characterAttributes: string[];
locations: string[];
locationElements: string[];
locationSubElements: string[];
worlds: string[];
worldElements: string[];
incidents: string[];
plotPoints: string[];
issues: string[];
actSummaries: string[];
guideLine: boolean;
aiGuideLine: boolean;
bookTools: boolean;
spells: string[];
spellTags: string[];
}
export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null {
const changedChapterIds: string[] = [];
const changedChapterContentIds: string[] = [];
const changedChapterInfoIds: string[] = [];
const changedCharacterIds: string[] = [];
const changedCharacterAttributeIds: string[] = [];
const changedLocationIds: string[] = [];
const changedLocationElementIds: string[] = [];
const changedLocationSubElementIds: string[] = [];
const changedWorldIds: string[] = [];
const changedWorldElementIds: string[] = [];
const changedIncidentIds: string[] = [];
const changedPlotPointIds: string[] = [];
const changedIssueIds: string[] = [];
const changedActSummaryIds: string[] = [];
const changedSpellIds: string[] = [];
const changedSpellTagIds: string[] = [];
let guideLineChanged: boolean = false;
let aiGuideLineChanged: boolean = false;
let bookToolsChanged: boolean = false;
newerBook.chapters.forEach((newerChapter: SyncedChapter): void => {
const olderChapter: SyncedChapter | undefined = olderBook.chapters.find((chapter: SyncedChapter): boolean => chapter.id === newerChapter.id);
if (!olderChapter) {
changedChapterIds.push(newerChapter.id);
newerChapter.contents.forEach((content: SyncedChapterContent): void => {
changedChapterContentIds.push(content.id);
});
if (newerChapter.info) {
changedChapterInfoIds.push(newerChapter.info.id);
}
} else if (newerChapter.lastUpdate > olderChapter.lastUpdate) {
changedChapterIds.push(newerChapter.id);
} else {
newerChapter.contents.forEach((newerContent: SyncedChapterContent): void => {
const olderContent: SyncedChapterContent | undefined = olderChapter.contents.find((content: SyncedChapterContent): boolean => content.id === newerContent.id);
if (!olderContent || newerContent.lastUpdate > olderContent.lastUpdate) {
changedChapterContentIds.push(newerContent.id);
}
});
if (newerChapter.info && olderChapter.info) {
if (newerChapter.info.lastUpdate > olderChapter.info.lastUpdate) {
changedChapterInfoIds.push(newerChapter.info.id);
}
} else if (newerChapter.info && !olderChapter.info) {
changedChapterInfoIds.push(newerChapter.info.id);
}
}
});
newerBook.characters.forEach((newerCharacter: SyncedCharacter): void => {
const olderCharacter: SyncedCharacter | undefined = olderBook.characters.find((character: SyncedCharacter): boolean => character.id === newerCharacter.id);
if (!olderCharacter) {
changedCharacterIds.push(newerCharacter.id);
newerCharacter.attributes.forEach((attribute: SyncedCharacterAttribute): void => {
changedCharacterAttributeIds.push(attribute.id);
});
} else if (newerCharacter.lastUpdate > olderCharacter.lastUpdate) {
changedCharacterIds.push(newerCharacter.id);
} else {
newerCharacter.attributes.forEach((newerAttribute: SyncedCharacterAttribute): void => {
const olderAttribute: SyncedCharacterAttribute | undefined = olderCharacter.attributes.find((attribute: SyncedCharacterAttribute): boolean => attribute.id === newerAttribute.id);
if (!olderAttribute || newerAttribute.lastUpdate > olderAttribute.lastUpdate) {
changedCharacterAttributeIds.push(newerAttribute.id);
}
});
}
});
newerBook.locations.forEach((newerLocation: SyncedLocation): void => {
const olderLocation: SyncedLocation | undefined = olderBook.locations.find((location: SyncedLocation): boolean => location.id === newerLocation.id);
if (!olderLocation) {
changedLocationIds.push(newerLocation.id);
newerLocation.elements.forEach((element: SyncedLocationElement): void => {
changedLocationElementIds.push(element.id);
element.subElements.forEach((subElement: SyncedLocationSubElement): void => {
changedLocationSubElementIds.push(subElement.id);
});
});
} else if (newerLocation.lastUpdate > olderLocation.lastUpdate) {
changedLocationIds.push(newerLocation.id);
} else {
newerLocation.elements.forEach((newerElement: SyncedLocationElement): void => {
const olderElement: SyncedLocationElement | undefined = olderLocation.elements.find((element: SyncedLocationElement): boolean => element.id === newerElement.id);
if (!olderElement) {
changedLocationElementIds.push(newerElement.id);
newerElement.subElements.forEach((subElement: SyncedLocationSubElement): void => {
changedLocationSubElementIds.push(subElement.id);
});
} else if (newerElement.lastUpdate > olderElement.lastUpdate) {
changedLocationElementIds.push(newerElement.id);
} else {
newerElement.subElements.forEach((newerSubElement: SyncedLocationSubElement): void => {
const olderSubElement: SyncedLocationSubElement | undefined = olderElement.subElements.find((subElement: SyncedLocationSubElement): boolean => subElement.id === newerSubElement.id);
if (!olderSubElement || newerSubElement.lastUpdate > olderSubElement.lastUpdate) {
changedLocationSubElementIds.push(newerSubElement.id);
}
});
}
});
}
});
newerBook.worlds.forEach((newerWorld: SyncedWorld): void => {
const olderWorld: SyncedWorld | undefined = olderBook.worlds.find((world: SyncedWorld): boolean => world.id === newerWorld.id);
if (!olderWorld) {
changedWorldIds.push(newerWorld.id);
newerWorld.elements.forEach((element: SyncedWorldElement): void => {
changedWorldElementIds.push(element.id);
});
} else if (newerWorld.lastUpdate > olderWorld.lastUpdate) {
changedWorldIds.push(newerWorld.id);
} else {
newerWorld.elements.forEach((newerElement: SyncedWorldElement): void => {
const olderElement: SyncedWorldElement | undefined = olderWorld.elements.find((element: SyncedWorldElement): boolean => element.id === newerElement.id);
if (!olderElement || newerElement.lastUpdate > olderElement.lastUpdate) {
changedWorldElementIds.push(newerElement.id);
}
});
}
});
newerBook.incidents.forEach((newerIncident: SyncedIncident): void => {
const olderIncident: SyncedIncident | undefined = olderBook.incidents.find((incident: SyncedIncident): boolean => incident.id === newerIncident.id);
if (!olderIncident || newerIncident.lastUpdate > olderIncident.lastUpdate) {
changedIncidentIds.push(newerIncident.id);
}
});
newerBook.plotPoints.forEach((newerPlotPoint: SyncedPlotPoint): void => {
const olderPlotPoint: SyncedPlotPoint | undefined = olderBook.plotPoints.find((plotPoint: SyncedPlotPoint): boolean => plotPoint.id === newerPlotPoint.id);
if (!olderPlotPoint || newerPlotPoint.lastUpdate > olderPlotPoint.lastUpdate) {
changedPlotPointIds.push(newerPlotPoint.id);
}
});
newerBook.issues.forEach((newerIssue: SyncedIssue): void => {
const olderIssue: SyncedIssue | undefined = olderBook.issues.find((issue: SyncedIssue): boolean => issue.id === newerIssue.id);
if (!olderIssue || newerIssue.lastUpdate > olderIssue.lastUpdate) {
changedIssueIds.push(newerIssue.id);
}
});
newerBook.actSummaries.forEach((newerActSummary: SyncedActSummary): void => {
const olderActSummary: SyncedActSummary | undefined = olderBook.actSummaries.find((actSummary: SyncedActSummary): boolean => actSummary.id === newerActSummary.id);
if (!olderActSummary || newerActSummary.lastUpdate > olderActSummary.lastUpdate) {
changedActSummaryIds.push(newerActSummary.id);
}
});
if (newerBook.guideLine && olderBook.guideLine) {
guideLineChanged = newerBook.guideLine.lastUpdate > olderBook.guideLine.lastUpdate;
} else if (newerBook.guideLine && !olderBook.guideLine) {
guideLineChanged = true;
}
if (newerBook.aiGuideLine && olderBook.aiGuideLine) {
aiGuideLineChanged = newerBook.aiGuideLine.lastUpdate > olderBook.aiGuideLine.lastUpdate;
} else if (newerBook.aiGuideLine && !olderBook.aiGuideLine) {
aiGuideLineChanged = true;
}
if (newerBook.bookTools && olderBook.bookTools) {
bookToolsChanged = newerBook.bookTools.lastUpdate > olderBook.bookTools.lastUpdate;
} else if (newerBook.bookTools && !olderBook.bookTools) {
bookToolsChanged = true;
}
newerBook.spellTags.forEach((newerSpellTag: SyncedSpellTag): void => {
const olderSpellTag: SyncedSpellTag | undefined = olderBook.spellTags.find((spellTag: SyncedSpellTag): boolean => spellTag.id === newerSpellTag.id);
if (!olderSpellTag || newerSpellTag.lastUpdate > olderSpellTag.lastUpdate) {
changedSpellTagIds.push(newerSpellTag.id);
}
});
newerBook.spells.forEach((newerSpell: SyncedSpell): void => {
const olderSpell: SyncedSpell | undefined = olderBook.spells.find((spell: SyncedSpell): boolean => spell.id === newerSpell.id);
if (!olderSpell || newerSpell.lastUpdate > olderSpell.lastUpdate) {
changedSpellIds.push(newerSpell.id);
}
});
const hasChanges: boolean =
changedChapterIds.length > 0 ||
changedChapterContentIds.length > 0 ||
changedChapterInfoIds.length > 0 ||
changedCharacterIds.length > 0 ||
changedCharacterAttributeIds.length > 0 ||
changedLocationIds.length > 0 ||
changedLocationElementIds.length > 0 ||
changedLocationSubElementIds.length > 0 ||
changedWorldIds.length > 0 ||
changedWorldElementIds.length > 0 ||
changedIncidentIds.length > 0 ||
changedPlotPointIds.length > 0 ||
changedIssueIds.length > 0 ||
changedActSummaryIds.length > 0 ||
changedSpellIds.length > 0 ||
changedSpellTagIds.length > 0 ||
guideLineChanged ||
aiGuideLineChanged ||
bookToolsChanged;
if (!hasChanges) {
return null;
}
return {
id: newerBook.id,
chapters: changedChapterIds,
chapterContents: changedChapterContentIds,
chapterInfos: changedChapterInfoIds,
characters: changedCharacterIds,
characterAttributes: changedCharacterAttributeIds,
locations: changedLocationIds,
locationElements: changedLocationElementIds,
locationSubElements: changedLocationSubElementIds,
worlds: changedWorldIds,
worldElements: changedWorldElementIds,
incidents: changedIncidentIds,
plotPoints: changedPlotPointIds,
issues: changedIssueIds,
actSummaries: changedActSummaryIds,
guideLine: guideLineChanged,
aiGuideLine: aiGuideLineChanged,
bookTools: bookToolsChanged,
spells: changedSpellIds,
spellTags: changedSpellTagIds
};
}
export interface SyncedBook {
id: string;
type: string;
title: string;
subTitle: string | null;
seriesId: string | null;
lastUpdate: number;
chapters: SyncedChapter[];
characters: SyncedCharacter[];
locations: SyncedLocation[];
worlds: SyncedWorld[];
incidents: SyncedIncident[];
plotPoints: SyncedPlotPoint[];
issues: SyncedIssue[];
actSummaries: SyncedActSummary[];
guideLine: SyncedGuideLine | null;
aiGuideLine: SyncedAIGuideLine | null;
bookTools: SyncedBookTools | null;
spells: SyncedSpell[];
spellTags: SyncedSpellTag[];
}
export interface SyncedChapter {
id: string;
name: string;
lastUpdate: number;
contents: SyncedChapterContent[];
info: SyncedChapterInfo | null;
}
export interface SyncedChapterContent {
id: string;
lastUpdate: number;
}
export interface SyncedChapterInfo {
id: string;
lastUpdate: number;
}
export interface SyncedCharacter {
id: string;
name: string;
lastUpdate: number;
attributes: SyncedCharacterAttribute[];
}
export interface SyncedCharacterAttribute {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedLocation {
id: string;
name: string;
lastUpdate: number;
elements: SyncedLocationElement[];
}
export interface SyncedLocationElement {
id: string;
name: string;
lastUpdate: number;
subElements: SyncedLocationSubElement[];
}
export interface SyncedLocationSubElement {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedWorld {
id: string;
name: string;
lastUpdate: number;
elements: SyncedWorldElement[];
}
export interface SyncedWorldElement {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedIncident {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedPlotPoint {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedIssue {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedActSummary {
id: string;
lastUpdate: number;
}
export interface SyncedGuideLine {
lastUpdate: number;
}
export interface SyncedAIGuideLine {
lastUpdate: number;
}
export interface SyncedBookTools {
lastUpdate: number;
}
export interface SyncedSpell {
id: string;
name: string;
lastUpdate: number;
}
export interface SyncedSpellTag {
id: string;
name: string;
lastUpdate: number;
}
export interface BookSyncCompare {
id: string;
chapters: string[];
chapterContents: string[];
chapterInfos: string[];
characters: string[];
characterAttributes: string[];
locations: string[];
locationElements: string[];
locationSubElements: string[];
worlds: string[];
worldElements: string[];
incidents: string[];
plotPoints: string[];
issues: string[];
actSummaries: string[];
guideLine: boolean;
aiGuideLine: boolean;
bookTools: boolean;
spells: string[];
spellTags: string[];
}
export function compareBookSyncs(newerBook: SyncedBook, olderBook: SyncedBook): BookSyncCompare | null {
const changedChapterIds: string[] = [];
const changedChapterContentIds: string[] = [];
const changedChapterInfoIds: string[] = [];
const changedCharacterIds: string[] = [];
const changedCharacterAttributeIds: string[] = [];
const changedLocationIds: string[] = [];
const changedLocationElementIds: string[] = [];
const changedLocationSubElementIds: string[] = [];
const changedWorldIds: string[] = [];
const changedWorldElementIds: string[] = [];
const changedIncidentIds: string[] = [];
const changedPlotPointIds: string[] = [];
const changedIssueIds: string[] = [];
const changedActSummaryIds: string[] = [];
const changedSpellIds: string[] = [];
const changedSpellTagIds: string[] = [];
let guideLineChanged: boolean = false;
let aiGuideLineChanged: boolean = false;
let bookToolsChanged: boolean = false;
newerBook.chapters.forEach((newerChapter: SyncedChapter): void => {
const olderChapter: SyncedChapter | undefined = olderBook.chapters.find((chapter: SyncedChapter): boolean => chapter.id === newerChapter.id);
if (!olderChapter) {
changedChapterIds.push(newerChapter.id);
newerChapter.contents.forEach((content: SyncedChapterContent): void => { changedChapterContentIds.push(content.id); });
if (newerChapter.info) { changedChapterInfoIds.push(newerChapter.info.id); }
} else if (newerChapter.lastUpdate > olderChapter.lastUpdate) {
changedChapterIds.push(newerChapter.id);
} else {
newerChapter.contents.forEach((newerContent: SyncedChapterContent): void => {
const olderContent: SyncedChapterContent | undefined = olderChapter.contents.find((content: SyncedChapterContent): boolean => content.id === newerContent.id);
if (!olderContent || newerContent.lastUpdate > olderContent.lastUpdate) { changedChapterContentIds.push(newerContent.id); }
});
if (newerChapter.info && olderChapter.info) {
if (newerChapter.info.lastUpdate > olderChapter.info.lastUpdate) { changedChapterInfoIds.push(newerChapter.info.id); }
} else if (newerChapter.info && !olderChapter.info) {
changedChapterInfoIds.push(newerChapter.info.id);
}
}
});
newerBook.characters.forEach((newerCharacter: SyncedCharacter): void => {
const olderCharacter: SyncedCharacter | undefined = olderBook.characters.find((character: SyncedCharacter): boolean => character.id === newerCharacter.id);
if (!olderCharacter) {
changedCharacterIds.push(newerCharacter.id);
newerCharacter.attributes.forEach((attribute: SyncedCharacterAttribute): void => { changedCharacterAttributeIds.push(attribute.id); });
} else if (newerCharacter.lastUpdate > olderCharacter.lastUpdate) {
changedCharacterIds.push(newerCharacter.id);
} else {
newerCharacter.attributes.forEach((newerAttribute: SyncedCharacterAttribute): void => {
const olderAttribute: SyncedCharacterAttribute | undefined = olderCharacter.attributes.find((attribute: SyncedCharacterAttribute): boolean => attribute.id === newerAttribute.id);
if (!olderAttribute || newerAttribute.lastUpdate > olderAttribute.lastUpdate) { changedCharacterAttributeIds.push(newerAttribute.id); }
});
}
});
newerBook.locations.forEach((newerLocation: SyncedLocation): void => {
const olderLocation: SyncedLocation | undefined = olderBook.locations.find((location: SyncedLocation): boolean => location.id === newerLocation.id);
if (!olderLocation) {
changedLocationIds.push(newerLocation.id);
newerLocation.elements.forEach((element: SyncedLocationElement): void => {
changedLocationElementIds.push(element.id);
element.subElements.forEach((subElement: SyncedLocationSubElement): void => { changedLocationSubElementIds.push(subElement.id); });
});
} else if (newerLocation.lastUpdate > olderLocation.lastUpdate) {
changedLocationIds.push(newerLocation.id);
} else {
newerLocation.elements.forEach((newerElement: SyncedLocationElement): void => {
const olderElement: SyncedLocationElement | undefined = olderLocation.elements.find((element: SyncedLocationElement): boolean => element.id === newerElement.id);
if (!olderElement) {
changedLocationElementIds.push(newerElement.id);
newerElement.subElements.forEach((subElement: SyncedLocationSubElement): void => { changedLocationSubElementIds.push(subElement.id); });
} else if (newerElement.lastUpdate > olderElement.lastUpdate) {
changedLocationElementIds.push(newerElement.id);
} else {
newerElement.subElements.forEach((newerSubElement: SyncedLocationSubElement): void => {
const olderSubElement: SyncedLocationSubElement | undefined = olderElement.subElements.find((subElement: SyncedLocationSubElement): boolean => subElement.id === newerSubElement.id);
if (!olderSubElement || newerSubElement.lastUpdate > olderSubElement.lastUpdate) { changedLocationSubElementIds.push(newerSubElement.id); }
});
}
});
}
});
newerBook.worlds.forEach((newerWorld: SyncedWorld): void => {
const olderWorld: SyncedWorld | undefined = olderBook.worlds.find((world: SyncedWorld): boolean => world.id === newerWorld.id);
if (!olderWorld) {
changedWorldIds.push(newerWorld.id);
newerWorld.elements.forEach((element: SyncedWorldElement): void => { changedWorldElementIds.push(element.id); });
} else if (newerWorld.lastUpdate > olderWorld.lastUpdate) {
changedWorldIds.push(newerWorld.id);
} else {
newerWorld.elements.forEach((newerElement: SyncedWorldElement): void => {
const olderElement: SyncedWorldElement | undefined = olderWorld.elements.find((element: SyncedWorldElement): boolean => element.id === newerElement.id);
if (!olderElement || newerElement.lastUpdate > olderElement.lastUpdate) { changedWorldElementIds.push(newerElement.id); }
});
}
});
newerBook.incidents.forEach((newerIncident: SyncedIncident): void => {
const olderIncident: SyncedIncident | undefined = olderBook.incidents.find((incident: SyncedIncident): boolean => incident.id === newerIncident.id);
if (!olderIncident || newerIncident.lastUpdate > olderIncident.lastUpdate) { changedIncidentIds.push(newerIncident.id); }
});
newerBook.plotPoints.forEach((newerPlotPoint: SyncedPlotPoint): void => {
const olderPlotPoint: SyncedPlotPoint | undefined = olderBook.plotPoints.find((plotPoint: SyncedPlotPoint): boolean => plotPoint.id === newerPlotPoint.id);
if (!olderPlotPoint || newerPlotPoint.lastUpdate > olderPlotPoint.lastUpdate) { changedPlotPointIds.push(newerPlotPoint.id); }
});
newerBook.issues.forEach((newerIssue: SyncedIssue): void => {
const olderIssue: SyncedIssue | undefined = olderBook.issues.find((issue: SyncedIssue): boolean => issue.id === newerIssue.id);
if (!olderIssue || newerIssue.lastUpdate > olderIssue.lastUpdate) { changedIssueIds.push(newerIssue.id); }
});
newerBook.actSummaries.forEach((newerActSummary: SyncedActSummary): void => {
const olderActSummary: SyncedActSummary | undefined = olderBook.actSummaries.find((actSummary: SyncedActSummary): boolean => actSummary.id === newerActSummary.id);
if (!olderActSummary || newerActSummary.lastUpdate > olderActSummary.lastUpdate) { changedActSummaryIds.push(newerActSummary.id); }
});
if (newerBook.guideLine && olderBook.guideLine) {
guideLineChanged = newerBook.guideLine.lastUpdate > olderBook.guideLine.lastUpdate;
} else if (newerBook.guideLine && !olderBook.guideLine) {
guideLineChanged = true;
}
if (newerBook.aiGuideLine && olderBook.aiGuideLine) {
aiGuideLineChanged = newerBook.aiGuideLine.lastUpdate > olderBook.aiGuideLine.lastUpdate;
} else if (newerBook.aiGuideLine && !olderBook.aiGuideLine) {
aiGuideLineChanged = true;
}
if (newerBook.bookTools && olderBook.bookTools) {
bookToolsChanged = newerBook.bookTools.lastUpdate > olderBook.bookTools.lastUpdate;
} else if (newerBook.bookTools && !olderBook.bookTools) {
bookToolsChanged = true;
}
newerBook.spellTags.forEach((newerSpellTag: SyncedSpellTag): void => {
const olderSpellTag: SyncedSpellTag | undefined = olderBook.spellTags.find((spellTag: SyncedSpellTag): boolean => spellTag.id === newerSpellTag.id);
if (!olderSpellTag || newerSpellTag.lastUpdate > olderSpellTag.lastUpdate) { changedSpellTagIds.push(newerSpellTag.id); }
});
newerBook.spells.forEach((newerSpell: SyncedSpell): void => {
const olderSpell: SyncedSpell | undefined = olderBook.spells.find((spell: SyncedSpell): boolean => spell.id === newerSpell.id);
if (!olderSpell || newerSpell.lastUpdate > olderSpell.lastUpdate) { changedSpellIds.push(newerSpell.id); }
});
const hasChanges: boolean =
changedChapterIds.length > 0 || changedChapterContentIds.length > 0 || changedChapterInfoIds.length > 0 ||
changedCharacterIds.length > 0 || changedCharacterAttributeIds.length > 0 ||
changedLocationIds.length > 0 || changedLocationElementIds.length > 0 || changedLocationSubElementIds.length > 0 ||
changedWorldIds.length > 0 || changedWorldElementIds.length > 0 ||
changedIncidentIds.length > 0 || changedPlotPointIds.length > 0 || changedIssueIds.length > 0 || changedActSummaryIds.length > 0 ||
changedSpellIds.length > 0 || changedSpellTagIds.length > 0 ||
guideLineChanged || aiGuideLineChanged || bookToolsChanged;
if (!hasChanges) { return null; }
return {
id: newerBook.id,
chapters: changedChapterIds,
chapterContents: changedChapterContentIds,
chapterInfos: changedChapterInfoIds,
characters: changedCharacterIds,
characterAttributes: changedCharacterAttributeIds,
locations: changedLocationIds,
locationElements: changedLocationElementIds,
locationSubElements: changedLocationSubElementIds,
worlds: changedWorldIds,
worldElements: changedWorldElementIds,
incidents: changedIncidentIds,
plotPoints: changedPlotPointIds,
issues: changedIssueIds,
actSummaries: changedActSummaryIds,
guideLine: guideLineChanged,
aiGuideLine: aiGuideLineChanged,
bookTools: bookToolsChanged,
spells: changedSpellIds,
spellTags: changedSpellTagIds
};
}

36
lib/types/user.ts Normal file
View File

@@ -0,0 +1,36 @@
export interface Author {
id: string;
name: string;
}
export interface UserProps {
id: string;
username: string;
authorName?: string;
email?: string;
accountVerified?: boolean;
termsAccepted?: boolean;
aiUsage: number;
apiKeys: {
gemini: boolean;
openai: boolean;
anthropic: boolean;
};
guideTour?: GuideTour[];
subscription?: Subscription[];
writingLang: number | null;
writingLevel: number | null;
ritePoints: number;
creditsBalance: number;
groupId: number;
}
export interface GuideTour {
[key: string]: boolean;
}
export interface Subscription {
subType: string;
subTier: number;
status: boolean;
}

57
lib/types/world.ts Normal file
View File

@@ -0,0 +1,57 @@
import {LucideIcon} from 'lucide-react';
export interface ElementSection {
title: string;
section: WorldElementSection;
icon: LucideIcon;
}
export interface WorldElement {
id: string;
name: string;
description: string;
}
export type WorldElementSection =
'laws'
| 'biomes'
| 'issues'
| 'customs'
| 'kingdoms'
| 'climate'
| 'resources'
| 'wildlife'
| 'arts'
| 'ethnicGroups'
| 'socialClasses'
| 'importantCharacters';
export type WorldTextField = 'name' | 'history' | 'politics' | 'economy' | 'religion' | 'languages';
export interface WorldProps {
id: string;
name: string;
history: string;
politics: string;
economy: string;
religion: string;
languages: string;
laws: WorldElement[];
biomes: WorldElement[];
issues: WorldElement[];
customs: WorldElement[];
kingdoms: WorldElement[];
climate: WorldElement[];
resources: WorldElement[];
wildlife: WorldElement[];
arts: WorldElement[];
ethnicGroups: WorldElement[];
socialClasses: WorldElement[];
importantCharacters: WorldElement[];
seriesWorldId?: string | null;
}
export interface WorldListResponse {
worlds: WorldProps[];
enabled: boolean;
}

28
lib/utils/book.ts Normal file
View File

@@ -0,0 +1,28 @@
import {SelectBoxProps} from "@/components/form/SelectBox";
import {SyncedBook} from "@/lib/types/synced-book";
export function booksToSelectBox(books: SyncedBook[]): SelectBoxProps[] {
return books.map((book: SyncedBook): SelectBoxProps => {
return {
label: book.title,
value: book.id,
};
});
}
export function getBookTypeLabel(value: string): string {
switch (value) {
case 'short':
return 'bookTypes.short';
case 'novelette':
return 'bookTypes.novelette';
case 'long':
return 'bookTypes.novella';
case 'chapbook':
return 'bookTypes.chapbook';
case 'novel':
return 'bookTypes.novel';
default:
return 'bookTypes.novel';
}
}

33
lib/utils/cookies.ts Normal file
View File

@@ -0,0 +1,33 @@
export function getCookie(name: string): string | null {
const nameEQ: string = `${name}=`;
const allCookies: string[] = document.cookie.split(';');
for (let i: number = 0; i < allCookies.length; i++) {
let cookie: string = allCookies[i];
while (cookie.charAt(0) === ' ') cookie = cookie.substring(1, cookie.length);
if (cookie.indexOf(nameEQ) === 0) return cookie.substring(nameEQ.length, cookie.length);
}
return null;
}
export function setCookie(name: string, value: string, days: number): void {
const date: Date = new Date();
date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000));
const expires: string = `expires=${date.toUTCString()}`;
let domain: string = '';
if (!/localhost|127\.0\.0\.1/.test(window.location.hostname)) {
domain = `domain=${window.location.hostname};`;
}
const secure: string = 'Secure;';
const sameSite: string = 'SameSite=Strict;';
document.cookie = `${name}=${value}; ${expires}; ${domain} path=/; ${secure} ${sameSite}`;
}
export function removeCookie(name: string): void {
let domain: string = '';
if (!/localhost|127\.0\.0\.1/.test(window.location.hostname)) {
domain = `domain=${window.location.hostname};`;
}
const secure: string = 'Secure;';
const sameSite: string = 'SameSite=Strict;';
document.cookie = `${name}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; ${domain} path=/; ${secure} ${sameSite}`;
}

View File

@@ -0,0 +1,75 @@
const styleMap: Map<string, string> = new Map();
let sheet: CSSStyleSheet | null = null;
function getSheet(): CSSStyleSheet {
if (!sheet) {
sheet = new CSSStyleSheet();
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
}
return sheet;
}
function sanitizeForClassName(value: string): string {
return value.replace(/[^a-zA-Z0-9]/g, '');
}
export function dynamicBg(color: string): string {
const key: string = `bg-${color}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-bg-${sanitizeForClassName(color)}`;
getSheet().insertRule(`.${className} { background-color: ${color}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}
export function dynamicText(color: string): string {
const key: string = `text-${color}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-text-${sanitizeForClassName(color)}`;
getSheet().insertRule(`.${className} { color: ${color}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}
export function dynamicBorder(color: string, side: string = ''): string {
const prop: string = side ? `border-${side}-color` : 'border-color';
const key: string = `border-${side}-${color}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-border-${side ? side + '-' : ''}${sanitizeForClassName(color)}`;
getSheet().insertRule(`.${className} { ${prop}: ${color}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}
export function dynamicBgWithOpacity(hexColor: string, opacityHex: string): string {
const key: string = `bg-${hexColor}-${opacityHex}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-bg-${sanitizeForClassName(hexColor)}-${opacityHex}`;
getSheet().insertRule(`.${className} { background-color: ${hexColor}${opacityHex}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}
export function dynamicBorderWithOpacity(hexColor: string, opacityHex: string): string {
const key: string = `border-${hexColor}-${opacityHex}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-border-${sanitizeForClassName(hexColor)}-${opacityHex}`;
getSheet().insertRule(`.${className} { border-color: ${hexColor}${opacityHex}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}
export function dynamicBorderLeft(color: string, width: string = '3px'): string {
const key: string = `bl-${width}-${color}`;
if (styleMap.has(key)) return styleMap.get(key)!;
const className: string = `dyn-bl-${sanitizeForClassName(color)}`;
getSheet().insertRule(`.${className} { border-left: ${width} solid ${color}; }`, getSheet().cssRules.length);
styleMap.set(key, className);
return className;
}

6
lib/utils/editor.ts Normal file
View File

@@ -0,0 +1,6 @@
export function convertToHtml(text: string): string {
return text
.split(/\n\s*\n/)
.map((paragraph: string): string => `<p>${paragraph.trim()}</p>`)
.join('');
}

27
lib/utils/html.ts Normal file
View File

@@ -0,0 +1,27 @@
export function formatHTMLContent(htmlContent: string): string {
return htmlContent
.replace(/<h1>/g, '<h1 style="color: var(--color-text-primary); text-indent: 5px; font-size: 28px; font-weight: bold; text-align: left; margin-vertical: 10px;">')
.replace(/<p>/g, '<p style="color: var(--color-editor-text); text-indent: 30px; font-size: 16px; line-height: 22px; margin-vertical: 5px;">')
.replace(/<blockquote>/g, '<blockquote style="border-left-width: 4px; border-left-color: var(--color-gray-light); padding-left: 10px; font-style: italic; color: var(--color-text-dimmed);">');
}
export function textContentToHtml(content: string): string {
const paragraphs: string[] = content
.split(/\n+/)
.map((paragraph: string) => paragraph.trim())
.filter((paragraph: string) => paragraph.length > 0);
return paragraphs
.map((paragraph: string) => `<p>${paragraph}</p>`)
.join('');
}
export function htmlToText(html: string): string {
return html
.replace(/<br\s*\/?>/gi, '\n')
.replace(/<\/?(p|h[1-6]|div)(\s+[^>]*)?>/gi, '\n')
.replace(/<\/?[^>]+(>|$)/g, '')
.replace(/(\n\s*){2,}/g, '\n\n')
.replace(/^\s+|\s+$|(?<=\s)\s+/g, '')
.trim();
}

41
lib/utils/quillsense.ts Normal file
View File

@@ -0,0 +1,41 @@
import {SessionProps} from "@/lib/types/session";
import {Subscription} from "@/lib/types/user";
import {getCurrentSubscription} from "@/lib/utils/user";
export function getSubLevel(session: SessionProps): number {
let currentSub: Subscription | null = getCurrentSubscription(session?.user, 'quill-sense');
if (!currentSub) {
currentSub = getCurrentSubscription(session?.user, 'quill-trial');
if (!currentSub) {
return 0;
}
}
switch (currentSub?.subTier) {
case 1:
return 1;
case 2:
return 2;
case 3:
return 3;
default:
return 0;
}
}
export function isBringYourKeys(session: SessionProps): boolean {
if (!session?.user) return false;
const currentSub: Subscription | null = getCurrentSubscription(session?.user, 'use-your-keys');
return currentSub?.status || session.user.groupId <= 4;
}
export function isGeminiEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.gemini || false;
}
export function isAnthropicEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.anthropic || false;
}
export function isOpenAIEnabled(session: SessionProps): boolean {
return session.user?.apiKeys.openai || false;
}

430
lib/utils/story.ts Normal file
View File

@@ -0,0 +1,430 @@
import {Dispatch, SetStateAction} from "react";
import {VerbalTimeProps} from "@/lib/types/story";
export function getVerbesStyle(verbalTimeValue: number, level: number): VerbalTimeProps {
switch (verbalTimeValue) {
case 1:
return {
actions: level === 1 ? 'Passé composé' : 'Passé simple',
descriptions: 'Imparfait',
dialogues: 'Passé composé',
thoughts: level === 3 ? 'Subjonctif imparfait' : 'Plus-que-parfait',
summary: '→ Narrations épurées, style classique',
};
case 2:
return {
actions: 'Passé composé',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Imparfait + infinitifs' : 'Conditionnel présent',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Présent + impératif' : 'Impératif',
thoughts: level === 1 ? 'Futur proche' : level === 2 ? 'Conditionnel présent' : 'Subjonctif présent',
summary: '→ Témoignages, récits autobiographiques',
};
case 3:
return {
actions: level === 1 ? 'Plus-que-parfait' : 'Passé antérieur',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Plus-que-parfait' : 'Conditionnel passé',
dialogues: level === 3 ? 'Passé antérieur' : 'Passé simple',
thoughts: level === 1 ? 'Plus-que-parfait' : 'Subjonctif imparfait',
summary: '→ Flashbacks littéraires, tragédies',
};
case 4:
return {
actions: level === 1 ? 'Présent simple' : level === 2 ? 'Présent' : 'Présent + participe présent',
descriptions: level === 1 ? 'Participe présent' : level === 2 ? 'Participe présent + infinitifs' : 'Participes présents enchaînés',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Impératif' : 'Impératif + infinitifs',
thoughts: level === 1 ? 'Futur proche' : level === 2 ? 'Futur simple' : 'Futur antérieur',
summary: '→ Urgence, immersion totale',
};
case 5:
return {
actions: 'Présent',
descriptions: level === 1 ? 'Gérondif' : level === 2 ? 'Gérondif + infinitifs' : 'Gérondif + conditionnel',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Conditionnel présent' : 'Infinitif',
thoughts: level === 1 ? 'Infinitif' : 'Infinitif passé',
summary: '→ Méditations philosophiques',
};
case 6:
return {
actions: level === 1 ? 'Futur simple' : 'Futur antérieur',
descriptions: level === 1 ? 'Futur proche' : level === 2 ? 'Futur antérieur' : 'Futur antérieur',
dialogues: level === 1 ? 'Futur simple' : 'Futur proche',
thoughts: level === 1 ? 'Futur proche' : 'Futur antérieur',
summary: '→ Prophéties, plans stratégiques',
};
case 7:
return {
actions: level === 1 ? 'Futur simple' : 'Futur antérieur',
descriptions: level === 1 ? 'Futur proche' : level === 2 ? 'Futur simple + conditionnel' : 'Conditionnel passé',
dialogues: level === 1 ? 'Futur proche' : level === 2 ? 'Futur antérieur' : 'Futur simple',
thoughts: level === 1 ? 'Futur simple' : level === 2 ? 'Conditionnel passé' : 'Futur antérieur',
summary: '→ Dystopies, récits post-apocalyptiques',
};
case 8:
return {
actions: 'Imparfait',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Conditionnel présent' : 'Conditionnel passé',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Infinitif' : 'Infinitifs',
thoughts: level === 1 ? 'Subjonctif présent' : level === 2 ? 'Subjonctif imparfait' : 'Subjonctif imparfait',
summary: '→ Rêves, souvenirs déformés',
};
case 9:
return {
actions: 'Conditionnel présent',
descriptions: 'Conditionnel passé',
dialogues: 'Subjonctif imparfait',
thoughts: level === 3 ? 'Subjonctif imparfait' : 'Plus-que-parfait',
summary: '→ Uchronies, réalités alternatives',
};
case 10:
return {
actions: level === 1 ? 'Subjonctif présent' : 'Subjonctif imparfait',
descriptions: level === 1 ? 'Subjonctif présent' : 'Subjonctif imparfait',
dialogues: 'Impératif',
thoughts: level === 3 ? 'Subjonctif imparfait' : 'Conditionnel passé',
summary: '→ Drames psychologiques, dilemmes',
};
case 11:
return {
actions: 'Passé composé',
descriptions: 'Imparfait',
dialogues: 'Plus-que-parfait',
thoughts: 'Infinitif passé',
summary: '→ Regrets, introspection nostalgique',
};
case 12:
return {
actions: 'Présent',
descriptions: level === 1 ? 'Passé composé' : 'Passé composé + futur antérieur',
dialogues: level === 1 ? 'Futur proche' : 'Futur antérieur',
thoughts: 'Participe présent',
summary: '→ Crise en cours, compte à rebours',
};
case 13:
return {
actions: level === 1 ? 'Présent simple' : level === 2 ? 'Présent + participe présent' : 'Participes présents enchaînés',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Participe présent + adjectifs' : 'Subjonctif présent',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Conditionnel présent' : 'Subjonctif présent',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Infinitif passé' : 'Subjonctif imparfait',
summary: '→ Émotions intenses, introspections vives (romances, drames psychologiques)',
};
case 14:
return {
actions: 'Présent',
descriptions: level === 1 ? 'Gérondif' : level === 2 ? 'Gérondif + infinitifs' : 'Conditionnel présent',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Impératif' : 'Infinitif',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Infinitif passé' : 'Subjonctif imparfait',
summary: '→ Réflexions profondes, analyse des émotions (nouvelles philosophiques, récits introspectifs)',
};
case 15:
return {
actions: level === 1 ? 'Présent simple' : level === 2 ? 'Présent + passé simple' : 'Présent + passé antérieur',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Passé composé' : 'Conditionnel passé',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Passé simple' : 'Futur antérieur',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Plus-que-parfait' : 'Subjonctif imparfait',
summary: '→ Histoires historiques avec une intensité immédiate (batailles, moments décisifs)',
};
case 16:
return {
actions: level === 1 ? 'Passé composé' : level === 2 ? 'Imparfait + passé simple' : 'Plus-que-parfait',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Participe passé' : 'Conditionnel passé',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Imparfait' : 'Subjonctif imparfait',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Infinitif passé' : 'Subjonctif présent',
summary: '→ Récits introspectifs, auto-analyse (autofictions, récits de croissance personnelle)',
};
case 17:
return {
actions: level === 1 ? 'Futur simple' : level === 2 ? 'Futur antérieur' : 'Conditionnel passé',
descriptions: level === 1 ? 'Futur proche' : level === 2 ? 'Futur antérieur' : 'Conditionnel présent',
dialogues: level === 1 ? 'Futur simple' : level === 2 ? 'Futur antérieur' : 'Subjonctif présent',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Futur antérieur' : 'Conditionnel passé',
summary: '→ Prophéties, visions apocalyptiques (récits mystiques, romans de science-fiction)',
};
case 18:
return {
actions: level === 1 ? 'Conditionnel présent' : level === 2 ? 'Conditionnel passé' : 'Subjonctif imparfait',
descriptions: level === 1 ? 'Conditionnel présent' : level === 2 ? 'Conditionnel passé' : 'Subjonctif présent',
dialogues: level === 1 ? 'Conditionnel présent' : level === 2 ? 'Subjonctif imparfait' : 'Impératif',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Infinitif passé' : 'Subjonctif imparfait',
summary: '→ Mondes parallèles, uchronies (romans alternatifs, récits de fantasy)',
};
case 19:
return {
actions: level === 1 ? 'Imparfait' : level === 2 ? 'Imparfait + participe présent' : 'Participes présents enchaînés',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Participe présent + adjectifs' : 'Subjonctif présent',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Imparfait' : 'Subjonctif imparfait',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Infinitif passé' : 'Subjonctif présent',
summary: '→ Lyrisme, poésie narrative (récits oniriques, nouvelles littéraires)',
};
case 20:
return {
actions: level === 1 ? 'Présent simple' : level === 2 ? 'Imparfait' : 'Futur simple',
descriptions: level === 1 ? 'Imparfait' : level === 2 ? 'Participe présent' : 'Conditionnel présent',
dialogues: level === 1 ? 'Présent' : level === 2 ? 'Imparfait' : 'Futur proche',
thoughts: level === 1 ? 'Infinitif' : level === 2 ? 'Plus-que-parfait' : 'Subjonctif présent',
summary: '→ Immersion totale (récits interactifs, jeux de rôle, romans à choix multiples)',
};
default:
return {
actions: 'Passé simple',
descriptions: 'Imparfait',
dialogues: 'Passé composé',
thoughts: 'Plus-que-parfait',
summary: '→ Narrations épurées, style classique',
};
}
}
export function presetStoryType(
presetType: string,
setTone: Dispatch<SetStateAction<string>>,
setAtmosphere: Dispatch<SetStateAction<string>>,
setVerbTense: Dispatch<SetStateAction<string>>,
setPerson: Dispatch<SetStateAction<string>>,
setDialogueType: Dispatch<SetStateAction<string>>,
setIsExplicit: Dispatch<SetStateAction<boolean>>,
): void {
switch (presetType) {
case '1':
setTone('Suspense angoissant, mystère troublant');
setAtmosphere('Tension oppressante, ombres menaçantes');
setVerbTense('3');
setPerson('1');
setDialogueType('3');
setIsExplicit(false);
break;
case '2':
setTone('Brutalité crue, terreur psychologique');
setAtmosphere('Claustrophobie, clair-obscur sinistre');
setVerbTense('10');
setPerson('4');
setDialogueType('4');
setIsExplicit(true);
break;
case '3':
setTone('Magie envoûtante, innocence poétique');
setAtmosphere('Forêt luminescente, brume enchantée');
setVerbTense('19');
setPerson('3');
setDialogueType('1');
setIsExplicit(false);
break;
case '4':
setTone('Froidure technologique, désespoir systémique');
setAtmosphere('Métal rouillé, lumières néon vacillantes');
setVerbTense('7');
setPerson('5');
setDialogueType('3');
setIsExplicit(false);
break;
case '5':
setTone('Passion tourmentée, mélancolie sensuelle');
setAtmosphere('Pluie fine, chambres aux rideaux lourds');
setVerbTense('13');
setPerson('1');
setDialogueType('1');
setIsExplicit(true);
break;
case '6':
setTone('Héroïsme grandiose, dangers exaltants');
setAtmosphere('Vastes paysages, ruines anciennes');
setVerbTense('4');
setPerson('6');
setDialogueType('3');
setIsExplicit(false);
break;
case '7':
setTone('Méditation existentielle, questions sans réponses');
setAtmosphere('Bibliothèque poussiéreuse, nuit silencieuse');
setVerbTense('5');
setPerson('5');
setDialogueType('4');
setIsExplicit(true);
break;
case '8':
setTone('Tension psychologique, suspense mental');
setAtmosphere('Isolation, paranoïa croissante');
setVerbTense('10');
setPerson('4');
setDialogueType('4');
setIsExplicit(true);
break;
case '9':
setTone('Mystère obscur, surnaturel inquiétant');
setAtmosphere('Forêts sombres, créatures cachées');
setVerbTense('3');
setPerson('1');
setDialogueType('3');
setIsExplicit(false);
break;
case '10':
setTone('Amour interdit, passion à travers les âges');
setAtmosphere('Châteaux majestueux, bals somptueux');
setVerbTense('1');
setPerson('3');
setDialogueType('1');
setIsExplicit(true);
break;
case '11':
setTone('Dure réalité, enquête sombre');
setAtmosphere('Rues sombres, ambiance de crime');
setVerbTense('16');
setPerson('5');
setDialogueType('4');
setIsExplicit(true);
break;
case '12':
setTone('Espoir futuriste, société idéale');
setAtmosphere('Villes lumineuses, technologie avancée');
setVerbTense('6');
setPerson('4');
setDialogueType('3');
setIsExplicit(false);
break;
case '13':
setTone('Magie contemporaine, réalisme enchanté');
setAtmosphere('Ville moderne, éléments féeriques');
setVerbTense('4');
setPerson('1');
setDialogueType('1');
setIsExplicit(true);
break;
case '14':
setTone('Conflits émotionnels, relations complexes');
setAtmosphere('Intérieur chaleureux, tensions sous-jacentes');
setVerbTense('13');
setPerson('1');
setDialogueType('1');
setIsExplicit(true);
break;
case '15':
setTone('Exploration audacieuse, dangers marins');
setAtmosphere('Océan infini, navires anciens');
setVerbTense('4');
setPerson('6');
setDialogueType('3');
setIsExplicit(false);
break;
case '16':
setTone('Quête héroïque, magie puissante');
setAtmosphere('Mondes imaginaires, créatures mythiques');
setVerbTense('19');
setPerson('3');
setDialogueType('1');
setIsExplicit(true);
break;
case '17':
setTone('Amour moderne, relations actuelles');
setAtmosphere('Ville animée, cafés cosy');
setVerbTense('13');
setPerson('1');
setDialogueType('1');
setIsExplicit(true);
break;
case '18':
setTone("Intrigue internationale, secrets d'État");
setAtmosphere('Villes étrangères, tensions diplomatiques');
setVerbTense('16');
setPerson('5');
setDialogueType('4');
setIsExplicit(true);
break;
case '19':
setTone('Survie désespérée, monde en ruines');
setAtmosphere('Paysages dévastés, ressources rares');
setVerbTense('7');
setPerson('4');
setDialogueType('3');
setIsExplicit(false);
break;
case '20':
setTone('Leçons de vie, valeurs profondes');
setAtmosphere('Village paisible, nature environnante');
setVerbTense('1');
setPerson('3');
setDialogueType('1');
setIsExplicit(true);
break;
}
}
export function getNarrativePerson(value: number, level: number): string {
if (level === 1) {
switch (value) {
case 1:
return 'Première personne (Je acteur) - Parfait pour les débuts (ex: Je marchais)';
case 3:
return 'Troisième omnisciente - Narration globale (ex: Il marchait)';
default:
return 'Première personne';
}
} else if (level === 2) {
switch (value) {
case 1:
return 'Première personne (Je acteur)';
case 2:
return 'Première personne (Je témoin) - Observateur (ex: Je le regardais marcher)';
case 3:
return 'Troisième omnisciente';
case 4:
return 'Troisième limitée - Focus sur un personnage (ex: Il marchait, ignorant le danger)';
default:
return 'Première personne';
}
} else if (level === 3) {
switch (value) {
case 1:
return 'Première personne (Je acteur)';
case 2:
return 'Première personne (Je témoin)';
case 3:
return 'Troisième omnisciente';
case 4:
return 'Troisième limitée';
case 5:
return 'Deuxième personne (Tu) - Immersion forte (ex: Tu marches vers la mort)';
case 6:
return 'Nous collectif - Voix chorale (ex: Nous marchions, unis par le destin)';
default:
return 'Troisième omnisciente';
}
}
return 'Première personne';
}
export function getDialogueType(value: number, level: number): string {
if (level === 1) {
switch (value) {
case 1:
return 'Dialogue direct - Paroles exactes (ex: "Je t\'aime !")';
case 2:
return 'Dialogue indirect - Résumé par le narrateur (ex: Il dit qu\'il m\'aime)';
default:
return 'Dialogue direct';
}
} else if (level === 2) {
switch (value) {
case 1:
return 'Dialogue direct';
case 2:
return 'Dialogue indirect';
case 3:
return 'Dialogue mixte (ex: "Je t\'aime" dit-il, puis explique ses sentiments)';
default:
return 'Dialogue direct';
}
} else if (level === 3) {
switch (value) {
case 1:
return 'Dialogue direct';
case 2:
return 'Dialogue indirect';
case 3:
return 'Dialogue mixte';
case 4:
return 'Monologue intérieur (ex: *Je ne peux pas le perdre...*)';
default:
return 'Dialogue direct';
}
}
return 'Dialogue direct';
}

View File

@@ -16,7 +16,7 @@ import {
SyncedActSummary,
SyncedGuideLine,
SyncedAIGuideLine
} from "@/lib/models/SyncedBook";
} from "@/lib/types/synced-book";
/**
* Résultat de comparaison pour un livre

3
lib/utils/time.ts Normal file
View File

@@ -0,0 +1,3 @@
export function timeStampInSeconds(): number {
return Math.floor(new Date().getTime() / 1000);
}

140
lib/utils/tiptap.ts Normal file
View File

@@ -0,0 +1,140 @@
import {TiptapAttrValue, TiptapLinkAttrs, TiptapNode} from "@/lib/types/chapter";
function isTiptapLinkAttrs(value: TiptapAttrValue): value is TiptapLinkAttrs {
return typeof value === 'object' && value !== null && 'href' in value;
}
export function getPageCount(text: string): number {
const charactersPerLine: number = 90;
const linesPerPage: number = 40;
const lines: string[] = text.split('\n');
let totalLines: number = 0;
lines.forEach((line: string) => {
const lineLength: number = line.length;
const estimatedLines: number = Math.ceil(lineLength / charactersPerLine);
totalLines += estimatedLines;
});
return Math.ceil(totalLines / linesPerPage);
}
export function convertTiptapToHTML(node: TiptapNode): string {
let html: string = '';
switch (node.type) {
case 'doc':
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
break;
case 'paragraph':
html += '<p>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</p>';
break;
case 'text':
let textContent: string = node.text || '';
if (node.attrs) {
if (node.attrs.bold) {
textContent = `<strong>${textContent}</strong>`;
}
if (node.attrs.italic) {
textContent = `<em>${textContent}</em>`;
}
if (node.attrs.underline) {
textContent = `<u>${textContent}</u>`;
}
if (node.attrs.strike) {
textContent = `<s>${textContent}</s>`;
}
if (node.attrs.link && isTiptapLinkAttrs(node.attrs.link)) {
textContent = `<a href="${node.attrs.link.href}">${textContent}</a>`;
}
}
html += textContent;
break;
case 'heading':
const level: number = typeof node.attrs?.level === 'number' ? node.attrs.level : 1;
html += `<h${level}>`;
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += `</h${level}>`;
break;
case 'bulletList':
html += '<ul>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</ul>';
break;
case 'orderedList':
html += '<ol>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</ol>';
break;
case 'listItem':
html += '<li>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</li>';
break;
case 'blockquote':
html += '<blockquote>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</blockquote>';
break;
case 'codeBlock':
html += '<pre><code>';
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
html += '</code></pre>';
break;
default:
if (node.content) {
node.content.forEach((childNode: TiptapNode) => {
html += convertTiptapToHTML(childNode);
});
}
break;
}
return html;
}

44
lib/utils/user.ts Normal file
View File

@@ -0,0 +1,44 @@
import {GuideTour, Subscription, UserProps} from "@/lib/types/user";
import {SessionProps} from "@/lib/types/session";
export function getCurrentSubscription(user: UserProps | null, type: "quill-sense" | "use-your-keys" | "quill-trial"): Subscription | null {
if (!user || !user.subscription || user.subscription.length === 0) {
return null;
}
return user.subscription.find((sub: Subscription): boolean => {
return sub.subType === type && sub.status;
}) || null;
}
export function getWritingLevel(level: number): string {
switch (level) {
case 1:
return 'Débutant';
case 2:
return 'Intermédiaire';
case 3:
return 'Avancé';
default:
return 'Débutant';
}
}
export function guideTourDone(guide: GuideTour[], tour: string): boolean {
if (!guide || !tour) return false;
return guide.find((guide: GuideTour): boolean => guide[tour]) === undefined;
}
export function setNewGuideTour(session: SessionProps, tour: string): SessionProps {
if (!session.user) return session;
const newGuideTour: GuideTour[] = [
...(session.user.guideTour ?? []),
{[tour]: true}
];
return {
...session,
user: {
...session.user,
guideTour: newGuideTour
}
};
}

4
lib/utils/validation.ts Normal file
View File

@@ -0,0 +1,4 @@
export function verifyInput(input: string): boolean {
const pattern: RegExp = new RegExp('(<.*?>)|(&.*?;)|({.*?})', 'gmi');
return pattern.test(input);
}