From 21ff6dad5405c8570f22c6dcbcc6b9b19ac8e449 Mon Sep 17 00:00:00 2001 From: adityapat24 Date: Mon, 16 Feb 2026 17:21:51 -0500 Subject: [PATCH] new sign up page design --- frontend/src/Register.tsx | 248 +++++------------- frontend/src/sign-up/BrandingPanel.tsx | 17 ++ frontend/src/sign-up/InputField.tsx | 38 +++ frontend/src/sign-up/LoginPrompt.tsx | 22 ++ frontend/src/sign-up/PasswordField.tsx | 101 +++++++ frontend/src/sign-up/PasswordRequirements.tsx | 66 +++++ frontend/src/sign-up/SignUpButton.tsx | 24 ++ frontend/src/sign-up/SignUpForm.tsx | 122 +++++++++ frontend/src/sign-up/index.ts | 8 + 9 files changed, 463 insertions(+), 183 deletions(-) create mode 100644 frontend/src/sign-up/BrandingPanel.tsx create mode 100644 frontend/src/sign-up/InputField.tsx create mode 100644 frontend/src/sign-up/LoginPrompt.tsx create mode 100644 frontend/src/sign-up/PasswordField.tsx create mode 100644 frontend/src/sign-up/PasswordRequirements.tsx create mode 100644 frontend/src/sign-up/SignUpButton.tsx create mode 100644 frontend/src/sign-up/SignUpForm.tsx create mode 100644 frontend/src/sign-up/index.ts diff --git a/frontend/src/Register.tsx b/frontend/src/Register.tsx index adde283..eafa2d4 100644 --- a/frontend/src/Register.tsx +++ b/frontend/src/Register.tsx @@ -1,38 +1,45 @@ import React, { useState } from "react"; import { observer } from "mobx-react-lite"; import { useNavigate } from "react-router-dom"; -import logo from "./images/logo.svg"; import { useAuthContext } from "./context/auth/authContext"; +import { SignUpForm, BrandingPanel } from "./sign-up"; import "./styles/index.css"; +const PASSWORD_REGEX = + /^(?=.*[!@#$%^&*])(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/; +const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + /** - * Register a new BCAN user + * Register a new BCAN user. Uses sign-up components and matches Figma layout. */ const Register = observer(() => { - const [username, setUsername] = useState(""); - const [email, setEmail] = useState(""); - const [password, setPassword] = useState(""); - const [passwordRe, setPasswordRe] = useState(""); - const [failure, setFailure] = useState({ - state: false, - message: "", - item: "", - }); const navigate = useNavigate(); - const { register } = useAuthContext(); - const passswordRegex: RegExp = - /^(?=.*[!@#$%^&*])(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$/; - const emailRegex: RegExp = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - const defaultPasswordMessage: string = `• Passwords must have at least one special character (!@#$%^&*) -• Passwords must have at least one digit character ('0'-'9') -• Passwords must have at least one uppercase letter ('A'-'Z') and one lowercase letter ('a'-'z') -• Passwords must be at least 8 characters long`; + + const [values, setValues] = useState({ + firstName: "", + lastName: "", + email: "", + password: "", + passwordRe: "", + }); + const [failure, setFailure] = useState<{ + state: boolean; + message: string; + item: string; + }>({ state: false, message: "", item: "" }); + + const updateField = (field: keyof typeof values, value: string) => { + setValues((prev) => ({ ...prev, [field]: value })); + if (failure.state) { + setFailure({ state: false, message: "", item: "" }); + } + }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); - // Input validation - if (!emailRegex.test(email)) { + + if (!EMAIL_REGEX.test(values.email)) { setFailure({ state: true, message: "Please enter a valid email address.", @@ -40,189 +47,64 @@ const Register = observer(() => { }); return; } - if (!passswordRegex.test(password)) { + if (!PASSWORD_REGEX.test(values.password)) { setFailure({ state: true, - message: defaultPasswordMessage, + message: + "Password must have at least one special character (!@#$%^&*), one digit, one uppercase, one lowercase, and be at least 8 characters long.", item: "password", }); return; } - const success = await register(username, password, email); - if (password === passwordRe && success.state) { - navigate("/registered"); - } else { - setFailure({ - state: true, - message: "Registration failed: " + success.message, - item: "registration", - }); - console.warn("Registration failed"); - } - }; - - // Handlers for password and password confirmation inputs - const handlePassword = (e: string) => { - setPassword(e); - if (e !== passwordRe && passwordRe !== "") { + if (values.password !== values.passwordRe) { setFailure({ state: true, message: "Passwords do not match.", item: "password", }); - } else { - setFailure({ state: false, message: "", item: "" }); + return; } - }; - const handlePasswordMatch = (e: string) => { - setPasswordRe(e); - if (e !== password) { + const first = values.firstName.trim(); + const last = values.lastName.trim(); + const username = + first || last + ? `${first}_${last}`.replace(/\s+/g, "_").replace(/_+/g, "_") + : values.email; + const success = await register(username, values.password, values.email); + + if (success.state) { + navigate("/registered"); + } else { setFailure({ state: true, - message: "Passwords do not match.", - item: "password", + message: success.message, + item: "registration", }); - } else { - setFailure({ state: false, message: "", item: "" }); } }; return ( -
-
- {/*/ Left side: Registration form */} -
-

Get Started Now

-
- -
-
-
- -
- setUsername(e.target.value)} - placeholder="Enter your username" - className="block min-w-0 rounded-md grow bg-white py-1.5 pr-3 pl-4 text-base placeholder:text-gray-500 border border-grey-400" - /> -
-
-
- -
- setEmail(e.target.value)} - placeholder="Enter your email" - className={`block min-w-0 rounded-md grow bg-white py-1.5 pr-3 pl-4 text-base placeholder:text-gray-500 border ${ - failure.item === "email" - ? "border-red" - : "border-grey-400" - }`} - /> -
-
-
- -
- handlePassword(e.target.value)} - placeholder="Enter your password" - className={`block min-w-0 rounded-md grow bg-white py-1.5 pr-3 pl-4 text-base placeholder:text-gray-500 border ${ - failure.item === "password" - ? "border-red" - : "border-grey-400" - }`} - /> -
-
-
- -
- handlePasswordMatch(e.target.value)} - required - placeholder="Re-enter your password" - className={`block min-w-0 rounded-md grow bg-white py-1.5 pr-3 pl-4 text-base placeholder:text-gray-500 border ${ - failure.item === "password" - ? "border-red" - : "border-grey-400" - }`} - /> -
-
-
-
-
- {failure.state ? failure.message : defaultPasswordMessage} -
-
- - -
-
-
or
-
-
-
- Have an account?{" "} - -
-
+
+
+
- {/*/ Right side: logo */} -
-
- BCAN Logo -
+
+
); diff --git a/frontend/src/sign-up/BrandingPanel.tsx b/frontend/src/sign-up/BrandingPanel.tsx new file mode 100644 index 0000000..44f4c38 --- /dev/null +++ b/frontend/src/sign-up/BrandingPanel.tsx @@ -0,0 +1,17 @@ +import logo from "../images/logo.svg"; + +/** + * Right-hand branding panel with orange background and BostonCAN logo. + * Uses Tailwind and primary-800 for panel background. + */ +export default function BrandingPanel() { + return ( +
+ BostonCAN +
+ ); +} diff --git a/frontend/src/sign-up/InputField.tsx b/frontend/src/sign-up/InputField.tsx new file mode 100644 index 0000000..490438e --- /dev/null +++ b/frontend/src/sign-up/InputField.tsx @@ -0,0 +1,38 @@ +import type { InputHTMLAttributes } from "react"; + +type InputFieldProps = { + id: string; + label: string; + required?: boolean; + error?: boolean; +} & Omit, "id" | "className">; + +/** + * Reusable text input with label and optional required asterisk. + * Uses Tailwind and project color tokens (grey-400, red, etc.). + */ +export default function InputField({ + id, + label, + required, + error, + ...inputProps +}: InputFieldProps) { + return ( +
+ +
+ +
+
+ ); +} diff --git a/frontend/src/sign-up/LoginPrompt.tsx b/frontend/src/sign-up/LoginPrompt.tsx new file mode 100644 index 0000000..81bd6c4 --- /dev/null +++ b/frontend/src/sign-up/LoginPrompt.tsx @@ -0,0 +1,22 @@ +import { useNavigate } from "react-router-dom"; + +/** + * "Already have an account? Log in here" prompt with link. + * Uses Tailwind and secondary-500 for link. + */ +export default function LoginPrompt() { + const navigate = useNavigate(); + + return ( +

+ Already have an account?{" "} + +

+ ); +} diff --git a/frontend/src/sign-up/PasswordField.tsx b/frontend/src/sign-up/PasswordField.tsx new file mode 100644 index 0000000..e0555ba --- /dev/null +++ b/frontend/src/sign-up/PasswordField.tsx @@ -0,0 +1,101 @@ +import { useState } from "react"; +import type { InputHTMLAttributes } from "react"; + +type PasswordFieldProps = { + id: string; + label: string; + required?: boolean; + error?: boolean; +} & Omit, "id" | "type" | "className">; + +/** Eye icon (visible password). */ +function EyeIcon({ className }: { className?: string }) { + return ( + + + + + ); +} + +/** Eye-slash icon (hidden password). */ +function EyeSlashIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +/** + * Password input with label and show/hide toggle. + * Uses Tailwind and project color tokens. + */ +export default function PasswordField({ + id, + label, + required, + error, + ...inputProps +}: PasswordFieldProps) { + const [visible, setVisible] = useState(false); + + return ( +
+ +
+ + +
+
+ ); +} diff --git a/frontend/src/sign-up/PasswordRequirements.tsx b/frontend/src/sign-up/PasswordRequirements.tsx new file mode 100644 index 0000000..69c32d2 --- /dev/null +++ b/frontend/src/sign-up/PasswordRequirements.tsx @@ -0,0 +1,66 @@ +/** + * Displays password criteria with checkmark-style layout. + * Each requirement shows green (met) or gray (unmet) based on current password. + * Uses Tailwind and project color tokens (green-light, green-dark, grey-200, grey-600). + */ +export type PasswordRequirement = { + id: string; + label: string; + check: (password: string) => boolean; +}; + +export const PASSWORD_REQUIREMENTS: PasswordRequirement[] = [ + { id: "length", label: "Minimum 8 characters", check: (p) => p.length >= 8 }, + { id: "upper", label: "1 Uppercase", check: (p) => /[A-Z]/.test(p) }, + { id: "number", label: "1 Number", check: (p) => /\d/.test(p) }, + { + id: "special", + label: "At least 1 special character", + check: (p) => /[!@#$%^&*]/.test(p), + }, + { id: "lower", label: "1 Lowercase", check: (p) => /[a-z]/.test(p) }, +]; + +type PasswordRequirementsProps = { + password: string; +}; + +function CheckIcon({ className }: { className?: string }) { + return ( + + + + ); +} + +export default function PasswordRequirements({ password }: PasswordRequirementsProps) { + return ( +
+ {PASSWORD_REQUIREMENTS.map(({ id, label, check }) => { + const met = check(password); + return ( + + + {label} + + ); + })} +
+ ); +} diff --git a/frontend/src/sign-up/SignUpButton.tsx b/frontend/src/sign-up/SignUpButton.tsx new file mode 100644 index 0000000..2cc2f7e --- /dev/null +++ b/frontend/src/sign-up/SignUpButton.tsx @@ -0,0 +1,24 @@ +type SignUpButtonProps = { + disabled?: boolean; +}; + +/** + * Primary Sign Up submit button. + * When requirements are met: primary-900 (#E16F39), clickable. + * When not met: primary-700 (current/inactive), disabled. + */ +export default function SignUpButton({ disabled }: SignUpButtonProps) { + return ( + + ); +} diff --git a/frontend/src/sign-up/SignUpForm.tsx b/frontend/src/sign-up/SignUpForm.tsx new file mode 100644 index 0000000..e86351c --- /dev/null +++ b/frontend/src/sign-up/SignUpForm.tsx @@ -0,0 +1,122 @@ +import InputField from "./InputField"; +import PasswordField from "./PasswordField"; +import PasswordRequirements from "./PasswordRequirements"; +import SignUpButton from "./SignUpButton"; +import LoginPrompt from "./LoginPrompt"; + +export type SignUpFormValues = { + firstName: string; + lastName: string; + email: string; + password: string; + passwordRe: string; +}; + +export type SignUpFormProps = { + values: SignUpFormValues; + onChange: (field: keyof SignUpFormValues, value: string) => void; + onSubmit: (e: React.FormEvent) => void; + error?: { state: boolean; message: string; item: string }; + /** When true, Sign Up button uses primary-900 and is clickable; when false, primary-700 and disabled. */ + passwordRequirementsMet?: boolean; + /** When true, all required fields (first name, last name, email, password, re-enter password) are filled. */ + allFieldsFilled?: boolean; + /** When true, password and re-enter password are exactly the same. */ + passwordsMatch?: boolean; +}; + +/** + * Sign Up form layout: title, fields, requirements, button, login prompt. + * Composes sign-up subcomponents; state and submit logic live in the parent. + */ +export default function SignUpForm({ + values, + onChange, + onSubmit, + error, + passwordRequirementsMet = false, + allFieldsFilled = false, + passwordsMatch = false, +}: SignUpFormProps) { + const hasError = error?.state ?? false; + const errorItem = error?.item ?? ""; + const canSubmit = + passwordRequirementsMet && allFieldsFilled && passwordsMatch; + + return ( +
+

Sign Up

+ +
+
+ onChange("firstName", e.target.value)} + error={errorItem === "firstName"} + /> + onChange("lastName", e.target.value)} + error={errorItem === "lastName"} + /> +
+ +
+ onChange("email", e.target.value)} + error={errorItem === "email"} + /> +
+ +
+ onChange("password", e.target.value)} + error={errorItem === "password"} + /> +
+ +
+ onChange("passwordRe", e.target.value)} + error={errorItem === "password"} + /> +
+ + + + {hasError && error?.message && ( +
+ {error.message} +
+ )} + + + + +
+ ); +} diff --git a/frontend/src/sign-up/index.ts b/frontend/src/sign-up/index.ts new file mode 100644 index 0000000..5b3ec8b --- /dev/null +++ b/frontend/src/sign-up/index.ts @@ -0,0 +1,8 @@ +export { default as BrandingPanel } from "./BrandingPanel"; +export { default as InputField } from "./InputField"; +export { default as LoginPrompt } from "./LoginPrompt"; +export { default as PasswordField } from "./PasswordField"; +export { default as PasswordRequirements } from "./PasswordRequirements"; +export { default as SignUpButton } from "./SignUpButton"; +export { default as SignUpForm } from "./SignUpForm"; +export type { SignUpFormProps, SignUpFormValues } from "./SignUpForm";