Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 59 additions & 56 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
@@ -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: <Home />,
},
{
path: "/products",
element: (
<PrivateRoute>
<Marketplace />
</PrivateRoute>
),
},
{
path: "/add-product",
element: (
<PrivateRoute>
<AddProduct />
</PrivateRoute>
),
},
{
path: "/edit-product/:id",
element: (
<PrivateRoute>
<EditProduct />
</PrivateRoute>
),
},
{
path: "/products/:id",
element: (
<PrivateRoute>
<IndividualProductPage />
</PrivateRoute>
),
},
{
path: "/saved-products",
element: (
<PrivateRoute>
<SavedProducts />
</PrivateRoute>
),
},
{
path: "*",
element: <PageNotFound />,
element: <RootLayout />,
children: [
{
path: "/",
element: <Home />,
},
{
path: "/products",
element: (
<PrivateRoute>
<Marketplace />
</PrivateRoute>
),
},
{
path: "/add-product",
element: (
<PrivateRoute>
<AddProduct />
</PrivateRoute>
),
},
{
path: "/edit-product/:id",
element: (
<PrivateRoute>
<EditProduct />
</PrivateRoute>
),
},
{
path: "/products/:id",
element: (
<PrivateRoute>
<IndividualProductPage />
</PrivateRoute>
),
},
{
path: "/saved-products",
element: (
<PrivateRoute>
<SavedProducts />
</PrivateRoute>
),
},
{
path: "*",
element: <PageNotFound />,
},
],
},
]);

