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 0167fd1..ef44292 100644 --- a/frontend/src/components/Navbar.tsx +++ b/frontend/src/components/Navbar.tsx @@ -1,16 +1,77 @@ -import { useContext, useEffect, useRef, useState } from "react"; -import { faBars, faXmark } 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 { 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((o) => !o); + 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 ( @@ -21,6 +82,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 = () => { if (window.matchMedia("(min-width: 768px)").matches) setMobileMenuOpen(false); @@ -33,35 +101,73 @@ export function Navbar() { }; }, []); - // Shared circular icon button style - const iconBtn = - "w-9 h-9 rounded-full border border-gray-200 flex items-center justify-center text-gray-500 hover:text-ucsd-blue hover:border-ucsd-blue transition"; + const tabStyling = "text-gray-400 hover:text-gray-800"; + const selectedTabStyling = "text-ucsd-blue"; return ( <> -
+
+ + +
+ +
+ + + + +
onClick={() => (window.location.href = "/add-product")} className="hover:text-ucsd-blue transition-colors" > 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 80d2ee6..4136003 100644 --- a/frontend/src/components/SearchBar.tsx +++ b/frontend/src/components/SearchBar.tsx @@ -1,4 +1,6 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; +import { useSearchParams } from "react-router-dom"; +import { get } from "src/api/requests"; interface Props { setProducts: (query: string) => void; @@ -6,13 +8,18 @@ interface Props { } export default function SearchBar({ setProducts, setError }: Props) { - const [query, setQuery] = useState(null); + const [searchParams] = useSearchParams(); + const [query, setQuery] = useState(searchParams.get("query") || ""); const handleChange = (value: string) => { setQuery(value); setProducts(value); }; + useEffect(() => { + setQuery(searchParams.get("query")); + }, [searchParams]); + 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 c385c94..e9ec6e0 100644 --- a/frontend/src/pages/Marketplace.tsx +++ b/frontend/src/pages/Marketplace.tsx @@ -1,11 +1,12 @@ -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 { useNavigate } from "react-router-dom"; import Product from "src/components/Product"; import SearchBar from "src/components/SearchBar"; import FilterSort from "src/components/FilterSort"; import { FirebaseContext } from "src/utils/FirebaseProvider"; -import { get, post } from "src/api/requests"; interface FilterState { minPrice?: number; @@ -108,6 +109,7 @@ export function Marketplace() { return ( <> +
Low-Price Center diff --git a/frontend/tailwind.config.js b/frontend/tailwind.config.js index 1d48fea..e962211 100644 --- a/frontend/tailwind.config.js +++ b/frontend/tailwind.config.js @@ -20,6 +20,7 @@ export default { fontFamily: { jetbrains: ["JetBrains Mono", "monospace"], inter: ["Inter", "sans-serif"], + rubik: ["Rubik", "sans-serif"], }, }, },