Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions src/components/layouts/AuthCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@ import React from 'react';
interface AuthCardProps {
title: string;
description: string;
children: React.ReactNode;
children: React.ReactNode;
}

const AuthCard = ({ title, description, children }: AuthCardProps) => {
return (
<div className="bg-white/95 backdrop-blur-sm border border-white/20 rounded-2xl p-8 shadow-strong w-full max-w-md animate-fadeIn">
<div className="text-center mb-8">
<h1 className="text-neutral-900 text-3xl font-bold mb-2">{title}</h1>
<p className="text-neutral-600">{description}</p>
<div className="bg-white/90 backdrop-blur-sm rounded-2xl p-12 shadow-2xl shadow-slate-black/50 w-full max-w-xl">
<div className="text-center mb-10">
<h1 className="text-slate-900 text-3xl font-bold">{title}</h1>
<p className="text-slate-500 text-base mt-3">{description}</p>
</div>
{children}
</div>
);
};

export default AuthCard;
export default AuthCard;
29 changes: 19 additions & 10 deletions src/components/layouts/AuthLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,29 @@
import { Outlet, Link } from 'react-router-dom';
import { BookOpen, ArrowLeft } from 'lucide-react';

const AuthLayout = () => {
return (
<div className="min-h-screen w-full flex items-center justify-center p-4 bg-gradient-to-br from-purple-400 via-pink-500 to-red-500">
<Link
to="/"
className="absolute top-8 left-8 text-primary-100 no-underline font-semibold hover:text-white transition-colors"
>
Home
</Link>

<main>
<div className="min-h-screen w-full flex items-center justify-center p-4 bg-gradient-to-br from-blue-50 via-white to-green-50">
<div className="absolute top-6 left-6">
<Link
to="/"
className="flex items-center gap-4 text-slate-800 hover:text-blue-600 transition-colors group"
>
<ArrowLeft className="w-6 h-6" />
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-blue-400 to-green-400 rounded-lg flex items-center justify-center shadow-md group-hover:scale-105 transition-transform">
<BookOpen className="w-5 h-5 text-white" />
</div>
<span className="text-xl font-bold">UpSkill</span>
</div>
</Link>
</div>

<main className="w-full flex justify-center">
<Outlet />
</main>
</div>
);
};

export default AuthLayout;
export default AuthLayout;
37 changes: 28 additions & 9 deletions src/components/ui/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,45 @@
import React from 'react';

type ButtonVariant = 'primary' | 'outline';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: React.ReactNode;
isLoading?: boolean;
children: React.ReactNode;
isLoading?: boolean;
variant?: ButtonVariant;
}

const Button = ({ children, isLoading = false, ...props }: ButtonProps) => {
const baseClasses = "w-full inline-flex items-center justify-center py-3 px-6 rounded-lg font-medium text-sm text-white bg-gradient-to-r from-primary-600 to-primary-700 shadow-medium hover:from-primary-700 hover:to-primary-800 hover:scale-[1.02] hover:shadow-strong transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed disabled:transform-none";
const Button = ({
children,
isLoading = false,
variant = 'primary',
className,
...props
}: ButtonProps) => {
const baseClasses = `
w-full inline-flex items-center justify-center py-2.5 px-4 rounded-lg font-medium
text-sm transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2
disabled:opacity-50 disabled:cursor-not-allowed
`;

const variantClasses = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
outline:
'bg-transparent border border-slate-300 text-slate-700 hover:bg-slate-50 focus:ring-slate-400',
};

return (
<button
className={baseClasses}
disabled={isLoading}
{...props}
className={`${baseClasses} ${variantClasses[variant]} ${className || ''}`}
disabled={isLoading || props.disabled}
{...props}
>
{isLoading ? (
<span className="animate-spin h-5 w-5 border-2 border-transparent border-t-white rounded-full"></span>
<span className="animate-spin h-5 w-5 border-2 border-transparent border-t-current rounded-full"></span>
) : (
children
)}
</button>
);
};

export default Button;
export default Button;
64 changes: 36 additions & 28 deletions src/components/ui/Input.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,62 @@
import React, { useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons';
import React from 'react';
import { Eye, EyeOff } from 'lucide-react';

interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
label: string;
icon?: React.ReactNode;
error?: string | null;
icon?: React.ReactNode;
error?: string | null;
}