Expand All @@ -69,11 +74,9 @@ export default function App() {
<HelmetProvider>
<FirebaseProvider>
<div className="flex flex-col min-h-screen">
<Navbar />
<div className="flex-grow">
<RouterProvider router={router} />
</div>
<Footer />
</div>
</FirebaseProvider>
</HelmetProvider>
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/components/Header.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export function Header() {
return (
<>
<div className="bg-[url('./ucsd-pricecenter.png')] bg-[center_25%] bg-cover w-full h-[60%] shadow-[8px_8px_0px_rgba(246,174,45,0.6)] fixed -z-50"/>
<div className="h-[50vh]"/>
</>
);
}
146 changes: 126 additions & 20 deletions frontend/src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -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<HTMLDivElement> {
open: boolean;
onSubmit: React.FormEventHandler;
}

const MiniSearchbar = forwardRef<HTMLFormElement, MiniSearchbarProps>(
({ open, onSubmit, ...props }, ref) => {
return (
<div
className={`absolute mt-8 -translate-x-1/2 left-1/2 w-[40vh] p-4 bg-white shadow-ucsd-blue shadow-md
flex flex-col gap-4
rounded-lg transform transition-all duration-150 ease-out
${open ? "opacity-100 " : "opacity-0 pointer-events-none"}
`}
{...props}
>
<p className="text-md">Search the marketplace</p>
<form onSubmit={onSubmit} ref={ref}>
<div className={`flex flex-row w-full rounded-full border-2 p-2 text-[16px]`}>
<div className="items-center px-2 pointer-events-none">
<FontAwesomeIcon
icon={faMagnifyingGlass}
aria-label="faMagnifyingGlass"
className="text-gray-400"
/>
</div>
<input
name="query"
className={`w-full focus:outline-none text-[16px]`}
placeholder="Search UCSD"
/>
</div>
</form>
</div>
);
},
);

MiniSearchbar.displayName = "MiniSearchbar";

export function Navbar() {
const { user, signOutFromFirebase, openGoogleAuthentication } = useContext(FirebaseContext);
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isSearchBarOpen, setSearchbarOpen] = useState<boolean>(false);
const menuRef = useRef<HTMLUListElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
const searchRef = useRef<HTMLFormElement>(null);
const navigate = useNavigate();

const toggleMobileMenu = () => setMobileMenuOpen((o) => !o);

const handleSearch = (e: React.FormEvent<HTMLFormElement>) => {
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 (
Expand All @@ -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);
Expand All @@ -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 (
<>
<nav className="bg-white border-b border-gray-100 w-full h-12 px-6 flex items-center justify-between sticky top-0 z-50 shadow-sm">

{/* ── Brand ── */}
<nav className="font-rubik shadow-md shadow-ucsd-blue bg-white c left-0 right-0 min-w-0 mx-12 rounded-b-lg h-12 max-h-12 px-6 py-10 flex items-center justify-between fixed top-0 z-50">
{/* Desktop View */}
<button
className="font-jetbrains font-bold text-lg shrink-0"
className="text-lg font-semibold"
onClick={() => (window.location.href = "/products")}
>
<span className="text-ucsd-blue">Low </span>
<span className="text-ucsd-gold">Price Center</span>
<span className="text-3xl text-ucsd-blue">Low </span>
<span className="text-3xl text-ucsd-gold">Price Center</span>
</button>

{/* ── Desktop centre nav links ── */}
<ul className="hidden md:flex items-center gap-8 font-inter text-sm font-medium text-gray-600">
<li>
<div
className={`hidden ${!user && "opacity-0"} md:flex flex-row gap-3 items-center justify-center`}
>
{[
{ label: "Shop", path: "/products" },
{ label: "Sell", path: "/sell" },
{ label: "Student Organizations", path: "/organizations" },
].map((val) => (
<button
onClick={() => (window.location.href = "/products")}
className="text-ucsd-blue font-semibold border-b-2 border-ucsd-blue pb-0.5 transition-colors"
key={val.label}
className={`${window.location.pathname === val.path ? selectedTabStyling : tabStyling}`}
onClick={() => {
window.location.href = val.path;
}}
>
Shop
{val.label}
</button>
</li>
<li>
))}
</div>
<div className="hidden md:flex items-center text-2xl space-x-4">
<button
onClick={() => (window.location.href = "/saved-products")}
className={`${!user && "opacity-0"} w-12 h-12 text-xl flex items-center justify-center border-2 hover:bg-gray-300 rounded-full transition-colors`}
>
<FontAwesomeIcon icon={faHeart} aria-label="Heart Icon" />
</button>

<div className="relative">
<button
onClick={() => {
setSearchbarOpen(true);
}}
className={`${!user && "opacity-0"} w-12 h-12 text-xl flex items-center justify-center border-2 hover:bg-gray-300 rounded-full transition-colors`}
>
<FontAwesomeIcon icon={faMagnifyingGlass} aria-label="faMagnifyingGlass" />
</button>
<MiniSearchbar open={isSearchBarOpen} ref={searchRef} onSubmit={handleSearch} />
</div>

<button
onClick={() => (window.location.href = "/products")}
className={`${!user && "opacity-0"} w-12 h-12 text-xl flex items-center justify-center border-2 hover:bg-gray-300 rounded-full transition-colors`}
>
<FontAwesomeIcon icon={faCartShopping} aria-label="Shopping Cart" />
</button>

<button
onClick={user ? signOutFromFirebase : openGoogleAuthentication}
className="px-4 py-1 bg-transparent border-transparent rounded transition-colors"
>
{user ? "Log Out" : "Log In"}
</button>
</div>
onClick={() => (window.location.href = "/add-product")}
className="hover:text-ucsd-blue transition-colors"
>
Expand Down
13 changes: 13 additions & 0 deletions frontend/src/components/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Outlet } from "react-router-dom";
import { Navbar } from "src/components/Navbar";

export const RootLayout = () => {
return (
<>
<Navbar />
<div className="flex-grow">
<Outlet />
</div>
</>
);
};
11 changes: 9 additions & 2 deletions frontend/src/components/SearchBar.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,25 @@
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;
setError: (error: string) => void;
}

export default function SearchBar({ setProducts, setError }: Props) {
const [query, setQuery] = useState<string | null>(null);
const [searchParams] = useSearchParams();
const [query, setQuery] = useState<string | null>(searchParams.get("query") || "");

const handleChange = (value: string) => {
setQuery(value);
setProducts(value);
};

useEffect(() => {
setQuery(searchParams.get("query"));
}, [searchParams]);

return (
<input
type="text"
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -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;
Loading