diff --git a/client/src/components/App/App.js b/client/src/components/App/App.js index 2c083444..de2f5cab 100644 --- a/client/src/components/App/App.js +++ b/client/src/components/App/App.js @@ -15,6 +15,7 @@ import Privacy from '@/pages/Privacy/Privacy'; import License from '@/pages/License/License'; import UserProfile from '@/pages/Userprofile/UserProfile'; import Contact from '@/pages/Contact/Contact'; +import Community from '@/pages/Community/Community'; import NotFound from '@/pages/NotFound/NotFound'; import './App.css'; @@ -64,6 +65,7 @@ function App() { } /> } /> } /> + } /> } /> } /> } /> diff --git a/client/src/components/Button/Button.js b/client/src/components/Button/Button.js new file mode 100644 index 00000000..a3374259 --- /dev/null +++ b/client/src/components/Button/Button.js @@ -0,0 +1,53 @@ +import React from 'react'; +import { Button } from '@mui/material'; + +import { styled } from '@mui/material/styles'; + +export const GreenButton = (props) => { + const Green = styled(Button)({ + backgroundColor: '#147d16', + borderRadius: '.3vw', + color: 'white', + height: '32px', + }); + + return ( + + {props.children} + + ); +}; + +export const WhiteButton = (props) => { + const White = styled(Button)({ + backgroundColor: 'white', + outline: '1px #147d16 solid', + borderRadius: '.3vw', + color: '#00000050', + height: '32px', + fontSize: '12px', + }); + + return ( + + {props.children} + + ); +}; + +export const GrayButton = (props) => { + const Gray = styled(Button)({ + backgroundColor: 'white', + borderRadius: '.3vw', + outline: '1px solid #00000050', + color: '#00000050', + height: '32px', + fontSize: '12px', + }); + + return ( + + {props.children} + + ); +}; diff --git a/client/src/components/Button/index.js b/client/src/components/Button/index.js new file mode 100644 index 00000000..8b166a86 --- /dev/null +++ b/client/src/components/Button/index.js @@ -0,0 +1 @@ +export * from './Button'; diff --git a/client/src/components/Header/Header.js b/client/src/components/Header/Header.js index 13cc03ca..89069cb7 100644 --- a/client/src/components/Header/Header.js +++ b/client/src/components/Header/Header.js @@ -68,6 +68,12 @@ const Header = () => { + + + + + + diff --git a/client/src/components/Icons/Link.js b/client/src/components/Icons/Link.js new file mode 100644 index 00000000..2d6c93b5 --- /dev/null +++ b/client/src/components/Icons/Link.js @@ -0,0 +1,11 @@ +import React from 'react'; + +import { Link, LinkOff } from '@mui/icons-material'; + +export const LinkIcon = (props) => ( + +); + +export const LinkOffIcon = (props) => ( + +); diff --git a/client/src/components/Icons/Search.js b/client/src/components/Icons/Search.js new file mode 100644 index 00000000..a29fef28 --- /dev/null +++ b/client/src/components/Icons/Search.js @@ -0,0 +1,6 @@ +import React from 'react'; +import SearchIcon from '@mui/icons-material/Search'; + +export const Search = (props) => ( + +); diff --git a/client/src/components/Icons/Sort.js b/client/src/components/Icons/Sort.js new file mode 100644 index 00000000..0861702d --- /dev/null +++ b/client/src/components/Icons/Sort.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { Remove, ExpandLess, ExpandMore } from '@mui/icons-material'; + +export const Neutral = (props) => ( + +); + +export const SortUp = (props) => ( + +); + +export const SortDown = (props) => ( + +); diff --git a/client/src/components/Icons/Upload.js b/client/src/components/Icons/Upload.js new file mode 100644 index 00000000..9af5d3b5 --- /dev/null +++ b/client/src/components/Icons/Upload.js @@ -0,0 +1,6 @@ +import React from 'react'; +import { UploadFile } from '@mui/icons-material'; + +export const UploadFileIcon = (props) => ( + +); diff --git a/client/src/components/Icons/index.js b/client/src/components/Icons/index.js index f7c1ff2c..2eec4d27 100644 --- a/client/src/components/Icons/index.js +++ b/client/src/components/Icons/index.js @@ -1,3 +1,7 @@ export * from './Adopt'; export * from './Drop'; export * from './Like'; +export * from './Link' +export * from './Upload' +export * from './Search' +export * from './Sort' \ No newline at end of file diff --git a/client/src/components/SearchBar/SearchBar.js b/client/src/components/SearchBar/SearchBar.js new file mode 100644 index 00000000..e3963f79 --- /dev/null +++ b/client/src/components/SearchBar/SearchBar.js @@ -0,0 +1,31 @@ +import React from 'react'; + +import { Search } from '@/components/Icons'; + +import './SearchBar.scss'; +export const SearchBar = (props) => { + return ( +
+ + + +
+ ); +}; diff --git a/client/src/components/SearchBar/SearchBar.scss b/client/src/components/SearchBar/SearchBar.scss new file mode 100644 index 00000000..3ed7e360 --- /dev/null +++ b/client/src/components/SearchBar/SearchBar.scss @@ -0,0 +1,17 @@ +.searchbar{ + display: flex; + align-items: center; + border: 1px solid #00000050; + &__input{ + border: none; + width: 100%; + height: 100%; + } + &__input:focus{ + outline: none; + + } + &__input::-webkit-input-placeholder{ + color: '#00000050', + } +} \ No newline at end of file diff --git a/client/src/components/Section/Section.js b/client/src/components/Section/Section.js index 92ec690f..6caad30b 100644 --- a/client/src/components/Section/Section.js +++ b/client/src/components/Section/Section.js @@ -1,18 +1,9 @@ import React from 'react'; import { Box } from '@mui/material'; -export default function Section({ children, title }) { +export default function Section({ children, sx }) { return ( - -

{title}

+ {children} ); diff --git a/client/src/components/Table/TableRow.js b/client/src/components/Table/TableRow.js new file mode 100644 index 00000000..1a5363fa --- /dev/null +++ b/client/src/components/Table/TableRow.js @@ -0,0 +1,97 @@ +import React, { useState } from 'react'; + +import { Table, TableBody, TableRow, TableCell } from '@mui/material'; + +import { styled } from '@mui/material/styles'; + +export default function CommunityTableRow({ row }) { + const [hover, setHover] = useState(false); + + const StyledCell = styled(TableCell)({ + borderBottom: '1px #00000050 solid', + color: '#000000', + fontSize: '14px', + fontFamily: 'Montserrat', + padding: '0px', + }); + + const ShortCell = styled(TableCell)( + hover + ? { + fontWeight: 'bold', + width: '16.67%', + borderBottom: '1px #00000050 solid', + color: '#000000', + fontSize: '14px', + fontFamily: 'Montserrat', + padding: '0px', + } + : { + width: '16.67%', + borderBottom: '1px #00000050 solid', + color: '#000000', + fontSize: '14px', + fontFamily: 'Montserrat', + padding: '0px', + }, + ); + + const LongCell = styled(TableCell)( + hover + ? { + fontWeight: 'bold', + width: '25%', + borderBottom: '1px #00000050 solid', + color: '#000000', + fontSize: '14px', + fontFamily: 'Montserrat', + padding: '0px', + } + : { + width: '25%', + borderBottom: '1px #00000050 solid', + color: '#000000', + fontSize: '14px', + fontFamily: 'Montserrat', + padding: '0px', + }, + ); + + return ( + <> + + + setHover(true)} + onMouseLeave={() => setHover(false)} + > + {row.country} + {row.city} + {row.territory} + {row.service} + + + {row.organization} + + + + +
+ + ); +} diff --git a/client/src/pages/Community/Communities.js b/client/src/pages/Community/Communities.js new file mode 100644 index 00000000..6cd91009 --- /dev/null +++ b/client/src/pages/Community/Communities.js @@ -0,0 +1,428 @@ +import React, { useState, useEffect, useRef } from 'react'; +import { IconButton } from '@mui/material'; +import { + SortUp, + SortDown, + LinkIcon, + LinkOffIcon, + UploadFileIcon, +} from '@/components/Icons'; + +import { GrayButton } from '@/components/Button/Button'; +import PanelDrawer from '@/components/PanelDrawer/PanelDrawer'; +import CommunityTableRow from '@/components/Table/TableRow'; +import { SearchBar } from '@/components/SearchBar/SearchBar'; +import Section from '@/components/Section/Section'; + +import SideMenu from './SideMenu'; +import './Communities.scss'; + +export default function Communities() { + const [search, setSearch] = useState(''); + + const [open, setOpen] = useState(false); + const [state, setState] = useState({}); + const [Links, setLinks] = useState([]); + const [country, setCountry] = useState(true); + const [city, setCity] = useState(true); + const [territory, setTerritory] = useState(true); + const [service, setService] = useState(true); + const [organization, setOrganization] = useState(true); + const [hover, setHover] = useState(false); + + const filteredLinks = Links?.filter((link) => { + return ( + link.country.toLowerCase().includes(search.toLowerCase()) || + link.city.toLowerCase().includes(search.toLowerCase()) || + link.territory.toLowerCase().includes(search.toLowerCase()) || + link.service.toLowerCase().includes(search.toLowerCase()) + ); + }); + + useEffect(() => { + let links = [ + { + country: 'United States', + city: 'Oakland', + territory: 'California', + service: 'Tree Service', + organization: 'Oakland Tree Services', + link: 'https://www.oaklandca.gov/topics/tree-services', + }, + { + country: 'United States', + city: 'Berkeley', + territory: 'California', + service: 'Tree Service', + organization: 'Berkeley Tree Services', + link: 'https://berkeleyca.gov/city-services/streets-sidewalks-sewers-and-utilities/city-trees-and-coast-live-oak-ordinance#:~:text=To%20apply%20for%20a%20permit,or%20removal%20will%20be%20permitted', + }, + { + country: 'United States', + city: 'Alameda', + territory: 'California', + service: 'Tree Service', + organization: 'Alameda Tree Services', + link: 'https://www.alamedaca.gov/Departments/Public-Works-Department/Street-Trees', + }, + { + country: 'United States', + city: 'San Francisco', + territory: 'California', + service: 'Tree Service', + organization: 'San Francisco Tree Services', + link: 'https://sfpublicworks.org/remove-street-tree', + }, + { + country: 'United States', + city: 'Monterey', + territory: 'California', + service: 'Organization', + organization: 'City of Monterey', + link: 'https://monterey.org/city_hall/parks___recreation/beaches,_parks___playgrounds/trees___urban_forestry/local_tree___plant_selections.php', + }, + { + country: 'United States', + city: 'Oxnard', + territory: 'California', + service: 'Organization', + organization: 'City of Oxnard', + link: 'https://www.oxnard.org/environmental-resources/', + }, + ]; + setLinks( + [...links].sort( + (a, b) => + a.country.toLowerCase().localeCompare(b.country.toLowerCase()) || + a.city.toLowerCase().localeCompare(b.city.toLowerCase()) || + a.territory.toLowerCase().localeCompare(b.territory.toLowerCase()), + ), + ); + }, []); + + const sortCountryAsc = () => { + setLinks((state) => + [...state].sort((a, b) => { + return ( + a.country.toLowerCase().localeCompare(b.country.toLowerCase()) || + a.city.toLowerCase().localeCompare(b.city.toLowerCase()) || + a.territory.toLowerCase().localeCompare(b.territory.toLowerCase()) + ); + }), + ); + setCountry((state) => !state); + }; + + const sortCountryDesc = () => { + setLinks((state) => + [...state].sort((a, b) => { + return ( + b.country.toLowerCase().localeCompare(a.country.toLowerCase()) || + a.city.toLowerCase().localeCompare(b.city.toLowerCase()) || + a.territory.toLowerCase().localeCompare(b.territory.toLowerCase()) + ); + }), + ); + setCountry((state) => !state); + }; + + const sortCityAsc = () => { + setLinks((state) => + [...state].sort((a, b) => + a.city.toLowerCase().localeCompare(b.city.toLowerCase()), + ), + ); + setCity((state) => !state); + }; + + const sortCityDesc = () => { + setLinks((state) => + [...state].sort((a, b) => + b.city.toLowerCase().localeCompare(a.city.toLowerCase()), + ), + ); + setCity((state) => !state); + }; + + const sortTerritoryAsc = () => { + setLinks((state) => + [...state].sort((a, b) => + a.territory.toLowerCase().localeCompare(b.territory.toLowerCase()), + ), + ); + setTerritory((state) => !state); + }; + + const sortTerritoryDesc = () => { + setLinks((state) => + [...state].sort((a, b) => + b.territory.toLowerCase().localeCompare(a.territory.toLowerCase()), + ), + ); + setTerritory((state) => !state); + }; + + const sortServiceAsc = () => { + setLinks((state) => + [...state].sort((a, b) => { + return a.service.toLowerCase().localeCompare(b.service.toLowerCase()); + }), + ); + setService((state) => !state); + }; + + const sortServiceDesc = () => { + setLinks((state) => + [...state].sort((a, b) => { + return b.service.toLowerCase().localeCompare(a.service.toLowerCase()); + }), + ); + setService((state) => !state); + }; + + const sortOrganizationAsc = () => { + setLinks((state) => + [...state].sort((a, b) => + a.organization + .toLowerCase() + .localeCompare(b.organization.toLowerCase()), + ), + ); + setOrganization((state) => !state); + }; + + const sortOrganizationDesc = () => { + setLinks((state) => + [...state].sort((a, b) => + b.organization + .toLowerCase() + .localeCompare(a.organization.toLowerCase()), + ), + ); + setOrganization((state) => !state); + }; + + const handleSearch = (e) => { + setSearch(e.target.value); + }; + + const handleAddLink = async () => { + await setOpen(false); + const newState = {}; + newState.header = 'Add Link'; + newState.summary = + "Didn't find your organization and want to add it to our list? Input the required text and link, then submit it. A team member will review the request, and if it meets the requirements you will see the organization on the list ASAP."; + newState.inputs = [ + { label: 'Country', name: 'country', text: '' }, + { label: 'City', name: 'city', text: '' }, + { label: 'State', name: 'state', text: '' }, + { label: 'Organization Type', name: 'type', text: '' }, + { label: 'Link', name: 'link', text: '' }, + ]; + await setState(newState); + await setOpen(true); + }; + + const handleReportLink = async () => { + await setOpen(false); + const newState = {}; + newState.header = 'Report Broken Link'; + newState.summary = + "Clicked a link and it sent you to an error page? Or found yourself where the page doesn't exist anymore? Search for the broken link and set the link then add the new link to submit. A team member will review the request, and if it meets the requirements, you will see the updated link on the list ASAP."; + newState.inputs = [ + { label: 'Broken Link', name: 'broken', text: '' }, + { label: 'New Link', name: 'new', text: '' }, + ]; + newState.links = [ + { + organization: 'Oakland Tree Services', + link: 'https://www.oaklandca.gov/topics/tree-services', + }, + { + organization: 'Berkeley Tree Services', + link: 'https://berkeleyca.gov/city-services/streets-sidewalks-sewers-and-utilities/city-trees-and-coast-live-oak-ordinance#:~:text=To%20apply%20for%20a%20permit,or%20removal%20will%20be%20permitted', + }, + { + organization: 'Alameda Tree Services', + link: 'https://www.alamedaca.gov/Departments/Public-Works-Department/Street-Trees', + }, + { + organization: 'San Francisco Tree Services', + link: 'https://sfpublicworks.org/remove-street-tree', + }, + { + organization: 'City of Monterey', + link: 'https://monterey.org/city_hall/parks___recreation/beaches,_parks___playgrounds/trees___urban_forestry/local_tree___plant_selections.php', + }, + { + organization: 'City of Oxnard', + link: 'https://www.oxnard.org/environmental-resources/', + }, + ]; + await setState(newState); + await setOpen(true); + }; + + const ref = useClickOutside(() => { + setHover(false); + }); + + const handleClose = () => { + setOpen(false); + }; + + const exportXslx = (e) => { + setHover(false); + }; + + const exportDoc = (e) => { + setHover(false); + }; + + return ( +
+
+
+

Community Search

+
+
+ + In the community search section, you can find and view other city + government procedure to add,remove, or trim a tree or find nurseries + and tree organizations nearby. You can view and filter as much + information present such as Country, Service, etc. Viewers are able + to submit a link to a city tree service, nursery, tree organization + and a team member will review and approve the submission. + +
+
+ +
setHover(true)}> + + + Export + + {hover && ( +
+
+
+ Export as .xslx +
+
+ Export as .doc +
+
+
+ )} +
+ + + Add Link + + + + + Report Broken Link + +
+ {open && ( + + + + )} +
+
+ Country + + {country ? : } + +
+
+ City + + {city ? : } + +
+
+ State/Territory + + {territory ? : } + +
+
+ Service + + {service ? : } + +
+
+ Organization + + {organization ? : } + +
+
+ +
+ {filteredLinks?.map( + ({ country, city, territory, service, organization, link }, i) => ( + + ), + )} +
+
+
+
+
+ ); +} +// handle export close menu when clicking outside + +function useClickOutside(handler) { + const ref = useRef(); + useEffect(() => { + const onClose = (e) => { + if (!ref.current?.contains(e.target)) { + handler(); + } + }; + + document.addEventListener('mousedown', onClose); + + return () => { + document.removeEventListener('mousedown', onClose); + }; + }); + + return ref; +} diff --git a/client/src/pages/Community/Communities.scss b/client/src/pages/Community/Communities.scss new file mode 100644 index 00000000..3aba82a7 --- /dev/null +++ b/client/src/pages/Community/Communities.scss @@ -0,0 +1,48 @@ +.communities { + display: flex; + flex-direction: column; + width: 50%; + margin-bottom: 3.29vh; + + &__main { + &__p { + font-size: 1.2em; + } + + &__search { + display: flex; + justify-content: space-between; + margin-top: 16px; + margin-bottom: 32px; + + &__input { + width: 60%; + border: 1px #00000050 solid; + } + + &__hovermenu { + display: flex; + flex-direction: column; + justify-content: space-around; + min-width: 104px; + height: 72px; + font-family: Montserrat; + font-size: 14px; + padding: 6px; + outline: 1px solid #00000050; + color: #00000050; + background-color: white; + border-radius: .3vw; + } + } + + &__section { + display: flex; + border-bottom: 1px #00000050 solid; + + &__one { + width: 16.67%; + } + } + } +} \ No newline at end of file diff --git a/client/src/pages/Community/Community.js b/client/src/pages/Community/Community.js new file mode 100644 index 00000000..28ff56c2 --- /dev/null +++ b/client/src/pages/Community/Community.js @@ -0,0 +1,58 @@ +import React from 'react'; +import { Link } from 'react-router-dom'; + +import { Footer } from '@/components/Footer/Footer'; +import TreeImage from '@/assets/images/addtree/treefattrunk.png'; + +import Communities from './Communities'; +import './Community.scss'; + +export default function Community() { + return ( +
+
+ tree +
+
+

