SAVE
This commit is contained in:
parent
ccecbe7c1f
commit
0ebef51f5c
|
@ -50,5 +50,10 @@
|
|||
"select": "Select language",
|
||||
"es": "Spanish",
|
||||
"en": "English"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Light theme",
|
||||
"dark": "Dark theme",
|
||||
"select": "Select theme"
|
||||
}
|
||||
}
|
|
@ -50,5 +50,10 @@
|
|||
"select": "Seleccionar idioma",
|
||||
"es": "Español",
|
||||
"en": "Inglés"
|
||||
},
|
||||
"theme": {
|
||||
"light": "Tema claro",
|
||||
"dark": "Tema oscuro",
|
||||
"select": "Seleccionar tema"
|
||||
}
|
||||
}
|
|
@ -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 });
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
interface LoginRequest {
|
||||
email: string;
|
||||
password: string;
|
||||
rememberMe?: boolean;
|
||||
}
|
||||
|
||||
interface RegisterRequest {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue
Block a user