diff --git a/app/globals.css b/app/globals.css index 096d817..3bd0318 100644 --- a/app/globals.css +++ b/app/globals.css @@ -22,6 +22,10 @@ --color-gray-light: #A0A0A0; --color-gray-dark: #404040; + /* Aliases pour compatibilité */ + --color-textPrimary: #FFFFFF; + --color-textSecondary: #B0B0B0; + /* Font Family */ --font-family-lora: 'Lora', Georgia, serif; --font-family-lora-italic: 'Lora Italic', serif; diff --git a/app/login/LoginWrapper.tsx b/app/login/LoginWrapper.tsx new file mode 100644 index 0000000..1898e75 --- /dev/null +++ b/app/login/LoginWrapper.tsx @@ -0,0 +1,77 @@ +'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 {SessionContext} from "@/context/SessionContext"; +import {AlertContext} from "@/context/AlertContext"; + +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 messages = messagesMap[locale]; + + const [session, setSession] = useState({ + isConnected: false, + user: null, + accessToken: '' + }) + + useEffect((): void => { + checkAuthentification().then() + }, []); + + useEffect((): void => { + if (session.isConnected) { + // Pas de router.push dans Electron, le main process gère + if (!window.electron) { + window.location.href = '/'; + } + } + }, [session]); + + async function checkAuthentification(): Promise { + const language: "fr" | "en" | null = System.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('') + }}/> + } +
+
+
+
+
+ ) +} \ No newline at end of file diff --git a/app/login/layout.tsx b/app/login/layout.tsx new file mode 100755 index 0000000..19c06d7 --- /dev/null +++ b/app/login/layout.tsx @@ -0,0 +1,22 @@ +import "../globals.css"; +import {ReactNode} from "react"; +import LoginWrapper from "@/app/login/LoginWrapper"; + +export default function RootLayout({ + children, + }: Readonly<{ + children: ReactNode; +}>) { + return ( + + + ERitors - Connexion + + + + + + + + ); +} diff --git a/app/login/login/LoginForm.tsx b/app/login/login/LoginForm.tsx new file mode 100755 index 0000000..7a442f5 --- /dev/null +++ b/app/login/login/LoginForm.tsx @@ -0,0 +1,131 @@ +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 {SessionContext, SessionContextProps} from "@/context/SessionContext"; +import {AlertContext} from "@/context/AlertContext"; +import {useTranslations} from "next-intl"; +import {LangContext, LangContextProps} from "@/context/LangContext"; + +export default function LoginForm() { + const {errorMessage} = useContext(AlertContext); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const {setSession} = useContext(SessionContext); + 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; + } + // Stocker le token dans electron-store via IPC + if (window.electron) { + await window.electron.setToken(response); + window.electron.loginSuccess(response); + } else { + // Fallback pour le mode dev web + System.setCookie('token', response, 30); + const token: string | null = System.getCookie('token'); + if (!token) { + errorMessage(t('loginForm.error.connection')); + setIsLoading(false); + return; + } + setSession({isConnected: true, user: null, accessToken: token}) + } + } catch (e: unknown) { + if (e instanceof Error) { + errorMessage(t('loginForm.error.server')); + } else { + errorMessage(t('loginForm.error.unknown')); + } + } finally { + setIsLoading(false); + } + } + + return ( +
+
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+ +
+
+ + + {t('loginForm.fields.password.forgot')} + +
+
+ + setPassword(e.target.value)} + required + /> +
+
+ + +
+ ) +} diff --git a/app/login/login/SocialForm.tsx b/app/login/login/SocialForm.tsx new file mode 100755 index 0000000..3cecdba --- /dev/null +++ b/app/login/login/SocialForm.tsx @@ -0,0 +1,135 @@ +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faApple, faFacebookF, faGoogle} from "@fortawesome/free-brands-svg-icons"; +import React, {useContext, useEffect} from "react"; +import Link from "next/link"; +import System from "@/lib/models/System"; +import {AlertContext} from "@/context/AlertContext"; +import {configs} from "@/lib/configs"; +import {SessionContext, SessionContextProps} from "@/context/SessionContext"; +import {useTranslations} from "next-intl"; +import {LangContext, LangContextProps} from "@/context/LangContext"; + +export default function SocialForm() { + const {errorMessage} = useContext(AlertContext); + const {setSession} = useContext(SessionContext) + 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 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; + } + System.setCookie('token', response, 30); + const token: string | null = System.getCookie('token'); + if (!token) { + errorMessage(t('socialForm.error.connection')); + return; + } + setSession({isConnected: true, user: null, accessToken: token}); + } + } + + 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')); + } + System.setCookie('token', response, 30); + const token: string | null = System.getCookie('token'); + if (!token) { + errorMessage(t('socialForm.error.connection')); + return; + } + setSession({isConnected: true, user: null, accessToken: token}); + } + } + + 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; + } + System.setCookie('token', response, 30); + const token: string | null = System.getCookie('token'); + if (!token) { + errorMessage(t('socialForm.error.connection')); + return; + } + setSession({isConnected: true, user: null, accessToken: token}); + } + } + + return ( +
+ + + + + + + + + + + +
+ ) +} diff --git a/app/login/login/page.tsx b/app/login/login/page.tsx new file mode 100755 index 0000000..823f41d --- /dev/null +++ b/app/login/login/page.tsx @@ -0,0 +1,84 @@ +'use client' +import {useContext} from 'react'; +import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; +import {faEnvelope} 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"; + +export default function LoginPage() { + const t = useTranslations(); + const {lang, setLang} = useContext(LangContext); + + const toggleLanguage = () => { + const newLang = lang === 'fr' ? 'en' : 'fr'; + setLang(newLang); + System.setCookie('lang', newLang, 365); + }; + + return ( +
+
+
+
+ ERitors +
+
+ +
+ +
+
+
+ +
+
+