const Input = ({ label, id, type, icon, error, ...props }: InputProps) => {
const [showPassword, setShowPassword] = useState(false);
const isPassword = type === 'password';

const inputType = isPassword ? (showPassword ? 'text' : 'password') : type;

const inputClasses = `w-full ${icon ? 'pl-10' : 'pl-4'} ${isPassword ? 'pr-10' : 'pr-4'} py-3 border rounded-lg text-base bg-white focus:outline-none focus:border-primary-500 focus:ring-2 focus:ring-primary-500/20 transition-all`;
const [isPasswordVisible, setPasswordVisible] = React.useState(false);
const isPasswordInput = type === 'password';
const inputType = isPasswordInput
? isPasswordVisible
? 'text'
: 'password'
: type;

const inputClasses = `
w-full py-3 border rounded-lg transition-all
bg-slate-100/70 border-transparent
focus:outline-none focus:ring-2 focus:ring-blue-500/50 focus:border-blue-500 focus:bg-white
${icon ? 'pl-10' : 'pl-4'}
${isPasswordInput ? 'pr-10' : 'pr-4'}
${error ? 'border-red-500 ring-red-200' : ''}
`;

return (
<div className="flex flex-col gap-2">
<label htmlFor={id} className="text-sm font-medium text-neutral-700">
<label htmlFor={id} className="text-sm font-medium text-slate-700">
{label}
</label>
<div className="relative">
{icon && (
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-neutral-400 pointer-events-none">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-slate-400 pointer-events-none">
{icon}
</span>
)}

<input
id={id}
type={inputType}
className={`${inputClasses} ${error ? 'border-error' : 'border-neutral-300'}`}
{...props}
/>

{isPassword && (
<input id={id} type={inputType} className={inputClasses} {...props} />
{isPasswordInput && (
<button
type="button"
onClick={() => setShowPassword(!showPassword)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-neutral-400 hover:text-neutral-600"
aria-label={showPassword ? "Hide password" : "Show password"}
onClick={() => setPasswordVisible(!isPasswordVisible)}
className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 hover:text-slate-600"
aria-label={
isPasswordVisible ? 'Ocultar contraseña' : 'Mostrar contraseña'
}
>
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
{isPasswordVisible ? (
<EyeOff className="h-5 w-5" />
) : (
<Eye className="h-5 w-5" />
)}
</button>
)}
</div>
{error && <p className="text-sm text-error mt-1">{error}</p>}
{error && <p className="text-sm text-red-500 mt-1">{error}</p>}
</div>
);
};

export default Input;
export default Input;
100 changes: 58 additions & 42 deletions src/pages/Auth/LoginPage.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
import { useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import type React from 'react';
import { Mail, Lock } from 'lucide-react';
import { useAuth } from '../../hooks/useAuth';
import { authService } from '../../api/services/auth.service';
import type React from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faEnvelope, faLock } from '@fortawesome/free-solid-svg-icons';
import Button from '../../components/ui/Button.tsx';
import Input from '../../components/ui/Input.tsx';
import AuthCard from '../../components/layouts/AuthCard.tsx';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import AuthCard from '../../components/layouts/AuthCard';

const LoginPage = () => {
const [credentials, setCredentials] = useState({ mail: '', password: '' });
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const navigate = useNavigate();
const auth = useAuth();

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setCredentials((prev) => ({ ...prev, [e.target.name]: e.target.value }));
};

const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setIsLoading(true);
setError(null);

try {
const payload = { mail: credentials.mail, password_plaintext: credentials.password };
const payload = {
mail: credentials.mail,
password_plaintext: credentials.password,
};
const { user, token } = await authService.login(payload);
auth.login(user, token);
navigate('/');
} catch (err) {
setError('Incorrect credentials. Please try again.');
setError('Credenciales incorrectas. Por favor, inténtalo de nuevo.');
console.error(err);
} finally {
setIsLoading(false);
Expand All @@ -40,61 +39,78 @@ const LoginPage = () => {

return (
<AuthCard
title='Welcome'
description='Please enter your credentials to continue'
title="Iniciar Sesión"
description="Accede a tu cuenta para continuar aprendiendo"
>

<form onSubmit={handleSubmit} className="flex flex-col gap-6">
<form onSubmit={handleSubmit} className="space-y-6">
<Input
id="mail"
label="Email"
label="Correo electrónico"
name="mail"
type="mail"
placeholder="your@email.com"
type="email"
placeholder="tu@email.com"
value={credentials.mail}
onChange={handleChange}
icon={<FontAwesomeIcon icon={faEnvelope} />}
error={error && error.includes('mail') ? error : null}
icon={<Mail className="h-5 w-5" />}
required
autoComplete="email"
/>

<Input
id="password"
label="Password"
label="Contraseña"
name="password"
type="password"
placeholder="••••••••"
placeholder="Tu contraseña"
value={credentials.password}
onChange={handleChange}
icon={<FontAwesomeIcon icon={faLock} />}
icon={<Lock className="h-5 w-5" />}
required
autoComplete="current-password"
/>

<div className="flex items-center justify-between mt-2">
<div className="flex items-center justify-between text-sm pt-1">
<div className="flex items-center gap-2">
<input id="remember" name="remember" type="checkbox" className="h-4 w-4 rounded accent-primary-600" />
<label htmlFor="remember" className="text-sm text-neutral-600 font-bold cursor-pointer">Remember me</label>
<input
id="remember"
name="remember"
type="checkbox"
className="h-4 w-4 rounded border-slate-300 text-blue-500 focus:ring-blue-500"
/>
<label htmlFor="remember" className="text-slate-600 cursor-pointer">
Recordarme
</label>
</div>
<a href="#" className="text-sm text-primary-600 hover:text-primary-800 hover:underline font-medium">
Forgot your password?
</a>
<Link
to="/forgot-password"
className="font-medium text-blue-500 hover:underline"
>
¿Olvidaste tu contraseña?
</Link>
</div>

{error && <p className="text-sm text-error mt-1">{error}</p>}
{error && <p className="text-sm text-red-500 text-center">{error}</p>}

<div className="pt-6">
<Button
type="submit"
isLoading={isLoading}
className="w-full text-base py-3"
>
Iniciar Sesión
</Button>
</div>

<Button type="submit" isLoading={isLoading}>
Login
</Button>

<div className="text-center mt-2">
<p className="text-neutral-600">
You don't have an account?
<Link to="/register" className="text-primary-600 hover:text-primary-800 font-medium ml-2 hover:underline">Create one</Link>
</p>
<div className="text-center text-sm text-slate-500 pt-6">
¿No tienes una cuenta?{' '}
<Link
to="/register"
className="font-semibold text-blue-500 hover:underline"
>
Regístrate aquí
</Link>
</div>
</form>
</AuthCard>
);
};

export default LoginPage;
export default LoginPage;
Loading