Community

+
+
+
+ + Want a tree planted in front of your house? Have branches that need + to be + + + trimmed? Is there a stump that needs to be removed? + + + Get in touch with your city tree services, most of the time it is + free of charge. + +
+ +
+

Have tree data you want share?

+
+
+ + We are always looking to add or update our data to be as current as + possible. To + + + upload tree data go to our{' '} + + source page + + . + +
+
+
+
+ ); +} diff --git a/client/src/pages/Community/Community.scss b/client/src/pages/Community/Community.scss new file mode 100644 index 00000000..ae844d3e --- /dev/null +++ b/client/src/pages/Community/Community.scss @@ -0,0 +1,57 @@ +.community { + display: flex; + flex-direction: column; + margin-top: 112px; + font-family: Montserrat; + + &__logo { + display: flex; + justify-content: center; + + &__image { + max-width: 2.5vw; + aspect-ratio: 1/1; + } + } + + &__header { + display: flex; + justify-content: center; + margin-top: 2.461vh; + } + + &__main { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + + &__header { + display: flex; + justify-content: center; + } + + &__p { + font-size: medium; + display: flex; + flex-direction: column; + align-items: center; + margin-bottom: 56px; + + &__text { + display: flex; + flex-direction: column; + margin-bottom: 2.461vh; + } + + &__link { + color: #3FAB45; + + &:hover { + color: #3FAB45; + } + } + } + } + +} \ No newline at end of file diff --git a/client/src/pages/Community/SideMenu.js b/client/src/pages/Community/SideMenu.js new file mode 100644 index 00000000..a9060af7 --- /dev/null +++ b/client/src/pages/Community/SideMenu.js @@ -0,0 +1,134 @@ +import React, { useState, useEffect, useRef } from 'react'; + +import { Form } from '@/components/Form'; +import { SearchBar } from '@/components/SearchBar/SearchBar'; +import { GreenButton, GrayButton } from '@/components/Button'; + +import './SideMenu.scss'; + +export default function SideMenu({ state, ...props }) { + const [search, setSearch] = useState(''); + const [menu, setMenu] = useState(false); + + const handleSearch = (e) => { + setMenu(true); + setSearch(e.target.value); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const payload = {}; + state.inputs.forEach((input) => { + input.name + ? (payload[`${input.name}`] = e.target[`${input.name}`].value) + : null; + }); + + // Submit payload + }; + + const filteredLinks = state?.links?.filter((links) => + links.organization.toLowerCase().includes(search.toLowerCase()), + ); + + const ref = useClickOutside(() => { + setMenu(false); + }); + + const handleMenuClick = (i) => { + const input = document.querySelector('.form__fields__text__input'); + input.value = filteredLinks[i].link; + setSearch(filteredLinks[i].organization); + setMenu(false); + }; + + return ( +
+

{state.summary}

+

+ {state.header === 'Report Broken Link' && ( +
+

+ Search Broken Link or Organization +

+

+
setMenu(true)}> + +
+ {menu && ( +
+
+ {filteredLinks.map((link, i) => ( + handleMenuClick(i)}> + {link.organization} + + ))} + {filteredLinks.length === 0 && ( + No Organization Found + )} +
+
+ )} +
+ )} +
+ <> +
+ {state.inputs?.map((input, i) => ( +
+ + +
+ ))} +
+
+ + Cancel + + + Submit Link + +
+ +
+
+ ); +} + +// handle export close menu when clicking outside + +function useClickOutside(handler) { + const ref = useRef(); + useEffect(() => { + const onClose = (e) => { + if (!ref.current?.contains(e.target)) { + handler(); + } + }; + + document.addEventListener('mousedown', onClose); + + return () => { + document.removeEventListener('mousedown', onClose); + }; + }); + + return ref; +} diff --git a/client/src/pages/Community/SideMenu.scss b/client/src/pages/Community/SideMenu.scss new file mode 100644 index 00000000..f9f76cb7 --- /dev/null +++ b/client/src/pages/Community/SideMenu.scss @@ -0,0 +1,70 @@ +.communityform { + width: 400px; + font-family: Montserrat; +} + +.form { + font-family: Montserrat; + + &__summary { + font-size: 12px; + margin-bottom: 32px; + font-family: Montserrat; + } + + &__search { + margin-bottom: 16px; + font-family: Montserrat; + font-size: 14px; + margin-bottom: 16px; + + &__label { + margin-bottom: 8px; + } + } + + &__menu { + position: relative; + top: 5px; + right: 0px; + width: 0%; + height: 0%; + z-index: 1; + + &__container { + position: absolute; + display: flex; + flex-direction: column; + outline: 1px solid #00000050; + width: 400px; + background-color: white; + padding: 6px; + border-radius: .3vw; + } + } + + &__fields { + &__text { + display: flex; + flex-direction: column; + font-family: Montserrat; + font-size: 14px; + margin-bottom: 16px; + + &__label { + margin-bottom: 8px; + } + + &__input { + border: 1px solid #00000050; + border-radius: .3vw; + + } + } + } + + &__buttons { + display: flex; + justify-content: flex-end; + } +} \ No newline at end of file