SAVE
This commit is contained in:
parent
03eae11b3b
commit
3a99ab39c6
|
@ -0,0 +1 @@
|
||||||
|
MONGODB_URI=mongodb+srv://adrianborrageirosmourelos:FOcz19ZnmULvFZeY@cluster0.gf3secu.mongodb.net/draw_dev?retryWrites=true&w=majority&appName=Cluster0
|
|
@ -31,7 +31,7 @@ yarn-error.log*
|
||||||
.pnpm-debug.log*
|
.pnpm-debug.log*
|
||||||
|
|
||||||
# env files (can opt-in for committing if needed)
|
# env files (can opt-in for committing if needed)
|
||||||
.env*
|
.env
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
.vercel
|
.vercel
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = nextConfig
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev --turbopack",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint"
|
"lint": "next lint"
|
||||||
|
@ -21,11 +21,15 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
|
"autoprefixer": "10",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.3.2",
|
"eslint-config-next": "15.3.2",
|
||||||
|
"postcss": "8",
|
||||||
|
"tailwindcss": "3",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
module.exports = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
const config = {
|
||||||
|
plugins: ["@tailwindcss/postcss"],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
|
@ -1,12 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { login } from '@/controllers/authController';
|
|
||||||
import dbConnect from '@/lib/db/connection';
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
return res.status(405).json({ error: 'Method not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbConnect();
|
|
||||||
return login(req, res);
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { loginUser } from "@/controllers/authController";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Llamar a la función loginUser del controlador
|
||||||
|
const { token, user, error, status } = await loginUser(body);
|
||||||
|
|
||||||
|
// Si hay un error, devolverlo con el status correspondiente
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json({ error }, { status: status || 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devolver la respuesta con el token y el usuario
|
||||||
|
return NextResponse.json({ token, user }, { status: 200 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error en el login:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Error interno del servidor" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +0,0 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
|
||||||
import { register } from '@/controllers/authController';
|
|
||||||
import dbConnect from '@/lib/db/connection';
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
if (req.method !== 'POST') {
|
|
||||||
return res.status(405).json({ error: 'Method not allowed' });
|
|
||||||
}
|
|
||||||
|
|
||||||
await dbConnect();
|
|
||||||
return register(req, res);
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { register } from "@/controllers/authController";
|
||||||
|
|
||||||
|
export async function POST(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json();
|
||||||
|
|
||||||
|
// Llamar a la función register del controlador
|
||||||
|
const { token, user, error, status } = await register(body);
|
||||||
|
|
||||||
|
// Si hay un error, devolverlo con el status correspondiente
|
||||||
|
if (error) {
|
||||||
|
return NextResponse.json({ error }, { status: status || 500 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Devolver la respuesta con el token y el usuario
|
||||||
|
return NextResponse.json({ token, user }, { status: 201 });
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error en el registro:", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Error interno del servidor" },
|
||||||
|
{ status: 500 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,161 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
email: '',
|
||||||
|
password: ''
|
||||||
|
});
|
||||||
|
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 handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/auth/login', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
email: formData.email,
|
||||||
|
password: formData.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Error al iniciar sesión');
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
router.push('/dashboard');
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setError(err.message);
|
||||||
|
} else {
|
||||||
|
setError('Ha ocurrido un error desconocido');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100 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">Iniciar sesión</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Accede a tu cuenta para comenzar a dibujar
|
||||||
|
</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">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-5">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Correo electrónico
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
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"
|
||||||
|
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">
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<div className="text-xs">
|
||||||
|
<a href="#" className="text-gray-500 hover:text-gray-700">
|
||||||
|
¿Olvidaste tu contraseña?
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
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"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center">
|
||||||
|
<input
|
||||||
|
id="remember-me"
|
||||||
|
name="remember-me"
|
||||||
|
type="checkbox"
|
||||||
|
className="h-4 w-4 text-blue-600 focus:ring-0 border-gray-300 rounded"
|
||||||
|
/>
|
||||||
|
<label htmlFor="remember-me" className="ml-2 block text-sm text-gray-600">
|
||||||
|
Recordarme
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Iniciando sesión...
|
||||||
|
</>
|
||||||
|
) : 'Iniciar sesión'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
¿No tienes una cuenta?{' '}
|
||||||
|
<Link href="/auth/register" className="text-blue-600 hover:text-blue-500 font-medium">
|
||||||
|
Regístrate
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,190 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export default function Register() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
username: '',
|
||||||
|
email: '',
|
||||||
|
password: '',
|
||||||
|
confirmPassword: ''
|
||||||
|
});
|
||||||
|
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 handleSubmit = async (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setError('');
|
||||||
|
|
||||||
|
if (formData.password !== formData.confirmPassword) {
|
||||||
|
setError('Las contraseñas no coinciden');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (formData.password.length < 6) {
|
||||||
|
setError('La contraseña debe tener al menos 6 caracteres');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
const response = await fetch('/api/auth/register', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username: formData.username,
|
||||||
|
email: formData.email,
|
||||||
|
password: formData.password,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(data.error || 'Error al registrar usuario');
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('token', data.token);
|
||||||
|
router.push('/dashboard');
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
setError(err.message);
|
||||||
|
} else {
|
||||||
|
setError('Ha ocurrido un error desconocido');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-gray-100 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">Crear cuenta</h2>
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
Regístrate para comenzar a dibujar
|
||||||
|
</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">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label htmlFor="username" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Nombre de usuario
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="username"
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
autoComplete="username"
|
||||||
|
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"
|
||||||
|
placeholder="usuario"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="email" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Correo electrónico
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
autoComplete="email"
|
||||||
|
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"
|
||||||
|
placeholder="tu@ejemplo.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="password" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Contraseña
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
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"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
<p className="mt-1 text-xs text-gray-500">Mínimo 6 caracteres</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label htmlFor="confirmPassword" className="block text-sm font-medium text-gray-700 mb-1">
|
||||||
|
Confirmar contraseña
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
id="confirmPassword"
|
||||||
|
name="confirmPassword"
|
||||||
|
type="password"
|
||||||
|
autoComplete="new-password"
|
||||||
|
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"
|
||||||
|
placeholder="••••••••"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-2">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="w-full flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<svg className="animate-spin -ml-1 mr-2 h-4 w-4 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
||||||
|
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||||
|
</svg>
|
||||||
|
Creando cuenta...
|
||||||
|
</>
|
||||||
|
) : 'Crear cuenta'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</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">
|
||||||
|
¿Ya tienes una cuenta?{' '}
|
||||||
|
<Link href="/auth/login" className="text-blue-600 hover:text-blue-500 font-medium">
|
||||||
|
Inicia sesión
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,3 +1,7 @@
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
"use client"
|
|
||||||
import LoginForm from '@/components/LoginForm';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default function Login() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
|
||||||
Sign in to your account
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<LoginForm />
|
|
||||||
<div className="text-center">
|
|
||||||
<Link href="/register" className="text-blue-500 hover:text-blue-600">
|
|
||||||
Dont have an account? Register
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
113
src/app/page.tsx
113
src/app/page.tsx
|
@ -1,58 +1,69 @@
|
||||||
'use client';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
|
||||||
import styles from "./page.module.css";
|
|
||||||
|
|
||||||
interface Drawing {
|
|
||||||
_id: string;
|
|
||||||
title: string;
|
|
||||||
content: string;
|
|
||||||
creator: string;
|
|
||||||
createdAt: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const [drawings, setDrawings] = useState<Drawing[]>([]);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const fetchDrawings = async () => {
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/drawings');
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.success) {
|
|
||||||
setDrawings(data.data);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
console.error('Error fetching drawings');
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
fetchDrawings();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.page}>
|
<div className="min-h-screen bg-gray-100 flex flex-col">
|
||||||
<h1>Mis Dibujos</h1>
|
<header className="py-6 bg-white 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">DrawApp</div>
|
||||||
|
<nav>
|
||||||
|
<Link
|
||||||
|
href="/auth/login"
|
||||||
|
className="text-sm text-gray-700 hover:text-gray-900 mr-4"
|
||||||
|
>
|
||||||
|
Iniciar sesión
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/auth/register"
|
||||||
|
className="text-sm bg-blue-600 text-white py-2 px-3 rounded-md hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Registrarse
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
{loading ? (
|
<main className="flex-grow flex">
|
||||||
<p>Cargando dibujos...</p>
|
<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">
|
||||||
) : drawings.length > 0 ? (
|
<div className="lg:w-1/2 lg:pr-12 mb-10 lg:mb-0">
|
||||||
<ul>
|
<h1 className="text-4xl font-bold text-gray-900 mb-6">
|
||||||
{drawings.map((drawing) => (
|
Dibuja lo que imagines
|
||||||
<li key={drawing._id}>
|
</h1>
|
||||||
<h2>{drawing.title}</h2>
|
<p className="text-xl text-gray-600 mb-8">
|
||||||
<p>Creado por: {drawing.creator}</p>
|
Una aplicación simple y poderosa para expresar tu creatividad mediante dibujos digitales.
|
||||||
<p>Fecha: {new Date(drawing.createdAt).toLocaleDateString()}</p>
|
</p>
|
||||||
</li>
|
<div className="flex flex-col sm:flex-row space-y-3 sm:space-y-0 sm:space-x-4">
|
||||||
))}
|
<Link
|
||||||
</ul>
|
href="/auth/register"
|
||||||
) : (
|
className="inline-flex justify-center items-center px-6 py-3 border border-transparent rounded-md shadow-sm text-base font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none"
|
||||||
<p>No hay dibujos disponibles</p>
|
>
|
||||||
)}
|
Comenzar gratis
|
||||||
|
</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"
|
||||||
|
>
|
||||||
|
Conocer más
|
||||||
|
</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">
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="bg-white border-t border-gray-200 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">
|
||||||
|
© 2023 DrawApp. Todos los derechos reservados.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,24 +0,0 @@
|
||||||
|
|
||||||
"use client"
|
|
||||||
import RegisterForm from '@/components/RegisterForm';
|
|
||||||
import Link from 'next/link';
|
|
||||||
|
|
||||||
export default function Register() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen flex items-center justify-center bg-gray-50 py-12 px-4 sm:px-6 lg:px-8">
|
|
||||||
<div className="max-w-md w-full space-y-8">
|
|
||||||
<div>
|
|
||||||
<h2 className="mt-6 text-center text-3xl font-extrabold text-gray-900">
|
|
||||||
Create your account
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
<RegisterForm />
|
|
||||||
<div className="text-center">
|
|
||||||
<Link href="/login" className="text-blue-500 hover:text-blue-600">
|
|
||||||
Already have an account? Login
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
"use client"
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { LoginData } from '../types/auth';
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
|
||||||
const [formData, setFormData] = useState<LoginData>({
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
});
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth/login', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || 'Login failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('token', data.token);
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Login failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Email"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Password"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Login
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
"use client"
|
|
||||||
import { useState } from 'react';
|
|
||||||
import { RegisterData } from '../types/auth';
|
|
||||||
|
|
||||||
export default function RegisterForm() {
|
|
||||||
const [formData, setFormData] = useState<RegisterData>({
|
|
||||||
username: '',
|
|
||||||
email: '',
|
|
||||||
password: '',
|
|
||||||
});
|
|
||||||
const [error, setError] = useState('');
|
|
||||||
|
|
||||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
const { name, value } = e.target;
|
|
||||||
setFormData(prev => ({ ...prev, [name]: value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
setError('');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch('/api/auth/register', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(formData),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(data.error || 'Registration failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage.setItem('token', data.token);
|
|
||||||
window.location.href = '/dashboard';
|
|
||||||
} catch (err) {
|
|
||||||
setError(err instanceof Error ? err.message : 'Registration failed');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="username"
|
|
||||||
value={formData.username}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Username"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="email"
|
|
||||||
name="email"
|
|
||||||
value={formData.email}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Email"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="password"
|
|
||||||
value={formData.password}
|
|
||||||
onChange={handleChange}
|
|
||||||
placeholder="Password"
|
|
||||||
className="w-full p-2 border rounded"
|
|
||||||
required
|
|
||||||
minLength={6}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{error && <p className="text-red-500">{error}</p>}
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600"
|
|
||||||
>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</form>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,17 +1,23 @@
|
||||||
import { NextApiRequest, NextApiResponse } from 'next';
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
import bcrypt from 'bcryptjs';
|
import bcrypt from "bcryptjs";
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from "jsonwebtoken";
|
||||||
import User from '../models/User';
|
import User from "../models/User";
|
||||||
|
import dbConnect from "@/lib/db/connection";
|
||||||
|
|
||||||
const JWT_SECRET = process.env.JWT_SECRET || 'your-secret-key';
|
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";
|
||||||
|
|
||||||
export async function register(req: NextApiRequest, res: NextApiResponse) {
|
export async function register(data: {
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}) {
|
||||||
try {
|
try {
|
||||||
const { username, email, password } = req.body;
|
await dbConnect();
|
||||||
|
const { username, email, password } = data;
|
||||||
|
|
||||||
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
|
const existingUser = await User.findOne({ $or: [{ email }, { username }] });
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
return res.status(400).json({ error: 'User already exists' });
|
return { error: "User already exists", status: 400 };
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
@ -22,44 +28,42 @@ export async function register(req: NextApiRequest, res: NextApiResponse) {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
});
|
});
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
|
||||||
{ userId: user._id },
|
expiresIn: "7d",
|
||||||
JWT_SECRET,
|
});
|
||||||
{ expiresIn: '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(201).json({
|
return {
|
||||||
token,
|
token,
|
||||||
user: {
|
user: {
|
||||||
id: user._id,
|
id: user._id,
|
||||||
username: user.username,
|
username: user.username,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
});
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
console.error("Error en el registro:", error);
|
||||||
|
return { error: "Internal server error", status: 500 };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(req: NextApiRequest, res: NextApiResponse) {
|
export async function login(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
try {
|
||||||
|
await dbConnect();
|
||||||
const { email, password } = req.body;
|
const { email, password } = req.body;
|
||||||
|
|
||||||
const user = await User.findOne({ email });
|
const user = await User.findOne({ email });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return res.status(401).json({ error: 'Invalid credentials' });
|
return res.status(401).json({ error: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const isValidPassword = await bcrypt.compare(password, user.password);
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||||
if (!isValidPassword) {
|
if (!isValidPassword) {
|
||||||
return res.status(401).json({ error: 'Invalid credentials' });
|
return res.status(401).json({ error: "Invalid credentials" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = jwt.sign(
|
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
|
||||||
{ userId: user._id },
|
expiresIn: "7d",
|
||||||
JWT_SECRET,
|
});
|
||||||
{ expiresIn: '7d' }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
token,
|
token,
|
||||||
|
@ -69,7 +73,40 @@ export async function login(req: NextApiRequest, res: NextApiResponse) {
|
||||||
email: user.email,
|
email: user.email,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
return res.status(500).json({ error: 'Internal server error' });
|
return res.status(500).json({ error: "Internal server error" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loginUser(data: { email: string; password: string }) {
|
||||||
|
try {
|
||||||
|
await dbConnect();
|
||||||
|
const { email, password } = data;
|
||||||
|
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
if (!user) {
|
||||||
|
return { error: "Invalid credentials", status: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidPassword = await bcrypt.compare(password, user.password);
|
||||||
|
if (!isValidPassword) {
|
||||||
|
return { error: "Invalid credentials", status: 401 };
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign({ userId: user._id }, JWT_SECRET, {
|
||||||
|
expiresIn: "7d",
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user._id,
|
||||||
|
username: user.username,
|
||||||
|
email: user.email,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error en el login:", error);
|
||||||
|
return { error: "Internal server error", status: 500 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
module.exports = {
|
||||||
|
content: [
|
||||||
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user