Index: frontend/src/pages/Login/Login.jsx
===================================================================
--- frontend/src/pages/Login/Login.jsx	(revision 3ebe47cce61b3bfc1dee2566f7475e378374591e)
+++ frontend/src/pages/Login/Login.jsx	(revision 3ebe47cce61b3bfc1dee2566f7475e378374591e)
@@ -0,0 +1,99 @@
+import React, { useEffect, useState } from "react";
+import { Link, useNavigate } from "react-router-dom";
+
+import api from "../../api/axios";
+
+const Login = () => {
+  const navigate = useNavigate();
+  const [usernameOrEmail, setUsernameOrEmail] = useState("");
+  const [password, setPassword] = useState("");
+  const [error, setError] = useState("");
+  const [isSubmitting, setIsSubmitting] = useState(false);
+
+  useEffect(() => {
+    const token = localStorage.getItem("authToken");
+    if (token) navigate("/dashboard", { replace: true });
+  }, [navigate]);
+
+  const onSubmit = async (e) => {
+    e.preventDefault();
+    setError("");
+    setIsSubmitting(true);
+    try {
+      const res = await api.post("/auth/login", {
+        usernameOrEmail,
+        password,
+      });
+      localStorage.setItem("authToken", res.data.token);
+      localStorage.setItem(
+        "authUser",
+        JSON.stringify({
+          userId: res.data.userId,
+          username: res.data.username,
+          email: res.data.email,
+        }),
+      );
+      navigate("/dashboard");
+    } catch (err) {
+      const message =
+        err?.response?.data?.message ||
+        err?.response?.data?.errors?.usernameOrEmail ||
+        "Login failed";
+      setError(message);
+    } finally {
+      setIsSubmitting(false);
+    }
+  };
+
+  return (
+    <div className="min-h-screen flex items-center justify-center p-4">
+      <form
+        onSubmit={onSubmit}
+        className="fieldset bg-base-200 border-base-300 rounded-box w-xs border p-4"
+      >
+        <legend className="fieldset-legend">Login</legend>
+
+        <label className="label">Username or Email</label>
+        <input
+          type="text"
+          className="input"
+          placeholder="Username or Email"
+          value={usernameOrEmail}
+          onChange={(e) => setUsernameOrEmail(e.target.value)}
+          autoComplete="username"
+          required
+        />
+
+        <label className="label">Password</label>
+        <input
+          type="password"
+          className="input"
+          placeholder="Password"
+          value={password}
+          onChange={(e) => setPassword(e.target.value)}
+          autoComplete="current-password"
+          required
+        />
+
+        {error ? <p className="text-error mt-2 text-sm">{error}</p> : null}
+
+        <button
+          className="btn btn-neutral mt-4"
+          type="submit"
+          disabled={isSubmitting}
+        >
+          {isSubmitting ? "Logging in..." : "Login"}
+        </button>
+
+        <p className="mt-3 text-sm">
+          Don&apos;t have an account?{" "}
+          <Link className="link link-primary" to="/register">
+            Register
+          </Link>
+        </p>
+      </form>
+    </div>
+  );
+};
+
+export default Login;
