diff --git a/.DS_Store b/.DS_Store index 139e6f0..5f66833 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/.gitignore b/.gitignore index 30922db..4218a49 100644 --- a/.gitignore +++ b/.gitignore @@ -32,6 +32,8 @@ yarn-error.log* # env files (can opt-in for committing if needed) .env* +.firebase-key.json +firebase-key.json # vercel .vercel diff --git a/components/LoadingSpinner.tsx b/components/LoadingSpinner.tsx new file mode 100644 index 0000000..2f033c7 --- /dev/null +++ b/components/LoadingSpinner.tsx @@ -0,0 +1,38 @@ +interface LoadingSpinnerProps { + size?: "sm" | "md" | "lg"; + className?: string; + fullScreen?: boolean; +} + +export default function LoadingSpinner({ + size = "md", + className = "", + fullScreen = false, +}: LoadingSpinnerProps) { + const sizeClasses = { + sm: "h-4 w-4", + md: "h-8 w-8", + lg: "h-12 w-12", + }; + + const spinner = ( +
+ Loading... +
+ ); + + if (fullScreen) { + return ( +
+ {spinner} +
+ ); + } + + return spinner; +} + diff --git a/components/LogoBox.tsx b/components/LogoBox.tsx index 834d9e5..012b2c5 100644 --- a/components/LogoBox.tsx +++ b/components/LogoBox.tsx @@ -1,4 +1,6 @@ +"use client"; import React from "react"; +import Image from "next/image"; interface MeteorCardProps { children: React.ReactNode; @@ -10,26 +12,31 @@ interface MeteorCardProps { const MeteorCard: React.FC = ({ children, logoSrc, - logoAlt = "Logo", + logoAlt = "MeteorMate Logo", className = "", }) => { return (
{/* Logo peeking out the top */} -
- {logoAlt} -
+ {logoSrc && ( +
+ {logoAlt} +
+ )}
{children}
diff --git a/components/ScreenBlocker.tsx b/components/ScreenBlocker.tsx index fef88da..1423d72 100644 --- a/components/ScreenBlocker.tsx +++ b/components/ScreenBlocker.tsx @@ -1,4 +1,5 @@ import { ComputerDesktopIcon } from "@heroicons/react/24/outline"; +import Image from "next/image"; export default function ScreenBlocker() { return ( @@ -14,9 +15,11 @@ export default function ScreenBlocker() {

- Peechi Star
diff --git a/components/Skeleton.tsx b/components/Skeleton.tsx new file mode 100644 index 0000000..79e097c --- /dev/null +++ b/components/Skeleton.tsx @@ -0,0 +1,32 @@ +interface SkeletonProps { + className?: string; + variant?: "text" | "circular" | "rectangular"; + width?: string; + height?: string; +} + +export default function Skeleton({ + className = "", + variant = "rectangular", + width, + height, +}: SkeletonProps) { + const variantClasses = { + text: "h-4 rounded", + circular: "rounded-full", + rectangular: "rounded", + }; + + const style: React.CSSProperties = {}; + if (width) style.width = width; + if (height) style.height = height; + + return ( +
+ ); +} + diff --git a/components/forms/EmailInput.tsx b/components/forms/EmailInput.tsx new file mode 100644 index 0000000..d53a208 --- /dev/null +++ b/components/forms/EmailInput.tsx @@ -0,0 +1,57 @@ +import React from "react"; + +interface EmailInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + label?: string; + error?: string; + disabled?: boolean; + className?: string; +} + +export default function EmailInput({ + value, + onChange, + placeholder = "Email", + label, + error, + disabled = false, + className = "", +}: EmailInputProps) { + return ( +
+ {label && ( + + )} +
+ + + + +
+ {error &&

{error}

} +
+ ); +} + diff --git a/components/forms/PasswordInput.tsx b/components/forms/PasswordInput.tsx new file mode 100644 index 0000000..1b248a7 --- /dev/null +++ b/components/forms/PasswordInput.tsx @@ -0,0 +1,106 @@ +import React, { useState } from "react"; + +interface PasswordInputProps { + value: string; + onChange: (e: React.ChangeEvent) => void; + placeholder?: string; + label?: string; + error?: string; + disabled?: boolean; + className?: string; + showToggle?: boolean; +} + +export default function PasswordInput({ + value, + onChange, + placeholder = "Password", + label, + error, + disabled = false, + className = "", + showToggle = false, +}: PasswordInputProps) { + const [showPassword, setShowPassword] = useState(false); + + return ( +
+ {label && ( + + )} +
+ + + + + {showToggle && ( + + )} +
+ {error &&

{error}

} +
+ ); +} + diff --git a/components/forms/VerificationCodeInput.tsx b/components/forms/VerificationCodeInput.tsx new file mode 100644 index 0000000..341ac8f --- /dev/null +++ b/components/forms/VerificationCodeInput.tsx @@ -0,0 +1,79 @@ +"use client"; +import React, { useRef, useState } from "react"; + +interface VerificationCodeInputProps { + length?: number; + onComplete?: (code: string) => void; + disabled?: boolean; + className?: string; +} + +export default function VerificationCodeInput({ + length = 6, + onComplete, + disabled = false, + className = "", +}: VerificationCodeInputProps) { + const [code, setCode] = useState(Array(length).fill("")); + const inputsRef = useRef<(HTMLInputElement | null)[]>([]); + + const handleChange = (value: string, index: number) => { + if (/^\d$/.test(value)) { + const newCode = [...code]; + newCode[index] = value; + setCode(newCode); + + if (index < length - 1 && inputsRef.current[index + 1]) { + inputsRef.current[index + 1]?.focus(); + } + + // Check if all fields are filled + if (newCode.every((digit) => digit !== "") && onComplete) { + onComplete(newCode.join("")); + } + } + }; + + const handleKeyDown = ( + e: React.KeyboardEvent, + index: number + ) => { + if (e.key === "Backspace") { + const newCode = [...code]; + if (code[index]) { + newCode[index] = ""; + setCode(newCode); + } else if (index > 0) { + newCode[index - 1] = ""; + setCode(newCode); + inputsRef.current[index - 1]?.focus(); + } + } else if (e.key === "Delete") { + const newCode = [...code]; + newCode[index] = ""; + setCode(newCode); + } + }; + + return ( +
+ {code.map((digit, index) => ( + handleChange(e.target.value, index)} + onKeyDown={(e) => handleKeyDown(e, index)} + ref={(el: HTMLInputElement | null) => { + inputsRef.current[index] = el; + }} + disabled={disabled} + className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500 disabled:opacity-50 disabled:cursor-not-allowed" + /> + ))} +
+ ); +} + diff --git a/components/landing/ContactUs.tsx b/components/landing/ContactUs.tsx new file mode 100644 index 0000000..b640d09 --- /dev/null +++ b/components/landing/ContactUs.tsx @@ -0,0 +1,102 @@ +"use client"; +import Image from "next/image"; +import { Mail, Linkedin, Instagram, MapPin } from "lucide-react"; +import { FaDiscord } from "react-icons/fa"; +import LandingSection from "./LandingSection"; + +export default function ContactUs() { + return ( + +
+
+ {/* Logo */} + MeteorMate Logo +
+
+ {/* div for the text */} +
+ {/* Left side text */} +
+

Contact us

+
+
+ +

University of Texas at Dallas

+
+
+ +

MeteorMateSupport@gmail.com

+
+
+
+ + {/* Center text - absolutely positioned for true centering */} +
+

MeteorMate

+ +
+

Home

+

About us

+

Contact us

+
+
+ + {/* Right side text */} +
+

Contact Us

+

Feel free to reach out and leave your

+

feedback!

+ +
+
+ {/* Footer Bottom */} +
+ © 2025 Meteor Mate UTD. All rights reserved + Powered by ACM Development +
+ Terms + Privacy + Data Protection +
+
+
+ ); +} + diff --git a/components/landing/GetStarted.tsx b/components/landing/GetStarted.tsx new file mode 100644 index 0000000..8ddf5d6 --- /dev/null +++ b/components/landing/GetStarted.tsx @@ -0,0 +1,48 @@ +"use client"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import LoadingSpinner from "../LoadingSpinner"; +import LandingSection from "./LandingSection"; + +export default function GetStarted() { + const router = useRouter(); + const [isNavigating, setIsNavigating] = useState(false); + + return ( + +
+

Ready to Find Your

+

Perfect Match?

+
+
+

+ Join other UTD students on the hunt for the +

+

perfect roomate!

+
+ {/* Button for start your search */} +
+ +
+
+ ); +} + diff --git a/components/landing/HeroSection.tsx b/components/landing/HeroSection.tsx new file mode 100644 index 0000000..de10c47 --- /dev/null +++ b/components/landing/HeroSection.tsx @@ -0,0 +1,92 @@ +"use client"; +import Image from "next/image"; +import { Link } from "react-scroll"; +import { useEffect, useState } from "react"; + +export default function HeroSection() { + const [navScrollOffset, setNavScrollOffset] = useState(-96); + + useEffect(() => { + const read = () => { + const raw = getComputedStyle(document.documentElement) + .getPropertyValue("--navbar-height") + .trim(); + const px = Number.parseFloat(raw.replace("px", "")) || 96; + setNavScrollOffset(-Math.max(48, Math.ceil(px))); + }; + read(); + window.addEventListener("resize", read, { passive: true }); + return () => window.removeEventListener("resize", read); + }, []); + + return ( +
+ {/* spacing for fixed navbar */} +
+
+
+

+ Your UTD roomate match starts here. +

+

+ Find your perfect roomate +

+

+ here at UT Dallas! +

+

+ Our goal it to help students like you find +

+

+ compatible roommates based on lifestyle, +

+

+ habits, and interests! Create your profile +

+

+ and explore potential matches to have a +

+

+ roommate that fits your vibe! +

+
+ + Get Started + + + Learn More + +
+
+
+ Laptop showing MeteorMate interface +
+
+
+
+ ); +} + diff --git a/components/landing/HowItWorks.tsx b/components/landing/HowItWorks.tsx new file mode 100644 index 0000000..61f645a --- /dev/null +++ b/components/landing/HowItWorks.tsx @@ -0,0 +1,118 @@ +"use client"; +import Image from "next/image"; +import LandingSection from "./LandingSection"; + +interface FeatureCardProps { + imageSrc: string; + imageAlt: string; + title: string; + description: string; + imageWidth: number; + imageHeight: number; +} + +function FeatureCard({ + imageSrc, + imageAlt, + title, + description, + imageWidth, + imageHeight, +}: FeatureCardProps) { + return ( +
+ {imageAlt} +

{title}

+

{description}

+
+ ); +} + +export default function HowItWorks() { + const features = [ + { + imageSrc: "/images/landing_logo1_S2.png", + imageAlt: "AI Powered Matchmaking Icon", + title: "AI Powered Matchmaking", + description: + "Our advanced algorithm analyzes personality traits and preferences to find you the ideal roommate.", + imageWidth: 88, + imageHeight: 68, + }, + { + imageSrc: "/images/L2.png", + imageAlt: "Data Driven Insights Icon", + title: "Data Driven Insights", + description: + "View comprehensive compatibility metrics and compare potential roommates using interactive charts and graphs.", + imageWidth: 92, + imageHeight: 68, + }, + { + imageSrc: "/images/L3.png", + imageAlt: "Multistep Verification Icon", + title: "Multistep Verification", + description: + "Secure system with your school email and social media verification ensures all users are genuine UTD students.", + imageWidth: 100, + imageHeight: 68, + }, + { + imageSrc: "/images/L4.png", + imageAlt: "Privacy First Icon", + title: "Privacy First", + description: + "Your data is always protected. You control what information you share and who can see it.", + imageWidth: 116, + imageHeight: 72, + }, + { + imageSrc: "/images/L5.png", + imageAlt: "Personalized Matchmaking Icon", + title: "Personalized Matchmaking", + description: + "Tinder styled swiping interface with detailed profile and compatibility scores makes finding your roommate fun and intuitive.", + imageWidth: 88, + imageHeight: 68, + }, + { + imageSrc: "/images/L6.png", + imageAlt: "Social Integration Icon", + title: "Social Integration", + description: + "Optional social media connection for enhanced matching and verification.", + imageWidth: 100, + imageHeight: 80, + }, + ]; + + return ( + +

+ Fast Solution and Best Matches +

+

+ Find your ideal roommate match with our comprehensive platform +

+

+ designed specificially for students just like you +

+
+ {features.map((feature, index) => ( + + ))} +
+
+ ); +} + diff --git a/components/landing/LandingSection.tsx b/components/landing/LandingSection.tsx new file mode 100644 index 0000000..fb24d4d --- /dev/null +++ b/components/landing/LandingSection.tsx @@ -0,0 +1,32 @@ +import React from "react"; + +type LandingSectionProps = { + id: string; + className?: string; + style?: React.CSSProperties; + children: React.ReactNode; +}; + +/** + * Consistent landing-page section wrapper: + * - Standard vertical rhythm between sections + * - Standard scroll offset for fixed navbar + */ +export default function LandingSection({ + id, + className = "", + style, + children, +}: LandingSectionProps) { + return ( +
+ {children} +
+ ); +} + + diff --git a/components/landing/MeetTheTeam.tsx b/components/landing/MeetTheTeam.tsx new file mode 100644 index 0000000..7757ee2 --- /dev/null +++ b/components/landing/MeetTheTeam.tsx @@ -0,0 +1,125 @@ +"use client"; +import LandingSection from "./LandingSection"; +import MemberCard, { TeamMember } from "./MemberCard"; + +export default function MeetTheTeam() { + const teamLeads: TeamMember[] = [ + { + id: 1, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: true, + }, + { + id: 2, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: true, + }, + ]; + + const teamDevelopers: TeamMember[] = [ + { + id: 3, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + { + id: 4, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + { + id: 5, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + { + id: 6, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + { + id: 7, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + { + id: 8, + name: "[NAME HERE]", + gradYear: "[CLASS OF X]", + major: "[MAJOR HERE]", + role: "[ROLE HERE]", + imageSrc: "/images/hero_section_background.png", + isLead: false, + }, + ]; + + return ( + +
+
+

+ Meet the Team +

+

+ The passionate individuals building MeteorMate +

+
+ +
+

+ Project Managers +

+
+ {teamLeads.map((member) => ( + + ))} +
+
+ +
+

+ Development Team +

+
+ {teamDevelopers.map((member) => ( + + ))} +
+
+
+
+ ); +} + diff --git a/components/landing/MemberCard.tsx b/components/landing/MemberCard.tsx new file mode 100644 index 0000000..e1af10f --- /dev/null +++ b/components/landing/MemberCard.tsx @@ -0,0 +1,67 @@ +"use client"; +import Image from "next/image"; + +export interface TeamMember { + id: number; + name: string; + gradYear: string; + major: string; + role: string; + imageSrc: string; + isLead: boolean; +} + +interface MemberCardProps { + member: TeamMember; +} + +const getRoleColor = (role: string) => { + const roleLower = role.toLowerCase(); + if (roleLower.includes("ui/ux") || roleLower.includes("ux")) { + return "bg-gradient-to-r from-purple-500 to-pink-500"; + } else if (roleLower.includes("frontend")) { + return "bg-gradient-to-r from-blue-500 to-cyan-500"; + } else if (roleLower.includes("backend")) { + return "bg-gradient-to-r from-green-500 to-emerald-500"; + } else if (roleLower.includes("full-stack") || roleLower.includes("fullstack")) { + return "bg-gradient-to-r from-orange-500 to-yellow-500"; + } + return "bg-gradient-to-r from-gray-500 to-gray-600"; +}; + +export default function MemberCard({ member }: MemberCardProps) { + return ( +
+
+ {member.name} +
+
+ +
+
+ {member.isLead && ( + + LEAD + + )} +

{member.name}

+ {!member.isLead && ( +
+ + {member.role} + +
+ )} +

{member.major}

+

Class of {member.gradYear}

+
+
+
+ ); +} + diff --git a/components/landing/Navbar.tsx b/components/landing/Navbar.tsx new file mode 100644 index 0000000..0af18b6 --- /dev/null +++ b/components/landing/Navbar.tsx @@ -0,0 +1,131 @@ +"use client"; +import Image from "next/image"; +import { Link } from "react-scroll"; +import { useRouter } from "next/navigation"; +import { useEffect, useRef, useState } from "react"; +import LoadingSpinner from "../LoadingSpinner"; + +export default function Navbar() { + const router = useRouter(); + const [isNavigating, setIsNavigating] = useState(false); + const [isScrolled, setIsScrolled] = useState(false); + const [scrollOffset, setScrollOffset] = useState(-96); + const headerRef = useRef(null); + + useEffect(() => { + const onScroll = () => setIsScrolled(window.scrollY > 12); + onScroll(); + window.addEventListener("scroll", onScroll, { passive: true }); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + useEffect(() => { + const updateOffset = () => { + const h = headerRef.current?.getBoundingClientRect().height ?? 96; + const px = Math.max(48, Math.ceil(h)); + setScrollOffset(-px); + document.documentElement.style.setProperty("--navbar-height", `${px}px`); + }; + + updateOffset(); + window.addEventListener("resize", updateOffset, { passive: true }); + return () => window.removeEventListener("resize", updateOffset); + }, []); + + return ( +
+
+
router.push("/")}> +
+ MeteorMate Logo + {/* Subtle glow effect around logo */} +
+
+
+

+ MeteorMate +

+ + Powered by ACM Dev + +
+
+ + + + {/* Mobile Login Button - keeping it simple but bigger */} +
+ +
+
+
+ ); +} + diff --git a/next.config.ts b/next.config.ts index e9ffa30..cb24a53 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,13 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + // Since images are in the public folder, no need to configure domains + // But we can optimize them + formats: ['image/avif', 'image/webp'], + deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840], + imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], + }, }; export default nextConfig; diff --git a/src/app/_document.tsx b/src/app/_document.tsx deleted file mode 100644 index 5ee27a7..0000000 --- a/src/app/_document.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { Html, Head, Main, NextScript } from "next/document"; - -export default function Document() { - return ( - - - {/* Preconnect for better font loading performance */} - - - - {/* Urbanist font */} - - - {/* Oranienbaum font */} - - - {/* Outfit font */} - - {/* Didact Gothic font */} - - - {/* Didact Inter font */} - - - {/* inter font */} - - - {/* pavanam font */} - - - -
- - - - ); -} diff --git a/src/app/authentication/createAccount/page.tsx b/src/app/authentication/createAccount/page.tsx index 69a1b4e..f8853c2 100644 --- a/src/app/authentication/createAccount/page.tsx +++ b/src/app/authentication/createAccount/page.tsx @@ -9,6 +9,13 @@ import { } from "@/firebase/auth"; //import { getAuth, sendEmailVerification } from 'firebase/auth'; import { Check, X } from "lucide-react"; +import { + validateUTDEmail, + validatePassword, + validatePasswordMatch, + getEmailValidationError, +} from "@/utils/validation"; +import LoadingSpinner from "../../../../components/LoadingSpinner"; export default function CreateAccountPage() { const router = useRouter(); @@ -17,65 +24,27 @@ export default function CreateAccountPage() { const [emailError, setEmailError] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [confirmPasswordError, setConfirmPasswordError] = useState(""); + const [passwordValidation, setPasswordValidation] = useState( + validatePassword("") + ); - // requirements - const [minCharacters, setMinCharacters] = useState(false); - const [lowercaseLetter, setLowercaseLetter] = useState(false); - const [uppercaseLetter, setUppercaseLetter] = useState(false); - const [nonAlphanumericCharacter, setNonAlphanumericCharacter] = - useState(false); - const [numberCharacter, setNumberCharacter] = useState(false); - const [requirementsMet, setRequirementsMet] = useState(false); - - //const userLoggedIn = auth?.userLoggedIn; const [isSigningUp, setIsSigningUp] = useState(false); const handleEmailChange = (e: React.ChangeEvent) => { const value = e.target.value; setEmail(value); - - if (!value.endsWith("@utdallas.edu")) { - setEmailError("Email must end with @utdallas.edu"); - } else { - setEmailError(""); - } + setEmailError(getEmailValidationError(value)); }; const handlePasswordChange = (e: React.ChangeEvent) => { - setPassword(e.target.value); - // password length check - if (e.target.value.length >= 8) { - setMinCharacters(true); - } else { - setMinCharacters(false); - } - // lowercase letter check - if (e.target.value.search(/[a-z]/) == -1) { - setLowercaseLetter(false); - } else { - setLowercaseLetter(true); - } - // uppcase letter check - if (e.target.value.search(/[A-Z]/) == -1) { - setUppercaseLetter(false); - } else { - setUppercaseLetter(true); - } - // nonalphanumeric character check - if (e.target.value.search(/[$*.[\]{}()?\"!@#%&/\\,<>':;|_~]/) == -1) { - setNonAlphanumericCharacter(false); - } else { - setNonAlphanumericCharacter(true); - } - if (e.target.value.search(/[0-9]/) == -1) { - setNumberCharacter(false); - } else { - setNumberCharacter(true); - } - if (e.target.value.length >= 6 && e.target.value.search(/[a-z]/) != -1 && e.target.value.search(/[A-Z]/) != -1 && e.target.value.search(/[$*.[\]{}()?\"!@#%&/\\,<>':;|_~]/) != -1 && e.target.value.search(/[0-9]/) != -1){ - setRequirementsMet(true); - } else { - setRequirementsMet(false); + const value = e.target.value; + setPassword(value); + const validation = validatePassword(value); + setPasswordValidation(validation); + + // Update confirm password error if confirm password is already filled + if (confirmPassword) { + setConfirmPasswordError(validatePasswordMatch(value, confirmPassword)); } }; @@ -84,21 +53,27 @@ export default function CreateAccountPage() { ) => { const value = e.target.value; setConfirmPassword(value); - if (value !== password) { - setConfirmPasswordError("Passwords do not match"); - } else { - setConfirmPasswordError(""); - } + setConfirmPasswordError(validatePasswordMatch(password, value)); }; const handleCreateAccount = async () => { - if (!email.endsWith("@utdallas.edu")) { - setEmailError("Please enter a valid @utdallas.edu email."); + // Validate email + const emailErr = getEmailValidationError(email); + if (emailErr) { + setEmailError(emailErr); + return; + } + + // Validate password + if (!passwordValidation.isValid) { + setEmailError("Please fix password requirements before continuing."); return; } - if (password !== confirmPassword) { - setConfirmPasswordError("Passwords do not match"); + // Validate password match + const passwordMatchError = validatePasswordMatch(password, confirmPassword); + if (passwordMatchError) { + setConfirmPasswordError(passwordMatchError); return; } @@ -118,12 +93,15 @@ export default function CreateAccountPage() { router.push("/authentication/verifyEmail"); } - } catch (err: any) { + } catch (err: unknown) { console.error("Signup error:", err); - if (err.code === "auth/email-already-in-use") { + if (err && typeof err === "object" && "code" in err && err.code === "auth/email-already-in-use") { setEmailError("An account with this email already exists."); } else { - setEmailError(err.message || "Sign Up failed"); + const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string" + ? err.message + : "Sign Up failed"; + setEmailError(errorMessage); } } finally { setIsSigningUp(false); @@ -196,7 +174,8 @@ export default function CreateAccountPage() { value={email} onChange={handleEmailChange} placeholder="Email" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSigningUp} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" />
{emailError && ( @@ -229,7 +208,8 @@ export default function CreateAccountPage() { value={password} onChange={handlePasswordChange} placeholder="Password" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSigningUp} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" />
@@ -259,7 +239,8 @@ export default function CreateAccountPage() { value={confirmPassword} onChange={handleConfirmPasswordChange} placeholder="Re-Enter Password" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSigningUp} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" />
{confirmPasswordError && ( @@ -271,7 +252,7 @@ export default function CreateAccountPage() {

Passwords must:

- {minCharacters ? ( + {passwordValidation.checks.minLength ? (

Be at least 8 characters

@@ -280,7 +261,7 @@ export default function CreateAccountPage() { Be at least 8 characters

)} - {lowercaseLetter ? ( + {passwordValidation.checks.lowercase ? (

Include at least one lowercase letter (a-z) @@ -291,7 +272,7 @@ export default function CreateAccountPage() { (a-z)

)} - {uppercaseLetter ? ( + {passwordValidation.checks.uppercase ? (

Include at least one uppercase letter (A-Z) @@ -302,7 +283,7 @@ export default function CreateAccountPage() { (A-Z)

)} - {nonAlphanumericCharacter ? ( + {passwordValidation.checks.special ? (

Include a special character (!@#$%)

@@ -311,7 +292,7 @@ export default function CreateAccountPage() { Include a special character (!@#$%)

)} - {numberCharacter ? ( + {passwordValidation.checks.number ? (

Include a number (0-9)

@@ -325,15 +306,16 @@ export default function CreateAccountPage() { {/* create account button */} +
diff --git a/src/app/authentication/forgotPassword/page.tsx b/src/app/authentication/forgotPassword/page.tsx index f7e3eb1..6f1bef9 100644 --- a/src/app/authentication/forgotPassword/page.tsx +++ b/src/app/authentication/forgotPassword/page.tsx @@ -1,29 +1,29 @@ "use client"; -import React, { useRef, useState } from "react"; +import React, { useState } from "react"; import LogoBox from "../../../../components/LogoBox"; import { useRouter } from "next/navigation"; +import { getEmailValidationError } from "@/utils/validation"; +import LoadingSpinner from "../../../../components/LoadingSpinner"; export default function VerifyEmailPage() { const router = useRouter(); const [email, setEmail] = useState(""); - const [code, setCode] = useState(Array(6).fill("")); - const inputsRef = useRef<(HTMLInputElement | null)[]>([]); const [emailError, setEmailError] = useState(""); const [isSending, setIsSending] = useState(false); const handleEmailChange = (e: React.ChangeEvent) => { const value = e.target.value; setEmail(value); - - if (!value.endsWith("@utdallas.edu")) { - setEmailError("Email must end with @utdallas.edu"); - } else { - setEmailError(""); - } + setEmailError(getEmailValidationError(value)); }; const handleResetPassword = async () => { - if (!email || emailError) return; + // Validate email before proceeding + const emailErr = getEmailValidationError(email); + if (emailErr) { + setEmailError(emailErr); + return; + } try { setIsSending(true); @@ -51,9 +51,12 @@ export default function VerifyEmailPage() { router.push( `/authentication/verifyPassword?email=${encodeURIComponent(email)}` ); - } catch (err: any) { + } catch (err: unknown) { console.error("Error sending reset verification:", err); - setEmailError(err.message || "Something went wrong. Please try again."); + const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string" + ? err.message + : "Something went wrong. Please try again."; + setEmailError(errorMessage); } finally { setIsSending(false); } @@ -93,7 +96,8 @@ export default function VerifyEmailPage() { value={email} onChange={handleEmailChange} placeholder="abc123452@utdallas.edu" - className="pl-11 pr-10 border border-black py-3 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSending} + className="pl-11 pr-10 border border-black py-3 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" />
{emailError && ( @@ -105,8 +109,9 @@ export default function VerifyEmailPage() { diff --git a/src/app/authentication/newPassword/page.tsx b/src/app/authentication/newPassword/page.tsx index 2b72a9f..6f68542 100644 --- a/src/app/authentication/newPassword/page.tsx +++ b/src/app/authentication/newPassword/page.tsx @@ -2,6 +2,8 @@ import React, { useState, useEffect } from "react"; import LogoBox from "../../../../components/LogoBox"; import { useRouter } from "next/navigation"; +import { validatePasswordMatch, validatePassword } from "@/utils/validation"; +import LoadingSpinner from "../../../../components/LoadingSpinner"; export default function NewPasswordPage() { const router = useRouter(); @@ -29,12 +31,11 @@ export default function NewPasswordPage() { }, [router]); const handlePasswordChange = (e: React.ChangeEvent) => { - setPassword(e.target.value); - // live update match error if confirm is already filled - if (confirmPassword && e.target.value !== confirmPassword) { - setConfirmPasswordError("Passwords do not match"); - } else { - setConfirmPasswordError(""); + const value = e.target.value; + setPassword(value); + // Live update match error if confirm password is already filled + if (confirmPassword) { + setConfirmPasswordError(validatePasswordMatch(value, confirmPassword)); } }; @@ -43,11 +44,7 @@ export default function NewPasswordPage() { ) => { const value = e.target.value; setConfirmPassword(value); - if (value !== password) { - setConfirmPasswordError("Passwords do not match"); - } else { - setConfirmPasswordError(""); - } + setConfirmPasswordError(validatePasswordMatch(password, value)); }; const handleSubmit = async () => { @@ -57,8 +54,18 @@ export default function NewPasswordPage() { setErrorMsg("Please fill out both password fields."); return; } - if (password !== confirmPassword) { - setConfirmPasswordError("Passwords do not match"); + + // Validate password requirements + const passwordValidation = validatePassword(password); + if (!passwordValidation.isValid) { + setErrorMsg("Password does not meet requirements. Please ensure it has at least 8 characters, includes uppercase, lowercase, number, and special character."); + return; + } + + // Validate password match + const passwordMatchError = validatePasswordMatch(password, confirmPassword); + if (passwordMatchError) { + setConfirmPasswordError(passwordMatchError); return; } if (!email || !code) { @@ -97,9 +104,12 @@ export default function NewPasswordPage() { // Redirect to login router.push("../authentication"); - } catch (err: any) { + } catch (err: unknown) { console.error("Reset password error:", err); - setErrorMsg(err.message || "Something went wrong."); + const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string" + ? err.message + : "Something went wrong."; + setErrorMsg(errorMessage); } finally { setIsSubmitting(false); } @@ -137,7 +147,8 @@ export default function NewPasswordPage() { value={password} onChange={handlePasswordChange} placeholder="Password" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSubmitting} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" /> @@ -167,7 +178,8 @@ export default function NewPasswordPage() { value={confirmPassword} onChange={handleConfirmPasswordChange} placeholder="Re-Enter Password" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSubmitting} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" /> {confirmPasswordError && ( @@ -185,8 +197,9 @@ export default function NewPasswordPage() { diff --git a/src/app/authentication/page.tsx b/src/app/authentication/page.tsx index 0cfbc8d..316569f 100644 --- a/src/app/authentication/page.tsx +++ b/src/app/authentication/page.tsx @@ -5,6 +5,8 @@ import LogoBox from "../../../components/LogoBox"; import { doSignInWithEmailAndPassword } from "../../firebase/auth"; import { useAuth } from "../../contexts/authContext"; import { useSearchParams } from "next/navigation"; +import { validateUTDEmail, getEmailValidationError } from "@/utils/validation"; +import LoadingSpinner from "../../../components/LoadingSpinner"; export default function LoginPage() { const router = useRouter(); @@ -30,12 +32,7 @@ export default function LoginPage() { const handleEmailChange = (e: React.ChangeEvent) => { const value = e.target.value; setEmail(value); - - if (!value.endsWith("@utdallas.edu")) { - setEmailError("Email must end with @utdallas.edu"); - } else { - setEmailError(""); - } + setEmailError(getEmailValidationError(value)); }; const handlePasswordChange = (e: React.ChangeEvent) => { @@ -43,10 +40,12 @@ export default function LoginPage() { }; const handleLogin = async () => { - if (!email.endsWith("@utdallas.edu")) { - setEmailError("Please enter a valid @utdallas.edu email."); + // Validate email + const emailErr = getEmailValidationError(email); + if (emailErr) { + setEmailError(emailErr); return; - } // just making sure that the email ends with @utdallas.edu + } //then we try to get if is signing in is true whne the value is flipped then we set the value to be actually true and then call signing with email and password and then router.push it to the createAccount page try { @@ -55,9 +54,12 @@ export default function LoginPage() { await doSignInWithEmailAndPassword(email, password); router.push("../dashboard"); // redirect after login CHANGE HERE ONCE THE HOME PAGE IS UP } - } catch (err: any) { //just in case there's a problem signing in + } catch (err: unknown) { //just in case there's a problem signing in console.error("Login error:", err); - setEmailError(err.message || "Login failed"); // for what reasons + const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string" + ? err.message + : "Login failed"; + setEmailError(errorMessage); // for what reasons } finally { setIsSigningIn(false); } @@ -122,7 +124,8 @@ export default function LoginPage() { value={email} onChange={handleEmailChange} placeholder="Email" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSigningIn} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" /> {emailError && ( @@ -152,7 +155,8 @@ export default function LoginPage() { value={password} onChange={handlePasswordChange} placeholder="Password" - className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full" + disabled={isSigningIn} + className="pl-11 pr-4 border border-black py-2 rounded-3xl font-light text-[12px] md:text-[15px] text-left w-full disabled:opacity-50 disabled:cursor-not-allowed" /> @@ -160,8 +164,9 @@ export default function LoginPage() { diff --git a/src/app/authentication/verifyEmail/page.tsx b/src/app/authentication/verifyEmail/page.tsx index 7112558..5301ca4 100644 --- a/src/app/authentication/verifyEmail/page.tsx +++ b/src/app/authentication/verifyEmail/page.tsx @@ -2,13 +2,15 @@ import React, { useRef, useState } from "react"; import LogoBox from "../../../../components/LogoBox"; import { useRouter } from "next/navigation"; +import LoadingSpinner from "../../../../components/LoadingSpinner"; export default function VerifyEmailPage() { - const router = useRouter(); // NEW: actually use the router + const router = useRouter(); const [code, setCode] = useState(Array(6).fill("")); const inputsRef = useRef<(HTMLInputElement | null)[]>([]); const [email] = useState(null); - const [error, setError] = useState(null); // NEW: error message state + const [error, setError] = useState(null); + const [isVerifying, setIsVerifying] = useState(false); const handleChange = (value: string, index: number) => { if (/^\d$/.test(value)) { @@ -45,14 +47,17 @@ export default function VerifyEmailPage() { const handleVerifyEmail = async () => { const verificationCode = code.join(""); - setError(null); // NEW: clear any previous error + setError(null); if (verificationCode.length !== 6) { setError("Please enter the 6-digit code."); return; } + if (isVerifying) return; // Prevent multiple submissions + try { + setIsVerifying(true); const email = localStorage.getItem("verificationEmail"); if (!email) { @@ -73,17 +78,17 @@ export default function VerifyEmailPage() { if (!response.ok) { const errorData = await response.json().catch(() => ({})); - // NEW: show a red error message above the button setError(errorData.detail || "Invalid code. Please try again."); return; } - // success -> reroute to login - // NEW: - router.push("../authentication"); + // Success -> redirect to login + router.push("../authentication?created=1"); } catch (err) { setError("Something went wrong. Please try again."); console.error("Verification error:", (err as Error).message); + } finally { + setIsVerifying(false); } }; @@ -117,12 +122,13 @@ export default function VerifyEmailPage() { ref={(el: HTMLInputElement | null) => { inputsRef.current[index] = el; }} - className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500" + disabled={isVerifying} + className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500 disabled:opacity-50 disabled:cursor-not-allowed" /> ))} - {/* NEW: inline error message */} + {/* Error message */} {error && (

{error}

)} @@ -130,9 +136,11 @@ export default function VerifyEmailPage() { {/* verify button */} diff --git a/src/app/authentication/verifyPassword/page.tsx b/src/app/authentication/verifyPassword/page.tsx index 5be0233..50c5c10 100644 --- a/src/app/authentication/verifyPassword/page.tsx +++ b/src/app/authentication/verifyPassword/page.tsx @@ -2,6 +2,7 @@ import React, { useRef, useState } from "react"; import LogoBox from "../../../../components/LogoBox"; import { useRouter, useSearchParams } from "next/navigation"; +import LoadingSpinner from "../../../../components/LoadingSpinner"; export default function VerifyPassword() { const router = useRouter(); @@ -98,9 +99,12 @@ export default function VerifyPassword() { // ✅ only go to new password page *after* successful verification router.push("/authentication/newPassword"); - } catch (err: any) { + } catch (err: unknown) { console.error("Error verifying reset code:", err); - setError(err.message || "Verification failed. Please try again."); + const errorMessage = err && typeof err === "object" && "message" in err && typeof err.message === "string" + ? err.message + : "Verification failed. Please try again."; + setError(errorMessage); } finally { setIsVerifying(false); } @@ -136,7 +140,8 @@ export default function VerifyPassword() { ref={(el: HTMLInputElement | null) => { inputsRef.current[index] = el; }} - className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500" + disabled={isVerifying} + className="w-12 h-12 text-center text-xl border-2 border-gray-300 rounded-lg focus:outline-none focus:border-green-500 disabled:opacity-50 disabled:cursor-not-allowed" /> ))} @@ -149,8 +154,9 @@ export default function VerifyPassword() { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 67be090..11a1cab 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -10,8 +10,58 @@ export default function RootLayout({ children: React.ReactNode; }) { return ( - - + + + {/* Preconnect for better font loading performance */} + + + + {/* Urbanist font */} + + + {/* Oranienbaum font */} + + + {/* Outfit font */} + + {/* Didact Gothic font */} + + + {/* Didact Inter font */} + + + {/* inter font */} + + + {/* pavanam font */} + + +
{children}
diff --git a/src/app/page.tsx b/src/app/page.tsx index 078b93c..6622363 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,343 +1,23 @@ "use client"; -import { useRouter } from "next/navigation"; import "./globals.css"; -import { Mail, Linkedin, Instagram, MapPin } from "lucide-react"; -import { FaDiscord } from "react-icons/fa"; - -import { Link } from "react-scroll"; +import Navbar from "../../components/landing/Navbar"; +import HeroSection from "../../components/landing/HeroSection"; +import HowItWorks from "../../components/landing/HowItWorks"; +import GetStarted from "../../components/landing/GetStarted"; +import MeetTheTeam from "../../components/landing/MeetTheTeam"; +import ContactUs from "../../components/landing/ContactUs"; export default function Home() { - const router = useRouter(); - return (
- {/* the first landing page screen */} -
- {/* black background behind background image */} -
-
- {/* navbar div */} -
-
- logo -

- MeteorMate -

-

- Powered by ACM Dev -

-
- - How It Works - - - - Get Started - - - Contact Us - - -
- {/* main landing page content */} -
-
-

- Your UTD roomate match starts here. -

-

- Find your perfect roomate -

-

- here at UT Dallas! -

-

- Our goal it to help students like you find -

-

- compatible roommates based on lifestyle, -

-

- habits, and interests! Create your profile -

-

- and explore potential matches to have a -

-

- roommate that fits your vibe! -

-
- - Get Started - - - Learn More - -
-
-
- -
-
-
-
-
- - {/* Second screen */} -
-

- Fast Solution and Best Matches -

-

- Find your ideal roommate match with our comprehensive platform -

-

- designed specificially for students just like you -

-
-
- AI Powered -

AI Powered Matchmaking

-

- Our advanced algorithm analyzes personality traits and preferences to - find you the ideal roommate. -

-
- -
- AI Powered -

Data Driven Insights

-

- View comprehensive compatibility metrics and compare potential roommates - using interactive charts and graphs. -

-
- -
- AI Powered -

Multistep Verification

-

- Secure system with your school email and social media verification - ensures all users are genuine UTD students. -

-
- -
- AI Powered -

Privacy First

-

- Your data is always protected. You control what information you share and - who can see it. -

-
- -
- AI Powered -

Personalized Matchmaking

-

- Tinder styled swiping interface with detailed profile and compatibility - scores makes finding your roommate fun and intuitive. -

-
- -
- AI Powered -

Social Integration

-

- Optional social media connection for enhanced matching and verification. -

-
-
-
- - {/* Third section */} -
-
-

Ready to Find Your

-

Perfect Match?

-
-
-

- Join other UTD students on the hunt for the -

-

perfect roomate!

-
- {/* Button for start your search */} -
- -
-
- - {/* Fourth section */} -
-
-
- {/* Logo */} - Logo -
-
- {/* div for the text */} -
- {/* Left side text */} -
-

Contact us

-
-
- -

University of Texas at Dallas

-
-
- -

MeteorMateSupport@gmail.com

-
-
-
- - {/* Center text - absolutely positioned for true centering */} -
-

MeteorMate

- -
-

Home

-

About us

-

Contact us

-
-
- - {/* Right side text */} -
-

Contact Us

-

Feel free to reach out and leave your

-

feedback!

- -
-
- {/* Footer Bottom */} -
- © 2025 Meteor Mate UTD. All rights reserved - Powered by ACM Development -
- Terms - Privacy - Data Protection -
-
-
+ +
+ + + + + +
); } diff --git a/src/app/surveyPage/page.tsx b/src/app/surveyPage/page.tsx index 59861e1..c463a0a 100644 --- a/src/app/surveyPage/page.tsx +++ b/src/app/surveyPage/page.tsx @@ -1,7 +1,4 @@ "use client" -import { getAuth } from "firebase/auth"; -import { useEffect, useState } from "react"; -import { useRouter } from "next/navigation"; export default function VerifyEmailPage() { diff --git a/src/utils/validation.ts b/src/utils/validation.ts new file mode 100644 index 0000000..845295d --- /dev/null +++ b/src/utils/validation.ts @@ -0,0 +1,104 @@ +export const VALIDATION_RULES = { + UTD_EMAIL: { + pattern: /^[a-zA-Z0-9._%+-]+@utdallas\.edu$/, + message: 'Email must end with @utdallas.edu', + }, + PASSWORD: { + minLength: 8, + requireLowercase: true, + requireUppercase: true, + requireNumber: true, + requireSpecial: true, + }, +} as const; + +export interface PasswordValidationResult { + isValid: boolean; + errors: string[]; + checks: { + minLength: boolean; + lowercase: boolean; + uppercase: boolean; + number: boolean; + special: boolean; + }; +} + +/** + * Validates if an email ends with @utdallas.edu + * @param email - The email address to validate + * @returns true if valid, false otherwise + */ +export function validateUTDEmail(email: string): boolean { + return VALIDATION_RULES.UTD_EMAIL.pattern.test(email); +} + +/** + * Validates a password against all requirements + * @param password - The password to validate + * @returns PasswordValidationResult with validation status and checks + */ +export function validatePassword(password: string): PasswordValidationResult { + const checks = { + minLength: password.length >= VALIDATION_RULES.PASSWORD.minLength, + lowercase: /[a-z]/.test(password), + uppercase: /[A-Z]/.test(password), + number: /[0-9]/.test(password), + special: /[$*.[\]{}()?"!@#%&/\\,<>':;|_~]/.test(password), + }; + + const errors: string[] = []; + if (!checks.minLength) { + errors.push(`Password must be at least ${VALIDATION_RULES.PASSWORD.minLength} characters`); + } + if (!checks.lowercase) { + errors.push('Password must include a lowercase letter'); + } + if (!checks.uppercase) { + errors.push('Password must include an uppercase letter'); + } + if (!checks.number) { + errors.push('Password must include a number'); + } + if (!checks.special) { + errors.push('Password must include a special character'); + } + + return { + isValid: Object.values(checks).every(check => check === true), + errors, + checks, + }; +} + +/** + * Validates if two passwords match + * @param password - The original password + * @param confirmPassword - The password to confirm + * @returns Error message if passwords don't match, empty string if they match + */ +export function validatePasswordMatch( + password: string, + confirmPassword: string +): string { + if (password !== confirmPassword) { + return 'Passwords do not match'; + } + return ''; +} + +/** + * Gets a user-friendly email validation error message + * @param email - The email to validate + * @returns Error message if invalid, empty string if valid + */ +export function getEmailValidationError(email: string): string { + if (!email) { + return 'Email is required'; + } + if (!validateUTDEmail(email)) { + return VALIDATION_RULES.UTD_EMAIL.message; + } + return ''; +} +