{t('loginPage.title')}

+

{t('loginPage.welcome')}

+
+ +
+
+
+
+
+ {t('loginPage.orSocial')} +
+
+
+ + + + +
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/login/register/StepOne.tsx b/app/login/register/StepOne.tsx new file mode 100755 index 0000000..9ed0dbc --- /dev/null +++ b/app/login/register/StepOne.tsx @@ -0,0 +1,205 @@ +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 ( +
+
+ +
+ + setFirstName(e.target.value)} + required + /> +
+
+
+ +
+ + setLastName(e.target.value)} + required + /> +
+
+
+ +
+ + setUsername(e.target.value)} + required + /> +
+

{t('registerStepOne.fields.username.note')}

+
+
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+ +
+ +
+ + setPassword(e.target.value)} + required + /> +
+
+
+ +
+ + setRepeatPassword(e.target.value)} + required + /> +
+
+ +
+ ) +} diff --git a/app/login/register/StepTree.tsx b/app/login/register/StepTree.tsx new file mode 100755 index 0000000..2b52512 --- /dev/null +++ b/app/login/register/StepTree.tsx @@ -0,0 +1,114 @@ +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')}

+
+
+ + + +
+
+ ) + ) +} diff --git a/app/login/register/page.tsx b/app/login/register/page.tsx new file mode 100755 index 0000000..5a6d9dd --- /dev/null +++ b/app/login/register/page.tsx @@ -0,0 +1,95 @@ +'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 ( +
+
+
+
+ ERitors +
+
+ +
+
+
+
+ +
+
+

