├── .prettierrc
├── static
├── favicon.ico
├── fonts
│ ├── Metropolis-Black.woff
│ ├── Metropolis-Bold.woff
│ ├── Metropolis-Medium.woff
│ ├── Metropolis-SemiBold.woff
│ ├── Metropolis-ExtraBold.woff
│ └── fonts.css
└── icons
│ ├── chevron_down.svg
│ ├── up-arrow.svg
│ ├── down_arrow.svg
│ └── icons.js
├── .prettierrc.json
├── .prettierignore
├── assets
├── api_key_instructions.png
└── base_id_instructions.png
├── src
├── components
│ ├── Button.js
│ ├── Search
│ │ ├── Search.js
│ │ └── search.css
│ ├── Header.js
│ ├── Hero.js
│ ├── Layout.js
│ ├── seo.js
│ ├── Footer.js
│ ├── FlagMenu.js
│ ├── DonationSlider.js
│ └── donationCard.js
├── gatsby-plugin-chakra-ui
│ └── theme.js
├── utils
│ ├── mediaQueries.js
│ └── geolocation.js
├── assets
│ └── icon.svg
└── pages
│ ├── about.js
│ └── index.js
├── .github
├── dependabot.yml
├── ISSUE_TEMPLATE
│ ├── feature_request.md
│ └── bug_report.md
└── workflows
│ ├── lint.yml
│ └── main.yml
├── .eslintrc.json
├── font-preload-cache.json
├── LICENSE
├── .gitignore
├── gatsby-config.js
├── package.json
├── CODE_OF_CONDUCT.md
└── README.md
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "semi": false
4 | }
5 |
--------------------------------------------------------------------------------
/static/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/favicon.ico
--------------------------------------------------------------------------------
/.prettierrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "printWidth": 80,
3 | "singleQuote": true,
4 | "trailingComma": "es5"
5 | }
6 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .cache
2 | package.json
3 | package-lock.json
4 | public
5 | .github
6 | font-preload-cache.json
--------------------------------------------------------------------------------
/assets/api_key_instructions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/assets/api_key_instructions.png
--------------------------------------------------------------------------------
/assets/base_id_instructions.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/assets/base_id_instructions.png
--------------------------------------------------------------------------------
/static/fonts/Metropolis-Black.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/fonts/Metropolis-Black.woff
--------------------------------------------------------------------------------
/static/fonts/Metropolis-Bold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/fonts/Metropolis-Bold.woff
--------------------------------------------------------------------------------
/static/fonts/Metropolis-Medium.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/fonts/Metropolis-Medium.woff
--------------------------------------------------------------------------------
/static/fonts/Metropolis-SemiBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/fonts/Metropolis-SemiBold.woff
--------------------------------------------------------------------------------
/static/fonts/Metropolis-ExtraBold.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jamrants/donations/HEAD/static/fonts/Metropolis-ExtraBold.woff
--------------------------------------------------------------------------------
/src/components/Button.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Button } from "@chakra-ui/core"
3 |
4 | const CustomButton = props => (
5 |
15 | )
16 |
17 | export default CustomButton
18 |
--------------------------------------------------------------------------------
/static/fonts/fonts.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Metropolis";
3 | src: url("Metropolis-Black.woff");
4 | font-weight: 900;
5 | }
6 |
7 | @font-face {
8 | font-family: "Metropolis";
9 | src: url("Metropolis-ExtraBold.woff");
10 | font-weight: 800;
11 | }
12 |
13 | @font-face {
14 | font-family: "Metropolis";
15 | src: url("Metropolis-Bold.woff");
16 | font-weight: 700;
17 | }
18 |
19 | @font-face {
20 | font-family: "Metropolis";
21 | src: url("Metropolis-SemiBold.woff");
22 | font-weight: 600;
23 | }
24 |
25 | @font-face {
26 | font-family: "Metropolis";
27 | src: url("Metropolis-Medium.woff");
28 | font-weight: 500;
29 | }
30 |
--------------------------------------------------------------------------------
/src/components/Search/Search.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Input } from "@chakra-ui/core"
3 |
4 | const SearchField = ({ value, onChange, placeholder }) => (
5 |
23 | )
24 |
25 | export default SearchField
26 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 |
9 | # Maintain dependencies for GitHub Actions
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "daily"
14 |
15 | # Maintain dependencies for npm
16 | - package-ecosystem: "npm"
17 | directory: "/"
18 | schedule:
19 | interval: "daily"
20 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Feature request
3 | about: Suggest an idea for this project
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Is your feature request related to a problem? Please describe.**
10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
11 |
12 | **Describe the solution you'd like**
13 | A clear and concise description of what you want to happen.
14 |
15 | **Describe alternatives you've considered**
16 | A clear and concise description of any alternative solutions or features you've considered.
17 |
18 | **Additional context**
19 | Add any other context or screenshots about the feature request here.
20 |
--------------------------------------------------------------------------------
/src/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Box, List, ListItem } from "@chakra-ui/core"
3 |
4 | const Header = () => (
5 |
14 |
15 | Donations Revealed
16 |
17 |
18 |
19 |
20 | About
21 |
22 |
23 |
24 |
25 | )
26 |
27 | export default Header
28 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["airbnb", "plugin:prettier/recommended", "prettier/react"],
3 | "env": {
4 | "browser": true,
5 | "commonjs": true,
6 | "es6": true,
7 | "jest": true,
8 | "node": true
9 | },
10 | "rules": {
11 | "jsx-a11y/href-no-hash": ["off"],
12 | "react/jsx-filename-extension": ["warn", { "extensions": [".js", ".jsx"] }],
13 | "max-len": [
14 | "warn",
15 | {
16 | "code": 80,
17 | "tabWidth": 2,
18 | "comments": 80,
19 | "ignoreComments": false,
20 | "ignoreTrailingComments": true,
21 | "ignoreUrls": true,
22 | "ignoreStrings": true,
23 | "ignoreTemplateLiterals": true,
24 | "ignoreRegExpLiterals": true
25 | }
26 | ]
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/gatsby-plugin-chakra-ui/theme.js:
--------------------------------------------------------------------------------
1 | import { theme } from "@chakra-ui/core"
2 | import customIcons from "../../static/icons/icons"
3 |
4 | export default {
5 | ...theme,
6 | fonts: {
7 | ...theme.fonts,
8 | heading: "Metropolis, system-ui, sans-serif",
9 | body: "Metropolis, system-ui, sans-serif",
10 | },
11 | colors: {
12 | ...theme.colors,
13 | dark: "#17171D",
14 | primary: {
15 | green: "#0AD48B",
16 | red: "#EC3750",
17 | },
18 | snow: "#F9FAFC",
19 | darkless: "#252429",
20 | smoke: "#E0E6ED",
21 | accent: "#338EDA",
22 | slate: "#3C4858",
23 | },
24 | // 460, 768, 992, 1600
25 | breakpoints: ["30em", "48em", "62em", "107em"],
26 | icons: {
27 | ...theme.icons,
28 | ...customIcons,
29 | },
30 | }
31 |
--------------------------------------------------------------------------------
/src/utils/mediaQueries.js:
--------------------------------------------------------------------------------
1 | import { useMediaQuery } from "react-responsive"
2 |
3 | export const Desktop = ({ children }) => {
4 | const isDesktop = useMediaQuery({ minWidth: 961 })
5 | return isDesktop ? children : null
6 | }
7 |
8 | export const Mobile = ({ children }) => {
9 | const isMobile = useMediaQuery({ maxWidth: 960 })
10 | return isMobile ? children : null
11 | }
12 |
13 | /**
14 | * Determines viewport sizing of client
15 | * @returns {Boolean} Whether or not the window is desktop sized
16 | */
17 | export const IsDesktop = () => {
18 | const desktop = useMediaQuery({ minWidth: 961 })
19 | return desktop
20 | }
21 |
22 | /**
23 | * Determines viewport sizing of client
24 | * @returns {Boolean} Whether or not the window is mobile sized
25 | */
26 | export const IsMobile = () => {
27 | const mobile = useMediaQuery({ maxWidth: 960 })
28 | return mobile
29 | }
30 |
--------------------------------------------------------------------------------
/static/icons/chevron_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/static/icons/up-arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/font-preload-cache.json:
--------------------------------------------------------------------------------
1 | {
2 | "timestamp": 1592361549892,
3 | "hash": "5eecfe5e53c548f27d15f073b833e582",
4 | "assets": {
5 | "/dev-404-page/": {
6 | "/fonts/Metropolis-Medium.woff": true,
7 | "/fonts/Metropolis-ExtraBold.woff": true,
8 | "/fonts/Metropolis-Bold.woff": true,
9 | "/fonts/Metropolis-Black.woff": true,
10 | "/fonts/Metropolis-SemiBold.woff": true
11 | },
12 | "/": {
13 | "/fonts/Metropolis-Black.woff": true,
14 | "/fonts/Metropolis-ExtraBold.woff": true,
15 | "/fonts/Metropolis-Bold.woff": true,
16 | "/fonts/Metropolis-SemiBold.woff": true,
17 | "/fonts/Metropolis-Medium.woff": true
18 | },
19 | "/about/": {
20 | "/fonts/Metropolis-Black.woff": true,
21 | "/fonts/Metropolis-ExtraBold.woff": true,
22 | "/fonts/Metropolis-Bold.woff": true,
23 | "/fonts/Metropolis-SemiBold.woff": true,
24 | "/fonts/Metropolis-Medium.woff": true
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/static/icons/down_arrow.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug_report.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: Bug report
3 | about: Create a report to help us improve
4 | title: ""
5 | labels: ""
6 | assignees: ""
7 | ---
8 |
9 | **Describe the bug**
10 | A clear and concise description of what the bug is.
11 |
12 | **To Reproduce**
13 | Steps to reproduce the behavior:
14 |
15 | 1. Go to '...'
16 | 2. Click on '....'
17 | 3. Scroll down to '....'
18 | 4. See error
19 |
20 | **Expected behavior**
21 | A clear and concise description of what you expected to happen.
22 |
23 | **Screenshots**
24 | If applicable, add screenshots to help explain your problem.
25 |
26 | **Desktop (please complete the following information):**
27 |
28 | - OS: [e.g. iOS]
29 | - Browser [e.g. chrome, safari]
30 | - Version [e.g. 22]
31 |
32 | **Smartphone (please complete the following information):**
33 |
34 | - Device: [e.g. iPhone6]
35 | - OS: [e.g. iOS8.1]
36 | - Browser [e.g. stock browser, safari]
37 | - Version [e.g. 22]
38 |
39 | **Additional context**
40 | Add any other context about the problem here.
41 |
--------------------------------------------------------------------------------
/src/components/Hero.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Box, PseudoBox, Text } from "@chakra-ui/core"
3 | import Header from "./Header"
4 |
5 | const Hero = ({ padding, title, subtitle }) => {
6 | return (
7 |
12 |
13 |
19 |
26 | {title}
27 |
28 |
33 | {subtitle}
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default Hero
41 |
--------------------------------------------------------------------------------
/src/components/Layout.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Box, PseudoBox } from "@chakra-ui/core"
3 | import Hero from "./Hero"
4 | import Footer from "./Footer"
5 |
6 | const breakpoints = ["30em", "48em", "62em", "65.9375em", "75em", "107em"]
7 | // 1200px - 7%
8 | // 1055px - 5%
9 | // 62em - 20% - hero - 10%
10 | breakpoints.xs = breakpoints[0]
11 | breakpoints.sm = breakpoints[1]
12 | breakpoints.md = breakpoints[2]
13 | breakpoints.lg = breakpoints[3]
14 | breakpoints.xl = breakpoints[4]
15 | breakpoints.xxl = breakpoints[5]
16 |
17 | const Layout = ({ children, title, subtitle, page }) => (
18 |
19 |
24 | {page === "Home" ? (
25 | {children}
26 | ) : (
27 | {children}
28 | )}
29 |
30 |
31 | )
32 |
33 | export default Layout
34 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on: push
4 |
5 | jobs:
6 | run-linters:
7 | name: Run linters
8 | runs-on: ubuntu-latest
9 |
10 | steps:
11 | - name: Check out Git repository
12 | uses: actions/checkout@v2.3.0
13 |
14 | - name: Set up Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 12
18 |
19 | - name: Cache node_modules
20 | id: cache-node-modules
21 | uses: actions/cache@v2
22 | with:
23 | path: node_modules
24 | key: ${{ runner.os }}-build-npm-${{ hashFiles('**/package-lock.json') }}
25 | restore-keys: |
26 | ${{ runner.os }}-build-npm-
27 |
28 | - name: Install Dependencies
29 | if: steps.cache-node-modules.outputs.cache-hit != 'true'
30 | run: npm install
31 |
32 | - name: Run linters
33 | uses: samuelmeuli/lint-action@v1
34 | with:
35 | github_token: ${{ secrets.github_token }}
36 | # Enable linters
37 | eslint: true
38 | prettier: true
39 | auto_fix: true
40 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 jamrants
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 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 |
8 | # Runtime data
9 | pids
10 | *.pid
11 | *.seed
12 | *.pid.lock
13 |
14 | # Directory for instrumented libs generated by jscoverage/JSCover
15 | lib-cov
16 |
17 | # Coverage directory used by tools like istanbul
18 | coverage
19 |
20 | # nyc test coverage
21 | .nyc_output
22 |
23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
24 | .grunt
25 |
26 | # Bower dependency directory (https://bower.io/)
27 | bower_components
28 |
29 | # node-waf configuration
30 | .lock-wscript
31 |
32 | # Compiled binary addons (http://nodejs.org/api/addons.html)
33 | build/Release
34 |
35 | # Dependency directories
36 | node_modules/
37 | jspm_packages/
38 |
39 | # Typescript v1 declaration files
40 | typings/
41 |
42 | # Optional npm cache directory
43 | .npm
44 |
45 | # Optional eslint cache
46 | .eslintcache
47 |
48 | # Optional REPL history
49 | .node_repl_history
50 |
51 | # Output of 'npm pack'
52 | *.tgz
53 |
54 | # dotenv environment variable files
55 | .env*
56 |
57 | # gatsby files
58 | .cache/
59 | public
60 |
61 | # Mac files
62 | .DS_Store
63 |
64 | # Yarn
65 | yarn-error.log
66 | .pnp/
67 | .pnp.js
68 | # Yarn Integrity file
69 | .yarn-integrity
70 |
--------------------------------------------------------------------------------
/gatsby-config.js:
--------------------------------------------------------------------------------
1 | require("dotenv").config({
2 | path: `.env.${process.env.NODE_ENV}`,
3 | })
4 |
5 | module.exports = {
6 | siteMetadata: {
7 | title: "Donations Revealed",
8 | description:
9 | "Corporations have made headlines with big donations recently — how much would the average household need to match their donation?",
10 | author: "jamrants",
11 | },
12 | plugins: [
13 | {
14 | resolve: `gatsby-source-airtable`,
15 | options: {
16 | apiKey: process.env.AIRTABLE_KEY,
17 | tables: [
18 | {
19 | baseId: process.env.BASE_ID,
20 | tableName: `Corporations`,
21 | mapping: { Logo: `fileNode` },
22 | },
23 | {
24 | baseId: process.env.BASE_ID,
25 | tableName: `CountryIncomes`,
26 | queryName: "CountryIncomes",
27 | separateNodeType: true,
28 | },
29 | ],
30 | },
31 | },
32 | {
33 | resolve: `gatsby-plugin-chakra-ui`,
34 | options: {
35 | isResettingCSS: true,
36 | isUsingColorMode: false,
37 | },
38 | },
39 | {
40 | resolve: `gatsby-plugin-web-font-loader`,
41 | options: {
42 | custom: {
43 | families: [`Metropolis`],
44 | urls: [`/fonts/fonts.css`],
45 | },
46 | },
47 | },
48 | `gatsby-plugin-sharp`,
49 | `gatsby-transformer-sharp`,
50 | {
51 | resolve: `gatsby-plugin-manifest`,
52 | options: {
53 | name: `Donations Revealed`,
54 | short_name: `Donations`,
55 | start_url: `/`,
56 | background_color: `#17171d`,
57 | theme_color: `#32d6a6`,
58 | display: `standalone`,
59 | icon: `src/assets/icon.svg`,
60 | },
61 | },
62 | `gatsby-plugin-offline`,
63 | `gatsby-plugin-react-helmet`,
64 | `gatsby-plugin-preload-fonts`,
65 | ],
66 | }
67 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "donations-exposed",
3 | "private": true,
4 | "description": "Tracking how much corporation donations would be to everyday people",
5 | "version": "1.0.0",
6 | "license": "MIT",
7 | "scripts": {
8 | "build": "gatsby build",
9 | "develop": "gatsby develop",
10 | "format": "prettier --write \"**/*.{js,jsx,json,md}\"",
11 | "start": "npm run develop",
12 | "serve": "gatsby serve",
13 | "clean": "gatsby clean",
14 | "test": "echo \"Write tests! -> https://gatsby.dev/unit-testing\" && exit 1",
15 | "preload-fonts": "gatsby-preload-fonts"
16 | },
17 | "dependencies": {
18 | "@chakra-ui/core": "^0.8.0",
19 | "@emotion/core": "^10.0.28",
20 | "@emotion/styled": "^10.0.27",
21 | "emotion-theming": "^10.0.27",
22 | "gatsby": "^2.23.3",
23 | "gatsby-image": "^2.4.7",
24 | "gatsby-plugin-chakra-ui": "^0.1.4",
25 | "gatsby-plugin-manifest": "^2.4.11",
26 | "gatsby-plugin-offline": "^3.2.9",
27 | "gatsby-plugin-preload-fonts": "^1.2.10",
28 | "gatsby-plugin-react-helmet": "^3.3.4",
29 | "gatsby-plugin-sharp": "^2.6.11",
30 | "gatsby-plugin-web-font-loader": "^1.0.4",
31 | "gatsby-source-airtable": "^2.1.1",
32 | "gatsby-source-filesystem": "^2.3.11",
33 | "gatsby-transformer-sharp": "^2.5.5",
34 | "locale-currency": "0.0.2",
35 | "react": "^16.12.0",
36 | "react-country-flag": "^2.1.0",
37 | "react-data-sort": "^1.2.1",
38 | "react-dom": "^16.12.0",
39 | "react-helmet": "^6.1.0",
40 | "react-responsive": "^8.1.0"
41 | },
42 | "devDependencies": {
43 | "babel-eslint": "^10.1.0",
44 | "eslint": "^7.5.0",
45 | "eslint-config-airbnb": "^18.1.0",
46 | "eslint-config-prettier": "^6.11.0",
47 | "eslint-plugin-import": "^2.21.2",
48 | "eslint-plugin-jsx-a11y": "^6.3.1",
49 | "eslint-plugin-prettier": "^3.1.4",
50 | "eslint-plugin-react": "^7.20.0",
51 | "prettier": "^2.0.5"
52 | },
53 | "repository": {
54 | "type": "git",
55 | "url": "https://github.com/jamrants/donations"
56 | },
57 | "bugs": {
58 | "url": "https://github.com/jamrants/donations/issues"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/src/components/seo.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import PropTypes from "prop-types"
3 | import { Helmet } from "react-helmet"
4 | import { useStaticQuery, graphql } from "gatsby"
5 |
6 | function SEO({ description, lang, meta, title }) {
7 | const { site } = useStaticQuery(
8 | graphql`
9 | query {
10 | site {
11 | siteMetadata {
12 | title
13 | description
14 | author
15 | }
16 | }
17 | }
18 | `
19 | )
20 | const metaDescription = description || site.siteMetadata.description
21 | return (
22 |
67 |
73 |
74 | )
75 | }
76 | SEO.defaultProps = {
77 | lang: `en`,
78 | meta: [],
79 | description: ``,
80 | }
81 | SEO.propTypes = {
82 | description: PropTypes.string,
83 | lang: PropTypes.string,
84 | meta: PropTypes.arrayOf(PropTypes.object),
85 | title: PropTypes.string.isRequired,
86 | }
87 | export default SEO
88 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | # This action works with pull requests and pushes
4 | on:
5 | pull_request:
6 | push:
7 | branches:
8 | - production
9 | - develop
10 | repository_dispatch:
11 | types: [airtable]
12 |
13 | jobs:
14 | build:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Setup Node.js environment
18 | uses: actions/setup-node@v1.4.2
19 |
20 | - name: Checkout Repo
21 | uses: actions/checkout@v2.3.0
22 | with:
23 | # Make sure the actual branch is checked out when running on pull requests
24 | ref: ${{ github.head_ref }}
25 |
26 | - name: Extract Branch Name
27 | shell: bash
28 | run: echo "##[set-output name=branch;]$(echo ${GITHUB_REF#refs/heads/})"
29 | id: extract-branch
30 |
31 | - name: Extract Commit Name
32 | shell: bash
33 | run: echo "##[set-output name=commit;]$(git log -1 --pretty=%B)"
34 | id: extract-commit
35 |
36 | - name: Cache node_modules
37 | id: cache-node-modules
38 | uses: actions/cache@v2
39 | with:
40 | path: node_modules
41 | key: ${{ runner.os }}-build-npm-${{ hashFiles('**/package-lock.json') }}
42 | restore-keys: |
43 | ${{ runner.os }}-build-npm-
44 |
45 | - name: Install Dependencies
46 | if: steps.cache-node-modules.outputs.cache-hit != 'true'
47 | run: npm install
48 |
49 | - name: Build Site
50 | run: npm run build
51 | env:
52 | AIRTABLE_KEY: ${{ secrets.AIRTABLE_KEY }}
53 | BASE_ID: ${{ secrets.BASE_ID }}
54 | LOCATIONIQ: ${{ secrets.LOCATIONIQ }}
55 |
56 | - name: Deploy to Netlify
57 | id: netlify-deploy
58 | uses: nwtgck/actions-netlify@v1.1.5
59 | with:
60 | # Publish directory
61 | publish-dir: "./public"
62 | production-branch: production
63 | github-token: ${{ secrets.GITHUB_TOKEN }}
64 | deploy-message: Deploy from ${{ steps.extract-branch.outputs.branch }} | ${{ steps.extract-commit.outputs.commit }} | ${{ github.sha }}
65 | env:
66 | NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
67 | NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
68 |
69 | - name: Run Lighthouse Tests
70 | run: |
71 | npm install -g @lhci/cli@0.4.x
72 | lhci autorun --upload.target=temporary-public-storage --collect.url=${{ steps.netlify-deploy.outputs.deploy-url }} || echo "LHCI failed!"
73 | env:
74 | LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
75 |
--------------------------------------------------------------------------------
/static/icons/icons.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export default {
4 | chevron_down: {
5 | path: (
6 |
12 | ),
13 | viewBox: "0 0 11 8",
14 | },
15 | up_arrow: {
16 | path: (
17 |
23 | ),
24 | viewBox: "0 0 14 16",
25 | },
26 | down_arrow: {
27 | path: (
28 |
34 | ),
35 | viewBox: "0 0 14 16",
36 | },
37 | }
38 |
--------------------------------------------------------------------------------
/src/components/Footer.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { Box, PseudoBox, Text } from "@chakra-ui/core"
3 |
4 | const Footer = ({ ...params }) => (
5 |
16 |
24 | Get Informed about BLM →
25 |
26 |
27 | Built by{" "}
28 |
36 | David Li
37 |
38 | {", "}
39 |
47 | Jason Huang
48 |
49 | {", and "}
50 |
58 | James Ah Yong
59 |
60 | .
61 |
62 |
63 | Design inspired by{" "}
64 |
72 | @lachlanjc
73 |
74 | {"'s "}
75 |
83 | GunFunded.com
84 |
85 | .
86 |
87 |
88 |
95 | Open Source on Github
96 |
97 | {" | "}
98 |
105 | Hosted on Netlify
106 |
107 | {" | "}
108 |
115 | Buy us a Coffee
116 |
117 |
118 |
119 | )
120 |
121 | export default Footer
122 |
--------------------------------------------------------------------------------
/src/assets/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
19 |
25 |
26 |
45 |
48 |
49 |
51 |
52 |
54 | image/svg+xml
55 |
57 |
58 |
59 |
60 |
61 |
65 |
69 |
73 | $
84 |
85 |
86 |
87 |
--------------------------------------------------------------------------------
/src/components/Search/search.css:
--------------------------------------------------------------------------------
1 | .search-field::placeholder {
2 | color: #3c4858 !important;
3 | }
4 |
5 | .card-image {
6 | width: 45px !important;
7 | height: 45px !important;
8 | display: flex !important;
9 | justify-content: center;
10 | align-items: center;
11 | }
12 |
13 | .card-image > picture > img {
14 | height: auto !important;
15 | position: relative !important;
16 | }
17 |
18 | .card-image > div {
19 | display: none;
20 | }
21 |
22 | .card-image > img {
23 | height: auto !important;
24 | }
25 |
26 | @media screen and (min-width: 48em) {
27 | .card-image {
28 | width: 60px !important;
29 | height: 60px !important;
30 | }
31 | }
32 |
33 | @media screen and (min-width: 62em) {
34 | .card-image {
35 | width: 70px !important;
36 | height: 70px !important;
37 | }
38 | }
39 |
40 | @media screen and (min-width: 107em) {
41 | .card-image {
42 | width: 80px !important;
43 | height: 80px !important;
44 | }
45 | }
46 |
47 | @media screen and (max-width: 86.25em) and (min-width: 65.9375em) {
48 | .content-container {
49 | padding-left: 7%;
50 | padding-right: 7%;
51 | }
52 | }
53 |
54 | @media screen and (max-width: 65.9375em) and (max-width: 86.25em) {
55 | .content-container {
56 | padding-left: 5%;
57 | padding-right: 5%;
58 | }
59 | }
60 |
61 | @media screen and (min-width: 118em) {
62 | .content-container {
63 | padding-left: 20%;
64 | padding-right: 20%;
65 | }
66 | }
67 |
68 | @media screen and (min-width: 86.25em) and (max-width: 118em) {
69 | .content-container {
70 | padding-left: 17%;
71 | padding-right: 17%;
72 | }
73 |
74 | .padding-normal {
75 | padding-left: 17% !important;
76 | padding-right: 17% !important;
77 | }
78 | }
79 |
80 | @media screen and (min-width: 75em) and (max-width: 86.25em) {
81 | .content-container {
82 | padding-left: 12%;
83 | padding-right: 12%;
84 | }
85 | }
86 |
87 | @media screen and (max-width: 62em) and (min-width: 48em) {
88 | .content-container {
89 | padding-left: 20%;
90 | padding-right: 20%;
91 | }
92 |
93 | .padding-normal {
94 | padding-left: 10% !important;
95 | padding-right: 10% !important;
96 | }
97 | }
98 |
99 | @media screen and (max-width: 991px) and (min-width: 33.125em) {
100 | .content-container {
101 | padding-left: 10%;
102 | padding-right: 10%;
103 | }
104 | }
105 |
106 | /* The emerging W3C standard
107 | that is currently Firefox-only */
108 | * {
109 | scrollbar-width: thin;
110 | scrollbar-color: #f9fafc #121217;
111 | }
112 |
113 | /* Works on Chrome/Edge/Safari */
114 | *::-webkit-scrollbar {
115 | width: 16px;
116 | }
117 | *::-webkit-scrollbar-track {
118 | background: #121217;
119 | }
120 | *::-webkit-scrollbar-thumb {
121 | background-color: #f9fafc;
122 | border-radius: 20px;
123 | border: 4px solid #121217;
124 | }
125 |
126 | .flag-menu::-webkit-scrollbar {
127 | width: 10px;
128 | }
129 |
130 | .flag-menu::-webkit-scrollbar-thumb {
131 | border-width: 2px;
132 | }
133 |
134 | /** why section **/
135 | @media screen and (max-width: 748px) {
136 | .why-text {
137 | flex-direction: column;
138 | }
139 |
140 | .space {
141 | display: none;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, sex characteristics, gender identity and expression,
9 | level of experience, education, socio-economic status, nationality, personal
10 | appearance, race, religion, or sexual identity and orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | - Using welcoming and inclusive language
18 | - Being respectful of differing viewpoints and experiences
19 | - Gracefully accepting constructive criticism
20 | - Focusing on what is best for the community
21 | - Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | - The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | - Trolling, insulting/derogatory comments, and personal or political attacks
28 | - Public or private harassment
29 | - Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | - Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at jamrants@retrocraft.ca. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
72 |
73 | [homepage]: https://www.contributor-covenant.org
74 |
75 | For answers to common questions about this code of conduct, see
76 | https://www.contributor-covenant.org/faq
77 |
--------------------------------------------------------------------------------
/src/utils/geolocation.js:
--------------------------------------------------------------------------------
1 | import LocaleCurrency from "locale-currency"
2 |
3 | export const convertableCurrencies = [
4 | "CAD",
5 | "HKD",
6 | "ISK",
7 | "PHP",
8 | "DKK",
9 | "HUF",
10 | "CZK",
11 | "GBP",
12 | "RON",
13 | "SEK",
14 | "IDR",
15 | "INR",
16 | "BRL",
17 | "RUB",
18 | "HRK",
19 | "JPY",
20 | "THB",
21 | "CHF",
22 | "EUR",
23 | "MYR",
24 | "BGN",
25 | "TRY",
26 | "CNY",
27 | "NOK",
28 | "NZD",
29 | "ZAR",
30 | "USD",
31 | "MXN",
32 | "SGD",
33 | "AUD",
34 | "ILS",
35 | "KRW",
36 | "PLN",
37 | ]
38 |
39 | /**
40 | * Get the user's locale using navigator.language
41 | * @returns {{ lang: string, country: string, currency: string }} ISO language/country/currency codes
42 | */
43 | export const getLocale = () => {
44 | // get user country, sort through locales + income to adjust
45 | let locale = "en"
46 | if (typeof window !== "undefined") {
47 | locale =
48 | navigator.language ||
49 | navigator.browserLanguage ||
50 | (navigator.languages || ["en"])[0]
51 | }
52 |
53 | let lang = locale
54 | let country = null
55 | // 'en-US' -> ['en', 'US'] and account for zh-Hans_CN
56 | if (locale.includes("-")) {
57 | lang = locale.substring(0, 2)
58 | country = locale.slice(-2)
59 | }
60 | // if already used geolocation, use that country
61 | if (locationCache) {
62 | country = locationCache.country
63 | }
64 | return { lang, country, currency: getCurrency(country) }
65 | }
66 |
67 | /**
68 | * Get convertable currency of country
69 | * @param {string} country ISO country code
70 | */
71 | export const getCurrency = country => {
72 | const currency = LocaleCurrency.getCurrency(country)
73 | console.log(currency)
74 | if (convertableCurrencies.includes(currency)) {
75 | return currency
76 | }
77 | return "USD"
78 | }
79 |
80 | let ipLocationCache = null
81 | /**
82 | * Get user's locale based on IP location
83 | * @returns {Promise<{ lang: string, country: string, currency: string, postcode: string }>}
84 | */
85 | export const getIpLocation = async () => {
86 | if (ipLocationCache) return ipLocationCache
87 | const url = "https://ipapi.co/json"
88 | const res = await fetch(url, { headers: { "accept-language": "en" } })
89 | const data = await res.json()
90 | if (data.error) {
91 | throw new Error(data.reason)
92 | }
93 | ipLocationCache = {
94 | lang: getLocale().lang,
95 | country: data.country_code,
96 | postcode: data.postal,
97 | currency: getCurrency(data.country_code),
98 | }
99 | return ipLocationCache
100 | }
101 |
102 | let locationCache = null
103 | /**
104 | * Get the user's current location
105 | * @returns {Promise<{ country: string, currency: string, postcode: string }>} ISO country/currency codes and postal/ZIP code
106 | */
107 | export const getLocation = async () => {
108 | try {
109 | if (!navigator.geolocation) {
110 | throw new Error("Browser does not support geolocation")
111 | }
112 | if (locationCache) return locationCache
113 | const pos = await new Promise((res, rej) =>
114 | navigator.geolocation.getCurrentPosition(res, rej, {
115 | enableHighAccuracy: true,
116 | })
117 | )
118 | const { latitude, longitude } = pos.coords
119 | const url = `https://nominatim.openstreetmap.org/reverse?lat=${latitude}&lon=${longitude}&addressdetails=1&format=jsonv2`
120 | const res = await fetch(url)
121 | const { address } = await res.json()
122 | const country = address.country_code.toUpperCase()
123 | let { postcode } = address
124 | // overwrite with MSOA instead of postcode for UK
125 | // income data is provied at the MSOA level, not postcode level
126 | if (country === "GB") {
127 | const pUrl = `https://api.postcodes.io/postcodes?lat=${latitude}&lon=${longitude}&wideSearch&limit=1`
128 | // cors requires accept-language
129 | const pRes = await fetch(pUrl, { headers: { "accept-language": "en" } })
130 | const data = await pRes.json()
131 | if (data.result !== null) {
132 | postcode = data.result[0].msoa
133 | }
134 | }
135 | locationCache = { country, postcode, currency: getCurrency(country) }
136 | return locationCache
137 | } catch (e) {
138 | // fallback on IP location if exists
139 | if (!ipLocationCache) throw e
140 | return ipLocationCache
141 | }
142 | }
143 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
21 |
22 | [![Contributors][contributors-shield]][contributors-url]
23 | [![Forks][forks-shield]][forks-url]
24 | [![Stargazers][stars-shield]][stars-url]
25 | [![Issues][issues-shield]][issues-url]
26 | [![MIT License][license-shield]][license-url]
27 | [![CI][ci-shield]][ci-url]
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
Donations Revealed
39 |
40 |
41 | Corporations have made headlines with big pledges recently – tracking how much they'd be to the average person.
42 |
43 | View the Website »
44 |
45 |
46 | Explore the dataset
47 | ·
48 | Report Bug
49 | ·
50 | Request Feature
51 |
52 |
53 |
54 |
55 |
56 | ## Table of Contents
57 |
58 | - [Table of Contents](#table-of-contents)
59 | - [About The Project](#about-the-project)
60 | - [Built With](#built-with)
61 | - [Getting Started](#getting-started)
62 | - [Prerequisites](#prerequisites)
63 | - [Installation](#installation)
64 | - [Usage](#usage)
65 | - [Roadmap](#roadmap)
66 | - [Contributing](#contributing)
67 | - [License](#license)
68 | - [Contact](#contact)
69 | - [Acknowledgements](#acknowledgements)
70 |
71 |
72 |
73 | ## About The Project
74 |
75 |
80 |
81 | ### Built With
82 |
83 | - [Gatsby](https://www.gatsbyjs.org/)
84 | - [Airtable](https://airtable.com/)
85 | - [Netlify](https://www.netlify.com/)
86 |
87 |
88 |
89 | ## Getting Started
90 |
91 | To get a local copy up and running follow these simple steps.
92 |
93 | ### Prerequisites
94 |
95 | This is an example of how to list things you need to use the software and how to install them.
96 |
97 | - npm
98 |
99 | ```sh
100 | npm install npm@latest -g
101 | ```
102 |
103 | - Gatsby CLI
104 |
105 | ```sh
106 | npm install gatsby-cli -g
107 | ```
108 |
109 | - Airtable
110 |
111 | [Make a copy of our base](https://airtable.com/addBaseFromShare/shrb6pZwkGX6rLIQa?utm_source=airtable_shared_application), and make a note of your Airtable API key, and your Base ID.
112 |
113 | [You can find your API key under your profile](assets/api_key_instructions.png)
114 |
115 | [And you can find your Base ID under **help**](assets/base_id_instructions.png)
116 |
117 | ### Installation
118 |
119 | 1. Clone the repo
120 |
121 | ```sh
122 | git clone https://github.com/jamrants/donations.git
123 | ```
124 |
125 | 2. Install NPM packages
126 |
127 | ```sh
128 | npm install
129 | ```
130 |
131 | 3. Add Environment Variables
132 |
133 | Create a file named `.env.development` in the root of the repo, and add the following info:
134 |
135 | ```
136 | AIRTABLE_KEY=`yourAirtableKey`
137 | BASE_ID=`yourBaseId`
138 | ```
139 |
140 | 4. Run Gatsby
141 |
142 | ```sh
143 | gatsby develop
144 | ```
145 |
146 |
147 |
148 | ## Usage
149 |
150 | This will be filled out at a later date
151 |
152 |
155 |
156 |
157 |
158 | ## Roadmap
159 |
160 | See the [open issues](https://github.com/jamrants/donations/issues) for a list of proposed features (and known issues).
161 |
162 |
163 |
164 | ## Contributing
165 |
166 | Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
167 |
168 | We're using the [Gitflow Workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow).
169 | Because Github Actions doesn't let us run our CI on a fork, please work off of the Develop branch so we can make sure Master doesn't break.
170 |
171 | 1. Fork the Project
172 | 2. Checkout the Develop Branch (`git checkout develop`)
173 | 3. Create your Feature Branch (`git branch feature/AmazingFeature`)
174 | 4. Commit your Changes (`git commit -m 'Add some AmazingFeature'`)
175 | 5. Push to the Branch (`git push origin feature/AmazingFeature`)
176 | 6. Open a Pull Request to the Develop Branch
177 |
178 |
179 |
180 | ## License
181 |
182 | Distributed under the MIT License. See `LICENSE` for more information.
183 |
184 |
185 |
186 | ## Contact
187 |
188 | [@jamrants](https://instagram.com/jamrants) - jamrants@retrocraft.ca
189 |
190 | Project Link: [https://github.com/jamrants/donations](https://github.com/jamrants/donations)
191 |
192 |
193 |
194 | ## Acknowledgements
195 |
196 | - [David Li](https://github.com/davidli3100)
197 | - [James Ah Yong](https://github.com/RetroCraft)
198 | - [Jason Huang](https://github.com/err53)
199 |
200 |
201 |
202 |
203 | [contributors-shield]: https://img.shields.io/github/contributors/jamrants/donations.svg?style=flat-square
204 | [contributors-url]: https://github.com/jamrants/donations/graphs/contributors
205 | [forks-shield]: https://img.shields.io/github/forks/jamrants/donations.svg?style=flat-square
206 | [forks-url]: https://github.com/jamrants/donations/network/members
207 | [stars-shield]: https://img.shields.io/github/stars/jamrants/donations.svg?style=flat-square
208 | [stars-url]: https://github.com/jamrants/donations/stargazers
209 | [issues-shield]: https://img.shields.io/github/issues/jamrants/donations.svg?style=flat-square
210 | [issues-url]: https://github.com/jamrants/donations/issues
211 | [license-shield]: https://img.shields.io/github/license/jamrants/donations.svg?style=flat-square
212 | [license-url]: https://github.com/jamrants/donations/blob/master/LICENSE.txt
213 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555
214 | [linkedin-url]: https://linkedin.com/in/othneildrew
215 | [ci-shield]: https://github.com/jamrants/donations/workflows/CI/badge.svg
216 | [ci-url]: https://github.com/jamrants/donations/actions?query=workflow%3ACI
217 | [product-screenshot]: images/screenshot.png
218 |
--------------------------------------------------------------------------------
/src/pages/about.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 | import { DarkMode, Box, Text, List, ListItem } from "@chakra-ui/core"
3 | import Layout from "../components/Layout"
4 | import SEO from "../components/seo"
5 |
6 | const H1 = ({ children }) => (
7 |
14 | {children}
15 |
16 | )
17 |
18 | const H2 = ({ children }) => (
19 |
26 | {children}
27 |
28 | )
29 |
30 | const Paragraph = ({ children }) => (
31 |
32 | {children}
33 |
34 | )
35 |
36 | const Link = ({ children, href }) => (
37 |
45 | {children}
46 |
47 | )
48 |
49 | const About = () => {
50 | return (
51 |
52 |
56 |
60 |
61 |
62 | Why?
63 |
64 | Businesses have made a lot of headlines recently with huge
65 | donations.{" "}
66 |
67 | Have you ever felt as if you just can't make the same level of
68 | impact?
69 | {" "}
70 | Individuals, like us, who want to help the cause, feel like our
71 | $10 is nothing compared to a celebrity or company's $10 million.
72 | Why donate if we can only make one-millionth of the impact? But
73 | when you scale down these seven-figure donations from
74 | billion-dollar multinational to the average household, many of
75 | these donations have a proportionate impact of our donating less
76 | than $100.
77 |
78 |
79 | A company like Home Depot gets loads of good PR from a million
80 | dollar pledge, but they make gross profit of around $40 billion.
81 | That's a donation of just 0.003% of their income. 0.003% of the
82 | average Canadian's income comes out to less than a toonie.{" "}
83 |
84 | We can all spare a toonie, can't we?
85 |
86 |
87 |
88 | We made this site to{" "}
89 |
90 | demonstrate how every one of us can make the same proportional
91 | impact
92 |
93 | {". "}
94 | If companies, in their performative activism, can pledge pocket
95 | change and make a difference, so can we.
96 |
97 |
98 |
99 | Take action and donate or sign a petition today.
100 |
101 |
102 |
103 |
104 | Methodology
105 |
106 | Corporate and personal finances are two wildly different worlds.
107 | Because funny corporate accounting tends to skew taxable income,
108 | we chose to compare pre-tax figures that we felt best represented
109 | the amount of money that is under the company/person's control to
110 | decide spending.
111 |
112 |
113 | Many businesses post negative taxable revenues (e.g. Uber) while
114 | still clearly generating revenue, so we used a measure of revenue
115 | less cost of revenue. Public companies have wide ability to choose
116 | exactly what figures to report, so the cost of revenue was not
117 | always provided. We used the following list of figures, in order
118 | of priority, moving down the list if the previous figure was not
119 | available, to calculate "profit".
120 |
121 |
122 |
130 | Revenue less cost of revenue
131 | Gross profit
132 | Operating income
133 |
134 | Third-party estimates of gross revenue (for private companies)
135 |
136 |
137 |
138 |
139 | In the case of brands being wholly-owned subsidiaries, we
140 | attribute the whole donation to the parent company. For joint
141 | donations, we assume an equal split between organizations. If a
142 | company pledges to donate over a number of years, the donation
143 | value shown is the whole pledge divided by the number of years. If
144 | a number of years is not given, we divide by five.
145 |
146 |
147 | In the personal world, we chose to this metric with total income,
148 | since there is no cost of revenue in personal finances. The{" "}
149 |
150 | Canadian
151 |
152 | {", "}
153 |
154 | American
155 |
156 | {", and "}
157 |
158 | British
159 | {" "}
160 | censuses report median household total income by{" "}
161 | FSAs{" "}
162 | (the first three letters of postal codes){", "}
163 |
164 | ZIP codes
165 |
166 | , and{" "}
167 | MSOAs,
168 | respectively.
169 |
170 |
171 | This website includes information copyright the Canada Post
172 | Corporation (2016). This website uses United States Census
173 | Bureau data but is not endorsed or certified by the Census
174 | Bureau. This website contains data from the United Kingdom
175 | Office for National Statistics licensed under the Open
176 | Government Licence v3.0 and Royal Mail data under Royal Mail
177 | copyright and database right (2020).
178 |
179 |
180 |
181 |
182 | The adjusted figure shown is calculated by taking the percent of
183 | profits donated and multiplying by the local median income.
184 |
185 |
186 |
187 |
188 | Are we missing a donation?{" "}
189 |
190 | Let us know
191 |
192 | .
193 |
194 |
195 |
196 |
197 | Want to talk?{" "}
198 | Email us!
199 |
200 |
201 |
202 |
203 | View all the raw data{" "}
204 | here.
205 |
206 |
207 |
208 |
209 |
210 | )
211 | }
212 |
213 | export default About
214 |
--------------------------------------------------------------------------------
/src/components/FlagMenu.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react"
2 | import {
3 | Menu,
4 | MenuButton,
5 | MenuList,
6 | MenuItem,
7 | Icon,
8 | Input,
9 | PseudoBox,
10 | Skeleton,
11 | useToast,
12 | } from "@chakra-ui/core"
13 | import ReactCountryFlag from "react-country-flag"
14 | import { getLocation } from "../utils/geolocation"
15 |
16 | const renderMenuItems = (locales, homeLocale, activeLocale, onClick) => {
17 | const { country } = homeLocale
18 | // countries with implemented postal-level data
19 | const postalCountries = ["US", "CA", "GB"]
20 | return [
21 | // custom income
22 | onClick("mine")}
26 | key="mine"
27 | fontSize={["16px", "18px", "20px", "22px", "24px"]}
28 | >
29 |
30 |
37 | {" my household"}
38 |
39 | ,
40 | // geolocation + ZIP/FSA income
41 | postalCountries.includes(country) && (
42 | onClick("geo")}
46 | key="geo"
47 | fontSize={["16px", "18px", "20px", "22px", "24px"]}
48 | >
49 |
50 |
57 | {" household near me"}
58 |
59 |
60 | ),
61 | ...locales.map((locale, i) => {
62 | return (
63 | onClick(locale)}
68 | key={locale.Code}
69 | fontSize={["16px", "18px", "20px", "22px", "24px"]}
70 | >
71 |
72 | {" "}
79 | {locale.Demonym}
80 | {" household"}
81 |
82 |
83 | )
84 | }),
85 | ]
86 | }
87 |
88 | const FlagMenu = ({ onClick, locales, activeLocale, homeLocale, setHome }) => {
89 | const [mine, setMine] = useState(null)
90 | const [loading, setLoading] = useState(false)
91 | const [geoCache, setGeoCache] = useState(null)
92 | const toast = useToast()
93 |
94 | const onChange = (locale, income = null) => {
95 | if (locale === "mine") {
96 | if (income === null) {
97 | if (mine === null) {
98 | // initialize to country median income
99 | const countryLocale = locales.find(l => l.Code === homeLocale.country)
100 | income = countryLocale ? countryLocale.Income : 0
101 | } else {
102 | income = mine
103 | }
104 | }
105 | onClick({
106 | Income: income,
107 | Language: homeLocale.lang,
108 | Code: homeLocale.country,
109 | Demonym: "my",
110 | Currency: homeLocale.currency,
111 | Measure: "household income",
112 | Custom: true,
113 | })
114 | setMine(income)
115 | } else if (locale === "geo") {
116 | const newLocale = {
117 | Language: homeLocale.lang,
118 | Code: homeLocale.country,
119 | Currency: homeLocale.currency,
120 | Measure: "median household income",
121 | Demonym: "my neighborhood's",
122 | Geo: true,
123 | }
124 | if (geoCache) {
125 | newLocale.Income = geoCache.income
126 | newLocale.Postcode = geoCache.code
127 | onClick(newLocale)
128 | } else {
129 | setLoading(true)
130 | getLocation()
131 | .then(({ country, currency, postcode }) => {
132 | setHome({ lang: homeLocale.lang, country, currency })
133 | newLocale.Currency = currency
134 | newLocale.Code = country
135 | return fetch(
136 | `https://us-central1-donations-exposed.cloudfunctions.net/income?country=${country}&postcode=${postcode}`
137 | )
138 | })
139 | .then(res => res.json())
140 | .then(data => {
141 | newLocale.Income = data.income
142 | newLocale.Postcode = data.code
143 | console.log(newLocale)
144 | onClick(newLocale)
145 | setGeoCache(data)
146 | })
147 | .catch(err => {
148 | toast({
149 | title: "Oops!",
150 | description:
151 | "Either we couldn't find your location or we don't have regional data for your area. Input your own income data instead.",
152 | status: "error",
153 | duration: 9000,
154 | isClosable: true,
155 | })
156 | console.error(err)
157 | onChange("mine")
158 | })
159 | .finally(() => setLoading(false))
160 | }
161 | } else {
162 | onClick(locale)
163 | }
164 | }
165 |
166 | const onMineChange = e => {
167 | setMine(e.target.value)
168 | onChange("mine", e.target.value)
169 | }
170 |
171 | const formatter = new Intl.NumberFormat(undefined, {
172 | style: "currency",
173 | currency: activeLocale.Currency,
174 | })
175 | let currencySymbol = activeLocale.Currency
176 | let symbolAfter = true
177 | // TODO: polyfill
178 | if (formatter.formatToParts) {
179 | const currencyFormat = formatter.formatToParts(1)
180 | currencySymbol = currencyFormat.find(_ => _.type === "currency").value
181 | symbolAfter =
182 | currencyFormat.findIndex(_ => _.type === "currency") >
183 | currencyFormat.findIndex(_ => _.type === "integer")
184 | }
185 |
186 | return (
187 |
188 |
189 | {!activeLocale.Custom && " the average "}
190 |
197 |
205 |
211 |
218 |
219 | {activeLocale.Geo
220 | ? " household near me"
221 | : ` ${activeLocale.Demonym} household`}
222 |
228 |
229 |
230 |
240 | {renderMenuItems(locales, homeLocale, activeLocale, onChange)}
241 |
242 |
243 | {activeLocale.Demonym === "my" && (
244 | <>
245 | , with an income of{" "}
246 |
252 | {!symbolAfter && currencySymbol}
253 |
268 | {symbolAfter && currencySymbol}
269 |
270 | ,{" "}
271 | >
272 | )}
273 |
274 | )
275 | }
276 |
277 | export default FlagMenu
278 |
--------------------------------------------------------------------------------
/src/components/DonationSlider.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from "react"
2 | import {
3 | Slider,
4 | SliderFilledTrack,
5 | SliderTrack,
6 | SliderThumb,
7 | PseudoBox,
8 | MenuItem,
9 | Menu,
10 | MenuList,
11 | MenuButton,
12 | Text,
13 | Icon,
14 | } from "@chakra-ui/core"
15 | import { useMediaQuery } from "react-responsive"
16 | import CustomButton from "./Button"
17 |
18 | const logScale = (min, max, x) => {
19 | const n = 1.0 - Math.log(min) / Math.log(max)
20 | return min * Math.pow(max, (n * x) / 100)
21 | }
22 | const invLogScale = (min, max, x) => {
23 | return 1.0 - Math.log(x / max) / Math.log(min / max)
24 | }
25 | const fakeLog = x => {
26 | if (x >= 0 && x <= 25) return x
27 | if (x > 25 && x <= 75) return 1.5 * x - 12.5
28 | return 20 * x - 1400
29 | }
30 |
31 | const causes = {
32 | blm: {
33 | name: "Black Lives Matter",
34 | url:
35 | "https://secure.actblue.com/donate/ms_blm_homepage_2019?amount=%s&ref=donations.exposed",
36 | },
37 | coc: {
38 | name: "Color of Change",
39 | url:
40 | "https://secure.actblue.com/contribute/page/support-us?amount=%s&ref=donations.exposed",
41 | },
42 | eji: {
43 | name: "the Equal Justice Initiative",
44 | url:
45 | "https://support.eji.org/give/153413/#!/donation/checkout?amount=%s&ref=donations.exposed",
46 | },
47 | naacpLdf: {
48 | name: "the NAACP Legal Defense Fund",
49 | url:
50 | "https://org2.salsalabs.com/o/6857/p/salsa/donation/common/public/?donate_page_KEY=15780&amount=%s&ref=donations.exposed",
51 | },
52 | }
53 |
54 | const DonationSlider = ({ locale, corporations, overrideValue }) => {
55 | const [rawValue, setRawValue] = useState(0)
56 | const [causeValue, setCauseValue] = useState(Object.keys(causes)[0])
57 | // it breaks for some reason if this isn't stored in state AND I DONT KNOW WHY
58 | const [corps] = useState(corporations)
59 | const mobile = useMediaQuery({ maxWidth: 650 })
60 |
61 | const [min, setMin] = useState(0)
62 | const [max, setMax] = useState(0)
63 | const value = logScale(min, max, rawValue)
64 |
65 | const headerRef = useRef()
66 | useEffect(() => {
67 | if (overrideValue > 0) {
68 | setRawValue(invLogScale(min, max, overrideValue) * 100)
69 | window.scrollTo({
70 | left: 0,
71 | top: headerRef.current.offsetTop,
72 | behavior: "smooth",
73 | })
74 | }
75 | }, [overrideValue])
76 |
77 | const [formatter, setFormatter] = useState(new Intl.NumberFormat())
78 | const [intFormatter, setIntFormatter] = useState(new Intl.NumberFormat())
79 | useEffect(() => {
80 | const currency = locale.Currency || "USD"
81 | setMin(
82 | Number(
83 | (locale.Income * corps[corps.length - 1].Percent_Profits).toPrecision(1)
84 | )
85 | )
86 | setMax(Number((locale.Income * corps[0].Percent_Profits).toPrecision(1)))
87 | setFormatter(
88 | new Intl.NumberFormat(undefined, {
89 | style: "currency",
90 | currency,
91 | })
92 | )
93 | setIntFormatter(
94 | new Intl.NumberFormat(undefined, {
95 | style: "currency",
96 | minimumFractionDigits: 0,
97 | currency,
98 | })
99 | )
100 | }, [locale])
101 | const formattedValue = formatter.format(value || 0)
102 |
103 | const notches = []
104 | for (let i = 0; ; i++) {
105 | const notch = !mobile
106 | ? Math.pow(2, (i - 1) % 2) * 5 * Math.pow(10, Math.floor((i - 1) / 2)) // OEIS A268100 (1/5/10...)
107 | : // ? (Math.pow(i % 3, 2) + 1) * Math.pow(10, Math.floor(i / 3)) // OEIS A051109 (1/2/5/10...)
108 | Math.pow(10, i)
109 | if (notch >= max) break
110 | if (notch > min) notches.push(notch)
111 | }
112 | const corp = corps.find(_ => value > _.Percent_Profits * locale.Income)
113 |
114 | const makeDonation = async () => {
115 | let USD = value
116 | if (locale.Currency !== "USD") {
117 | const api = await fetch("https://api.exchangeratesapi.io/latest?base=USD")
118 | const { rates } = await api.json()
119 | USD = value / rates[locale.Currency]
120 | }
121 | // open new tab + log plausible event
122 | fetch("https://us-central1-donations-exposed.cloudfunctions.net/donate", {
123 | method: "POST",
124 | body: JSON.stringify({
125 | id: causes[causeValue].name,
126 | amount: parseFloat(USD.toFixed(2)),
127 | }),
128 | headers: {
129 | "Content-Type": "application/json; charset=utf-8",
130 | },
131 | credentials: "same-origin",
132 | }).catch(err => console.error(err))
133 | window.plausible("Donation")
134 | const target = causes[causeValue].url.replace("%s", USD.toFixed(2))
135 | window.open(target, "_blank")
136 | }
137 |
138 | return (
139 | <>
140 |
147 | Make a Donation
148 |
149 |
150 |
158 | Every dollar counts. We're not trying to shame companies for donating
159 | pennies on the dollar.
160 | If you haven't donated yet, donate some pennies on yours to{" "}
161 |
173 | {causes[causeValue].name}
174 |
175 |
176 |
177 |
187 | {Object.entries(causes).map(([id, cause]) => (
188 | {
195 | setCauseValue(id)
196 | }}
197 | >
198 | {cause.name}
199 |
200 | ))}
201 |
202 |
203 |
213 |
214 |
219 |
224 |
225 | {notches.map((notch, i) => {
226 | const percent = invLogScale(min, max, notch)
227 | return (
228 | = notch ? "snow" : "slate"}
239 | // six significant figures for accuracy with KRW/JPY
240 | onClick={() =>
241 | setRawValue(Math.ceil(percent * 1000000) / 10000)
242 | }
243 | >
244 | {intFormatter.format(notch)}
245 |
246 | )
247 | })}
248 |
249 |
250 |
251 |
258 | Donate {formattedValue}
259 |
260 | {corp && (
261 |
262 |
267 | That's more than {corp.Name}!
268 |
269 |
270 | )}
271 |
272 | >
273 | )
274 | }
275 |
276 | export default DonationSlider
277 |
--------------------------------------------------------------------------------
/src/components/donationCard.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react"
2 | import {
3 | Box,
4 | Image,
5 | Text,
6 | PseudoBox,
7 | Tooltip,
8 | Popover,
9 | PopoverTrigger,
10 | PopoverContent,
11 | } from "@chakra-ui/core"
12 | import { graphql } from "gatsby"
13 | import Img from "gatsby-image"
14 |
15 | const DonationCard = ({
16 | image,
17 | name,
18 | amount,
19 | donationCurrency,
20 | profits,
21 | percent,
22 | locale,
23 | donationNote,
24 | donationSources,
25 | profitNote,
26 | profitSources,
27 | onClick,
28 | }) => {
29 | amount = parseInt(amount)
30 | profits = parseFloat(profits)
31 | const [zIndex, setZIndex] = useState(0)
32 | const click = () => {
33 | // scrolling handled in slider
34 | onClick(percent * locale.Income + 0.01)
35 | }
36 | return (
37 |
51 |
52 |
60 |
61 |
62 |
63 |
72 | {name}
73 |
74 |
80 |
81 |
82 |
83 | ${(amount * 1000).toLocaleString("en-us")}
84 |
85 |
86 |
96 |
97 | {donationNote ? {donationNote} : <>>}
98 | {donationSources.split("\n").length > 1 ? (
99 | donationSources.split("\n").map((source, index) => (
100 | <>
101 |
113 | Source {index + 1}
114 |
115 |
116 | >
117 | ))
118 | ) : (
119 |
126 | Source
127 |
128 | )}
129 |
130 |
131 |
132 |
133 | {` ${donationCurrency}`}
134 |
135 |
136 |
137 |
138 |
143 |
144 |
149 | {(percent * 100).toFixed(3)}%
150 |
151 |
156 | of{" "}
157 | {
161 | setZIndex(1)
162 | }}
163 | onClose={() => {
164 | setZIndex(0)
165 | }}
166 | >
167 |
168 |
169 | profits
170 |
171 |
172 |
182 |
183 | {profitNote ? {profitNote} : <>>}
184 | {profitSources.split("\n").length > 1 ? (
185 | profitSources.split("\n").map((source, index) => (
186 | <>
187 |
199 | Source {index + 1}
200 |
201 |
202 | >
203 | ))
204 | ) : (
205 |
212 | Source
213 |
214 | )}
215 |
216 |
217 |
218 | {" donated"}
219 |
220 |
221 |
222 |
227 | {(locale.Income * percent > 0
228 | ? locale.Income * percent
229 | : 0
230 | ).toLocaleString(undefined, {
231 | style: "currency",
232 | currency: locale.Currency,
233 | })}
234 |
235 |
240 | adjusted to{" "}
241 | {
245 | setZIndex(1)
246 | }}
247 | onClose={() => {
248 | setZIndex(0)
249 | }}
250 | >
251 |
252 |
253 | {locale.Custom ? "my" : "avg."} income
254 |
255 |
256 |
266 |
267 | {locale.Demonym} {locale.Measure} of{" "}
268 | {locale.Income.toLocaleString(undefined, {
269 | style: "currency",
270 | currency: locale.Currency ? locale.Currency : "USD",
271 | })}{" "}
272 | × {(percent * 100).toFixed(3)}%
273 |
274 |
275 |
276 |
277 |
278 |
279 |
280 | )
281 | }
282 |
283 | export default DonationCard
284 |
--------------------------------------------------------------------------------
/src/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react"
2 |
3 | import { graphql } from "gatsby"
4 | import {
5 | Box,
6 | Text,
7 | DarkMode,
8 | Skeleton,
9 | Icon,
10 | MenuButton,
11 | Menu,
12 | MenuList,
13 | MenuItem,
14 | PseudoBox,
15 | } from "@chakra-ui/core"
16 | import Layout from "../components/Layout"
17 | import SearchField from "../components/Search/Search"
18 | import "../components/Search/search.css"
19 | import DonationCard from "../components/donationCard"
20 | import CustomButton from "../components/Button"
21 | import DataSort from "react-data-sort"
22 | import SEO from "../components/seo"
23 | import FlagMenu from "../components/FlagMenu"
24 | import DonationSlider from "../components/DonationSlider"
25 | import { getLocale, getIpLocation } from "../utils/geolocation"
26 |
27 | const Home = ({ data }) => {
28 | const [filteredCorporations, setFilteredCorporations] = useState(
29 | data.allAirtable.nodes
30 | )
31 | const [localeList, setLocaleList] = useState(
32 | data.allAirtableCountryIncomes.edges.map(node => node.node.data)
33 | )
34 | const [activeLocale, setActiveLocale] = useState()
35 | const [homeLocale, setHomeLocale] = useState()
36 |
37 | // Slider
38 | const [sliderOverrideValue, setSliderOverrideValue] = useState()
39 |
40 | // Search Field
41 | const [searchValue, setSearchValue] = useState("")
42 |
43 | // Sorting states
44 | const [sortByText, setSortByText] = useState("% Profit Donated")
45 | const [sortByField, setSortByField] = useState("Percent_Profits")
46 | const [sortType, setSortType] = useState("asc")
47 | const [activePage, setActivePage] = useState(0)
48 | const [itemsPerPage, setItemsPerPage] = useState(20)
49 |
50 | const searchFieldOnChange = e => {
51 | setSearchValue(e.target.value)
52 | }
53 |
54 | useEffect(() => {
55 | // default to english if not exist
56 | let selected = localeList.find(l => l.Language === "en")
57 | let home = { lang: "en", country: "US", currency: "USD" }
58 | // first try IP location
59 | getIpLocation()
60 | .then(location => {
61 | home = location
62 | const locale = localeList.find(l => l.Code === location.country)
63 | if (locale) {
64 | selected = locale
65 | } else {
66 | throw new Error("Unknown country, falling back on browser")
67 | }
68 | })
69 | .catch(() => {
70 | // fallback to browser language
71 | const locale = getLocale()
72 | if (locale.country) {
73 | const byCountry = localeList.find(l => l.Code === locale.country)
74 | if (byCountry) {
75 | selected = byCountry
76 | home = locale
77 | } else {
78 | const byLang = localeList.find(l => l.Language === locale.lang)
79 | if (byLang) {
80 | selected = byLang
81 | home = {
82 | lang: locale.lang,
83 | country: byLang.Code,
84 | currency: byLang.Currency,
85 | }
86 | }
87 | }
88 | }
89 | })
90 | .finally(() => {
91 | console.log(home, selected)
92 | setHomeLocale(home)
93 | setActiveLocale(selected)
94 | })
95 | }, [localeList])
96 |
97 | // sorting functions
98 | const toggleSortType = () => {
99 | setSortType(sortType === "asc" ? "desc" : "asc")
100 | }
101 |
102 | // set sortbyfield and text
103 | const setSortBy = field => {
104 | setItemsPerPage(20)
105 | if (field === "Name") {
106 | setSortByField(`data.${field}`)
107 | setSortByText("Name")
108 | }
109 |
110 | if (field === "Percent_Profits") {
111 | setSortByField(`data.${field}`)
112 | setSortByText("% Profit Donated")
113 | }
114 |
115 | if (field === "Donation__thousands_") {
116 | setSortByField(`data.${field}`)
117 | setSortByText("Amount Donated")
118 | }
119 | }
120 |
121 | const loadMoreItems = () => {
122 | setItemsPerPage(itemsPerPage + 20)
123 | }
124 |
125 | return (
126 |
127 |
131 | Donation
132 |
136 | $
137 | {" "}
138 | Revealed
139 | >
140 | }
141 | subtitle={
142 | <>
143 | Corporations have made headlines with big donations recently. How
144 | much would{" "}
145 | {activeLocale ? (
146 |
153 | ) : (
154 |
159 | )}{" "}
160 | need to match their donation?
161 | >
162 | }
163 | >
164 |
165 |
166 |
173 | {activeLocale ? (
174 | _.data)
178 | .sort((a, b) => b.Percent_Profits - a.Percent_Profits)}
179 | overrideValue={sliderOverrideValue}
180 | />
181 | ) : (
182 | <>
183 |
189 |
195 | >
196 | )}
197 |
198 |
199 |
200 |
209 |
216 | What?
217 |
218 |
235 | When you donate, you choose to spend a percent of your money. When
236 | a company donates, it chooses to spend a percent of its money. We
237 | can compare these two numbers:
238 |
239 |
246 |
247 | its donation
248 |
249 |
250 | ÷ its profit
251 |
252 |
253 | × your income
254 |
255 | = your donation
256 |
257 |
264 | More on methodology →
265 |
266 |
267 |
268 |
269 |
276 |
283 | Find a Corporation
284 |
285 |
291 | Select a card to make an equivalent donation.
292 |
293 |
298 |
299 |
300 |
301 |
302 | Sort by {sortByText}{" "}
303 |
304 |
305 |
306 |
312 | {
314 | setSortBy("Name")
315 | }}
316 | >
317 | Name
318 |
319 | {
321 | setSortBy("Percent_Profits")
322 | }}
323 | >
324 | % Profit Donated
325 |
326 | {
328 | setSortBy("Donation__thousands_")
329 | }}
330 | >
331 | Amount Donated
332 |
333 |
334 |
335 |
339 |
343 |
344 |
345 |
346 |
347 |
354 | {activeLocale ? (
355 | {
364 | return data.map((corporation, i) => {
365 | if (
366 | corporation.data.Donation__thousands_ &&
367 | corporation.data.Gross_Profit__millions_
368 | ) {
369 | return (
370 |
388 | )
389 | }
390 | })
391 | }}
392 | />
393 | ) : (
394 | <>
395 |
401 |
407 |
413 |
419 |
425 |
431 |
437 |
443 | >
444 | )}
445 |
446 | {!searchValue ? (
447 |
454 |
455 | {itemsPerPage >= filteredCorporations.length
456 | ? "No Donations Left"
457 | : "Load More"}
458 |
459 |
460 | ) : (
461 |
462 | )}
463 |
464 |
465 | )
466 | }
467 |
468 | export const query = graphql`
469 | query corporationsQuery {
470 | allAirtable(
471 | filter: {
472 | table: { eq: "Corporations" }
473 | data: {
474 | Donation__thousands_: { gt: 0 }
475 | Gross_Profit__millions_: { gt: 0 }
476 | }
477 | }
478 | sort: { fields: data___Percent_Profits, order: ASC }
479 | ) {
480 | nodes {
481 | data {
482 | Currency
483 | Date
484 | Donation_Note
485 | Donation__thousands_
486 | Name
487 | Gross_Profit__millions_
488 | Profit_Note
489 | Profit_Currency
490 | Profit_Sources
491 | Profit_Year
492 | Sources
493 | Donation_Recipients
494 | Logo {
495 | localFiles {
496 | childImageSharp {
497 | fluid(maxWidth: 80) {
498 | ...GatsbyImageSharpFluid
499 | ...GatsbyImageSharpFluidLimitPresentationSize
500 | }
501 | }
502 | }
503 | }
504 | Percent_Profits
505 | Brands
506 | }
507 | }
508 | }
509 |
510 | allAirtableCountryIncomes(
511 | filter: { data: {} }
512 | sort: { fields: data___Demonym, order: ASC }
513 | ) {
514 | edges {
515 | node {
516 | data {
517 | Demonym
518 | Language
519 | Code
520 | Income
521 | Measure
522 | Currency
523 | }
524 | }
525 | }
526 | }
527 | }
528 | `
529 | export default Home
530 |
--------------------------------------------------------------------------------