diff --git a/src/pages/compoundbay/CompoundBay.module.scss b/src/pages/compoundbay/CompoundBay.module.scss index 5cdd61b4..ebeca548 100644 --- a/src/pages/compoundbay/CompoundBay.module.scss +++ b/src/pages/compoundbay/CompoundBay.module.scss @@ -4,36 +4,39 @@ display: flex; flex-direction: column; height: 100vh; - width: 100%; -} - -.banner { - text-align: center; - padding: 20px; - background-color: #333; - color: white; + overflow: hidden; + background-color: #1e1e1e; } .searchBarContainer { display: flex; justify-content: center; - padding: 20px; - background-color: #444; + align-items: center; + padding: 15px; + gap: 10px; } .searchBar { - width: 300px; + width: 70%; + max-width: 600px; padding: 10px; font-size: 16px; - border: none; + border: 1px solid #ccc; + border-radius: 4px; } -.searchButton { +.searchButton, +.mobileFilterButton { padding: 10px; background-color: #007bff; color: white; border: none; cursor: pointer; + border-radius: 4px; +} + +.mobileFilterButton { + display: none; } .content { @@ -50,6 +53,7 @@ overflow-y: auto; height: 100%; color: #333; + transition: transform 0.3s ease-in-out; } // Styles for ResultsSidebar @@ -60,3 +64,52 @@ height: 100%; background-color: white; } + +@media (max-width: 1024px) { + .filterSidebar { + width: 200px; + } +} + +@media (max-width: 768px) { + .searchBar { + width: calc(100% - 80px); + } + + .mobileFilterButton { + display: block; + } + + .content { + flex-direction: column; + } + + .filterSidebar { + position: fixed; + top: 0; + left: 0; + width: 80%; + max-width: 300px; + height: 100%; + z-index: 1000; + transform: translateX(-100%); + } + + .filterSidebar.visible { + transform: translateX(0); + } + + .results { + width: 100%; + } +} + +@media (max-width: 480px) { + .searchBarContainer { + padding: 10px; + } + + .searchBar { + font-size: 14px; + } +} diff --git a/src/pages/compoundbay/CompoundBay.tsx b/src/pages/compoundbay/CompoundBay.tsx index 1dd88a21..98a49eb1 100644 --- a/src/pages/compoundbay/CompoundBay.tsx +++ b/src/pages/compoundbay/CompoundBay.tsx @@ -1,7 +1,7 @@ import { observer } from "mobx-react-lite"; import Papa from "papaparse"; -import { useState, useEffect } from "react"; -import { FaSearch } from "react-icons/fa"; +import { useState, useEffect, useMemo } from "react"; +import { FaSearch, FaFilter } from "react-icons/fa"; import styles from "./CompoundBay.module.scss"; @@ -9,6 +9,9 @@ import { GroupBuySale } from "../../types/groupBuySale"; import FilterSidebar from "./FilterSidebar"; import ResultsSidebar from "./ResultsSidebar"; +const CSV_URL = + "https://docs.google.com/spreadsheets/d/e/2PACX-1vQYEkd82Pu4ukikZXz-APjdT2LYpMP2htYYbD_zJHq0bCshqFIWKF9vOJFMrPxMb_wuODyadyTETly1/pub?gid=1445611808&single=true&output=csv"; + const CompoundBay = observer(() => { const [groupBuySales, setGroupBuySales] = useState([]); const [visibleGroupBuySales, setVisibleGroupBuySales] = useState< @@ -16,7 +19,7 @@ const CompoundBay = observer(() => { >([]); const [searchTerm, setSearchTerm] = useState(""); const [loadCount, setLoadCount] = useState(10); - const [expandedSales, setExpandedSales] = useState>(new Set()); + //const [expandedSales, setExpandedSales] = useState>(new Set()); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); @@ -31,117 +34,82 @@ const CompoundBay = observer(() => { const [countryOptions, setCountryOptions] = useState([]); const [compoundOptions, setCompoundOptions] = useState([]); + const [isMobileFilterVisible, setIsMobileFilterVisible] = useState(false); + + const fetchGroupBuySales = async () => { + setIsLoading(true); + try { + const response = await fetch(CSV_URL); + const csvText = await response.text(); + + Papa.parse(csvText, { + header: true, + complete: (results) => { + const sales = results.data as GroupBuySale[]; + setGroupBuySales(sales); + setVisibleGroupBuySales(sales.slice(0, loadCount)); + + // Extract unique options for dropdowns + const vendors = [ + ...new Set(sales.map((sale) => sale.Vendor)), + ]; + const countries = [ + ...new Set( + sales.map((sale) => sale["Ships from Country"]), + ), + ]; + const compounds = [ + ...new Set(sales.map((sale) => sale.Compound)), + ]; + + setVendorOptions(vendors); + setCountryOptions(countries); + setCompoundOptions(compounds); + }, + error: (error) => { + console.error("Error parsing CSV:", error); + setError("Failed to parse group buy sales data."); + }, + }); + } catch (err) { + console.error("Error fetching data:", err); + setError("Failed to fetch group buy sales data."); + } finally { + setIsLoading(false); + } + }; + useEffect(() => { - const fetchGroupBuySales = async () => { - setIsLoading(true); - try { - const response = await fetch("/data/group_buy_sales.csv"); - const csvText = await response.text(); - Papa.parse(csvText, { - header: true, - complete: (results) => { - const parsedSales = results.data.map((row: any) => ({ - vendor: row.Vendor, - vendorRating: row["Vendor rating"], - type: row.Type, - compound: row.Compound, - dose: row.Dose, - unit: row.Unit, - format: row.Format, - quantity: row.Quantity, - price: row.Price, - shipsFromCountry: row["Ships from Country"], - shippingCost: row["Shipping $"], - moq: row.MOQ, - analysis: row.Analysis, - purityGuarantee: row["Purity guarantee"], - massGuarantee: row["Mass guarantee"], - reshipGuarantee: row["Re-ship guarantee"], - start: row.Start, - close: row.Close, - pepChatLink: row["PepChat Link"], - discordLink: row["Discord Link"], - telegramLink: row["Telegram Link"], - notes: row.Notes, - })) as GroupBuySale[]; - - console.log("Parsed sales:", parsedSales); // Debug log - - // Check for missing properties - const validSales = parsedSales.filter((sale) => { - if ( - !sale.vendor || - !sale.compound || - !sale.shipsFromCountry - ) { - console.warn("Invalid sale object:", sale); - return false; - } - return true; - }); - - setGroupBuySales(validSales); - setVisibleGroupBuySales(validSales.slice(0, loadCount)); - - // Extract unique options for dropdowns - const vendors = [ - ...new Set(validSales.map((sale) => sale.vendor)), - ]; - const countries = [ - ...new Set( - validSales.map((sale) => sale.shipsFromCountry), - ), - ]; - const compounds = [ - ...new Set(validSales.map((sale) => sale.compound)), - ]; - - setVendorOptions(vendors); - setCountryOptions(countries); - setCompoundOptions(compounds); - }, - error: (err) => { - console.error("Error parsing CSV:", err); - setError("Failed to load group buy sales data."); - }, - }); - } catch (err) { - console.error("Error fetching CSV:", err); - setError("Failed to fetch group buy sales data."); - } finally { - setIsLoading(false); - } - }; - fetchGroupBuySales(); + const intervalId = setInterval(fetchGroupBuySales, 15 * 60 * 1000); // Refresh every 15 minutes + + return () => clearInterval(intervalId); }, []); - useEffect(() => { - const filteredSales = groupBuySales.filter((sale) => { + const filteredSales = useMemo(() => { + return groupBuySales.filter((sale) => { return ( - (vendorRating === "" || sale.vendorRating === vendorRating) && + (vendorRating === "" || + sale["Vendor rating"] === vendorRating) && (vendor === "" || - sale.vendor.toLowerCase().includes(vendor.toLowerCase())) && + sale.Vendor.toLowerCase().includes(vendor.toLowerCase())) && (shipsFromCountry === "" || - sale.shipsFromCountry + sale["Ships from Country"] .toLowerCase() .includes(shipsFromCountry.toLowerCase())) && (compound === "" || - sale.compound - .toLowerCase() - .includes(compound.toLowerCase())) && + sale.Compound.toLowerCase().includes( + compound.toLowerCase(), + )) && (searchTerm === "" || - sale.vendor - .toLowerCase() - .includes(searchTerm.toLowerCase()) || - sale.compound - .toLowerCase() - .includes(searchTerm.toLowerCase())) + sale.Vendor.toLowerCase().includes( + searchTerm.toLowerCase(), + ) || + sale.Compound.toLowerCase().includes( + searchTerm.toLowerCase(), + )) ); }); - - console.log("Filtered sales:", filteredSales); // Debug log - setVisibleGroupBuySales(filteredSales.slice(0, loadCount)); }, [ groupBuySales, vendorRating, @@ -149,9 +117,30 @@ const CompoundBay = observer(() => { shipsFromCountry, compound, searchTerm, - loadCount, ]); + const dynamicVendorOptions = useMemo(() => { + return [...new Set(filteredSales.map((sale) => sale.Vendor))]; + }, [filteredSales]); + + const dynamicCountryOptions = useMemo(() => { + return [ + ...new Set(filteredSales.map((sale) => sale["Ships from Country"])), + ]; + }, [filteredSales]); + + const dynamicCompoundOptions = useMemo(() => { + return [...new Set(filteredSales.map((sale) => sale.Compound))]; + }, [filteredSales]); + + const dynamicVendorRatingOptions = useMemo(() => { + return [...new Set(filteredSales.map((sale) => sale["Vendor rating"]))]; + }, [filteredSales]); + + useEffect(() => { + setVisibleGroupBuySales(filteredSales.slice(0, loadCount)); + }, [filteredSales, loadCount]); + const handleSearch = () => { setLoadCount(10); }; @@ -163,18 +152,6 @@ const CompoundBay = observer(() => { } }; - const toggleExpand = (id: string) => { - setExpandedSales((prevExpanded) => { - const newExpanded = new Set(prevExpanded); - if (newExpanded.has(id)) { - newExpanded.delete(id); - } else { - newExpanded.add(id); - } - return newExpanded; - }); - }; - const clearFilters = () => { setVendorRating(""); setVendor(""); @@ -184,9 +161,12 @@ const CompoundBay = observer(() => { setLoadCount(10); }; + const toggleMobileFilter = () => { + setIsMobileFilterVisible(!isMobileFilterVisible); + }; + return (
-

CompoundBay

{ +
{ handleShipsFromCountryChange={setShipsFromCountry} handleCompoundChange={setCompound} clearFilters={clearFilters} - vendorOptions={vendorOptions} - countryOptions={countryOptions} - compoundOptions={compoundOptions} + vendorOptions={dynamicVendorOptions} + countryOptions={dynamicCountryOptions} + compoundOptions={dynamicCompoundOptions} + vendorRatingOptions={dynamicVendorRatingOptions} + isMobileFilterVisible={isMobileFilterVisible} + toggleMobileFilter={toggleMobileFilter} />
diff --git a/src/pages/compoundbay/FilterSidebar.module.scss b/src/pages/compoundbay/FilterSidebar.module.scss index 69d98343..35d8111f 100644 --- a/src/pages/compoundbay/FilterSidebar.module.scss +++ b/src/pages/compoundbay/FilterSidebar.module.scss @@ -6,12 +6,15 @@ } .filterSidebar { - width: 250px; + width: 300px; + min-width: 300px; padding: 20px; background-color: #f8f9fa; border-right: 1px solid #ddd; overflow-y: auto; color: #333; + height: 100%; + transition: transform 0.3s ease-in-out; } .filterSection { @@ -19,6 +22,7 @@ h3 { margin-bottom: 10px; + font-size: 18px; } label { @@ -28,7 +32,10 @@ select { width: 100%; - padding: 5px; + padding: 8px; + font-size: 16px; + border: 1px solid #ddd; + border-radius: 4px; } } @@ -38,10 +45,66 @@ background-color: #007bff; color: white; border: none; + border-radius: 4px; cursor: pointer; margin-top: 20px; + font-size: 16px; &:hover { background-color: #0056b3; } } + +.closeMobileFilter { + display: none; +} + +@media (max-width: 768px) { + .filterSidebar { + position: fixed; + top: 0; + left: 0; + width: 80%; + max-width: 300px; + height: 100%; + z-index: 1000; + transform: translateX(-100%); + box-shadow: 2px 0 5px rgba(0, 0, 0, 0.1); + } + + .visible { + transform: translateX(0); + } + + .closeMobileFilter { + display: block; + width: 100%; + padding: 10px; + background-color: #f0f0f0; + border: none; + border-bottom: 1px solid #ddd; + font-size: 16px; + text-align: right; + cursor: pointer; + } +} + +@media (max-width: 480px) { + .filterSidebar { + width: 90%; + } + + .filterSection { + h3 { + font-size: 16px; + } + + select { + font-size: 14px; + } + } + + .clearFilters { + font-size: 14px; + } +} diff --git a/src/pages/compoundbay/FilterSidebar.tsx b/src/pages/compoundbay/FilterSidebar.tsx index 1749d0b1..c9026191 100644 --- a/src/pages/compoundbay/FilterSidebar.tsx +++ b/src/pages/compoundbay/FilterSidebar.tsx @@ -15,6 +15,9 @@ interface FilterSidebarProps { vendorOptions: string[]; countryOptions: string[]; compoundOptions: string[]; + vendorRatingOptions: string[]; + isMobileFilterVisible: boolean; + toggleMobileFilter: () => void; } const FilterSidebar: React.FC = ({ @@ -30,9 +33,20 @@ const FilterSidebar: React.FC = ({ vendorOptions, countryOptions, compoundOptions, + vendorRatingOptions, + isMobileFilterVisible, + toggleMobileFilter, }) => { return ( -
+
+

Filters

@@ -41,9 +55,11 @@ const FilterSidebar: React.FC = ({ value={vendorRating} onChange={(e) => handleVendorRatingChange(e.target.value)}> - - - + {vendorRatingOptions.map((rating) => ( + + ))}
diff --git a/src/pages/compoundbay/ResultsSidebar.module.scss b/src/pages/compoundbay/ResultsSidebar.module.scss index 633c30f5..242e68ec 100644 --- a/src/pages/compoundbay/ResultsSidebar.module.scss +++ b/src/pages/compoundbay/ResultsSidebar.module.scss @@ -53,7 +53,8 @@ .links { margin-top: 15px; display: flex; - justify-content: space-between; + flex-wrap: wrap; + gap: 10px; } .linkItem { @@ -73,3 +74,55 @@ a { text-decoration: underline; } } + +@media (max-width: 768px) { + .results { + padding: 15px; + } + + .saleItem { + padding: 12px; + } + + .saleTitle { + font-size: 16px; + } + + .detailsGrid { + grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); + } + + .detailItem, + .notes, + .linkItem { + font-size: 13px; + } +} + +@media (max-width: 480px) { + .results { + padding: 10px; + } + + .saleItem { + padding: 10px; + } + + .saleTitle { + font-size: 15px; + } + + .detailsGrid { + grid-template-columns: 1fr; + } + + .detailItem, + .notes, + .linkItem { + font-size: 12px; + } + + .links { + flex-direction: column; + } +} diff --git a/src/pages/compoundbay/ResultsSidebar.tsx b/src/pages/compoundbay/ResultsSidebar.tsx new file mode 100644 index 00000000..7dd80d7f --- /dev/null +++ b/src/pages/compoundbay/ResultsSidebar.tsx @@ -0,0 +1,131 @@ +import React from "react"; + +import styles from "./ResultsSidebar.module.scss"; + +import { GroupBuySale } from "../../types/groupBuySale"; + +interface ResultsSidebarProps { + visibleGroupBuySales: GroupBuySale[]; + handleScroll: (e: React.UIEvent) => void; +} + +const ResultsSidebar: React.FC = ({ + visibleGroupBuySales, + handleScroll, +}) => { + const renderLink = (link: string, text: string) => { + if ( + link && + link.toLowerCase() !== "invite only" && + link.toLowerCase() !== "n/a" + ) { + return ( + + {text} + + ); + } + return link || "N/A"; + }; + + const renderDetailItem = (label: string, value: string) => ( +
+ {label}: {value} +
+ ); + + return ( +
+ {visibleGroupBuySales.length > 0 ? ( + visibleGroupBuySales.map((sale, index) => ( +
+

+ {sale.Vendor} - {sale.Compound} +

+

Price: {sale.Price}

+
+
+ {renderDetailItem("Type", sale.Type)} + {renderDetailItem( + "Dose", + `${sale.Dose} ${sale.Unit}`, + )} + {renderDetailItem("Format", sale.Format)} + {renderDetailItem("Quantity", sale.Quantity)} + {renderDetailItem( + "Ships from", + sale["Ships from Country"], + )} + {renderDetailItem( + "Shipping Cost", + sale["Shipping $"], + )} + {renderDetailItem("MOQ", sale.MOQ)} + {renderDetailItem( + "Vendor Rating", + sale["Vendor rating"], + )} + {renderDetailItem("Analysis", sale.Analysis)} + {renderDetailItem( + "Purity Guarantee", + sale["Purity guarantee"], + )} + {renderDetailItem( + "Mass Guarantee", + sale["Mass guarantee"], + )} + {renderDetailItem( + "Reship Guarantee", + sale["Re-ship guarantee"], + )} + {renderDetailItem("Start", sale.Start)} + {renderDetailItem("Close", sale.Close)} +
+
+ + Notes: + {" "} + {sale.Notes} +
+
+
+ + PepChat: + {" "} + {renderLink( + sale["PepChat Link"], + "PepChat", + )} +
+
+ + Discord: + {" "} + {renderLink( + sale["Discord Link"], + "Discord", + )} +
+
+ + Telegram: + {" "} + {renderLink( + sale["Telegram Link"], + "Telegram", + )} +
+
+
+
+ )) + ) : ( +

+ No group buy sales available. +

+ )} +
+ ); +}; + +export default ResultsSidebar; diff --git a/src/types/groupBuySale.ts b/src/types/groupBuySale.ts new file mode 100644 index 00000000..181a987d --- /dev/null +++ b/src/types/groupBuySale.ts @@ -0,0 +1,24 @@ +export interface GroupBuySale { + Vendor: string; + "Vendor rating": string; + Type: string; + Compound: string; + Dose: string; + Unit: string; + Format: string; + Quantity: string; + Price: string; + "Ships from Country": string; + "Shipping $": string; + MOQ: string; + Analysis: string; + "Purity guarantee": string; + "Mass guarantee": string; + "Re-ship guarantee": string; + Start: string; + Close: string; + "PepChat Link": string; + "Discord Link": string; + "Telegram Link": string; + Notes: string; +}