From f81f8010f441a87e92fd28b97cf3d133a7b74e4f Mon Sep 17 00:00:00 2001 From: jecheung9 Date: Mon, 8 Sep 2025 13:05:55 -0700 Subject: [PATCH] filter and sort dashboard --- src/components/FilterSidebar.tsx | 124 ++++++++++++++++ .../CategoriesDropdownSelect.tsx | 2 +- src/index.css | 135 +++++++++++++++++- src/pages/CreateNewItem.tsx | 4 +- src/pages/CreateNewStore.tsx | 4 +- src/pages/Dashboard.tsx | 117 ++++++++++++--- src/pages/Home.tsx | 4 +- src/pages/NotFound.tsx | 4 +- 8 files changed, 367 insertions(+), 27 deletions(-) create mode 100644 src/components/FilterSidebar.tsx diff --git a/src/components/FilterSidebar.tsx b/src/components/FilterSidebar.tsx new file mode 100644 index 0000000..71f9ae9 --- /dev/null +++ b/src/components/FilterSidebar.tsx @@ -0,0 +1,124 @@ +import React, { useEffect, useState } from "react"; +import { categoryOptions } from "../custom-dropdown-select/CategoriesDropdownSelect"; +import { generateClient } from 'aws-amplify/data'; +import type { Schema } from "../../amplify/data/resource"; + +const client = generateClient(); + +interface FilterSidebarProps { + selectedCategories: string[]; + onCategoryChange: (categories: string[]) => void; + selectedStores: string[]; + onStoreChange: (stores: string[]) => void; + minPrice: number | null; + maxPrice: number | null; + onMinPriceChange: (value: number | null) => void; + onMaxPriceChange: (value: number | null) => void; +} + +const FilterSidebar: React.FC = ({ + selectedCategories, + onCategoryChange, + selectedStores, + onStoreChange, + minPrice, + maxPrice, + onMinPriceChange, + onMaxPriceChange, +}) => { + const [storeOptions, setStoreOptions] = useState([]); + + const fetchStores = async () => { + try { + const { data: stores } = await client.models.Store.list(); + if (stores && stores.length > 0) { + setStoreOptions(stores.map(store => store.storeName)); + } + } catch (error) { + console.error("Error fetching stores:", error); + setStoreOptions([]); + } + }; + + useEffect(() => { + fetchStores(); + }, []); + + + const handleCategoryChange = (value: string) => { + const newSelected = selectedCategories.includes(value) + ? selectedCategories.filter(v => v !== value) + : [...selectedCategories, value]; + onCategoryChange(newSelected); + }; + + const handleStoreChange = (store: string) => { + const newSelected = selectedStores.includes(store) + ? selectedStores.filter(s => s !== store) + : [...selectedStores, store]; + onStoreChange(newSelected); + }; + + return ( +
+
+

Filter by Store

+ {storeOptions.map(store => ( +
+ +
+ ))} +
+ +
+

Filter by Price

+
+ + onMinPriceChange(e.target.value ? Number(e.target.value) : null) + } + /> + to + + onMaxPriceChange(e.target.value ? Number(e.target.value) : null) + } + /> +
+
+ +
+

Filter by Category

