protecting routes for auth

This commit is contained in:
2026-05-08 15:16:04 +02:00
parent 965aa80fe0
commit 7c9e9c5001
15 changed files with 273 additions and 58 deletions

View File

@@ -3,9 +3,12 @@ import { BrowserRouter, Routes, Route} from 'react-router-dom'
import Navbar from './components/Navbar'
import Home from './pages/Home'
import NuevaVacante from './pages/NuevaVacante'
import NuevaOferta from './pages/NuevaOferta'
import './index.css'
import Login from './pages/Login'
import PublicRoute from './components/PublicRoute'
import PrivateRoute from './components/PrivateRoute'
import Register from './pages/Register'
function App() {
@@ -15,8 +18,9 @@ function App() {
<Navbar />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/login" element={<Login />} />
<Route path="/nueva-oferta" element={<NuevaVacante />} />
<Route path="/login" element={<PublicRoute><Login /></PublicRoute>} />
<Route path="/register" element={<PublicRoute><Register /></PublicRoute>} />
<Route path="/nueva-oferta" element={<PrivateRoute><NuevaOferta /></PrivateRoute>} />
</Routes>
</div>
</BrowserRouter>

View File

@@ -3,7 +3,7 @@ import { AuthContext } from "../context/AuthContext"
import { Link } from "react-router-dom"
function Navbar() {
const { user } = useContext(AuthContext)
const { user, logout } = useContext(AuthContext)
return (
<nav className="py-10 flex justify-between">
@@ -20,7 +20,7 @@ function Navbar() {
) : (
<div className="flex gap-4 items-center">
<span className="text-white">Hola, {user.username}</span>
<button className="text-red-400 font-bold uppercase">Cerrar sesión</button>
<button onClick={logout} className="text-red-400 font-bold uppercase">Cerrar sesión</button>
</div>
)}
</div>

View File

@@ -0,0 +1,14 @@
import { useContext } from "react"
import { AuthContext } from "../context/AuthContext"
import { Navigate, useLocation } from "react-router-dom"
const PrivateRoute = ({children}) => {
const { user, isLoading } = useContext(AuthContext)
const location = useLocation()
if (isLoading) return (null)
return user ? children : <Navigate to="/login" state={{ from: location.pathname }} />
}
export default PrivateRoute

View File

@@ -0,0 +1,11 @@
import { useContext } from "react"
import { AuthContext } from "../context/AuthContext"
import { Navigate } from "react-router-dom"
const PublicRoute = ({children}) => {
const { user } = useContext(AuthContext)
return user ? <Navigate to="/" /> : children
}
export default PublicRoute

1
frontend/src/config.js Normal file
View File

@@ -0,0 +1 @@
export const API_URL = "http://localhost:5000"

View File

@@ -4,10 +4,12 @@ export const AuthContext = createContext()
export function AuthProvider({ children }) {
const [user, setUser] = useState(null)
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
const user = localStorage.getItem('user')
if (user) setUser(JSON.parse(user))
setIsLoading(false)
}, [])
const login = (data) => {
@@ -23,7 +25,7 @@ export function AuthProvider({ children }) {
}
return (
<AuthContext.Provider value={{ user, login, logout }}>
<AuthContext.Provider value={{ user, login, logout, isLoading }}>
{children}
</AuthContext.Provider>
)

View File

@@ -1,9 +1,26 @@
import { useEffect } from "react"
import { API_URL } from "../config"
import { useState } from "react"
function Home() {
const [offers, setOffers] = useState([])
useEffect(() => {
fetch(`${API_URL}/offers`)
.then(res => res.json())
.then(data => setOffers(data))
}, [])
const contratos = {
freelance: "Freelance",
completo: "Jornada completa",
medio: "Media jornada"
}
return (
<>
<div class="py-5 md:py-20 flex flex-col gap-4 border-b border-b-gray-700">
<div className="py-5 md:py-20 flex flex-col gap-4 border-b border-b-gray-700">
<h2 className="text-4xl font-semibold">HadiJobs</h2>
<p>Encuentra y publica trabajos para desarrolladores</p>
<a className="bg-[#00A4B6] py-3 px-10 rounded uppercase font-extrabold tracking-wider w-fit"
@@ -12,29 +29,31 @@ function Home() {
<div className="py-5 md:py-20">
<h2 className="text-4xl font-semibold pb-5">Lista de ofertas</h2>
<div className="flex flex-row justify-between">
{offers.length === 0 && <p className="text-gray-500">No hay ofertas disponibles</p>}
{offers.map(offer => (
<div key={offer.id} className="flex flex-row items-center justify-between border-b border-gray-700 py-6 hover:bg-gray-800 cursor-pointer rounded px-4 transition">
<div className="flex flex-col flex-1">
<h3 className="text-xl ">{offer.empresa}</h3>
<p className="text-gray-500 ">{offer.titulo}</p>
</div>
<div className="flex flex-col">
<h3 className="text-xl ">Facebook</h3>
<p className="text-gray-500 ">React developer</p>
<div className="flex flex-col flex-1">
<h3 className="text-gray-500 ">Ubicacion</h3>
<p className="">{offer.ubicacion}</p>
</div>
<div className="flex flex-col flex-1">
<h3 className="text-gray-500">Contrato</h3>
<p>{contratos[offer.contrato]}</p>
</div>
<div className="flex flex-col">
<a className="bg-[#00A4B6] py-3 px-10 rounded uppercase font-extrabold tracking-wider w-fit"
href="#">Info</a>
</div>
</div>
<div className="flex flex-col">
<h3 className="text-gray-500 ">Ubicacion</h3>
<p className="">Remoto</p>
</div>
<div className="flex flex-col">
<h3 className="text-gray-500">Contrato</h3>
<p>Tiempo completo</p>
</div>
<div className="flex flex-col">
<a className="bg-[#00C897] py-3 px-10 rounded uppercase font-extrabold tracking-wider w-fit"
href="#">Info</a>
</div>
</div>
))}
</div>

View File

@@ -2,11 +2,13 @@ import { useContext } from "react"
import { useState } from "react"
import { AuthContext } from "../context/AuthContext"
import { useNavigate } from "react-router-dom"
import { API_URL } from "../config"
function Login() {
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [error, setError] = useState("")
const { login } = useContext(AuthContext)
const navigate = useNavigate()
@@ -14,7 +16,7 @@ function Login() {
const handleSubmit = async (e) => {
e.preventDefault()
const res = await fetch('http://localhost:5000/login', {
const res = await fetch(`${API_URL}/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
@@ -26,6 +28,7 @@ function Login() {
login(data)
navigate('/')
} else {
setError(data.error)
console.error(data.error)
}
}
@@ -38,6 +41,8 @@ function Login() {
<form onSubmit={handleSubmit}>
<h3 className="text-center text-5xl mb-16">Inicia sesion</h3>
{ error && <p className="text-red-400">{error}</p> }
<div className="flex flex-col mb-8">
<label className="w-[9rem] pt-2 text-white font-bold shrink-0 uppercase">
Correo
@@ -53,13 +58,13 @@ function Login() {
Contraseña
</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
placeholder="Escribe su contraseña..." required
placeholder="Escriba su contraseña..." required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
</div>
<div>
<input type="submit" value="Acceder" class="bg-[#00A4B6] hover:bg-[#027481] transition cursor-pointer py-3 px-10 rounded uppercase font-extrabold tracking-wider w-full" />
<input type="submit" value="Acceder" className="bg-[#00A4B6] hover:bg-[#027481] transition cursor-pointer py-3 px-10 rounded uppercase font-extrabold tracking-wider w-full" />
</div>
</form>

View File

@@ -1,23 +1,59 @@
import { useState } from "react"
import { useNavigate } from "react-router-dom"
import { API_URL } from "../config"
function NuevaVacante() {
function NuevaOferta() {
const navigate = useNavigate()
const [titulo, setTitulo] = useState("")
const [empresa, setEmpresa] = useState("")
const [salario, setSalario] = useState("")
const [contrato, setContrato] = useState("")
const [ubicacion, setUbicacion] = useState("")
const [descripcion, setDescripcion] = useState("")
const [error, setError] = useState("")
const handleSubmit = async (e) => {
e.preventDefault()
setError("")
const res = await fetch(`${API_URL}/crear-oferta`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${localStorage.getItem("token")}`
},
body: JSON.stringify({titulo, empresa, ubicacion, salario, contrato, descripcion})
})
const data = await res.json()
if (res.ok) {
navigate('/')
} else {
setError(data.error)
}
}
return (
<>
<div className="py-5 md:py-20 flex flex-col gap-4 border-b border-b-gray-700">
<h2 className="text-4xl font-semibold">Nueva vacante</h2>
<h2 className="text-4xl font-semibold">Nueva oferta</h2>
<p>Rellena el formulario para publicar tu oferta de trabajo</p>
</div>
<div className="max-w-[800px] mx-auto p-10">
<form>
<form onSubmit={handleSubmit} >
<h3 className="text-gray-500 mb-8">Informacion general</h3>
{ error && <p className="text-red-500">{error}</p>}
<div className="flex mb-8">
<label className="w-[9rem] pt-2 text-white font-bold shrink-0 uppercase">
Titulo
</label>
<input type="text" name="titulo"
<input type="text" name="titulo" value={titulo} onChange={(e) => setTitulo(e.target.value)}
placeholder="Ej: React Developer" required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
@@ -27,7 +63,7 @@ function NuevaVacante() {
<label className="w-[9rem] pt-2 text-white font-bold shrink-0 uppercase">
Empresa
</label>
<input type="text" name="empresa"
<input type="text" name="empresa" value={empresa} onChange={(e) => setEmpresa(e.target.value)}
placeholder="Ej: HadiES" required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
@@ -37,7 +73,7 @@ function NuevaVacante() {
<label className="w-[9rem] pt-2 text-white font-bold shrink-0 uppercase">
Ubicacion
</label>
<input type="text" name="ubicacion"
<input type="text" name="ubicacion" value={ubicacion} onChange={(e) => setUbicacion(e.target.value)}
placeholder="Ej: España... Global..." required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
@@ -48,7 +84,7 @@ function NuevaVacante() {
Salario (EUR)
</label>
<input type="text" name="salario"
placeholder="Ej: 50€/hora o 200€" required
placeholder="Ej: 50€/hora o 200€" required value={salario} onChange={(e) => setSalario(e.target.value)}
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
</div>
@@ -58,8 +94,9 @@ function NuevaVacante() {
Contrato
</label>
<select name="contrato" className="bg-gray-200 text-black w-full text-center font-normal">
<option value="" selected disabled>-- Selecciona una opcion --</option>
<select name="contrato" defaultValue={0} value={contrato} onChange={(e) => setContrato(e.target.value)}
className="bg-gray-200 text-black w-full text-center font-normal">
<option value="" disabled>-- Selecciona una opcion --</option>
<option value="freelance">Freelance</option>
<option value="completo">Jornada completa</option>
<option value="medio">Media jornada</option>
@@ -69,14 +106,14 @@ function NuevaVacante() {
<div className="flex flex-col gap-4 mb-8">
<h3 className="font-bold uppercase">Descripcion del puesto</h3>
<textarea name="descripcion"
<textarea name="descripcion" value={descripcion} onChange={(e) => setDescripcion(e.target.value)}
placeholder="Describe el puesto..."
className="bg-gray-200 flex-1 px-5 py-3 rounded text-black border-none font-normal min-h-[200px]"
/>
</div>
<div class="">
<input type="submit" value="Publicar" class="bg-[#00A4B6] py-3 px-10 rounded uppercase font-extrabold tracking-wider w-full" />
<div className="">
<input type="submit" value="Publicar" className="bg-[#00A4B6] hover:bg-[#027481] transition py-3 px-10 rounded uppercase font-extrabold tracking-wider w-full cursor-pointer" />
</div>
</form>
@@ -85,4 +122,4 @@ function NuevaVacante() {
)
}
export default NuevaVacante
export default NuevaOferta

View File

@@ -0,0 +1,102 @@
import { useContext } from "react"
import { useState } from "react"
import { AuthContext } from "../context/AuthContext"
import { useNavigate } from "react-router-dom"
import { API_URL } from "../config"
function Register() {
const [username, setUsername] = useState("")
const [email, setEmail] = useState("")
const [password, setPassword] = useState("")
const [type, setType] = useState("")
const [error, setError] = useState("")
const { login } = useContext(AuthContext)
const navigate = useNavigate()
const handleSubmit = async (e) => {
e.preventDefault()
if (!type) return setError("Debes seleccionar un tipo de cuenta ('Busco trabajo / Ofrezco trabajo')")
const res = await fetch(`${API_URL}/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
})
const data = await res.json()
if (res.ok) {
login(data)
navigate('/')
} else {
setError(data.error)
console.error(data.error)
}
}
return (
<>
<div className="max-w-[800px] mx-auto p-20">
<form onSubmit={handleSubmit}>
<h3 className="text-center text-5xl mb-16">Registrate</h3>
{ error && <p className="text-red-400">{error}</p> }
<div className="flex flex-col mb-8">
<label className="pt-2 text-white font-bold shrink-0 uppercase">
Nombre de usuario
</label>
<input type="username" value={username} onChange={(e) => setUsername(e.target.value)}
placeholder="Escriba su nombre de usuario..." required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
</div>
<div className="flex flex-col mb-8">
<label className="pt-2 text-white font-bold shrink-0 uppercase">
Correo
</label>
<input type="email" value={email} onChange={(e) => setEmail(e.target.value)}
placeholder="Escriba su email..." required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
</div>
<div className="flex flex-col mb-8">
<label className="pt-2 text-white font-bold shrink-0 uppercase">
Contraseña
</label>
<input type="password" value={password} onChange={(e) => setPassword(e.target.value)}
placeholder="Escriba su contraseña..." required
className="bg-gray-200 flex-1 px-5 py-1 rounded text-black w-full border-none font-normal"
/>
</div>
<div className="flex flex-col mb-8">
<label className="pt-2 text-white font-bold shrink-0 uppercase">
¿Buscas trabajo o quieres publicar ofertas?
</label>
<div className="flex gap-5 justify-between my-5">
<button className={`w-1/2 py-3 transition font-bold uppercase hover:bg-[#00A4B6] active:brightness-125 ${type === "candidato" ? "bg-[#00A4B6]" : "bg-gray-600"}`}
type="button" onClick={() => setType("candidato")}>Busco</button>
<button className={`w-1/2 py-3 transition font-bold uppercase hover:bg-[#00A4B6] active:brightness-125 ${type === "reclutador" ? "bg-[#00A4B6]" : "bg-gray-600"}`}
type="button" onClick={() => setType("reclutador")}>Publico</button>
</div>
</div>
<div>
<input type="submit" value="Crear cuenta" className="bg-[#00A4B6] hover:bg-[#027481] transition cursor-pointer py-3 px-10 rounded uppercase font-extrabold tracking-wider w-full" />
</div>
</form>
</div>
</>
)
}
export default Register