diff --git a/frontend/bun.lockb b/frontend/bun.lockb index 3f20aeb..b2ec65a 100755 Binary files a/frontend/bun.lockb and b/frontend/bun.lockb differ diff --git a/frontend/public/card_view_icon_selected.svg b/frontend/public/card_view_icon_selected.svg new file mode 100644 index 0000000..b6db560 --- /dev/null +++ b/frontend/public/card_view_icon_selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/card_view_icon_unselected.svg b/frontend/public/card_view_icon_unselected.svg new file mode 100644 index 0000000..9b0abe2 --- /dev/null +++ b/frontend/public/card_view_icon_unselected.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/list_view_icon_selected.svg b/frontend/public/list_view_icon_selected.svg new file mode 100644 index 0000000..f467e81 --- /dev/null +++ b/frontend/public/list_view_icon_selected.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/public/list_view_icon_unselected.svg b/frontend/public/list_view_icon_unselected.svg new file mode 100644 index 0000000..b45c0e0 --- /dev/null +++ b/frontend/public/list_view_icon_unselected.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/src/components/FilterDropdown.tsx b/frontend/src/components/FilterDropdown.tsx index 7f131ee..b3c9f88 100644 --- a/frontend/src/components/FilterDropdown.tsx +++ b/frontend/src/components/FilterDropdown.tsx @@ -9,9 +9,6 @@ const AllFiltersContainer = styled.div` flex-direction: column; justify-content: flex-start; align-items: flex-start; - margin-left: 95px; - margin-right: 95px; - margin-top: 55px; gap: 16px; `; diff --git a/frontend/src/components/FilterPanel.tsx b/frontend/src/components/FilterPanel.tsx index 751333b..ef1a870 100644 --- a/frontend/src/components/FilterPanel.tsx +++ b/frontend/src/components/FilterPanel.tsx @@ -18,12 +18,9 @@ import { FiltersContext } from "@/pages/Home"; const PanelBackground = styled.div` min-width: 284px; + max-height: calc(100vh - 70px); background-color: #fff; - position: fixed; - top: 70px; - bottom: 0; - overflow-y: scroll; overflow-x: hidden; diff --git a/frontend/src/components/HousingLocatorTable.tsx b/frontend/src/components/HousingLocatorTable.tsx index 741eb3c..0eccf70 100644 --- a/frontend/src/components/HousingLocatorTable.tsx +++ b/frontend/src/components/HousingLocatorTable.tsx @@ -2,7 +2,7 @@ import { useContext, useEffect, useState } from "react"; import styled from "styled-components"; import { Button } from "./Button"; -import { TablePagination } from "./TablePagination"; +import { Pagination } from "./Pagination"; import { User, demoteUser } from "@/api/users"; import { DataContext } from "@/contexts/DataContext"; @@ -68,9 +68,19 @@ const HLRow = styled.div` margin-bottom: 20px; `; +const HLTableFooterWrapper = styled.div` + display: flex; + flex-direction: row; + margin-right: 24px; + margin-bottom: 20px; + justify-content: right; +`; + const HLTableFooter = styled.div` - padding-left: 85%; - margin: 1vh 0vw 3vh 0vw; + display: flex; + flex-direction: row; + + justify-content: space-between; `; const Overlay = styled.div` @@ -219,6 +229,15 @@ export const HousingLocatorTable = () => { > ))} + + + + + {popup && selectedUser && ( <> @@ -303,13 +322,6 @@ export const HousingLocatorTable = () => { )} - - - ); }; diff --git a/frontend/src/components/Pagination.tsx b/frontend/src/components/Pagination.tsx new file mode 100644 index 0000000..faef7b6 --- /dev/null +++ b/frontend/src/components/Pagination.tsx @@ -0,0 +1,216 @@ +import styled from "styled-components"; + +const NumPageButton = styled.button` + width: 25px; + height: 24px; + border-radius: 4px; + border: 1px solid #eeeeee; + opacity: 0px; + background: #f5f5f5; + align-content: center; + + &:hover { + cursor: pointer; + background: #f5f5f5; + } + + &.active { + background: #ec8537; + color: #ffffff; + border-color: #ec8537; + } +`; + +const ButtonWrapper = styled.div` + display: flex; + flex-direction: row; + //width: 341.87px; + height: 24px; + justify-content: space-between; + gap: 20px; + opacity: 0px; +`; + +const NavButton = styled(NumPageButton)``; + +const NavButtonIcon = styled.img` + width: 24px; + height: 24px; + flex-shrink: 0; + border-radius: 4px; + border: 0; +`; + +const EllipsesWrapper = styled.div` + width: 24px; + height: 24px; +`; + +type PaginationProps = { + totalPages: number; + currPage: number; + setPageNumber: (newPageNumber: number) => void; +}; + +const BUTTONS_PER_PAGE = 5; + +export const Pagination = (props: PaginationProps) => { + const pages = []; + + const handleClick = (increase: boolean): void => { + if (increase && props.currPage !== props.totalPages) { + props.setPageNumber(props.currPage + 1); + } + + if (!increase && props.currPage > 1) { + props.setPageNumber(props.currPage - 1); + } + }; + + //case 1: num pages < buttons per page -> show all + if (props.totalPages <= BUTTONS_PER_PAGE) { + for (let i = 1; i <= props.totalPages; i++) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } + } + //case 2: on page 1-3 -> show first 4 ... and last + else if (props.currPage <= BUTTONS_PER_PAGE - 2) { + for (let i = 1; i <= props.totalPages; i++) { + //add button for first three pages + if (i <= BUTTONS_PER_PAGE - 1) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } else if (i === props.totalPages) { + pages.push(...); + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } + } + } + //case 3: on page end - 4 + 1-> show first ... last 4 + else if (props.currPage > props.totalPages - BUTTONS_PER_PAGE + 2) { + //add button for first three pages + for (let i = 1; i <= props.totalPages; i++) { + if (i >= props.totalPages - BUTTONS_PER_PAGE + 2) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } else if (i === 1) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + pages.push(...); + } + } + } + //case 4: middle -> show first ... middle - 1, middle, middle + 1, ... last + else { + for (let i = 1; i <= props.totalPages; i++) { + if (i === 1) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + pages.push(...); + } else if (i === props.currPage - 1 || i === props.currPage || i === props.currPage + 1) { + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } else if (i === props.totalPages) { + pages.push(...); + pages.push( + { + props.setPageNumber(i); + }} + className={props.currPage === i ? "active" : ""} + > + {i} + , + ); + } + } + } + + return ( + + + { + handleClick(false); + }} + /> + + {pages} + + { + handleClick(true); + }} + /> + + + ); +}; diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index 05f429b..41a3ecb 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -1,7 +1,7 @@ import { useState } from "react"; import styled from "styled-components"; -import { TablePagination } from "./TablePagination"; +import { Pagination } from "./Pagination"; const TableContainer = styled.div` display: flex; @@ -43,6 +43,9 @@ const TableCell = styled.div` `; const TableFooter = styled.div` + display: flex; + flex-direction: row; + justify-content: right; padding-top: 20px; `; @@ -82,7 +85,7 @@ export const Table = (props: TableProps) => { ))} {/* TODO: Replace this with new pagination when it is implemented */} - ` - display: flex; - width: 164px; - height: 55px; - align-items: center; - justify-content: center; - border-radius: ${(props) => (props.selected ? "12px" : "12px 0px 0px 12px")}; - border: 1px solid ${(props) => (props.selected ? "rgba(162, 61, 4, 0.80)" : "#EEE")}; - background: ${(props) => (props.selected ? "#B64201" : "#EEE")}; - color: ${(props) => (props.selected ? "#EEE" : "#2E2E2E")}; - font-family: Poppins; - font-size: 14px; - font-style: normal; - font-weight: 600; - line-height: normal; - z-index: ${(props) => (props.selected ? 1 : 0)}; - cursor: pointer; -`; - -const ListingsButton = styled(PendingButton)` - border-radius: ${(props) => (props.selected ? "12px" : "0px 12px 12px 0px")}; - position: relative; - left: -10px; -`; - const AddListings = styled.div` display: flex; flex-direction: column; @@ -104,56 +51,24 @@ export const UnitCardGrid = ({ showPendingUnits = false, }: UnitCardGridProps) => { const navigate = useNavigate(); - const [pendingSelected, setPendingSelected] = useState(showPendingUnits); const dataContext = useContext(DataContext); return ( <> - - - {pendingSelected ? ( - Pending Approval - ) : ( - Available Properties - )} - {dataContext.currentUser?.isHousingLocator && ( - - { - setPendingSelected(true); - refreshUnits("pending"); - }} - selected={pendingSelected} - > - Pending Listings - - { - setPendingSelected(false); - refreshUnits("approved"); - }} - selected={!pendingSelected} - > - All Listings - - - )} - - - {units.length > 0 && - units.map((option, index) => ( - { - refreshUnits(pendingSelected ? "pending" : "approved"); - }} - key={index} - /> - ))} - {units.length === 0 && No matching units found} - - + + {units.length > 0 && + units.map((option, index) => ( + { + refreshUnits(showPendingUnits ? "pending" : "approved"); + }} + key={index} + /> + ))} + {units.length === 0 && No matching units found} + {dataContext.currentUser?.isHousingLocator && ( { diff --git a/frontend/src/components/UnitList.tsx b/frontend/src/components/UnitList.tsx new file mode 100644 index 0000000..b3a7e51 --- /dev/null +++ b/frontend/src/components/UnitList.tsx @@ -0,0 +1,199 @@ +import { useContext, useState } from "react"; +import { Link, useLocation } from "react-router-dom"; +import styled from "styled-components"; + +import { Pagination } from "./Pagination"; + +import { Unit } from "@/api/units"; +import { FiltersContext } from "@/pages/Home"; + +const ENTRIES_PER_PAGE = 6; + +const UnitTable = styled.div` + display: flex; + flex-direction: column; + justify-content: center; + margin-bottom: 30px; + background-color: white; +`; + +const UnitTableHeaderRow = styled.div` + display: flex; + flex-direction: row; + margin-left: 44px; + margin-right: 44px; + margin-top: 32px; + + padding-bottom: 17px; + justify-content: space-between; + gap: 20px; //causes problems when shrinking, status overflows out of table + border-bottom: 0.4px solid #cdcacacc; +`; + +const UnitTableRow = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: center; + font-family: Montserrat; + color: black; + margin-left: 44px; + margin-right: 44px; + height: 100px; + + padding-bottom: 17px; + padding-top: 17px; + + border-bottom: 0.4px solid #cdcacacc; + gap: 20px; + &:hover { + background-color: #ec85371a; + border: 0.4px solid #b64201; + } +`; + +const UnitItemWrapper = styled.div` + display: flex; + flex-direction: column; + font-size: 16px; + font-family: "Montserrat"; + align-items: center; + width: 150px; +`; + +const ListingAddressWrapper = styled.div` + display: flex; + flex-direction: column; + font-size: 16px; + font-family: "Montserrat"; + width: 200px; + align-items: center; +`; + +const UnitHeaderItemWrapper = styled.div` + display: flex; + flex-direction: column; + font-family: "Poppins"; + font-size: 14px; + line-height: 21px; + color: #909090; + align-items: center; + width: 150px; +`; + +const UnitListFooterWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: right; + margin-left: 44px; + margin-right: 44px; + padding-top: 17px; + height: 90px; +`; + +const UnitListFooter = styled.div` + display: flex; + flex-direction: row; + + justify-content: space-between; +`; + +const AvailableWrapper = styled.div` + display: flex; + flex-direction: row; + width: 85.45px; + height: 29px; + padding: 4px 12px 4px 12px; + gap: 10px; + border-radius: 4px; + background: #7a923a33; + border: 1px solid #7a923a; + color: #7a923a; + font-family: Poppins; + font-size: 14px; + font-weight: 500; + line-height: 21px; + letter-spacing: -0.01em; + justify-content: center; +`; + +const PendingWrapper = styled(AvailableWrapper)` + background: #b6420133; + border: 1px solid #b64201; + color: #b64201; +`; + +const UnitNotFoundWrapper = styled.div` + margin-top: 40px; + align-self: center; +`; + +export type UnitListProps = { + units: Unit[]; +}; + +export const UnitList = ({ units }: UnitListProps) => { + const { pathname } = useLocation(); + const { filters } = useContext(FiltersContext); + const [pageNumber, setPageNumber] = useState(1); + + return ( + + + Listing Address + Price + Beds + Baths + Sqft + Status + + <> + {units.length > 0 && + units + .slice((pageNumber - 1) * ENTRIES_PER_PAGE, pageNumber * ENTRIES_PER_PAGE) + .map((unit: Unit, index) => ( + + + {unit.listingAddress} + ${unit.monthlyRent} + {unit.numBeds} + {unit.numBaths} + {unit.sqft} + + {" "} + {unit.availableNow && unit.approved ? ( + + Available + + ) : !unit.approved ? ( + + Pending + + ) : unit.leasedStatus !== undefined ? ( + Leased + ) : ( + Not Available + )} + + + + ))} + {units.length === 0 && No matching units found} + + + + + + + + ); +}; diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 131f69d..fcaf3bd 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from "react"; +import React, { useContext, useEffect, useState } from "react"; import { Helmet } from "react-helmet-async"; import { useLocation } from "react-router-dom"; import styled from "styled-components"; @@ -9,6 +9,56 @@ import { FilterPanel } from "@/components/FilterPanel"; import { NavBar } from "@/components/NavBar"; import { Page } from "@/components/Page"; import { UnitCardGrid } from "@/components/UnitCardGrid"; +import { UnitList } from "@/components/UnitList"; +import { DataContext } from "@/contexts/DataContext"; + +const ButtonsWrapper = styled.div` + display: flex; + justify-content: end; +`; + +const HeaderText = styled.span` + font-family: "Neutraface Text"; + font-size: 32px; +`; + +const ToggleButtonWrapper = styled.div` + display: flex; + padding: 0; + width: 195px; + height: 140px; + gap: 0px; + border-radius: 100px 0px 0px 0px; + opacity: 0px; +`; + +const CardViewButton = styled.img<{ selected: boolean }>` + width: 100px; + height: 50px; + padding: 7px 33px 8px 32px; + gap: 8px; + border-radius: 100px 0px 0px 100px; + opacity: 0px; + background: ${(props) => (props.selected ? "#ec85371a" : "#EEEEEE")}; + + &:hover { + opacity: 0px; + background: #ec85371a; + cursor: pointer; + } +`; + +const ListViewButton = styled(CardViewButton)` + padding: 7px 32px 7px 32px; + border-radius: 0px 100px 100px 0px; + background: ${(props) => (props.selected ? "#ec85371a" : "#EEEEEE")}; +`; + +const SearchStateWrapper = styled.div` + display: flex; + flex-direction: row; + justify-content: space-between; +`; export type FilterContextType = { filters: FilterParams; @@ -17,6 +67,43 @@ export type FilterContextType = { export const FiltersContext = React.createContext({} as FilterContextType); +const PropertiesRow = styled.span` + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + color: black; + font-family: "Montserrat"; + font-size: 27px; + font-weight: 700; + margin-bottom: 30px; +`; + +const PendingButton = styled.div<{ selected: boolean }>` + display: flex; + width: 164px; + height: 55px; + align-items: center; + justify-content: center; + border-radius: ${(props) => (props.selected ? "12px" : "12px 0px 0px 12px")}; + border: 1px solid ${(props) => (props.selected ? "rgba(162, 61, 4, 0.80)" : "#EEE")}; + background: ${(props) => (props.selected ? "#B64201" : "#EEE")}; + color: ${(props) => (props.selected ? "#EEE" : "#2E2E2E")}; + font-family: Poppins; + font-size: 14px; + font-style: normal; + font-weight: 600; + line-height: normal; + z-index: ${(props) => (props.selected ? 1 : 0)}; + cursor: pointer; +`; + +const ListingsButton = styled(PendingButton)` + border-radius: ${(props) => (props.selected ? "12px" : "0px 12px 12px 0px")}; + position: relative; + left: -10px; +`; + const HomePageLayout = styled.div` display: flex; flex-direction: row; @@ -24,12 +111,14 @@ const HomePageLayout = styled.div` height: 100%; `; -const FilterPadding = styled.div` - min-width: 250px; - height: 100%; +const UnitContent = styled.div` + width: 100%; + max-height: calc(100vh - 70px); + overflow-y: scroll; + padding: 70px 60px; `; - export function Home() { + const dataContext = useContext(DataContext); const previousFilters = useLocation().state as FilterParams; const [units, setUnits] = useState([]); const [filters, setFilters] = useState( @@ -38,6 +127,7 @@ export function Home() { approved: "approved", }, ); + const [viewMode, setViewMode] = useState("card"); const fetchUnits = (filterParams: FilterParams) => { let query: GetUnitsParams = { @@ -81,10 +171,23 @@ export function Home() { .catch(console.error); }; + const refreshUnits = (approved: "pending" | "approved") => { + const newFilters = { ...filters, approved }; + setFilters(newFilters); + }; + useEffect(() => { fetchUnits(filters); }, [filters]); + const handleCardView = () => { + setViewMode("card"); + }; + + const handleListView = () => { + setViewMode("list"); + }; + return ( @@ -94,20 +197,72 @@ export function Home() { - -
- - { - const newFilters = { ...filters, approved }; - setFilters(newFilters); - }} - /> -
+ + + + + + + + + + + + {filters.approved === "pending" ? ( + Pending Approval + ) : ( + Available Properties + )} + {dataContext.currentUser?.isHousingLocator && ( + + { + refreshUnits("pending"); + }} + selected={filters.approved === "pending"} + > + Pending Listings + + { + refreshUnits("approved"); + }} + selected={filters.approved === "approved"} + > + All Listings + + + )} + + {viewMode === "card" ? ( + + ) : ( + + )} +