+ {categoryOptions.map(option => ( +
+ +
+ ))} +
+
+ ); +}; + +export default FilterSidebar; diff --git a/src/custom-dropdown-select/CategoriesDropdownSelect.tsx b/src/custom-dropdown-select/CategoriesDropdownSelect.tsx index 0c5e987..fd3dd3a 100644 --- a/src/custom-dropdown-select/CategoriesDropdownSelect.tsx +++ b/src/custom-dropdown-select/CategoriesDropdownSelect.tsx @@ -10,7 +10,7 @@ interface CategoriesDropdownSelectProps { value?: string; } -const categoryOptions = [ +export const categoryOptions = [ { label: "Meat", value: "Meat" }, { label: "Seafood", value: "Seafood" }, { label: "Fruits & Vegetables", value: "Fruits & Vegetables" }, diff --git a/src/index.css b/src/index.css index 64cb560..72c5cbc 100644 --- a/src/index.css +++ b/src/index.css @@ -155,6 +155,13 @@ h1 { left: 0; } +.page-title { + position: absolute; + left: 50%; + transform: translateX(-50%); + white-space: nowrap; +} + .nav-bar { display: flex; align-items: center; @@ -262,7 +269,7 @@ h1 { flex-direction: column; align-items: center; justify-content: center; - margin-top: 5em; + margin-top: 4em; } .page-text { @@ -413,9 +420,11 @@ h1 { .items-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 1rem; width: 90%; + justify-content: center; + margin: 0 auto; } .item-card { @@ -452,7 +461,8 @@ h1 { .item-category, .item-store { font-size: 1.1rem; font-weight: 600; - margin-bottom: 0.7rem; + margin-bottom: 0.6rem; + line-height: 1.2; } .item-price { @@ -530,8 +540,79 @@ h1 { text-align: center; } -@media (max-width: 1059.98px) { /* smaller desktop layout */ +.dashboard { + display: flex; + width: 100%; + padding: 0 1rem; + box-sizing: border-box; +} + +.price-filter { + display: flex; + gap: 0.7rem; + margin-top: 1rem; + align-items: center; +} + +.price-filter input { + max-width: 6.5rem; + font-size: 16px; + width: 100%; + box-sizing: border-box; + padding: 0.4em 0 0.4em 1em; + border-radius: 3rem; + border: 1px solid #ccc; + background-color: #2c2c2c; + color: #f0f0f0; + font-size: 1rem; +} + +.filter-sidebar { + width: max-content; + padding-right: 1rem; + white-space: nowrap; + padding-bottom: 1rem; +} + +.filter-sidebar h3 { + margin: 0; +} + +.filter-section { + margin-bottom: 1rem; +} + +.sidebar-toggle-button, .sidebar-close-button { + display: none; + margin-bottom: 0; +} +.sidebar-close-button { + margin-bottom: 1rem; +} + +.sidebar-results-counter { + margin-bottom: 1rem; + margin-top: 0; +} + +.page-header { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 0 1rem; + box-sizing: border-box; + height: 7rem; +} + +.sort-button { + height: 2.5rem; + padding: 0 1rem; + margin-left: auto; +} + +@media (max-width: 1059.98px) { /* smaller desktop layout */ .nav-bar { display: grid; grid-template-columns: auto 1fr; @@ -688,6 +769,12 @@ h1 { padding: 0 1em; } + .page-header { + height: 5.5rem; + align-items: center; + padding: 0; + } + /* navigation */ .nav-bar { display: flex; @@ -809,6 +896,46 @@ h1 { } /* footer good at 650px, so removed. if >3 footers, need formatting */ + + /* dashboard */ + + .filter-sidebar { + display: none; + position: fixed; + top: 0; + left: 0; + height: 100%; + width: 100%; + z-index: 1000; + background-color: #1a1a1a; + padding: 1rem; + overflow-y: auto; + overscroll-behavior: contain; + box-shadow: 2px 0 8px rgba(0,0,0,0.3); + font-size: 20px; + } + + .filter-sidebar.open { + display: block; + animation: fadeInMenu 0.15s ease forwards; + } + + .sidebar-toggle-button, .sidebar-close-button, .sort-button{ + display: block; + font-size: 1.25rem; + padding: 0.5rem 1rem; + height: 2.5rem; + } + + .sidebar-toggle-button, .sort-button { + width: 5.5rem; + padding: 0; + + } + + .filter-sidebar .filter-section:last-child { + margin-bottom: 3rem; + } } @media (max-width: 450px) { /* small mobile screens */ diff --git a/src/pages/CreateNewItem.tsx b/src/pages/CreateNewItem.tsx index 3d15557..a38268f 100644 --- a/src/pages/CreateNewItem.tsx +++ b/src/pages/CreateNewItem.tsx @@ -169,7 +169,9 @@ const CreateNewItem: React.FC = () => {
-

Create New Item

+
+

Create New Item

+
{
-

Create New Store

+
+

Create New Store

+
(); const Dashboard: React.FC = () => { const [dashboardItems, setDashboardItems] = useState([]); + const [selectedCategories, setSelectedCategories] = useState([]); + const [selectedStores, setSelectedStores] = useState([]); + const [minPrice, setMinPrice] = useState(null); + const [maxPrice, setMaxPrice] = useState(null); const [loading, setLoading] = useState(true); + const [isSidebarOpen, setIsSidebarOpen] = useState(false); + const [sortOption, setSortOption] = useState<"none" | "lowToHigh" | "highToLow">("none"); const fetchItems = async () => { try { @@ -34,27 +41,101 @@ const Dashboard: React.FC = () => { if (loading) return

Loading items...

; + + const filteredItems = dashboardItems.filter(item => { + const categoryMatch = + selectedCategories.length === 0 || selectedCategories.includes(item.category); + + const storesMatch = + selectedStores.length === 0 || selectedStores.includes(item.storeName); + + const price = item.isDiscount ? item.discountedPrice : item.itemPrice; + + const pricesMatch = + price !== undefined && + (minPrice === null || price >= minPrice) && + (maxPrice === null || price <= maxPrice); + + return categoryMatch && storesMatch && pricesMatch; + }); + + const handleSortToggle = () => { + setSortOption((prev) => { + if (prev === "none") return "lowToHigh"; + if (prev === "lowToHigh") return "highToLow"; + return "none"; + }); + }; + + let sortedItems = [...filteredItems]; + + if (sortOption === "lowToHigh") { + sortedItems.sort((a, b) => { + const priceA = a.isDiscount ? a.discountedPrice : a.itemPrice; + const priceB = b.isDiscount ? b.discountedPrice : b.itemPrice; + return (priceA ?? 0) - (priceB ?? 0); + }); + } else if (sortOption === "highToLow") { + sortedItems.sort((a, b) => { + const priceA = a.isDiscount ? a.discountedPrice : a.itemPrice; + const priceB = b.isDiscount ? b.discountedPrice : b.itemPrice; + return (priceB ?? 0) - (priceA ?? 0); + }); + } + + const toggleSidebar = () => setIsSidebarOpen(!isSidebarOpen); + return (
-

Dashboard

-
- {dashboardItems.length === 0 ? ( -

No items found.

- ) : ( - dashboardItems.map((item) => ( - - )) - )} +
+ +

Dashboard

+ +
+ +
+ {isSidebarOpen &&
} +
+

Showing {sortedItems.length} results

+ + +
+
+ {sortedItems.length === 0 ? ( +

No items found.

+ ) : ( + sortedItems.map((item) => ( + + )) + )} +
diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 1a9f53a..5a0377f 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -5,7 +5,9 @@ const Home: React.FC = () => { return (
-

Welcome to Price Wolves!

+
+

Welcome to Price Wolves!

+

Price Wolves aim to hunt stores with the lowest prices.

diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 10b4cf8..0d5b5ba 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -4,7 +4,9 @@ const NotFound: React.FC = () => { return (
-

Page Not Found | 404

+
+

Page Not Found | 404

+

Page Not Found