├── .editorconfig
├── .env.development
├── .env.production
├── .eslintrc.json
├── .gitattributes
├── .github
└── workflows
│ └── dev.yml
├── .gitignore
├── .lintstagedrc.js
├── .mailmap
├── .nvmrc
├── .prettierignore
├── .prettierrc.json
├── .vscode
└── launch.json
├── .yarnrc
├── ci
└── commit-msg.mjs
├── docs
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── architecture-decisions.md
├── commit-convention.md
└── review-criteria.md
├── etl
├── .gitignore
├── address-main.jq
├── api-main.mjs
├── fridge-cfm.jq
├── fridge-nyc.jq
├── geoapify-location-invalid.pat
├── geoapify.jq
├── input
│ ├── cfm.kml
│ └── nyc.json
├── main-api.jq
├── output
│ └── api.json
├── process.md
├── table
│ └── main.json
└── tagsFrom-cfm.jq
├── jest.config.js
├── jsconfig.json
├── mock
├── fake-data.json
├── img
│ ├── food.webp
│ └── fridge.webp
├── server-routes.json
└── src
│ └── create-reports.mjs
├── next.config.js
├── package.json
├── public
├── brand
│ ├── favicon
│ │ └── favicon-32x32.png
│ └── logo.webp
├── card
│ ├── paragraph
│ │ ├── apple.svg
│ │ ├── jumpingBlueberries.svg
│ │ ├── pearTomatoAndFridge.svg
│ │ └── plumAndFridge.svg
│ └── title
│ │ ├── becomeDriver.svg
│ │ ├── donate.svg
│ │ ├── joinCommunity.svg
│ │ ├── serviceFridge.svg
│ │ ├── sourceFood.svg
│ │ └── startFridge.svg
├── favicon.ico
├── feedback
│ ├── emailError.svg
│ ├── emailSuccess.svg
│ └── happyFridge.svg
├── hero
│ ├── about.webp
│ ├── become_a_driver.webp
│ ├── best_practices.webp
│ ├── donate_to_a_fridge.webp
│ ├── get-involved.webp
│ ├── index.webp
│ ├── join_a_community_group.webp
│ ├── service_fridges.webp
│ ├── source_food.webp
│ └── start_a_fridge.webp
└── paragraph
│ └── pamphlet
│ └── about
│ ├── collective_focus.webp
│ ├── independence_for_each_fridge.webp
│ ├── public_art_installation_on_our_sidewalks.webp
│ └── technology_empowers_us.webp
├── src
├── components
│ ├── atoms
│ │ ├── ButtonLink
│ │ │ ├── ButtonLink.jsx
│ │ │ └── index.js
│ │ ├── FeedbackCard
│ │ │ ├── FeedbackCard.jsx
│ │ │ └── index.js
│ │ ├── MapToggle
│ │ │ ├── MapToggle.jsx
│ │ │ └── index.js
│ │ ├── NextLink
│ │ │ ├── NextLink.jsx
│ │ │ └── index.js
│ │ ├── PageFooter
│ │ │ ├── PageFooter.jsx
│ │ │ └── index.js
│ │ ├── PageHero
│ │ │ ├── PageHero.jsx
│ │ │ └── index.js
│ │ ├── PamphletParagraph
│ │ │ ├── PamphletParagraph.jsx
│ │ │ └── index.js
│ │ ├── ParagraphCard
│ │ │ ├── ParagraphCard.jsx
│ │ │ └── index.js
│ │ ├── TitleCard
│ │ │ ├── TitleCard.jsx
│ │ │ └── index.js
│ │ └── index.js
│ ├── molecules
│ │ ├── AppBar
│ │ │ ├── AppBar.jsx
│ │ │ └── index.js
│ │ ├── Backtrack
│ │ │ ├── Backtrack.jsx
│ │ │ └── index.js
│ │ ├── FridgeInformation
│ │ │ ├── FridgeInformation.jsx
│ │ │ └── index.js
│ │ └── index.js
│ └── organisms
│ │ ├── browse
│ │ ├── List.jsx
│ │ ├── Map.jsx
│ │ ├── components
│ │ │ ├── LegendDrawer.jsx
│ │ │ ├── MapMarkerList.jsx
│ │ │ └── SearchMap.jsx
│ │ └── model
│ │ │ └── markersFrom.js
│ │ ├── dialog
│ │ └── components
│ │ │ ├── PanelConfirm.jsx
│ │ │ ├── PanelFridge.jsx
│ │ │ ├── PanelMaintainer.jsx
│ │ │ ├── PanelNotes.jsx
│ │ │ └── PanelReport.jsx
│ │ └── index.js
├── lib
│ ├── analytics.js
│ ├── browser.js
│ ├── createEmotionCache.js
│ ├── data.js
│ ├── format.js
│ ├── format.test.js
│ ├── geo.mjs
│ └── search.mjs
├── model
│ ├── data
│ │ └── fridge
│ │ │ ├── REST.yaml
│ │ │ ├── prop-types.js
│ │ │ └── yup.mjs
│ └── view
│ │ ├── component
│ │ └── prop-types.js
│ │ ├── dialog
│ │ └── yup.js
│ │ ├── index.js
│ │ └── prop-types.js
├── pages
│ ├── _app.js
│ ├── _document.js
│ ├── browse.jsx
│ ├── demo
│ │ └── styles.jsx
│ ├── fridge
│ │ └── [id].jsx
│ ├── index.jsx
│ ├── pamphlet
│ │ ├── about.jsx
│ │ ├── best-practices.jsx
│ │ ├── get-involved.jsx
│ │ └── get-involved
│ │ │ ├── become-a-driver.jsx
│ │ │ ├── donate-to-a-fridge.jsx
│ │ │ ├── join-a-community-group.jsx
│ │ │ ├── service-fridges.jsx
│ │ │ ├── source-food.jsx
│ │ │ └── start-a-fridge.jsx
│ └── user
│ │ ├── contact.jsx
│ │ └── fridge
│ │ ├── add.jsx
│ │ └── report.jsx
└── theme
│ ├── icons.js
│ ├── index.js
│ ├── palette.js
│ └── typography.js
├── svgo.config.js
└── yarn.lock
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_FF_API_URL=http://127.0.0.1:3050
2 | NEXT_PUBLIC_FLAG_useLocalDatabase=1
3 | NEXT_PUBLIC_ANALYTICS_ID=G-4H99PPWYCC
4 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | NEXT_PUBLIC_FF_API_URL=https://api-prod.communityfridgefinder.com
2 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "es2021": true,
5 | "node": true
6 | },
7 | "extends": ["next/core-web-vitals", "prettier"], // prettier has to be last
8 | "rules": {
9 | "no-restricted-imports": [
10 | "error",
11 | {
12 | "paths": [
13 | {
14 | "name": "@emotion/styled",
15 | "message": "Please use MUI/System instead."
16 | },
17 | {
18 | "name": "@mui/material/styles",
19 | "importNames": ["styled"],
20 | "message": "Please use MUI/System instead."
21 | }
22 | ],
23 | "patterns": ["@mui/*/*/*", "!@mui/material/test-utils/*"]
24 | }
25 | ],
26 | "react/prop-types": "warn"
27 | },
28 | "settings": {
29 | "react": {
30 | "version": "detect"
31 | }
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto eol=lf
--------------------------------------------------------------------------------
/.github/workflows/dev.yml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | on:
4 | push:
5 | branches:
6 | - dev
7 | pull_request:
8 | branches: [dev]
9 |
10 | jobs:
11 | Install_Dependencies:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - uses: actions/checkout@v3
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: "19"
18 | cache: "yarn"
19 |
20 | - name: Get yarn cache directory path
21 | id: yarn-cache-dir-path
22 | run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
23 |
24 | - name: Cache Yarn dependencies
25 | uses: actions/cache@v3
26 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
27 | with:
28 | path: node_modules
29 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
30 |
31 | - name: Install dependencies
32 | if: steps.yarn-cache.outputs.cache-hit != 'true'
33 | run: yarn install
34 | continue-on-error: false
35 |
36 | Run_Build:
37 | needs:
38 | - Install_Dependencies
39 | runs-on: ubuntu-latest
40 | steps:
41 | - uses: actions/checkout@v3
42 | - uses: actions/setup-node@v3
43 | with:
44 | node-version-file: ".nvmrc"
45 | cache: "yarn"
46 |
47 | - name: Cache Yarn Dependencies
48 | uses: actions/cache@v3
49 | with:
50 | path: node_modules
51 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
52 |
53 | - name: Run Build
54 | run: |
55 | yarn build
56 |
57 | Run_Test:
58 | needs:
59 | - Install_Dependencies
60 | runs-on: ubuntu-latest
61 | steps:
62 | - uses: actions/checkout@v3
63 | - uses: actions/setup-node@v3
64 | with:
65 | node-version-file: ".nvmrc"
66 | cache: "yarn"
67 |
68 | - name: Cache Yarn Dependencies
69 | uses: actions/cache@v3
70 | with:
71 | path: node_modules
72 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
73 |
74 | - name: Run Tests
75 | run: |
76 | yarn test
77 |
78 | Run_Lint:
79 | needs:
80 | - Install_Dependencies
81 | runs-on: ubuntu-latest
82 | steps:
83 | - uses: actions/checkout@v3
84 | - uses: actions/setup-node@v3
85 | with:
86 | node-version-file: ".nvmrc"
87 | cache: "yarn"
88 |
89 | - name: Cache Yarn Dependencies
90 | uses: actions/cache@v3
91 | with:
92 | path: node_modules
93 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
94 |
95 | - name: Run Lint
96 | run: |
97 | yarn lint
98 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # mac os
2 | .DS_Store
3 |
4 | # dependencies
5 | /node_modules
6 |
7 | # testing
8 | /coverage
9 |
10 | # next.js
11 | /.next/
12 | /out/
13 |
14 | # production
15 | /build
16 |
17 | # debug
18 | npm-debug.log*
19 | yarn-debug.log*
20 | yarn-error.log*
21 | .pnpm-debug.log*
22 |
23 | # local env files
24 | .env*.local
25 |
26 | # git backups
27 | *.orig
28 |
29 | # mock server
30 | /mock/fake-data.json
31 |
32 | # editor
33 | /.vscode/
34 | !/.vscode/launch.json
35 |
--------------------------------------------------------------------------------
/.lintstagedrc.js:
--------------------------------------------------------------------------------
1 | console.log('Running "yarn style" on commit files. Please wait...');
2 |
3 | module.exports = {
4 | '*': ['prettier --write --ignore-unknown'],
5 | '*.svg': ['svgo --quiet'],
6 | };
7 |
--------------------------------------------------------------------------------
/.mailmap:
--------------------------------------------------------------------------------
1 | Ioana Tiplea
2 |
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | v19.8.1
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # next.js
2 | /.next/
3 | /out/
4 |
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "printWidth": 80,
4 | "semi": true,
5 | "singleQuote": true,
6 | "trailingComma": "es5",
7 | "overrides": [
8 | {
9 | "files": ["*.yml"],
10 | "options": {
11 | "singleQuote": false
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.2.0",
3 | "configurations": [
4 | {
5 | "name": "CFM: API+Next+Browser",
6 | "type": "node-terminal",
7 | "request": "launch",
8 | "command": "yarn dev",
9 | "serverReadyAction": {
10 | "pattern": "started server on .+, url: http://localhost:([0-9]+)",
11 | "uriFormat": "http://localhost:%s",
12 | "action": "debugWithChrome"
13 | }
14 | },
15 | {
16 | "name": "CFM: Browser",
17 | "type": "pwa-chrome",
18 | "request": "launch",
19 | "url": "http://localhost:4000"
20 | }
21 | ]
22 | }
23 |
--------------------------------------------------------------------------------
/.yarnrc:
--------------------------------------------------------------------------------
1 | network-timeout 500000
--------------------------------------------------------------------------------
/ci/commit-msg.mjs:
--------------------------------------------------------------------------------
1 | // Invoked on the commit-msg git hook by simple-git-hooks.
2 |
3 | import { readFileSync } from 'fs';
4 | import colors from 'picocolors';
5 |
6 | // get $1 from commit-msg script
7 | const msgFilePath = process.argv[2];
8 | const msgFileContents = readFileSync(msgFilePath, 'utf-8');
9 | const commitTitle = msgFileContents.split(/\r?\n/)[0];
10 |
11 | const commitRE =
12 | /^(revert: )?(feat|fix|refactor|test|perf|style|asset|doc|ci|chore|wip)(\(.+\))?: [A-Z].{1,68}[^.]$/;
13 |
14 | if (!commitRE.test(commitTitle)) {
15 | console.log();
16 | console.error(
17 | ` ${colors.bgRed(colors.white(' ERROR '))} ${colors.white(
18 | `Invalid commit title format or length.`
19 | )}\n\n` +
20 | colors.white(
21 | ` Commit messages must under 70 characters and have the following format:\n\n`
22 | ) +
23 | ` ${colors.green(`feat: Add 'comments' option`)}\n` +
24 | ` ${colors.green(`fix: Handle events on blur (close #28)`)}\n\n` +
25 | colors.white(` See ./docs/commit-convention.md for more details.\n`)
26 | );
27 | process.exit(1);
28 | }
29 |
--------------------------------------------------------------------------------
/docs/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Code of Conduct: Fridge Finder
2 |
3 | Like the technical community as a whole, the Fridge Map team is made up of a mixture of professionals and volunteers from all over the world, working on every aspect of the mission - including mentorship, teaching, and connecting people. Diversity is one of our huge strengths, but it can also lead to communication issues and unhappiness. To that end, we have a few ground rules that we ask people to adhere to. This code applies equally to founders, mentors and those seeking help and guidance.
4 |
5 | This isn't an exhaustive list of things that you can't do. Rather, take it in the spirit in which it's intended - a guide to make it easier to enrich all of us and the technical communities in which we participate. This code of conduct applies to all spaces managed by the Fridge Map project. This includes the Discord Server, the Trello Board, and any other forums created by the project team which the community uses for communication. In addition, violations of this code outside these spaces may affect a person's ability to participate within them.
6 |
7 | If you believe someone is violating the code of conduct, we ask that you report it by emailing .
8 |
9 | - **Be friendly and patient.**
10 |
11 | - **Be welcoming.** We strive to be a community that welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability.
12 |
13 | - **Be considerate.** Your work will be used by other people, and you in turn will depend on the work of others. Any decision you take will affect users and colleagues, and you should take those consequences into account when making decisions. Remember that we're a world-wide community, so you might not be communicating in someone else's primary language.
14 |
15 | - **Be respectful.** Not all of us will agree all the time, but disagreement is no excuse for poor behavior and poor manners. We might all experience some frustration now and then, but we cannot allow that frustration to turn into a personal attack. It's important to remember that a community where people feel uncomfortable or threatened is not a productive one. Members of the Fridge Map community should be respectful when dealing with other members as well as with people outside the Fridge Map community.
16 |
17 | - **Be careful in the words that you choose.** We are a community of professionals, and we conduct ourselves professionally. Be kind to others. Do
18 | not insult or put down other participants. Harassment and other exclusionary behavior aren't acceptable. This includes, but is not limited to:
19 |
20 | - Violent threats or language directed against another person.
21 | - Discriminatory jokes and language.
22 | - Posting sexually explicit or violent material.
23 | - Posting (or threatening to post) other people's personally identifying information ("doxing").
24 | - Personal insults, especially those using racist or sexist terms.
25 | - Unwelcome sexual attention.
26 | - Advocating for, or encouraging, any of the above behavior.
27 | - Repeated harassment of others. In general, if someone asks you to stop, then stop.
28 |
29 | - **When we disagree, try to understand why.** Disagreements, both social and technical, happen all the time and Fridge Map is no exception. It is important that we resolve disagreements and differing views constructively. Remember that we're different. The strength of Fridge Map comes from its varied community, people from a wide range of backgrounds. Different people have different perspectives on issues. Being unable to understand why someone holds a viewpoint doesn't mean that they're wrong. Don't forget that it is human to err and blaming each other doesn't get us anywhere. Instead, focus on helping to resolve issues and learning from mistakes.
30 |
31 | Original text courtesy of the [Django Project](https://www.djangoproject.com/conduct/)
32 |
--------------------------------------------------------------------------------
/docs/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Collective Focus
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
Fridge Finder
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | A community fridge is a decentralized resource where businesses and individuals can [donate perishable food](https://www.thrillist.com/lifestyle/new-york/nyc-community-fridges-how-to-support). There are dozens of fridges hosted by volunteers across the country. The Fridge Finder website is available at [fridgefinder.app](https://fridgefinder.app/)
23 |
24 | Fridge Finder is a project sponsored by [Collective Focus](https://collectivefocus.site/), a community organization in Brooklyn, New York. Our goal is to make it easy for people to find fridge locations and get involved with food donation programs in their community. We are building a responsive, mobile first, multi-lingual web application with administrative controls for fridge maintainers. To join the project read our [contributing guidelines](./CONTRIBUTING.md) and [code of conduct](./CODE_OF_CONDUCT.md).
25 |
26 | Made possible by contributions from these lovely people …
27 |
28 |
29 |
30 |
31 |
32 | ❤ Thank you for all your hard work
33 |
34 | ## System Requirements
35 |
36 | 1. [Node](https://nodejs.org/en/)
37 |
38 | ## System Setup
39 |
40 | 1. Verify your system meets the requirements
41 |
42 | ```bash
43 | node --version # must be greater than 14.6.0
44 | ```
45 |
46 | 1. Install global dependencies
47 |
48 | ```bash
49 | npm install --global yarn prettier svgo lint-staged concurrently json-server
50 | corepack enable # for yarn
51 | ```
52 |
53 | 1. Setup the frontend environment
54 |
55 | ```bash
56 | git clone https://github.com/FridgeFinder/CFM_Frontend frontend
57 | cd frontend
58 | git checkout dev
59 | yarn install
60 | ```
61 |
62 | 1. Run the unit tests
63 |
64 | ```bash
65 | yarn test
66 | ```
67 |
68 | 1. Run the application locally
69 |
70 | ```bash
71 | # to run both development database and Next.js web server
72 | yarn dev
73 |
74 | # to run only the web server
75 | yarn web
76 |
77 | # to run only the development database
78 | yarn data
79 | ```
80 |
81 | in a different terminal window
82 |
83 | ```bash
84 | start "Google Chrome" http://localhost:3000/ # Windows
85 | open -a "Google Chrome" http://localhost:3000/ # MacOS
86 | ```
87 |
--------------------------------------------------------------------------------
/docs/architecture-decisions.md:
--------------------------------------------------------------------------------
1 | # Architecture Decisions
2 |
3 | ## 2022-05-26 -- CSS Engine
4 |
5 | [MUI recommends the use of emotion for CSS styling](https://mui.com/material-ui/guides/styled-engine/).
6 |
7 | > Warning: Using styled-components as an engine at this moment is not working when used in a SSR projects. The reason is that the babel-plugin-styled-components is not picking up correctly the usages of the styled() utility inside the @mui packages. For more details, take a look at this issue. We strongly recommend using emotion for SSR projects.
8 |
9 | ## 2022-07-17 -- Deprecate the use of styled()
10 |
11 | The following functions have been deprecated because they are slow to render, cause issues with css caching, and cannot be rendered server side. Use the MUI/System `sx` prop instead.
12 |
13 | ```js
14 | import { styled } from '@mui/material/styles';
15 | import { styled } from '@emotion';
16 | ```
17 |
18 | ## 2022-07-17 -- API field sizes
19 |
20 | Tag : 140 characters. The size of a twitter hash tag.
21 |
22 | Location.street : 55 characters. The longest street name in the U.S. is 38 characters long: "Jean Baptiste Point du Sable Lake Shore Drive" located in Chicago, Illinois. eg: 1001 Jean Baptiste Point du Sable Lake Shore Drive #33
23 |
24 | Location.city : 35 characters. The longest city name in the U.S. is "Village of Grosse Pointe Shores" in Michigan.
25 |
26 | Maintainer.name : 70 characters. https://stackoverflow.com/questions/30485/what-is-a-reasonable-length-limit-on-person-name-fields
27 |
28 | ## 2022-07-23 -- Standardize to bash shell
29 |
30 | Standardizing all script commands for bash has proven more complicated than it is worth. The node runtime and `yarn dev` execute commands via the system shell. Overriding this to use bash has proven difficult because the configuration steps differ between the various node versions and yarn versions. In addition the bash directory path is not POSIX compliant in Windows and MacOS. `/usr/bin/bash` wont work in either.
31 |
--------------------------------------------------------------------------------
/docs/commit-convention.md:
--------------------------------------------------------------------------------
1 | # Git Commit Message Convention
2 |
3 | This convention is adapted from:
4 |
5 | - [How to Write a Git Commit Message](https://cbea.ms/git-commit/) by Chris Beams
6 | - [Semantic Commit Messages](https://gist.github.com/joshbuchea/6f47e86d2510bce28f8e7f42ae84c716) by Josh Buchea
7 |
8 | ## synopsis
9 |
10 | - commit message header format: `(): `
11 | - `subject` must start with a capital letter
12 | - `subject` must not end with a period
13 | - `subject` must not exceed 70 characters
14 |
15 | commit header examples:
16 |
17 | - `feat: Add hat wobble`
18 | - `wip: Moved commit-convention to docs`
19 |
20 | ## commit message
21 |
22 | A commit message consists of a `header`, `body` and `footer`. The header has a `type`, `scope` and `subject`:
23 |
24 | ```
25 | ():
26 |
27 |
28 |
29 |
30 | ```
31 |
32 | ### header
33 |
34 | The `header` is mandatory and the `scope` of the header is optional.
35 |
36 | ```
37 | feat: Add hat wobble
38 | ^--^ ^------------^
39 | | |
40 | | +-> Summary in present tense.
41 | |
42 | +-------> Type: chore, docs, feat, fix, refactor, style, or test.
43 | ```
44 |
45 | #### type
46 |
47 | If the type is `feat`, `fix` or `perf`, it will appear in the changelog. However, if there is any [BREAKING CHANGE](#footer), the commit will always appear in the changelog.
48 |
49 | The types are as follows:
50 |
51 | - `feat`: new feature for the user, not a new feature for build script
52 | - `fix`: bug fix for the user, not a fix to a build script
53 | - `refactor`: refactoring production code, eg. renaming a variable
54 | - `test`: adding missing tests, refactoring tests; no production code change
55 | - `perf`: performance improvements to production code
56 | - `style`: formatting, missing semi colons, etc; no production code change
57 | - `asset`: changing static assets. ie: css files, images, etc
58 | - `doc`: changes to the documentation
59 | - `ci`: updating CD/CI pipeline; no local script changes
60 | - `chore`: updating grunt tasks etc; no production code change
61 | - `revert`: reverting a previously published commit
62 | - `wip`: work in progress commit
63 |
64 | ##### revert
65 |
66 | If the commit reverts a previous commit, it should begin with `revert: `, followed by the header of the reverted commit. In the body, it should say: `This reverts commit .`, where the hash is the SHA of the commit being reverted.
67 |
68 | #### scope
69 |
70 | The scope could be anything specifying the place of the commit change. For example `dev`, `build`, `workflow`, `cli`, etc. If you don't know what the scope is, leave it blank.
71 |
72 | #### subject
73 |
74 | 1. Separate subject from body with a blank line
75 | 1. Limit the subject line to 70 characters
76 | 1. Capitalize the subject line
77 | 1. Do not end the subject line with a period
78 | 1. Use the imperative mood in the subject line
79 | 1. Wrap the body at 72 characters
80 | 1. Use the body to explain what and why vs. how
81 |
82 | ### body
83 |
84 | Just as in the `subject`, use the imperative, present tense: "change" not "changed" nor "changes".
85 | The body should include the motivation for the change and contrast this with previous behavior.
86 |
87 | ### footer
88 |
89 | The footer should contain any information about **Breaking Changes** and is also the place to
90 | reference GitHub issues that this commit **Closes**.
91 |
92 | **Breaking Changes** should start with the word `BREAKING CHANGE:` with a space or two newlines. The rest of the commit message is then used for this.
93 |
94 | ### examples
95 |
96 | Appears under "Features" header, `dev` subheader:
97 |
98 | ```
99 | feat(dev): Add 'comments' option
100 | ```
101 |
102 | Appears under "Bug Fixes" header, `dev` subheader, with a link to issue #28:
103 |
104 | ```
105 | fix(dev): Fix dev error
106 |
107 | close #28
108 | ```
109 |
110 | Appears under "Performance Improvements" header, and under "Breaking Changes" with the breaking change explanation:
111 |
112 | ```
113 | perf(build): Remove 'foo' option
114 |
115 | BREAKING CHANGE: The 'foo' option has been removed.
116 | ```
117 |
118 | The following commit and commit `667ecc1` do not appear in the changelog if they are under the same release. If not, the revert commit appears under the "Reverts" header.
119 |
120 | ```
121 | revert: feat(compiler): Add 'comments' option
122 |
123 | This reverts commit 667ecc1654a317a13331b17617d973392f415f02.
124 | ```
125 |
--------------------------------------------------------------------------------
/docs/review-criteria.md:
--------------------------------------------------------------------------------
1 | # Review: Fridge Finder
2 |
3 | 1. UI Design
4 |
5 | - code implements all the elements from the linked Figma node
6 | - uses rounded MUI icons
7 |
8 | 1. Code Quality
9 |
10 | - there are no linting warnings in the shell terminal
11 | - there are no errors in the browser console
12 | - [no superfluous React import](https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html#removing-unused-react-imports)
13 | - must not import `styled` from '@mui/material/styles' or '@emotion/styled'
14 |
15 | 1. Test
16 |
17 | - ui component has a snapshot test
18 | - ui behaviour is tested
19 |
20 | 1. File Structure
21 |
22 | - ui component is in the correct directory `/atoms, /molecules, /organisms`
23 | - ui component file name communicates its purpose
24 | - library function is in a `/lib` module
25 | - library function is documented
26 |
27 | 1. Commit
28 |
29 | - atomic commit
30 |
--------------------------------------------------------------------------------
/etl/.gitignore:
--------------------------------------------------------------------------------
1 | temp/
2 |
--------------------------------------------------------------------------------
/etl/address-main.jq:
--------------------------------------------------------------------------------
1 | [
2 | .[] |
3 | {
4 | mainId,
5 | address: (.locationStreet + ", " + .locationCity + ", " + .locationState + " " + .locationZip),
6 | }
7 | ]
8 |
--------------------------------------------------------------------------------
/etl/fridge-cfm.jq:
--------------------------------------------------------------------------------
1 | [
2 | .[] |
3 | {
4 | id,
5 | fridgeName: .name,
6 | fridgeVerified: false,
7 | fridgeNotes: "",
8 |
9 | maintainerName: "",
10 | maintainerEmail: "",
11 | maintainerOrganization: "",
12 | maintainerPhone: "",
13 | maintainerInstagram: (.data | .[] | select(."-name" == "ig handle").value ),
14 | }
15 | ]
16 |
--------------------------------------------------------------------------------
/etl/fridge-nyc.jq:
--------------------------------------------------------------------------------
1 | [
2 | .[] |
3 | {
4 | id,
5 | fridgeName: .name,
6 | fridgeVerified: false,
7 | fridgeNotes: "",
8 |
9 | maintainerName: "",
10 | maintainerEmail: "",
11 | maintainerOrganization: "",
12 | maintainerPhone: "",
13 | maintainerInstagram: .instagram,
14 | maintainerWebsite: "",
15 | }
16 | ]
17 |
--------------------------------------------------------------------------------
/etl/geoapify-location-invalid.pat:
--------------------------------------------------------------------------------
1 | "locationName"\: "",=;
2 | "locationName"\: "Manhattan",=;
3 | "locationName"\: "Elmhurst",=;
4 | "locationName"\: "Brooklyn",=;
5 | "locationName"\: "New York",=;
6 |
--------------------------------------------------------------------------------
/etl/geoapify.jq:
--------------------------------------------------------------------------------
1 | [
2 | .[] |
3 | {
4 | mainId: .id | tonumber,
5 | locationName: .name,
6 | locationStreet: (.housenumber + " " + .street),
7 | locationCity: .city,
8 | locationState: .state_code,
9 | locationZip: .postcode,
10 | locationGeoLat: .lat | tonumber,
11 | locationGeoLng: .lon | tonumber,
12 | }
13 | ]
14 |
--------------------------------------------------------------------------------
/etl/main-api.jq:
--------------------------------------------------------------------------------
1 | [
2 | .fridges | .[] |
3 | {
4 | mainId: .mainId,
5 | fridgeId: .id,
6 | fridgeName: .name,
7 | fridgeVerified: .verified,
8 | locationName: .location.name,
9 | locationStreet: .location.street,
10 | locationCity: .location.city,
11 | locationState: .location.state,
12 | locationZip: .location.zip,
13 | fridgeNotes: .notes,
14 | maintainerName: .maintainer.name,
15 | maintainerEmail: .maintainer.email,
16 | maintainerOrganization: .maintainer.organization,
17 | maintainerPhone: .maintainer.phone,
18 | maintainerInstagram: .maintainer.instagram,
19 | maintainerWebsite: .maintainer.website,
20 | }
21 | ]
22 |
--------------------------------------------------------------------------------
/etl/tagsFrom-cfm.jq:
--------------------------------------------------------------------------------
1 | [
2 | .[] |
3 | {
4 | id,
5 | freezer: (.data | .[] | select(."-name" == "Has a freezer").value )
6 | }
7 | ]
8 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | const nextJest = require('next/jest');
2 |
3 | const createJestConfig = nextJest({
4 | dir: './' /** path to next.config.js and .env files in your test environment */,
5 | });
6 |
7 | /**
8 | * Configuration passed directly to Jest.
9 | *
10 | * @type {import('jest').Config}
11 | */
12 | const customJestConfig = {
13 | testEnvironment: 'jest-environment-jsdom',
14 | moduleDirectories: ['node_modules', 'src'],
15 |
16 | setupFilesAfterEnv: [
17 | '@testing-library/jest-dom/extend-expect',
18 | '@testing-library/react',
19 | ],
20 | };
21 |
22 | // createJestConfig is exported this way to ensure that next/jest can load the Next.js config which is async
23 | module.exports = createJestConfig(customJestConfig);
24 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./src"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/mock/fake-data.json:
--------------------------------------------------------------------------------
1 | {
2 | "contact": [],
3 | "fridges": [
4 | {
5 | "id": "allfieldsfridge",
6 | "name": "All Fields Fridge",
7 | "tags": ["harlem", "halal", "kashrut"],
8 | "location": {
9 | "name": "The Bakery Store",
10 | "street": "17 Nassau Ave",
11 | "city": "Brooklyn",
12 | "state": "NY",
13 | "zip": "11222",
14 | "geoLat": 40.72283,
15 | "geoLng": -73.95412
16 | },
17 | "maintainer": {
18 | "name": "Emily Ward",
19 | "email": "eward@example.net",
20 | "organization": "Food For People",
21 | "phone": "(782) 654-4748",
22 | "website": "http://gray.com/",
23 | "instagram": "https://www.instagram.com/theneighborhoodfridge"
24 | },
25 | "notes": "Next to Lot Radio. Go up the stairs and to the right.",
26 | "photoUrl": "http://127.0.0.1:3000/fridge.webp",
27 | "verified": false
28 | },
29 | {
30 | "id": "requiredfieldsfridge",
31 | "name": "Required Fields Fridge",
32 | "location": {
33 | "street": "326 6th St",
34 | "city": "Brooklyn",
35 | "state": "NY",
36 | "zip": "11215",
37 | "geoLat": 40.67108,
38 | "geoLng": -73.9852
39 | }
40 | },
41 | {
42 | "id": "happyfridge",
43 | "name": "Happy Fridge",
44 | "tags": ["harlem", "halal", "kashrut"],
45 | "location": {
46 | "street": "326 6th St",
47 | "city": "Brooklyn",
48 | "state": "NY",
49 | "zip": "11215",
50 | "geoLat": 40.67108,
51 | "geoLng": -73.9852
52 | },
53 | "maintainer": {
54 | "name": "Emily Ward",
55 | "email": "eward@example.net",
56 | "organization": "Food For People",
57 | "phone": "(782) 654-4748",
58 | "instagram": "https://www.instagram.com/theneighborhoodfridge"
59 | },
60 | "notes": "Next to Lot Radio.",
61 | "photoUrl": "http://127.0.0.1:3000/fridge.webp",
62 | "verified": true
63 | },
64 | {
65 | "id": "smartfridge",
66 | "name": "Smart Fridge",
67 | "tags": ["harlem", "halal", "kashrut"],
68 | "location": {
69 | "street": "326 6th St",
70 | "city": "Brooklyn",
71 | "state": "NY",
72 | "zip": "11215",
73 | "geoLat": 40.67108,
74 | "geoLng": -73.9852
75 | },
76 | "maintainer": {
77 | "website": "http://gray.com/"
78 | },
79 | "notes": "Next to Lot Radio.",
80 | "verified": false
81 | }
82 | ],
83 | "photo": [
84 | {
85 | "id": 1
86 | }
87 | ],
88 | "reports": [
89 | {
90 | "id": "2022-03-29T18:10:38.547Z",
91 | "fridgeId": "allfieldsfridge",
92 | "timestamp": "2022-03-29T18:10:38.547Z",
93 | "condition": "good",
94 | "foodPercentage": 100,
95 | "photoUrl": "http://127.0.0.1:3000/food.webp",
96 | "notes": "Filled with Mars bars and M&M candy."
97 | },
98 | {
99 | "id": "2022-03-29T18:10:38.547Z",
100 | "fridgeId": "allfieldsfridge",
101 | "timestamp": "2022-03-29T18:10:38.547Z",
102 | "condition": "dirty",
103 | "foodPercentage": 0,
104 | "photoUrl": "http://127.0.0.1:3000/food.webp",
105 | "notes": ""
106 | },
107 | {
108 | "id": "2022-03-29T18:10:38.547Z",
109 | "fridgeId": "requiredfieldsfridge",
110 | "timestamp": "2022-03-29T18:10:38.547Z",
111 | "condition": "out of order",
112 | "foodPercentage": 100
113 | },
114 | {
115 | "id": "2022-03-29T18:10:38.547Z",
116 | "fridgeId": "requiredfieldsfridge",
117 | "timestamp": "2022-03-29T18:10:38.547Z",
118 | "condition": "not at location",
119 | "foodPercentage": 0
120 | }
121 | ]
122 | }
123 |
--------------------------------------------------------------------------------
/mock/img/food.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/mock/img/food.webp
--------------------------------------------------------------------------------
/mock/img/fridge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/mock/img/fridge.webp
--------------------------------------------------------------------------------
/mock/server-routes.json:
--------------------------------------------------------------------------------
1 | {
2 | "/v1/*": "/$1"
3 | }
4 |
--------------------------------------------------------------------------------
/mock/src/create-reports.mjs:
--------------------------------------------------------------------------------
1 | import { faker } from '@faker-js/faker';
2 | import http from 'http';
3 | const { randomInt } = await import('crypto');
4 |
5 | const apiHost = '127.0.0.1';
6 | const apiPort = 3050;
7 |
8 | const httpGetOptions = {
9 | hostname: apiHost,
10 | port: apiPort,
11 | path: '/v1/fridges',
12 | method: 'GET',
13 | };
14 |
15 | const httpPostOptions = {
16 | hostname: apiHost,
17 | port: apiPort,
18 | method: 'POST',
19 | };
20 |
21 | main();
22 |
23 | function main() {
24 | const req = http.request(httpGetOptions, (res) => {
25 | let body = '';
26 |
27 | res.on('data', (chunk) => (body += chunk));
28 | res.on('end', () => {
29 | try {
30 | let json = JSON.parse(body);
31 | for (let fridge of json) {
32 | createReportFor(fridge);
33 | }
34 | } catch (error) {
35 | console.error(error.message);
36 | }
37 | });
38 | });
39 |
40 | req.on('error', (error) => {
41 | console.error(error);
42 | });
43 |
44 | req.end();
45 | }
46 |
47 | function createReportFor({ id }) {
48 | // 1/10 chance of no report
49 | if (oneIn(10)) {
50 | return;
51 | }
52 | const data = JSON.stringify({
53 | fridgeId: id,
54 | timestamp: faker.date.recent(),
55 | condition: oneIn(50)
56 | ? 'not at location'
57 | : faker.helpers.arrayElement(['good', 'dirty', 'out of order', 'ghost']),
58 | foodPercentage: faker.helpers.arrayElement([0, 1, 2, 3]),
59 | photoUrl: 'http://placekitten.com/200/300',
60 | notes: faker.lorem.lines(1),
61 | });
62 |
63 | httpPostOptions.path = `/v1/fridges/${id}/reports`;
64 | httpPostOptions.headers = {
65 | 'Content-Type': 'application/json',
66 | 'Content-Length': data.length,
67 | };
68 |
69 | const postReq = http.request(httpPostOptions, (res) => {
70 | if (res.statusCode != 201) {
71 | console.log(`statusCode: ${res.statusCode}`);
72 | }
73 | });
74 |
75 | postReq.on('error', (error) => {
76 | console.error(error);
77 | });
78 |
79 | postReq.write(data);
80 | postReq.end();
81 | }
82 |
83 | const oneIn = (n) => randomInt(n) === 0;
84 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | reactStrictMode: true,
3 |
4 | images: {
5 | domains: [
6 | 'placekitten.com',
7 | 'community-fridge-map-images-prod.s3.amazonaws.com',
8 | ],
9 | },
10 |
11 | // --- Next.js@12.2.2
12 | swcMinify: true,
13 | compiler: {
14 | emotion: true,
15 | },
16 | modularizeImports: {
17 | '@mui/material': {
18 | transform: '@mui/material/{{member}}',
19 | },
20 | '@mui/icons-material': {
21 | transform: '@mui/icons-material/{{member}}',
22 | },
23 | 'components/atoms': {
24 | transform: 'components/atoms/{{member}}',
25 | },
26 | 'components/molecules': {
27 | transform: 'components/molecules/{{member}}',
28 | },
29 | 'components/organisms': {
30 | transform: 'components/organisms/{{member}}',
31 | },
32 | },
33 |
34 | // ---
35 |
36 | eslint: {
37 | dirs: ['src/', 'ci/'],
38 | },
39 | };
40 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "0.1.0",
3 | "name": "community-fridge-map",
4 | "description": "A mobile friendly website that displays an interactive map of all the community fridges in and around New York City.",
5 | "keywords": [
6 | "nyc",
7 | "food bank",
8 | "community fridge",
9 | "map"
10 | ],
11 | "homepage": "https://github.com/FridgeFinder/CFM_Frontend#readme",
12 | "author": "Fridge Finder ",
13 | "contributors": [
14 | "Andrew R",
15 | "Bernard Martis (https://bernardm.github.io/)",
16 | "Hamaad Chughtai (https://github.com/Hamaad102)",
17 | "Ioana Tiplea (https://github.com/ioanat94)",
18 | "Jaron Earle (https://github.com/jaronaearle)",
19 | "Ricardo Camacho Mireles (https://github.com/rcamach7)",
20 | "Ricky Saka (https://github.com/SakaRicky)",
21 | "Sean Redmon (https://github.com/seanred360)",
22 | "Trillium Smith (https://github.com/Spiteless)",
23 | "Weston Norwood (https://github.com/wheninseattle)",
24 | "Youssef El Rhilassi (https://github.com/YELrhilassi)"
25 | ],
26 | "license": "MIT",
27 | "private": true,
28 | "repository": {
29 | "type": "git",
30 | "url": "git+https://github.com/FridgeFinder/CFM_Frontend.git"
31 | },
32 | "bugs": {
33 | "url": "https://github.com/FridgeFinder/CFM_Frontend/issues"
34 | },
35 | "directories": {
36 | "doc": "./docs"
37 | },
38 | "scripts": {
39 | "postinstall": "npx simple-git-hooks",
40 | "dev": "concurrently --names \"MOCK,NEXT\" --prefix-colors \"yellow,green\" --kill-others \"json-server --watch etl/output/api.json --routes mock/server-routes.json --host 127.0.0.1 --port 3050\" \"npm exec --offline -- next\"",
41 | "web": "next",
42 | "data": "json-server --watch etl/output/api.json --routes mock/server-routes.json --host 127.0.0.1 --port 3050",
43 | "build": "next build",
44 | "start": "next start",
45 | "lint": "next lint --fix",
46 | "style": "prettier --write --ignore-unknown . && svgo -qrf public/",
47 | "test": "jest --coverage"
48 | },
49 | "browserslist": [
50 | ">0.3%",
51 | "not ie 11",
52 | "not dead",
53 | "not op_mini all"
54 | ],
55 | "packageManager": "yarn@1.22.15",
56 | "simple-git-hooks": {
57 | "pre-commit": "lint-staged --quiet --no-stash --concurrent true",
58 | "commit-msg": "node ci/commit-msg.mjs $1"
59 | },
60 | "dependencies": {
61 | "@emotion/cache": "^11.7.1",
62 | "@emotion/react": "^11.9.0",
63 | "@emotion/server": "^11.4.0",
64 | "@emotion/styled": "^11.8.1",
65 | "@mui/icons-material": "^5.8.3",
66 | "@mui/material": "^5.8.1",
67 | "formik": "^2.2.9",
68 | "leaflet": "^1.8.0",
69 | "next": "^13.4.3",
70 | "prop-types": "^15.8.1",
71 | "react": "^18.2.0",
72 | "react-dom": "^18.2.0",
73 | "react-leaflet": "^4.0.1",
74 | "sharp": "^0.31.1",
75 | "yup": "^0.32.11"
76 | },
77 | "devDependencies": {
78 | "@babel/core": "^7.18.6",
79 | "@faker-js/faker": "^7.5.0",
80 | "@testing-library/jest-dom": "^5.16.5",
81 | "@testing-library/react": "^14.0.0",
82 | "eslint": "8.15.0",
83 | "eslint-config-next": "^13.4.3",
84 | "eslint-config-prettier": "^8.5.0",
85 | "jest": "^29.5.0",
86 | "jest-environment-jsdom": "^29.5.0",
87 | "picocolors": "^1.0.0",
88 | "simple-git-hooks": "^2.7.0"
89 | },
90 | "optionalDependencies": {
91 | "entities": "^4.3.1",
92 | "url-exist": "^3.0.1"
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/public/brand/favicon/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/brand/favicon/favicon-32x32.png
--------------------------------------------------------------------------------
/public/brand/logo.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/brand/logo.webp
--------------------------------------------------------------------------------
/public/card/paragraph/apple.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/paragraph/jumpingBlueberries.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/paragraph/plumAndFridge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/becomeDriver.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/donate.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/joinCommunity.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/serviceFridge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/sourceFood.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/card/title/startFridge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/favicon.ico
--------------------------------------------------------------------------------
/public/feedback/emailError.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/feedback/emailSuccess.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/feedback/happyFridge.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/hero/about.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/about.webp
--------------------------------------------------------------------------------
/public/hero/become_a_driver.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/become_a_driver.webp
--------------------------------------------------------------------------------
/public/hero/best_practices.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/best_practices.webp
--------------------------------------------------------------------------------
/public/hero/donate_to_a_fridge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/donate_to_a_fridge.webp
--------------------------------------------------------------------------------
/public/hero/get-involved.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/get-involved.webp
--------------------------------------------------------------------------------
/public/hero/index.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/index.webp
--------------------------------------------------------------------------------
/public/hero/join_a_community_group.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/join_a_community_group.webp
--------------------------------------------------------------------------------
/public/hero/service_fridges.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/service_fridges.webp
--------------------------------------------------------------------------------
/public/hero/source_food.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/source_food.webp
--------------------------------------------------------------------------------
/public/hero/start_a_fridge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/hero/start_a_fridge.webp
--------------------------------------------------------------------------------
/public/paragraph/pamphlet/about/collective_focus.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/paragraph/pamphlet/about/collective_focus.webp
--------------------------------------------------------------------------------
/public/paragraph/pamphlet/about/independence_for_each_fridge.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/paragraph/pamphlet/about/independence_for_each_fridge.webp
--------------------------------------------------------------------------------
/public/paragraph/pamphlet/about/public_art_installation_on_our_sidewalks.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/paragraph/pamphlet/about/public_art_installation_on_our_sidewalks.webp
--------------------------------------------------------------------------------
/public/paragraph/pamphlet/about/technology_empowers_us.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FridgeFinder/CFM_Frontend/a62b8308f1a3e08df9bad9cbfaf4730c78954a88/public/paragraph/pamphlet/about/technology_empowers_us.webp
--------------------------------------------------------------------------------
/src/components/atoms/ButtonLink/ButtonLink.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { Button } from '@mui/material';
3 | import { NextLink } from 'components/atoms';
4 |
5 | export default function ButtonLink(props) {
6 | return ;
7 | }
8 |
9 | const toPathname = PropTypes.shape({
10 | pathname: PropTypes.string.isRequired,
11 | query: PropTypes.object.isRequired,
12 | });
13 |
14 | ButtonLink.propTypes = {
15 | /**
16 | * The URL or pathname object to navigate to.
17 | *
18 | * to='/about'
19 | * to={{ pathname: '/blog/[slug]', query: { slug: post.slug }, }}
20 | */
21 | to: PropTypes.oneOfType([PropTypes.string, toPathname]).isRequired,
22 |
23 | /**
24 | * Text describing the content of the link.
25 | * eg: aria-label='Learn more about the Fridge Finder project.'
26 | */
27 | 'aria-label': PropTypes.string.isRequired,
28 |
29 | /***
30 | * Style of button from theme
31 | */
32 | variant: PropTypes.oneOf(['outlined', 'contained']).isRequired,
33 | };
34 |
--------------------------------------------------------------------------------
/src/components/atoms/ButtonLink/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ButtonLink';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/FeedbackCard/FeedbackCard.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Image from 'next/legacy/image';
3 | import { Box, Button, Typography } from '@mui/material';
4 | import {
5 | WarningAmberRounded as ErrorIcon,
6 | TaskAltRounded as SuccessIcon,
7 | } from '@mui/icons-material';
8 | import { ButtonLink } from 'components/atoms';
9 |
10 | const sxSuccessIcon = { fontSize: '1.1em', verticalAlign: 'top' };
11 | const sxErrorIcon = { fontSize: '1.3em', verticalAlign: 'text-bottom' };
12 | const displayHeading = {
13 | EmailSuccess: (
14 | <>
15 | Success!
16 | >
17 | ),
18 | EmailError: (
19 | <>
20 | Error!
21 | >
22 | ),
23 | ReportStatus: (
24 | <>
25 | Success!
26 | >
27 | ),
28 | CreateFridge: (
29 | <>
30 | Success!
31 | >
32 | ),
33 | };
34 |
35 | const displayText = {
36 | EmailSuccess: 'Your email was sent.',
37 | EmailError: 'Your email was not sent.',
38 | ReportStatus: 'You have successfully submitted a status report!',
39 | CreateFridge: 'You have successfully added a fridge listing!',
40 | };
41 |
42 | const displayImg = {
43 | EmailSuccess: {
44 | src: '/feedback/emailSuccess.svg',
45 | width: 313,
46 | height: 280,
47 | alt: 'Email success image',
48 | },
49 | EmailError: {
50 | src: '/feedback/emailError.svg',
51 | width: 163,
52 | height: 245,
53 | alt: 'Email error image',
54 | },
55 | ReportStatus: {
56 | src: '/feedback/happyFridge.svg',
57 | width: 163,
58 | height: 245,
59 | alt: 'Happy fridge image',
60 | },
61 | CreateFridge: {
62 | src: '/feedback/happyFridge.svg',
63 | width: 163,
64 | height: 245,
65 | alt: 'Happy fridge image',
66 | },
67 | };
68 |
69 | const displayButton = {
70 | EmailSuccess: (
71 |
78 | BACK TO HOME
79 |
80 | ),
81 | EmailError: null,
82 | ReportStatus: (
83 |
90 | GO TO FRIDGE
91 |
92 | ),
93 | CreateFridge: (
94 | <>
95 |
102 | GO TO FRIDGE
103 |
104 |
111 | EDIT FRIDGE
112 |
113 | >
114 | ),
115 | };
116 |
117 | export default function FeedbackCard({ type, action = null }) {
118 | /**
119 | * TODO This is a temporary bypass until I figure out how the other dialogs handle failure. There is no point in implementing a Button factory until then -- Bernard
120 | */
121 | if (type === 'EmailError') {
122 | if (action) {
123 | displayButton[type] = (
124 |
131 | TRY AGAIN
132 |
133 | );
134 | } else {
135 | throw 'Missing EmailError callback function';
136 | }
137 | }
138 |
139 | return (
140 |
148 |
149 | {displayHeading[type]}
150 |
151 |
152 | {displayText[type]}
153 |
154 |
155 | {displayButton[type]}
156 |
157 | );
158 | }
159 | FeedbackCard.propTypes = {
160 | type: PropTypes.oneOf([
161 | 'ReportStatus',
162 | 'CreateFridge',
163 | 'EmailSuccess',
164 | 'EmailError',
165 | ]),
166 | action: PropTypes.func,
167 | };
168 |
--------------------------------------------------------------------------------
/src/components/atoms/FeedbackCard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './FeedbackCard';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/MapToggle/MapToggle.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { Button } from '@mui/material';
3 | import {
4 | MapOutlined as MapIcon,
5 | FormatListBulletedOutlined as ListIcon,
6 | } from '@mui/icons-material';
7 |
8 | export default function MapToggle({ currentView, setView }) {
9 | return (
10 | :
14 | }
15 | sx={{
16 | position: 'fixed',
17 | bottom: 0,
18 | zIndex: 999,
19 | height: 60,
20 | backgroundColor: '#fff',
21 | border: 'none',
22 | borderRadius: 3,
23 | borderBottomLeftRadius: 0,
24 | borderBottomRightRadius: 0,
25 | boxShadow: '-2px 0px 4px rgb(0 0 0 / 20%)',
26 | justifyContent: 'left',
27 | padding: 5,
28 | fontWeight: 500,
29 | textTransform: 'none',
30 | ':hover': { backgroundColor: '#fff' },
31 | }}
32 | onClick={() =>
33 | setView(
34 | currentView === MapToggle.view.map
35 | ? MapToggle.view.list
36 | : MapToggle.view.map
37 | )
38 | }
39 | >
40 | {currentView === MapToggle.view.map ? 'List View' : 'Map View'}
41 |
42 | );
43 | }
44 | MapToggle.propTypes = {
45 | currentView: PropTypes.symbol,
46 | setView: PropTypes.func,
47 | };
48 |
49 | MapToggle.view = Object.freeze({
50 | map: Symbol(0),
51 | list: Symbol(1),
52 | });
53 |
--------------------------------------------------------------------------------
/src/components/atoms/MapToggle/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './MapToggle';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/NextLink/NextLink.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import PropTypes from 'prop-types';
3 | import Link from 'next/link';
4 |
5 | /**
6 | * This component allows Next/Link to function correctly when it is passed into another react component using props.
7 | *
8 | * @see https://nextjs.org/docs/routing/introduction
9 | * @see https://github.com/mui/material-ui/blob/master/examples/nextjs-with-typescript/src/Link.tsx
10 | *
11 | * @param {Object} props
12 | * @param {Element} ref
13 | */
14 | const NextLink = React.forwardRef(function (props, ref) {
15 | const {
16 | to,
17 | linkAs,
18 | replace,
19 | scroll,
20 | shallow,
21 | prefetch = false,
22 | locale,
23 | ...attributes
24 | } = props;
25 |
26 | return (
27 |
39 | );
40 | });
41 |
42 | NextLink.displayName = 'NextLink';
43 |
44 | const toPathname = PropTypes.shape({
45 | pathname: PropTypes.string.isRequired,
46 | query: PropTypes.object.isRequired,
47 | });
48 |
49 | NextLink.propTypes = {
50 | /**
51 | * The URL or pathname object to navigate to.
52 | *
53 | * to='/about'
54 | * to={{ pathname: '/blog/[slug]', query: { slug: post.slug }, }}
55 | */
56 | to: PropTypes.oneOfType([PropTypes.string, toPathname]).isRequired,
57 |
58 | /**
59 | * Optional HTML attributes for the tag
60 | */
61 | attributes: PropTypes.object,
62 |
63 | /**
64 | * Optional decorator for the path that will be shown in the browser URL bar.
65 | */
66 | linkAs: PropTypes.string,
67 |
68 | /**
69 | * Allows for providing a different locale.
70 | */
71 | locale: PropTypes.oneOfType([PropTypes.string, PropTypes.bool]),
72 |
73 | /**
74 | * Prefetch the page in the background.
75 | */
76 | prefetch: PropTypes.bool,
77 |
78 | /**
79 | * Replace the current history state instead of adding a new url into the stack.
80 | */
81 | replace: PropTypes.bool,
82 |
83 | /**
84 | * Scroll to the top of the page after a navigation.
85 | */
86 | scroll: PropTypes.bool,
87 |
88 | /**
89 | * Update the path of the current page without rerunning getStaticProps, getServerSideProps or getInitialProps.
90 | */
91 | shallow: PropTypes.bool,
92 | };
93 |
94 | export default NextLink;
95 |
--------------------------------------------------------------------------------
/src/components/atoms/NextLink/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './NextLink';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/PageFooter/PageFooter.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { designColor } from 'theme/palette';
3 | import { Box, Typography } from '@mui/material';
4 |
5 | export default function PageFooter({
6 | scrollButton = true,
7 | fixedAtBottom = false,
8 | }) {
9 | const sxFooter = {
10 | padding: 2,
11 | backgroundColor: designColor.magneticGray,
12 | width: '100%',
13 | display: 'flex',
14 | flexFlow: 'row wrap',
15 | rowGap: '0.2em',
16 | };
17 |
18 | if (fixedAtBottom) {
19 | sxFooter['position'] = 'fixed';
20 | sxFooter['bottom'] = 0;
21 | scrollButton = false;
22 | }
23 |
24 | return (
25 |
26 | {PageScroll(scrollButton)}
27 |
28 | © 2022, Collective Focus. All rights reserved.
29 |
30 |
31 | We may use cookies for storing information to help provide you with a
32 | better, faster, and safer experience and for SEO purposes.
33 |
34 |
35 | );
36 | }
37 | PageFooter.propTypes = {
38 | scrollButton: PropTypes.bool,
39 | fixedAtBottom: PropTypes.bool,
40 | };
41 | PageFooter.defaultProps = {
42 | scrollButton: true,
43 | fixedAtBottom: false,
44 | };
45 |
46 | function PageScroll(display) {
47 | return display ? (
48 | \') center no-repeat',
60 | boxShadow: '0 0.25rem 0.5rem 0 #222',
61 | opacity: 0.6,
62 | }}
63 | />
64 | ) : null;
65 | }
66 |
--------------------------------------------------------------------------------
/src/components/atoms/PageFooter/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PageFooter';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/PageHero/PageHero.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Image from 'next/legacy/image';
3 | import { typesNextFillImage } from 'model/view/component/prop-types';
4 | import { Box } from '@mui/material';
5 | import { ButtonLink } from 'components/atoms';
6 |
7 | export default function PageHero({ img, button }) {
8 | return (
9 |
19 |
20 | {button && (
21 |
32 | {button.title}
33 |
34 | )}
35 |
36 | );
37 | }
38 | PageHero.propTypes = {
39 | img: typesNextFillImage.isRequired,
40 | button: PropTypes.shape(ButtonLink.propTypes),
41 | };
42 |
--------------------------------------------------------------------------------
/src/components/atoms/PageHero/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PageHero';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/PamphletParagraph/PamphletParagraph.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Image from 'next/image';
3 | import { Box, Divider, Typography } from '@mui/material';
4 | import { ButtonLink } from 'components/atoms';
5 | import { applyAlpha, designColor } from 'theme/palette';
6 | import { SoftWrap } from 'lib/format';
7 | import { typesNextImage } from 'model/view/component/prop-types';
8 |
9 | const DividerGrey = () => (
10 |
13 | );
14 |
15 | export default function PamphletParagraph({
16 | title,
17 | variant,
18 | img,
19 | body,
20 | button,
21 | hasDivider = false,
22 | sx = {},
23 | }) {
24 | return (
25 |
26 | {hasDivider && }
27 |
28 |
29 | {SoftWrap(title)}
30 |
31 |
32 | {img && (
33 |
34 |
35 |
36 | )}
37 |
38 | {body &&
39 | body.map((val, index) => (
40 |
45 | {val}
46 |
47 | ))}
48 |
49 | {button && (
50 |
51 |
57 | {button.title}
58 |
59 |
60 | )}
61 |
62 | );
63 | }
64 | PamphletParagraph.propTypes = {
65 | title: PropTypes.string.isRequired,
66 | variant: PropTypes.oneOf(['h1', 'h2', 'h3']).isRequired,
67 | img: typesNextImage,
68 | body: PropTypes.arrayOf(PropTypes.string),
69 | button: PropTypes.shape(ButtonLink.propTypes),
70 | hasDivider: PropTypes.bool,
71 | sx: PropTypes.object,
72 | };
73 |
74 | const sxParagraphMargin = {
75 | mb: 7,
76 | mx: { xs: 10, lg: 15, xl: 20 },
77 | };
78 | const sxTitleMargin = {
79 | mt: 7,
80 | mb: 7,
81 | mx: { xs: 10, lg: 15, xl: 20 },
82 | };
83 |
--------------------------------------------------------------------------------
/src/components/atoms/PamphletParagraph/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './PamphletParagraph';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/ParagraphCard/ParagraphCard.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Image from 'next/legacy/image';
3 | import { typesNextImage } from 'model/view/component/prop-types';
4 | import { Box, Typography, Card, CardContent, CardActions } from '@mui/material';
5 | import { ButtonLink } from 'components/atoms';
6 |
7 | export default function ParagraphCard({ variant, img, title, text, link }) {
8 | if (variant === 'h2') {
9 | return (
10 |
20 |
21 | {title}
22 |
23 |
24 |
25 |
26 |
27 |
28 |
35 |
39 | {title}
40 |
41 | {text}
42 |
48 | LEARN MORE
49 |
50 |
51 |
52 | );
53 | } else {
54 | return (
55 |
62 |
63 |
64 |
65 |
66 | {title}
67 |
68 | {text}
69 |
70 |
71 |
72 |
82 | LEARN MORE
83 |
84 |
85 |
86 | );
87 | }
88 | }
89 | ParagraphCard.propTypes = {
90 | variant: PropTypes.oneOf(['h2', 'h3']).isRequired,
91 | img: typesNextImage.isRequired,
92 | title: PropTypes.string.isRequired,
93 | text: PropTypes.string.isRequired,
94 | link: PropTypes.string.isRequired,
95 | };
96 |
--------------------------------------------------------------------------------
/src/components/atoms/ParagraphCard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './ParagraphCard';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/TitleCard/TitleCard.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {
3 | Card,
4 | CardContent,
5 | CardMedia,
6 | CardActionArea,
7 | Typography,
8 | } from '@mui/material';
9 | import { NextLink } from 'components/atoms';
10 |
11 | export default function TitleCard({ img, title, link }) {
12 | return (
13 |
20 |
31 |
40 |
41 |
42 | {title}
43 |
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | const imgShape = {
51 | src: PropTypes.string.isRequired,
52 | alt: PropTypes.string.isRequired,
53 | };
54 |
55 | TitleCard.propTypes = {
56 | img: PropTypes.shape(imgShape).isRequired,
57 | title: PropTypes.string.isRequired,
58 | link: PropTypes.string.isRequired,
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/atoms/TitleCard/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './TitleCard';
2 |
--------------------------------------------------------------------------------
/src/components/atoms/index.js:
--------------------------------------------------------------------------------
1 | export { default as ButtonLink } from './ButtonLink';
2 | export { default as FeedbackCard } from './FeedbackCard';
3 | export { default as MapToggle } from './MapToggle';
4 | export { default as NextLink } from './NextLink';
5 | export { default as PageFooter } from './PageFooter';
6 | export { default as PageHero } from './PageHero';
7 | export { default as PamphletParagraph } from './PamphletParagraph';
8 | export { default as ParagraphCard } from './ParagraphCard';
9 | export { default as TitleCard } from './TitleCard';
10 |
--------------------------------------------------------------------------------
/src/components/molecules/AppBar/AppBar.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Image from 'next/legacy/image';
3 |
4 | import {
5 | AppBar,
6 | Box,
7 | Drawer,
8 | IconButton,
9 | List,
10 | ListItem,
11 | ListItemIcon,
12 | Toolbar,
13 | Tooltip,
14 | Typography,
15 | } from '@mui/material';
16 |
17 | import { NextLink } from 'components/atoms';
18 |
19 | import { Menu as MenuIcon } from '@mui/icons-material';
20 |
21 | import {
22 | AboutIcon,
23 | ContactUsIcon,
24 | FridgeAddIcon,
25 | FridgeFindIcon,
26 | GetInvolvedIcon,
27 | GuidelineIcon,
28 | HomeIcon,
29 | } from 'theme/icons';
30 |
31 | const menuItems = [
32 | { icon: HomeIcon, title: 'Home', link: '/' },
33 | { icon: FridgeFindIcon, title: 'Find a Fridge', link: '/browse' },
34 | { icon: FridgeAddIcon, title: 'Add a Fridge', link: '/user/fridge/add' },
35 | { icon: AboutIcon, title: 'About', link: '/pamphlet/about' },
36 | {
37 | icon: GuidelineIcon,
38 | title: 'Best Practices',
39 | link: '/pamphlet/best-practices',
40 | },
41 | {
42 | icon: GetInvolvedIcon,
43 | title: 'Get Involved',
44 | link: '/pamphlet/get-involved',
45 | },
46 | { icon: ContactUsIcon, title: 'Contact Us', link: '/user/contact' },
47 | ];
48 | const menuDesktopFirstItem = 1;
49 | const sxDesktopIcon = {
50 | sx: { borderRadius: '50%', width: '48px', height: '48px' },
51 | };
52 | const sxMobileIcon = {
53 | sx: { borderRadius: '50%', width: '40px', height: '40px' },
54 | };
55 |
56 | export default function ResponsiveAppBar() {
57 | const [mobileOpen, setMobileOpen] = React.useState(false);
58 |
59 | const handleMobileMenuToggle = () => {
60 | setMobileOpen(!mobileOpen);
61 | };
62 |
63 | const MenuDesktop = () =>
64 | menuItems.slice(menuDesktopFirstItem).map((item) => (
65 |
66 |
76 | {item.icon(sxDesktopIcon)}
77 |
78 |
79 | ));
80 |
81 | const MenuMobile = () => (
82 |
83 | {menuItems.map((item) => (
84 |
85 | {item.icon(sxMobileIcon)}
86 |
92 | {item.title}
93 |
94 |
95 | ))}
96 |
97 | );
98 |
99 | return (
100 |
101 |
102 |
109 |
115 |
116 |
125 |
126 |
127 |
135 |
143 |
144 |
145 |
158 |
159 |
160 |
161 | );
162 | }
163 |
--------------------------------------------------------------------------------
/src/components/molecules/AppBar/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './AppBar';
2 |
--------------------------------------------------------------------------------
/src/components/molecules/Backtrack/Backtrack.jsx:
--------------------------------------------------------------------------------
1 | import { useRouter } from 'next/router';
2 | import { Box, Container, Typography } from '@mui/material';
3 | import { themeOptions } from '../../../theme/index';
4 | import { ArrowBack } from '@mui/icons-material';
5 |
6 | export default function Backtrack() {
7 | const router = useRouter();
8 | const { asPath } = useRouter();
9 |
10 | const prevLocation =
11 | asPath.includes('update') || asPath.includes('checkin') ? 'Fridge' : 'Map';
12 |
13 | return (
14 |
15 | router.back()}
20 | sx={{
21 | minWidth: '100%',
22 | height: '54px',
23 | paddingLeft: '17px',
24 | display: 'flex',
25 | alignItems: 'center',
26 | color: themeOptions.palette.text.secondary,
27 | ':hover': { cursor: 'pointer' },
28 | }}
29 | >
30 |
31 |
36 | {`Back to ${prevLocation}`}
37 |
38 |
39 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/molecules/Backtrack/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './Backtrack';
2 |
--------------------------------------------------------------------------------
/src/components/molecules/FridgeInformation/index.js:
--------------------------------------------------------------------------------
1 | export { default } from './FridgeInformation';
2 |
--------------------------------------------------------------------------------
/src/components/molecules/index.js:
--------------------------------------------------------------------------------
1 | export { default as AppBar } from './AppBar';
2 | export { default as Backtrack } from './Backtrack';
3 | export { default as FridgeInformation } from './FridgeInformation';
4 |
--------------------------------------------------------------------------------
/src/components/organisms/browse/List.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | import Image from 'next/legacy/image';
4 | import Link from 'next/link';
5 |
6 | import { Button, List, ListItem, Stack, Typography } from '@mui/material';
7 | import {
8 | CalendarMonthOutlined as CalendarIcon,
9 | Instagram as InstagramIcon,
10 | LocationOnOutlined as LocationOnOutlinedIcon,
11 | } from '@mui/icons-material';
12 |
13 | import typesView from 'model/view/prop-types';
14 |
15 | function Location({ location }) {
16 | return (
17 |
18 |
19 |
20 | {`${location.street} ${location.city}, ${location.state} ${location.zip}`}
21 |
22 |
23 | );
24 | }
25 | Location.propTypes = {
26 | location: typesView.Location,
27 | };
28 |
29 | function Instagram({ instagramUrl }) {
30 | const instagramRegex =
31 | /(?:(?:http|https):\/\/)?(?:www.)?(?:instagram.com|instagr.am|instagr.com)\/(\w+)/gim;
32 | const handle = instagramRegex.exec(instagramUrl);
33 | return (
34 |
35 |
36 |
37 | @{handle[1]}
38 |
39 |
40 | );
41 | }
42 | Instagram.propTypes = {
43 | instagramUrl: PropTypes.string.isRequired,
44 | };
45 |
46 | function LastUpdate({ date }) {
47 | return (
48 |
49 |
50 |
51 | Last Update: {date.toLocaleDateString()}
52 |
53 |
54 | );
55 | }
56 | LastUpdate.propTypes = {
57 | date: PropTypes.object.isRequired,
58 | };
59 |
60 | export default function FridgeList({ fridges }) {
61 | return (
62 |
63 | {fridges.map((fridge, fridgeIndex) => (
64 |
69 |
70 |
71 |
72 |
73 | {fridge.name}
74 |
75 |
76 | {fridge.maintainer?.instagram ? (
77 |
78 | ) : null}
79 | {fridge.report ? (
80 |
81 | ) : null}
82 |
83 | {fridge.photoUrl ? (
84 |
85 |
93 |
94 | ) : null}
95 |
96 |
103 | More Info
104 |
105 |
106 |
107 | ))}
108 |
109 | );
110 | }
111 | FridgeList.propTypes = {
112 | fridges: PropTypes.arrayOf(typesView.Fridge),
113 | };
114 |
--------------------------------------------------------------------------------
/src/components/organisms/browse/Map.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { MapContainer, TileLayer, useMap } from 'react-leaflet';
3 | import typesView from 'model/view/prop-types';
4 |
5 | import { deltaInMeters } from 'lib/geo.mjs';
6 |
7 | import LegendDrawer from './components/LegendDrawer';
8 | import MapMarkerList from './components/MapMarkerList';
9 | import markersFrom from './model/markersFrom';
10 |
11 | const fridgePaperBoyLoveGallery = [40.697759, -73.927282];
12 | const defaultZoom = 13.2;
13 | const defaultMapCenter = fridgePaperBoyLoveGallery;
14 |
15 | const lookupNearestFridge = (userLocation, fridgeList) => {
16 | fridgeList.map((fridge, index) => {
17 | const location = fridge.location;
18 | const { geoLat, geoLng } = location;
19 | const dist = deltaInMeters(
20 | [userLocation.lat, userLocation.lng],
21 | [geoLat, geoLng]
22 | );
23 | fridgeList[index].distFromUser = dist;
24 | fridgeList.sort((a, b) => {
25 | return a.distFromUser - b.distFromUser;
26 | });
27 | });
28 | return fridgeList[0];
29 | };
30 |
31 | function UpdateCenter({ fridgeList }) {
32 | const pixelRadius = 1000;
33 |
34 | const maxUserToDefaultCenterMeters = 200000;
35 | const map = useMap();
36 | map.locate().on('locationfound', (e) => {
37 | const userPosition = e.latlng;
38 | const userToDefaultCenterMeters = deltaInMeters(defaultMapCenter, [
39 | userPosition.lat,
40 | userPosition.lng,
41 | ]);
42 |
43 | if (userToDefaultCenterMeters <= maxUserToDefaultCenterMeters) {
44 | // Zoom level adjusted by 1/2 a level for each 50 KM
45 | const zoomAdjustment =
46 | Math.ceil(userToDefaultCenterMeters / 1000 / 50) * 0.5;
47 | map.flyTo(userPosition, defaultZoom - zoomAdjustment);
48 | } else {
49 | const nearestFridge = lookupNearestFridge(userPosition, fridgeList);
50 | const { geoLat, geoLng } = nearestFridge.location;
51 | const fridgeLatLng = {
52 | lat: geoLat,
53 | lng: geoLng,
54 | };
55 | const userFridgeBBox = L.latLngBounds(userPosition, fridgeLatLng);
56 | map.flyToBounds(userFridgeBBox);
57 | }
58 | L.circleMarker(userPosition, pixelRadius).addTo(map);
59 | });
60 | }
61 |
62 | export default function Map({ fridgeList }) {
63 | return (
64 | <>
65 |
72 |
73 |
79 |
80 |
81 |
82 | >
83 | );
84 | }
85 | Map.propTypes = {
86 | fridgeList: PropTypes.arrayOf(typesView.Fridge).isRequired,
87 | };
88 |
--------------------------------------------------------------------------------
/src/components/organisms/browse/components/MapMarkerList.jsx:
--------------------------------------------------------------------------------
1 | import Link from 'next/link';
2 | import { Marker, Popup } from 'react-leaflet';
3 | import { Typography } from '@mui/material';
4 |
5 | const LinkToFridge = (id, str) => {str};
6 |
7 | export default function MapMarkerList({ markerDataList }) {
8 | return markerDataList.map(({ marker, popup }, index) => {
9 | const {
10 | id,
11 | name: fridgeName,
12 | location: { street, city, state, zip },
13 | } = popup;
14 |
15 | return (
16 |
17 |
18 |
19 | {LinkToFridge(id, fridgeName)}
20 |
21 |
22 |
27 | {street}
28 |
29 | {city}, {state} {zip}
30 |
31 | {LinkToFridge(id, 'more info...')}
32 |
33 |
34 |
35 | );
36 | });
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/organisms/browse/components/SearchMap.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { useState } from 'react';
3 | import { Box, InputBase, IconButton } from '@mui/material';
4 | import {
5 | Search as SearchIcon,
6 | Cancel as CancelIcon,
7 | ArrowBackIosNew as ArrowBackIcon,
8 | } from '@mui/icons-material';
9 |
10 | import { applyAlpha, designColor } from 'theme/palette';
11 |
12 | const flexStyles = {
13 | display: 'flex',
14 | justifyContent: 'center',
15 | alignItems: 'center',
16 | px: 3,
17 | };
18 |
19 | export default function SearchMap({ setShowSearchMap }) {
20 | const [searchQuery, setSearchQuery] = useState('');
21 |
22 | function handleSearch(e) {
23 | e.preventDefault();
24 | }
25 |
26 | return (
27 |
38 | setShowSearchMap(false)}
41 | >
42 |
43 |
44 |
56 |
57 | setSearchQuery(e.target.value)}
61 | sx={{ mx: 1 }}
62 | fullWidth
63 | />
64 | {searchQuery.length > 0 && (
65 | setSearchQuery('')}
68 | sx={{ p: '5px' }}
69 | >
70 |
71 |
72 | )}
73 |
74 |
75 | );
76 | }
77 | SearchMap.propTypes = {
78 | setShowSearchMap: PropTypes.func,
79 | };
80 |
--------------------------------------------------------------------------------
/src/components/organisms/browse/model/markersFrom.js:
--------------------------------------------------------------------------------
1 | import Leaflet from 'leaflet';
2 | import { pinColor } from 'theme/palette';
3 | import {
4 | svgDecorationDirty,
5 | svgDecorationOutOfOrder,
6 | svgUrlPinGhost,
7 | svgUrlPinLocation,
8 | svgUrlPinNoReport,
9 | svgUrlPinNotAtLocation,
10 | } from 'theme/icons';
11 |
12 | export default function markersFrom(fridgeList) {
13 | return fridgeList.map((fridge) => {
14 | const { id, name, location, report } = fridge;
15 | const { condition, foodPercentage } = report ?? {
16 | condition: 'no report',
17 | foodPercentage: 0,
18 | };
19 |
20 | return {
21 | marker: {
22 | title: name,
23 | position: [location.geoLat, location.geoLng],
24 | icon: iconFrom(condition, foodPercentage),
25 | riseOnHover: true,
26 | riseOffset: 50,
27 | },
28 | popup: { id, name, location },
29 | };
30 | });
31 | }
32 |
33 | const colorFrom = Object.freeze({
34 | 0: pinColor.itemsEmpty,
35 | 1: pinColor.itemsFew,
36 | 2: pinColor.itemsMany,
37 | 3: pinColor.itemsFull,
38 | });
39 | const decorationFrom = Object.freeze({
40 | good: '',
41 | dirty: svgDecorationDirty,
42 | 'out of order': svgDecorationOutOfOrder,
43 | });
44 |
45 | const iconCache = {};
46 | function getLeafletIcon(hash, svgUrl) {
47 | let icon;
48 | if (hash in iconCache) {
49 | icon = iconCache[hash];
50 | } else {
51 | icon = new Leaflet.Icon({
52 | iconUrl: svgUrl,
53 | popupAnchor: [0, -24],
54 | iconSize: [40, 40],
55 | });
56 | iconCache[hash] = icon;
57 | }
58 | return icon;
59 | }
60 |
61 | function iconFrom(condition, foodPercentage) {
62 | switch (condition) {
63 | case 'not at location':
64 | return getLeafletIcon(condition, svgUrlPinNotAtLocation());
65 | case 'no report':
66 | return getLeafletIcon(condition, svgUrlPinNoReport());
67 | case 'ghost':
68 | return getLeafletIcon(condition, svgUrlPinGhost());
69 | default:
70 | return getLeafletIcon(
71 | condition + foodPercentage,
72 | svgUrlPinLocation(colorFrom[foodPercentage], decorationFrom[condition])
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/src/components/organisms/dialog/components/PanelConfirm.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Button,
3 | Stack,
4 | StepContent,
5 | StepLabel,
6 | Typography,
7 | } from '@mui/material';
8 | import typesValidation from './prop-types';
9 |
10 | export default function PanelConfirm({ buttonTitle, handleBack, handleNext }) {
11 | buttonTitle = buttonTitle.toLowerCase();
12 | return (
13 | <>
14 | Confirm
15 |
16 |
22 |
23 | Verify the details and click {buttonTitle} to confirm.
24 |
25 |
31 |
37 | {buttonTitle}
38 |
39 |
45 | Cancel
46 |
47 |
48 |
49 |
50 | >
51 | );
52 | }
53 | PanelConfirm.propTypes = typesValidation.PanelConfirm;
54 |
--------------------------------------------------------------------------------
/src/components/organisms/dialog/components/PanelFridge.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import { useFormik } from 'formik';
3 | import {
4 | Button,
5 | Stack,
6 | StepContent,
7 | StepLabel,
8 | TextField,
9 | } from '@mui/material';
10 | import typesValidation from './prop-types';
11 | import dialogValidation from 'model/view/dialog/yup';
12 |
13 | const panelFridgeValidation = dialogValidation.Fridge.pick(['name', 'notes']).concat(
14 | dialogValidation.Location.pick(['street', 'city', 'state', 'zip'])
15 | );
16 |
17 | const fridgeUrl = process.env.NEXT_PUBLIC_CFM_API_URL + '/v1/contact/';
18 |
19 | export default function PanelFridge({ handleBack, handleNext }) {
20 | const onSubmitFn = (values) => {
21 | console.error({ values })
22 | };
23 |
24 | const formik = useFormik({
25 | initialValues: {
26 | name: '',
27 | street: '',
28 | city: '',
29 | state: '',
30 | zip: '',
31 | notes: 'hello',
32 | },
33 | validationSchema: panelFridgeValidation,
34 | onSubmit: (values) => {
35 | console.error({ values })
36 | alert(JSON.stringify(values, null, 2));
37 | },
38 | });
39 |
40 | return (
41 | <>
42 | Fridge Location Information
43 |
44 |
120 |
121 | >
122 | );
123 | }
124 | PanelFridge.propTypes = typesValidation.Panel
125 |
--------------------------------------------------------------------------------
/src/components/organisms/dialog/components/PanelMaintainer.jsx:
--------------------------------------------------------------------------------
1 | import { useFormik } from 'formik';
2 | import {
3 | Button,
4 | Stack,
5 | StepContent,
6 | StepLabel,
7 | TextField,
8 | } from '@mui/material';
9 | import typesValidation from './prop-types';
10 |
11 | export default function PanelMaintainer(props) {
12 | const formik = useFormik({
13 | initialValues: {
14 | fullName: '',
15 | organization: '',
16 | phoneNumber: '',
17 | email: '',
18 | website: '',
19 | instagram: '',
20 | },
21 | onSubmit: (values) => {
22 | alert(JSON.stringify(values, null, 2));
23 | },
24 | });
25 | return (
26 | <>
27 | Maintainer Contact Information
28 |
29 |
30 |
37 |
44 |
51 |
58 |
65 |
72 |
78 |
84 | Continue
85 |
86 |
92 | Back
93 |
94 |
95 |
96 |
97 | >
98 | );
99 | }
100 | PanelMaintainer.propTypes = typesValidation.Panel;
101 |
--------------------------------------------------------------------------------
/src/components/organisms/dialog/components/PanelNotes.jsx:
--------------------------------------------------------------------------------
1 | import { typesPanel } from './prop-types';
2 | import { dialogReport } from 'model/view/dialog/yup';
3 | import { useFormik } from 'formik';
4 |
5 | import {
6 | Button,
7 | Stack,
8 | StepLabel,
9 | StepContent,
10 | TextField,
11 | } from '@mui/material';
12 |
13 | const dialogNotesValidation = dialogReport.pick(['notes']);
14 |
15 | export default function PanelNotes({ handleNext, handleBack, getPanelValues }) {
16 | const formik = useFormik({
17 | initialValues: {
18 | notes: '',
19 | },
20 | validationSchema: dialogNotesValidation,
21 | onSubmit: (values) => {
22 | getPanelValues(values);
23 | handleNext();
24 | },
25 | });
26 |
27 | return (
28 | <>
29 | Notes
30 |
31 |
77 |
78 | >
79 | );
80 | }
81 | PanelNotes.propTypes = typesPanel.isRequired;
82 |
--------------------------------------------------------------------------------
/src/components/organisms/dialog/components/PanelReport.jsx:
--------------------------------------------------------------------------------
1 | import { typesPanel } from './prop-types';
2 | import { dialogReport } from 'model/view/dialog/yup';
3 | import { useFormik } from 'formik';
4 |
5 | import {
6 | Button,
7 | FormControl,
8 | FormControlLabel,
9 | FormGroup,
10 | FormLabel,
11 | RadioGroup,
12 | Slider,
13 | Radio,
14 | Stack,
15 | StepContent,
16 | StepLabel,
17 | } from '@mui/material';
18 |
19 | const panelReportValidation = dialogReport.pick([
20 | 'foodPercentage',
21 | 'condition',
22 | ]);
23 |
24 | export default function PanelReport({
25 | handleNext,
26 | handleBack,
27 | getPanelValues,
28 | }) {
29 | //Functionality for MUI slider component
30 | const sliderMarks = [
31 | {
32 | value: 0,
33 | label: 'Empty',
34 | },
35 | {
36 | value: 33,
37 | label: 'A Few Items',
38 | },
39 | {
40 | value: 67,
41 | label: 'Many Items',
42 | },
43 | {
44 | value: 100,
45 | label: 'Full',
46 | },
47 | ];
48 |
49 | const fridgeSliderStyles = {
50 | mx: 'auto',
51 | width: 7 / 8,
52 | '.MuiSlider-markLabel': {
53 | fontSize: 12,
54 | // color: theme.palette.text.secondary,
55 | },
56 | '.MuiSlider-markLabelActive': {
57 | fontSize: 12,
58 | // color: theme.palette.text.primary,
59 | },
60 | };
61 |
62 | const formik = useFormik({
63 | initialValues: {
64 | foodPercentage: 0,
65 | condition: 'good',
66 | },
67 | // validationSchema: panelReportValidation,
68 | onSubmit: (values) => {
69 | getPanelValues(values);
70 | handleNext();
71 | },
72 | });
73 |
74 | return (
75 | <>
76 | Status
77 |
78 |
152 |
153 | >
154 | );
155 | }
156 | PanelReport.propTypes = typesPanel.isRequired;
157 |
--------------------------------------------------------------------------------
/src/components/organisms/index.js:
--------------------------------------------------------------------------------
1 | export { default as DialogUpdateFridgeStatus } from './DialogUpdateFridgeStatus';
2 | export { default as BrowseList } from './browse/List';
3 | export { default as BrowseMap } from './browse/Map';
4 |
--------------------------------------------------------------------------------
/src/lib/analytics.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Analytics tracking.
3 | * @module lib/analytics
4 | *
5 | * From https://github.com/vercel/next.js/tree/canary/examples/with-google-analytics
6 | */
7 |
8 | /**
9 | * @constant {string} TRACKING_ID - Analytics tracking id.
10 | *
11 | * Local, Staging, and Production each have their own id. Therefore the id is set in the envirnoment configurion.
12 | */
13 | const TRACKING_ID = process.env.NEXT_PUBLIC_ANALYTICS_ID;
14 |
15 | /**
16 | * Track a view for the specified URL.
17 | * @param {string} url - The url of the page.
18 | */
19 | const view = (url) => {
20 | window.gtag('config', TRACKING_ID, { page_path: url });
21 | };
22 |
23 | /**
24 | * Track a specific event.
25 | * @param {string} type - The type of event, such as a Google Ads conversion event or a Google Analytics 4 event
26 | * @param {string} parameters - Object of name/value pairs that describes the event
27 | */
28 | const event = (type, parameters) => {
29 | window.gtag('event', type, parameters);
30 | };
31 |
32 | const GoogleAnalytics = { TRACKING_ID, view, event };
33 |
34 | export default GoogleAnalytics;
35 |
--------------------------------------------------------------------------------
/src/lib/browser.js:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from 'react';
2 |
3 | export function useWindowHeight() {
4 | const [availableHeight, setAvailableHeight] = useState(0);
5 | const calculateAvailableHeight = () =>
6 | window.innerHeight - document.getElementById('AppBar').offsetHeight;
7 |
8 | useEffect(() => {
9 | function handleResize() {
10 | setAvailableHeight(calculateAvailableHeight());
11 | }
12 |
13 | setAvailableHeight(calculateAvailableHeight());
14 |
15 | window.addEventListener('resize', handleResize);
16 | return () => window.removeEventListener('resize', handleResize);
17 | }, []);
18 |
19 | return availableHeight;
20 | }
21 |
22 | export function geolocation() {
23 | return new Promise((resolve, reject) => {
24 | if (location.protocol === 'https:' && navigator.geolocation) {
25 | navigator.geolocation.getCurrentPosition((position) => {
26 | if (position.coords) {
27 | resolve({
28 | lat: position.coords.latitude,
29 | lng: position.coords.longitude,
30 | });
31 | } else {
32 | reject(new Error('browser did not return coordinates'));
33 | }
34 | });
35 | } else {
36 | reject(new Error('browser does not support geolocation api'));
37 | }
38 | });
39 | }
40 |
--------------------------------------------------------------------------------
/src/lib/createEmotionCache.js:
--------------------------------------------------------------------------------
1 | import createCache from '@emotion/cache';
2 |
3 | const createEmotionCache = () => {
4 | return createCache({ key: 'css', prepend: true });
5 | };
6 |
7 | export default createEmotionCache;
8 |
--------------------------------------------------------------------------------
/src/lib/data.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Simple object check.
3 | * @param item
4 | * @returns {boolean}
5 | *
6 | * From https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
7 | * By https://stackoverflow.com/users/2938161/salakar
8 | */
9 | export function isObject(item) {
10 | return item && typeof item === 'object' && !Array.isArray(item);
11 | }
12 |
13 | /**
14 | * Deep merge two objects.
15 | * @param target
16 | * @param ...sources
17 | *
18 | * From https://stackoverflow.com/questions/27936772/how-to-deep-merge-instead-of-shallow-merge
19 | * By https://stackoverflow.com/users/2938161/salakar
20 | */
21 | export function mergeDeep(target, ...sources) {
22 | if (!sources.length) return target;
23 | const source = sources.shift();
24 |
25 | if (isObject(target) && isObject(source)) {
26 | for (const key in source) {
27 | if (isObject(source[key])) {
28 | if (!target[key]) Object.assign(target, { [key]: {} });
29 | mergeDeep(target[key], source[key]);
30 | } else {
31 | Object.assign(target, { [key]: source[key] });
32 | }
33 | }
34 | }
35 |
36 | return mergeDeep(target, ...sources);
37 | }
38 |
--------------------------------------------------------------------------------
/src/lib/format.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | /**
4 | * Text and HTML formatting.
5 | * @module lib/format
6 | */
7 |
8 | const isOdd = (x) => x % 2 === 1;
9 |
10 | /**
11 | * Soft wrap every sentence so it wraps at the period and not in the middle.
12 | * @param paragraph Sentences seperated by punctuation such as ".!?:;" and others used by foreign languages
13 | */
14 | const regxSentence = /(\p{Terminal_Punctuation})/gu;
15 | const shortSentenceLength = 5;
16 | export function SoftWrap(paragraph) {
17 | const chunks = paragraph.split(regxSentence);
18 |
19 | if (isOdd(chunks.length)) {
20 | chunks.push('');
21 | }
22 |
23 | let sentence = '';
24 | const lines = [];
25 | for (let ix = 0; ix < chunks.length; ix += 2) {
26 | sentence += chunks[ix] + chunks[ix + 1];
27 | if (sentence.length > shortSentenceLength) {
28 | lines.push(sentence);
29 | sentence = '';
30 | }
31 | }
32 |
33 | // the last sentence goes on a line regardless of length
34 | if (sentence !== '') {
35 | lines.push(sentence);
36 | }
37 |
38 | return lines.length === 1
39 | ? lines[0]
40 | : lines.map((line, ix) => (
41 |
46 | {line}
47 |
48 | ));
49 | }
50 | SoftWrap.propTypes = {
51 | paragraph: PropTypes.string.isRequired,
52 | };
53 |
--------------------------------------------------------------------------------
/src/lib/format.test.js:
--------------------------------------------------------------------------------
1 | import { SoftWrap } from './format';
2 | import { screen, render } from '@testing-library/react';
3 | import '@testing-library/jest-dom';
4 |
5 | describe('Does the function wrap each sentence in a paragraph with a span?', function () {
6 | it('Does not wrap a lone sentence with a span', function () {
7 | const paragraph = 'Take what you need.';
8 | render(SoftWrap(paragraph));
9 |
10 | expect(screen.queryByTestId('span')).not.toBeInTheDocument();
11 | });
12 |
13 | it('Does not wrap a lone, bulleted sentence with a span', function () {
14 | const paragraph = '1. Take what you need.';
15 | render(SoftWrap(paragraph));
16 |
17 | expect(screen.queryByTestId('span')).not.toBeInTheDocument();
18 | });
19 |
20 | it('Wraps Sentences ending in .!?:;。 with a span', function () {
21 | const paragraph =
22 | 'Take what you need. Leave what you can: Toma lo que necesitas! Deja lo que puedas? 拿走你需要的食物。把不需要的食物留下。';
23 | render(SoftWrap(paragraph));
24 |
25 | expect(screen.getByText('Take what you need.')).toBeInTheDocument();
26 | expect(screen.getByText('Leave what you can:')).toBeInTheDocument();
27 | expect(screen.getByText('Toma lo que necesitas!')).toBeInTheDocument();
28 | expect(screen.getByText('Deja lo que puedas?')).toBeInTheDocument();
29 | expect(screen.getByText('拿走你需要的食物。')).toBeInTheDocument();
30 | expect(screen.getByText('把不需要的食物留下。')).toBeInTheDocument();
31 | expect(
32 | screen.queryByText('Take what you need. Leave what you can.')
33 | ).not.toBeInTheDocument();
34 | expect(
35 | screen.queryByText(
36 | 'Take what you need. Leave what you can: Toma lo que necesitas! Deja lo que puedas? 拿走你需要的食物。把不需要的食物留下。'
37 | )
38 | ).not.toBeInTheDocument();
39 | });
40 |
41 | it('Wraps correctly when the string ends without punctuation', function () {
42 | const paragraph =
43 | 'Technology Empowers Us. 科技赋予我们力量。La Tecnología Nos Da Poder';
44 | render(SoftWrap(paragraph));
45 |
46 | expect(screen.getByText('Technology Empowers Us.')).toBeInTheDocument();
47 | expect(screen.getByText('科技赋予我们力量。')).toBeInTheDocument();
48 | expect(screen.getByText('La Tecnología Nos Da Poder')).toBeInTheDocument();
49 | expect(
50 | screen.queryByText('科技赋予我们力量。 La Tecnología Nos Da Poder')
51 | ).not.toBeInTheDocument();
52 | expect(
53 | screen.queryByText(
54 | 'Technology Empowers Us. 科技赋予我们力量。 La Tecnología Nos Da Poder'
55 | )
56 | ).not.toBeInTheDocument();
57 | });
58 |
59 | it('Puts numbered bullets together with the sentence that follows it', function () {
60 | const paragraph =
61 | '1. Read Best Practices. Leer Mejores Prácticas. 参与其中。';
62 | render(SoftWrap(paragraph));
63 |
64 | expect(screen.getByText('1. Read Best Practices.')).toBeInTheDocument();
65 | expect(screen.getByText('Leer Mejores Prácticas.')).toBeInTheDocument();
66 | expect(screen.getByText('参与其中。')).toBeInTheDocument();
67 | expect(screen.queryByText('1.')).not.toBeInTheDocument();
68 | expect(screen.queryByText('Read Best Practices.')).not.toBeInTheDocument();
69 | expect(
70 | screen.queryByText(
71 | '1. Read Best Practices. Leer Mejores Prácticas. 参与其中。'
72 | )
73 | ).not.toBeInTheDocument();
74 | });
75 |
76 | it('Handles bullets like this 1:', function () {
77 | const paragraph =
78 | '1: About Community Fridges. Sobre Refrigeradores Comunitarios. 关于社区冰箱';
79 | render(SoftWrap(paragraph));
80 |
81 | expect(screen.getByText('1: About Community Fridges.')).toBeInTheDocument();
82 | expect(
83 | screen.getByText('Sobre Refrigeradores Comunitarios.')
84 | ).toBeInTheDocument();
85 | expect(screen.getByText('关于社区冰箱')).toBeInTheDocument();
86 | expect(screen.queryByText('1:')).not.toBeInTheDocument();
87 | expect(
88 | screen.queryByText('About Community Fridges.')
89 | ).not.toBeInTheDocument();
90 | });
91 | });
92 |
--------------------------------------------------------------------------------
/src/lib/geo.mjs:
--------------------------------------------------------------------------------
1 | export function groupWithinBound(boundInMeters, geoList) {
2 | if (geoList.length <= 1) {
3 | return [];
4 | }
5 |
6 | const origin = [geoList[0].lat, geoList[0].lng];
7 | const insideBound = [geoList[0]],
8 | outsideBound = [];
9 |
10 | for (let ix = 1; ix < geoList.length; ++ix) {
11 | const destination = [geoList[ix].lat, geoList[ix].lng];
12 | if (deltaInMeters(origin, destination) <= boundInMeters) {
13 | insideBound.push(geoList[ix]);
14 | } else {
15 | outsideBound.push(geoList[ix]);
16 | }
17 | }
18 |
19 | return insideBound.length > 1
20 | ? [insideBound].concat(groupWithinBound(boundInMeters, outsideBound))
21 | : groupWithinBound(boundInMeters, outsideBound);
22 | }
23 |
24 | /**
25 | * Distance in meters between two geo coordinates
26 | *
27 | * From https://stackoverflow.com/questions/43167417/calculate-distance-between-two-points-in-leaflet
28 | * By https://stackoverflow.com/users/4496505/gaurav-mukherjee
29 | */
30 | export function deltaInMeters(origin, destination) {
31 | const lon1 = toRadian(origin[1]),
32 | lat1 = toRadian(origin[0]),
33 | lon2 = toRadian(destination[1]),
34 | lat2 = toRadian(destination[0]);
35 |
36 | const deltaLat = lat2 - lat1;
37 | const deltaLon = lon2 - lon1;
38 |
39 | const a =
40 | Math.pow(Math.sin(deltaLat / 2), 2) +
41 | Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(deltaLon / 2), 2);
42 | const c = 2 * Math.asin(Math.sqrt(a));
43 | const EARTH_RADIUS = 6371;
44 | return c * EARTH_RADIUS * 1000;
45 | }
46 |
47 | function toRadian(degree) {
48 | return (degree * Math.PI) / 180;
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/search.mjs:
--------------------------------------------------------------------------------
1 | const rxPunctuationAndDigits = /[.,\/#!$%\^&\*;:{}=\-_`~()\'\d]/g;
2 |
3 | export function wordRank(sentences, excludedWords) {
4 | const words = sentences
5 | .join(' ')
6 | .replaceAll(rxPunctuationAndDigits, '')
7 | .split(/\s/)
8 | .map((word) => word.toLowerCase())
9 | .filter((word) => word.length > 2)
10 | .filter((word) => !excludedWords.includes(word));
11 |
12 | const wordCounts = {};
13 | words.forEach((word) => (wordCounts[word] = 1 + (wordCounts[word] ?? 0)));
14 | return wordCounts;
15 | }
16 |
--------------------------------------------------------------------------------
/src/model/data/fridge/prop-types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | export const typeTag = PropTypes.string.isRequired;
4 |
5 | export const fieldsLocation = {
6 | name: PropTypes.string,
7 | street: PropTypes.string.isRequired,
8 | city: PropTypes.string.isRequired,
9 | state: PropTypes.string.isRequired,
10 | zip: PropTypes.string.isRequired,
11 | geoLat: PropTypes.number.isRequired,
12 | geoLng: PropTypes.number.isRequired,
13 | };
14 |
15 | export const fieldsMaintainer = {
16 | name: PropTypes.string,
17 | email: PropTypes.string,
18 | organization: PropTypes.string,
19 | phone: PropTypes.string,
20 | website: PropTypes.string,
21 | instagram: PropTypes.string,
22 | };
23 |
24 | export const fieldsFridge = {
25 | id: PropTypes.string.isRequired,
26 | name: PropTypes.string.isRequired,
27 | location: PropTypes.exact(fieldsLocation).isRequired,
28 | tags: PropTypes.arrayOf(typeTag),
29 | maintainer: PropTypes.exact(fieldsMaintainer),
30 | photoUrl: PropTypes.string,
31 | notes: PropTypes.string,
32 | verified: PropTypes.bool,
33 | };
34 |
35 | export const typeCondition = PropTypes.oneOf([
36 | 'good',
37 | 'dirty',
38 | 'out of order',
39 | 'not at location',
40 | 'ghost',
41 | ]);
42 | export const typeFoodPercentage = PropTypes.oneOf([0, 1, 2, 3]);
43 |
44 | export const fieldsReport = {
45 | timestamp: PropTypes.object.isRequired,
46 | condition: typeCondition.isRequired,
47 | foodPercentage: typeFoodPercentage.isRequired,
48 | photoUrl: PropTypes.string,
49 | notes: PropTypes.string,
50 | };
51 |
--------------------------------------------------------------------------------
/src/model/data/fridge/yup.mjs:
--------------------------------------------------------------------------------
1 | import { array, boolean, date, number, object, string } from 'yup';
2 |
3 | // fridge database records
4 | export const ValuesTag = string().max(140).trim().required();
5 | export const ValuesTags = array().of(ValuesTag).nullable();
6 |
7 | export const ValuesLocation = object({
8 | name: string().max(70).trim().optional(),
9 | street: string().max(55).trim().required(),
10 | city: string().max(35).trim().required(),
11 | state: string().length(2).uppercase().required(),
12 | zip: string()
13 | .matches(/(^\d{5}$)|(^\d{5}-\d{4}$)/)
14 | .required(),
15 | geoLat: number().required(),
16 | geoLng: number().required(),
17 | });
18 |
19 | export const ValuesMaintainer = object({
20 | name: string().max(70).trim().optional(),
21 | email: string().email().lowercase().optional(),
22 | organization: string().max(80).trim().optional(),
23 | phone: string()
24 | .matches(/^\(\d{3}\) \d{3}-\d{4}$/)
25 | .optional(),
26 | website: string().url().optional(),
27 | instagram: string().url().optional(),
28 | }).nullable();
29 |
30 | export const ValuesFridge = object({
31 | id: string().min(4).max(60).required(),
32 | name: string().min(4).max(60).trim().required(),
33 | location: ValuesLocation.required(),
34 | tags: ValuesTag.optional(),
35 | maintainer: ValuesMaintainer.optional(),
36 | photoUrl: string().url().optional(),
37 | notes: string().min(1).max(700).trim().optional(),
38 | verified: boolean().default(false),
39 | });
40 |
41 | export const ValuesReport = object({
42 | timestamp: date().required(),
43 | condition: string()
44 | .oneOf(['good', 'dirty', 'out of order', 'not at location', 'ghost'])
45 | .required(),
46 | foodPercentage: number().integer().oneOf([0, 1, 2, 3]).required(),
47 | photoUrl: string().url().optional(),
48 | notes: string().min(0).max(300).trim().optional(),
49 | });
50 |
51 | // website contact form data
52 | export const ValuesContact = object({
53 | name: string().max(70).trim().required(),
54 | email: string().email().required(),
55 | subject: string().max(70).trim().required(),
56 | message: string().max(2048).trim().required(),
57 | });
58 |
59 | const valuesDataFridge = {
60 | Contact: ValuesContact,
61 | Fridge: ValuesFridge,
62 | Location: ValuesLocation,
63 | Report: ValuesReport,
64 | Tag: ValuesTag,
65 | Tags: ValuesTags,
66 | };
67 | export default valuesDataFridge;
68 |
--------------------------------------------------------------------------------
/src/model/view/component/prop-types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 |
3 | /**
4 | * next/image props
5 | *
6 | * From https://nextjs.org/docs/api-reference/next/image
7 | * By https://github.com/bernardm
8 | */
9 | export const typesNextImage = PropTypes.exact({
10 | src: PropTypes.string.isRequired,
11 | alt: PropTypes.string.isRequired,
12 | width: PropTypes.number.isRequired,
13 | height: PropTypes.number.isRequired,
14 | layout: PropTypes.oneOf(['intrinsic', 'fixed', 'responsive']),
15 | });
16 | export const typesNextFillImage = PropTypes.exact({
17 | src: PropTypes.string.isRequired,
18 | alt: PropTypes.string.isRequired,
19 | });
20 |
21 | /**
22 | * Formik props
23 | *
24 | * From https://jaredpalmer.com/formik/docs/api/formik#formik-render-methods-and-props
25 | * By https://github.com/ClementParis016
26 | */
27 | export const typesFormik = PropTypes.exact({
28 | dirty: PropTypes.bool.isRequired,
29 | errors: PropTypes.object.isRequired,
30 | handleBlur: PropTypes.func.isRequired,
31 | handleChange: PropTypes.func.isRequired,
32 | handleReset: PropTypes.func.isRequired,
33 | handleSubmit: PropTypes.func.isRequired,
34 | isSubmitting: PropTypes.bool.isRequired,
35 | isValid: PropTypes.bool.isRequired,
36 | isValidating: PropTypes.bool.isRequired,
37 | resetForm: PropTypes.func.isRequired,
38 | setErrors: PropTypes.func.isRequired,
39 | setFieldError: PropTypes.func.isRequired,
40 | setFieldTouched: PropTypes.func.isRequired,
41 | submitForm: PropTypes.func.isRequired,
42 | submitCount: PropTypes.number.isRequired,
43 | setFieldValue: PropTypes.func.isRequired,
44 | setStatus: PropTypes.func.isRequired,
45 | setSubmitting: PropTypes.func.isRequired,
46 | setTouched: PropTypes.func.isRequired,
47 | setValues: PropTypes.func.isRequired,
48 | status: PropTypes.any,
49 | touched: PropTypes.object.isRequired,
50 | values: PropTypes.object.isRequired,
51 | validateForm: PropTypes.func.isRequired,
52 | validateField: PropTypes.func.isRequired,
53 | });
54 |
55 | const typesViewComponent = {
56 | Formik: typesFormik,
57 | NextImage: typesNextImage,
58 | NextFillImage: typesNextFillImage,
59 | };
60 | export default typesViewComponent;
61 |
--------------------------------------------------------------------------------
/src/model/view/dialog/yup.js:
--------------------------------------------------------------------------------
1 | import apiFridge from 'model/data/fridge/yup.mjs';
2 |
3 | export const dialogContact = apiFridge.Contact;
4 |
5 | export const dialogLocation = apiFridge.Location.omit(['geoLat', 'geoLng']);
6 |
7 | export const dialogFridge = apiFridge.Fridge.omit([
8 | 'id',
9 | 'verified',
10 | 'tags',
11 | 'location',
12 | ]).shape({
13 | location: dialogLocation.required(),
14 | });
15 |
16 | export const dialogReport = apiFridge.Report.omit(['timestamp']);
17 |
18 | const dialogDataValidation = {
19 | Contact: dialogContact,
20 | Fridge: dialogFridge,
21 | Location: dialogLocation,
22 | Report: dialogReport,
23 | };
24 | export default dialogDataValidation;
25 |
--------------------------------------------------------------------------------
/src/model/view/index.js:
--------------------------------------------------------------------------------
1 | import { ValuesFridge, ValuesReport } from 'model/data/fridge/yup.mjs';
2 |
3 | // in memory cache
4 | const cacheViewFridgeList = [];
5 | const viewFridgeFor = {};
6 |
7 | export async function getFridgeList() {
8 | if (cacheViewFridgeList.length === 0) {
9 | await fetchAllData();
10 | }
11 | return cacheViewFridgeList;
12 | }
13 |
14 | const sortByNameAsc = (a, b) => {
15 | const nameA = a.name;
16 | const nameB = b.name;
17 | if (nameA < nameB) {
18 | return -1;
19 | }
20 | if (nameA > nameB) {
21 | return 1;
22 | }
23 | return 0;
24 | };
25 |
26 | const castOptions = Object.freeze({ stripUnknown: true }); // yup configuration
27 |
28 | function viewFridgeFromLocal(apiFridge) {
29 | const viewFridge = ValuesFridge.cast(apiFridge, castOptions);
30 | viewFridge['report'] = null;
31 | return viewFridge;
32 | }
33 |
34 | function viewFridgeFromRemote(apiFridge) {
35 | const report = apiFridge.latestFridgeReport ?? null;
36 | const viewFridge = ValuesFridge.cast(apiFridge, castOptions);
37 | viewFridge['report'] = ValuesReport.cast(report, castOptions);
38 | return viewFridge;
39 | }
40 |
41 | function loadIntoCache({ fridges }, fnConverter) {
42 | cacheViewFridgeList.length = fridges.length;
43 |
44 | for (let n = 0; n < fridges.length; n++) {
45 | const apiFridge = fridges[n];
46 | viewFridgeFor[apiFridge.id] = cacheViewFridgeList[n] =
47 | fnConverter(apiFridge);
48 | }
49 | cacheViewFridgeList.sort(sortByNameAsc);
50 | }
51 |
52 | function mergeIntoCache({ reports }) {
53 | for (const apiReport of reports) {
54 | viewFridgeFor[apiReport.fridgeId].report = Object.freeze(
55 | ValuesReport.cast(apiReport, castOptions)
56 | );
57 | }
58 | }
59 |
60 | const apiFridges = `${process.env.NEXT_PUBLIC_FF_API_URL}/v1/fridges/`;
61 | const apiReports = `${process.env.NEXT_PUBLIC_FF_API_URL}/v1/reports/`;
62 | const apiHeader = { headers: { Accept: 'application/json' } };
63 |
64 | async function fetchAllData() {
65 | if (process.env.NEXT_PUBLIC_FLAG_useLocalDatabase) {
66 | await fetchAllLocalData();
67 | } else {
68 | await fetchAllServerData();
69 | }
70 | }
71 |
72 | function fetchAllServerData() {
73 | return fetch(apiFridges, apiHeader)
74 | .then((response) => {
75 | if (!response.ok) {
76 | throw `ERROR ${response.url} ${response.status}: ${response.statusText}`;
77 | }
78 | return response.json();
79 | })
80 | .then((fridges) => loadIntoCache({ fridges }, viewFridgeFromRemote))
81 | .catch((error) => console.error(error));
82 | }
83 |
84 | async function fetchAllLocalData() {
85 | const responses = await Promise.all([
86 | fetch(apiFridges, apiHeader),
87 | fetch(apiReports, apiHeader),
88 | ]);
89 | let fetchOK = true;
90 | for (const response of responses) {
91 | if (!response.ok) {
92 | fetchOK = false;
93 | console.error(
94 | `ERROR ${response.url} ${response.status}: ${response.statusText}`
95 | );
96 | }
97 | }
98 | if (fetchOK) {
99 | const [fridges, reports] = await Promise.all(
100 | responses.map((response) => response.json())
101 | );
102 | loadIntoCache({ fridges }, viewFridgeFromLocal);
103 | mergeIntoCache({ reports });
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/src/model/view/prop-types.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import {
3 | fieldsFridge,
4 | fieldsLocation,
5 | fieldsReport,
6 | } from 'model/data/fridge/prop-types';
7 |
8 | export const typesLocation = PropTypes.exact(fieldsLocation);
9 |
10 | export const typesFridge = PropTypes.exact({
11 | ...fieldsFridge,
12 | report: PropTypes.exact(fieldsReport),
13 | });
14 |
15 | export const typesGeolocation = PropTypes.exact({
16 | geoLat: PropTypes.number.isRequired,
17 | geoLng: PropTypes.number.isRequired,
18 | });
19 |
20 | const typesView = {
21 | Fridge: typesFridge,
22 | Location: typesLocation,
23 | Geolocation: typesGeolocation,
24 | };
25 | export default typesView;
26 |
--------------------------------------------------------------------------------
/src/pages/_app.js:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Head from 'next/head';
3 | import Script from 'next/script';
4 | import { useRouter } from 'next/router';
5 | import { useEffect } from 'react';
6 | import { CacheProvider } from '@emotion/react';
7 | import CssBaseline from '@mui/material/CssBaseline';
8 | import { ThemeProvider } from '@mui/material/styles';
9 |
10 | import AppBar from 'components/molecules/AppBar';
11 |
12 | import createEmotionCache from 'lib/createEmotionCache';
13 | import GoogleAnalytics from 'lib/analytics';
14 | import theme from 'theme';
15 |
16 | const clientSideEmotionCache = createEmotionCache();
17 |
18 | export default function MyApp({
19 | Component,
20 | emotionCache = clientSideEmotionCache,
21 | pageProps,
22 | }) {
23 | const router = useRouter();
24 |
25 | useEffect(() => {
26 | const handleRouteChange = (url) => {
27 | GoogleAnalytics.view(url);
28 | };
29 | router.events.on('routeChangeComplete', handleRouteChange);
30 | return () => {
31 | router.events.off('routeChangeComplete', handleRouteChange);
32 | };
33 | }, [router.events]);
34 |
35 | return (
36 | <>
37 |
38 |
39 |
40 | {/* Global Site Tag (gtag.js) - Google Analytics */}
41 |
46 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 | >
64 | );
65 | }
66 | MyApp.propTypes = {
67 | Component: PropTypes.elementType.isRequired,
68 | emotionCache: PropTypes.object,
69 | pageProps: PropTypes.object.isRequired,
70 | };
71 |
--------------------------------------------------------------------------------
/src/pages/_document.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Document, { Html, Head, Main, NextScript } from 'next/document';
3 | import createEmotionServer from '@emotion/server/create-instance';
4 | import createEmotionCache from 'lib/createEmotionCache';
5 |
6 | export default class MyDocument extends Document {
7 | render() {
8 | return (
9 |
10 |
11 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | );
26 | }
27 | }
28 |
29 | // We are using the same emotion cache for all the SSR requests to speed up performance.
30 | // This can have global side effects, so disable this if there are rendering issues.
31 | const ssrEmotionCache = createEmotionCache();
32 |
33 | // `getInitialProps` belongs to `_document` (instead of `_app`),
34 | // it's compatible with static-site generation (SSG).
35 | MyDocument.getInitialProps = async (ctx) => {
36 | // Resolution order
37 | //
38 | // On the server:
39 | // 1. app.getInitialProps
40 | // 2. page.getInitialProps
41 | // 3. document.getInitialProps
42 | // 4. app.render
43 | // 5. page.render
44 | // 6. document.render
45 | //
46 | // On the server with error:
47 | // 1. document.getInitialProps
48 | // 2. app.render
49 | // 3. page.render
50 | // 4. document.render
51 | //
52 | // On the client
53 | // 1. app.getInitialProps
54 | // 2. page.getInitialProps
55 | // 3. app.render
56 | // 4. page.render
57 |
58 | const originalRenderPage = ctx.renderPage;
59 | const { extractCriticalToChunks } = createEmotionServer(ssrEmotionCache);
60 |
61 | /* eslint-disable */
62 | ctx.renderPage = () =>
63 | originalRenderPage({
64 | enhanceApp: (App) =>
65 | function EnhanceApp(props) {
66 | return ;
67 | },
68 | });
69 | /* eslint-enable */
70 |
71 | const initialProps = await Document.getInitialProps(ctx);
72 | // This is important. It prevents emotion to render invalid HTML.
73 | // See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153
74 | const emotionStyles = extractCriticalToChunks(initialProps.html);
75 | const emotionStyleTags = emotionStyles.styles.map((style) => (
76 |
82 | ));
83 |
84 | return {
85 | ...initialProps,
86 | // Styles fragment is rendered after the app and page rendering finish.
87 | styles: [
88 | ...React.Children.toArray(initialProps.styles),
89 | ...emotionStyleTags,
90 | ],
91 | };
92 | };
93 |
--------------------------------------------------------------------------------
/src/pages/browse.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 | import Head from 'next/head';
3 | import dynamic from 'next/dynamic';
4 |
5 | import {
6 | CircularProgress,
7 | Box,
8 | Typography,
9 | Divider,
10 | useMediaQuery,
11 | } from '@mui/material';
12 | import BrowseList from 'components/organisms/browse/List';
13 | import { MapToggle } from 'components/atoms/';
14 |
15 | import { getFridgeList } from 'model/view';
16 | import { useWindowHeight } from 'lib/browser';
17 |
18 | const DynamicMap = dynamic(
19 | () => {
20 | return import('../components/organisms/browse/Map');
21 | },
22 | { ssr: false }
23 | );
24 | const BrowseMap = (props) => ;
25 |
26 | const ProgressIndicator = (
27 |
35 |
36 |
37 | );
38 |
39 | let fridgeList = null;
40 | export default function BrowsePage() {
41 | const [hasDataLoaded, setHasDataLoaded] = useState(false);
42 | const [currentView, setCurrentView] = useState(MapToggle.view.map);
43 |
44 | const availableHeight = useWindowHeight();
45 | const isWindowDesktop = useMediaQuery((theme) => theme.breakpoints.up('md'));
46 |
47 | useEffect(() => {
48 | const fetchData = async () => {
49 | fridgeList = await getFridgeList();
50 | setHasDataLoaded(true);
51 | };
52 | fetchData().catch(console.error);
53 | }, []);
54 |
55 | const Map = hasDataLoaded
56 | ? BrowseMap({
57 | fridgeList,
58 | })
59 | : ProgressIndicator;
60 |
61 | const List = hasDataLoaded ? (
62 |
63 | ) : (
64 | ProgressIndicator
65 | );
66 |
67 | function determineView() {
68 | if (isWindowDesktop) {
69 | return (
70 | <>
71 |
72 |
73 | FRIDGES WITHIN THIS AREA
74 |
75 |
76 | {List}
77 |
78 |
79 | {Map}
80 | >
81 | );
82 | } else {
83 | return (
84 | <>
85 | {currentView === MapToggle.view.list ? (
86 | {List}
87 | ) : (
88 | {Map}
89 | )}
90 |
91 |
92 | >
93 | );
94 | }
95 | }
96 |
97 | return (
98 | <>
99 |
100 | Fridge Finder: Geographic Map
101 |
102 |
103 |
104 | {determineView()}
105 |
106 | >
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/src/pages/demo/styles.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Head from 'next/head';
3 | import { Stack, Typography } from '@mui/material';
4 |
5 | export async function getStaticProps() {
6 | return {
7 | props: {
8 | text: `Lorem ipsum dolor sit amet consectetur adipisicing elit. Enim corrupti
9 | soluta cum, facere ut alias quae quidem autem minima ad sunt, eum
10 | tempore, nobis odit beatae? Repudiandae saepe voluptates nihil?`,
11 | listItems: [`Lorem`, `ipsum`, `dolor`],
12 | },
13 | };
14 | }
15 |
16 | export default function DemoStylesPage({ text, listItems }) {
17 | return (
18 | <>
19 |
20 | Fridge Finder: Theme Styles Demo
21 |
22 |
23 |
24 |
25 | Bold
26 |
27 | {text}
28 |
29 |
30 | Italic
31 |
32 | {text}
33 |
34 |
35 | Subscript
36 |
37 | Lorem ipsum dolor sit amet consectetur adipisicing elit.
38 |
39 |
40 | Superscript
41 |
42 | Lorem ipsum dolor sit amet consectetur adipisicing elit.
43 |
44 |
45 | Blockquote
46 | {text}
47 |
48 |
49 | ul
50 |
51 | {listItems.map((item, i) => (
52 | {item}
53 | ))}
54 |
55 |
56 |
57 | ol
58 |
59 | {listItems.map((item, i) => (
60 | {item}
61 | ))}
62 |
63 |
64 |
65 | Typography caption
66 |
67 | {text}
68 |
69 |
70 | Typography subtitle1
71 |
72 | {text}
73 |
74 |
75 | Typography subtitle2
76 |
77 | {text}
78 |
79 |
80 | >
81 | );
82 | }
83 | DemoStylesPage.propTypes = {
84 | text: PropTypes.string.isRequired,
85 | listItems: PropTypes.arrayOf(PropTypes.string).isRequired,
86 | };
87 |
--------------------------------------------------------------------------------
/src/pages/fridge/[id].jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Head from 'next/head';
3 | import { FridgeInformation } from 'components/molecules';
4 |
5 | export async function getServerSideProps(context) {
6 | return {
7 | props: await getFridgeRecord({ id: context.params.id }),
8 | };
9 | }
10 |
11 | export default function FridgePage(props) {
12 | return (
13 | <>
14 |
15 | {'Fridge Finder: ' + props.fridge.name}
16 |
17 |
18 | >
19 | );
20 | }
21 | FridgePage.propTypes = FridgeInformation.propTypes
22 |
23 |
24 |
25 | const baseUrl = process.env.NEXT_PUBLIC_FF_API_URL + '/v1/fridges/';
26 |
27 |
28 |
29 | async function getAllFridgeIds() {
30 | return fetch(baseUrl, { headers: { Accept: 'application/json' } })
31 | .then((response) => response.json())
32 | .then((json) => json.map((fridge) => fridge.id))
33 | .catch((err) => {
34 | console.error(err);
35 | return [];
36 | });
37 | }
38 |
39 | async function getFridgeRecord({ id }) {
40 | const responses = await Promise.all([
41 | fetch(baseUrl + id, { headers: { Accept: 'application/json' } }),
42 | fetch(baseUrl + id + '/reports', {
43 | headers: { Accept: 'application/json' },
44 | }),
45 | ]);
46 | for (const response of responses) {
47 | if (!response.ok) {
48 | console.error(
49 | `ERROR ${response.url} ${response.status}: ${response.statusText}`
50 | );
51 | return {};
52 | }
53 | }
54 | const [fridge, reports] = await Promise.all(responses.map((r) => r.json()));
55 | const report = reports.length > 0 ? reports[0] : null;
56 | return { fridge, report };
57 | }
58 | getFridgeRecord.propTypes = {
59 | id: PropTypes.string.isRequired,
60 | };
61 |
--------------------------------------------------------------------------------
/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { Grid, Typography } from '@mui/material';
3 | import {
4 | PamphletParagraph,
5 | PageFooter,
6 | PageHero,
7 | ParagraphCard,
8 | } from 'components/atoms';
9 |
10 | const pageContent = {
11 | pageHero: {
12 | img: {
13 | src: '/hero/index.webp',
14 | alt: 'Picture of a New York fridge map',
15 | },
16 | button: {
17 | title: 'Find a Fridge',
18 | to: '/browse',
19 | 'aria-label': 'Browse the fridge map',
20 | variant: 'contained',
21 | },
22 | },
23 | introParagraph: {
24 | variant: 'h1',
25 | title: 'Take what you need. Leave what you can.',
26 | body: [
27 | 'Fridge Finder can help you find community fridges containing free food near you. Click the Find A Fridge button for the full map and list of fridges.',
28 | ],
29 | },
30 | paragraphCard: {
31 | h2: {
32 | variant: 'h2',
33 | img: {
34 | src: '/card/paragraph/pearTomatoAndFridge.svg',
35 | alt: 'Picture of pear dancing with tomatoes stacked on top of each other',
36 | width: 125,
37 | height: 95,
38 | },
39 | title: 'About Community Fridges',
40 | text: 'A community fridge is a decentralized food resource. There are dozens of fridges hosted by volunteers across the New York City area. This website was made to make it easy for people to find fridges and get involved with the community fridge project.',
41 | link: '/pamphlet/about',
42 | },
43 | h3: [
44 | {
45 | variant: 'h3',
46 | img: {
47 | src: '/card/paragraph/apple.svg',
48 | alt: 'Picture of smiling apple holding a list',
49 | width: 125,
50 | height: 95,
51 | },
52 | title: 'Read Best Practices',
53 | text: 'Please look over the guidelines for food donation best practices to keep our fridges safe and accessible to all.',
54 | link: '/pamphlet/best-practices',
55 | },
56 | {
57 | variant: 'h3',
58 | img: {
59 | src: '/card/paragraph/jumpingBlueberries.svg',
60 | alt: 'Picture of blueberries jumping and waving',
61 | width: 125,
62 | height: 95,
63 | },
64 | title: 'Get Involved',
65 | text: 'There are many ways to get involved with community fridges: from driving; donating food; or starting your own community fridge.',
66 | link: '/pamphlet/get-involved',
67 | },
68 | {
69 | variant: 'h3',
70 | img: {
71 | src: '/card/paragraph/plumAndFridge.svg',
72 | alt: 'Picture of smiling plum and smiling fridge',
73 | width: 125,
74 | height: 95,
75 | },
76 | title: 'Start a Fridge',
77 | text: 'Anyone can start a community fridge. Read our guidelines and discover the valuable lessons we learned from hosting two fridges in central New Jersey.',
78 | link: '/pamphlet/get-involved/start-a-fridge',
79 | },
80 | ],
81 | },
82 | };
83 |
84 | export default function HomePage() {
85 | const { pageHero, introParagraph, paragraphCard } = pageContent;
86 | return (
87 | <>
88 |
89 | Fridge Finder
90 |
91 |
92 |
93 |
94 |
95 |
103 |
112 |
113 |
114 |
115 |
120 | Get involved with community fridges!
121 |
122 |
123 |
130 | {paragraphCard.h3.map((card, index) => (
131 |
132 |
133 |
134 | ))}
135 |
136 |
137 |
138 | >
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/about.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/about.webp',
8 | alt: 'The group at Community Focus',
9 | },
10 | },
11 | content: [
12 | {
13 | variant: 'h1',
14 | title: 'How it Started',
15 | body: [
16 | 'Community fridges became popular in 2020 as a way to directly provide free food to people in need and combat food waste. During the pandemic, outdoor refrigerators served as emergency access points for life-sustaining nutrition in dozens of cities around the world. The food found inside fridges come from diverse sources, mostly individual donors or surplus food that is “rescued” from the supply chain.',
17 | ],
18 | },
19 | {
20 | img: {
21 | src: '/paragraph/pamphlet/about/independence_for_each_fridge.webp',
22 | alt: 'People making their own choices',
23 | width: 414,
24 | height: 276,
25 | },
26 | variant: 'h2',
27 | title: 'Independence for Each Fridge',
28 | body: [
29 | 'Community fridges are independently operated by local businesses, activists, neighbors, or faith-based organizations. Cumulatively, fridge hosts and organizers do not identify with a singular monolithic identity or ethos. Instead, fridges are as unique as the neighborhoods that have them. Community fridges are used by all different types of people everyday, oftentimes working class families, immigrants, and homeless populations in need of an extra food source. A common value is respect for the autonomy of each refrigerator.',
30 | ],
31 | },
32 | {
33 | img: {
34 | src: '/paragraph/pamphlet/about/public_art_installation_on_our_sidewalks.webp',
35 | alt: 'Artists painting',
36 | width: 414,
37 | height: 276,
38 | },
39 | variant: 'h2',
40 | title: 'A Public Art Installation on Our Sidewalks',
41 | body: [
42 | 'Artists popularized the community fridges by painting colorful designs on the doors and sides to attract interest and curiosity. This creative element effectively transforms an everyday object into a cultural artifact that carries a powerful message about circular economics and mutual aid. As public art installations, the fridges receive widespread support from the press and social media, giving visibility to this project’s social impact.',
43 | ],
44 | },
45 | {
46 | img: {
47 | src: '/paragraph/pamphlet/about/technology_empowers_us.webp',
48 | alt: 'Technology resources',
49 | width: 414,
50 | height: 276,
51 | },
52 | variant: 'h2',
53 | title: 'Technology Empowers Us',
54 | body: [
55 | 'Taking our mission a step further, a small team of artists and engineers united to build this fridge app. We are Collective Focus, an organization that provides resources and creative opportunities. The goal of our app is to support a larger consortium of people working together to maintain fridges in New York City. By using technology for good, we are building greater capacity for this project to prosper long term. With this app, people are empowered to check on fridges with more organizational capacity, volunteer drivers are mobilized to transport rescued food to fridges, and the incredible everyday contributions to this work are archived for future inspiration',
56 | ],
57 | },
58 | {
59 | img: {
60 | src: '/paragraph/pamphlet/about/collective_focus.webp',
61 | alt: 'Volunteers standing in front of the Collective Focus building',
62 | width: 414,
63 | height: 276,
64 | },
65 | variant: 'h2',
66 | title: 'Collective Focus',
67 | body: [
68 | 'Collective Focus is an artist community that builds prosperity through resource distribution and creative opportunities. Our team has launched, maintained, and operated community fridges for over 2 years, making an impact on 50 fridges worldwide. We currently host four fridges at our physical location in Brooklyn and we provide support to grassroots organizers within our network.',
69 | ],
70 | },
71 | {
72 | variant: 'h2',
73 | title: 'Help out the fridges',
74 | body: [
75 | 'The growth and success of the project depends on community support. There are many ways to help out the community fridge movement in New York and beyond.',
76 | ],
77 | button: {
78 | title: 'Get Involved',
79 | to: '/pamphlet/get-involved',
80 | 'aria-label': 'Get involved with the community fridge movement',
81 | variant: 'contained',
82 | size: 'wide',
83 | },
84 | },
85 | ],
86 | };
87 |
88 | export default function AboutPage() {
89 | const { pageHero, content } = pageContent;
90 | return (
91 | <>
92 |
93 | Fridge Finder: About Us
94 |
95 |
96 |
97 | {content.map((paragraph, index) => (
98 | 0}
102 | />
103 | ))}
104 |
105 |
106 | >
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/best-practices.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import PropTypes from 'prop-types';
3 | import Head from 'next/head';
4 | import { Box, Tab, Tabs, Typography } from '@mui/material';
5 | import { PageHero, PageFooter } from 'components/atoms';
6 |
7 | const pageContent = {
8 | pageHero: {
9 | img: {
10 | src: '/hero/best_practices.webp',
11 | alt: 'Donor placing food into a fridge',
12 | },
13 | },
14 | panelList: [
15 | {
16 | title: 'Dropping off',
17 | content: [
18 | 'Only bring good food to the community fridges. When considering what is good to donate, ask yourself if you would give the food item to your friends or family to eat? If so, your food donation is probably good for your neighbors, too.',
19 | 'Food must be fresh, stored at the proper temperature, and unexpired.',
20 | 'Portion donations into individual sized quantities that make it easy for people to take with them. Catering trays should not be stored in community fridges due to causing food contents to spill, and being inaccessible for the public to transport.',
21 | 'Food should be kept in clean, airtight containers to avoid food spills.',
22 | 'Label meals with ingredients and the date prepared.',
23 | 'If you notice a fridge needs to be cleaned, help clean it.',
24 | 'Do not bring anything else that is not food to a community fridge, unless told otherwise.',
25 | 'Take your trash with you, including cardboard boxes and food scraps.',
26 | ],
27 | },
28 | {
29 | title: 'Picking up',
30 | content: [
31 | 'Only take the amount of food that you need, and leave the rest for others. Many people depend on community fridges to get enough nutrition. Whenever possible, make sure there is enough food left for others to eat as well.',
32 | 'Only take good food from the fridges. If the food does not look good, throw it away using appropriate procedures and sanitation.',
33 | 'If you touch a food item, take it with you or throw it away.',
34 | 'Do not leave trash near community fridges.',
35 | 'If you notice a fridge needs to be cleaned, help clean it.',
36 | 'Be kind to others when interacting with community fridges.',
37 | ],
38 | },
39 | ],
40 | };
41 |
42 | function TabPanel(props) {
43 | const { children, currentTab, index, ...other } = props;
44 |
45 | return (
46 |
54 |
63 | {children}
64 |
65 |
66 | );
67 | }
68 | TabPanel.propTypes = {
69 | children: PropTypes.node,
70 | currentTab: PropTypes.number.isRequired,
71 | index: PropTypes.number.isRequired,
72 | };
73 |
74 | export default function BestPracticesPage() {
75 | const { pageHero, panelList } = pageContent;
76 | const [ixCurrentPanel, setCurrentPanelIndex] = useState(0);
77 |
78 | const handleChange = (event, newValue) => {
79 | setCurrentPanelIndex(newValue);
80 | };
81 |
82 | return (
83 | <>
84 |
85 | Fridge Finder: Best Practices
86 |
87 |
88 | Best Practices
89 |
90 |
91 |
99 | {panelList.map((panel, index) => (
100 |
107 | ))}
108 |
109 | {panelList.map((panel, ixPanel) => (
110 |
115 | {panel.content.map((item, ixContent) => (
116 |
117 | {item}
118 |
119 | ))}
120 |
121 | ))}
122 |
123 |
124 | >
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { Grid } from '@mui/material';
3 | import {
4 | PageFooter,
5 | PageHero,
6 | PamphletParagraph,
7 | TitleCard,
8 | } from 'components/atoms';
9 |
10 | const pageContent = {
11 | pageHero: {
12 | img: {
13 | src: '/hero/get-involved.webp',
14 | alt: 'Volunteers in front of a community fridge',
15 | },
16 | },
17 | introParagraph: {
18 | title: 'Get Involved!',
19 | body: ['There are many ways to support the future of the fridges.'],
20 | variant: 'h1',
21 | },
22 | titleCards: [
23 | {
24 | title: 'Start A Fridge',
25 | link: '/pamphlet/get-involved/start-a-fridge',
26 | img: {
27 | src: '/card/title/startFridge.svg',
28 | alt: 'A smiling fridge',
29 | },
30 | },
31 | {
32 | title: 'Become A Driver',
33 | link: '/pamphlet/get-involved/become-a-driver',
34 | img: {
35 | src: '/card/title/becomeDriver.svg',
36 | alt: 'A car with a smiling face',
37 | },
38 | },
39 | {
40 | title: 'Donate To A Fridge',
41 | link: '/pamphlet/get-involved/donate-to-a-fridge',
42 | img: {
43 | src: '/card/title/donate.svg',
44 | alt: 'A smiling piggy bank with a coin being inserted',
45 | },
46 | },
47 | {
48 | title: 'Source Food',
49 | link: '/pamphlet/get-involved/source-food',
50 | img: {
51 | src: '/card/title/sourceFood.svg',
52 | alt: 'A smiling bell pepper, tomato, and broccoli',
53 | },
54 | },
55 | {
56 | title: 'Service Fridges',
57 | link: '/pamphlet/get-involved/service-fridges',
58 | img: {
59 | src: '/card/title/serviceFridge.svg',
60 | alt: 'A smiling wrench and screwdriver',
61 | },
62 | },
63 | {
64 | title: 'Join A Community Group',
65 | link: '/pamphlet/get-involved/join-a-community-group',
66 | img: {
67 | src: '/card/title/joinCommunity.svg',
68 | alt: 'Four hands coming together with a smiling heart in the center',
69 | },
70 | },
71 | ],
72 | };
73 |
74 | export default function GetInvolvedPage() {
75 | const { pageHero, introParagraph, titleCards } = pageContent;
76 | return (
77 | <>
78 |
79 | Fridge Finder: Get Involved
80 |
81 |
82 |
83 |
84 | {titleCards.map((card, index) => (
85 |
93 |
94 |
95 | ))}
96 |
97 |
98 | >
99 | );
100 | }
101 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/become-a-driver.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/become_a_driver.webp',
8 | alt: 'Food carrier picking up lunch bags',
9 | },
10 | },
11 | content: [
12 | {
13 | title: 'Become a Driver',
14 | variant: 'h1',
15 | body: [
16 | 'Volunteer drivers have the capacity to support many fridges at once by transporting food to multiple locations. These driving routes are often coordinated between fridge organizers, food donors, and drivers. If you have access to a bike or vehicle, you can rescue food and feed people in need. The impact is immediate! Anyone is welcome to coordinate these efforts on their own, but if you would like to request our driver support, contact Fridge Finder.',
17 | ],
18 | button: {
19 | title: 'Become a Driver',
20 | to: {
21 | pathname: '/user/contact',
22 | query: { subject: 'Transport Food' },
23 | },
24 | 'aria-label': 'Become a Driver',
25 | variant: 'contained',
26 | },
27 | },
28 | ],
29 | };
30 |
31 | export default function BecomeADriverPage() {
32 | const { pageHero, content } = pageContent;
33 | return (
34 | <>
35 |
36 | Fridge Finder: Become a Driver
37 |
38 |
39 |
40 | {content.map((paragraph, index) => (
41 | 0}
45 | />
46 | ))}
47 |
48 |
49 | >
50 | );
51 | }
52 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/donate-to-a-fridge.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/donate_to_a_fridge.webp',
8 | alt: 'Volunteer hosting a bake sale to raise money for a community fridge',
9 | },
10 | },
11 | content: [
12 | {
13 | variant: 'h1',
14 | title: 'Donate to a fridge',
15 | body: ['Give your time, food, or funds to make a big impact!'],
16 | },
17 | {
18 | variant: 'h2',
19 | title: 'When donating time',
20 | body: [
21 | 'Most fridges are accessible 24/7, and these locations can use your support. Every community fridge has their own volunteer process, which can be found by contacting that fridge individually. As a general principle, we encourage everyone investing time into this project to treat others with kindness and respect.',
22 | 'To volunteer with our specific group, Collective Focus, contact us.',
23 | ],
24 | button: {
25 | title: 'Volunteer',
26 | to: {
27 | pathname: '/user/contact',
28 | query: { subject: 'Volunteer Interest' },
29 | },
30 | 'aria-label': 'Volunteer',
31 | variant: 'contained',
32 | },
33 | },
34 | {
35 | variant: 'h2',
36 | title: 'When donating food',
37 | body: [
38 | 'Food should be great quality, fresh, and sealed in airtight containers. Label donated meals with ingredients and date. Do not leave items outside of the fridges. Before donating, read our best practices.',
39 | ],
40 | button: {
41 | title: 'Best Practices',
42 | to: '/pamphlet/best-practices',
43 | 'aria-label': 'Best Practices',
44 | variant: 'contained',
45 | },
46 | },
47 | {
48 | variant: 'h2',
49 | title: 'When donating funds',
50 | body: [
51 | 'Fridges should be cleaned daily. If you see trash at a fridge location, take the trash with you to dispose of it properly. If the fridge is in need of a repair, you can alert our community on Fridge Finder by submitting a status report for that fridge. Do you have repair skills that can fix broken refrigerators? Can you help build fridge shelters? Contact us.',
52 | ],
53 | button: {
54 | title: 'Donate!',
55 | to: 'https://www.gofundme.com/f/hub-holiday2',
56 | 'aria-label': 'Donate funds to Fridge Finder!',
57 | variant: 'contained',
58 | },
59 | },
60 | ],
61 | };
62 |
63 | export default function DonateToAFridgePage() {
64 | const { pageHero, content } = pageContent;
65 | return (
66 | <>
67 |
68 | Fridge Finder: Donate to a fridge
69 |
70 |
71 |
72 |
73 | {content.map((paragraph, index) => (
74 | 0}
78 | />
79 | ))}
80 |
81 |
82 | >
83 | );
84 | }
85 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/join-a-community-group.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/join_a_community_group.webp',
8 | alt: 'Volunteers resting',
9 | },
10 | },
11 | content: [
12 | {
13 | title: 'Join a community group',
14 | variant: 'h1',
15 | body: [
16 | 'The fridges near you most likely needs help cleaning and sourcing food. Anyone can participate in running community fridges. You are welcome to organize your own initiatives because operations for community fridges are decentralized and autonomous.',
17 | 'On Fridge Finder, you can find a fridge near you, connect with the location, and share status updates. To collaborate with us behind the scenes, join our engineering or outreach teams.',
18 | ],
19 | button: {
20 | title: 'Volunteer',
21 | to: {
22 | pathname: '/user/contact',
23 | query: { subject: 'Volunteer Interest' },
24 | },
25 | 'aria-label': 'Volunteer',
26 | variant: 'contained',
27 | },
28 | },
29 | {
30 | variant: 'h2',
31 | title: 'Community groups we recommend',
32 | body: [
33 | 'There are groups across New York City that source donations and maintenance support for fridges. This is essential to keeping fridges active.',
34 | 'The following organizations have initiatives to support community fridges. To participate, contact the groups that interest you.',
35 | ],
36 | },
37 | ],
38 | };
39 | const organizations = [
40 | {
41 | name: 'Artists.Athletes.Activists',
42 | url: 'https://artists-athletes-activists.org/',
43 | },
44 | { name: 'Black Chef Movement', url: 'https://blackchefmovement.org/' },
45 | {
46 | name: 'Black Voices Matter',
47 | url: 'https://www.blackvoicesmatterpledge.org/',
48 | },
49 | { name: 'Bushwick Ayuda Mutua ', url: 'https://bushwickayudamutua.com/' },
50 | {
51 | name: 'Collective Focus Resource Hub',
52 | url: 'https://collectivefocus.site/',
53 | },
54 | { name: 'Freedge', url: 'https://freedge.org/' },
55 | { name: 'Nuestra Mesa Brooklyn', url: 'https://www.nuestramesabk.com/' },
56 | {
57 | name: 'One Love Community Fridge',
58 | url: 'https://www.onelovecommunityfridge.org/',
59 | },
60 | { name: 'Stuff4Good', url: 'https://officialgoodstuff.com/stuff4good/' },
61 | { name: 'Universe City', url: 'https://www.universecity.nyc/' },
62 | { name: 'Woodbine Mutual Aid', url: 'https://www.woodbine.nyc/mutualaid/' },
63 | ];
64 |
65 | export default function JoinACommunityGroupPage() {
66 | const { pageHero, content } = pageContent;
67 | return (
68 | <>
69 |
70 | Fridge Finder: Join a community group
71 |
72 |
73 |
74 | {content.map((paragraph, index) => (
75 | 0}
79 | />
80 | ))}
81 |
82 | {organizations.map(({ name, url }, index) => (
83 |
84 |
85 | {name}
86 |
87 |
88 | ))}
89 |
90 |
91 |
92 | >
93 | );
94 | }
95 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/service-fridges.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/service_fridges.webp',
8 | alt: 'Man inspecting fridge',
9 | },
10 | },
11 | content: [
12 | {
13 | variant: 'h1',
14 | title: 'Taking care of fridges',
15 | body: [
16 | 'Fridges should be cleaned daily. If you see trash at a fridge location, take the trash with you to dispose of it properly. If the fridge is in need of a repair, you can alert our community on Fridge Finder by submitting a status report for that fridge. Do you have repair skills that can fix broken refrigerators? Can you help build fridge shelters? Contact us.',
17 | ],
18 | button: {
19 | title: 'Service Fridges',
20 | to: {
21 | pathname: '/user/contact',
22 | query: { subject: 'Fridge Service Interest' },
23 | },
24 | 'aria-label': 'Interested in fixing and maintaining a fridge',
25 | variant: 'contained',
26 | },
27 | },
28 | ],
29 | };
30 |
31 | export default function ServiceFridgesPage() {
32 | const { pageHero, content } = pageContent;
33 | return (
34 | <>
35 |
36 | Fridge Finder: Service fridges
37 |
38 |
39 |
40 |
41 | {content.map((paragraph, index) => (
42 | 0}
46 | />
47 | ))}
48 |
49 |
50 | >
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/source-food.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/source_food.webp',
8 | alt: 'Boxes of food stacked up in front of Community Focus',
9 | },
10 | },
11 | content: [
12 | {
13 | variant: 'h1',
14 | title: 'Source Food',
15 | body: [
16 | 'Sourcing food donations is possible by building relationships with businesses that have surplus products. Fridge organizers are able to redirect food waste from bakeries, grocery stores, pantries, cafes,restaurants, and more. That way, perfectly good food can provide nutrition to people in need, instead of being thrown away. Businesses have many incentives to partner with community fridges. Excess food causes a negative environmental impact and inefficiencies within our economy, which community fridges can help resolve. The best way to approach a business about donating food would be to present materials about community fridges and create a proposal.',
17 | ],
18 | button: {
19 | title: 'Source Food',
20 | to: {
21 | pathname: '/user/contact',
22 | query: { subject: 'Sourcing Food Inquiry' },
23 | },
24 | 'aria-label': 'Sourcing Food Inquiry',
25 | variant: 'contained',
26 | size: 'wide',
27 | },
28 | },
29 | ],
30 | };
31 |
32 | export default function SourceFoodPage() {
33 | const { pageHero, content } = pageContent;
34 | return (
35 | <>
36 |
37 | Fridge Finder: Source Food
38 |
39 |
40 |
41 |
42 | {content.map((paragraph, index) => (
43 | 0}
47 | />
48 | ))}
49 |
50 |
51 | >
52 | );
53 | }
54 |
--------------------------------------------------------------------------------
/src/pages/pamphlet/get-involved/start-a-fridge.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { PageHero, PamphletParagraph, PageFooter } from 'components/atoms';
3 |
4 | const pageContent = {
5 | pageHero: {
6 | img: {
7 | src: '/hero/start_a_fridge.webp',
8 | alt: 'Volunteers building a fridge shelter',
9 | },
10 | },
11 | content: [
12 | {
13 | variant: 'h1',
14 | title: 'Start a community fridge',
15 | body: [
16 | 'Anyone can start a community fridge. The keys to success are finding a great host location, organizing a daily maintenance team, and communicating about your goals both online and in your neighborhood. If you are interested in starting a community fridge, contact us for advice or feedback.',
17 | ],
18 | },
19 | {
20 | variant: 'h2',
21 | title: '1. Form a team',
22 | body: [
23 | 'To ensure the success and longevity of your community fridge, it is best to have some support from the beginning. Put together a group of people that will help you run the community fridge. We recommend including your friends, neighbors and family. Ensure everyone is excited and committed to running a community fridge.',
24 | ],
25 | },
26 | {
27 | variant: 'h2',
28 | title: '2. Select a location and a fridge',
29 | body: [
30 | 'Your team can scout for a location to host a community fridge. A location host would provide your fridge with electricity. Create an agreement on how everyone will dispose of trash at the location, including cardboard boxes that carry food donations. The best fridge hosts are supportive, helpful, and reliable. Examples include restaurants, cafes, bars, small businesses, and churches.',
31 | 'Once you have a location confirmed, you can find a refrigerator. Some people raise money to buy a new fridge, but it is possible to find a free second-use fridge online or by asking us.',
32 | ],
33 | button: {
34 | title: 'Request A Fridge',
35 | to: {
36 | pathname: '/user/contact',
37 | query: { subject: 'Fridge Request' },
38 | },
39 | 'aria-label': 'Request a fridge',
40 | variant: 'contained',
41 | },
42 | },
43 | {
44 | variant: 'h2',
45 | title: '3. Build a fridge shelter',
46 | body: [
47 | 'To keep your fridge protected from outdoor elements like rain and snow, we strongly recommend building a weather resistant structure to protect your refrigerator. Fridge shelters are typically built from wood, which can be purchased or secured from donated materials. You can also do outreach to connect with volunteer carpenters.',
48 | ],
49 | button: {
50 | title: 'Get Construction Support',
51 | to: {
52 | pathname: '/user/contact',
53 | query: { subject: 'Construction Support Request' },
54 | },
55 | 'aria-label': 'Request construction support',
56 | variant: 'contained',
57 | },
58 | },
59 | {
60 | variant: 'h2',
61 | title: '4. Budgeting tips',
62 | body: [
63 | 'Running a community fridge is not expensive, but there are some costs involved. The basic costs for running a fridge are electricity, cleaning supplies, and trash removal. We also encourage organizers to save money for potential repairs or if the appliance needs to be replaced in the future. With additional funding support, community fridges can also reimburse volunteer drivers for their gas expenses. ',
64 | ],
65 | },
66 | {
67 | variant: 'h2',
68 | title: '5. Announce the launch',
69 | body: [
70 | 'You can introduce a new community fridge to the public by creating online profiles on Fridge Finder and social media. With Fridge Finder, launching a community fridge is easier because our platform allows everyone to share information in an open forum.',
71 | 'To keep your communication organized, create an email account for the new fridge. From there, you can find press, media opportunities, and collaborations as your food justice efforts grow.',
72 | ],
73 | },
74 | {
75 | variant: 'h2',
76 | title: 'Ready to start a fridge?',
77 | body: ['Contact us for more information on sourcing a fridge.'],
78 | button: {
79 | title: 'Request A Fridge',
80 | to: {
81 | pathname: '/user/contact',
82 | query: { subject: 'Fridge Request' },
83 | },
84 | 'aria-label': 'Request a fridge',
85 | variant: 'contained',
86 | },
87 | },
88 | ],
89 | };
90 |
91 | export default function StartAFridgePage() {
92 | const { pageHero, content } = pageContent;
93 | return (
94 | <>
95 |
96 | Fridge Finder: Start a Fridge
97 |
98 |
99 |
100 |
101 | {content.map((paragraph, index) => (
102 | 0}
106 | />
107 | ))}
108 |
109 |
110 | >
111 | );
112 | }
113 |
--------------------------------------------------------------------------------
/src/pages/user/contact.jsx:
--------------------------------------------------------------------------------
1 | import Head from 'next/head';
2 | import { useRouter } from 'next/router';
3 |
4 | import { useState } from 'react';
5 | import { useFormik } from 'formik';
6 |
7 | import { Button, Stack, TextField, Typography } from '@mui/material';
8 | import { ButtonLink, FeedbackCard } from 'components/atoms';
9 |
10 | import { typesFormik } from 'model/view/component/prop-types';
11 | import { dialogContact } from 'model/view/dialog/yup';
12 |
13 | const fridgeUrl = process.env.NEXT_PUBLIC_FF_API_URL + '/v1/contact/';
14 | const enumDisplay = Object.freeze({
15 | EmailDialog: 0,
16 | EmailSuccess: 1,
17 | EmailError: 2,
18 | });
19 |
20 | export default function ContactPage() {
21 | const [displayComponent, setDisplay] = useState(enumDisplay.EmailDialog);
22 | const { query } = useRouter();
23 |
24 | const onSubmitFn = (values) => {
25 | fetch(fridgeUrl, {
26 | method: 'POST',
27 | headers: { 'Content-Type': 'application/json' },
28 | body: JSON.stringify(values),
29 | })
30 | .then((response) =>
31 | setDisplay(
32 | response.ok ? enumDisplay.EmailSuccess : enumDisplay.EmailError
33 | )
34 | )
35 | .catch(() => setDisplay(enumDisplay.EmailError));
36 | };
37 | const formik = useFormik({
38 | initialValues: {
39 | name: '',
40 | email: '',
41 | subject: query['subject'] ?? '',
42 | message: '',
43 | },
44 | validationSchema: dialogContact,
45 | onSubmit: onSubmitFn,
46 | });
47 |
48 | let panel;
49 | switch (displayComponent) {
50 | case enumDisplay.EmailDialog:
51 | panel = ContactForm({ formik });
52 | break;
53 | case enumDisplay.EmailSuccess:
54 | panel = ;
55 | break;
56 | case enumDisplay.EmailError:
57 | panel = (
58 | setDisplay(enumDisplay.EmailDialog)}
61 | />
62 | );
63 | break;
64 | }
65 |
66 | return (
67 | <>
68 |
69 | Fridge Finder: Contact Us
70 |
71 | {panel}
72 | >
73 | );
74 | }
75 |
76 | function ContactForm({ formik }) {
77 | return (
78 |
79 |
80 | Contact Us!
81 |
82 |
149 |
150 | );
151 | }
152 | ContactForm.propTypes = {
153 | formik: typesFormik.isRequired,
154 | };
155 |
--------------------------------------------------------------------------------
/src/pages/user/fridge/add.jsx:
--------------------------------------------------------------------------------
1 | import PropTypes from 'prop-types';
2 | import Head from 'next/head';
3 |
4 | export async function getStaticProps() {
5 | return { props: {} };
6 | }
7 |
8 | export default function AddFridgePage() {
9 | return (
10 | <>
11 |
12 | CFM: Add a Fridge to the database
13 |
14 | CFM: Add a Fridge to the database
15 | >
16 | );
17 | }
18 | AddFridgePage.propTypes = PropTypes.exact({}).isRequired;
19 |
--------------------------------------------------------------------------------
/src/pages/user/fridge/report.jsx:
--------------------------------------------------------------------------------
1 | export default function FridgeReportPage() {
2 | return Fridge Status Report
;
3 | }
4 |
--------------------------------------------------------------------------------
/src/theme/index.js:
--------------------------------------------------------------------------------
1 | import { createTheme, responsiveFontSizes } from '@mui/material/styles';
2 |
3 | import { applyAlpha, designColor, default as palette } from './palette';
4 | import typography from './typography';
5 |
6 | const theme = responsiveFontSizes(
7 | createTheme({
8 | palette,
9 | typography,
10 | spacing: 4,
11 | props: {
12 | MuiAppBar: {
13 | color: designColor.blue.light,
14 | },
15 | },
16 | components: {
17 | MuiAppBar: {
18 | defaultProps: {
19 | color: 'secondary',
20 | },
21 | },
22 | MuiButton: {
23 | styleOverrides: {
24 | root: {
25 | borderRadius: 45,
26 | '&:hover': {
27 | borderColor: designColor.blue.dark,
28 | },
29 | '&.MuiButton-outlined': {
30 | color: designColor.neroGray,
31 | borderColor: designColor.blue.dark,
32 | },
33 | '&.Mui-disabled': {
34 | color: designColor.white,
35 | backgroundColor: applyAlpha('cc', designColor.neroGray),
36 | },
37 | },
38 | },
39 | variants: [
40 | {
41 | props: { size: 'wide' },
42 | style: { minWidth: 300 },
43 | },
44 | ],
45 | },
46 | },
47 | })
48 | );
49 |
50 | export default theme;
51 |
--------------------------------------------------------------------------------
/src/theme/palette.js:
--------------------------------------------------------------------------------
1 | export const applyAlpha = (alpha, color) => color + alpha;
2 |
3 | export const pinColor = {
4 | itemsFull: '#97ed7d',
5 | itemsMany: '#ffe55c',
6 | itemsFew: '#ffd4ff',
7 | itemsEmpty: '#ffffff',
8 | fridgeNotAtLocation: '#d3d3d3',
9 | fridgeOperation: '#222',
10 | fridgeGhost: '#e3f2fd',
11 | reportUnavailable: '#d3d3d3',
12 | };
13 |
14 | const grayscale = {
15 | gradient: [
16 | '#FFFFFF', //0] white
17 | '#F6F6F6', //1] whiteSmoke
18 | '#D8D8D8', //2] lightSilver - veryLightGray
19 | '#B4B4B4', //3] magneticGray
20 | '#222222', //4] neroGray
21 | ],
22 | };
23 |
24 | export const designColor = {
25 | white: grayscale.gradient[0],
26 | whiteSmoke: grayscale.gradient[1],
27 | lightSilver: grayscale.gradient[2],
28 | magneticGray: grayscale.gradient[3],
29 | neroGray: grayscale.gradient[4],
30 | black: '#000000',
31 | blue: {
32 | dark: '#1543D4',
33 | darkShade: ['#040B25'],
34 | light: '#88B3FF',
35 | },
36 | };
37 |
38 | const palette = {
39 | type: 'light',
40 | white: designColor.white,
41 | black: designColor.black,
42 | primary: {
43 | main: designColor.blue.dark,
44 | },
45 | secondary: {
46 | main: designColor.blue.light,
47 | },
48 | background: {
49 | default: designColor.white,
50 | paper: designColor.whiteSmoke,
51 | },
52 | text: {
53 | primary: designColor.neroGray,
54 | secondary: applyAlpha('cc', designColor.neroGray),
55 | disabled: designColor.magneticGray,
56 | hint: applyAlpha('cc', designColor.neroGray),
57 | },
58 | icon: applyAlpha('cc', designColor.neroGray),
59 | divider: designColor.neroGray,
60 | };
61 |
62 | export default palette;
63 |
--------------------------------------------------------------------------------
/src/theme/typography.js:
--------------------------------------------------------------------------------
1 | const typography = {
2 | fontFamily: [
3 | 'Inter',
4 | '"Helvetica Neue"',
5 | 'HelveticaNeue',
6 | 'Helvetica',
7 | '"TeX Gyre"',
8 | 'TeXGyre',
9 | 'Arial',
10 | 'sans-serif',
11 | ].join(','),
12 | h1: { fontSize: '36pt', fontWeight: 700, margin: '12pt 0 12pt 0' },
13 | h2: { fontSize: '28pt', fontWeight: 700 },
14 | h3: { fontSize: '28pt', fontWeight: 400 },
15 | h4: { fontSize: '18pt', fontWeight: 700 },
16 | h5: { fontSize: '18pt', fontWeight: 600 },
17 | h6: { fontSize: '16pt', fontWeight: 600 },
18 | body1: { fontSize: '18pt', fontWeight: 400 },
19 | body2: { fontSize: '16pt', fontWeight: 400 },
20 | button: { fontSize: '18pt', fontWeight: 700 },
21 | caption: { fontSize: '15pt', fontWeight: 700, letterSpacing: 0.5 },
22 | footer: { fontSize: '65%', fontWeight: 600 },
23 | };
24 |
25 | export default typography;
26 |
--------------------------------------------------------------------------------
/svgo.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | multipass: true,
3 | plugins: [
4 | 'preset-default',
5 | 'convertStyleToAttrs',
6 | {
7 | name: 'removeAttributesBySelector',
8 | params: {
9 | selectors: [
10 | {
11 | selector: '[stroke-opacity="1"]',
12 | attributes: ['path', 'circle'],
13 | },
14 | {
15 | selector: '[stroke-dasharray="none"]',
16 | attributes: ['path', 'circle'],
17 | },
18 | ],
19 | },
20 | },
21 | {
22 | name: 'sortAttrs',
23 | params: {
24 | xmlnsOrder: 'alphabetical',
25 | },
26 | },
27 | ],
28 | };
29 |
--------------------------------------------------------------------------------