Remove unused components and models for improved maintainability
- Deleted redundant components (`AddActionButton`, `AlertBox`, `AlertStack`, `BackButton`, `CancelButton`, and `CollapsableArea`) and related files. - Removed unused models (`Book`, `BookSerie`, `BookTables`, `Character`, and `Chapter`) to reduce codebase clutter. - Updated project structure and references to reflect these removals.
This commit is contained in:
@@ -1,40 +1,33 @@
|
||||
import {ChangeEvent, useContext, useEffect, useState} from "react";
|
||||
import React, {ChangeEvent, useContext, useEffect, useState} from "react";
|
||||
import {Editor, EditorContent, useEditor} from "@tiptap/react";
|
||||
import StarterKit from "@tiptap/starter-kit";
|
||||
import Underline from "@tiptap/extension-underline";
|
||||
import TextAlign from "@tiptap/extension-text-align";
|
||||
import System from "@/lib/models/System";
|
||||
import {ChapterContext} from "@/context/ChapterContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import {SelectBoxProps} from "@/shared/interface";
|
||||
import {AlertContext} from "@/context/AlertContext";
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {
|
||||
faCubes,
|
||||
faFeather,
|
||||
faGlobe,
|
||||
faMagicWandSparkles,
|
||||
faMapPin,
|
||||
faPalette,
|
||||
faUser
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
|
||||
import QSTextGeneratedPreview from "@/components/QSTextGeneratedPreview";
|
||||
import {EditorContext} from "@/context/EditorContext";
|
||||
import {useTranslations} from "next-intl";
|
||||
import QuillSense from "@/lib/models/QuillSense";
|
||||
import {apiGet} from "@/lib/api/client";
|
||||
import {configs, isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {textContentToHtml} from "@/lib/utils/html";
|
||||
import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {SelectBoxProps} from "@/components/form/SelectBox";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import {Boxes, Feather, Globe, MapPin, Palette, User, Wand2} from "lucide-react";
|
||||
import Button from "@/components/ui/Button";
|
||||
import QSTextGeneratedPreview from "@/components/ui/QSTextGeneratedPreview";
|
||||
import {EditorContext, EditorContextProps} from "@/context/EditorContext";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import {getSubLevel, isOpenAIEnabled} from "@/lib/utils/quillsense";
|
||||
import TextInput from "@/components/form/TextInput";
|
||||
import InputField from "@/components/form/InputField";
|
||||
import TexteAreaInput from "@/components/form/TexteAreaInput";
|
||||
import TextAreaInput from "@/components/form/TextAreaInput";
|
||||
import SuggestFieldInput from "@/components/form/SuggestFieldInput";
|
||||
import Collapse from "@/components/Collapse";
|
||||
import Collapse from "@/components/ui/Collapse";
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import {BookTags} from "@/lib/models/Book";
|
||||
import {BookTags} from "@/lib/types/book";
|
||||
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
|
||||
import {configs} from "@/lib/configs";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface CompanionContent {
|
||||
version: number;
|
||||
@@ -44,9 +37,8 @@ interface CompanionContent {
|
||||
|
||||
export default function DraftCompanion() {
|
||||
const t = useTranslations();
|
||||
const {setTotalPrice, setTotalCredits} = useContext<AIUsageContextProps>(AIUsageContext)
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||
const {setTotalPrice, setTotalCredits}: AIUsageContextProps = useContext<AIUsageContextProps>(AIUsageContext)
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
|
||||
|
||||
const mainEditor: Editor | null = useEditor({
|
||||
extensions: [
|
||||
@@ -60,11 +52,12 @@ export default function DraftCompanion() {
|
||||
editable: false,
|
||||
immediatelyRender: false,
|
||||
});
|
||||
const {editor} = useContext(EditorContext);
|
||||
const {chapter} = useContext(ChapterContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {errorMessage, infoMessage} = useContext(AlertContext);
|
||||
const {editor}: EditorContextProps = useContext<EditorContextProps>(EditorContext);
|
||||
const {chapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {errorMessage, infoMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
|
||||
const [draftVersion, setDraftVersion] = useState<number>(0);
|
||||
const [draftWordCount, setDraftWordCount] = useState<number>(0);
|
||||
@@ -100,10 +93,10 @@ export default function DraftCompanion() {
|
||||
const [useExplicit, setUseExplicit] = useState<boolean>(false);
|
||||
const [useSmart, setUseSmart] = useState<boolean>(false);
|
||||
|
||||
const isGPTEnabled: boolean = QuillSense.isOpenAIEnabled(session);
|
||||
const isSubTierTree: boolean = QuillSense.getSubLevel(session) === 3;
|
||||
const hasAccess: boolean = (isGPTEnabled || isSubTierTree) && !isCurrentlyOffline() && !book?.localBook;
|
||||
|
||||
const isGPTEnabled: boolean = isOpenAIEnabled(session);
|
||||
const isSubTierTree: boolean = getSubLevel(session) === 3;
|
||||
const hasAccess: boolean = isGPTEnabled || isSubTierTree;
|
||||
|
||||
useEffect((): void => {
|
||||
getDraftContent().then();
|
||||
if (showEnhancer) {
|
||||
@@ -113,14 +106,11 @@ export default function DraftCompanion() {
|
||||
|
||||
async function getDraftContent(): Promise<void> {
|
||||
try {
|
||||
let response: CompanionContent | null;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
response = await tauri.getCompanionContent(
|
||||
chapter?.chapterId ?? '',
|
||||
chapter?.chapterContent.version ?? 0,
|
||||
) as CompanionContent | null;
|
||||
let response: CompanionContent;
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.getCompanionContent(chapter?.chapterId ?? '', chapter?.chapterContent.version ?? 0) as CompanionContent;
|
||||
} else {
|
||||
response = await System.authGetQueryToServer<CompanionContent>(`chapter/content/companion`, session.accessToken, lang, {
|
||||
response = await apiGet<CompanionContent>(`chapter/content/companion`, session.accessToken, lang, {
|
||||
bookid: book?.bookId,
|
||||
chapterid: chapter?.chapterId,
|
||||
version: chapter?.chapterContent.version,
|
||||
@@ -160,11 +150,11 @@ export default function DraftCompanion() {
|
||||
|
||||
async function fetchTags(): Promise<void> {
|
||||
try {
|
||||
let responseTags: BookTags | null;
|
||||
if (isCurrentlyOffline() || book?.localBook) {
|
||||
responseTags = await tauri.getBookTags(book?.bookId ?? '') as BookTags | null;
|
||||
let responseTags: BookTags;
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
responseTags = await tauri.getBookTags(book?.bookId ?? '') as BookTags;
|
||||
} else {
|
||||
responseTags = await System.authGetQueryToServer<BookTags>(`book/tags`, session.accessToken, lang, {
|
||||
responseTags = await apiGet<BookTags>(`book/tags`, session.accessToken, lang, {
|
||||
bookId: book?.bookId
|
||||
});
|
||||
}
|
||||
@@ -190,13 +180,13 @@ export default function DraftCompanion() {
|
||||
infoMessage(t("draftCompanion.abortSuccess"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function handleQuillSenseRefined(): Promise<void> {
|
||||
if (chapter && session?.accessToken) {
|
||||
setIsRefining(true);
|
||||
setShowRefinedText(false);
|
||||
setRefinedText('');
|
||||
|
||||
|
||||
try {
|
||||
const response: Response = await fetch(`${configs.apiUrl}quillsense/refine`, {
|
||||
method: 'POST',
|
||||
@@ -242,9 +232,9 @@ export default function DraftCompanion() {
|
||||
while (true) {
|
||||
try {
|
||||
const {done, value}: ReadableStreamReadResult<Uint8Array> = await reader.read();
|
||||
|
||||
|
||||
if (done) break;
|
||||
|
||||
|
||||
const chunk: string = decoder.decode(value, {stream: true});
|
||||
const lines: string[] = chunk.split('\n');
|
||||
|
||||
@@ -258,11 +248,12 @@ export default function DraftCompanion() {
|
||||
useYourKey?: boolean;
|
||||
} = JSON.parse(dataStr);
|
||||
|
||||
// Si c'est un chunk de contenu
|
||||
if ('content' in data && data.content) {
|
||||
accumulatedText += data.content;
|
||||
setRefinedText(accumulatedText);
|
||||
}
|
||||
|
||||
// Si c'est le message final avec les totaux
|
||||
else if ('useYourKey' in data && 'totalPrice' in data) {
|
||||
if (data.useYourKey) {
|
||||
setTotalPrice((prev: number): number => prev + data.totalPrice!);
|
||||
@@ -271,7 +262,7 @@ export default function DraftCompanion() {
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
console.error('Error parsing SSE data:', e);
|
||||
errorMessage(t("draftCompanion.sseParsingError"));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -301,7 +292,7 @@ export default function DraftCompanion() {
|
||||
if (editor.getText().length > 0) {
|
||||
editor.commands.insertContent('\n\n');
|
||||
}
|
||||
editor.commands.insertContent(System.textContentToHtml(refinedText));
|
||||
editor.commands.insertContent(textContentToHtml(refinedText));
|
||||
setShowRefinedText(false);
|
||||
}
|
||||
}
|
||||
@@ -430,23 +421,21 @@ export default function DraftCompanion() {
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0 overflow-hidden">
|
||||
<div
|
||||
className="flex items-center justify-between p-4 bg-secondary/30 backdrop-blur-sm border-b border-secondary/50 flex-shrink-0 shadow-sm">
|
||||
className="flex items-center justify-between p-4 flex-shrink-0">
|
||||
<h2 className="text-text-primary font-['ADLaM_Display'] text-xl">Amélioration de texte</h2>
|
||||
<button
|
||||
onClick={(): void => setShowEnhancer(false)}
|
||||
className="px-5 py-2.5 bg-secondary/50 hover:bg-secondary text-text-primary rounded-xl transition-all duration-200 hover:scale-105 shadow-md border border-secondary/50 font-medium"
|
||||
>
|
||||
<Button variant="secondary" onClick={(): void => setShowEnhancer(false)}>
|
||||
Retour
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 space-y-4 custom-scrollbar">
|
||||
<Collapse
|
||||
variant="card"
|
||||
title="Style d'écriture"
|
||||
content={
|
||||
<div className="space-y-4">
|
||||
<InputField
|
||||
icon={faPalette}
|
||||
icon={Palette}
|
||||
fieldName={t("ghostWriter.toneAtmosphere")}
|
||||
input={
|
||||
<TextInput
|
||||
@@ -461,11 +450,12 @@ export default function DraftCompanion() {
|
||||
/>
|
||||
|
||||
<Collapse
|
||||
variant="card"
|
||||
title="Tags contextuels"
|
||||
content={
|
||||
<div className="space-y-4">
|
||||
<SuggestFieldInput inputFieldName={`Personnages`}
|
||||
inputFieldIcon={faUser}
|
||||
inputFieldIcon={User}
|
||||
searchTags={searchCharacters}
|
||||
tagued={taguedCharacters}
|
||||
handleTagSearch={(e) => handleCharacterSearch(e.target.value)}
|
||||
@@ -478,7 +468,7 @@ export default function DraftCompanion() {
|
||||
/>
|
||||
|
||||
<SuggestFieldInput inputFieldName={`Lieux`}
|
||||
inputFieldIcon={faMapPin}
|
||||
inputFieldIcon={MapPin}
|
||||
searchTags={searchLocations}
|
||||
tagued={taguedLocations}
|
||||
handleTagSearch={(e) => handleLocationSearch(e.target.value)}
|
||||
@@ -491,7 +481,7 @@ export default function DraftCompanion() {
|
||||
/>
|
||||
|
||||
<SuggestFieldInput inputFieldName={`Objets`}
|
||||
inputFieldIcon={faCubes}
|
||||
inputFieldIcon={Boxes}
|
||||
searchTags={searchObjects}
|
||||
tagued={taguedObjects}
|
||||
handleTagSearch={(e) => handleObjectSearch(e.target.value)}
|
||||
@@ -504,7 +494,7 @@ export default function DraftCompanion() {
|
||||
/>
|
||||
|
||||
<SuggestFieldInput inputFieldName={`Éléments mondiaux`}
|
||||
inputFieldIcon={faGlobe}
|
||||
inputFieldIcon={Globe}
|
||||
searchTags={searchWorldElements}
|
||||
tagued={taguedWorldElements}
|
||||
handleTagSearch={(e) => handleWorldElementSearch(e.target.value)}
|
||||
@@ -519,12 +509,12 @@ export default function DraftCompanion() {
|
||||
}
|
||||
/>
|
||||
|
||||
<div className="bg-secondary/20 rounded-xl p-5 shadow-inner border border-secondary/30">
|
||||
<div className="bg-tertiary rounded-xl p-4">
|
||||
<InputField
|
||||
icon={faMagicWandSparkles}
|
||||
icon={Wand2}
|
||||
fieldName="Spécifications"
|
||||
input={
|
||||
<TexteAreaInput
|
||||
<TextAreaInput
|
||||
value={specifications}
|
||||
setValue={(e: ChangeEvent<HTMLTextAreaElement>) => setSpecifications(e.target.value)}
|
||||
placeholder="Spécifications particulières pour l'amélioration..."
|
||||
@@ -543,15 +533,15 @@ export default function DraftCompanion() {
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="p-5 border-t border-secondary/50 bg-secondary/30 backdrop-blur-sm shrink-0 shadow-inner">
|
||||
className="p-5 shrink-0">
|
||||
<div className="flex justify-center">
|
||||
<SubmitButtonWLoading
|
||||
callBackAction={handleQuillSenseRefined}
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleQuillSenseRefined}
|
||||
isLoading={isRefining}
|
||||
text={t("draftCompanion.refine")}
|
||||
loadingText={t("draftCompanion.refining")}
|
||||
icon={faMagicWandSparkles}
|
||||
/>
|
||||
icon={Wand2}
|
||||
>{t("draftCompanion.refine")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
{(showRefinedText || isRefining) && (
|
||||
@@ -571,21 +561,21 @@ export default function DraftCompanion() {
|
||||
return (
|
||||
<div className="flex flex-col h-full min-h-0 overflow-hidden font-['Lora']">
|
||||
<div
|
||||
className="flex items-center justify-between p-4 bg-secondary/30 backdrop-blur-sm border-b border-secondary/50 flex-shrink-0 font-['ADLaM_Display'] shadow-sm">
|
||||
<div className="mr-4 text-primary-light">
|
||||
className="flex items-center justify-between p-4 flex-shrink-0 font-['ADLaM_Display']">
|
||||
<div className="mr-4 text-muted">
|
||||
<span>{t("draftCompanion.words")}: </span>
|
||||
<span className="text-text-primary">{draftWordCount}</span>
|
||||
</div>
|
||||
{
|
||||
hasAccess && book?.quillsenseEnabled !== false && chapter?.chapterContent.version === 3 && (
|
||||
book?.quillsenseEnabled !== false && hasAccess && chapter?.chapterContent.version === 3 && (
|
||||
<div className="flex gap-2">
|
||||
<SubmitButtonWLoading
|
||||
callBackAction={(): void => setShowEnhancer(true)}
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={(): void => setShowEnhancer(true)}
|
||||
isLoading={isRefining}
|
||||
text={t("draftCompanion.refine")}
|
||||
loadingText={t("draftCompanion.refining")}
|
||||
icon={faFeather}
|
||||
/>
|
||||
icon={Feather}
|
||||
>{t("draftCompanion.refine")}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
|
||||
import {faBookOpen} from "@fortawesome/free-solid-svg-icons";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {BookOpen} from 'lucide-react';
|
||||
import React from "react";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
|
||||
export default function NoBookHome() {
|
||||
const t = useTranslations();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full p-8 text-center">
|
||||
<div
|
||||
className="max-w-md bg-tertiary/90 backdrop-blur-sm p-10 rounded-2xl shadow-2xl border border-secondary/50">
|
||||
<FontAwesomeIcon icon={faBookOpen} className={"text-primary w-20 h-20 mb-6 animate-pulse"}/>
|
||||
<div className="bg-tertiary rounded-xl p-10 max-w-md">
|
||||
<BookOpen className={"text-primary w-20 h-20 mb-6 animate-pulse"} strokeWidth={1.75}/>
|
||||
<h3 className="text-2xl font-['ADLaM_Display'] text-text-primary mb-4">{t("noBookHome.title")}</h3>
|
||||
<p className="text-muted mb-6 text-lg leading-relaxed">
|
||||
{t("noBookHome.description")}
|
||||
</p>
|
||||
<div
|
||||
className="flex items-center justify-center gap-3 text-sm text-muted bg-secondary/30 p-4 rounded-xl border border-secondary/40">
|
||||
<FontAwesomeIcon icon={faBookOpen} className="text-primary w-5 h-5"/>
|
||||
className="flex items-center justify-center gap-3 text-sm text-muted bg-secondary p-4 rounded-xl border border-secondary">
|
||||
<BookOpen className="text-primary w-5 h-5" strokeWidth={1.75}/>
|
||||
<span>{t("noBookHome.hint")}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import {useContext, useState} from "react";
|
||||
import {ChapterContext} from "@/context/ChapterContext";
|
||||
import {BookContext} from "@/context/BookContext";
|
||||
import {SettingBookContext} from "@/context/SettingBookContext";
|
||||
import React, {useContext, useState} from "react";
|
||||
import {ChapterContext, ChapterContextProps} from "@/context/ChapterContext";
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {SettingBookContext, SettingBookContextProps} from "@/context/SettingBookContext";
|
||||
import TextEditor from "./TextEditor";
|
||||
import BookList from "@/components/book/BookList";
|
||||
import BookSettingOption from "@/components/book/settings/BookSettingOption";
|
||||
import NoBookHome from "@/components/editor/NoBookHome";
|
||||
|
||||
export default function ScribeEditor() {
|
||||
const {chapter} = useContext(ChapterContext);
|
||||
const {book} = useContext(BookContext);
|
||||
const {chapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
|
||||
const [bookSettingId, setBookSettingId] = useState<string>('');
|
||||
|
||||
|
||||
@@ -1,48 +1,44 @@
|
||||
'use client'
|
||||
import {useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {EditorContent} from '@tiptap/react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import React, {useCallback, useContext, useEffect, useRef, useState} from 'react';
|
||||
import {EditorContent, JSONContent} from '@tiptap/react';
|
||||
import {
|
||||
faAlignCenter,
|
||||
faAlignLeft,
|
||||
faAlignRight,
|
||||
faBold,
|
||||
faCog,
|
||||
faFloppyDisk,
|
||||
faGhost,
|
||||
faHeading,
|
||||
faLayerGroup,
|
||||
faListOl,
|
||||
faListUl,
|
||||
faParagraph,
|
||||
faUnderline,
|
||||
faXmark
|
||||
} from '@fortawesome/free-solid-svg-icons';
|
||||
import {EditorContext} from "@/context/EditorContext";
|
||||
import {ChapterContext} from '@/context/ChapterContext';
|
||||
import System from '@/lib/models/System';
|
||||
import {AlertContext} from '@/context/AlertContext';
|
||||
import {SessionContext} from "@/context/SessionContext";
|
||||
import {BookContext} from '@/context/BookContext';
|
||||
AlignCenter,
|
||||
AlignJustify,
|
||||
AlignLeft,
|
||||
AlignRight,
|
||||
Bold,
|
||||
Ghost,
|
||||
Heading1,
|
||||
Heading2,
|
||||
Heading3,
|
||||
Layers,
|
||||
List,
|
||||
ListOrdered,
|
||||
LucideIcon,
|
||||
Save,
|
||||
SlidersHorizontal,
|
||||
Underline
|
||||
} from 'lucide-react';
|
||||
import {EditorContext, EditorContextProps} from "@/context/EditorContext";
|
||||
import {ChapterContext, ChapterContextProps} from '@/context/ChapterContext';
|
||||
import {BookContext, BookContextProps} from "@/context/BookContext";
|
||||
import {apiPost} from '@/lib/api/client';
|
||||
import {isDesktop} from '@/lib/configs';
|
||||
import * as tauri from '@/lib/tauri';
|
||||
import OfflineContext, {OfflineContextType} from '@/context/OfflineContext';
|
||||
import {AlertContext, AlertContextProps} from '@/context/AlertContext';
|
||||
import {SessionContext, SessionContextProps} from "@/context/SessionContext";
|
||||
import DraftCompanion from "@/components/editor/DraftCompanion";
|
||||
import GhostWriter from "@/components/ghostwriter/GhostWriter";
|
||||
import SubmitButtonWLoading from "@/components/form/SubmitButtonWLoading";
|
||||
import CollapsableButton from "@/components/CollapsableButton";
|
||||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
|
||||
import IconButton from "@/components/ui/IconButton";
|
||||
import UserEditorSettings, {EditorDisplaySettings} from "@/components/editor/UserEditorSetting";
|
||||
import {useTranslations} from "next-intl";
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import {LangContext, LangContextProps} from "@/context/LangContext";
|
||||
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
|
||||
import {LocalSyncQueueContext, LocalSyncQueueContextProps} from "@/context/SyncQueueContext";
|
||||
import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
|
||||
import {SyncedBook} from "@/lib/models/SyncedBook";
|
||||
import * as tauri from '@/lib/tauri';
|
||||
|
||||
interface ToolbarButton {
|
||||
action: () => void;
|
||||
icon: IconDefinition;
|
||||
icon: LucideIcon;
|
||||
isActive: boolean;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
interface EditorClasses {
|
||||
@@ -57,7 +53,7 @@ interface EditorClasses {
|
||||
listItems: string;
|
||||
}
|
||||
|
||||
const DEFAULT_EDITOR_SETTINGS: EditorDisplaySettings = {
|
||||
const defaultEditorSettings: EditorDisplaySettings = {
|
||||
zoomLevel: 3,
|
||||
indent: 30,
|
||||
lineHeight: 1.5,
|
||||
@@ -67,53 +63,53 @@ const DEFAULT_EDITOR_SETTINGS: EditorDisplaySettings = {
|
||||
focusMode: false
|
||||
};
|
||||
|
||||
const FONT_SIZE_CLASSES = {
|
||||
const fontSizeClasses: Record<number, string> = {
|
||||
1: 'text-sm',
|
||||
2: 'text-base',
|
||||
3: 'text-lg',
|
||||
4: 'text-xl',
|
||||
5: 'text-2xl'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const H1_SIZE_CLASSES = {
|
||||
const h1SizeClasses: Record<number, string> = {
|
||||
1: 'text-xl',
|
||||
2: 'text-2xl',
|
||||
3: 'text-3xl',
|
||||
4: 'text-4xl',
|
||||
5: 'text-5xl'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const H2_SIZE_CLASSES = {
|
||||
const h2SizeClasses: Record<number, string> = {
|
||||
1: 'text-lg',
|
||||
2: 'text-xl',
|
||||
3: 'text-2xl',
|
||||
4: 'text-3xl',
|
||||
5: 'text-4xl'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const H3_SIZE_CLASSES = {
|
||||
const h3SizeClasses: Record<number, string> = {
|
||||
1: 'text-base',
|
||||
2: 'text-lg',
|
||||
3: 'text-xl',
|
||||
4: 'text-2xl',
|
||||
5: 'text-3xl'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const FONT_FAMILY_CLASSES = {
|
||||
const fontFamilyClasses: Record<string, string> = {
|
||||
'lora': 'Lora',
|
||||
'serif': 'font-serif',
|
||||
'sans-serif': 'font-sans',
|
||||
'monospace': 'font-mono'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const LINE_HEIGHT_CLASSES = {
|
||||
const lineHeightClasses: Record<number, string> = {
|
||||
1.2: 'leading-tight',
|
||||
1.5: 'leading-normal',
|
||||
1.75: 'leading-relaxed',
|
||||
2: 'leading-loose'
|
||||
} as const;
|
||||
};
|
||||
|
||||
const MAX_WIDTH_CLASSES = {
|
||||
const maxWidthClasses: Record<number, string> = {
|
||||
600: 'max-w-xl',
|
||||
650: 'max-w-2xl',
|
||||
700: 'max-w-3xl',
|
||||
@@ -127,9 +123,9 @@ const MAX_WIDTH_CLASSES = {
|
||||
1100: 'max-w-full',
|
||||
1150: 'max-w-full',
|
||||
1200: 'max-w-full'
|
||||
} as const;
|
||||
};
|
||||
|
||||
function getClosestKey<T extends Record<number, any>>(value: number, obj: T): keyof T {
|
||||
function getClosestKey(value: number, obj: Record<number, string>): number {
|
||||
const keys: number[] = Object.keys(obj).map(Number).sort((a: number, b: number): number => a - b);
|
||||
return keys.reduce((prev: number, curr: number): number =>
|
||||
Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev
|
||||
@@ -138,30 +134,27 @@ function getClosestKey<T extends Record<number, any>>(value: number, obj: T): ke
|
||||
|
||||
export default function TextEditor() {
|
||||
const t = useTranslations();
|
||||
const {lang} = useContext<LangContextProps>(LangContext)
|
||||
const {editor} = useContext(EditorContext);
|
||||
const {chapter, setChapter} = useContext(ChapterContext);
|
||||
const {book, setBook} = useContext(BookContext);
|
||||
const {errorMessage, successMessage} = useContext(AlertContext);
|
||||
const {session} = useContext(SessionContext);
|
||||
const {isCurrentlyOffline} = useContext<OfflineContextType>(OfflineContext);
|
||||
const {addToQueue} = useContext<LocalSyncQueueContextProps>(LocalSyncQueueContext);
|
||||
const {localSyncedBooks} = useContext<BooksSyncContextProps>(BooksSyncContext);
|
||||
|
||||
const {lang}: LangContextProps = useContext<LangContextProps>(LangContext)
|
||||
const {editor}: EditorContextProps = useContext<EditorContextProps>(EditorContext);
|
||||
const {chapter}: ChapterContextProps = useContext<ChapterContextProps>(ChapterContext);
|
||||
const {book}: BookContextProps = useContext<BookContextProps>(BookContext);
|
||||
const {errorMessage, successMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
const {session}: SessionContextProps = useContext<SessionContextProps>(SessionContext);
|
||||
const {isCurrentlyOffline}: OfflineContextType = useContext<OfflineContextType>(OfflineContext);
|
||||
|
||||
const [mainTimer, setMainTimer] = useState<number>(0);
|
||||
const [showDraftCompanion, setShowDraftCompanion] = useState<boolean>(false);
|
||||
const [showGhostWriter, setShowGhostWriter] = useState<boolean>(false);
|
||||
const [showUserSettings, setShowUserSettings] = useState<boolean>(false);
|
||||
const [isSaving, setIsSaving] = useState<boolean>(false);
|
||||
const [isClosing, setIsClosing] = useState<boolean>(false);
|
||||
const [editorSettings, setEditorSettings] = useState<EditorDisplaySettings>(DEFAULT_EDITOR_SETTINGS);
|
||||
const [editorSettings, setEditorSettings] = useState<EditorDisplaySettings>(defaultEditorSettings);
|
||||
const [editorClasses, setEditorClasses] = useState<EditorClasses>({
|
||||
base: 'text-lg font-serif leading-normal',
|
||||
h1: 'text-3xl font-bold',
|
||||
h2: 'text-2xl font-bold',
|
||||
h3: 'text-xl font-bold',
|
||||
container: 'max-w-3xl',
|
||||
theme: 'bg-tertiary/90 backdrop-blur-sm bg-opacity-25 text-text-primary',
|
||||
theme: 'bg-tertiary text-text-primary',
|
||||
paragraph: 'indent-6',
|
||||
lists: 'pl-10',
|
||||
listItems: 'text-lg'
|
||||
@@ -171,33 +164,31 @@ export default function TextEditor() {
|
||||
const timeoutRef: React.RefObject<number | null> = useRef<number | null>(null);
|
||||
|
||||
const updateEditorClasses: (settings: EditorDisplaySettings) => void = useCallback((settings: EditorDisplaySettings): void => {
|
||||
const fontSizeKey = settings.zoomLevel as keyof typeof FONT_SIZE_CLASSES;
|
||||
const h1SizeKey = settings.zoomLevel as keyof typeof H1_SIZE_CLASSES;
|
||||
const h2SizeKey = settings.zoomLevel as keyof typeof H2_SIZE_CLASSES;
|
||||
const h3SizeKey = settings.zoomLevel as keyof typeof H3_SIZE_CLASSES;
|
||||
const fontFamilyKey = settings.fontFamily as keyof typeof FONT_FAMILY_CLASSES;
|
||||
const lineHeightKey = settings.lineHeight as keyof typeof LINE_HEIGHT_CLASSES;
|
||||
const maxWidthKey: number = getClosestKey(settings.maxWidth, MAX_WIDTH_CLASSES);
|
||||
const zoomKey: number = getClosestKey(settings.zoomLevel, fontSizeClasses);
|
||||
const lineHeightKey: number = getClosestKey(settings.lineHeight, lineHeightClasses);
|
||||
const maxWidthKey: number = getClosestKey(settings.maxWidth, maxWidthClasses);
|
||||
|
||||
const indentClass = `indent-${Math.round(settings.indent / 4)}`;
|
||||
const fontFamily: string = fontFamilyClasses[settings.fontFamily] || fontFamilyClasses['lora'];
|
||||
const lineHeight: string = lineHeightClasses[lineHeightKey];
|
||||
const indentClass: string = `indent-${Math.round(settings.indent / 4)}`;
|
||||
|
||||
const baseClass = `${FONT_SIZE_CLASSES[fontSizeKey]} ${FONT_FAMILY_CLASSES[fontFamilyKey]} ${LINE_HEIGHT_CLASSES[lineHeightKey]}`;
|
||||
const h1Class = `${H1_SIZE_CLASSES[h1SizeKey]} font-bold ${FONT_FAMILY_CLASSES[fontFamilyKey]} ${LINE_HEIGHT_CLASSES[lineHeightKey]}`;
|
||||
const h2Class = `${H2_SIZE_CLASSES[h2SizeKey]} font-bold ${FONT_FAMILY_CLASSES[fontFamilyKey]} ${LINE_HEIGHT_CLASSES[lineHeightKey]}`;
|
||||
const h3Class = `${H3_SIZE_CLASSES[h3SizeKey]} font-bold ${FONT_FAMILY_CLASSES[fontFamilyKey]} ${LINE_HEIGHT_CLASSES[lineHeightKey]}`;
|
||||
const containerClass = MAX_WIDTH_CLASSES[maxWidthKey as keyof typeof MAX_WIDTH_CLASSES];
|
||||
const listsClass = `pl-${Math.round((settings.indent + 20) / 4)}`;
|
||||
const baseClass: string = `${fontSizeClasses[zoomKey]} ${fontFamily} ${lineHeight}`;
|
||||
const h1Class: string = `${h1SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const h2Class: string = `${h2SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const h3Class: string = `${h3SizeClasses[zoomKey]} font-bold ${fontFamily} ${lineHeight}`;
|
||||
const containerClass: string = maxWidthClasses[maxWidthKey];
|
||||
const listsClass: string = `pl-${Math.round((settings.indent + 20) / 4)}`;
|
||||
|
||||
let themeClass: string = '';
|
||||
switch (settings.theme) {
|
||||
case 'clair':
|
||||
themeClass = 'bg-white text-black';
|
||||
themeClass = 'bg-gray-light text-darkest-background';
|
||||
break;
|
||||
case 'sépia':
|
||||
themeClass = 'text-amber-900';
|
||||
themeClass = 'bg-editor-page-sepia text-darkest-background';
|
||||
break;
|
||||
default:
|
||||
themeClass = 'bg-tertiary/90 backdrop-blur-sm bg-opacity-25 text-text-primary';
|
||||
themeClass = 'bg-tertiary text-text-primary';
|
||||
}
|
||||
|
||||
setEditorClasses({
|
||||
@@ -213,109 +204,99 @@ export default function TextEditor() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
const containerStyle = useMemo(() => {
|
||||
if (editorSettings.theme === 'sépia') {
|
||||
return {backgroundColor: '#f4f1e8'};
|
||||
}
|
||||
return {};
|
||||
}, [editorSettings.theme]);
|
||||
const editorContainerRef: React.RefObject<HTMLDivElement | null> = useRef<HTMLDivElement>(null);
|
||||
|
||||
const toolbarButtons: ToolbarButton[] = (() => {
|
||||
const editorContainerBgClass: string =
|
||||
editorSettings.theme === 'sépia' ? 'bg-editor-page-sepia' : '';
|
||||
|
||||
const toolbarButtons: ToolbarButton[] = ((): ToolbarButton[] => {
|
||||
if (!editor) return [];
|
||||
|
||||
return [
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().setParagraph().run(),
|
||||
icon: faParagraph,
|
||||
icon: AlignJustify,
|
||||
isActive: editor.isActive('paragraph')
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleBold().run(),
|
||||
icon: faBold,
|
||||
icon: Bold,
|
||||
isActive: editor.isActive('bold')
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleUnderline().run(),
|
||||
icon: faUnderline,
|
||||
icon: Underline,
|
||||
isActive: editor.isActive('underline')
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().setTextAlign('left').run(),
|
||||
icon: faAlignLeft,
|
||||
icon: AlignLeft,
|
||||
isActive: editor.isActive({textAlign: 'left'})
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().setTextAlign('center').run(),
|
||||
icon: faAlignCenter,
|
||||
icon: AlignCenter,
|
||||
isActive: editor.isActive({textAlign: 'center'})
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().setTextAlign('right').run(),
|
||||
icon: faAlignRight,
|
||||
icon: AlignRight,
|
||||
isActive: editor.isActive({textAlign: 'right'})
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleBulletList().run(),
|
||||
icon: faListUl,
|
||||
icon: List,
|
||||
isActive: editor.isActive('bulletList')
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleOrderedList().run(),
|
||||
icon: faListOl,
|
||||
icon: ListOrdered,
|
||||
isActive: editor.isActive('orderedList')
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleHeading({level: 1}).run(),
|
||||
icon: faHeading,
|
||||
isActive: editor.isActive('heading', {level: 1}),
|
||||
label: '1'
|
||||
icon: Heading1,
|
||||
isActive: editor.isActive('heading', {level: 1})
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleHeading({level: 2}).run(),
|
||||
icon: faHeading,
|
||||
isActive: editor.isActive('heading', {level: 2}),
|
||||
label: '2'
|
||||
icon: Heading2,
|
||||
isActive: editor.isActive('heading', {level: 2})
|
||||
},
|
||||
{
|
||||
action: (): boolean => editor.chain().focus().toggleHeading({level: 3}).run(),
|
||||
icon: faHeading,
|
||||
isActive: editor.isActive('heading', {level: 3}),
|
||||
label: '3'
|
||||
icon: Heading3,
|
||||
isActive: editor.isActive('heading', {level: 3})
|
||||
},
|
||||
];
|
||||
})();
|
||||
|
||||
const saveContent: () => Promise<void> = useCallback(async (): Promise<void> => {
|
||||
if (!editor || !chapter) return;
|
||||
|
||||
|
||||
setIsSaving(true);
|
||||
const content = editor.state.doc.toJSON();
|
||||
const content: Record<string, unknown> = editor.state.doc.toJSON();
|
||||
const chapterId: string = chapter.chapterId || '';
|
||||
const version: number = chapter.chapterContent.version || 0;
|
||||
|
||||
|
||||
try {
|
||||
let response: boolean;
|
||||
const saveData = {
|
||||
chapterId,
|
||||
version,
|
||||
content,
|
||||
totalWordCount: editor.getText().length,
|
||||
currentTime: mainTimer
|
||||
};
|
||||
if (isCurrentlyOffline() || book?.localBook){
|
||||
if (isDesktop && (isCurrentlyOffline() || book?.localBook)) {
|
||||
response = await tauri.saveChapterContent({
|
||||
chapterId: saveData.chapterId,
|
||||
version: saveData.version,
|
||||
content: saveData.content,
|
||||
totalWordCount: saveData.totalWordCount,
|
||||
contentId: saveData.chapterId,
|
||||
chapterId,
|
||||
version,
|
||||
content,
|
||||
totalWordCount: editor.getText().length,
|
||||
contentId: '',
|
||||
});
|
||||
} else {
|
||||
response = await System.authPostToServer<boolean>(`chapter/content`, saveData, session?.accessToken, lang);
|
||||
|
||||
if (localSyncedBooks.find((syncedBook: SyncedBook): boolean => syncedBook.id === book?.bookId)) {
|
||||
addToQueue('save_chapter_content', {data: saveData});
|
||||
}
|
||||
response = await apiPost<boolean>(`chapter/content`, {
|
||||
chapterId,
|
||||
version,
|
||||
content,
|
||||
totalWordCount: editor.getText().length,
|
||||
currentTime: mainTimer
|
||||
}, session?.accessToken ?? '');
|
||||
}
|
||||
if (!response) {
|
||||
errorMessage(t('editor.error.savedFailed'));
|
||||
@@ -333,7 +314,7 @@ export default function TextEditor() {
|
||||
}
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage, addToQueue, book?.localBook, isCurrentlyOffline]);
|
||||
}, [editor, chapter, mainTimer, session?.accessToken, successMessage, errorMessage]);
|
||||
|
||||
const handleShowDraftCompanion: () => void = useCallback((): void => {
|
||||
setShowDraftCompanion((prev: boolean): boolean => !prev);
|
||||
@@ -354,21 +335,13 @@ export default function TextEditor() {
|
||||
setShowDraftCompanion(false);
|
||||
setShowGhostWriter(false);
|
||||
}, []);
|
||||
|
||||
const handleCloseBook: () => Promise<void> = useCallback(async (): Promise<void> => {
|
||||
setIsClosing(true);
|
||||
await saveContent();
|
||||
setBook && setBook(null);
|
||||
setChapter && setChapter(undefined);
|
||||
setIsClosing(false);
|
||||
}, [saveContent, setBook, setChapter]);
|
||||
|
||||
useEffect((): void => {
|
||||
if (!editor) return;
|
||||
|
||||
const editorElement: HTMLElement = editor.view.dom;
|
||||
if (editorElement) {
|
||||
const indentClasses: string[] = Array.from({length: 21}, (_, i) => `indent-${i}`);
|
||||
const indentClasses: string[] = Array.from({length: 21}, (_: unknown, i: number): string => `indent-${i}`);
|
||||
editorElement.classList.remove(...indentClasses);
|
||||
|
||||
if (editorClasses.paragraph) {
|
||||
@@ -384,8 +357,8 @@ export default function TextEditor() {
|
||||
useEffect((): () => void => {
|
||||
function startTimer(): void {
|
||||
if (timerRef.current === null) {
|
||||
timerRef.current = window.setInterval(() => {
|
||||
setMainTimer(prevTimer => prevTimer + 1);
|
||||
timerRef.current = window.setInterval((): void => {
|
||||
setMainTimer((prevTimer: number): number => prevTimer + 1);
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
@@ -431,9 +404,10 @@ export default function TextEditor() {
|
||||
if (!editor) return;
|
||||
if (chapter?.chapterContent.content) {
|
||||
try {
|
||||
const parsedContent = JSON.parse(chapter.chapterContent.content);
|
||||
const parsedContent: JSONContent = JSON.parse(chapter.chapterContent.content);
|
||||
editor.commands.setContent(parsedContent);
|
||||
} catch (error) {
|
||||
} catch (e: unknown) {
|
||||
errorMessage(t('editor.error.parsingContent'));
|
||||
editor.commands.setContent({
|
||||
type: "doc",
|
||||
content: [{type: "paragraph", content: []}]
|
||||
@@ -463,102 +437,86 @@ export default function TextEditor() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-1 w-full h-full">
|
||||
<div className="flex flex-col flex-1 w-full h-full bg-tertiary">
|
||||
<div
|
||||
className={`flex justify-between gap-2 lg:gap-3 border-b border-secondary/30 px-2 lg:px-4 py-2 lg:py-3 bg-gradient-to-b from-dark-background/80 to-dark-background/50 backdrop-blur-sm transition-opacity duration-300 shadow-md overflow-x-auto ${editorSettings.focusMode ? 'opacity-70 hover:opacity-100' : ''}`}>
|
||||
<div className="flex gap-1 flex-shrink-0">
|
||||
className={`flex justify-between items-center gap-3 rounded-xl mx-1 mb-1 px-4 py-2 bg-darkest-background transition-opacity duration-300 ${editorSettings.focusMode ? 'opacity-70 hover:opacity-100' : ''}`}>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{toolbarButtons.map((button: ToolbarButton, index: number) => (
|
||||
<button
|
||||
<IconButton
|
||||
key={index}
|
||||
icon={button.icon}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
selected={button.isActive}
|
||||
onClick={button.action}
|
||||
className={`group flex items-center px-2 lg:px-3 py-1.5 lg:py-2 rounded-lg transition-all duration-200 flex-shrink-0 ${button.isActive ? 'bg-primary text-text-primary shadow-md shadow-primary/30 scale-105' : 'text-muted hover:text-text-primary hover:bg-secondary/50 hover:shadow-sm hover:scale-105'}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={button.icon} className={'w-3.5 h-3.5 lg:w-4 lg:h-4 transition-transform duration-200 group-hover:scale-110'}/>
|
||||
{
|
||||
button.label &&
|
||||
<span className="ml-1 lg:ml-2 text-xs lg:text-sm font-medium">
|
||||
{t(`textEditor.toolbar.${button.label}`)}
|
||||
</span>
|
||||
}
|
||||
</button>
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-shrink-0">
|
||||
<CollapsableButton
|
||||
showCollapsable={showUserSettings}
|
||||
text={t("textEditor.preferences")}
|
||||
<div className="flex items-center gap-1">
|
||||
<IconButton
|
||||
icon={SlidersHorizontal}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
selected={showUserSettings}
|
||||
onClick={handleShowUserSettings}
|
||||
icon={faCog}
|
||||
tooltip={t("textEditor.preferences")}
|
||||
/>
|
||||
{chapter?.chapterContent.version === 2 && !isCurrentlyOffline() && !book?.localBook && book?.quillsenseEnabled !== false && (
|
||||
<CollapsableButton
|
||||
showCollapsable={showGhostWriter}
|
||||
text={t("textEditor.ghostWriter")}
|
||||
{chapter?.chapterContent.version === 2 && book?.quillsenseEnabled !== false && (
|
||||
<IconButton
|
||||
icon={Ghost}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
selected={showGhostWriter}
|
||||
onClick={handleShowGhostWriter}
|
||||
icon={faGhost}
|
||||
tooltip={t("textEditor.ghostWriter")}
|
||||
/>
|
||||
)}
|
||||
{chapter?.chapterContent.version && chapter.chapterContent.version > 2 && (
|
||||
<CollapsableButton
|
||||
showCollapsable={showDraftCompanion}
|
||||
text={t("textEditor.draftCompanion")}
|
||||
<IconButton
|
||||
icon={Layers}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
selected={showDraftCompanion}
|
||||
onClick={handleShowDraftCompanion}
|
||||
icon={faLayerGroup}
|
||||
tooltip={t("textEditor.draftCompanion")}
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
<IconButton
|
||||
icon={Save}
|
||||
variant="ghost"
|
||||
shape="square"
|
||||
onClick={saveContent}
|
||||
disabled={isSaving}
|
||||
className={`group py-2.5 px-3 lg:px-5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 relative overflow-hidden ${
|
||||
isSaving
|
||||
? 'bg-secondary cursor-not-allowed opacity-75'
|
||||
: 'bg-secondary/80 hover:bg-secondary shadow-md hover:shadow-lg hover:shadow-primary/20 hover:scale-105 border border-secondary/50 hover:border-primary/30'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFloppyDisk} className="w-4 h-4 transition-transform group-hover:scale-110 text-primary" />
|
||||
<span className="hidden lg:inline text-sm text-primary">{isSaving ? t("textEditor.saving") : t("textEditor.save")}</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCloseBook}
|
||||
disabled={isClosing}
|
||||
className={`group py-2.5 px-3 lg:px-5 rounded-lg font-semibold transition-all flex items-center justify-center gap-2 relative overflow-hidden ${
|
||||
isClosing
|
||||
? 'bg-secondary/30 cursor-not-allowed opacity-75'
|
||||
: 'bg-secondary/30 text-muted hover:text-text-primary hover:bg-secondary hover:shadow-sm hover:scale-105'
|
||||
}`}
|
||||
>
|
||||
<FontAwesomeIcon icon={faXmark} className="w-4 h-4 transition-transform duration-200 group-hover:scale-110" />
|
||||
<span className="hidden lg:inline text-sm">{t("textEditor.close")}</span>
|
||||
</button>
|
||||
tooltip={t("textEditor.save")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between w-full h-full overflow-auto">
|
||||
|
||||
<div className="flex flex-1 min-h-0 bg-tertiary">
|
||||
<div
|
||||
className={`flex-1 p-8 overflow-auto transition-all duration-300 ${editorSettings.focusMode ? 'bg-black/20' : ''}`}>
|
||||
<div
|
||||
className={`editor-container mx-auto p-6 rounded-2xl shadow-2xl min-h-[80%] border border-secondary/50 ${editorClasses.container} ${editorClasses.theme} relative`}
|
||||
style={containerStyle}>
|
||||
className={`flex-1 bg-background rounded-xl mx-1 overflow-hidden transition-all duration-300 ${editorSettings.focusMode ? 'bg-darkest-background' : ''}`}>
|
||||
<div className="h-full overflow-auto p-4">
|
||||
<div
|
||||
className="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent"></div>
|
||||
<div
|
||||
className="absolute bottom-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-primary/30 to-transparent"></div>
|
||||
<EditorContent className={`w-full h-full ${editorClasses.base} editor-content`}
|
||||
editor={editor}/>
|
||||
ref={editorContainerRef}
|
||||
className={`editor-container mx-auto p-4 rounded-xl min-h-full ${editorClasses.container} ${editorClasses.theme} ${editorContainerBgClass}`}>
|
||||
<EditorContent className={`w-full h-full ${editorClasses.base} editor-content`}
|
||||
editor={editor}/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{(showDraftCompanion || showGhostWriter || showUserSettings) && (
|
||||
<div
|
||||
className={`w-4/12 transition-opacity duration-300 ${editorSettings.focusMode ? 'opacity-50 hover:opacity-100' : ''}`}>
|
||||
{showDraftCompanion && <DraftCompanion/>}
|
||||
{showGhostWriter && <GhostWriter/>}
|
||||
{showUserSettings && (
|
||||
<UserEditorSettings
|
||||
settings={editorSettings}
|
||||
onSettingsChange={setEditorSettings}
|
||||
/>
|
||||
)}
|
||||
className={`w-4/12 bg-darkest-background rounded-xl mx-1 overflow-hidden flex flex-col transition-opacity duration-300 ${editorSettings.focusMode ? 'opacity-50 hover:opacity-100' : ''}`}>
|
||||
<div className="flex-1 overflow-auto flex flex-col">
|
||||
{showDraftCompanion && <DraftCompanion/>}
|
||||
{showGhostWriter && <GhostWriter/>}
|
||||
{showUserSettings && (
|
||||
<UserEditorSettings
|
||||
settings={editorSettings}
|
||||
onSettingsChange={setEditorSettings}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
'use client'
|
||||
import {ChangeEvent, useCallback, useEffect, useMemo} from 'react';
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
|
||||
import {faEye, faFont, faIndent, faPalette, faTextHeight, faTextWidth} from '@fortawesome/free-solid-svg-icons';
|
||||
import {useTranslations} from "next-intl";
|
||||
import React, {ChangeEvent, useCallback, useContext, useEffect, useMemo} from 'react';
|
||||
import {Baseline, CaseSensitive, Eye, Indent, Palette, Type} from 'lucide-react';
|
||||
import {useTranslations} from '@/lib/i18n';
|
||||
import SelectBox from "@/components/form/SelectBox";
|
||||
import Button from "@/components/ui/Button";
|
||||
import {AlertContext, AlertContextProps} from "@/context/AlertContext";
|
||||
|
||||
interface UserEditorSettingsProps {
|
||||
settings: EditorDisplaySettings;
|
||||
@@ -20,11 +21,15 @@ export interface EditorDisplaySettings {
|
||||
focusMode: boolean;
|
||||
}
|
||||
|
||||
const ZOOM_LABELS = ['Très petit', 'Petit', 'Normal', 'Grand', 'Très grand'] as const;
|
||||
const FONT_SIZES = [14, 16, 18, 20, 22] as const;
|
||||
const THEMES = ['clair', 'sombre', 'sépia'] as const;
|
||||
const zoomLabels = ['Très petit', 'Petit', 'Normal', 'Grand', 'Très grand'] as const;
|
||||
const fontSizes = [14, 16, 18, 20, 22] as const;
|
||||
const themes = ['clair', 'sombre', 'sépia'] as const;
|
||||
|
||||
const DEFAULT_SETTINGS: EditorDisplaySettings = {
|
||||
function isValidFontFamily(value: string): value is EditorDisplaySettings['fontFamily'] {
|
||||
return value === 'lora' || value === 'serif' || value === 'sans-serif' || value === 'monospace';
|
||||
}
|
||||
|
||||
const defaultSettings: EditorDisplaySettings = {
|
||||
zoomLevel: 3,
|
||||
indent: 30,
|
||||
lineHeight: 1.5,
|
||||
@@ -34,82 +39,91 @@ const DEFAULT_SETTINGS: EditorDisplaySettings = {
|
||||
focusMode: false
|
||||
};
|
||||
|
||||
export default function UserEditorSettings({settings, onSettingsChange}: UserEditorSettingsProps) {
|
||||
export default function UserEditorSettings({settings, onSettingsChange}: UserEditorSettingsProps): React.JSX.Element {
|
||||
const t = useTranslations();
|
||||
|
||||
const {errorMessage}: AlertContextProps = useContext<AlertContextProps>(AlertContext);
|
||||
|
||||
const handleSettingChange = useCallback(<K extends keyof EditorDisplaySettings>(
|
||||
key: K,
|
||||
value: EditorDisplaySettings[K]
|
||||
) => {
|
||||
): void => {
|
||||
onSettingsChange({...settings, [key]: value});
|
||||
}, [settings, onSettingsChange]);
|
||||
|
||||
const resetToDefaults = useCallback(() => {
|
||||
onSettingsChange(DEFAULT_SETTINGS);
|
||||
const resetToDefaults = useCallback((): void => {
|
||||
onSettingsChange(defaultSettings);
|
||||
}, [onSettingsChange]);
|
||||
|
||||
const zoomOptions = useMemo(() =>
|
||||
ZOOM_LABELS.map((label, index) => ({
|
||||
const zoomOptions = useMemo((): { value: string; label: string }[] =>
|
||||
zoomLabels.map((label: typeof zoomLabels[number], index: number): { value: string; label: string } => ({
|
||||
value: (index + 1).toString(),
|
||||
label: `${t(`userEditorSettings.zoom.${label}`)} (${FONT_SIZES[index]}px)`
|
||||
label: `${t(`userEditorSettings.zoom.${label}`)} (${fontSizes[index]}px)`
|
||||
}))
|
||||
, [t]);
|
||||
|
||||
const themeButtons = useMemo(() =>
|
||||
THEMES.map(theme => ({
|
||||
const themeButtons = useMemo((): { key: typeof themes[number]; isActive: boolean; className: string }[] =>
|
||||
themes.map((theme: typeof themes[number]): {
|
||||
key: typeof themes[number];
|
||||
isActive: boolean;
|
||||
className: string
|
||||
} => ({
|
||||
key: theme,
|
||||
isActive: settings.theme === theme,
|
||||
className: `p-2.5 rounded-xl border capitalize transition-all duration-200 font-medium ${
|
||||
className: `p-2.5 rounded-xl border capitalize transition-colors duration-150 font-medium ${
|
||||
settings.theme === theme
|
||||
? 'bg-primary text-text-primary border-primary shadow-md scale-105'
|
||||
: 'bg-secondary/50 border-secondary/50 text-muted hover:text-text-primary hover:border-secondary hover:bg-secondary hover:scale-102'
|
||||
? 'bg-secondary text-primary border-primary'
|
||||
: 'bg-secondary border-secondary text-muted hover:text-text-primary'
|
||||
}`
|
||||
}))
|
||||
, [settings.theme]);
|
||||
|
||||
|
||||
useEffect((): void => {
|
||||
try {
|
||||
const savedSettings: string | null = localStorage.getItem('userEditorSettings');
|
||||
if (savedSettings) {
|
||||
const parsed = JSON.parse(savedSettings);
|
||||
const parsed: Partial<EditorDisplaySettings> = JSON.parse(savedSettings);
|
||||
if (parsed && typeof parsed === 'object') {
|
||||
onSettingsChange({...DEFAULT_SETTINGS, ...parsed});
|
||||
onSettingsChange({...defaultSettings, ...parsed});
|
||||
}
|
||||
}
|
||||
} catch (e: unknown) {
|
||||
onSettingsChange(DEFAULT_SETTINGS);
|
||||
onSettingsChange(defaultSettings);
|
||||
}
|
||||
}, [onSettingsChange]);
|
||||
|
||||
|
||||
useEffect((): () => void => {
|
||||
const timeoutId = setTimeout((): void => {
|
||||
const timeoutId: ReturnType<typeof setTimeout> = setTimeout((): void => {
|
||||
try {
|
||||
localStorage.setItem('userEditorSettings', JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.error('Erreur lors de la sauvegarde des settings:', error);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorMessage(t('userEditorSettings.saveError'));
|
||||
} else {
|
||||
errorMessage(t('userEditorSettings.unknownError'));
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
|
||||
|
||||
return (): void => clearTimeout(timeoutId);
|
||||
}, [settings]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="p-5 bg-secondary/30 backdrop-blur-sm border-l border-secondary/50 h-full overflow-y-auto shadow-inner">
|
||||
<div className="flex items-center gap-3 mb-8 pb-4 border-b border-secondary/50">
|
||||
<FontAwesomeIcon icon={faEye} className="text-primary w-6 h-6"/>
|
||||
className="p-5 h-full overflow-y-auto">
|
||||
<div className="flex items-center gap-3 mb-8 pb-4 border-b border-secondary">
|
||||
<Eye className="text-primary w-6 h-6" strokeWidth={1.75}/>
|
||||
<h3 className="text-xl font-['ADLaM_Display'] text-text-primary">{t("userEditorSettings.displayPreferences")}</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faTextHeight} className="text-muted w-5 h-5"/>
|
||||
<CaseSensitive className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.textSize")}
|
||||
</label>
|
||||
<SelectBox
|
||||
defaultValue={settings.zoomLevel.toString()}
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>) => {
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => {
|
||||
handleSettingChange('zoomLevel', Number(e.target.value))
|
||||
}}
|
||||
data={zoomOptions}
|
||||
@@ -118,7 +132,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faIndent} className="text-muted w-5 h-5"/>
|
||||
<Indent className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.indent")}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
@@ -128,7 +142,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
max={50}
|
||||
step={5}
|
||||
value={settings.indent}
|
||||
onChange={(e) => handleSettingChange('indent', Number(e.target.value))}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('indent', Number(e.target.value))}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted">
|
||||
@@ -141,12 +155,12 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faTextWidth} className="text-muted w-5 h-5"/>
|
||||
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.lineHeight")}
|
||||
</label>
|
||||
<SelectBox
|
||||
defaultValue={settings.lineHeight.toString()}
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>) => handleSettingChange('lineHeight', Number(e.target.value))}
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => handleSettingChange('lineHeight', Number(e.target.value))}
|
||||
data={[
|
||||
{value: "1.2", label: t("userEditorSettings.lineHeightCompact")},
|
||||
{value: "1.5", label: t("userEditorSettings.lineHeightNormal")},
|
||||
@@ -158,12 +172,17 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faFont} className="text-muted w-5 h-5"/>
|
||||
<Type className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.fontFamily")}
|
||||
</label>
|
||||
<SelectBox
|
||||
defaultValue={settings.fontFamily}
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>) => handleSettingChange('fontFamily', e.target.value as EditorDisplaySettings['fontFamily'])}
|
||||
onChangeCallBack={(e: ChangeEvent<HTMLSelectElement>): void => {
|
||||
const fontValue: string = e.target.value;
|
||||
if (isValidFontFamily(fontValue)) {
|
||||
handleSettingChange('fontFamily', fontValue);
|
||||
}
|
||||
}}
|
||||
data={[
|
||||
{value: "lora", label: t("userEditorSettings.fontLora")},
|
||||
{value: "serif", label: t("userEditorSettings.fontSerif")},
|
||||
@@ -175,7 +194,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faTextWidth} className="text-muted w-5 h-5"/>
|
||||
<Baseline className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.maxWidth")}
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
@@ -185,7 +204,7 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
max={1200}
|
||||
step={50}
|
||||
value={settings.maxWidth}
|
||||
onChange={(e) => handleSettingChange('maxWidth', Number(e.target.value))}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('maxWidth', Number(e.target.value))}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
<div className="flex justify-between text-sm text-muted">
|
||||
@@ -198,14 +217,18 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
|
||||
<div>
|
||||
<label className="flex items-center gap-2 mb-2 text-text-primary">
|
||||
<FontAwesomeIcon icon={faPalette} className="text-muted w-5 h-5"/>
|
||||
<Palette className="text-muted w-5 h-5" strokeWidth={1.75}/>
|
||||
{t("userEditorSettings.theme")}
|
||||
</label>
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
{themeButtons.map((themeBtn) => (
|
||||
{themeButtons.map((themeBtn: {
|
||||
key: typeof themes[number];
|
||||
isActive: boolean;
|
||||
className: string
|
||||
}) => (
|
||||
<button
|
||||
key={themeBtn.key}
|
||||
onClick={() => handleSettingChange('theme', themeBtn.key)}
|
||||
onClick={(): void => handleSettingChange('theme', themeBtn.key)}
|
||||
className={themeBtn.className}
|
||||
>
|
||||
{t(`userEditorSettings.themeOption.${themeBtn.key}`)}
|
||||
@@ -219,20 +242,17 @@ export default function UserEditorSettings({settings, onSettingsChange}: UserEdi
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={settings.focusMode}
|
||||
onChange={(e) => handleSettingChange('focusMode', e.target.checked)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>): void => handleSettingChange('focusMode', e.target.checked)}
|
||||
className="w-4 h-4 accent-primary"
|
||||
/>
|
||||
<span className="text-text-primary">{t("userEditorSettings.focusMode")}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="pt-6 border-t border-secondary/50">
|
||||
<button
|
||||
onClick={resetToDefaults}
|
||||
className="w-full py-2.5 bg-secondary/50 border border-secondary/50 rounded-xl text-muted hover:text-text-primary hover:border-secondary hover:bg-secondary transition-all duration-200 hover:scale-105 shadow-sm hover:shadow-md font-medium"
|
||||
>
|
||||
<div className="pt-6 border-t border-secondary">
|
||||
<Button variant="secondary" onClick={resetToDefaults} fullWidth>
|
||||
{t("userEditorSettings.reset")}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user