diff --git a/app/globals.css b/app/globals.css
index 8b09ce4..84ff6ef 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -1,30 +1,178 @@
@import "tailwindcss";
+/*
+ * === ERitors Scribe Design Tokens ===
+ *
+ * Theme values are defined in :root and overridable via [data-theme].
+ * The @theme block references these variables so Tailwind utilities
+ * (bg-primary, text-muted, etc.) automatically follow the active theme.
+ *
+ * COLORS:
+ * Brand ........... primary, primary-dark, primary-light
+ * Surfaces ........ background, dark-background, darkest-background, secondary, tertiary
+ * Semantic surfaces surface-primary, surface-secondary, surface-elevated
+ * Text ............ text-primary, text-secondary, muted, text-dimmed
+ * Semantic ........ success, error, warning, info
+ * Editor .......... editor-text, editor-bold, editor-heading, editor-page-sepia
+ * Scrollbar ....... scrollbar-track, scrollbar-thumb, scrollbar-thumb-hover
+ * Utility ......... glass-bg, surface-gradient-dark, surface-gradient-light
+ * Accents ......... accent-blue, accent-green, accent-orange, accent-purple, accent-red
+ * Interactive ..... interactive, interactive-hover
+ *
+ * SHADOWS: shadow-feature-hover, shadow-focus-ring
+ * FONTS: font-family-lora, font-family-lora-italic, font-family-adlam
+ */
+
+:root, :root[data-theme="dark"] {
+ /* Brand */
+ --theme-primary: #51AE84;
+ --theme-primary-dark: #3A8B69;
+ --theme-primary-light: #74C9A0;
+
+ /* Surfaces */
+ --theme-secondary: #393B40;
+ --theme-tertiary: #26272B;
+ --theme-background: #1E1E22;
+ --theme-dark-background: #1B1C1F;
+ --theme-darkest-background: #191A1C;
+ --theme-surface-gradient-dark: #313337;
+ --theme-surface-gradient-light: #45474D;
+ --theme-glass-bg: rgba(43, 45, 48, 0.7);
+
+ /* Text */
+ --theme-text-primary: #F0F0F4;
+ --theme-text-secondary: #BCBEC4;
+ --theme-muted: #B0B0B0;
+ --theme-text-dimmed: #6F737A;
+
+ /* Semantic */
+ --theme-success: #28A745;
+ --theme-error: #DC3545;
+ --theme-warning: #FFC107;
+ --theme-info: #17A2B8;
+
+ /* Gray scale */
+ --theme-gray: #808388;
+ --theme-gray-light: #A0A3A8;
+ --theme-gray-dark: #45474D;
+
+ /* Scrollbar */
+ --theme-scrollbar-track: #1E1F22;
+ --theme-scrollbar-thumb: #51AE84;
+ --theme-scrollbar-thumb-hover: #3A8B69;
+
+ /* Editor content */
+ --theme-editor-text: #E0E0E4;
+ --theme-editor-bold: #F0F0F4;
+ --theme-editor-heading: #34acd0;
+ --theme-editor-page-sepia: #f4f1e8;
+
+ /* Accents */
+ --theme-accent-blue: #5C9CE6;
+ --theme-accent-green: #5FB87B;
+ --theme-accent-orange: #D4923A;
+ --theme-accent-purple: #B07CD8;
+ --theme-accent-red: #E06C75;
+
+ /* Shadows */
+ --theme-shadow-feature-hover: 0 10px 25px -5px rgba(81, 174, 132, 0.2);
+ --theme-shadow-focus-ring: 0 0 0 4px rgba(81, 174, 132, 0.1);
+}
+
+/* Light theme — à remplir quand le design sera prêt */
+:root[data-theme="light"] {
+ --theme-background: #F5F5F5;
+ --theme-secondary: #E0E0E0;
+ --theme-tertiary: #EBEBEB;
+ --theme-dark-background: #D5D5D5;
+ --theme-darkest-background: #C0C0C0;
+ --theme-surface-gradient-dark: #D0D0D0;
+ --theme-surface-gradient-light: #E8E8E8;
+ --theme-glass-bg: rgba(240, 240, 240, 0.7);
+ --theme-text-primary: #1A1A1A;
+ --theme-text-secondary: #555555;
+ --theme-muted: #888888;
+ --theme-text-dimmed: #999999;
+ --theme-scrollbar-track: #E0E0E0;
+ --theme-scrollbar-thumb: #51AE84;
+ --theme-scrollbar-thumb-hover: #3A8B69;
+ --theme-editor-text: #333333;
+ --theme-editor-bold: #1A1A1A;
+ --theme-editor-heading: #2196B8;
+ --theme-editor-page-sepia: #f4f1e8;
+ --theme-gray: #999999;
+ --theme-gray-light: #BBBBBB;
+ --theme-gray-dark: #666666;
+ --theme-accent-blue: #3A7BD5;
+ --theme-accent-green: #2E8B57;
+ --theme-accent-orange: #C47F17;
+ --theme-accent-purple: #8B5CC4;
+ --theme-accent-red: #C94F5A;
+}
+
@theme {
- /* Colors */
- --color-primary: #51AE84;
- --color-primary-dark: #3A8B69;
- --color-primary-light: #74C9A0;
- --color-secondary: #3E3E3E;
- --color-tertiary: #2C2C2C;
- --color-background: #2B2D30;
- --color-dark-background: #2C2C2C;
- --color-darkest-background: #1A1A1A;
- --color-text-primary: #FFFFFF;
- --color-text-secondary: #B0B0B0;
- --color-muted: #B0B0B0;
- --color-success: #28A745;
- --color-error: #DC3545;
- --color-warning: #FFC107;
- --color-info: #17A2B8;
+ /* Brand */
+ --color-primary: var(--theme-primary);
+ --color-primary-dark: var(--theme-primary-dark);
+ --color-primary-light: var(--theme-primary-light);
- --color-gray: #808080;
- --color-gray-light: #A0A0A0;
- --color-gray-dark: #404040;
+ /* Surfaces */
+ --color-secondary: var(--theme-secondary);
+ --color-tertiary: var(--theme-tertiary);
+ --color-background: var(--theme-background);
+ --color-dark-background: var(--theme-dark-background);
+ --color-darkest-background: var(--theme-darkest-background);
+ --color-surface-gradient-dark: var(--theme-surface-gradient-dark);
+ --color-surface-gradient-light: var(--theme-surface-gradient-light);
+ --color-glass-bg: var(--theme-glass-bg);
- /* Aliases pour compatibilité */
- --color-textPrimary: #FFFFFF;
- --color-textSecondary: #B0B0B0;
+ /* Semantic surfaces */
+ --color-surface-primary: var(--theme-background);
+ --color-surface-secondary: var(--theme-secondary);
+ --color-surface-elevated: var(--theme-darkest-background);
+
+ /* Text */
+ --color-text-primary: var(--theme-text-primary);
+ --color-text-secondary: var(--theme-text-secondary);
+ --color-muted: var(--theme-muted);
+ --color-text-dimmed: var(--theme-text-dimmed);
+
+ /* Semantic */
+ --color-success: var(--theme-success);
+ --color-error: var(--theme-error);
+ --color-warning: var(--theme-warning);
+ --color-info: var(--theme-info);
+
+ /* Gray scale */
+ --color-gray: var(--theme-gray);
+ --color-gray-light: var(--theme-gray-light);
+ --color-gray-dark: var(--theme-gray-dark);
+
+ /* Scrollbar */
+ --color-scrollbar-track: var(--theme-scrollbar-track);
+ --color-scrollbar-thumb: var(--theme-scrollbar-thumb);
+ --color-scrollbar-thumb-hover: var(--theme-scrollbar-thumb-hover);
+
+ /* Editor content */
+ --color-editor-text: var(--theme-editor-text);
+ --color-editor-bold: var(--theme-editor-bold);
+ --color-editor-heading: var(--theme-editor-heading);
+ --color-editor-page-sepia: var(--theme-editor-page-sepia);
+
+ /* Accents */
+ --color-accent-blue: var(--theme-accent-blue);
+ --color-accent-green: var(--theme-accent-green);
+ --color-accent-orange: var(--theme-accent-orange);
+ --color-accent-purple: var(--theme-accent-purple);
+ --color-accent-red: var(--theme-accent-red);
+
+ /* Interactive */
+ --color-interactive: var(--theme-primary);
+ --color-interactive-hover: var(--theme-primary-dark);
+
+ /* Shadows */
+ --shadow-feature-hover: var(--theme-shadow-feature-hover);
+ --shadow-focus-ring: var(--theme-shadow-focus-ring);
/* Font Family */
--font-family-lora: 'Lora', Georgia, serif;
@@ -53,15 +201,8 @@
font-style: normal;
}
-:root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-}
-
body {
- --tw-bg-opacity: 1;
- background-color: rgb(17 24 39 / var(--tw-bg-opacity))
+ background-color: var(--color-background);
}
::-webkit-scrollbar {
@@ -70,23 +211,23 @@ body {
}
::-webkit-scrollbar-track {
- background: #2d2d2d;
+ background: var(--color-scrollbar-track);
border-radius: 10px;
}
::-webkit-scrollbar-thumb {
- background: #51AE84;
+ background: var(--color-scrollbar-thumb);
border-radius: 10px;
}
::-webkit-scrollbar-thumb:hover {
- background: #3a8b69;
+ background: var(--color-scrollbar-thumb-hover);
}
/* Scrollbar Styles for Firefox */
* {
scrollbar-width: thin;
- scrollbar-color: #51AE84 #2d2d2d;
+ scrollbar-color: var(--color-scrollbar-thumb) var(--color-scrollbar-track);
}
.fade-in {
@@ -200,8 +341,15 @@ body {
}
/* Styles pour l'éditeur principal avec classes dynamiques */
+.editor-content .tiptap > p:first-child,
+.editor-content .tiptap > h1:first-child,
+.editor-content .tiptap > h2:first-child,
+.editor-content .tiptap > h3:first-child {
+ margin-top: 0;
+}
+
.editor-content .tiptap p {
- color: #dedede;
+ color: var(--color-editor-text);
margin-top: 0.7em;
margin-bottom: 0.7em;
}
@@ -212,11 +360,11 @@ body {
.editor-content .tiptap p strong {
font-weight: 900;
- color: #f9f9f9;
+ color: var(--color-editor-bold);
}
.editor-content .tiptap h1, .editor-content .tiptap h2, .editor-content .tiptap h3 {
- color: #34acd0;
+ color: var(--color-editor-heading);
margin-top: 1em;
margin-bottom: 0.5em;
}
@@ -251,6 +399,7 @@ body {
border: none !important;
}
+
.composer-panel-h {
height: calc(100vh - 8rem);
}
@@ -286,13 +435,13 @@ body {
position: relative;
transition: all 0.3s ease;
overflow: hidden;
- background-color: #3E3E3E;
+ background-color: var(--color-secondary);
border-radius: 0.75rem;
}
.feature-card:hover {
transform: translateY(-5px);
- box-shadow: 0 10px 25px -5px rgba(81, 174, 132, 0.2);
+ box-shadow: var(--shadow-feature-hover);
}
.feature-card-bg {
@@ -310,7 +459,7 @@ body {
width: 4rem;
height: 4rem;
border-radius: 50%;
- background: linear-gradient(135deg, #313131, #4A4A4A);
+ background: linear-gradient(135deg, var(--color-surface-gradient-dark), var(--color-surface-gradient-light));
display: flex;
align-items: center;
justify-content: center;
@@ -331,7 +480,7 @@ body {
display: flex;
align-items: center;
justify-content: center;
- color: white;
+ color: var(--color-text-primary);
}
.feature-title-container {
@@ -356,7 +505,7 @@ body {
.feature-shine-line {
width: 100%;
height: 1px;
- background-color: #4A4A4A;
+ background-color: var(--color-surface-gradient-light);
position: relative;
overflow: hidden;
}
@@ -421,7 +570,7 @@ body {
width: 4rem;
height: 4rem;
border-radius: 50%;
- background-color: rgba(255, 255, 255, 0.1);
+ background-color: var(--color-glass-bg);
display: flex;
align-items: center;
justify-content: center;
@@ -446,7 +595,7 @@ body {
}
.last-updated {
- color: #777777;
+ color: var(--color-text-dimmed);
font-size: 0.75rem;
text-align: center;
margin-top: 1rem;
@@ -484,19 +633,42 @@ body {
margin-bottom: 0.7em;
}
+/* Form input base */
+.input-base {
+ @apply w-full text-text-primary bg-dark-background px-4 py-2.5 rounded-xl
+ border border-secondary
+ focus:border-primary focus:ring-4 focus:ring-primary/20
+ placeholder:text-muted
+ outline-none transition-all duration-200;
+}
+
+.input-base:disabled {
+ @apply opacity-50 cursor-not-allowed;
+}
+
+.input-base[readonly] {
+ @apply cursor-default;
+}
+
+.scrollbar-hide {
+ -ms-overflow-style: none;
+ scrollbar-width: none;
+}
+
+.scrollbar-hide::-webkit-scrollbar {
+ display: none;
+}
+
/* Smooth transitions for all interactive elements */
button, a, input, textarea, select {
transition: all 0.2s ease-in-out;
}
/* Enhanced focus states */
-button:focus-visible,
-input:focus-visible,
-textarea:focus-visible,
-select:focus-visible {
- outline: 2px solid #51AE84;
+button:focus-visible {
+ outline: 2px solid var(--color-primary);
outline-offset: 2px;
- box-shadow: 0 0 0 4px rgba(81, 174, 132, 0.1);
+ box-shadow: var(--shadow-focus-ring);
}
/* Smooth hover scale for interactive elements */
@@ -509,7 +681,7 @@ select:focus-visible {
.literary-ornament::before,
.literary-ornament::after {
content: "❖";
- color: #51AE84;
+ color: var(--color-primary);
opacity: 0.3;
font-size: 0.8em;
margin: 0 0.5em;
@@ -531,9 +703,9 @@ select:focus-visible {
/* Glass morphism effect */
.glass-effect {
- background: rgba(62, 62, 62, 0.7);
+ background: var(--color-glass-bg);
backdrop-filter: blur(10px);
- border: 1px solid rgba(255, 255, 255, 0.1);
+ border: 1px solid var(--color-surface-gradient-light);
}
/* Fade in pour le texte qui stream */
@@ -549,3 +721,38 @@ select:focus-visible {
opacity: 1;
}
}
+
+/* Alert animations */
+@keyframes shrink {
+ from {
+ width: 100%;
+ }
+ to {
+ width: 0%;
+ }
+}
+
+.alert-progress-shrink {
+ animation: shrink 5s linear forwards;
+ width: 100%;
+}
+
+.alert-slide-in {
+ animation: slideInFromRight 0.3s ease-out forwards;
+}
+
+/* Pulse dots for loading indicators */
+.pulse-dot-1 {
+ animation-delay: 0ms;
+ animation-duration: 1.5s;
+}
+
+.pulse-dot-2 {
+ animation-delay: 300ms;
+ animation-duration: 1.5s;
+}
+
+.pulse-dot-3 {
+ animation-delay: 600ms;
+ animation-duration: 1.5s;
+}
diff --git a/app/layout.tsx b/app/layout.tsx
index f524906..159a6bf 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -1,21 +1,30 @@
+import type {Metadata} from "next";
import "./globals.css";
import {ReactNode} from "react";
+import ScribeShell from "@/components/layout/ScribeShell";
+
+export const metadata: Metadata = {
+ title: "ERitors Scribe",
+ description: "Les meilleurs livres sont ceux qui ont le meilleur plan.",
+ icons: {
+ icon: "/eritors-favicon-white.png"
+ },
+ robots: {
+ index: false,
+ follow: false,
+ },
+};
export default function RootLayout(
{
children,
}: Readonly<{
children: ReactNode;
-}>) {
+ }>) {
return (
-
- ERitors Scribe
-
-
-
- {children}
+ {children}
);
diff --git a/app/login/LoginWrapper.tsx b/app/login/LoginWrapper.tsx
index abb7dc6..c00236f 100644
--- a/app/login/LoginWrapper.tsx
+++ b/app/login/LoginWrapper.tsx
@@ -1,29 +1,19 @@
-'use client'
-
-import frMessages from '@/lib/locales/fr.json';
-import enMessages from '@/lib/locales/en.json';
import {useEffect, useState} from "react";
import {LangContext} from "@/context/LangContext";
-import {NextIntlClientProvider} from "next-intl";
-import StaticAlert from "@/components/StaticAlert";
-import {SessionProps} from "@/lib/models/Session";
-import System from "@/lib/models/System";
+import StaticAlert from "@/components/ui/StaticAlert";
+import {SessionProps} from "@/lib/types/session";
+import {getCookie} from "@/lib/utils/cookies";
import {SessionContext} from "@/context/SessionContext";
import {AlertContext} from "@/context/AlertContext";
+import {changeLanguage} from "@/lib/i18n";
import * as tauri from '@/lib/tauri';
-const messagesMap = {
- fr: frMessages,
- en: enMessages
-};
-
export default function LoginWrapper({children}: { children: React.ReactNode }) {
const [locale, setLocale] = useState<'fr' | 'en'>('fr');
const [errorMessage, setErrorMessage] = useState('');
const [successMessage, setSuccessMessage] = useState('');
const [infoMessage, setInfoMessage] = useState('');
const [warningMessage, setWarningMessage] = useState('');
- const messages = messagesMap[locale];
const [session, setSession] = useState({
isConnected: false,
@@ -35,6 +25,10 @@ export default function LoginWrapper({children}: { children: React.ReactNode })
checkAuthentification().then()
}, []);
+ useEffect((): void => {
+ changeLanguage(locale);
+ }, [locale]);
+
useEffect((): void => {
if (session.isConnected) {
tauri.loginSuccess();
@@ -42,36 +36,36 @@ export default function LoginWrapper({children}: { children: React.ReactNode })
}, [session]);
async function checkAuthentification(): Promise {
- const language: "fr" | "en" | null = System.getCookie('lang') as "fr" | "en" | null;
+ const language: "fr" | "en" | null = getCookie('lang') as "fr" | "en" | null;
if (language) {
setLocale(language);
}
-
- // Pas besoin de vérifier le token ici dans Electron
- // Le main process gère quelle fenêtre ouvrir
}
-
+
return (
-
-
- {children}
-
- {
- successMessage && {
- setSuccessMessage('')
- }}/>
- }
- {
- errorMessage && {
- setErrorMessage('')
- }}/>
- }
-
-
-
+
+ {children}
+
+ {
+ successMessage && {
+ setSuccessMessage('')
+ }}/>
+ }
+ {
+ errorMessage && {
+ setErrorMessage('')
+ }}/>
+ }
+
+
)
-}
\ No newline at end of file
+}
diff --git a/app/login/login/LoginForm.tsx b/app/login/login/LoginForm.tsx
index 6fdaad1..233e07e 100755
--- a/app/login/login/LoginForm.tsx
+++ b/app/login/login/LoginForm.tsx
@@ -1,117 +1,106 @@
-import {useContext, useState} from "react";
-import System from "@/lib/models/System";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faEnvelope, faLock} from "@fortawesome/free-solid-svg-icons";
-import {AlertContext} from "@/context/AlertContext";
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-import * as tauri from '@/lib/tauri';
-
-export default function LoginForm() {
- const {errorMessage} = useContext(AlertContext);
- const [email, setEmail] = useState('');
- const [password, setPassword] = useState('');
- const [isLoading, setIsLoading] = useState(false);
- const t = useTranslations();
- const {lang} = useContext(LangContext)
-
- async function handleSubmit(e: React.FormEvent): Promise {
- e.preventDefault();
- setIsLoading(true);
- errorMessage('');
-
- if (email.length === 0) {
- errorMessage(t('loginForm.error.emailRequired'));
- return;
- }
- if (password.length === 0) {
- errorMessage(t('loginForm.error.passwordRequired'));
- return;
- }
- if (email.length < 3) {
- errorMessage(t('loginForm.error.emailLength'));
- return;
- }
-
- if (System.verifyInput(email) || System.verifyInput(password)) {
- errorMessage(t('loginForm.error.emailInvalidChars'));
- return;
- }
- try {
- const response: string = await System.postToServer('login', {
- email: email,
- password: password,
- }, lang)
- if (!response) {
- errorMessage(t('loginForm.error.connection'));
- setIsLoading(false);
- return;
- }
- await tauri.setToken(response);
- await tauri.loginSuccess();
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(t('loginForm.error.server'));
- } else {
- errorMessage(t('loginForm.error.unknown'));
- }
- } finally {
- setIsLoading(false);
- }
- }
-
- return (
-
- )
-}
+import {useContext, useState} from "react";
+import {Mail, Lock} from "lucide-react";
+import {AlertContext} from "@/context/AlertContext";
+import {useTranslations} from "@/lib/i18n";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import * as tauri from '@/lib/tauri';
+import {verifyInput} from "@/lib/utils/validation";
+import {apiPostPublic} from "@/lib/api/client";
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+
+export default function LoginForm() {
+ const {errorMessage} = useContext(AlertContext);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const t = useTranslations();
+ const {lang} = useContext(LangContext)
+
+ async function handleSubmit(e: React.FormEvent): Promise {
+ e.preventDefault();
+ setIsLoading(true);
+ errorMessage('');
+
+ if (email.length === 0) {
+ errorMessage(t('loginForm.error.emailRequired'));
+ setIsLoading(false);
+ return;
+ }
+ if (password.length === 0) {
+ errorMessage(t('loginForm.error.passwordRequired'));
+ setIsLoading(false);
+ return;
+ }
+ if (email.length < 3) {
+ errorMessage(t('loginForm.error.emailLength'));
+ setIsLoading(false);
+ return;
+ }
+
+ if (verifyInput(email) || verifyInput(password)) {
+ errorMessage(t('loginForm.error.emailInvalidChars'));
+ setIsLoading(false);
+ return;
+ }
+ try {
+ const response: string = await apiPostPublic('login', {
+ email: email,
+ password: password,
+ }, lang)
+ if (!response) {
+ errorMessage(t('loginForm.error.connection'));
+ setIsLoading(false);
+ return;
+ }
+ await tauri.setToken(response);
+ await tauri.loginSuccess();
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(t('loginForm.error.server'));
+ } else {
+ errorMessage(t('loginForm.error.unknown'));
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/app/login/login/SocialForm.tsx b/app/login/login/SocialForm.tsx
index 1e78be5..331cfbb 100755
--- a/app/login/login/SocialForm.tsx
+++ b/app/login/login/SocialForm.tsx
@@ -1,124 +1,140 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faApple, faFacebookF, faGoogle} from "@fortawesome/free-brands-svg-icons";
-import React, {useContext, useEffect} from "react";
-import System from "@/lib/models/System";
-import {AlertContext} from "@/context/AlertContext";
-import {configs} from "@/lib/configs";
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-import * as tauri from '@/lib/tauri';
-
-export default function SocialForm() {
- const {errorMessage} = useContext(AlertContext);
- const t = useTranslations();
- const {lang} = useContext(LangContext)
-
- useEffect((): void => {
- const params = new URLSearchParams(window.location.search);
- const provider: string | null = params.get('provider');
- if (!provider) return;
-
- const code: string | null = params.get('code');
- if (!code) return;
-
- if (provider === 'google') {
- handleGoogleLogin(code).then();
- return;
- }
- if (provider === 'facebook') {
- const state: string | null = params.get('state');
- if (!state) return;
- handleFacebookLogin(code, state).then();
- return;
- }
- if (provider === 'apple') {
- const state: string | null = params.get('state');
- if (!state) return;
- handleAppleLogin(code, state).then();
- return;
- }
- }, []);
-
- async function handleLoginSuccess(token: string): Promise {
- await tauri.setToken(token);
- await tauri.loginSuccess();
- }
-
- async function handleFacebookLogin(code: string, state: string): Promise {
- if (code && state) {
- const response: string = await System.postToServer(`auth/facebook`, {
- code: code,
- state: state,
- }, lang);
- if (!response) {
- errorMessage(t('socialForm.error.connection'));
- return;
- }
- await handleLoginSuccess(response);
- }
- }
-
- async function handleGoogleLogin(code: string): Promise {
- if (code) {
- const response: string = await System.postToServer(`auth/google`, {
- code: code,
- }, lang);
- if (!response) {
- errorMessage(t('socialForm.error.connection'));
- return;
- }
- await handleLoginSuccess(response);
- }
- }
-
- async function handleAppleLogin(code: string, state: string): Promise {
- if (code && state) {
- const response: string = await System.postToServer(`auth/apple`, {
- code: code,
- state: state,
- }, lang);
- if (!response) {
- errorMessage(t('socialForm.error.connection'));
- return;
- }
- await handleLoginSuccess(response);
- }
- }
-
- async function handleOAuthClick(provider: 'google' | 'facebook' | 'apple'): Promise {
- try {
- const oauthUrl = `${configs.baseUrl}auth/${provider}/desktop`;
- await tauri.openExternal(oauthUrl);
- } catch {
- errorMessage(t('socialForm.error.connection'));
- }
- }
-
- return (
-
-
-
-
-
-
-
- )
-}
+import React, {useContext, useEffect} from "react";
+import {AlertContext} from "@/context/AlertContext";
+import {configs} from "@/lib/configs";
+import {useTranslations} from "@/lib/i18n";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import * as tauri from '@/lib/tauri';
+import {apiPostPublic} from "@/lib/api/client";
+
+const FacebookIcon = () => (
+
+);
+
+const GoogleIcon = () => (
+
+);
+
+const AppleIcon = () => (
+
+);
+
+export default function SocialForm() {
+ const {errorMessage} = useContext(AlertContext);
+ const t = useTranslations();
+ const {lang} = useContext(LangContext)
+
+ useEffect((): void => {
+ const params = new URLSearchParams(window.location.search);
+ const provider: string | null = params.get('provider');
+ if (!provider) return;
+
+ const code: string | null = params.get('code');
+ if (!code) return;
+
+ if (provider === 'google') {
+ handleGoogleLogin(code).then();
+ return;
+ }
+ if (provider === 'facebook') {
+ const state: string | null = params.get('state');
+ if (!state) return;
+ handleFacebookLogin(code, state).then();
+ return;
+ }
+ if (provider === 'apple') {
+ const state: string | null = params.get('state');
+ if (!state) return;
+ handleAppleLogin(code, state).then();
+ return;
+ }
+ }, []);
+
+ async function handleLoginSuccess(token: string): Promise {
+ await tauri.setToken(token);
+ await tauri.loginSuccess();
+ }
+
+ async function handleFacebookLogin(code: string, state: string): Promise {
+ if (code && state) {
+ const response: string = await apiPostPublic(`auth/facebook`, {
+ code: code,
+ state: state,
+ }, lang);
+ if (!response) {
+ errorMessage(t('socialForm.error.connection'));
+ return;
+ }
+ await handleLoginSuccess(response);
+ }
+ }
+
+ async function handleGoogleLogin(code: string): Promise {
+ if (code) {
+ const response: string = await apiPostPublic(`auth/google`, {
+ code: code,
+ }, lang);
+ if (!response) {
+ errorMessage(t('socialForm.error.connection'));
+ return;
+ }
+ await handleLoginSuccess(response);
+ }
+ }
+
+ async function handleAppleLogin(code: string, state: string): Promise {
+ if (code && state) {
+ const response: string = await apiPostPublic(`auth/apple`, {
+ code: code,
+ state: state,
+ }, lang);
+ if (!response) {
+ errorMessage(t('socialForm.error.connection'));
+ return;
+ }
+ await handleLoginSuccess(response);
+ }
+ }
+
+ async function handleOAuthClick(provider: 'google' | 'facebook' | 'apple'): Promise {
+ try {
+ const oauthUrl = `${configs.baseUrl}auth/${provider}/desktop`;
+ await tauri.openExternal(oauthUrl);
+ } catch {
+ errorMessage(t('socialForm.error.connection'));
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+ )
+}
diff --git a/app/login/login/page.tsx b/app/login/login/page.tsx
index f12c149..2b49041 100755
--- a/app/login/login/page.tsx
+++ b/app/login/login/page.tsx
@@ -1,172 +1,214 @@
-'use client'
-import {useContext, useEffect, useState} from 'react';
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faEnvelope, faWifi, faCloudArrowUp} from "@fortawesome/free-solid-svg-icons";
-import LoginForm from "@/app/login/login/LoginForm";
-import SocialForm from "@/app/login/login/SocialForm";
-import {useTranslations} from "next-intl";
-import {LangContext} from "@/context/LangContext";
-import System from "@/lib/models/System";
-import * as tauri from '@/lib/tauri';
-
-export default function LoginPage() {
- const t = useTranslations();
- const {lang, setLang} = useContext(LangContext);
- const [showOfflineWarning, setShowOfflineWarning] = useState(false);
- const [isOnline, setIsOnline] = useState(true);
- const [resetDone, setResetDone] = useState(false);
-
- const isDev = process.env.NODE_ENV === 'development';
-
- const handleDevReset = async () => {
- try {
- await tauri.devResetAll();
- setResetDone(true);
- } catch (error) {
- console.error('[DevReset]', error);
- }
- };
-
- const toggleLanguage = () => {
- const newLang = lang === 'fr' ? 'en' : 'fr';
- setLang(newLang);
- System.setCookie('lang', newLang, 365);
- };
-
- useEffect(() => {
- async function checkFirstConnectionAndNetwork() {
- try {
- const token = await tauri.getToken();
- const hasToken = !!token;
- const online = navigator.onLine;
- setIsOnline(online);
-
- if (!hasToken && !online) {
- setShowOfflineWarning(true);
- }
- } catch (error) {
- console.error('Error checking first connection:', error);
- }
- }
-
- checkFirstConnectionAndNetwork();
-
- const handleOnline = () => {
- setIsOnline(true);
- setShowOfflineWarning(false);
- };
- const handleOffline = async () => {
- setIsOnline(false);
- try {
- const token = await tauri.getToken();
- if (!token) {
- setShowOfflineWarning(true);
- }
- } catch {
- setShowOfflineWarning(true);
- }
- };
-
- window.addEventListener('online', handleOnline);
- window.addEventListener('offline', handleOffline);
-
- return () => {
- window.removeEventListener('online', handleOnline);
- window.removeEventListener('offline', handleOffline);
- };
- }, []);
-
- return (
-
-
-
-
-

-
-
-
- {/* Offline warning notification */}
- {showOfflineWarning && (
-
-
-
-
-
-
- {t('loginPage.offlineWarning.title')}
-
-
- {t('loginPage.offlineWarning.message')}
-
-
-
-
- )}
-
-
-
-
-
-
-
-
-
-
{t('loginPage.title')}
-
{t('loginPage.welcome')}
-
-
-
-
-
- {t('loginPage.orSocial')}
-
-
-
-
-
-
- {isDev && (
- resetDone
- ? Reset OK
- :
- )}
-
- );
-}
\ No newline at end of file
+'use client'
+import {useContext, useEffect, useState} from 'react';
+import {Mail, Lock, Wifi, CloudUpload, Globe} from "lucide-react";
+import SocialForm from "@/app/login/login/SocialForm";
+import {useTranslations} from "@/lib/i18n";
+import {AlertContext} from "@/context/AlertContext";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {setCookie} from "@/lib/utils/cookies";
+import {verifyInput} from "@/lib/utils/validation";
+import {apiPostPublic} from "@/lib/api/client";
+import * as tauri from '@/lib/tauri';
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+import ToggleGroup from "@/components/ui/ToggleGroup";
+
+export default function LoginPage() {
+ const t = useTranslations();
+ const {errorMessage} = useContext(AlertContext);
+ const {lang, setLang} = useContext(LangContext);
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [isLoading, setIsLoading] = useState(false);
+ const [showOfflineWarning, setShowOfflineWarning] = useState(false);
+ const [resetDone, setResetDone] = useState(false);
+
+ const isDev = process.env.NODE_ENV === 'development';
+
+ useEffect(() => {
+ async function checkFirstConnectionAndNetwork() {
+ try {
+ const token = await tauri.getToken();
+ const online = navigator.onLine;
+ if (!token && !online) {
+ setShowOfflineWarning(true);
+ }
+ } catch (error) {
+ console.error('Error checking first connection:', error);
+ }
+ }
+
+ checkFirstConnectionAndNetwork();
+
+ const handleOnline = () => setShowOfflineWarning(false);
+ const handleOffline = async () => {
+ try {
+ const token = await tauri.getToken();
+ if (!token) setShowOfflineWarning(true);
+ } catch {
+ setShowOfflineWarning(true);
+ }
+ };
+
+ window.addEventListener('online', handleOnline);
+ window.addEventListener('offline', handleOffline);
+ return () => {
+ window.removeEventListener('online', handleOnline);
+ window.removeEventListener('offline', handleOffline);
+ };
+ }, []);
+
+ async function handleSubmit(e: React.FormEvent): Promise {
+ e.preventDefault();
+ setIsLoading(true);
+ errorMessage('');
+
+ if (email.length === 0) {
+ errorMessage(t('loginForm.error.emailRequired'));
+ setIsLoading(false);
+ return;
+ }
+ if (password.length === 0) {
+ errorMessage(t('loginForm.error.passwordRequired'));
+ setIsLoading(false);
+ return;
+ }
+ if (email.length < 3) {
+ errorMessage(t('loginForm.error.emailLength'));
+ setIsLoading(false);
+ return;
+ }
+ if (verifyInput(email) || verifyInput(password)) {
+ errorMessage(t('loginForm.error.emailInvalidChars'));
+ setIsLoading(false);
+ return;
+ }
+
+ try {
+ const response: string = await apiPostPublic('login', {
+ email: email,
+ password: password,
+ }, lang);
+ if (!response) {
+ errorMessage(t('loginForm.error.connection'));
+ setIsLoading(false);
+ return;
+ }
+ await tauri.setToken(response);
+ await tauri.loginSuccess();
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(t('loginForm.error.server'));
+ } else {
+ errorMessage(t('loginForm.error.unknown'));
+ }
+ } finally {
+ setIsLoading(false);
+ }
+ }
+
+ return (
+
+
+
+

+
+
+ {showOfflineWarning && (
+
+
+
+
+
+
+ {t('loginPage.offlineWarning.title')}
+
+
+ {t('loginPage.offlineWarning.message')}
+
+
+
+
+ )}
+
+
+
{t('loginPage.title')}
+
{t('loginPage.welcome')}
+
+
+
+
+
+
+
+ {t('loginPage.orSocial')}
+
+
+
+
+
+
+ {isDev && (
+ resetDone
+ ? Reset OK
+ :
+ )}
+
+ );
+}
diff --git a/app/login/offline/page.tsx b/app/login/offline/page.tsx
index 9edba72..024a47a 100644
--- a/app/login/offline/page.tsx
+++ b/app/login/offline/page.tsx
@@ -1,79 +1,67 @@
-'use client';
-
-import { useEffect } from 'react';
-import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
-import { useTranslations } from 'next-intl';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { faWifi, faArrowLeft } from '@fortawesome/free-solid-svg-icons';
-import * as tauri from '@/lib/tauri';
-
-export default function OfflineLoginPage() {
- const t = useTranslations();
-
- async function handlePinSuccess(userId: string): Promise {
- try {
- const encryptionKey = await tauri.getUserEncryptionKey(userId);
- if (encryptionKey) {
- await tauri.dbInitialize(userId, encryptionKey);
- await tauri.loginSuccess();
- }
- } catch (error) {
- console.error('[OfflineLogin] Error initializing database:', error);
- }
- }
-
- function handleBackToOnline(): void {
- tauri.logout();
- }
-
- useEffect((): void => {
- async function checkOfflineCapability() {
- const offlineStatus = await tauri.offlineModeGet();
- if (!offlineStatus.hasPin) {
- window.location.href = '/login/login';
- }
- }
-
- checkOfflineCapability().then();
- }, []);
-
- return (
-
-
- {/* Offline indicator */}
-
-
-
- {t('offline.mode.title')}
-
-
-
- {/* Logo */}
-
-

-
-
- {/* PIN Verify Component */}
-
-
- {/* Back to online link */}
-
-
-
-
-
- );
-}
\ No newline at end of file
+'use client';
+
+import {useEffect} from 'react';
+import OfflinePinVerify from '@/components/offline/OfflinePinVerify';
+import {useTranslations} from '@/lib/i18n';
+import {Wifi, ArrowLeft} from 'lucide-react';
+import Button from '@/components/ui/Button';
+import * as tauri from '@/lib/tauri';
+
+export default function OfflineLoginPage() {
+ const t = useTranslations();
+
+ async function handlePinSuccess(userId: string): Promise {
+ try {
+ const encryptionKey = await tauri.getUserEncryptionKey(userId);
+ if (encryptionKey) {
+ await tauri.dbInitialize(userId, encryptionKey);
+ await tauri.loginSuccess();
+ }
+ } catch (error) {
+ console.error('[OfflineLogin] Error initializing database:', error);
+ }
+ }
+
+ function handleBackToOnline(): void {
+ tauri.logout();
+ }
+
+ useEffect((): void => {
+ async function checkOfflineCapability() {
+ const offlineStatus = await tauri.offlineModeGet();
+ if (!offlineStatus.hasPin) {
+ window.location.href = '/login/login';
+ }
+ }
+
+ checkOfflineCapability().then();
+ }, []);
+
+ return (
+
+
+
+

+
+
+
+
+
+ {t('offline.mode.title')}
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/login/register/StepOne.tsx b/app/login/register/StepOne.tsx
index 9ed0dbc..837d5e3 100755
--- a/app/login/register/StepOne.tsx
+++ b/app/login/register/StepOne.tsx
@@ -1,205 +1,147 @@
-import React, {Dispatch, SetStateAction, useContext, useState} from "react";
-import System from "@/lib/models/System";
-import {AlertContext} from "@/context/AlertContext";
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faEnvelope, faLock, faSignature, faUser} from '@fortawesome/free-solid-svg-icons';
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-
-export default function StepOne(
- {
- username,
- email,
- setUsername,
- setEmail,
- handleNextStep
- }: {
- username: string;
- email: string;
- setUsername: Dispatch>,
- setEmail: Dispatch>,
- handleNextStep: Function;
- }) {
- const {errorMessage, successMessage} = useContext(AlertContext);
- const [firstName, setFirstName] = useState('');
- const [lastName, setLastName] = useState('');
- const [password, setPassword] = useState('');
- const [repeatPassword, setRepeatPassword] = useState('');
- const [userId, setUserId] = useState('')
- const t = useTranslations();
- const {lang} = useContext(LangContext)
-
- async function handleStep(): Promise {
-
- if (!firstName || !lastName || !username || !password || !repeatPassword || !email) {
- errorMessage(t('registerStepOne.error.requiredFields'));
- return;
- }
- if (firstName.length < 2 || firstName.length > 50) {
- errorMessage(t('registerStepOne.error.firstNameLength'));
- return;
- }
- if (lastName.length < 2 || lastName.length > 50) {
- errorMessage(t('registerStepOne.error.lastNameLength'));
- return;
- }
- if (username.length < 3 || username.length > 50) {
- errorMessage(t('registerStepOne.error.usernameLength'));
- return;
- }
- if (System.verifyInput(firstName) || System.verifyInput(lastName) || System.verifyInput(username) || System.verifyInput(password) || System.verifyInput(repeatPassword) || System.verifyInput(email)) {
- errorMessage(t('registerStepOne.error.invalidInput'));
- return;
- }
-
- if (password != repeatPassword) {
- errorMessage(t('registerStepOne.error.passwordMismatch'));
- return;
- }
- try {
- const response: string = await System.postToServer(`register/pre`, {
- firstName,
- lastName,
- username,
- email,
- password,
- retypePass: repeatPassword
- }, lang);
- if (!response) {
- errorMessage(t('registerStepOne.error.preRegister'));
- return;
- }
- setUserId(response);
- successMessage(t('registerStepOne.success.preRegister'));
- handleNextStep();
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t('registerStepOne.error.unknown'));
- }
- }
- }
-
- return (
-
- )
-}
+import React, {Dispatch, SetStateAction, useContext, useState} from "react";
+import {AlertContext} from "@/context/AlertContext";
+import {Mail, Lock, PenLine, User} from 'lucide-react';
+import {useTranslations} from "@/lib/i18n";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {verifyInput} from "@/lib/utils/validation";
+import {apiPostPublic} from "@/lib/api/client";
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+
+export default function StepOne(
+ {
+ username,
+ email,
+ setUsername,
+ setEmail,
+ handleNextStep
+ }: {
+ username: string;
+ email: string;
+ setUsername: Dispatch>,
+ setEmail: Dispatch>,
+ handleNextStep: Function;
+ }) {
+ const {errorMessage, successMessage} = useContext(AlertContext);
+ const [firstName, setFirstName] = useState('');
+ const [lastName, setLastName] = useState('');
+ const [password, setPassword] = useState('');
+ const [repeatPassword, setRepeatPassword] = useState('');
+ const [userId, setUserId] = useState('')
+ const t = useTranslations();
+ const {lang} = useContext(LangContext)
+
+ async function handleStep(): Promise {
+
+ if (!firstName || !lastName || !username || !password || !repeatPassword || !email) {
+ errorMessage(t('registerStepOne.error.requiredFields'));
+ return;
+ }
+ if (firstName.length < 2 || firstName.length > 50) {
+ errorMessage(t('registerStepOne.error.firstNameLength'));
+ return;
+ }
+ if (lastName.length < 2 || lastName.length > 50) {
+ errorMessage(t('registerStepOne.error.lastNameLength'));
+ return;
+ }
+ if (username.length < 3 || username.length > 50) {
+ errorMessage(t('registerStepOne.error.usernameLength'));
+ return;
+ }
+ if (verifyInput(firstName) || verifyInput(lastName) || verifyInput(username) || verifyInput(password) || verifyInput(repeatPassword) || verifyInput(email)) {
+ errorMessage(t('registerStepOne.error.invalidInput'));
+ return;
+ }
+
+ if (password != repeatPassword) {
+ errorMessage(t('registerStepOne.error.passwordMismatch'));
+ return;
+ }
+ try {
+ const response: string = await apiPostPublic(`register/pre`, {
+ firstName,
+ lastName,
+ username,
+ email,
+ password,
+ retypePass: repeatPassword
+ }, lang);
+ if (!response) {
+ errorMessage(t('registerStepOne.error.preRegister'));
+ return;
+ }
+ setUserId(response);
+ successMessage(t('registerStepOne.success.preRegister'));
+ handleNextStep();
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(e.message);
+ } else {
+ errorMessage(t('registerStepOne.error.unknown'));
+ }
+ }
+ }
+
+ return (
+
+ )
+}
diff --git a/app/login/register/StepTree.tsx b/app/login/register/StepTree.tsx
index 2b52512..8252b8f 100755
--- a/app/login/register/StepTree.tsx
+++ b/app/login/register/StepTree.tsx
@@ -1,114 +1,91 @@
-import React, {useContext, useState} from "react";
-import Link from "next/link";
-import System from "@/lib/models/System";
-import {AlertContext} from "@/context/AlertContext";
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faKey} from '@fortawesome/free-solid-svg-icons';
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-
-export default function StepTree(
- {
- email,
- prevStep
- }: {
- email: string;
- prevStep: Function;
- }) {
-
- const {errorMessage, successMessage} = useContext(AlertContext);
-
- const [isConfirmed, setIsConfirmed] = useState(false);
- const [verifyCode, setVerifyCode] = useState('');
- const t = useTranslations();
- const {lang} = useContext(LangContext)
-
- async function handleVerifyCode(): Promise {
- if (verifyCode === '') {
- errorMessage(t('registerStepTwo.error.codeIncorrect'));
- return;
- }
- try {
- const response: boolean = await System.postToServer('register/verify-code', {
- verifyCode,
- email,
- }, lang);
- if (!response) {
- errorMessage(t('registerStepTwo.error.codeIncorrect'));
- return;
- }
- setIsConfirmed(true);
- successMessage(t('registerStepTwo.success.verified'));
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t('registerStepTwo.error.unknown'));
- }
- }
- }
-
- return (
- !isConfirmed ? (
-
-
-
{t('registerStepTwo.instructions.sent')}
-
{t('registerStepTwo.instructions.checkInbox')}
-
-
-
-
-
-
- setVerifyCode(e.target.value)}
- required
- />
-
-
-
-
-
-
-
-
-
- ) : (
-
-
-
{t('registerStepTwo.confirmed')}
-
-
-
-
-
-
-
- )
- )
-}
+import React, {useContext, useState} from "react";
+import {Link} from "@/lib/navigation";
+import {AlertContext} from "@/context/AlertContext";
+import {KeyRound} from 'lucide-react';
+import {useTranslations} from "@/lib/i18n";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {apiPostPublic} from "@/lib/api/client";
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+
+export default function StepTree(
+ {
+ email,
+ prevStep
+ }: {
+ email: string;
+ prevStep: Function;
+ }) {
+
+ const {errorMessage, successMessage} = useContext(AlertContext);
+
+ const [isConfirmed, setIsConfirmed] = useState(false);
+ const [verifyCode, setVerifyCode] = useState('');
+ const t = useTranslations();
+ const {lang} = useContext(LangContext)
+
+ async function handleVerifyCode(): Promise {
+ if (verifyCode === '') {
+ errorMessage(t('registerStepTwo.error.codeIncorrect'));
+ return;
+ }
+ try {
+ const response: boolean = await apiPostPublic('register/verify-code', {
+ verifyCode,
+ email,
+ }, lang);
+ if (!response) {
+ errorMessage(t('registerStepTwo.error.codeIncorrect'));
+ return;
+ }
+ setIsConfirmed(true);
+ successMessage(t('registerStepTwo.success.verified'));
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(e.message);
+ } else {
+ errorMessage(t('registerStepTwo.error.unknown'));
+ }
+ }
+ }
+
+ return (
+ !isConfirmed ? (
+
+
+
{t('registerStepTwo.instructions.sent')}
+
{t('registerStepTwo.instructions.checkInbox')}
+
+
+
setVerifyCode(e.target.value)}
+ placeholder={t('registerStepTwo.fields.code.placeholder')}/>
+ }/>
+
+
+
+
+
+
+
+ ) : (
+
+
+
{t('registerStepTwo.confirmed')}
+
+
+
+
+
+
+
+ )
+ )
+}
diff --git a/app/login/register/page.tsx b/app/login/register/page.tsx
index 5a6d9dd..3143328 100755
--- a/app/login/register/page.tsx
+++ b/app/login/register/page.tsx
@@ -1,95 +1,202 @@
-'use client'
-import {useState} from 'react';
-import StepOne from "./StepOne";
-import StepTree from "@/app/login/register/StepTree";
-import SocialForm from "@/app/login/login/SocialForm";
-import {useTranslations} from "next-intl";
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faArrowLeft} from '@fortawesome/free-solid-svg-icons';
-
-export default function Register() {
- const t = useTranslations();
- const [username, setUsername] = useState('');
- const [email, setEmail] = useState('');
- const [step, setStep] = useState(1);
-
- function handleNextStep(): void {
- setStep(step + 1);
- }
-
- function handlePrevStep(): void {
- setStep(step - 1);
- }
-
- return (
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
-
{t('registerPage.title')}
-
{t('registerPage.subtitle')}
-
-
-
-
-
= 1 ? 'bg-gradient-to-r from-primary to-info' : 'bg-secondary'}`}>
-
-
= 2 ? 'bg-gradient-to-r from-primary to-info' : 'bg-secondary'}`}>
-
-
- {t('registerPage.progress.infos')}
- {t('registerPage.progress.verif')}
-
-
- {
- step === 1 &&
- }
- {
- step === 1 && (
- <>
-
-
-
- {t('registerPage.backToLogin')}
-
- >
- )
- }
- {
- step === 2 && (
-
- )
- }
-
-
-
-
- );
-}
\ No newline at end of file
+'use client'
+import {useContext, useState} from 'react';
+import {Mail, Lock, User, KeyRound, ArrowLeft} from 'lucide-react';
+import {useTranslations} from "@/lib/i18n";
+import {AlertContext} from "@/context/AlertContext";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {verifyInput} from "@/lib/utils/validation";
+import {apiPostPublic} from "@/lib/api/client";
+import {Link} from "@/lib/navigation";
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+
+export default function Register() {
+ const t = useTranslations();
+ const {errorMessage, successMessage} = useContext(AlertContext);
+ const {lang} = useContext(LangContext);
+
+ const [step, setStep] = useState(1);
+ const [username, setUsername] = useState('');
+ const [email, setEmail] = useState('');
+ const [password, setPassword] = useState('');
+ const [repeatPassword, setRepeatPassword] = useState('');
+ const [verifyCode, setVerifyCode] = useState('');
+ const [isConfirmed, setIsConfirmed] = useState(false);
+
+ async function handleStepOne(): Promise {
+ if (!username || !password || !repeatPassword || !email) {
+ errorMessage(t('registerStepOne.error.requiredFields'));
+ return;
+ }
+ if (username.length < 3 || username.length > 50) {
+ errorMessage(t('registerStepOne.error.usernameLength'));
+ return;
+ }
+ if (verifyInput(username) || verifyInput(password) || verifyInput(repeatPassword) || verifyInput(email)) {
+ errorMessage(t('registerStepOne.error.invalidInput'));
+ return;
+ }
+ if (password !== repeatPassword) {
+ errorMessage(t('registerStepOne.error.passwordMismatch'));
+ return;
+ }
+ try {
+ const response: string = await apiPostPublic('register/pre', {
+ username, email, password, retypePass: repeatPassword
+ }, lang);
+ if (!response) {
+ errorMessage(t('registerStepOne.error.preRegister'));
+ return;
+ }
+ successMessage(t('registerStepOne.success.preRegister'));
+ setStep(2);
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(e.message);
+ } else {
+ errorMessage(t('registerStepOne.error.unknown'));
+ }
+ }
+ }
+
+ async function handleVerifyCode(): Promise {
+ if (verifyCode === '') {
+ errorMessage(t('registerStepTwo.error.codeIncorrect'));
+ return;
+ }
+ try {
+ const response: boolean = await apiPostPublic('register/verify-code', {
+ verifyCode, email,
+ }, lang);
+ if (!response) {
+ errorMessage(t('registerStepTwo.error.codeIncorrect'));
+ return;
+ }
+ setIsConfirmed(true);
+ successMessage(t('registerStepTwo.success.verified'));
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(e.message);
+ } else {
+ errorMessage(t('registerStepTwo.error.unknown'));
+ }
+ }
+ }
+
+ return (
+
+
+
+

+
+
+
+
{t('registerPage.title')}
+
{t('registerPage.subtitle')}
+
+
+ {step === 1 && !isConfirmed && (
+ <>
+
+
+
= 1 ? 'bg-primary' : 'bg-secondary'}`}>
+
+
= 2 ? 'bg-primary' : 'bg-secondary'}`}>
+
+
+
+
setUsername(e.target.value)}
+ placeholder={t('registerStepOne.fields.username.placeholder')}/>
+ }/>
+ {t('registerStepOne.fields.username.note')}
+
+
setEmail(e.target.value)}
+ placeholder={t('registerStepOne.fields.email.placeholder')}/>
+ }/>
+
+
+
+ {t('registerStepOne.fields.password.label')}
+
+ setPassword(e.target.value)} required/>
+
+
+
+
+ {t('registerStepOne.fields.repeatPassword.label')}
+
+ setRepeatPassword(e.target.value)} required/>
+
+
+
+
+
+ >
+ )}
+
+ {step === 2 && !isConfirmed && (
+ <>
+
+
+
+
+
{t('registerStepTwo.instructions.sent')}
+
{t('registerStepTwo.instructions.checkInbox')}
+
+
setVerifyCode(e.target.value)}
+ placeholder={t('registerStepTwo.fields.code.placeholder')}/>
+ }/>
+
+
+
+
+
+
+
+ >
+ )}
+
+ {isConfirmed && (
+ <>
+
+
+
{t('registerStepTwo.confirmed')}
+
+
+
+
+
+
+ >
+ )}
+
+
+ );
+}
diff --git a/app/login/reset-password/page.tsx b/app/login/reset-password/page.tsx
index 33ba747..e45a642 100755
--- a/app/login/reset-password/page.tsx
+++ b/app/login/reset-password/page.tsx
@@ -1,277 +1,190 @@
-'use client'
-import {useContext, useState} from "react";
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faEnvelope, faKey, faLock, faArrowLeft} from '@fortawesome/free-solid-svg-icons';
-import System from "@/lib/models/System";
-import {QueryDataResponse} from "@/shared/interface";
-import {AlertContext} from "@/context/AlertContext";
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-
-export default function ForgetPasswordPage() {
- const [step, setStep] = useState(1);
- const [email, setEmail] = useState('');
- const [verificationCode, setVerificationCode] = useState('');
- const [isConfirmed, setIsConfirmed] = useState(false);
- const [newPassword, setNewPassword] = useState('');
- const {errorMessage, successMessage} = useContext(AlertContext);
- const t = useTranslations();
- const {lang} = useContext(LangContext)
-
- function handleNextStep(): void {
- setStep(step + 1);
- }
-
- function handlePrevStep(): void {
- setStep(step - 1);
- }
-
- async function handleConfirm() {
- try {
- const response: QueryDataResponse = await System.postToServer>('user/verify-code', {
- verifyCode: verificationCode,
- email,
- }, lang);
- if (response.valid) {
- successMessage(response.message ?? '');
- setIsConfirmed(true);
- } else {
- errorMessage(response.message ?? '');
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(t('resetPassword.error.codeServer'));
- } else {
- errorMessage(t('resetPassword.error.codeUnknown'));
- }
- }
- }
-
- async function handleEmailCheck(): Promise {
- if (email == null || email == "") {
- errorMessage(t('resetPassword.error.emailInvalid'));
- return;
- }
-
- const emailRegEx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
-
- if (!emailRegEx.test(email)) {
- errorMessage(t('resetPassword.error.emailFormat'));
- return;
- }
-
- try {
- const response: QueryDataResponse = await System.postToServer>('user/email-check', {
- email: email
- }, lang);
- if (response.valid) {
- successMessage(response.message ?? '');
- handleNextStep();
- } else {
- errorMessage(response.message ?? '');
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(t('resetPassword.error.emailServer'));
- } else {
- errorMessage(t('resetPassword.error.emailUnknown'));
- }
- }
- }
-
- async function handleNewPassword(): Promise {
- try {
- const response: QueryDataResponse = await System.postToServer('password/reset', {
- email: email,
- newPassword: newPassword,
- code: verificationCode
- }, lang);
- if (response.valid) {
- successMessage(response.message ?? '');
- handleNextStep();
- } else {
- errorMessage(response.message ?? '');
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(t('resetPassword.error.passwordServer'));
- } else {
- errorMessage(t('resetPassword.error.passwordUnknown'));
- }
- }
- }
-
- return (
-
-
-
-
-

-
-
-
-
-
-
-
-
-
-
{t('resetPassword.title')}
-
{t('resetPassword.subtitle')}
-
-
-
-
-
= 1 ? 'bg-gradient-to-r from-primary to-info' : 'bg-secondary'}`}>
-
-
= 2 ? 'bg-gradient-to-r from-primary to-info' : 'bg-secondary'}`}>
-
-
= 3 ? 'bg-gradient-to-r from-primary to-info' : 'bg-secondary'}`}>
-
-
- {t('resetPassword.progress.email')}
- {t('resetPassword.progress.verification')}
- {t('resetPassword.progress.final')}
-
-
-
- {step === 1 && (
-
- )}
-
- {step === 2 && (
-
- )}
-
- {step === 3 && (
-
-
-
- {t('resetPassword.success')}
-
-
-
-
- )}
-
-
-
-
- )
-}
+'use client'
+import {useContext, useState} from "react";
+import {Mail, KeyRound, Lock, ArrowLeft} from 'lucide-react';
+import {QueryDataResponse} from "@/shared/interface";
+import {AlertContext} from "@/context/AlertContext";
+import {useTranslations} from "@/lib/i18n";
+import {LangContext, LangContextProps} from "@/context/LangContext";
+import {apiPostPublic} from "@/lib/api/client";
+import Button from "@/components/ui/Button";
+import InputField from "@/components/form/InputField";
+import TextInput from "@/components/form/TextInput";
+
+export default function ForgetPasswordPage() {
+ const [step, setStep] = useState(1);
+ const [email, setEmail] = useState('');
+ const [verificationCode, setVerificationCode] = useState('');
+ const [isConfirmed, setIsConfirmed] = useState(false);
+ const [newPassword, setNewPassword] = useState('');
+ const {errorMessage, successMessage} = useContext(AlertContext);
+ const t = useTranslations();
+ const {lang} = useContext(LangContext)
+
+ async function handleEmailCheck(): Promise {
+ if (!email) {
+ errorMessage(t('resetPassword.error.emailInvalid'));
+ return;
+ }
+ const emailRegEx = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegEx.test(email)) {
+ errorMessage(t('resetPassword.error.emailFormat'));
+ return;
+ }
+ try {
+ const response: QueryDataResponse = await apiPostPublic>('user/email-check', {email}, lang);
+ if (response.valid) {
+ successMessage(response.message ?? '');
+ setStep(2);
+ } else {
+ errorMessage(response.message ?? '');
+ }
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(t('resetPassword.error.emailServer'));
+ } else {
+ errorMessage(t('resetPassword.error.emailUnknown'));
+ }
+ }
+ }
+
+ async function handleConfirm(): Promise {
+ try {
+ const response: QueryDataResponse = await apiPostPublic>('user/verify-code', {
+ verifyCode: verificationCode, email,
+ }, lang);
+ if (response.valid) {
+ successMessage(response.message ?? '');
+ setIsConfirmed(true);
+ } else {
+ errorMessage(response.message ?? '');
+ }
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(t('resetPassword.error.codeServer'));
+ } else {
+ errorMessage(t('resetPassword.error.codeUnknown'));
+ }
+ }
+ }
+
+ async function handleNewPassword(): Promise {
+ try {
+ const response: QueryDataResponse = await apiPostPublic>('password/reset', {
+ email, newPassword, code: verificationCode
+ }, lang);
+ if (response.valid) {
+ successMessage(response.message ?? '');
+ setStep(3);
+ } else {
+ errorMessage(response.message ?? '');
+ }
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorMessage(t('resetPassword.error.passwordServer'));
+ } else {
+ errorMessage(t('resetPassword.error.passwordUnknown'));
+ }
+ }
+ }
+
+ return (
+
+
+
+

+
+
+
+
{t('resetPassword.title')}
+
{t('resetPassword.subtitle')}
+
+
+ {step < 3 && (
+ <>
+
+
+
= 1 ? 'bg-primary' : 'bg-secondary'}`}>
+
+
= 2 ? 'bg-primary' : 'bg-secondary'}`}>
+
+
= 3 ? 'bg-primary' : 'bg-secondary'}`}>
+
+
+
+
+
+ {step === 1 && (
+ <>
+
+
+
+
+ >
+ )}
+ {step === 2 && (
+ <>
+
+
+ >
+ )}
+
+ >
+ )}
+
+ {step === 3 && (
+ <>
+
+
+
{t('resetPassword.success')}
+
+
+
+
+
+
+ >
+ )}
+
+
+ )
+}
diff --git a/app/page.tsx b/app/page.tsx
index c2aef6d..5741499 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,797 +1,6 @@
'use client';
-import {useCallback, useContext, useEffect, useRef, useState} from 'react';
-import {BookContext} from "@/context/BookContext";
-import {ChapterProps} from "@/lib/models/Chapter";
-import {ChapterContext} from '@/context/ChapterContext';
-import {EditorContext} from '@/context/EditorContext'
-import {Editor, useEditor} from "@tiptap/react";
-import StarterKit from "@tiptap/starter-kit";
-import Underline from "@tiptap/extension-underline";
-import TextAlign from "@tiptap/extension-text-align";
-import {AlertContext, AlertProvider} from "@/context/AlertContext";
-import System from "@/lib/models/System";
-import {SessionContext} from '@/context/SessionContext';
-import {SessionProps} from "@/lib/models/Session";
-import User, {UserProps} from "@/lib/models/User";
-import {BookProps} from "@/lib/models/Book";
-import ScribeTopBar from "@/components/ScribeTopBar";
-import ScribeControllerBar from "@/components/ScribeControllerBar";
-import ScribeLeftBar from "@/components/leftbar/ScribeLeftBar";
-import ScribeEditor from "@/components/editor/ScribeEditor";
-import ComposerRightBar from "@/components/rightbar/ComposerRightBar";
-import ScribeFooterBar from "@/components/ScribeFooterBar";
-import GuideTour, {GuideStep} from "@/components/GuideTour";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faBookMedical, faFeather} from "@fortawesome/free-solid-svg-icons";
-import TermsOfUse from "@/components/TermsOfUse";
-import frMessages from '@/lib/locales/fr.json';
-import enMessages from '@/lib/locales/en.json';
-import {NextIntlClientProvider, useTranslations} from "next-intl";
-import {LangContext} from "@/context/LangContext";
-import {AIUsageContext} from "@/context/AIUsageContext";
-import OfflineProvider from "@/context/OfflineProvider";
-import OfflineContext, {OfflineMode} from "@/context/OfflineContext";
-import OfflinePinSetup from "@/components/offline/OfflinePinSetup";
-import OfflinePinVerify from "@/components/offline/OfflinePinVerify";
-import {SyncedBook, BookSyncCompare, compareBookSyncs} from "@/lib/models/SyncedBook";
-import {SyncedSeries, SeriesSyncCompare, compareSeriesSyncs} from "@/lib/models/SyncedSeries";
-import {BooksSyncContext} from "@/context/BooksSyncContext";
-import {SeriesSyncContext} from "@/context/SeriesSyncContext";
-import useSyncBooks from "@/hooks/useSyncBooks";
-import useSyncSeries from "@/hooks/useSyncSeries";
-import {LocalSyncQueueContext, LocalSyncOperation} from "@/context/SyncQueueContext";
-import * as tauri from '@/lib/tauri';
+import BookList from '@/components/book/BookList';
-interface RemovedItemRecord {
- removal_id: string;
- table_name: string;
- entity_id: string;
- book_id: string | null;
- user_id: string;
- deleted_at: number;
-}
-
-interface SyncedBooksResponse {
- books: SyncedBook[];
- tombstones: RemovedItemRecord[];
-}
-
-interface SyncedSeriesResponse {
- series: SyncedSeries[];
- tombstones: RemovedItemRecord[];
-}
-
-const messagesMap = {
- fr: frMessages,
- en: enMessages
-};
-
-function AutoSyncOnReconnect() {
- const {offlineMode} = useContext(OfflineContext);
- const {session} = useContext(SessionContext);
- const {syncAllToServer: syncAllBooksToServer, syncAllFromServer: syncAllBooksFromServer, refreshBooks, booksToSyncToServer, booksToSyncFromServer} = useSyncBooks();
- const {syncAllToServer: syncAllSeriesToServer, syncAllFromServer: syncAllSeriesFromServer, refreshSeries, seriesToSyncToServer, seriesToSyncFromServer} = useSyncSeries();
- const isSyncingRef = useRef(false);
- const hasRefreshedRef = useRef(false);
-
- const saveLastOnlineTimestamp = useCallback((): void => {
- const timestamp: number = Math.floor(Date.now() / 1000);
- localStorage.setItem('lastOnlineTimestamp', timestamp.toString());
- }, []);
-
- // Refresh sync data when online + authenticated + DB ready
- useEffect((): void => {
- if (!offlineMode.isOffline && session.isConnected && offlineMode.isDatabaseInitialized) {
- hasRefreshedRef.current = true;
- Promise.all([refreshBooks(), refreshSeries()]);
- }
- }, [offlineMode.isOffline, session.isConnected, offlineMode.isDatabaseInitialized]);
-
- // Auto-sync when diffs become available (reactive, no flags)
- useEffect((): void => {
- if (offlineMode.isOffline || !session.isConnected || isSyncingRef.current || !hasRefreshedRef.current) return;
-
- const syncPromises: Promise[] = [];
-
- if (booksToSyncToServer.length > 0) syncPromises.push(syncAllBooksToServer());
- if (booksToSyncFromServer.length > 0) syncPromises.push(syncAllBooksFromServer());
- if (seriesToSyncToServer.length > 0) syncPromises.push(syncAllSeriesToServer());
- if (seriesToSyncFromServer.length > 0) syncPromises.push(syncAllSeriesFromServer());
-
- if (syncPromises.length > 0) {
- isSyncingRef.current = true;
- Promise.all(syncPromises).then((): void => {
- saveLastOnlineTimestamp();
- isSyncingRef.current = false;
- }).catch((): void => {
- isSyncingRef.current = false;
- });
- }
- }, [booksToSyncToServer, booksToSyncFromServer, seriesToSyncToServer, seriesToSyncFromServer]);
-
- // Update lastOnlineTimestamp every 5 minutes while online
- useEffect((): (() => void) | void => {
- if (!offlineMode.isOffline && session.isConnected) {
- const intervalId: NodeJS.Timeout = setInterval((): void => {
- saveLastOnlineTimestamp();
- }, 5 * 60 * 1000);
-
- return (): void => clearInterval(intervalId);
- }
- }, [offlineMode.isOffline, session.isConnected, saveLastOnlineTimestamp]);
-
- return null;
-}
-
-function ScribeContent() {
- const t = useTranslations();
- const {lang: locale} = useContext(LangContext);
- const {errorMessage} = useContext(AlertContext);
- const {initializeDatabase, setOfflineMode, isCurrentlyOffline, offlineMode} = useContext(OfflineContext);
- const editor: Editor | null = useEditor({
- extensions: [
- StarterKit,
- Underline,
- TextAlign.configure({
- types: ['heading', 'paragraph'],
- }),
- ],
- injectCSS: false,
- immediatelyRender: false,
- shouldRerenderOnTransaction: true,
- });
-
- const [session, setSession] = useState({user: null, accessToken: '', isConnected: false});
- const [currentChapter, setCurrentChapter] = useState(undefined);
- const [currentBook, setCurrentBook] = useState(null);
-
- const [serverSyncedBooks, setServerSyncedBooks] = useState([]);
- const [localSyncedBooks, setLocalSyncedBooks] = useState([]);
- const [bookSyncDiffsFromServer, setBookSyncDiffsFromServer] = useState([]);
- const [bookSyncDiffsToServer, setBookSyncDiffsToServer] = useState([]);
- const [serverOnlyBooks, setServerOnlyBooks] = useState([]);
- const [localOnlyBooks, setLocalOnlyBooks] = useState([]);
-
- const [serverSyncedSeries, setServerSyncedSeries] = useState([]);
- const [localSyncedSeries, setLocalSyncedSeries] = useState([]);
- const [seriesSyncDiffsFromServer, setSeriesSyncDiffsFromServer] = useState([]);
- const [seriesSyncDiffsToServer, setSeriesSyncDiffsToServer] = useState([]);
- const [serverOnlySeries, setServerOnlySeries] = useState([]);
- const [localOnlySeries, setLocalOnlySeries] = useState([]);
-
- const [currentCredits, setCurrentCredits] = useState(160);
- const [amountSpent, setAmountSpent] = useState(session.user?.aiUsage || 0);
-
- const [isLoading, setIsLoading] = useState(true);
-
- const [isTermsAccepted, setIsTermsAccepted] = useState(false);
- const [homeStepsGuide, setHomeStepsGuide] = useState(false);
- const [showPinSetup, setShowPinSetup] = useState(false);
- const [showPinVerify, setShowPinVerify] = useState(false);
-
- const [localSyncQueue, setLocalSyncQueue] = useState([]);
- const [isQueueProcessing, setIsQueueProcessing] = useState(false);
-
-
- function addToLocalSyncQueue(channel: string, data: Record): void {
- const operation: LocalSyncOperation = {
- id: `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`,
- channel,
- data,
- timestamp: Date.now(),
- };
- setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] => [...prev, operation]);
- }
-
- useEffect((): void => {
- if (localSyncQueue.length === 0 || isQueueProcessing) {
- return;
- }
-
- async function processQueue(): Promise {
- setIsQueueProcessing(true);
-
- const queueCopy: LocalSyncOperation[] = [...localSyncQueue];
-
- for (const operation of queueCopy) {
- try {
- await tauri.invoke(operation.channel, operation.data);
- setLocalSyncQueue((prev: LocalSyncOperation[]): LocalSyncOperation[] =>
- prev.filter((op: LocalSyncOperation): boolean => op.id !== operation.id)
- );
- } catch (error) {
- console.error(`[LocalSyncQueue] Failed to process operation ${operation.channel}:`, error);
- }
- }
-
- setIsQueueProcessing(false);
- }
-
- processQueue().then();
- }, [localSyncQueue, isQueueProcessing]);
-
- const homeSteps: GuideStep[] = [
- {
- id: 0,
- x: 50,
- y: 50,
- title: t("homePage.guide.welcome", {name: session.user?.name || ''}),
- content: (
-
-
{t("homePage.guide.step0.description1")}
-
-
{t("homePage.guide.step0.description2")}
-
- ),
- },
- {
- id: 1, position: 'right',
- targetSelector: `[data-guide="left-panel-container"]`,
- title: t("homePage.guide.step1.title"),
- content: (
-
-
-
- :
-
- {t("homePage.guide.step1.addBook")}
-
-
-
: {t("homePage.guide.step1.generateStory")}
-
-
- ),
- },
- {
- id: 2,
- title: t("homePage.guide.step2.title"), position: 'bottom',
- targetSelector: `[data-guide="search-bar"]`,
- content: (
-
-
{t("homePage.guide.step2.description")}
-
- ),
- },
- {
- id: 3,
- title: t("homePage.guide.step3.title"),
- targetSelector: `[data-guide="user-dropdown"]`,
- position: 'auto',
- content: (
-
-
{t("homePage.guide.step3.description")}
-
- ),
- },
- {
- id: 4,
- title: t("homePage.guide.step4.title"),
- content: (
-
-
{t("homePage.guide.step4.description1")}
-
-
{t("homePage.guide.step4.description2")}
-
- ),
- },
- ];
-
- useEffect((): void => {
- checkAuthentification().then();
-
- let unlisten: (() => void) | undefined;
- import('@tauri-apps/api/event').then(function ({listen}) {
- listen('auth-success', function () {
- checkAuthentification().then();
- }).then(function (fn) {
- unlisten = fn;
- });
- });
-
- return (): void => {
- if (unlisten) unlisten();
- };
- }, []);
-
- useEffect((): void => {
- if (session.isConnected) {
- setIsTermsAccepted(session.user?.termsAccepted ?? false);
- setHomeStepsGuide(User.guideTourDone(session.user?.guideTour ?? [], 'home-basic'));
- setIsLoading(false);
- }
- }, [session]);
-
- useEffect((): void => {
- if (session.isConnected) {
- if (currentBook) {
- getLastChapter().then();
- } else {
- refreshBooks().then();
- }
- }
- }, [currentBook]);
-
- useEffect((): void => {
- const diffsFromServer: BookSyncCompare[] = [];
- const diffsToServer: BookSyncCompare[] = [];
-
- serverSyncedBooks.forEach((serverBook: SyncedBook): void => {
- const localBook: SyncedBook | undefined = localSyncedBooks.find((book: SyncedBook): boolean => book.id === serverBook.id);
- if (!localBook) {
- return;
- }
-
- const diff: BookSyncCompare | null = compareBookSyncs(serverBook, localBook);
- if (diff) {
- diffsFromServer.push(diff);
- }
- });
-
- localSyncedBooks.forEach((localBook: SyncedBook): void => {
- const serverBook: SyncedBook | undefined = serverSyncedBooks.find((book: SyncedBook): boolean => book.id === localBook.id);
- if (!serverBook) {
- return;
- }
-
- const diff: BookSyncCompare | null = compareBookSyncs(localBook, serverBook);
- if (diff) {
- diffsToServer.push(diff);
- }
- });
-
- setBookSyncDiffsFromServer(diffsFromServer);
- setBookSyncDiffsToServer(diffsToServer);
- setServerOnlyBooks(serverSyncedBooks.filter((serverBook: SyncedBook):boolean => !localSyncedBooks.find((localBook: SyncedBook):boolean => localBook.id === serverBook.id)))
- setLocalOnlyBooks(localSyncedBooks.filter((localBook: SyncedBook):boolean => !serverSyncedBooks.find((serverBook: SyncedBook):boolean => serverBook.id === localBook.id)))
- }, [localSyncedBooks, serverSyncedBooks]);
-
- useEffect((): void => {
- const diffsFromServer: SeriesSyncCompare[] = [];
- const diffsToServer: SeriesSyncCompare[] = [];
-
- serverSyncedSeries.forEach((serverSeries: SyncedSeries): void => {
- const localSeries: SyncedSeries | undefined = localSyncedSeries.find((series: SyncedSeries): boolean => series.id === serverSeries.id);
- if (!localSeries) {
- return;
- }
-
- const diff: SeriesSyncCompare | null = compareSeriesSyncs(serverSeries, localSeries);
- if (diff) {
- diffsFromServer.push(diff);
- }
- });
-
- localSyncedSeries.forEach((localSeries: SyncedSeries): void => {
- const serverSeries: SyncedSeries | undefined = serverSyncedSeries.find((series: SyncedSeries): boolean => series.id === localSeries.id);
- if (!serverSeries) {
- return;
- }
-
- const diff: SeriesSyncCompare | null = compareSeriesSyncs(localSeries, serverSeries);
- if (diff) {
- diffsToServer.push(diff);
- }
- });
-
- setSeriesSyncDiffsFromServer(diffsFromServer);
- setSeriesSyncDiffsToServer(diffsToServer);
- setServerOnlySeries(serverSyncedSeries.filter((serverSeries: SyncedSeries): boolean => !localSyncedSeries.find((localSeries: SyncedSeries): boolean => localSeries.id === serverSeries.id)));
- setLocalOnlySeries(localSyncedSeries.filter((localSeries: SyncedSeries): boolean => !serverSyncedSeries.find((serverSeries: SyncedSeries): boolean => serverSeries.id === localSeries.id)));
- }, [localSyncedSeries, serverSyncedSeries]);
-
- async function refreshBooks(): Promise {
- try {
- let localBooksResponse: SyncedBook[] = [];
- let serverBooksResponse: SyncedBook[] = [];
-
- if (!isCurrentlyOffline()) {
- if (offlineMode.isDatabaseInitialized) {
- localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
- const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
- const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
- const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
- const serverResponse: SyncedBooksResponse = await System.authPostToServer('books/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
- serverBooksResponse = serverResponse.books;
- await tauri.applyBookTombstones(serverResponse.tombstones);
- } else {
- const serverResponse: SyncedBooksResponse = await System.authPostToServer('books/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
- serverBooksResponse = serverResponse.books;
- }
- } else {
- if (offlineMode.isDatabaseInitialized) {
- localBooksResponse = await tauri.getSyncedBooks() as SyncedBook[];
- }
- }
-
- setServerSyncedBooks(serverBooksResponse);
- setLocalSyncedBooks(localBooksResponse);
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.fetchBooksError"));
- }
- }
- }
-
- async function refreshSeries(): Promise {
- try {
- let localSeriesResponse: SyncedSeries[] = [];
- let serverSeriesResponse: SyncedSeries[] = [];
-
- if (!isCurrentlyOffline()) {
- if (offlineMode.isDatabaseInitialized) {
- localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
- const lastOnlineStr: string | null = localStorage.getItem('lastOnlineTimestamp');
- const lastOnlineTimestamp: number = lastOnlineStr ? parseInt(lastOnlineStr, 10) : 0;
- const localTombstones: RemovedItemRecord[] = await tauri.getTombstonesSince(lastOnlineTimestamp) as RemovedItemRecord[];
- const serverResponse: SyncedSeriesResponse = await System.authPostToServer('series/synced', { lastOnlineTimestamp, tombstones: localTombstones }, session.accessToken, locale);
- serverSeriesResponse = serverResponse.series;
- await tauri.applySeriesTombstones(serverResponse.tombstones);
- } else {
- const serverResponse: SyncedSeriesResponse = await System.authPostToServer('series/synced', { lastOnlineTimestamp: 0, tombstones: [] }, session.accessToken, locale);
- serverSeriesResponse = serverResponse.series;
- }
- } else {
- if (offlineMode.isDatabaseInitialized) {
- localSeriesResponse = await tauri.getSyncedSeries() as SyncedSeries[];
- }
- }
-
- setServerSyncedSeries(serverSeriesResponse);
- setLocalSyncedSeries(localSeriesResponse);
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.fetchSeriesError"));
- }
- }
- }
-
-
- async function handlePinVerifySuccess(userId: string): Promise {
- try {
- const storedToken: string | null = await tauri.getToken();
- const encryptionKey: string | null = await tauri.getUserEncryptionKey(userId);
-
- if (encryptionKey) {
- await tauri.dbInitialize(userId, encryptionKey);
- setOfflineMode(prev => ({...prev, isDatabaseInitialized: true}));
-
- const localUser: UserProps = await tauri.getUserInfo();
- if (localUser && localUser.id) {
- setSession({
- isConnected: true,
- user: localUser,
- accessToken: storedToken || '',
- });
- setShowPinVerify(false);
- setCurrentCredits(localUser.creditsBalance || 0);
- setAmountSpent(localUser.aiUsage || 0);
- } else {
- errorMessage(t("homePage.errors.localDataError"));
- }
- } else {
- errorMessage(t("homePage.errors.encryptionKeyError"));
- }
- } catch (error) {
- console.error('[OfflinePin] Error initializing offline mode:', error);
- errorMessage(t("homePage.errors.offlineModeError"));
- }
- }
-
- async function handleHomeTour(): Promise {
- try {
- if (!isCurrentlyOffline()) {
- const response: boolean = await System.authPostToServer('logs/tour', {
- plateforme: 'desktop',
- tour: 'home-basic'
- },
- session.accessToken,
- locale
- );
- if (response) {
- setSession(User.setNewGuideTour(session, 'home-basic'));
- setHomeStepsGuide(false);
- }
- } else {
- const completedGuides = JSON.parse(localStorage.getItem('completedGuides') || '[]');
- if (!completedGuides.includes('home-basic')) {
- completedGuides.push('home-basic');
- localStorage.setItem('completedGuides', JSON.stringify(completedGuides));
- }
- setSession(User.setNewGuideTour(session, 'home-basic'));
- setHomeStepsGuide(false);
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.termsError"));
- }
- }
- }
-
- async function checkAuthentification(): Promise {
- let token: string | null = null;
-
- try {
- token = await tauri.getToken();
- } catch (e) {
- console.error('Error getting token:', e);
- }
-
- if (token) {
- try {
- const user: UserProps = await System.authGetQueryToServer('user/infos', token, locale);
- if (!user) {
- errorMessage(t("homePage.errors.userNotFound"));
- await tauri.removeToken();
- tauri.logout();
- return;
- }
-
- if (user.id) {
- try {
- const initResult = await tauri.initUser(user.id);
- if (!initResult.success) {
- errorMessage(initResult.error || t("homePage.errors.offlineInitError"));
- return;
- }
- try {
- const offlineStatus = await tauri.offlineModeGet();
- if (!offlineStatus.hasPin) {
- setTimeout(():void => {
- setShowPinSetup(true);
- }, 2000);
- }
- } catch (error) {
- console.error('[Page] Error checking offline mode:', error);
- }
- } catch (error) {
- console.error('[Page] Error initializing user:', error);
- }
- }
- if (user.id) {
- try {
- const dbInitialized: boolean = await initializeDatabase(user.id);
- if (dbInitialized) {
- try {
- await tauri.syncUser({
- userId: user.id,
- username: user.username,
- email: user.email
- });
- } catch (syncError) {
- console.error('[Page] syncUser failed:', syncError);
- errorMessage(t("homePage.errors.syncError"));
- }
- } else {
- errorMessage(t("homePage.errors.dbInitError"));
- }
- } catch (error) {
- console.error('[Page] DB init or sync failed:', error);
- errorMessage(t("homePage.errors.syncError"));
- }
- }
- setSession({
- isConnected: true,
- user: user,
- accessToken: token,
- });
- setCurrentCredits(user.creditsBalance)
- setAmountSpent(user.aiUsage)
- } catch (e: unknown) {
- try {
- const offlineStatus = await tauri.offlineModeGet();
-
- if (offlineStatus.hasPin && offlineStatus.lastUserId) {
- setOfflineMode((prev:OfflineMode):OfflineMode => ({...prev, isOffline: true, isManuallyOffline: true, isNetworkOnline: false}));
- setShowPinVerify(true);
- setIsLoading(false);
- return;
- } else {
- await tauri.removeToken();
- tauri.logout();
- }
- } catch (offlineError) {
- errorMessage(t("homePage.errors.offlineError"));
- }
-
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.authenticationError"));
- }
- }
- } else {
- try {
- const offlineStatus = await tauri.offlineModeGet();
-
- if (offlineStatus.hasPin && offlineStatus.lastUserId) {
- setOfflineMode(prev => ({...prev, isOffline: true, isManuallyOffline: true, isNetworkOnline: false}));
- setShowPinVerify(true);
- setIsLoading(false);
- return;
- }
- } catch (error) {
- errorMessage(t("homePage.errors.authenticationError"));
- }
- tauri.logout();
- }
- }
-
- async function handleTermsAcceptance(): Promise {
- try {
- const response: boolean = await System.authPostToServer(`user/terms/accept`, {
- version: '2025-07-1'
- }, session.accessToken, locale);
- if (response) {
- setIsTermsAccepted(true);
- setHomeStepsGuide(true);
- const newSession: SessionProps = {
- ...session,
- user: {
- ...session?.user as UserProps,
- termsAccepted: true
- }
- }
- setSession(newSession);
- } else {
- errorMessage(t("homePage.errors.termsAcceptError"));
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.termsAcceptError"));
- }
- }
- }
-
- async function getLastChapter(): Promise {
- if (session?.accessToken) {
- try {
- let response: ChapterProps | null
- if (isCurrentlyOffline()){
- if (!offlineMode.isDatabaseInitialized) {
- setCurrentChapter(undefined);
- return;
- }
- response = await tauri.getLastChapter(currentBook?.bookId ?? '')
- } else {
- if (currentBook?.localBook) {
- if (!offlineMode.isDatabaseInitialized) {
- setCurrentChapter(undefined);
- return;
- }
- response = await tauri.getLastChapter(currentBook?.bookId ?? '')
- } else {
- response = await System.authGetQueryToServer(`chapter/last-chapter`, session.accessToken, locale, {bookid: currentBook?.bookId});
- }
- }
- if (response) {
- setCurrentChapter(response)
- } else {
- setCurrentChapter(undefined);
- }
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("homePage.errors.lastChapterError"));
- }
- }
- }
- }
-
- if (isLoading) {
- return (
-
-
-
-

-
-
-
- {t("homePage.loading")}
-
-
-
- )
- }
-
- return (
-
-
-
-
-
-
-
-
-
- {
- homeStepsGuide && !isCurrentlyOffline() &&
- setHomeStepsGuide(false)}/>
- }
- {
- !isTermsAccepted && !isCurrentlyOffline() &&
- }
- {
- showPinSetup && (
- setShowPinSetup(false)}
- onSuccess={():void => {
- setShowPinSetup(false);
- }}
- />
- )
- }
- {
- showPinVerify && (
- {}}
- />
- )
- }
-
-
-
-
-
-
-
- );
-}
-
-export default function Scribe() {
- const [locale, setLocale] = useState<'fr' | 'en'>('fr');
-
- useEffect((): void => {
- const lang: "fr" | "en" | null = System.getCookie('lang') as "fr" | "en" | null;
- if (lang) {
- setLocale(lang);
- }
- }, []);
-
- const messages = messagesMap[locale];
-
- return (
-
-
-
-
-
-
-
-
-
- );
+export default function HomePage() {
+ return ;
}
diff --git a/components/AlertBox.tsx b/components/AlertBox.tsx
deleted file mode 100644
index dae6978..0000000
--- a/components/AlertBox.tsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import {useEffect, useState} from 'react';
-import {createPortal} from 'react-dom';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faCheck, faExclamationTriangle, faInfoCircle, faTimes} from '@fortawesome/free-solid-svg-icons';
-import ConfirmButton from "@/components/form/ConfirmButton";
-import CancelButton from "@/components/form/CancelButton";
-
-export type AlertType = 'alert' | 'danger' | 'informatif' | 'success';
-
-interface AlertBoxProps {
- title: string;
- message: string;
- type: AlertType;
- confirmText?: string;
- cancelText?: string;
- onConfirm: () => Promise;
- onCancel: () => void;
- children?: React.ReactNode;
-}
-
-export default function AlertBox(
- {
- title,
- message,
- type,
- confirmText = 'Confirmer',
- cancelText = 'Annuler',
- onConfirm,
- onCancel,
- children
- }: AlertBoxProps) {
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- return () => setMounted(false);
- }, []);
-
- function getAlertConfig(alertType: AlertType) {
- switch (alertType) {
- case 'alert':
- return {
- background: 'bg-warning',
- borderColor: 'border-warning/30',
- icon: faExclamationTriangle,
- iconBg: 'bg-warning/10'
- };
- case 'danger':
- return {
- background: 'bg-error',
- borderColor: 'border-error/30',
- icon: faTimes,
- iconBg: 'bg-error/10'
- };
- case 'informatif':
- return {
- background: 'bg-info',
- borderColor: 'border-info/30',
- icon: faInfoCircle,
- iconBg: 'bg-info/10'
- };
- case 'success':
- default:
- return {
- background: 'bg-success',
- borderColor: 'border-success/30',
- icon: faCheck,
- iconBg: 'bg-success/10'
- };
- }
- }
-
- const alertSettings = getAlertConfig(type);
-
- const alertContent = (
-
-
-
-
-
-
{message}
- {children}
-
-
-
-
-
-
-
- );
-
- if (!mounted) return null;
-
- return createPortal(alertContent, document.body);
-}
diff --git a/components/AlertStack.tsx b/components/AlertStack.tsx
deleted file mode 100644
index 3958ebc..0000000
--- a/components/AlertStack.tsx
+++ /dev/null
@@ -1,57 +0,0 @@
-'use client';
-
-import {useEffect, useState} from 'react';
-import {createPortal} from 'react-dom';
-import StaticAlert from '@/components/StaticAlert';
-import {Alert} from '@/context/AlertProvider';
-
-interface AlertStackProps {
- alerts: Alert[];
- onClose: (id: string) => void;
-}
-
-export default function AlertStack({alerts, onClose}: AlertStackProps) {
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- return () => setMounted(false);
- }, []);
-
- if (!mounted) return null;
-
- const alertContent = (
-
- {alerts.map((alert, index) => (
-
- onClose(alert.id)}
- />
-
- ))}
-
-
- );
-
- return createPortal(alertContent, document.body);
-}
diff --git a/components/CollapsableArea.tsx b/components/CollapsableArea.tsx
deleted file mode 100644
index ed79ab5..0000000
--- a/components/CollapsableArea.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faChevronDown, faChevronUp, IconDefinition} from "@fortawesome/free-solid-svg-icons";
-import React from "react";
-
-interface CollapsableAreaProps {
- title: string;
- children: React.ReactNode;
- icon?: IconDefinition;
-}
-
-export default function CollapsableArea(
- {
- title,
- children,
- icon,
- }: CollapsableAreaProps) {
- const [isExpanded, setIsExpanded] = React.useState(false);
-
- return (
-
-
-
- {isExpanded && (
-
- {children}
-
- )}
-
- );
-}
diff --git a/components/CollapsableButton.tsx b/components/CollapsableButton.tsx
deleted file mode 100644
index 9954a6a..0000000
--- a/components/CollapsableButton.tsx
+++ /dev/null
@@ -1,33 +0,0 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import React from "react";
-import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
-
-interface CollapsableButtonProps {
- showCollapsable: boolean;
- text: string;
- onClick: () => void;
- icon?: IconDefinition;
-}
-
-export default function CollapsableButton(
- {
- showCollapsable,
- text,
- icon,
- onClick
- }: CollapsableButtonProps) {
- return (
-
- )
-}
diff --git a/components/Collapse.tsx b/components/Collapse.tsx
deleted file mode 100644
index 08d8534..0000000
--- a/components/Collapse.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import {JSX, useState} from "react";
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faChevronDown, faChevronRight} from '@fortawesome/free-solid-svg-icons';
-
-export interface CollapseProps {
- title: string;
- content: JSX.Element;
-}
-
-export default function Collapse({title, content}: CollapseProps) {
- const [isOpen, setIsOpen] = useState(false);
-
- function toggleCollapse(): void {
- setIsOpen(!isOpen);
- }
-
- return (
-
-
- {isOpen && (
-
- )}
-
- );
-}
\ No newline at end of file
diff --git a/components/CreditMeters.tsx b/components/CreditMeters.tsx
deleted file mode 100644
index da83f0b..0000000
--- a/components/CreditMeters.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import React, {useContext} from "react";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faCoins, faDollarSign} from "@fortawesome/free-solid-svg-icons";
-import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
-
-export default function CreditCounter({isCredit}: { isCredit: boolean }) {
- const {totalCredits, totalPrice} = useContext(AIUsageContext)
-
- if (isCredit) {
- return (
-
-
-
- {Math.round(totalCredits)} crédits
-
-
- );
- }
-
- return (
-
-
-
- {totalPrice ? totalPrice.toFixed(2) : '0.00'}
-
-
- );
-}
\ No newline at end of file
diff --git a/components/GuideTour.tsx b/components/GuideTour.tsx
index 314a078..f5a7328 100644
--- a/components/GuideTour.tsx
+++ b/components/GuideTour.tsx
@@ -1,6 +1,8 @@
-import {JSX, useEffect, useMemo, useRef, useState} from 'react';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faXmark} from '@fortawesome/free-solid-svg-icons';
+import React, {JSX, useEffect, useRef, useState} from 'react';
+import {createPortal} from 'react-dom';
+import {X} from 'lucide-react';
+import Button from '@/components/ui/Button';
+import IconButton from '@/components/ui/IconButton';
export type GuidePosition =
'top'
@@ -38,23 +40,33 @@ interface GuideTourProps {
* position, and properties for spotlight rendering.
* @return {string} The CSS background string representing the spotlight effect.
*/
+function getOverlayColor(opacity: number): string {
+ const style: CSSStyleDeclaration = getComputedStyle(document.documentElement);
+ const darkest: string = style.getPropertyValue('--theme-darkest-background').trim() || '#1A1A1A';
+ const hex: string = darkest.replace('#', '');
+ const r: number = parseInt(hex.substring(0, 2), 16);
+ const g: number = parseInt(hex.substring(2, 4), 16);
+ const b: number = parseInt(hex.substring(4, 6), 16);
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
+}
+
function getSpotlightBackground(step: GuideStep): string {
if (step.x !== undefined && step.y !== undefined) {
- return 'rgba(0, 0, 0, 0.5)';
+ return getOverlayColor(0.5);
}
if (!step.targetSelector) {
- return 'rgba(0, 0, 0, 0.5)';
+ return getOverlayColor(0.5);
}
- const element = document.querySelector(step.targetSelector) as HTMLElement | null;
+ const element: HTMLElement | null = document.querySelector(step.targetSelector);
if (!element) {
- return 'rgba(0, 0, 0, 0.5)';
+ return getOverlayColor(0.5);
}
const rect: DOMRect = element.getBoundingClientRect();
const centerX: number = rect.left + rect.width / 2;
const centerY: number = rect.top + rect.height / 2;
const radius: number = Math.max(rect.width, rect.height) / 2 + (step.highlightRadius || 10);
-
- return `radial-gradient(circle at ${centerX}px ${centerY}px, transparent ${radius}px, rgba(0, 0, 0, 0.65) ${radius + 20}px)`;
+
+ return `radial-gradient(circle at ${centerX}px ${centerY}px, transparent ${radius}px, ${getOverlayColor(0.65)} ${radius + 20}px)`;
}
/**
@@ -63,7 +75,13 @@ function getSpotlightBackground(step: GuideStep): string {
* @param {GuideStep} step - An object containing the configuration for positioning the popover, including its x and y coordinates, target selector, and preferred position.
* @return {React.CSSProperties} An object representing the CSS properties to position the popover, including `left`, `top`, and optionally `transform` values.
*/
-function getPopoverPosition(step: GuideStep): React.CSSProperties {
+interface PopoverPosition {
+ left: string;
+ top: string;
+ transform?: string;
+}
+
+function getPopoverPosition(step: GuideStep): PopoverPosition {
if (step.x !== undefined && step.y !== undefined) {
return {
left: `${step.x}%`,
@@ -80,7 +98,7 @@ function getPopoverPosition(step: GuideStep): React.CSSProperties {
};
}
- const element = document.querySelector(step.targetSelector) as HTMLElement | null;
+ const element: HTMLElement | null = document.querySelector(step.targetSelector);
if (!element) {
return {
left: '50%',
@@ -88,12 +106,12 @@ function getPopoverPosition(step: GuideStep): React.CSSProperties {
transform: 'translate(-50%, -50%)'
};
}
-
+
const rect: DOMRect = element.getBoundingClientRect();
const {left, top, width, height} = rect;
- const popoverWidth = 420;
- const popoverHeight = 300;
- const margin = 20;
+ const popoverWidth: number = 420;
+ const popoverHeight: number = 300;
+ const margin: number = 20;
const position: GuidePosition = step.position || 'auto';
switch (position) {
@@ -188,16 +206,17 @@ export default function GuideTour({stepId, steps, onClose, onComplete}: GuideTou
const [currentStep, setCurrentStep] = useState(0);
const [isVisible, setIsVisible] = useState(false);
const [rendered, setRendered] = useState(false);
+ const overlayRef: React.RefObject = useRef(null);
- const filteredSteps: GuideStep[] = useMemo((): GuideStep[] => {
+ const filteredSteps: GuideStep[] = React.useMemo((): GuideStep[] => {
return steps.filter((step: GuideStep): boolean => step.id >= stepId);
}, [steps, stepId]);
const currentStepData: GuideStep = filteredSteps[currentStep];
- const timeoutRef = useRef(null);
+ const timeoutRef: React.RefObject = useRef(null);
- const showStep = (index: number) => {
+ function showStep(index: number): void {
setIsVisible(false);
if (timeoutRef.current) {
@@ -210,7 +229,7 @@ export default function GuideTour({stepId, steps, onClose, onComplete}: GuideTou
const step: GuideStep = filteredSteps[index];
if (step?.targetSelector) {
- const element = document.querySelector(step.targetSelector) as HTMLElement;
+ const element: HTMLElement | null = document.querySelector(step.targetSelector);
if (element) {
element.scrollIntoView({behavior: 'smooth', block: 'center'});
}
@@ -224,42 +243,46 @@ export default function GuideTour({stepId, steps, onClose, onComplete}: GuideTou
}, 50);
}, 600);
}, 200);
- };
+ }
useEffect((): () => void => {
showStep(0);
-
+
return (): void => {
if (timeoutRef.current) {
clearTimeout(timeoutRef.current);
}
};
}, []);
+
+ useEffect((): void => {
+ if (overlayRef.current) {
+ overlayRef.current.style.background = rendered ? getSpotlightBackground(currentStepData) : getOverlayColor(0.5);
+ overlayRef.current.style.opacity = isVisible ? '1' : '0';
+ }
+ }, [rendered, isVisible, currentStepData]);
- const handleNext: () => void = (): void => {
+ function handleNext(): void {
if (currentStep < filteredSteps.length - 1) {
showStep(currentStep + 1);
} else {
onComplete();
}
- };
+ }
- const handlePrevious: () => void = (): void => {
+ function handlePrevious(): void {
if (currentStep > 0) {
showStep(currentStep - 1);
}
- };
+ }
if (!filteredSteps.length || !currentStepData) {
return null;
}
- return (
-
+ return createPortal(
+
{rendered && (
@@ -273,7 +296,8 @@ export default function GuideTour({stepId, steps, onClose, onComplete}: GuideTou
onClose={onClose}
/>
)}
-
+
,
+ document.body
);
}
@@ -309,18 +333,25 @@ function GuidePopup(
onNext: () => void;
onClose: () => void;
}): JSX.Element {
- const positionStyle = useMemo(() => {
- return getPopoverPosition(step);
+ const popupRef: React.RefObject = useRef(null);
+
+ useEffect((): void => {
+ if (popupRef.current) {
+ const pos: PopoverPosition = getPopoverPosition(step);
+ popupRef.current.style.left = pos.left;
+ popupRef.current.style.top = pos.top;
+ popupRef.current.style.transform = pos.transform || '';
+ }
}, [step]);
-
- return (
+
+ return (
-
+
@@ -331,24 +362,18 @@ function GuidePopup(
Étape {currentStep + 1} sur {totalSteps}
- {Array.from({length: totalSteps}).map((_, index) => (
+ {Array.from({length: totalSteps}).map((_: unknown, index: number) => (
))}
-
+
@@ -356,26 +381,18 @@ function GuidePopup(
{step.content}
-
+
{currentStep > 0 ? (
-
) : (
)}
-
+
{currentStep === totalSteps - 1 ? '🎉 Terminer' : 'Continuer →'}
-
+
diff --git a/components/ListItem.tsx b/components/ListItem.tsx
deleted file mode 100644
index cd718d5..0000000
--- a/components/ListItem.tsx
+++ /dev/null
@@ -1,154 +0,0 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faArrowDown, faArrowUp, faCheck, faPen, faTrash, faX, IconDefinition} from "@fortawesome/free-solid-svg-icons";
-import {ChangeEvent, useState} from "react";
-import TextInput from "@/components/form/TextInput";
-
-interface ListItemProps {
- onClick: () => void;
- selectedId: number | string;
- id: number | string;
- icon?: IconDefinition;
- numericalIdentifier?: number;
- isEditable?: boolean;
- text: string;
- handleDelete?: (itemId: string) => void;
- handleUpdate?: (itemId: string, newValue: string, subNewValue: number) => void;
-}
-
-export default function ListItem(
- {
- text,
- selectedId,
- id,
- icon,
- onClick,
- isEditable = false,
- handleDelete,
- numericalIdentifier,
- handleUpdate
- }: ListItemProps) {
-
- const [itemHover, setItemHover] = useState(false);
- const [editMode, setEditMode] = useState(false);
-
- const [newName, setNewName] = useState('');
- const [newChapterOrder, setNewChapterOrder] = useState(numericalIdentifier ?? 0);
-
- function handleEdit(itemName: string): void {
- setNewName(itemName)
- setEditMode(true)
- }
-
- function handleSave(): void {
- if (!handleUpdate) return;
- handleUpdate(id as string, newName, newChapterOrder)
- setEditMode(false);
- }
-
-
- function moveItem(direction: "up" | "down"): void {
- switch (direction) {
- case "up":
- if (newChapterOrder > 0) {
- setNewChapterOrder(newChapterOrder - 1)
- }
- break;
- case "down":
- if (newChapterOrder < 100) {
- setNewChapterOrder(newChapterOrder + 1)
- }
- break;
- default:
- break;
- }
- }
-
- return (
- setItemHover(true)} onMouseLeave={(): void => setItemHover(false)}
- className={`group relative flex items-center p-3 rounded-xl transition-colors duration-200 border-l-4 ${
- selectedId === id
- ? 'bg-secondary border-primary'
- : 'bg-secondary/50 hover:bg-secondary border-transparent'
- }`}>
- {
- (numericalIdentifier != null && newChapterOrder >= 0) && (
-
- {newChapterOrder >= 0 ? newChapterOrder : numericalIdentifier}.
-
- )
- }
- {
- icon && (
-
-
-
- )
- }
-
- {
- editMode ? (
-
-
- ): void => setNewName(e.target.value)}
- placeholder=""
- />
-
-
moveItem('up')}
- disabled={numericalIdentifier === 0}
- >
-
-
-
moveItem("down")}
- >
-
-
-
- ) : (
-
{text}
- )
- }
- {
- !editMode && isEditable && (
-
- handleEdit(text)}
- className="p-1 rounded-lg bg-secondary hover:bg-primary/10 transition-colors">
-
-
- handleDelete && handleDelete(id.toString())}
- className="p-1 rounded-lg bg-secondary hover:bg-error/10 transition-colors">
-
-
-
- )
- }
- {
- editMode && isEditable && (
-
-
-
-
- setEditMode(false)}
- className="p-2 rounded-lg hover:bg-error/10 transition-all">
-
-
-
- )
- }
-
-
- )
-}
diff --git a/components/Modal.tsx b/components/Modal.tsx
deleted file mode 100644
index 65c08d9..0000000
--- a/components/Modal.tsx
+++ /dev/null
@@ -1,93 +0,0 @@
-import React, {ReactNode, useEffect, useState} from 'react';
-import {createPortal} from 'react-dom';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faX} from "@fortawesome/free-solid-svg-icons";
-
-interface ModalProps {
- title: string;
- children: ReactNode;
- size: 'small' | 'medium' | 'large';
- onClose: () => void;
- onConfirm: () => void;
- confirmText?: string;
- cancelText?: string;
- enableFooter?: boolean;
- enableOverflow?: boolean;
-}
-
-export default function Modal(
- {
- title,
- children,
- size,
- onClose,
- onConfirm,
- confirmText = 'Confirm',
- cancelText = 'Cancel',
- enableFooter = true,
- enableOverflow = true,
- }: ModalProps) {
- const [mounted, setMounted] = useState(false);
-
- useEffect(() => {
- setMounted(true);
- return () => setMounted(false);
- }, []);
-
- function getSizeClasses(size: 'small' | 'medium' | 'large'): string {
- switch (size) {
- case 'small':
- return 'w-1/4';
- case 'medium':
- return 'w-1/2';
- case 'large':
- return 'w-3/4';
- default:
- return 'w-1/2';
- }
- }
-
- const modalContent = (
-
-
-
-
{title}
-
-
-
-
-
- {children}
-
- {
- enableFooter && (
-
-
- {cancelText || 'Annuler'}
-
-
- {confirmText || 'Confirmer'}
-
-
- )
- }
-
-
- );
-
- if (!mounted) return null;
-
- return createPortal(modalContent, document.body);
-}
diff --git a/components/NoPicture.tsx b/components/NoPicture.tsx
deleted file mode 100644
index 05fc915..0000000
--- a/components/NoPicture.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import {useContext} from "react";
-import {SessionContext} from "@/context/SessionContext";
-
-export default function NoPicture() {
- const {session} = useContext(SessionContext);
- return (
-
- {session.user?.name && session.user.name.charAt(0).toUpperCase()}
- {session.user?.lastName && session.user.lastName.charAt(0).toUpperCase()}
-
- )
-}
diff --git a/components/PanelHeader.tsx b/components/PanelHeader.tsx
deleted file mode 100644
index fc8728c..0000000
--- a/components/PanelHeader.tsx
+++ /dev/null
@@ -1,88 +0,0 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faSave, faX} from "@fortawesome/free-solid-svg-icons";
-import React from "react";
-import {IconDefinition} from "@fortawesome/fontawesome-svg-core";
-
-interface PanelHeaderProps {
- title: string;
- badge?: string;
- description: string;
- icon?: IconDefinition;
- callBackAction?: () => Promise;
- secondActionIcon?: IconDefinition;
- secondActionCallback?: () => Promise;
- actionIcon?: IconDefinition;
- actionText?: string;
-}
-
-export default function PanelHeader(
- {
- title,
- badge,
- description,
- icon,
- callBackAction,
- secondActionCallback,
- secondActionIcon = faSave,
- actionIcon = faX,
- actionText
- }: PanelHeaderProps) {
- return (
-
-
-
-
- {
- icon && (
-
-
-
- )
- }
- {title}
- {
- badge &&
- {badge}
- }
-
- {description &&
{description}
}
-
-
- {
- actionText && (
-
-
-
-
- {
- actionText && {actionText}
- }
-
- )
- }
- {
- secondActionCallback && (
-
-
-
- )
- }
- {
- callBackAction && actionIcon && !actionText && (
-
-
-
- )
- }
-
-
-
- );
-}
diff --git a/components/QSTextGeneratedPreview.tsx b/components/QSTextGeneratedPreview.tsx
deleted file mode 100644
index 3b45a85..0000000
--- a/components/QSTextGeneratedPreview.tsx
+++ /dev/null
@@ -1,149 +0,0 @@
-import {ReactPortal, useEffect, useState} from 'react';
-import {createPortal} from 'react-dom';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faPaperPlane, faStop, faSync, faX} from '@fortawesome/free-solid-svg-icons';
-import {useTranslations} from "next-intl";
-
-interface QSTextGeneratedPreviewProps {
- onClose: () => void;
- onRefresh: () => void;
- value: string;
- onInsert: () => void;
- isGenerating?: boolean;
- onStop?: () => void;
-}
-
-export default function QSTextGeneratedPreview(
- {
- onClose,
- onRefresh,
- value,
- onInsert,
- isGenerating = false,
- onStop,
- }: QSTextGeneratedPreviewProps): ReactPortal | null {
-
- const [mounted, setMounted] = useState(false);
- const [isVisible, setIsVisible] = useState(false);
- const t = useTranslations();
-
- const filteredValue: string = value.replace(/^starting\.{0,3}\s*/i, '').trim();
- const hasRealContent: boolean = filteredValue.length > 0;
-
- useEffect((): () => void => {
- setMounted(true);
- const timer = setTimeout(() => setIsVisible(true), 10);
- return (): void => {
- setMounted(false);
- setIsVisible(false);
- clearTimeout(timer);
- };
- }, []);
-
- const handleClose = (): void => {
- setIsVisible(false);
- setTimeout(onClose, 300); // Attend la fin de l'animation avant de fermer
- };
-
- if (!mounted) return null;
-
- const modalContent = (
-
-
-
-
-
-
{t("qsTextPreview.title")}
-
-
-
- {isGenerating && onStop ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
-
-
-
-
- {isGenerating && !hasRealContent ? (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) : (
-
- )}
-
-
-
-
-
- {t("qsTextPreview.insert")}
-
-
-
-
- );
-
- return createPortal(modalContent, document.body);
-}
diff --git a/components/ScribeControllerBar.tsx b/components/ScribeControllerBar.tsx
deleted file mode 100644
index 84c7d2c..0000000
--- a/components/ScribeControllerBar.tsx
+++ /dev/null
@@ -1,186 +0,0 @@
-import React, {useContext, useState} from "react";
-import * as tauri from '@/lib/tauri';
-import {ChapterProps, chapterVersions} from "@/lib/models/Chapter";
-import {ChapterContext} from "@/context/ChapterContext";
-import {BookContext} from "@/context/BookContext";
-import System from "@/lib/models/System";
-import UserMenu from "@/components/UserMenu";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faGear, faGlobe, faHome} from "@fortawesome/free-solid-svg-icons";
-import {SelectBoxProps} from "@/shared/interface";
-import {AlertContext} from "@/context/AlertContext";
-import {SessionContext} from "@/context/SessionContext";
-import Book, {BookProps} from "@/lib/models/Book";
-import BookSetting from "@/components/book/settings/BookSetting";
-import SelectBox from "@/components/form/SelectBox";
-import {useTranslations} from "next-intl";
-import {LangContext, LangContextProps} from "@/context/LangContext";
-import CreditCounter from "@/components/CreditMeters";
-import QuillSense from "@/lib/models/QuillSense";
-import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
-import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
-
-export default function ScribeControllerBar() {
- const {chapter, setChapter} = useContext(ChapterContext);
- const {book, setBook} = useContext(BookContext);
- const {errorMessage} = useContext(AlertContext)
- const {session} = useContext(SessionContext);
- const t = useTranslations();
- const {lang, setLang} = useContext(LangContext);
- const {isCurrentlyOffline} = useContext(OfflineContext)
- const {serverSyncedBooks, serverOnlyBooks, localOnlyBooks} = useContext(BooksSyncContext);
-
- const isGPTEnabled: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
- const isGemini: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
- const isAnthropic: boolean = !isCurrentlyOffline() && QuillSense.isOpenAIEnabled(session);
- const isSubTierTwo: boolean = !isCurrentlyOffline() && QuillSense.getSubLevel(session) >= 2;
- const hasAccess: boolean = (isGPTEnabled || isAnthropic || isGemini) || isSubTierTwo;
-
- const [showSettingPanel, setShowSettingPanel] = useState(false);
-
- async function handleChapterVersionChanged(version: number) {
- try {
- let response: ChapterProps | null;
- if (isCurrentlyOffline() || book?.localBook) {
- response = await tauri.getWholeChapter(chapter?.chapterId ?? '', version, book?.bookId ?? '');
- } else {
- response = await System.authGetQueryToServer(`chapter/whole`, session.accessToken, lang, {
- bookid: book?.bookId,
- id: chapter?.chapterId,
- version: version,
- });
- }
- if (!response) {
- errorMessage(t("controllerBar.chapterNotFound"));
- return;
- }
- setChapter(response);
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("controllerBar.unknownChapterError"));
- }
- }
- }
-
- async function getBook(bookId: string): Promise {
- try {
- const response: BookProps = await System.authGetQueryToServer(`book/basic-information`, session.accessToken, lang, {
- id: bookId,
- });
- if (!response) {
- errorMessage(t("controllerBar.bookNotFound"));
- return;
- }
- setBook!!({
- bookId: response.bookId,
- type: response.type,
- title: response.title,
- subTitle: response.subTitle,
- summary: response.summary,
- publicationDate: response.publicationDate,
- desiredWordCount: response.desiredWordCount,
- totalWordCount: response.desiredWordCount,
- quillsenseEnabled: response.quillsenseEnabled,
- tools: response?.tools,
- seriesId: response.seriesId,
- });
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(e.message);
- } else {
- errorMessage(t("controllerBar.unknownBookError"));
- }
- }
- }
-
- function handleLanguageChange(language: "fr" | "en"): void {
- System.setCookie('lang', language, 365);
- const newLang: "en" | "fr" | null = System.getCookie('lang') as "en" | "fr" | null;
- if (newLang) {
- setLang(language);
- }
- }
-
- return (
-
-
-
- {book && (
- setShowSettingPanel(true)}
- className="group p-2 rounded-lg text-muted hover:text-text-primary hover:bg-secondary/50 transition-all hover:scale-110">
-
-
- )}
- {
- book && (
- {
- setBook && setBook(null)
- setChapter && setChapter(undefined)
- }}
- className="group p-2 rounded-lg text-muted hover:text-primary hover:bg-secondary/50 transition-all hover:scale-110">
-
-
- )
- }
-
-
- getBook(e.target.value)}
- data={Book.booksToSelectBox([...serverOnlyBooks, ...localOnlyBooks])} defaultValue={book?.bookId}
- placeholder={t("controllerBar.selectBook")}/>
-
- {chapter && (
-
- handleChapterVersionChanged(parseInt(e.target.value))}
- data={chapterVersions.filter((version: SelectBoxProps): boolean => {
- return !(version.value === '1' && (!hasAccess || book?.quillsenseEnabled === false));
- }).map((version: SelectBoxProps) => {
- return {
- value: version.value.toString(),
- label: t(version.label)
- }
- })} defaultValue={chapter?.chapterContent.version.toString()}/>
-
- )}
-
-
- {
- hasAccess && book?.quillsenseEnabled !== false &&
-
- }
-
-
-
-
-
handleLanguageChange('fr')}
- className={`px-4 py-2 text-sm font-semibold transition-all ${
- lang === 'fr'
- ? 'bg-primary text-text-primary shadow-md'
- : 'bg-transparent text-text-secondary hover:bg-secondary/50 hover:text-text-primary'
- }`}
- >
- FR
-
-
handleLanguageChange('en')}
- className={`px-4 py-2 text-sm font-semibold transition-all ${
- lang === 'en'
- ? 'bg-primary text-text-primary shadow-md'
- : 'bg-transparent text-text-secondary hover:bg-secondary/50 hover:text-text-primary'
- }`}
- >
- EN
-
-
-
-
- {showSettingPanel &&
setShowSettingPanel(false)}/>}
-
- )
-}
\ No newline at end of file
diff --git a/components/ScribeFooterBar.tsx b/components/ScribeFooterBar.tsx
deleted file mode 100644
index 5ccf2d7..0000000
--- a/components/ScribeFooterBar.tsx
+++ /dev/null
@@ -1,111 +0,0 @@
-import {ChapterContext} from "@/context/ChapterContext";
-import {EditorContext} from "@/context/EditorContext";
-import {useContext, useEffect, useState} from "react";
-import {Editor} from "@tiptap/react";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faBook, faChartSimple, faHeart, faSheetPlastic, faHardDrive} from "@fortawesome/free-solid-svg-icons";
-import {useTranslations} from "next-intl";
-import {AlertContext} from "@/context/AlertContext";
-import {BookContext} from "@/context/BookContext";
-import {BooksSyncContext, BooksSyncContextProps} from "@/context/BooksSyncContext";
-import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
-
-export default function ScribeFooterBar() {
- const t = useTranslations();
- const {chapter} = useContext(ChapterContext);
- const {book} = useContext(BookContext);
- const editor: Editor | null = useContext(EditorContext).editor;
- const {errorMessage} = useContext(AlertContext)
- const {offlineMode} = useContext(OfflineContext)
- const {localOnlyBooks,serverSyncedBooks} = useContext(BooksSyncContext);
-
- const [wordsCount, setWordsCount] = useState(0);
-
- useEffect((): void => {
- getWordCount();
- }, [editor?.state.doc.textContent]);
-
- function getWordCount(): void {
- if (editor) {
- try {
- const content: string = editor?.state.doc.textContent;
- const texteNormalise: string = content
- .replace(/'/g, ' ')
- .replace(/-/g, ' ')
- .replace(/\s+/g, ' ')
- .trim();
- const mots: string[] = texteNormalise.split(' ');
- const wordCount: number = mots.filter(
- (mot: string): boolean => mot.length > 0,
- ).length;
- setWordsCount(wordCount);
- } catch (e: unknown) {
- if (e instanceof Error) {
- errorMessage(t('errors.wordCountError') + ` (${e.message})`);
- } else {
- errorMessage(t('errors.wordCountError'));
- }
- }
- }
- }
-
- return (
-
-
-
- {chapter && (
-
-
- {chapter.chapterOrder < 0 ? t('scribeFooterBar.sheet') : `${chapter.chapterOrder}.`}
-
-
- )}
-
- {
- chapter?.title || book?.title || (
- <>
- {t('scribeFooterBar.madeWith')}
-
- >
- )}
-
-
-
- {
- chapter || book ? (
-
-
-
- {t('scribeFooterBar.words')}:
- {wordsCount}
-
-
-
- {Math.ceil(wordsCount / 300)}
-
-
- ) : (
-
- {
- !offlineMode.isOffline &&
-
- {serverSyncedBooks.length}
-
- }
- {(localOnlyBooks.length > 0 || offlineMode.isOffline) && (
-
-
- {localOnlyBooks.length}
-
- )}
-
- )
- }
-
- )
-}
\ No newline at end of file
diff --git a/components/ScribeTopBar.tsx b/components/ScribeTopBar.tsx
deleted file mode 100644
index e6e8b83..0000000
--- a/components/ScribeTopBar.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// Removed Next.js Image import for Electron
-import {useContext} from "react";
-import {BookContext, BookContextProps} from "@/context/BookContext";
-import {useTranslations} from "next-intl";
-import OfflineToggle from "@/components/offline/OfflineToggle";
-
-export default function ScribeTopBar() {
- const book: BookContextProps = useContext(BookContext);
- const t = useTranslations();
- return (
-
-
-
-

-
-
{t("scribeTopBar.scribe")}
-
- {book.book && (
-
-
-
-
- {book.book.title}
-
- {book.book.subTitle && (
-
- {book.book.subTitle}
-
- )}
-
-
-
- )}
-
-
-
-
- )
-}
\ No newline at end of file
diff --git a/components/SettingsPanel.tsx b/components/SettingsPanel.tsx
deleted file mode 100644
index 4dcc8d2..0000000
--- a/components/SettingsPanel.tsx
+++ /dev/null
@@ -1,50 +0,0 @@
-'use client'
-import {ReactNode, useEffect, useState} from "react";
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faX} from "@fortawesome/free-solid-svg-icons";
-import {createPortal} from "react-dom";
-
-interface SettingsPanelProps {
- title: string;
- sidebar: ReactNode;
- children: ReactNode;
- onClose: () => void;
-}
-
-export default function SettingsPanel({title, sidebar, children, onClose}: SettingsPanelProps) {
- const [mounted, setMounted] = useState(false);
-
- useEffect((): void => {
- setMounted(true);
- }, []);
-
- if (!mounted) return null;
-
- return createPortal(
-
-
-
-
-
{title}
-
-
-
-
-
-
-
- {sidebar}
-
-
- {children}
-
-
-
-
-
,
- document.body
- );
-}
diff --git a/components/ShortStoryGenerator.tsx b/components/ShortStoryGenerator.tsx
index 5f75e70..00f1e4a 100644
--- a/components/ShortStoryGenerator.tsx
+++ b/components/ShortStoryGenerator.tsx
@@ -1,28 +1,28 @@
-import {ChangeEvent, RefObject, useContext, useEffect, useRef, useState} from 'react';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
+import React, {ChangeEvent, useContext, useEffect, useRef, useState} from 'react';
+import {
+ BarChart2,
+ BookMarked,
+ BookOpen,
+ ChevronRight,
+ Clock,
+ CloudSun,
+ FileText,
+ GraduationCap,
+ Languages,
+ Loader2,
+ LucideIcon,
+ MessageSquare,
+ Music,
+ Pencil,
+ RotateCw,
+ Square,
+ User,
+ UserPen,
+ Wand2,
+ X
+} from 'lucide-react';
+import {writingLevel} from "@/lib/constants/user";
import {
- faBookBookmark,
- faBookOpen,
- faChartSimple,
- faChevronRight,
- faClock,
- faCloudSun,
- faComments,
- faFileLines,
- faGraduationCap,
- faLanguage,
- faMagicWandSparkles,
- faMusic,
- faPencilAlt,
- faRotateRight,
- faSpinner,
- faStop,
- faUserAstronaut,
- faUserEdit,
- faX
-} from "@fortawesome/free-solid-svg-icons";
-import {writingLevel} from "@/lib/models/User";
-import Story, {
advancedDialogueTypes,
advancedNarrativePersons,
advancedPredefinedType,
@@ -34,23 +34,27 @@ import Story, {
intermediatePredefinedType,
langues,
verbalTime
-} from '@/lib/models/Story';
+} from '@/lib/constants/story';
+import {presetStoryType} from '@/lib/utils/story';
import SelectBox from "@/components/form/SelectBox";
import TextInput from "@/components/form/TextInput";
-import TexteAreaInput from "@/components/form/TexteAreaInput";
-import {SessionContext} from "@/context/SessionContext";
-import System from "@/lib/models/System";
-import {AlertContext} from "@/context/AlertContext";
+import TextAreaInput from "@/components/form/TextAreaInput";
+import {SessionContext, SessionContextProps} from "@/context/SessionContext";
+import {apiPost} from '@/lib/api/client';
+import {AlertContext, AlertContextProps} from "@/context/AlertContext";
import {configs} from "@/lib/configs";
import InputField from "@/components/form/InputField";
import NumberInput from "@/components/form/NumberInput";
+import Button from "@/components/ui/Button";
+import IconButton from "@/components/ui/IconButton";
+import PulseLoader from "@/components/ui/PulseLoader";
import {Editor as TipEditor, EditorContent, useEditor} from "@tiptap/react";
-import Editor from "@/lib/models/Editor";
+import {convertToHtml} from "@/lib/utils/editor";
import StarterKit from "@tiptap/starter-kit";
import Underline from "@tiptap/extension-underline";
import TextAlign from "@tiptap/extension-text-align";
-import QuillSense from "@/lib/models/QuillSense";
-import {useTranslations} from "next-intl";
+import {getSubLevel, isAnthropicEnabled} from "@/lib/utils/quillsense";
+import {useTranslations} from '@/lib/i18n';
import {LangContext, LangContextProps} from "@/context/LangContext";
import {AIUsageContext, AIUsageContextProps} from "@/context/AIUsageContext";
import AdvancedGenerationOptions from "@/components/form/AdvancedGenerationOptions";
@@ -59,33 +63,35 @@ interface ShortStoryGeneratorProps {
onClose: () => void;
}
+interface TabItem {
+ id: number;
+ label: string;
+ icon: LucideIcon;
+}
+
export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps) {
- const {session} = useContext(SessionContext);
- const {errorMessage, infoMessage} = useContext(AlertContext);
- const {lang} = useContext(LangContext)
+ const {session}: SessionContextProps = useContext(SessionContext);
+ const {errorMessage, infoMessage}: AlertContextProps = useContext(AlertContext);
+ const {lang}: LangContextProps = useContext(LangContext)
const t = useTranslations();
- const {setTotalPrice, setTotalCredits} = useContext(AIUsageContext)
+ const {setTotalPrice, setTotalCredits}: AIUsageContextProps = useContext(AIUsageContext)
const [tone, setTone] = useState('');
const [atmosphere, setAtmosphere] = useState('');
const [verbTense, setVerbTense] = useState('0');
const [person, setPerson] = useState('0');
const [characters, setCharacters] = useState('');
- const [language, setLanguage] = useState(
- session.user?.writingLang.toString() ?? '0',
- );
+ const [language, setLanguage] = useState(session.user?.writingLang?.toString() ?? '0');
const [dialogueType, setDialogueType] = useState('0');
const [wordsCount, setWordsCount] = useState(500)
const [directives, setDirectives] = useState('');
- const [authorLevel, setAuthorLevel] = useState(
- session.user?.writingLevel.toString() ?? '0',
- );
+ const [authorLevel, setAuthorLevel] = useState(session.user?.writingLevel?.toString() ?? '0');
const [presetType, setPresetType] = useState('0');
const [activeTab, setActiveTab] = useState(1);
const [progress, setProgress] = useState(25);
- const modalRef: RefObject = useRef(null);
const [isGenerating, setIsGenerating] = useState(false);
+ const progressRef = useRef(null);
const [generatedText, setGeneratedText] = useState('');
const [generatedStoryTitle, setGeneratedStoryTitle] = useState('');
@@ -98,10 +104,16 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
const [useExplicit, setUseExplicit] = useState(false);
const [useSmart, setUseSmart] = useState(false);
- const isAnthropicEnabled: boolean = QuillSense.isAnthropicEnabled(session);
- const isSubTierTwo: boolean = QuillSense.getSubLevel(session) >= 2;
- const hasAccess: boolean = isAnthropicEnabled || isSubTierTwo;
-
+ useEffect((): void => {
+ if (progressRef.current) {
+ progressRef.current.style.width = `${progress}%`;
+ }
+ }, [progress]);
+
+ const anthropicEnabled: boolean = isAnthropicEnabled(session);
+ const isSubTierTwo: boolean = getSubLevel(session) >= 2;
+ const hasAccess: boolean = anthropicEnabled || isSubTierTwo;
+
const editor: TipEditor | null = useEditor({
extensions: [
StarterKit,
@@ -122,7 +134,7 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
}, []);
useEffect((): void => {
- Story.presetStoryType(
+ presetStoryType(
presetType,
setTone,
setAtmosphere,
@@ -140,7 +152,7 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
useEffect((): void => {
if (editor)
- editor.commands.setContent(Editor.convertToHtml(generatedText))
+ editor.commands.setContent(convertToHtml(generatedText))
getWordCount();
}, [editor, generatedText]);
@@ -151,7 +163,7 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
infoMessage(t("shortStoryGenerator.result.abortSuccess"));
}
}
-
+
async function handleGeneration(): Promise {
setIsGenerating(true);
setGeneratedText('');
@@ -219,12 +231,13 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
totalPrice?: number;
totalCost?: number;
} = JSON.parse(line.slice(6));
-
+
if (data.content && data.content !== 'starting') {
accumulatedText += data.content;
setGeneratedText(accumulatedText);
}
+ // Le message final du endpoint avec title, totalPrice, useYourKey, totalCost
if (data.title && data.useYourKey !== undefined && data.totalPrice !== undefined) {
setGeneratedStoryTitle(data.title);
if (data.useYourKey) {
@@ -242,7 +255,7 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
break;
}
}
-
+
setIsGenerating(false);
setHasGenerated(true);
setAbortController(null);
@@ -284,10 +297,10 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
}
async function handleSave(): Promise {
- let content: string = '';
- if (editor) content = editor?.state?.doc.toJSON();
+ let content: Record | string = '';
+ if (editor) content = editor.state.doc.toJSON();
try {
- const bookId: string = await System.authPostToServer(
+ const bookId: string = await apiPost(
`quillsense/generate/add`,
{
title: generatedStoryTitle,
@@ -324,106 +337,102 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
if (!hasAccess) {
return (
+ className="fixed inset-0 flex items-center justify-center p-4 bg-darkest-background/60 z-50 backdrop-blur-md animate-fadeIn">
+ className="relative bg-tertiary text-text-primary rounded-xl overflow-hidden w-full max-w-md p-6">
-
+
{t("shortStoryGenerator.accessDenied.title")}
{t("shortStoryGenerator.accessDenied.message")}
-
+
{t("shortStoryGenerator.accessDenied.close")}
-
+
);
}
-
+
return (
-
-
+
+
-
+
-
+
{t("shortStoryGenerator.title")}
-
-
-
+
-
-
-
+
-
-
- {[
- {id: 1, label: t("shortStoryGenerator.tabs.basics"), icon: faBookOpen},
- {id: 2, label: t("shortStoryGenerator.tabs.structure"), icon: faUserEdit},
- {id: 3, label: t("shortStoryGenerator.tabs.atmosphere"), icon: faCloudSun},
- ...(hasGenerated || isGenerating ? [{
- id: 4,
- label: t("shortStoryGenerator.tabs.result"),
- icon: faFileLines
- }] : [])
- ].map(tab => (
- setActiveTab(tab.id)}
- disabled={isGenerating}
- className={`flex items-center px-6 py-3 font-medium transition-colors ${
- activeTab === tab.id
- ? 'text-primary border-b-2 border-primary bg-primary/5'
- : 'text-text-secondary hover:text-text-primary'
- }`}
- >
-
- {tab.label}
- {tab.id === 4 && isGenerating && !generatedText && (
-
- )}
-
- ))}
-
-
-
+
+
+ {([
+ {id: 1, label: t("shortStoryGenerator.tabs.basics"), icon: BookOpen},
+ {id: 2, label: t("shortStoryGenerator.tabs.structure"), icon: UserPen},
+ {id: 3, label: t("shortStoryGenerator.tabs.atmosphere"), icon: CloudSun},
+ ...(hasGenerated || isGenerating ? [{
+ id: 4,
+ label: t("shortStoryGenerator.tabs.result"),
+ icon: FileText
+ }] : [])
+ ] satisfies TabItem[]).map((tab: TabItem): React.JSX.Element => {
+ const TabIcon: LucideIcon = tab.icon;
+ return (
+ setActiveTab(tab.id)}
+ disabled={isGenerating}
+ className={`flex items-center px-6 py-3 font-medium transition-colors ${
+ activeTab === tab.id
+ ? 'text-primary border-b-2 border-primary bg-primary/5'
+ : 'text-text-secondary hover:text-text-primary'
+ }`}
+ >
+
+ {tab.label}
+ {tab.id === 4 && isGenerating && !generatedText && (
+
+ )}
+
+ );
+ })}
+
+
+
{activeTab === 1 && (
-
+
setAuthorLevel(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setAuthorLevel(e.target.value)}
data={writingLevel}
defaultValue={authorLevel}
/>
}
/>
setPresetType(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setPresetType(e.target.value)}
data={
authorLevel === '1'
? beginnerPredefinedType
@@ -436,18 +445,18 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
}
/>
setLanguage(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setLanguage(e.target.value)}
data={langues}
defaultValue={language}
/>
}
/>
)}
-
+
{activeTab === 2 && (
-
+
setVerbTense(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setVerbTense(e.target.value)}
data={verbalTime}
defaultValue={verbTense}
/>
}
/>
setPerson(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setPerson(e.target.value)}
data={
authorLevel === '1'
? beginnerNarrativePersons
@@ -493,13 +502,13 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
}
/>
-
+
setDialogueType(e.target.value)}
+ onChangeCallBack={(e: ChangeEvent): void => setDialogueType(e.target.value)}
data={
authorLevel === '1'
? beginnerDialogueTypes
@@ -511,12 +520,12 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
/>
}
/>
-
+
) => setDirectives(e.target.value)}
placeholder={t("shortStoryGenerator.placeholders.directives")}
@@ -525,39 +534,35 @@ export default function ShortStoryGenerator({onClose}: ShortStoryGeneratorProps)
/>
)}
-
+
{activeTab === 3 && (
-
-
- ) => setTone(e.target.value)}
- placeholder={t("shortStoryGenerator.placeholders.tone")}
- />
- }
- />
-
-
-
- ) => setAtmosphere(e.target.value)}
- placeholder={t("shortStoryGenerator.placeholders.atmosphere")}
- />
- }
- />
-
+
+ ) => setTone(e.target.value)}
+ placeholder={t("shortStoryGenerator.placeholders.tone")}
+ />
+ }
+ />
) => setAtmosphere(e.target.value)}
+ placeholder={t("shortStoryGenerator.placeholders.atmosphere")}
+ />
+ }
+ />
+
+
)}
-
+
{activeTab === 4 && (
-
+
{generatedStoryTitle || t("shortStoryGenerator.result.title")}
-
+
{isGenerating ? (
-
-
-
+
) : generatedText && (
<>
-
-
-
-
-
-
+
+
>
)}
-
+
{isGenerating && !generatedText ? (
-
-
-
{t("shortStoryGenerator.result.generating")}
-
+
) : (
+ className="rounded-lg p-6 overflow-auto max-h-96 fade-in-text">
)}
-
+
{generatedText && (
-
+
{totalWordsCount} {t("shortStoryGenerator.result.words")}
)}
)}
+
-
-
setActiveTab(Math.max(1, activeTab - 1))}
- className={`px-4 py-2 rounded-xl transition-all duration-200 flex items-center ${
- activeTab > 1 && !isGenerating
- ? 'text-text-secondary hover:text-text-primary hover:bg-secondary hover:scale-105 shadow-sm'
- : 'text-muted cursor-not-allowed'
- }`}
- disabled={activeTab === 1 || isGenerating}
- >
-
+
+
setActiveTab(Math.max(1, activeTab - 1))}
+ disabled={activeTab === 1 || isGenerating}>
+
{t("shortStoryGenerator.navigation.previous")}
-
-
+
+
-
+
{activeTab === 4 && hasGenerated ? t("shortStoryGenerator.navigation.close") : t("shortStoryGenerator.navigation.cancel")}
-
-
+
+
{activeTab < 3 ? (
- setActiveTab(activeTab + 1)}
- disabled={isGenerating}
- className="px-6 py-2.5 rounded-xl bg-primary text-text-primary hover:bg-primary-dark transition-all duration-200 hover:scale-105 flex items-center disabled:opacity-50 shadow-md hover:shadow-lg font-medium"
- >
+ setActiveTab(activeTab + 1)}
+ disabled={isGenerating}>
{t("shortStoryGenerator.navigation.next")}
-
-
+
+
) : activeTab === 3 && (
-
- {isGenerating ? (
- <>
-
- {t("shortStoryGenerator.actions.generating")}
- >
- ) : (
- <>
-
- {t("shortStoryGenerator.actions.generate")}
- >
- )}
-
+
+ {t("shortStoryGenerator.actions.generate")}
+
)}
diff --git a/components/StaticAlert.tsx b/components/StaticAlert.tsx
deleted file mode 100644
index 0e91b92..0000000
--- a/components/StaticAlert.tsx
+++ /dev/null
@@ -1,136 +0,0 @@
-'use client'
-import {useEffect, useState, useRef} from 'react';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {
- faCheckCircle,
- faExclamationCircle,
- faInfoCircle,
- faTimes,
- faTimesCircle
-} from '@fortawesome/free-solid-svg-icons';
-
-interface StaticAlertProps {
- type: 'success' | 'error' | 'info' | 'warning';
- message: string;
- onClose: () => void;
-}
-
-const iconMap = {
- success: faCheckCircle,
- error: faExclamationCircle,
- info: faInfoCircle,
- warning: faTimesCircle,
-};
-
-const bgColorMap = {
- success: 'bg-success',
- error: 'bg-error',
- info: 'bg-info',
- warning: 'bg-warning',
-};
-
-export default function StaticAlert(
- {type, message, onClose}: StaticAlertProps) {
- const [visible, setVisible] = useState(false);
- const onCloseRef = useRef(onClose);
-
- useEffect(() => {
- onCloseRef.current = onClose;
- }, [onClose]);
-
- useEffect(() => {
- setVisible(true);
- const timer = setTimeout(() => {
- setVisible(false);
- setTimeout(() => onCloseRef.current(), 500); // Wait for fade out animation to complete
- }, 4800);
-
- return () => {
- clearTimeout(timer);
- };
- }, []);
-
- const handleClose = () => {
- setVisible(false);
- setTimeout(() => onCloseRef.current(), 1000); // Wait for fade out animation to complete
- };
-
- return (
-
-
-
-
-
-
-
-
- {typeof message === 'string' ? message : String(message ?? 'Une erreur est survenue')}
-
-
-
{
- e.currentTarget.style.transform = 'rotate(90deg)';
- }}
- onMouseLeave={(e) => {
- e.currentTarget.style.transform = 'rotate(0deg)';
- }}
- >
-
-
-
-
-
-
- );
-}
diff --git a/components/SyncBook.tsx b/components/SyncBook.tsx
index 9fdcc29..b75f798 100644
--- a/components/SyncBook.tsx
+++ b/components/SyncBook.tsx
@@ -1,10 +1,10 @@
-import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
-import {faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner} from "@fortawesome/free-solid-svg-icons";
-import {useTranslations} from "next-intl";
+import {Cloud, CloudDownload, CloudUpload, Loader2} from "lucide-react";
+import {useTranslations} from "@/lib/i18n";
import {useState, useEffect, useContext} from "react";
import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
import {SyncType} from "@/context/BooksSyncContext";
import useSyncBooks from "@/hooks/useSyncBooks";
+import IconButton from "@/components/ui/IconButton";
interface SyncBookProps {
bookId: string;
@@ -27,7 +27,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) {
async function upload(): Promise {
if (isOffline) return;
setIsLoading(true);
- const success:boolean = await hookUpload(bookId);
+ const success: boolean = await hookUpload(bookId);
if (success) setCurrentStatus('synced');
setIsLoading(false);
}
@@ -59,9 +59,7 @@ export default function SyncBook({bookId, status}: SyncBookProps) {
if (isLoading) {
return (
-
-
-
+
);
}
@@ -69,57 +67,48 @@ export default function SyncBook({bookId, status}: SyncBookProps) {
return (
{currentStatus === 'synced' && (
-
-
-
+
)}
-
+
{currentStatus === 'local-only' && (
-
-
-
+ tooltip={t("bookCard.localOnly")}
+ />
)}
{currentStatus === 'server-only' && (
-
-
-
+ tooltip={t("bookCard.serverOnly")}
+ />
)}
{currentStatus === 'to-sync-from-server' && (
-
-
-
+ tooltip={t("bookCard.toSyncFromServer")}
+ />
)}
{currentStatus === 'to-sync-to-server' && (
-
-
-
+ tooltip={t("bookCard.toSyncToServer")}
+ />
)}
);
diff --git a/components/SyncSeries.tsx b/components/SyncSeries.tsx
index 114121b..89ce2f2 100644
--- a/components/SyncSeries.tsx
+++ b/components/SyncSeries.tsx
@@ -1,131 +1,115 @@
-import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
-import { faCloud, faCloudArrowDown, faCloudArrowUp, faSpinner } from "@fortawesome/free-solid-svg-icons";
-import { useTranslations } from "next-intl";
-import { useState, useContext, useEffect } from "react";
-import OfflineContext, { OfflineContextType } from "@/context/OfflineContext";
-import { SeriesSyncType } from "@/context/SeriesSyncContext";
-import useSyncSeries from "@/hooks/useSyncSeries";
-
-interface SyncSeriesProps {
- seriesId: string;
- status: SeriesSyncType;
-}
-
-export default function SyncSeries({ seriesId, status }: SyncSeriesProps) {
- const t = useTranslations();
- const { isCurrentlyOffline } = useContext(OfflineContext);
- const [isLoading, setIsLoading] = useState(false);
- const [currentStatus, setCurrentStatus] = useState(status);
- const { upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer } = useSyncSeries();
-
- // Synchroniser le state local avec le prop quand il change (ex: après sync auto)
- useEffect(() => {
- setCurrentStatus(status);
- }, [status]);
-
- const isOffline: boolean = isCurrentlyOffline();
-
- async function upload(event: React.MouseEvent): Promise {
- event.stopPropagation();
- if (isOffline) return;
- setIsLoading(true);
- const success: boolean = await hookUpload(seriesId);
- if (success) setCurrentStatus('synced');
- setIsLoading(false);
- }
-
- async function download(event: React.MouseEvent): Promise {
- event.stopPropagation();
- if (isOffline) return;
- setIsLoading(true);
- const success = await hookDownload(seriesId);
- if (success) setCurrentStatus('synced');
- setIsLoading(false);
- }
-
- async function syncFromServer(event: React.MouseEvent): Promise {
- event.stopPropagation();
- if (isOffline) return;
- setIsLoading(true);
- const success = await hookSyncFromServer(seriesId);
- if (success) setCurrentStatus('synced');
- setIsLoading(false);
- }
-
- async function syncToServer(event: React.MouseEvent): Promise {
- event.stopPropagation();
- if (isOffline || isLoading) return;
- setIsLoading(true);
- const success = await hookSyncToServer(seriesId);
- if (success) setCurrentStatus('synced');
- setIsLoading(false);
- }
-
- if (isLoading) {
- return (
-
-
-
-
-
- );
- }
-
- return (
-
- {currentStatus === 'synced' && (
-
-
-
- )}
-
- {currentStatus === 'local-only' && (
-
-
-
- )}
- {currentStatus === 'server-only' && (
-
-
-
- )}
- {currentStatus === 'to-sync-from-server' && (
-
-
-
- )}
- {currentStatus === 'to-sync-to-server' && (
-
-
-
- )}
-
- );
-}
+import {Cloud, CloudDownload, CloudUpload, Loader2} from "lucide-react";
+import {useTranslations} from "@/lib/i18n";
+import {useState, useContext, useEffect} from "react";
+import OfflineContext, {OfflineContextType} from "@/context/OfflineContext";
+import {SeriesSyncType} from "@/context/SeriesSyncContext";
+import useSyncSeries from "@/hooks/useSyncSeries";
+import IconButton from "@/components/ui/IconButton";
+
+interface SyncSeriesProps {
+ seriesId: string;
+ status: SeriesSyncType;
+}
+
+export default function SyncSeries({seriesId, status}: SyncSeriesProps) {
+ const t = useTranslations();
+ const {isCurrentlyOffline} = useContext(OfflineContext);
+ const [isLoading, setIsLoading] = useState(false);
+ const [currentStatus, setCurrentStatus] = useState(status);
+ const {upload: hookUpload, download: hookDownload, syncFromServer: hookSyncFromServer, syncToServer: hookSyncToServer} = useSyncSeries();
+
+ useEffect(() => {
+ setCurrentStatus(status);
+ }, [status]);
+
+ const isOffline: boolean = isCurrentlyOffline();
+
+ async function upload(): Promise {
+ if (isOffline) return;
+ setIsLoading(true);
+ const success: boolean = await hookUpload(seriesId);
+ if (success) setCurrentStatus('synced');
+ setIsLoading(false);
+ }
+
+ async function download(): Promise {
+ if (isOffline) return;
+ setIsLoading(true);
+ const success = await hookDownload(seriesId);
+ if (success) setCurrentStatus('synced');
+ setIsLoading(false);
+ }
+
+ async function syncFromServer(): Promise {
+ if (isOffline) return;
+ setIsLoading(true);
+ const success = await hookSyncFromServer(seriesId);
+ if (success) setCurrentStatus('synced');
+ setIsLoading(false);
+ }
+
+ async function syncToServer(): Promise {
+ if (isOffline || isLoading) return;
+ setIsLoading(true);
+ const success = await hookSyncToServer(seriesId);
+ if (success) setCurrentStatus('synced');
+ setIsLoading(false);
+ }
+
+ if (isLoading) {
+ return (
+ e.stopPropagation()}>
+
+
+ );
+ }
+
+ return (
+ e.stopPropagation()}>
+ {currentStatus === 'synced' && (
+
+ )}
+
+ {currentStatus === 'local-only' && (
+
+ )}
+ {currentStatus === 'server-only' && (
+
+ )}
+ {currentStatus === 'to-sync-from-server' && (
+
+ )}
+ {currentStatus === 'to-sync-to-server' && (
+
+ )}
+
+ );
+}
diff --git a/components/TermsOfUse.tsx b/components/TermsOfUse.tsx
index 948ca61..01a8725 100644
--- a/components/TermsOfUse.tsx
+++ b/components/TermsOfUse.tsx
@@ -1,25 +1,28 @@
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faExternalLinkAlt, faFileContract} from '@fortawesome/free-solid-svg-icons';
-// Removed Next.js router and Link imports for Electron
+import React from 'react';
+import {ExternalLink, FileText} from 'lucide-react';
+import Button from '@/components/ui/Button';
+import {AppRouterInstance, Link, useRouter} from '@/lib/navigation';
interface TermsOfUseProps {
onAccept: () => void;
}
export default function TermsOfUse({onAccept}: TermsOfUseProps) {
+ const router: AppRouterInstance = useRouter();
+
function handleAcceptTerm(): void {
onAccept();
}
return (
+ className="fixed inset-0 z-50 bg-darkest-background/90 backdrop-blur-sm flex items-center justify-center p-6 font-['Lora']">
-
+ className="bg-tertiary border border-primary/40 rounded-2xl max-w-2xl w-full max-h-[90vh] overflow-hidden">
+
-
+
Termes d'utilisation
@@ -52,7 +55,7 @@ export default function TermsOfUse({onAccept}: TermsOfUseProps) {
-
+
Documentation complète
@@ -67,14 +70,14 @@ export default function TermsOfUse({onAccept}: TermsOfUseProps) {
className="inline-flex items-center space-x-2 text-primary hover:text-primary-light transition-colors duration-200 font-medium"
>
Consulter les termes complets
-
+
-
+
@@ -90,28 +93,23 @@ export default function TermsOfUse({onAccept}: TermsOfUseProps) {
-
+
-
+
Décision requise pour continuer
diff --git a/components/TwoFactorSetup.tsx b/components/TwoFactorSetup.tsx
index 7412656..9ec2a9b 100644
--- a/components/TwoFactorSetup.tsx
+++ b/components/TwoFactorSetup.tsx
@@ -1,196 +1,198 @@
-'use client';
-
-import {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from 'react';
-import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
-import {faApple, faGooglePlay} from '@fortawesome/free-brands-svg-icons';
-import {faCheck, faKey, faMobileAlt, faQrcode} from '@fortawesome/free-solid-svg-icons';
-import System from "@/lib/models/System";
-import {AlertContext, AlertContextProps} from "@/context/AlertContext";
-import {FormResponse} from "@/shared/interface";
-import {SessionContext} from "@/context/SessionContext";
-import TextInput from "@/components/form/TextInput";
-
-export default function TwoFactorSetup({setShowSetup}: { setShowSetup: Dispatch
> }) {
- const {session} = useContext(SessionContext);
- const alert: AlertContextProps = useContext(AlertContext);
-
- const [step, setStep] = useState(1);
- const [token, setToken] = useState('')
- const [qrCode, setQrCode] = useState(null);
- const [loadingQRCode, setLoadingQRCode] = useState(false);
-
- async function getQRCode() {
- try {
- const response: { qrCode: string } = await System.authPostToServer('twofactor/setup', {
- email: session?.user?.email,
- }, session?.accessToken ?? '');
- setQrCode(response.qrCode);
- } catch (e: any) {
- alert.errorMessage(e.message);
- console.error(e);
- }
- }
-
- async function handleNextStep() {
- if (step === 3) {
- await validateToken();
- } else if (step === 1) {
- if (qrCode === null) {
- getQRCode();
- }
- setStep((prev: number) => Math.min(prev + 1, 3));
- } else {
- setStep((prev: number) => Math.min(prev + 1, 3));
- }
- }
-
- async function validateToken() {
- try {
- const response: FormResponse = await System.authPostToServer('twofactor/activate', {
- email: session?.user?.email, token: token
- }, session?.accessToken ?? '');
- if (response.valid) {
- alert.successMessage(response.message ?? '');
- setShowSetup(false);
- }
- } catch (e: any) {
- alert.errorMessage(e.message);
- console.error(e);
- }
- }
-
- function handlePrevStep() {
- setStep((prev) => Math.max(prev - 1, 1));
- }
-
- function getProgressClass(currentStep: number) {
- return `flex-grow h-2.5 rounded-full transition-all duration-300 ${
- step >= currentStep ? 'bg-primary shadow-sm' : 'bg-secondary/50'
- }`;
- }
-
- return (
-
-
- Setup Two-Factor Authentication
-
-
- {/* Step Indicator */}
-
-
- {/* Step Content */}
-
- {step === 1 && (
-
-
- Follow these steps to enable two-factor authentication for your account:
-
-
- -
-
- Download a two-factor authentication app like Google Authenticator or Authy.
-
- -
-
- Open the app and select the option to scan a QR code.
-
- -
-
- Proceed to the next step to scan the QR code provided.
-
-
-
-
- )}
- {step === 2 && (
-
-
- Scan the QR code below with your authentication app to link your account.
-
-
-
- {loadingQRCode ? (
-
Loading QR Code...
- ) : qrCode ? (
-

- ) : (
-
Failed to load QR Code.
- )}
-
-
-
- Having trouble? Make sure your app supports QR code scanning.
-
-
- )}
- {step === 3 && (
-
-
- Enter the 6-digit code generated by your authentication app to verify the setup.
-
-
- ) => setToken(e.target.value)}
- placeholder="Enter 6-digit code"
- />
-
-
-
- )}
-
-
- {/* Navigation Buttons */}
-
-
- Back
-
-
- {step === 3 ? 'Finish' : 'Next'}
-
-
-
- );
-}
+'use client';
+
+import {ChangeEvent, Dispatch, SetStateAction, useContext, useState} from 'react';
+import {Check, KeyRound, Smartphone, QrCode} from 'lucide-react';
+import {apiPost} from "@/lib/api/client";
+import {AlertContext, AlertContextProps} from "@/context/AlertContext";
+import {FormResponse} from "@/shared/interface";
+import {SessionContext} from "@/context/SessionContext";
+import TextInput from "@/components/form/TextInput";
+import InputField from "@/components/form/InputField";
+import Button from "@/components/ui/Button";
+
+const AppleIcon = () => (
+
+);
+
+const GooglePlayIcon = () => (
+
+);
+
+export default function TwoFactorSetup({setShowSetup}: { setShowSetup: Dispatch> }) {
+ const {session} = useContext(SessionContext);
+ const alert: AlertContextProps = useContext(AlertContext);
+
+ const [step, setStep] = useState(1);
+ const [token, setToken] = useState('')
+ const [qrCode, setQrCode] = useState(null);
+ const [loadingQRCode, setLoadingQRCode] = useState(false);
+
+ async function getQRCode() {
+ try {
+ const response: { qrCode: string } = await apiPost('twofactor/setup', {
+ email: session?.user?.email,
+ }, session?.accessToken ?? '');
+ setQrCode(response.qrCode);
+ } catch (e: unknown) {
+ if (e instanceof Error) alert.errorMessage(e.message);
+ }
+ }
+
+ async function handleNextStep() {
+ if (step === 3) {
+ await validateToken();
+ } else if (step === 1) {
+ if (qrCode === null) {
+ getQRCode();
+ }
+ setStep((prev: number) => Math.min(prev + 1, 3));
+ } else {
+ setStep((prev: number) => Math.min(prev + 1, 3));
+ }
+ }
+
+ async function validateToken() {
+ try {
+ const response: FormResponse = await apiPost('twofactor/activate', {
+ email: session?.user?.email, token: token
+ }, session?.accessToken ?? '');
+ if (response.valid) {
+ alert.successMessage(response.message ?? '');
+ setShowSetup(false);
+ }
+ } catch (e: unknown) {
+ if (e instanceof Error) alert.errorMessage(e.message);
+ }
+ }
+
+ function handlePrevStep() {
+ setStep((prev) => Math.max(prev - 1, 1));
+ }
+
+ function getProgressClass(currentStep: number) {
+ return `flex-grow h-2.5 rounded-full transition-all duration-300 ${
+ step >= currentStep ? 'bg-primary shadow-sm' : 'bg-secondary/50'
+ }`;
+ }
+
+ return (
+
+
+ Setup Two-Factor Authentication
+
+
+
+
+
+ {step === 1 && (
+