diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 2a21084..3b12d45 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,66 +1,71 @@ import { HelmetProvider } from "react-helmet-async"; import { RouterProvider, createBrowserRouter } from "react-router-dom"; -import { Footer } from "src/components/Footer"; import { Navbar } from "src/components/Navbar"; +import { PrivateRoute } from "src/components/PrivateRoute"; +import { RootLayout } from "src/components/RootLayout"; import { Home } from "src/pages"; +import { AddProduct } from "src/pages/AddProduct"; +import { EditProduct } from "src/pages/EditProduct"; +import { IndividualProductPage } from "src/pages/Individual-product-page"; import { Marketplace } from "src/pages/Marketplace"; - -import { PrivateRoute } from "../src/components/PrivateRoute"; -import { AddProduct } from "../src/pages/AddProduct"; -import { EditProduct } from "../src/pages/EditProduct"; -import { IndividualProductPage } from "../src/pages/Individual-product-page"; -import { PageNotFound } from "../src/pages/PageNotFound"; -import FirebaseProvider from "../src/utils/FirebaseProvider"; -import { SavedProducts } from "./pages/SavedProducts"; +import { PageNotFound } from "src/pages/PageNotFound"; +import { SavedProducts } from "src/pages/SavedProducts"; +import FirebaseProvider from "src/utils/FirebaseProvider"; const router = createBrowserRouter([ { path: "/", - element: , - }, - { - path: "/products", - element: ( - - - - ), - }, - { - path: "/add-product", - element: ( - - - - ), - }, - { - path: "/edit-product/:id", - element: ( - - - - ), - }, - { - path: "/products/:id", - element: ( - - - - ), - }, - { - path: "/saved-products", - element: ( - - - - ), - }, - { - path: "*", - element: , + element: , + children: [ + { + path: "/", + element: , + }, + { + path: "/products", + element: ( + + + + ), + }, + { + path: "/add-product", + element: ( + + + + ), + }, + { + path: "/edit-product/:id", + element: ( + + + + ), + }, + { + path: "/products/:id", + element: ( + + + + ), + }, + { + path: "/saved-products", + element: ( + + + + ), + }, + { + path: "*", + element: , + }, + ], }, ]); @@ -69,11 +74,9 @@ export default function App() {
-
-
diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..3c3d6c6 --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,8 @@ +export function Header() { + return ( + <> +
+
+ + ); +} diff --git a/frontend/src/components/Navbar.tsx b/frontend/src/components/Navbar.tsx index b35b3c6..990a826 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,18 +1,79 @@ -import { faBars, faCartShopping, faUser, faXmark, faHeart } from "@fortawesome/free-solid-svg-icons"; +import { faHeart } from "@fortawesome/free-regular-svg-icons"; +import { + faBars, + faCartShopping, + faMagnifyingGlass, + faUser, + faXmark, +} from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import { useContext, useEffect, useRef, useState } from "react"; +import { HTMLAttributes, forwardRef, useContext, useEffect, useRef, useState } from "react"; +import { useNavigate } from "react-router-dom"; import { FirebaseContext } from "src/utils/FirebaseProvider"; +interface MiniSearchbarProps extends HTMLAttributes { + open: boolean; + onSubmit: React.FormEventHandler; +} + +const MiniSearchbar = forwardRef( + ({ open, onSubmit, ...props }, ref) => { + return ( +
+

Search the marketplace

+
+
+
+ +
+ +
+
+
+ ); + }, +); + +MiniSearchbar.displayName = "MiniSearchbar"; + export function Navbar() { const { user, signOutFromFirebase, openGoogleAuthentication } = useContext(FirebaseContext); const [isMobileMenuOpen, setMobileMenuOpen] = useState(false); + const [isSearchBarOpen, setSearchbarOpen] = useState(false); const menuRef = useRef(null); const buttonRef = useRef(null); + const searchRef = useRef(null); + const navigate = useNavigate(); const toggleMobileMenu = () => { setMobileMenuOpen(!isMobileMenuOpen); }; + const handleSearch = (e: React.FormEvent) => { + if (!searchRef.current) return; + e.preventDefault(); + const formData = new FormData(searchRef.current); + const url = new URL("/products", window.location.origin); + url.searchParams.set("query", formData.get("query") as string); + navigate(url.pathname + url.search); + return; + }; + useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if ( @@ -23,6 +84,13 @@ export function Navbar() { ) { setMobileMenuOpen(false); } + + if (searchRef.current && !searchRef.current.contains(event.target as Node)) { + setSearchbarOpen(false); + setTimeout(() => { + if (searchRef.current) searchRef.current.reset(); + }, 100); + } }; const handleResize = () => { @@ -40,65 +108,73 @@ export function Navbar() { }; }, []); + const tabStyling = "text-gray-400 hover:text-gray-800"; + const selectedTabStyling = "text-ucsd-blue"; + return ( <> -
+
+ + +
- -
  • - {user ? ( - - ) : ( - - )} -
  • - + +
    + + + + +
    {/* Mobile View */}
    diff --git a/frontend/src/components/RootLayout.tsx b/frontend/src/components/RootLayout.tsx new file mode 100644 index 0000000..3e8f523 --- /dev/null +++ b/frontend/src/components/RootLayout.tsx @@ -0,0 +1,13 @@ +import { Outlet } from "react-router-dom"; +import { Navbar } from "src/components/Navbar"; + +export const RootLayout = () => { + return ( + <> + +
    + +
    + + ); +}; diff --git a/frontend/src/components/SearchBar.tsx b/frontend/src/components/SearchBar.tsx index aa40a0f..7d9852b 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; import { get } from "src/api/requests"; interface Props { @@ -7,7 +8,8 @@ interface Props { } export default function SearchBar({ setProducts, setError }: Props) { - const [query, setQuery] = useState(null); + const [searchParams] = useSearchParams(); + const [query, setQuery] = useState(searchParams.get("query") || ""); useEffect(() => { /* @@ -41,9 +43,14 @@ export default function SearchBar({ setProducts, setError }: Props) { search(); }, [query]); + useEffect(() => { + setQuery(searchParams.get("query")); + }, [searchParams]); + return ( setQuery(e.target.value)} placeholder="Search for a product..." className="w-full bg-[#F8F8F8] shadow-md p-3 px-6 mx-auto my-2 rounded-3xl" diff --git a/frontend/src/index.css b/frontend/src/index.css index b5c61c9..d26d7a0 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,3 +1,5 @@ +@import url("https://fonts.googleapis.com/css2?family=Rubik:wght@300;400;500;600;700&display=swap"); + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 3843f61..b5eada6 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,55 +1,33 @@ import { useContext } from "react"; -import { Helmet } from "react-helmet-async"; import { Navigate } from "react-router-dom"; +import { Header } from "src/components/Header"; import { FirebaseContext } from "src/utils/FirebaseProvider"; const buttonStyles = - "bg-slate-800 text-white py-3 px-4 rounded-md mt-2 hover:bg-slate-900 transition-colors w-full sm:w-fit flex flex-row gap-3 justify-center"; + "text-gray-400 text-lg border-2 shadow-md py-2 px-4 rounded-full hover:bg-gray-200 transition-colors w-full sm:w-fit flex items-center justify-center"; export function Home() { - const { user, openGoogleAuthentication, signOutFromFirebase } = useContext(FirebaseContext); + const { user, openGoogleAuthentication } = useContext(FirebaseContext); if (user) { return ; } return ( - <> - - Low-Price Center - -
    -
    -

    - Welcome to -
    - Low-Price Center -

    -
    -

    - An online marketplace made by and for UCSD students - to buy and sell goods. -

    -
    - {user ? ( - - ) : ( - - )} -
    -
    - UCSD Price Center -
    -
    - +
    +
    +
    +
    + Low + Price Center +
    +
    +
    Log in to sell on Low Price Center
    + +
    +
    +
    ); } diff --git a/frontend/src/pages/Marketplace.tsx b/frontend/src/pages/Marketplace.tsx index a80effd..23ae778 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect, useContext } from "react"; +import { useContext, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; +import { get, post } from "src/api/requests"; +import { Header } from "src/components/Header"; import Product from "src/components/Product"; import SearchBar from "src/components/SearchBar"; import { FirebaseContext } from "src/utils/FirebaseProvider"; -import { get, post } from "src/api/requests"; export function Marketplace() { const [products, setProducts] = useState< @@ -50,6 +51,7 @@ export function Marketplace() { return ( <> +
    Low-Price Center Marketplace diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 1247a45..43d6fd7 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -16,6 +16,7 @@ export default { fontFamily: { jetbrains: ["JetBrains Mono", "monospace"], inter: ["Inter", "sans-serif"], + rubik: ["Rubik", "sans-serif"], }, }, },