This commit is contained in:
Adrián Borrageiros Mourelos 2025-05-13 12:22:20 +02:00
parent ccecbe7c1f
commit 0ebef51f5c
15 changed files with 293 additions and 62 deletions

View File

@ -50,5 +50,10 @@
"select": "Select language",
"es": "Spanish",
"en": "English"
},
"theme": {
"light": "Light theme",
"dark": "Dark theme",
"select": "Select theme"
}
}

View File

@ -50,5 +50,10 @@
"select": "Seleccionar idioma",
"es": "Español",
"en": "Inglés"
},
"theme": {
"light": "Tema claro",
"dark": "Tema oscuro",
"select": "Seleccionar tema"
}
}

View File

@ -4,8 +4,13 @@ import { loginUser } from "@/controllers/authController";
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { email, password, rememberMe } = body;
const { token, user, error, status } = await loginUser(body);
const { token, user, error, status } = await loginUser({
email,
password,
rememberMe,
});
if (error) {
return NextResponse.json({ error }, { status: status || 500 });

View File

@ -13,12 +13,20 @@ export default function Login() {
email: '',
password: ''
});
const [rememberMe, setRememberMe] = useState(false);
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
setFormData(prev => ({ ...prev, [name]: value }));
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name === 'remember-me') {
setRememberMe(checked);
}
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = async (e: React.FormEvent) => {
@ -30,6 +38,7 @@ export default function Login() {
const result = await login({
email: formData.email,
password: formData.password,
rememberMe: rememberMe
});
if (result.error) {
@ -54,26 +63,26 @@ export default function Login() {
if (isLoading) return null;
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex items-center justify-center p-6">
<div className="max-w-md w-full mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200">
<h2 className="text-2xl font-normal text-gray-800 mb-2">{t('auth.login.title')}</h2>
<p className="text-sm text-gray-500">
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-normal text-gray-800 dark:text-white mb-2">{t('auth.login.title')}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('auth.login.subtitle')}
</p>
</div>
<div className="px-8 py-6">
{error && (
<div className="mb-4 px-4 py-3 bg-red-50 text-sm text-red-600 rounded-md">
<div className="mb-4 px-4 py-3 bg-red-50 dark:bg-red-900/20 text-sm text-red-600 dark:text-red-400 rounded-md">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-5">
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.login.email')}
</label>
<input
@ -84,18 +93,18 @@ export default function Login() {
required
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="tu@ejemplo.com"
/>
</div>
<div>
<div className="flex items-center justify-between">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.login.password')}
</label>
<div className="text-xs">
<a href="#" className="text-gray-500 hover:text-gray-700">
<a href="#" className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300">
{t('auth.login.forgotPassword')}
</a>
</div>
@ -108,7 +117,7 @@ export default function Login() {
required
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="••••••••"
/>
</div>
@ -118,9 +127,11 @@ export default function Login() {
id="remember-me"
name="remember-me"
type="checkbox"
className="h-4 w-4 text-blue-600 focus:ring-0 border-gray-300 rounded"
checked={rememberMe}
onChange={handleChange}
className="h-4 w-4 text-blue-600 focus:ring-0 border-gray-300 dark:border-gray-600 rounded"
/>
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-600">
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-600 dark:text-gray-400">
{t('auth.login.rememberMe')}
</label>
</div>
@ -145,10 +156,10 @@ export default function Login() {
</form>
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-200">
<p className="text-sm text-center text-gray-600">
<div className="px-8 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600">
<p className="text-sm text-center text-gray-600 dark:text-gray-400">
{t('auth.login.noAccount')}{' '}
<Link href="/auth/register" className="text-blue-600 hover:text-blue-500 font-medium">
<Link href="/auth/register" className="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
{t('auth.login.signUp')}
</Link>
</p>

View File

@ -67,26 +67,26 @@ export default function Register() {
if (isLoading) return null;
return (
<div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex items-center justify-center p-6">
<div className="max-w-md w-full mx-auto">
<div className="bg-white shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200">
<h2 className="text-2xl font-normal text-gray-800 mb-2">{t('auth.register.title')}</h2>
<p className="text-sm text-gray-500">
<div className="bg-white dark:bg-gray-800 shadow-sm rounded-lg overflow-hidden">
<div className="px-8 pt-8 pb-6 border-b border-gray-200 dark:border-gray-700">
<h2 className="text-2xl font-normal text-gray-800 dark:text-white mb-2">{t('auth.register.title')}</h2>
<p className="text-sm text-gray-500 dark:text-gray-400">
{t('auth.register.subtitle')}
</p>
</div>
<div className="px-8 py-6">
{error && (
<div className="mb-4 px-4 py-3 bg-red-50 text-sm text-red-600 rounded-md">
<div className="mb-4 px-4 py-3 bg-red-50 dark:bg-red-900/20 text-sm text-red-600 dark:text-red-400 rounded-md">
{error}
</div>
)}
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="username" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.register.username')}
</label>
<input
@ -97,13 +97,13 @@ export default function Register() {
required
value={formData.username}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="usuario"
/>
</div>
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="email" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.register.email')}
</label>
<input
@ -114,13 +114,13 @@ export default function Register() {
required
value={formData.email}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="tu@ejemplo.com"
/>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="password" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.register.password')}
</label>
<input
@ -131,14 +131,14 @@ export default function Register() {
required
value={formData.password}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="••••••••"
/>
<p className="mt-1 text-xs text-gray-500">{t('auth.register.minChars')}</p>
<p className="mt-1 text-xs text-gray-500 dark:text-gray-400">{t('auth.register.minChars')}</p>
</div>
<div>
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
{t('auth.register.confirmPassword')}
</label>
<input
@ -149,7 +149,7 @@ export default function Register() {
required
value={formData.confirmPassword}
onChange={handleChange}
className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 text-sm"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md focus:outline-none focus:ring-1 focus:ring-gray-400 focus:border-gray-400 dark:focus:ring-gray-500 dark:focus:border-gray-500 text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-white"
placeholder="••••••••"
/>
</div>
@ -174,10 +174,10 @@ export default function Register() {
</form>
</div>
<div className="px-8 py-4 bg-gray-50 border-t border-gray-200">
<p className="text-sm text-center text-gray-600">
<div className="px-8 py-4 bg-gray-50 dark:bg-gray-700 border-t border-gray-200 dark:border-gray-600">
<p className="text-sm text-center text-gray-600 dark:text-gray-400">
{t('auth.register.haveAccount')}{' '}
<Link href="/auth/login" className="text-blue-600 hover:text-blue-500 font-medium">
<Link href="/auth/login" className="text-blue-600 hover:text-blue-500 dark:text-blue-400 dark:hover:text-blue-300 font-medium">
{t('auth.register.login')}
</Link>
</p>

View File

@ -7,11 +7,9 @@
--foreground: #171717;
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
.dark {
--background: #0a0a0a;
--foreground: #ededed;
}
body {
@ -20,6 +18,8 @@ body {
font-family: Arial, Helvetica, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--foreground);
background-color: var(--background);
}
* {
@ -31,4 +31,9 @@ body {
a {
color: inherit;
text-decoration: none;
}
/* Transición suave al cambiar de tema */
.dark *, * {
transition: background-color 0.3s ease, border-color 0.3s ease, color 0.3s ease;
}

View File

@ -3,6 +3,7 @@
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import { LanguageProvider } from "@/lib/i18n/language-context";
import { ThemeProvider } from "@/lib/theme-context";
import { useEffect, useState } from "react";
const geistSans = Geist({
@ -35,9 +36,11 @@ export default function RootLayout({
return (
<html lang={language}>
<body className={`${geistSans.variable} ${geistMono.variable}`}>
<LanguageProvider>
{children}
</LanguageProvider>
<ThemeProvider>
<LanguageProvider>
{children}
</LanguageProvider>
</ThemeProvider>
</body>
</html>
);

View File

@ -2,6 +2,7 @@
import Link from 'next/link';
import LanguageSwitcher from '@/components/LanguageSwitcher';
import ThemeSwitcher from '@/components/ThemeSwitcher';
import { useI18n } from '@/lib/i18n/useI18n';
export default function Home() {
@ -10,15 +11,16 @@ export default function Home() {
if (isLoading) return null;
return (
<div className="min-h-screen bg-gray-100 flex flex-col">
<header className="py-6 bg-white shadow-sm">
<div className="min-h-screen bg-gray-100 dark:bg-gray-900 flex flex-col">
<header className="py-6 bg-white dark:bg-gray-800 shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex justify-between items-center">
<div className="font-medium text-lg text-gray-900">Draw</div>
<div className="font-medium text-lg text-gray-900 dark:text-white">Draw</div>
<nav className="flex items-center space-x-4">
<ThemeSwitcher />
<LanguageSwitcher />
<Link
href="/auth/login"
className="text-sm text-gray-700 hover:text-gray-900"
className="text-sm text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
>
{t('header.login')}
</Link>
@ -35,10 +37,10 @@ export default function Home() {
<main className="flex-grow flex">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12 flex flex-col lg:flex-row items-center">
<div className="lg:w-1/2 lg:pr-12 mb-10 lg:mb-0">
<h1 className="text-4xl font-bold text-gray-900 mb-6">
<h1 className="text-4xl font-bold text-gray-900 dark:text-white mb-6">
{t('home.title')}
</h1>
<p className="text-xl text-gray-600 mb-8">
<p className="text-xl text-gray-600 dark:text-gray-300 mb-8">
{t('home.description')}
</p>
<div className="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-4">
@ -50,15 +52,15 @@ export default function Home() {
</Link>
<Link
href="/about"
className="inline-flex justify-center items-center px-6 py-3 border border-gray-300 rounded-md shadow-sm text-base font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none"
className="inline-flex justify-center items-center px-6 py-3 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm text-base font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none"
>
{t('home.learnMore')}
</Link>
</div>
</div>
<div className="lg:w-1/2 flex justify-center">
<div className="w-full max-w-md h-96 bg-white rounded-lg shadow-md flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor" className="w-24 h-24 text-gray-300">
<div className="w-full max-w-md h-96 bg-white dark:bg-gray-800 rounded-lg shadow-md flex items-center justify-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1} stroke="currentColor" className="w-24 h-24 text-gray-300 dark:text-gray-600">
<path strokeLinecap="round" strokeLinejoin="round" d="M9.53 16.122a3 3 0 0 0-5.78 1.128 2.25 2.25 0 0 1-2.4 2.245 4.5 4.5 0 0 0 8.4-2.245c0-.399-.078-.78-.22-1.128Zm0 0a15.998 15.998 0 0 0 3.388-1.62m-5.043-.025a15.994 15.994 0 0 1 1.622-3.395m3.42 3.42a15.995 15.995 0 0 0 4.764-4.648l3.876-5.814a1.151 1.151 0 0 0-1.597-1.597L14.146 6.32a15.996 15.996 0 0 0-4.649 4.763m3.42 3.42a6.776 6.776 0 0 0-3.42-3.42" />
</svg>
</div>
@ -66,9 +68,9 @@ export default function Home() {
</div>
</main>
<footer className="bg-white border-t border-gray-200 py-8">
<footer className="bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 py-8">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<p className="text-sm text-gray-500 text-center">
<p className="text-sm text-gray-500 dark:text-gray-400 text-center">
{t('footer.copyright')}
</p>
</div>

View File

@ -0,0 +1,84 @@
'use client';
import { useTheme } from '@/lib/theme-context';
import { useState, useRef, useEffect } from 'react';
import { useI18n } from '@/lib/i18n/useI18n';
export default function ThemeSwitcher() {
const { theme, toggleTheme } = useTheme();
const [isOpen, setIsOpen] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const { t, isLoading } = useI18n();
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
if (isLoading) return null;
return (
<div className="relative" ref={dropdownRef}>
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-300 dark:hover:text-white"
aria-label="Cambiar tema"
>
{theme === 'light' ? (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
) : (
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
)}
</button>
{isOpen && (
<div className="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-md shadow-lg py-1 z-10">
<button
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
theme === 'light' ? 'text-blue-600 font-medium' : 'text-gray-700 dark:text-gray-300'
}`}
onClick={() => {
if (theme === 'dark') toggleTheme();
setIsOpen(false);
}}
>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path fillRule="evenodd" d="M10 2a1 1 0 011 1v1a1 1 0 11-2 0V3a1 1 0 011-1zm4 8a4 4 0 11-8 0 4 4 0 018 0zm-.464 4.95l.707.707a1 1 0 001.414-1.414l-.707-.707a1 1 0 00-1.414 1.414zm2.12-10.607a1 1 0 010 1.414l-.706.707a1 1 0 11-1.414-1.414l.707-.707a1 1 0 011.414 0zM17 11a1 1 0 100-2h-1a1 1 0 100 2h1zm-7 4a1 1 0 011 1v1a1 1 0 11-2 0v-1a1 1 0 011-1zM5.05 6.464A1 1 0 106.465 5.05l-.708-.707a1 1 0 00-1.414 1.414l.707.707zm1.414 8.486l-.707.707a1 1 0 01-1.414-1.414l.707-.707a1 1 0 011.414 1.414zM4 11a1 1 0 100-2H3a1 1 0 000 2h1z" clipRule="evenodd" />
</svg>
{t ? t('theme.light') : 'Tema claro'}
</div>
</button>
<button
className={`block w-full text-left px-4 py-2 text-sm hover:bg-gray-100 dark:hover:bg-gray-700 ${
theme === 'dark' ? 'text-blue-600 font-medium' : 'text-gray-700 dark:text-gray-300'
}`}
onClick={() => {
if (theme === 'light') toggleTheme();
setIsOpen(false);
}}
>
<div className="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
<path d="M17.293 13.293A8 8 0 016.707 2.707a8.001 8.001 0 1010.586 10.586z" />
</svg>
{t ? t('theme.dark') : 'Tema oscuro'}
</div>
</button>
</div>
)}
</div>
);
}

View File

@ -45,10 +45,14 @@ export async function register(data: {
}
}
export async function loginUser(data: { email: string; password: string }) {
export async function loginUser(data: {
email: string;
password: string;
rememberMe?: boolean;
}) {
try {
await dbConnect();
const { email, password } = data;
const { email, password, rememberMe } = data;
const user = await User.findOne({ email });
if (!user) {
@ -60,9 +64,13 @@ export async function loginUser(data: { email: string; password: string }) {
return { error: "Invalid credentials", status: 401 };
}
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
expiresIn: "7d",
});
let tokenOptions = {};
if (!rememberMe) {
tokenOptions = { expiresIn: "7d" };
}
const token = jwt.sign({ userId: user._id }, JWT_SECRET, tokenOptions);
return {
token,

View File

@ -1,6 +1,7 @@
interface LoginRequest {
email: string;
password: string;
rememberMe?: boolean;
}
interface RegisterRequest {

View File

@ -20,7 +20,7 @@ const initI18next = async (lng: string, ns: string) => {
.use(
resourcesToBackend(
(language: string, namespace: string) =>
import(`../../public/locales/${language}/${namespace}.json`)
import(`../../../public/locales/${language}/${namespace}.json`)
)
)
.init(getOptions(lng, ns));

60
src/lib/theme-context.tsx Normal file
View File

@ -0,0 +1,60 @@
'use client';
import { createContext, useContext, useState, useEffect, ReactNode } from 'react';
type Theme = 'light' | 'dark';
type ThemeContextType = {
theme: Theme;
setTheme: (theme: Theme) => void;
toggleTheme: () => void;
};
const ThemeContext = createContext<ThemeContextType | null>(null);
export function ThemeProvider({ children }: { children: ReactNode }) {
const [theme, setTheme] = useState<Theme>('light');
useEffect(() => {
// Intentar cargar el tema guardado
const savedTheme = localStorage.getItem('theme') as Theme | null;
if (savedTheme && (savedTheme === 'light' || savedTheme === 'dark')) {
setTheme(savedTheme);
} else {
// Si no hay tema guardado, usar preferencia del sistema
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
setTheme(prefersDark ? 'dark' : 'light');
}
}, []);
useEffect(() => {
// Aplicar la clase al document.documentElement cuando cambie el tema
if (theme === 'dark') {
document.documentElement.classList.add('dark');
} else {
document.documentElement.classList.remove('dark');
}
// Guardar tema en localStorage
localStorage.setItem('theme', theme);
}, [theme]);
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<ThemeContext.Provider value={{ theme, setTheme, toggleTheme }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (!context) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}

41
src/types/drawing.ts Normal file
View File

@ -0,0 +1,41 @@
export interface DrawingData {
elements: readonly any[];
appState: any;
files: any;
}
export interface Drawing {
id: string;
title: string;
description?: string;
createdAt: Date;
updatedAt: Date;
userId: string;
isPublic: boolean;
data: DrawingData;
}
export interface DrawingSaveRequest {
title: string;
description?: string;
isPublic: boolean;
data: DrawingData;
}
export interface DrawingUpdateRequest {
id: string;
title?: string;
description?: string;
isPublic?: boolean;
data?: DrawingData;
}
export interface DrawingListItem {
id: string;
title: string;
description?: string;
createdAt: Date;
updatedAt: Date;
isPublic: boolean;
thumbnail?: string;
}

View File

@ -5,6 +5,7 @@ module.exports = {
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
],
darkMode: 'class',
theme: {
extend: {},
},