Major: Added Authentication Functionality, interfaces with backend;
Added new styles to HomePage & VideoPage; Added AuthContext to persist logged_in status;
This commit is contained in:
49
frontend/src/components/Auth/AuthModal.tsx
Normal file
49
frontend/src/components/Auth/AuthModal.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
import React, { useState } from "react";
|
||||
import { ToggleButton } from "../Layout/Button";
|
||||
import { LogIn as LogInIcon, User as UserIcon } from "lucide-react";
|
||||
import LoginForm from "./LoginForm";
|
||||
import RegisterForm from "./RegisterForm";
|
||||
|
||||
interface AuthModalProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const AuthModal: React.FC<AuthModalProps> = ({ onClose }) => {
|
||||
const [selectedTab, setSelectedTab] = useState<string>("Login");
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="blurring-layer fixed z-10 inset-0 w-screen h-screen backdrop-blur-sm group-has-[input:focus]:backdrop-blur-[5px]"></div>
|
||||
|
||||
<div className="modal-container fixed inset-0 bg-black/30 has-[input:focus]:bg-black/40 flex flex-col items-center justify-around z-50 h-[70vh] m-auto min-w-[40vw] w-fit py-[50px] rounded-[2rem] transition-all">
|
||||
<div className="login-methods w-full flex flex-row items-center justify-evenly">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="absolute top-[1rem] right-[2rem] text-[2rem] text-white hover:text-red-500 font-black hover:text-[2.5rem] transition-all"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
<ToggleButton
|
||||
toggled={selectedTab === "Login"}
|
||||
extraClasses="flex flex-col items-center px-8"
|
||||
onClick={() => setSelectedTab("Login")}
|
||||
>
|
||||
<LogInIcon className="h-[40px] w-[40px] mr-1" />
|
||||
Login
|
||||
</ToggleButton>
|
||||
<ToggleButton
|
||||
toggled={selectedTab === "Register"}
|
||||
extraClasses="flex flex-col items-center px-8"
|
||||
onClick={() => setSelectedTab("Register")}
|
||||
>
|
||||
<UserIcon className="h-[40px] w-[40px] mr-1" />
|
||||
Register
|
||||
</ToggleButton>
|
||||
</div>
|
||||
{selectedTab === "Login" ? <LoginForm /> : <RegisterForm />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AuthModal;
|
||||
@@ -1 +1,127 @@
|
||||
// login.html
|
||||
import React, { useState } from "react";
|
||||
import Input from "../Layout/Input";
|
||||
import Button from "../Layout/Button";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
interface LoginFormData {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
username?: string;
|
||||
password?: string;
|
||||
general?: string; // For general authentication errors
|
||||
}
|
||||
|
||||
const LoginForm: React.FC = () => {
|
||||
const { setIsLoggedIn } = useAuth();
|
||||
|
||||
const [formData, setFormData] = useState<LoginFormData>({
|
||||
username: "",
|
||||
password: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
// Check for empty fields
|
||||
Object.keys(formData).forEach((key) => {
|
||||
if (!formData[key as keyof LoginFormData]) {
|
||||
newErrors[key as keyof FormErrors] = "This field is required";
|
||||
}
|
||||
});
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validateForm()) {
|
||||
try {
|
||||
const response = await fetch("/api/login", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || "Login failed");
|
||||
}
|
||||
|
||||
if (data.logged_in) {
|
||||
//TODO: Handle successful login (e.g., redirect to home page)
|
||||
console.log("Login successful");
|
||||
setIsLoggedIn(true);
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Handle authentication errors
|
||||
if (data.errors) {
|
||||
setErrors({
|
||||
general: "Invalid username or password",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Login error:", error);
|
||||
setErrors({
|
||||
general: "An error occurred during login. Please try again.",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="login-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||
>
|
||||
{errors.general && (
|
||||
<p className="text-red-500 text-sm text-center">{errors.general}</p>
|
||||
)}
|
||||
|
||||
{errors.username && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
||||
)}
|
||||
<Input
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.username ? "border-red-500" : ""}`}
|
||||
/>
|
||||
|
||||
{errors.password && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.password}</p>
|
||||
)}
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.password ? "border-red-500" : ""}`}
|
||||
/>
|
||||
|
||||
<Button type="submit">Login</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default LoginForm;
|
||||
|
||||
155
frontend/src/components/Auth/RegisterForm.tsx
Normal file
155
frontend/src/components/Auth/RegisterForm.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import React, { useState } from "react";
|
||||
import Input from "../Layout/Input";
|
||||
import Button from "../Layout/Button";
|
||||
import { useAuth } from "../../context/AuthContext";
|
||||
|
||||
interface RegisterFormData {
|
||||
username: string;
|
||||
email: string;
|
||||
password: string;
|
||||
confirmPassword: string;
|
||||
}
|
||||
|
||||
interface FormErrors {
|
||||
username?: string;
|
||||
email?: string;
|
||||
password?: string;
|
||||
confirmPassword?: string;
|
||||
}
|
||||
|
||||
const RegisterForm: React.FC = () => {
|
||||
const { setIsLoggedIn } = useAuth();
|
||||
|
||||
const [formData, setFormData] = useState<RegisterFormData>({
|
||||
username: "",
|
||||
email: "",
|
||||
password: "",
|
||||
confirmPassword: "",
|
||||
});
|
||||
const [errors, setErrors] = useState<FormErrors>({});
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData((prev) => ({
|
||||
...prev,
|
||||
[name]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const newErrors: FormErrors = {};
|
||||
|
||||
// Check for empty fields
|
||||
Object.keys(formData).forEach((key) => {
|
||||
if (!formData[key as keyof RegisterFormData]) {
|
||||
newErrors[key as keyof FormErrors] = "This field is required";
|
||||
}
|
||||
});
|
||||
|
||||
// Check password match
|
||||
if (formData.password !== formData.confirmPassword) {
|
||||
newErrors.confirmPassword = "Passwords do not match";
|
||||
}
|
||||
|
||||
setErrors(newErrors);
|
||||
return Object.keys(newErrors).length === 0;
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
if (validateForm()) {
|
||||
try {
|
||||
const response = await fetch("/api/signup", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
credentials: "include",
|
||||
body: JSON.stringify(formData),
|
||||
});
|
||||
console.log(`sending data: ${JSON.stringify(formData)}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
data.message || `Registration failed. ${response.body}`
|
||||
);
|
||||
}
|
||||
|
||||
if (data.account_created) {
|
||||
//TODO Handle successful registration (e.g., redirect or show success message)
|
||||
console.log("Registration Successful! Account created successfully");
|
||||
setIsLoggedIn(true);
|
||||
window.location.reload();
|
||||
} else {
|
||||
// Handle validation errors from server
|
||||
const serverErrors: FormErrors = {};
|
||||
if (data.errors) {
|
||||
Object.entries(data.errors).forEach(([field, message]) => {
|
||||
serverErrors[field as keyof FormErrors] = message as string;
|
||||
});
|
||||
setErrors(serverErrors);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Registration error:", error);
|
||||
//TODO Show user-friendly error message via Alert component maybe
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="register-form h-[100%] flex flex-col h-full justify-evenly items-center"
|
||||
>
|
||||
{errors.username && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.username}</p>
|
||||
)}
|
||||
<Input
|
||||
name="username"
|
||||
placeholder="Username"
|
||||
value={formData.username}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.username ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{errors.email && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.email}</p>
|
||||
)}
|
||||
<Input
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="Email"
|
||||
value={formData.email}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.email ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{errors.password && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.password}</p>
|
||||
)}
|
||||
<Input
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Password"
|
||||
value={formData.password}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.password ? "border-red-500" : ""}`}
|
||||
/>
|
||||
{errors.confirmPassword && (
|
||||
<p className="text-red-500 mt-3 text-sm">{errors.confirmPassword}</p>
|
||||
)}
|
||||
<Input
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm Password"
|
||||
value={formData.confirmPassword}
|
||||
onChange={handleInputChange}
|
||||
extraClasses={`${errors.confirmPassword ? "border-red-500" : ""}`}
|
||||
/>
|
||||
<Button type="submit">Register</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default RegisterForm;
|
||||
Reference in New Issue
Block a user