{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 diff --git a/app/login/reset-password/page.tsx b/app/login/reset-password/page.tsx new file mode 100755 index 0000000..33ba747 --- /dev/null +++ b/app/login/reset-password/page.tsx @@ -0,0 +1,277 @@ +'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 ( +
+
+
+
+ ERitors +
+
+
+
+
+
+ +
+
+

{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 && ( +
+
+ +
+ + setEmail(e.target.value)} + required + /> +
+
+ +
+ + + + + {t('resetPassword.backToLogin')} + +
+
+ )} + + {step === 2 && ( +
+
+ +
+ + setVerificationCode(e.target.value)} + disabled={isConfirmed} + required + /> +
+
+ + {isConfirmed && ( +
+ +
+ + setNewPassword(e.target.value)} + required + /> +
+
+ )} + +
+ + + +
+
+ )} + + {step === 3 && ( +
+
+

+ {t('resetPassword.success')} +

+
+ +
+ )} +
+
+
+
+ ) +} diff --git a/app/page.tsx b/app/page.tsx index 86bb502..9a44f55 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -181,12 +181,33 @@ function ScribeContent() { } async function checkAuthentification(): Promise { - const token: string | null = System.getCookie('token'); + // Essayer de récupérer le token depuis electron-store en priorité + let token: string | null = null; + + if (typeof window !== 'undefined' && window.electron) { + try { + token = await window.electron.getToken(); + } catch (e) { + console.error('Error getting token from electron:', e); + } + } + + // Fallback sur les cookies si pas d'Electron + if (!token) { + token = System.getCookie('token'); + } + if (token) { try { const user: UserProps = await System.authGetQueryToServer('user/infos', token, locale); if (!user) { errorMessage(t("homePage.errors.userNotFound")); + // Token invalide, supprimer et logout + if (window.electron) { + await window.electron.removeToken(); + window.electron.logout(); + } + return; } setSession({ isConnected: true, @@ -201,12 +222,19 @@ function ScribeContent() { } else { errorMessage(t("homePage.errors.authenticationError")); } - // TODO: Afficher la fenêtre de login Electron - console.log('Pas de session - afficher login Electron'); + // Token invalide/erreur auth, supprimer et logout + if (window.electron) { + await window.electron.removeToken(); + window.electron.logout(); + } } } else { - // TODO: Afficher la fenêtre de login Electron - console.log('Pas de token - afficher login Electron'); + // Pas de token - en Electron cela ne devrait jamais arriver + // car main.ts vérifie le token avant d'ouvrir mainWindow + // Si on arrive ici, c'est une erreur - fermer et ouvrir login + if (window.electron) { + window.electron.logout(); + } } } diff --git a/components/UserMenu.tsx b/components/UserMenu.tsx index 22445d5..97f0148 100644 --- a/components/UserMenu.tsx +++ b/components/UserMenu.tsx @@ -30,9 +30,17 @@ export default function UserMenu() { }; }, [isProfileMenuOpen]); - function handleLogout(): void { + async function handleLogout(): Promise { System.removeCookie("token"); - document.location.href = "https://eritors.com/login"; + + // Si dans Electron, utiliser IPC pour logout + if (window.electron) { + await window.electron.removeToken(); + window.electron.logout(); + } else { + // Fallback web + document.location.href = "https://eritors.com/login"; + } } return ( diff --git a/package-lock.json b/package-lock.json index 8e89748..1a93da2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "antd": "^5.28.1", "autoprefixer": "^10.4.22", "axios": "^1.13.2", + "electron-store": "^11.0.2", "i18next": "^25.6.2", "js-cookie": "^3.0.5", "next": "^16.0.3", @@ -39,6 +40,7 @@ "tailwindcss": "^4.1.17" }, "devDependencies": { + "@types/electron-store": "^1.3.1", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", @@ -3710,6 +3712,16 @@ "@types/ms": "*" } }, + "node_modules/@types/electron-store": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@types/electron-store/-/electron-store-1.3.1.tgz", + "integrity": "sha512-RvEAlIWcy7ATEMeyw481SdnuceN6Pd2Qh5KSW5NohwtY1t1uP0MmC3Cvoszd+ueGLqTKCpRwhCJY4qdER5QQVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/fs-extra": { "version": "9.0.13", "resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-9.0.13.tgz", @@ -3943,6 +3955,45 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/ajv-keywords": { "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", @@ -4212,6 +4263,16 @@ "node": ">= 4.0.0" } }, + "node_modules/atomically": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.0.tgz", + "integrity": "sha512-+gDffFXRW6sl/HCwbta7zK4uNqbPjv4YJEAdz7Vu+FLQHe77eZ4bvbJGi4hE0QPeJlMYMA3piXEr1UL3dAwx7Q==", + "license": "MIT", + "dependencies": { + "stubborn-fs": "^2.0.0", + "when-exit": "^2.1.4" + } + }, "node_modules/autoprefixer": { "version": "10.4.22", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.22.tgz", @@ -4912,6 +4973,75 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/conf": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/conf/-/conf-15.0.2.tgz", + "integrity": "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw==", + "license": "MIT", + "dependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "atomically": "^2.0.3", + "debounce-fn": "^6.0.0", + "dot-prop": "^10.0.0", + "env-paths": "^3.0.0", + "json-schema-typed": "^8.0.1", + "semver": "^7.7.2", + "uint8array-extras": "^1.5.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/conf/node_modules/env-paths": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-3.0.0.tgz", + "integrity": "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==", + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/conf/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/conf/node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/config-file-ts": { "version": "0.2.8-rc1", "resolved": "https://registry.npmjs.org/config-file-ts/-/config-file-ts-0.2.8-rc1.tgz", @@ -5072,6 +5202,21 @@ "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", "license": "MIT" }, + "node_modules/debounce-fn": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/debounce-fn/-/debounce-fn-6.0.0.tgz", + "integrity": "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -5318,6 +5463,36 @@ "node": ">=8" } }, + "node_modules/dot-prop": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-10.1.0.tgz", + "integrity": "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q==", + "license": "MIT", + "dependencies": { + "type-fest": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz", + "integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dotenv": { "version": "16.6.1", "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", @@ -5535,6 +5710,37 @@ "node": ">= 10.0.0" } }, + "node_modules/electron-store": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/electron-store/-/electron-store-11.0.2.tgz", + "integrity": "sha512-4VkNRdN+BImL2KcCi41WvAYbh6zLX5AUTi4so68yPqiItjbgTjqpEnGAqasgnG+lB6GuAyUltKwVopp6Uv+gwQ==", + "license": "MIT", + "dependencies": { + "conf": "^15.0.2", + "type-fest": "^5.0.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/electron-store/node_modules/type-fest": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.2.0.tgz", + "integrity": "sha512-xxCJm+Bckc6kQBknN7i9fnP/xobQRsRQxR01CztFkp/h++yfVxUUcmMgfR2HttJx/dpWjS9ubVuyspJv24Q9DA==", + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.254", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.254.tgz", @@ -5843,6 +6049,22 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, "node_modules/fd-slicer": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", @@ -6763,6 +6985,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-typed": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.1.tgz", + "integrity": "sha512-XQmWYj2Sm4kn4WeTYvmpKEbyPsL7nBsb647c7pMe6l02/yx2+Jfc4dT6UZkEXnIUb5LhD55r2HPsJ1milQ4rDg==", + "license": "BSD-2-Clause" + }, "node_modules/json-stringify-safe": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", @@ -7322,6 +7550,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", @@ -9113,6 +9353,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resedit": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/resedit/-/resedit-1.7.2.tgz", @@ -9699,6 +9948,21 @@ "node": ">=8" } }, + "node_modules/stubborn-fs": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz", + "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==", + "license": "MIT", + "dependencies": { + "stubborn-utils": "^1.0.1" + } + }, + "node_modules/stubborn-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz", + "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==", + "license": "MIT" + }, "node_modules/styled-jsx": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", @@ -9766,6 +10030,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tailwindcss": { "version": "4.1.17", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz", @@ -10041,6 +10317,18 @@ "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", "license": "MIT" }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -10222,6 +10510,12 @@ "defaults": "^1.0.3" } }, + "node_modules/when-exit": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz", + "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index d028f9b..52ebbc8 100644 --- a/package.json +++ b/package.json @@ -1,13 +1,14 @@ { "name": "eritorsscribe", "version": "1.0.0", + "type": "module", "main": "dist/electron/main.js", "scripts": { "dev:next": "next dev -p 4000", "dev:electron": "NODE_ENV=development PORT=4000 electron -r tsx/cjs electron/main.ts", "dev": "concurrently \"npm run dev:next\" \"wait-on http://localhost:4000 && npm run dev:electron\"", "build:next": "next build", - "build:electron": "tsc --project tsconfig.electron.json", + "build:electron": "tsc --project tsconfig.electron.json && tsc --project tsconfig.preload.json", "build": "npm run build:next && npm run build:electron", "start": "electron .", "package": "npm run build && electron-builder build --mac --win --linux", @@ -20,6 +21,7 @@ "license": "ISC", "description": "", "devDependencies": { + "@types/electron-store": "^1.3.1", "@types/js-cookie": "^3.0.6", "@types/jsonwebtoken": "^9.0.10", "@types/node": "^24.10.1", @@ -50,6 +52,7 @@ "antd": "^5.28.1", "autoprefixer": "^10.4.22", "axios": "^1.13.2", + "electron-store": "^11.0.2", "i18next": "^25.6.2", "js-cookie": "^3.0.5", "next": "^16.0.3", diff --git a/tsconfig.electron.json b/tsconfig.electron.json index 7d45d93..e324aa9 100644 --- a/tsconfig.electron.json +++ b/tsconfig.electron.json @@ -1,13 +1,16 @@ { - "extends": "./tsconfig.json", "compilerOptions": { - "module": "commonjs", - "moduleResolution": "node", + "module": "node16", + "moduleResolution": "node16", "target": "ES2022", "outDir": "dist/electron", "rootDir": "electron", "lib": ["ES2022"], - "jsx": "react" + "esModuleInterop": true, + "skipLibCheck": true, + "strict": true, + "resolveJsonModule": true, + "noEmit": false }, "include": [ "electron/**/*"