diff --git a/.eslintignore b/.eslintignore
index 25e8090..2041a34 100644
--- a/.eslintignore
+++ b/.eslintignore
@@ -1 +1,2 @@
prisma/generated/
+__playground__/
diff --git a/.eslintrc.json b/.eslintrc.json
index b2adf13..a5fc36a 100644
--- a/.eslintrc.json
+++ b/.eslintrc.json
@@ -1,34 +1,39 @@
{
- "extends": ["esnext", "plugin:react/recommended", "prettier"],
- "plugins": ["react-hooks", "babel"],
- "settings": {
- "react": {
- "version": "16.9.0"
- }
- },
- "parser": "babel-eslint",
- "parserOptions": {
- "ecmaFeatures": {
- "jsx": true,
- "modules": true
- }
- },
- "env": {
- "browser": true,
- "es6": true,
- "node": true
- },
+ "extends": ["kentcdodds", "kentcdodds/react", "kentcdodds/jsx-a11y"],
"rules": {
"react/prop-types": 0,
- "no-use-before-define": ["error", { "variables": false }],
- "no-console": "error",
- "import/no-commonjs": 0,
+ "no-use-before-define": 0,
"react/react-in-jsx-scope": 0,
- "react/no-unescaped-entities": 0,
- "import/no-namespace": 0,
- "import/no-nodejs-modules": 0
+ "react-hooks/exhaustive-deps": 0,
+ "consistent-return": 0
},
"globals": {
"cssTheme": "readonly"
- }
+ },
+ "overrides": [
+ {
+ "files": ["**/*.ts"],
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["@typescript-eslint"],
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/eslint-recommended",
+ "plugin:@typescript-eslint/recommended",
+ "prettier",
+ "prettier/@typescript-eslint"
+ ],
+ "rules": {
+ "@typescript-eslint/ban-ts-ignore": 0,
+ "@typescript-eslint/no-explicit-any": 0
+ },
+ "settings": {
+ "import/resolver": {
+ "typescript": {
+ "alwaysTryTypes": true,
+ "directory": "tsconfig.json"
+ }
+ }
+ }
+ }
+ ]
}
diff --git a/.gitignore b/.gitignore
index ad46b30..1d235be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -56,6 +56,13 @@ typings/
# dotenv environment variables file
.env
+*.env
+
+
+# Config folder
+.config
# next.js build output
.next
+
+__playground__/
diff --git a/.prettierrc b/.prettierrc
index 083ae08..906a630 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,5 +1,17 @@
{
+ "arrowParens": "avoid",
+ "bracketSpacing": true,
+ "htmlWhitespaceSensitivity": "css",
+ "insertPragma": false,
+ "jsxBracketSameLine": false,
+ "jsxSingleQuote": false,
+ "printWidth": 80,
+ "proseWrap": "always",
+ "quoteProps": "as-needed",
+ "requirePragma": false,
"semi": false,
- "singleQuote": false,
- "trailingComma": "es5"
+ "singleQuote": true,
+ "tabWidth": 2,
+ "trailingComma": "all",
+ "useTabs": false
}
diff --git a/README.md b/README.md
index 792e8bb..e9c6559 100644
--- a/README.md
+++ b/README.md
@@ -1 +1 @@
-# Snapeat
\ No newline at end of file
+# SnapEat
diff --git a/app.json b/app.json
new file mode 100644
index 0000000..35d74c5
--- /dev/null
+++ b/app.json
@@ -0,0 +1,10 @@
+{
+ "name": "snapeat",
+ "description": "",
+ "scripts": {},
+ "buildpacks": [
+ {
+ "url": "heroku/nodejs"
+ }
+ ]
+}
diff --git a/apps/AuthenticatedApp.js b/apps/AuthenticatedApp.js
new file mode 100644
index 0000000..2093252
--- /dev/null
+++ b/apps/AuthenticatedApp.js
@@ -0,0 +1,35 @@
+import React, { useEffect } from 'react'
+import AuthenticatedAppProviders from '../context/AuthenicatedAppProviders'
+import { useAuth } from '../context/authContext'
+
+import views from '../views'
+
+import { CHANGE_VIEW, HOME, ONBOARDING } from '../utils/constants'
+
+import { useRouteState, useRouteDispatch } from '../context/routeContext'
+
+const AuthenticatedApp = () => {
+ const { currentView } = useRouteState()
+ const routeDispatch = useRouteDispatch()
+
+ const { snapeatUser } = useAuth()
+
+ useEffect(() => {
+ routeDispatch({
+ type: CHANGE_VIEW,
+ view: snapeatUser ? HOME : ONBOARDING,
+ })
+
+ return () => ({})
+ }, [])
+
+ const CurrentView = views[currentView]
+
+ return (
+
+
+
+ )
+}
+
+export default AuthenticatedApp
diff --git a/apps/UnauthenticatedApp.js b/apps/UnauthenticatedApp.js
new file mode 100644
index 0000000..9c84f35
--- /dev/null
+++ b/apps/UnauthenticatedApp.js
@@ -0,0 +1,15 @@
+import React from 'react'
+
+import views from '../views'
+
+import { useRouteStateUnauth } from '../context/unauthRouteContext'
+
+const UnauthenticatedApp = () => {
+ const { currentView } = useRouteStateUnauth()
+
+ const CurrentView = views[currentView]
+
+ return
+}
+
+export default UnauthenticatedApp
diff --git a/components/BugButton.js b/components/BugButton.js
new file mode 100644
index 0000000..c15d85d
--- /dev/null
+++ b/components/BugButton.js
@@ -0,0 +1,24 @@
+import styled from 'styled-components'
+
+import bugIcon from '../public/icons/bug_icon.svg'
+
+const BugButton = () => {
+ return (
+
+
+
+ )
+}
+
+const Icon = styled.img.attrs({
+ className: 'absolute w-10',
+})`
+ top: 2%;
+ right: 20%;
+`
+
+export default BugButton
diff --git a/components/Header.js b/components/Header.js
new file mode 100644
index 0000000..fa95fc4
--- /dev/null
+++ b/components/Header.js
@@ -0,0 +1,43 @@
+import styled from 'styled-components'
+import { useRouteDispatch } from '../context/routeContext'
+import { CHANGE_VIEW, MENU } from '../utils/constants'
+import snapeatLogo from '../public/logos/logo2.svg'
+import menuIcon from '../public/icons/menu.svg'
+import menuBG from '../public/backgrounds/menu_bg.svg'
+
+const MenuContainer = styled.div.attrs({
+ className: 'w-screen px-6 pt-5d5',
+})`
+ display: grid;
+ grid-template-columns: 1fr 12.5%;
+ grid-template-rows: 40% 1fr;
+ align-items: center;
+ background: url(${menuBG}) left top/cover no-repeat;
+`
+
+const SnapeatLogo = styled.img.attrs({
+ className: 'w-auto sm:w-25',
+})`
+ margin-left: calc((100vw - 48px - 80px) / 2);
+
+ @media ${cssTheme('media.sm')} {
+ margin-left: calc((100vw - 48px - 100px) / 2);
+ }
+`
+
+const HeaderWithLogo = () => {
+ const routeDispatch = useRouteDispatch()
+ return (
+
+
+ routeDispatch({ type: CHANGE_VIEW, view: MENU })}
+ onKeyPress={() => routeDispatch({ type: CHANGE_VIEW, view: MENU })}
+ >
+
+
+
+ )
+}
+
+export { HeaderWithLogo }
diff --git a/components/Input.js b/components/Input.js
new file mode 100644
index 0000000..6efe4b8
--- /dev/null
+++ b/components/Input.js
@@ -0,0 +1,85 @@
+import styled from 'styled-components'
+import * as R from 'ramda'
+import { Field, ErrorMessage } from 'formik'
+
+import dropdown from '../public/icons/dropdown.svg'
+
+const TextInput = styled(Field).attrs(({ placeholder, name, ...attrs }) => ({
+ className: 'bg-lightgray w-full border-b-2 border-navy',
+ type: 'text',
+ name,
+ placeholder,
+ ...attrs,
+}))``
+
+const SelectInput = ({ options, placeholder, name, ...attrs }) => {
+ const emptyOption = (
+
+ {placeholder}
+
+ )
+
+ const optionComponents = R.pipe(
+ R.map(({ label, value }) => (
+
+ {label}
+
+ )),
+ R.prepend(emptyOption),
+ )(options)
+
+ return {optionComponents}
+}
+
+const Select = styled.select.attrs(({ className, ...attrs }) => {
+ return {
+ className: `border-b-2 border-navy block w-full
+ bg-transparent text-center ${className}`,
+ ...attrs,
+ }
+})`
+ -moz-appearance: none;
+ -webkit-appearance: none;
+ appearance: none;
+ background-image: url(${dropdown});
+ background-repeat: no-repeat, repeat;
+ background-position: right 0 top 50%, 0 0;
+`
+
+const RadioLabel = styled.label.attrs({
+ className: 'ml-2d5',
+})``
+
+const Radio = styled(Field).attrs(({ name, ...attrs }) => ({
+ type: 'radio',
+ id: name,
+ name,
+ ...attrs,
+}))``
+
+const RadioInput = ({ name, children, id, ...attrs }) => (
+
+
+ {children}
+
+)
+
+const ErrorContainer = styled.div.attrs({
+ className: 'text-red',
+})``
+
+const Error = ({ name, className }) => (
+ {msg} }
+ />
+)
+
+const Input = ({ Component, ...attrs }) => (
+
+
+
+
+)
+
+export { Input, TextInput, RadioInput, SelectInput, Error }
diff --git a/components/ProportionExamples.js b/components/ProportionExamples.js
new file mode 100644
index 0000000..f655f63
--- /dev/null
+++ b/components/ProportionExamples.js
@@ -0,0 +1,81 @@
+import styled from 'styled-components'
+import * as R from 'ramda'
+
+import Quarter from '../public/images/example_quarter.png'
+import Half from '../public/images/example_half.png'
+import Mostly from '../public/images/example_mostly.png'
+import Whole from '../public/images/example_whole.png'
+import closeIcon from '../public/icons/close.svg'
+
+import { CardBackground } from './foodData/shared'
+
+const proportionArray = [
+ {
+ name: 'Quarter',
+ img: Quarter,
+ },
+ {
+ name: 'Half',
+ img: Half,
+ },
+ {
+ name: 'Mostly',
+ img: Mostly,
+ },
+ {
+ name: 'Whole',
+ img: Whole,
+ },
+]
+
+const ProportionExamples = ({ toggleExamples }) => {
+ return (
+
+
+
+
+
+ It can be hard to know how much fruit or veg there is on a plate. Here
+ are some examples to help.
+
+
+
+ {R.map(prop => (
+
+
+ {prop.name}
+
+ ))(proportionArray)}
+
+
+
+ If you're still not sure, just share your best guess!
+
+
+ )
+}
+
+const CloseButton = styled.button.attrs({
+ className: 'cursor-pointer block',
+})`
+ margin: 2rem 2rem 1rem auto;
+`
+
+const Text = styled.p.attrs({
+ className: 'w-11/12 mx-auto font-xl text-center mb-3 mt-3',
+})``
+
+const ExampleContainer = styled.section.attrs({
+ className: 'w-9/12 mx-auto',
+})`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-template-rows: auto;
+ grid-gap: 1rem;
+`
+
+const Example = styled.section.attrs({
+ className: 'flex flex-col',
+})``
+
+export default ProportionExamples
diff --git a/components/SecurityPages.js b/components/SecurityPages.js
new file mode 100644
index 0000000..4455cc6
--- /dev/null
+++ b/components/SecurityPages.js
@@ -0,0 +1,99 @@
+import styled from 'styled-components'
+import logo from '../public/logos/gstc-logo.png'
+import securityMenuBG from '../public/backgrounds/termsConditionsBackground.svg'
+import closeIconGrey from '../public/icons/close.svg'
+import closeIconWhite from '../public/icons/close_white.svg'
+
+const Header = styled.header.attrs({
+ className: 'w-full min-h-65',
+})`
+ display: grid;
+ grid-template-rows: 1fr 4rem;
+ align-items: center;
+ min-height: ${cssTheme('spacing.65')};
+ background: url(${securityMenuBG}) left top/cover no-repeat;
+`
+
+const H1 = styled.h1.attrs({
+ className: 'text-center font-header font-thin mx-17',
+})``
+
+const GSTCLogo = styled.img.attrs({
+ src: logo,
+ alt: 'Guy's & St Thomas' Charity logo',
+ className: 'mx-auto',
+})`
+ width: 4rem;
+`
+
+const CloseButton = styled.button.attrs({
+ className: 'w-5 h-5 mt-6 mr-6 absolute top-0 right-0',
+})`
+ justify-self: end;
+`
+
+const Text = styled.p.attrs({
+ className: 'm-4 font-xl font-thin',
+})``
+
+const Footer = styled.footer.attrs({
+ className: 'bg-white shadow-tooltip rounded-card px-4 pb-10',
+})``
+
+const Grid = styled.section.attrs({
+ className: ' py-5',
+})`
+ display: grid;
+ grid-template-columns: 45px 1fr;
+`
+
+const Checkbox = styled.input.attrs({
+ className: 'w-4',
+})``
+
+const Label = styled.label.attrs({
+ className: 'font-sm',
+})`
+ align-self: center;
+`
+
+const Button = styled.button.attrs(({ active }) => ({
+ className: `bg-navy shadow-button rounded-button w-full text-white py-4 ${
+ active
+ ? 'opacity-100 pointer-events-auto'
+ : 'opacity-40 pointer-events-none'
+ }`,
+}))``
+
+const Close = ({ close, closeColour }) => {
+ return (
+
+
+
+ )
+}
+
+const HeaderWithGSTCLogo = ({ text, close, closeColour }) => {
+ return (
+
+ {close && }
+ {text}
+
+
+ )
+}
+
+export {
+ HeaderWithGSTCLogo,
+ Button,
+ Text,
+ H1,
+ Label,
+ Grid,
+ Checkbox,
+ Footer,
+ Close,
+}
diff --git a/components/Tooltip.js b/components/Tooltip.js
new file mode 100644
index 0000000..8482ada
--- /dev/null
+++ b/components/Tooltip.js
@@ -0,0 +1,21 @@
+import styled from 'styled-components'
+import close from '../public/icons/close.svg'
+
+const TooltipLink = styled.button.attrs({
+ type: 'button',
+ className: 'border-0 underline text-blue bg-transparent self-start mt-2d5',
+})``
+
+const Tooltip = styled.aside.attrs({
+ className:
+ 'text-center fixed z-20 bg-white left-0 bottom-0 pt-6 pb-20 px-4 shadow-tooltip rounded-tooltip',
+})``
+
+const TooltipClose = styled.button.attrs({
+ className: 'border-0 bg-transparent w-5 h-5 block ml-auto mb-15',
+ type: 'button',
+ alt: 'Close tooltip',
+})`
+ background: url(${close});
+`
+export { Tooltip, TooltipLink, TooltipClose }
diff --git a/components/foodData/Categories.js b/components/foodData/Categories.js
new file mode 100644
index 0000000..45b2792
--- /dev/null
+++ b/components/foodData/Categories.js
@@ -0,0 +1,182 @@
+import {
+ FRUIT,
+ VEGETABLES,
+ MEAT,
+ FISH,
+ DAIRY,
+ EGG,
+ PASTA,
+ RICE,
+ POTATO,
+ BREAD,
+ NUTS,
+ DESSERT,
+ OIL,
+ BUTTER,
+ WATER,
+ FIZZY_DRINK,
+} from '../../utils/constants'
+
+// import clicked and unclicked variants of all icons
+import fruitIcon from '../../public/icons/categories/regular/fruit_icn.svg'
+import fruitIconSelected from '../../public/icons/categories/selected/fruit_icn-white.svg'
+import vegIcon from '../../public/icons/categories/regular/vegan.svg'
+import vegIconSelected from '../../public/icons/categories/selected/vegs-white.svg'
+import meatIcon from '../../public/icons/categories/regular/meat.svg'
+import meatIconSelected from '../../public/icons/categories/selected/meat-white.svg'
+import fishIcon from '../../public/icons/categories/regular/fish.svg'
+import fishIconSelected from '../../public/icons/categories/selected/fish-white.svg'
+import dairyIcon from '../../public/icons/categories/regular/dairy.svg'
+import dairyIconSelected from '../../public/icons/categories/selected/dairy-white.svg'
+import eggIcon from '../../public/icons/categories/regular/egg.svg'
+import eggIconSelected from '../../public/icons/categories/selected/egg-white.svg'
+import pastaIcon from '../../public/icons/categories/regular/pasta.svg'
+import pastaIconSelected from '../../public/icons/categories/selected/pasta-white.svg'
+import riceIcon from '../../public/icons/categories/regular/rice.svg'
+import riceIconSelected from '../../public/icons/categories/selected/rice-white.svg'
+import potatoIcon from '../../public/icons/categories/regular/potato.svg'
+import potatoIconSelected from '../../public/icons/categories/selected/potato-white.svg'
+import breadIcon from '../../public/icons/categories/regular/bread.svg'
+import breadIconSelected from '../../public/icons/categories/selected/bread-white.svg'
+import nutsIcon from '../../public/icons/categories/regular/nuts.svg'
+import nutsIconSelected from '../../public/icons/categories/selected/nuts-white.svg'
+import dessertIcon from '../../public/icons/categories/regular/dessert.svg'
+import dessertIconSelected from '../../public/icons/categories/selected/dessert-white.svg'
+import oilIcon from '../../public/icons/categories/regular/oil.svg'
+import oilIconSelected from '../../public/icons/categories/selected/oil-white.svg'
+import butterIcon from '../../public/icons/categories/regular/butter.svg'
+import butterIconSelected from '../../public/icons/categories/selected/butter.svg'
+import waterIcon from '../../public/icons/categories/regular/water.svg'
+import waterIconSelected from '../../public/icons/categories/selected/water-white.svg'
+import fizzyDrinkIcon from '../../public/icons/categories/regular/fizzy-drink.svg'
+import fizzyDrinkIconSelected from '../../public/icons/categories/selected/fizzy-drink-white.svg'
+
+import { Title, CardBackground, CheckboxTile, TileContainer } from './shared'
+
+import * as Steps from '.'
+
+const Categories = () => {
+ return (
+
+ What's on their plate?
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
+
+Categories.componentName = Steps.Categories
+
+export default Categories
diff --git a/components/foodData/Error.js b/components/foodData/Error.js
new file mode 100644
index 0000000..8dd1690
--- /dev/null
+++ b/components/foodData/Error.js
@@ -0,0 +1,47 @@
+import styled from 'styled-components'
+
+import logo from '../../public/logos/logo1.svg'
+import emptyplate from '../../public/images/empty-plate.jpg'
+
+const Text = styled.p.attrs({
+ className: 'font-xxl text-center mb-5 mt-5',
+})``
+
+const Error = () => {
+ return (
+
+
+
+
+ Oh no, it looks like something went wrong.
+
+
+ Please let us know what happened by{' '}
+
+ filling in this form
+ {' '}
+ and then try again.
+
+ {/*
+ If the problem persists, don't hesitate to{' '}
+
+ contact us.
+
+
*/}
+
+ )
+}
+
+Error.componentName = 'Error'
+
+export default Error
diff --git a/components/foodData/FoodDataStep.js b/components/foodData/FoodDataStep.js
new file mode 100644
index 0000000..ec9ca04
--- /dev/null
+++ b/components/foodData/FoodDataStep.js
@@ -0,0 +1,29 @@
+import { useState } from 'react'
+import { Tooltip, TooltipLink, TooltipClose } from '../Tooltip'
+import { CardBackground, Title } from './shared'
+
+const FoodDataStep = ({ children, h1, tooltipTitle, tooltipContents }) => {
+ const [showTooltip, setShowTooltip] = useState(false)
+ return (
+
+ {h1}
+ {children}
+
+ {tooltipTitle && (
+ setShowTooltip(true)}>
+ Why do you need this?
+
+ )}
+
+ {showTooltip && (
+
+ setShowTooltip(false)} />
+ {tooltipTitle}
+ {tooltipContents}
+
+ )}
+
+ )
+}
+
+export default FoodDataStep
diff --git a/components/foodData/FruitProportion.js b/components/foodData/FruitProportion.js
new file mode 100644
index 0000000..4bf60d0
--- /dev/null
+++ b/components/foodData/FruitProportion.js
@@ -0,0 +1,86 @@
+import allIcon from '../../public/icons/quantities/regular/all.svg'
+import allIconSelected from '../../public/icons/quantities/selected/all-selected.svg'
+import halfIcon from '../../public/icons/quantities/regular/half.svg'
+import halfIconSelected from '../../public/icons/quantities/selected/half-selected.svg'
+import mostlyIcon from '../../public/icons/quantities/regular/mostly.svg'
+import mostlyIconSelected from '../../public/icons/quantities/selected/mostly-selected.svg'
+import quarterIcon from '../../public/icons/quantities/regular/quarter.svg'
+import quarterIconSelected from '../../public/icons/quantities/selected/quarter-selected.svg'
+import fruitIcon from '../../public/icons/categories/regular/fruit_icn.svg'
+
+import ProportionExamples from '../ProportionExamples'
+
+import {
+ Title,
+ CardBackground,
+ RadioTile,
+ TileContainer,
+ FruitVegTile,
+ ExamplesButton,
+} from './shared'
+
+import * as Steps from '.'
+
+const FruitProportion = ({ toggleExamples, showExamples }) => {
+ return showExamples ? (
+
+ ) : (
+
+ You tagged Fruit
+
+
+
+ Fruit
+
+
+
+ Roughly, how much of the plate is fruit?
+
+
+
+
+
+
+
+
+
+
+ Not sure? Check out these examples
+
+
+ )
+}
+
+FruitProportion.componentName = Steps.FruitProportion
+
+export default FruitProportion
diff --git a/components/foodData/Results.js b/components/foodData/Results.js
new file mode 100644
index 0000000..28d900b
--- /dev/null
+++ b/components/foodData/Results.js
@@ -0,0 +1,180 @@
+import styled from 'styled-components'
+
+import {
+ FRUIT,
+ VEGETABLES,
+ MEAT,
+ FISH,
+ DAIRY,
+ EGG,
+ PASTA,
+ RICE,
+ POTATO,
+ BREAD,
+ NUTS,
+ DESSERT,
+ OIL,
+ BUTTER,
+ WATER,
+ FIZZY_DRINK,
+} from '../../utils/constants'
+
+import fruitIcon from '../../public/icons/categories/regular/fruit_icn.svg'
+import vegIcon from '../../public/icons/categories/regular/vegan.svg'
+import meatIcon from '../../public/icons/categories/regular/meat.svg'
+import fishIcon from '../../public/icons/categories/regular/fish.svg'
+import dairyIcon from '../../public/icons/categories/regular/dairy.svg'
+import eggIcon from '../../public/icons/categories/regular/egg.svg'
+import pastaIcon from '../../public/icons/categories/regular/pasta.svg'
+import riceIcon from '../../public/icons/categories/regular/rice.svg'
+import potatoIcon from '../../public/icons/categories/regular/potato.svg'
+import breadIcon from '../../public/icons/categories/regular/bread.svg'
+import nutsIcon from '../../public/icons/categories/regular/nuts.svg'
+import dessertIcon from '../../public/icons/categories/regular/dessert.svg'
+import oilIcon from '../../public/icons/categories/regular/oil.svg'
+import butterIcon from '../../public/icons/categories/regular/butter.svg'
+import waterIcon from '../../public/icons/categories/regular/water.svg'
+import fizzyDrinkIcon from '../../public/icons/categories/regular/fizzy-drink.svg'
+
+import allIcon from '../../public/icons/quantities/regular/all.svg'
+import halfIcon from '../../public/icons/quantities/regular/half.svg'
+import mostlyIcon from '../../public/icons/quantities/regular/mostly.svg'
+import quarterIcon from '../../public/icons/quantities/regular/quarter.svg'
+
+import * as Steps from '.'
+
+import {
+ TagButton,
+ CardBackground,
+ Title,
+ FruitVegTile,
+ TileContainer,
+} from './shared'
+
+const categoryIcons = {
+ [FRUIT]: fruitIcon,
+ [VEGETABLES]: vegIcon,
+ [MEAT]: meatIcon,
+ [FISH]: fishIcon,
+ [DAIRY]: dairyIcon,
+ [EGG]: eggIcon,
+ [PASTA]: pastaIcon,
+ [RICE]: riceIcon,
+ [POTATO]: potatoIcon,
+ [BREAD]: breadIcon,
+ [NUTS]: nutsIcon,
+ [DESSERT]: dessertIcon,
+ [OIL]: oilIcon,
+ [BUTTER]: butterIcon,
+ [WATER]: waterIcon,
+ [FIZZY_DRINK]: fizzyDrinkIcon,
+}
+
+const proportionIcons = {
+ all: allIcon,
+ half: halfIcon,
+ mostly: mostlyIcon,
+ quarter: quarterIcon,
+}
+
+const CategoryTile = ({ category, updatePage }) => (
+ {
+ updatePage('Categories')
+ }}
+ >
+
+
+
+ {category.charAt(0).toUpperCase() + category.slice(1)}
+
+
+
+)
+
+const TagsContainer = styled.div.attrs({
+ className: 'flex flex-wrap justify-around w-4/5 center m-auto',
+})``
+
+const FruitVegProportion = ({ proportion, category, updatePage }) => {
+ const page = category === 'fruit' ? 'FruitProportion' : 'VegetableProportion'
+ return (
+ <>
+ Roughly, the amount of {category} on the plate was:
+ {
+ updatePage(page)
+ }}
+ >
+
+
+
+ {proportion.charAt(0).toUpperCase() + proportion.slice(1)}
+
+
+
+ >
+ )
+}
+
+const Results = ({ values, updatePage }) => {
+ const { categories, proportionFruit, proportionVeg, tags } = values
+
+ return (
+
+
+ In summary, tonight's meal was composed of
+
+
+
+ {categories.map(category => (
+
+ ))}
+
+ {proportionVeg && (
+
+ )}
+ {proportionFruit && (
+
+ )}
+ and it was:
+
+ {tags.map(tag => (
+ {
+ updatePage('Tags')
+ }}
+ key={tag}
+ >
+ {tag}
+
+ ))}
+
+
+ )
+}
+
+Results.componentName = Steps.Results
+
+export default Results
diff --git a/components/foodData/Spinner.js b/components/foodData/Spinner.js
new file mode 100644
index 0000000..65dd947
--- /dev/null
+++ b/components/foodData/Spinner.js
@@ -0,0 +1,26 @@
+import styled from 'styled-components'
+
+import loading from '../../public/icons/loading.svg'
+
+const Text = styled.p.attrs({
+ className: 'font-xxl text-center text-white mb-5 mt-5',
+})``
+
+// eslint-disable-next-line no-unused-vars
+const Spinner = ({ values }) => {
+ // eslint-disable-next-line no-unused-vars
+ const { categories, proportionFruit, proportionVeg, tags } = values
+
+ return (
+
+
+
+
Uploading...
+
+
+ )
+}
+
+Spinner.componentName = 'Spinner'
+
+export default Spinner
diff --git a/components/foodData/Success.js b/components/foodData/Success.js
new file mode 100644
index 0000000..ddc93ea
--- /dev/null
+++ b/components/foodData/Success.js
@@ -0,0 +1,29 @@
+import styled from 'styled-components'
+
+import logo from '../../public/logos/logo1.svg'
+import checkmark from '../../public/illustrations/checkmark.svg'
+
+const Text = styled.p.attrs({
+ className: 'font-xxl text-center mb-5 mt-5',
+})``
+
+// eslint-disable-next-line no-unused-vars
+const Success = ({ values }) => {
+ // eslint-disable-next-line no-unused-vars
+ const { categories, proportionFruit, proportionVeg, tags } = values
+
+ return (
+
+
+
+ All done!
+ Thanks for logging what your child had for dinner today.
+
+ Have a great evening.
+
+ )
+}
+
+Success.componentName = 'Success'
+
+export default Success
diff --git a/components/foodData/Tags.js b/components/foodData/Tags.js
new file mode 100644
index 0000000..80d9aa8
--- /dev/null
+++ b/components/foodData/Tags.js
@@ -0,0 +1,20 @@
+import { TAG_ARRAY } from '../../utils/constants'
+import FoodDataStep from './FoodDataStep'
+
+import { TagButton, TagsContainer } from './shared'
+
+import * as Steps from '.'
+
+const Tags = () => (
+
+
+ {TAG_ARRAY.map(tag => (
+ {tag}
+ ))}
+
+
+)
+
+Tags.componentName = Steps.Tags
+
+export default Tags
diff --git a/components/foodData/VegetableProportion.js b/components/foodData/VegetableProportion.js
new file mode 100644
index 0000000..7c3ef67
--- /dev/null
+++ b/components/foodData/VegetableProportion.js
@@ -0,0 +1,86 @@
+import allIcon from '../../public/icons/quantities/regular/all.svg'
+import allIconSelected from '../../public/icons/quantities/selected/all-selected.svg'
+import halfIcon from '../../public/icons/quantities/regular/half.svg'
+import halfIconSelected from '../../public/icons/quantities/selected/half-selected.svg'
+import mostlyIcon from '../../public/icons/quantities/regular/mostly.svg'
+import mostlyIconSelected from '../../public/icons/quantities/selected/mostly-selected.svg'
+import quarterIcon from '../../public/icons/quantities/regular/quarter.svg'
+import quarterIconSelected from '../../public/icons/quantities/selected/quarter-selected.svg'
+import vegIcon from '../../public/icons/categories/regular/vegan.svg'
+
+import ProportionExamples from '../ProportionExamples'
+
+import {
+ Title,
+ CardBackground,
+ RadioTile,
+ TileContainer,
+ FruitVegTile,
+ ExamplesButton,
+} from './shared'
+
+import * as Steps from '.'
+
+const VegetableProportion = ({ toggleExamples, showExamples }) => {
+ return showExamples ? (
+
+ ) : (
+
+ You tagged Vegetables
+
+
+ Vegetables
+
+
+
+ Roughly, how much of the plate is vegetables?
+
+
+
+
+
+
+
+
+
+
+
+ Not sure? Check out these examples
+
+
+ )
+}
+
+VegetableProportion.componentName = Steps.VegetableProportion
+
+export default VegetableProportion
diff --git a/components/foodData/index.js b/components/foodData/index.js
new file mode 100644
index 0000000..62403d2
--- /dev/null
+++ b/components/foodData/index.js
@@ -0,0 +1,8 @@
+export const Categories = 'Categories'
+export const VegetableProportion = 'VegetableProportion'
+export const FruitProportion = 'FruitProportion'
+export const Tags = 'Tags'
+export const Results = 'Results'
+export const Success = 'Success'
+export const Error = 'Error'
+export const Spinner = 'Spinner'
diff --git a/components/foodData/shared.js b/components/foodData/shared.js
new file mode 100644
index 0000000..f4b6836
--- /dev/null
+++ b/components/foodData/shared.js
@@ -0,0 +1,201 @@
+import styled from 'styled-components'
+import { Field } from 'formik'
+
+const TileContainer = styled.section.attrs({
+ className: '',
+})`
+ display: grid;
+ grid-template-columns: 1fr 1fr;
+ grid-gap: 1rem;
+ justify-items: center;
+ max-width: 90%;
+ width: 90%;
+ margin: 0 auto;
+`
+
+const InputContainer = styled.label.attrs({
+ className: 'block relative cursor-pointer select-none',
+})`
+ width: 100%;
+ /* Hide the browser's default checkbox */
+ input {
+ position: absolute;
+ opacity: 0;
+ cursor: pointer;
+ height: 0;
+ width: 0;
+ }
+
+ div {
+ width: 100%;
+ height: 6rem;
+ border: 1px solid ${cssTheme('colors.navy')};
+ border-radius: 1rem;
+ display: flex;
+ flex-flow: column nowrap;
+ justify-content: center;
+ text-align: center;
+ }
+
+ /* Create a custom checkbox */
+ .checkmark {
+ width: 100%;
+ height: 4rem;
+ background-image: url(${props => props.icon});
+ background-repeat: no-repeat;
+ background-position: center;
+ display: block;
+ }
+ /* When the checkbox is checked, add a blue background */
+ input:checked ~ .background > .checkmark {
+ background-image: url(${props => props.iconSelected});
+ }
+
+ input:checked ~ .background {
+ background-color: ${cssTheme('colors.navy')};
+ color: white;
+ }
+
+ input:checked .parent {
+ background-color: blue;
+ }
+`
+
+const CheckboxTile = ({ label, icon, iconSelected, name }) => {
+ const text = label.charAt(0).toUpperCase() + label.slice(1)
+ return (
+
+
+
+
+ {text}
+
+
+ )
+}
+
+const RadioInput = ({
+ field: { name, value, onChange, onBlur },
+ id,
+ label,
+ ...props
+}) => {
+ return (
+
+ )
+}
+
+const RadioTile = ({ name, icon, iconSelected, label, id }) => {
+ const text = label.charAt(0).toUpperCase() + label.slice(1)
+ return (
+
+
+
+
+ {text}
+
+
+ )
+}
+
+const CardBackground = styled.section.attrs({
+ className: 'z-10 w-screen bg-white shadow-tooltip pb-4',
+})`
+ border-top-left-radius: 4em;
+ border-top-right-radius: 4em;
+`
+
+const Title = styled.h1.attrs({
+ className: 'font-xxl text-center py-5',
+})``
+
+const FruitVegTile = styled.div`
+ width: ${({ width = '190px' }) => width};
+ height: 6rem;
+ border: 1px solid ${cssTheme('colors.navy')};
+ border-radius: 1rem;
+
+ .checkmark {
+ background-image: url(${({ icon }) => icon});
+ height: 4rem;
+ background-repeat: no-repeat;
+ background-position: center;
+ }
+`
+
+const TagsContainer = styled.div.attrs({
+ className: 'flex flex-wrap justify-around w-4/5 center m-auto',
+})``
+
+const CheckboxContainer = styled.label.attrs({
+ className: 'relative cursor-pointer select-none',
+})`
+ input {
+ display: none;
+ }
+
+ .checkmark {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+ margin-bottom: ${cssTheme('spacing.2')};
+ padding: ${cssTheme('spacing.1')};
+ padding-left: ${cssTheme('spacing.3')};
+ padding-right: ${cssTheme('spacing.3')};
+ border: 1px solid ${cssTheme('colors.navy')};
+ border-radius: ${cssTheme('spacing.12')};
+
+ background: white;
+ color: ${cssTheme('colors.navy')};
+ }
+
+ input:checked ~ .checkmark {
+ background: ${cssTheme('colors.navy')};
+ color: white;
+ }
+`
+const TagButton = ({ children }) => {
+ return (
+
+
+ {children}
+
+ )
+}
+
+const ExamplesButton = styled.button.attrs({
+ className: 'w-11/12 font-base text-center mt-4 underline block mx-auto',
+})`
+ color: ${cssTheme('colors.blue')};
+`
+
+export {
+ Title,
+ TileContainer,
+ CardBackground,
+ CheckboxTile,
+ RadioTile,
+ FruitVegTile,
+ TagsContainer,
+ TagButton,
+ ExamplesButton,
+}
diff --git a/components/onboarding/Ages.js b/components/onboarding/Ages.js
new file mode 100644
index 0000000..4930143
--- /dev/null
+++ b/components/onboarding/Ages.js
@@ -0,0 +1,104 @@
+import { useEffect } from 'react'
+import * as Yup from 'yup'
+import { FieldArray, Field } from 'formik'
+import * as R from 'ramda'
+import R_ from '../../utils/R_'
+import createArrayOfLength from '../../utils/createArrayOfLength'
+
+import { NEXT_NOT_ATTEMPTED } from '../../utils/constants'
+
+import { RadioInput } from '../Input'
+import OnboardingStep, { SubQuestion } from './OnboardingStep'
+
+const validation = Yup.object().shape({
+ ages: Yup.array().of(
+ Yup.string().test(
+ 'childs-age-selected',
+ "Please make sure you have selected your child's age",
+ value => !!value,
+ ),
+ ),
+})
+
+const tooltipContents = (
+ <>
+
+ At the SnapEat Project we want to learn what children in Lambeth and
+ Southwark are eating when they are at home.
+
+
+ By securely sharing this additional information, you will help us to make
+ sure we are hearing and learning from a range of different households.
+
+ >
+)
+
+const AgesError = ({ name }) => (
+
+ value || status === NEXT_NOT_ATTEMPTED ? null : (
+ {error}
+ )
+ }
+ />
+)
+
+const AgeComponent = (_, i) => {
+ return (
+
+
Age range — Child {i + 1}
+
+
+ 0 - 4
+
+
+ 5 - 8
+
+
+ 9 - 12
+
+
+ 13 - 15
+
+
+ 16 - 18
+
+
+
+ )
+}
+
+const childrenQuestions = R.pipe(
+ createArrayOfLength,
+ R_.mapIndexed(AgeComponent),
+)
+
+const Ages = ({ values: { numberOfChildren }, setFieldValue }) => {
+ useEffect(() => {
+ setFieldValue('ages', createArrayOfLength(numberOfChildren))
+ }, [])
+
+ return (
+
+ childrenQuestions(numberOfChildren)}
+ />
+
+ )
+}
+
+Ages.componentName = 'Ages'
+Ages.validation = validation
+
+export default Ages
diff --git a/components/onboarding/Confirmation.js b/components/onboarding/Confirmation.js
new file mode 100644
index 0000000..c97f0e3
--- /dev/null
+++ b/components/onboarding/Confirmation.js
@@ -0,0 +1,4 @@
+const Confirmation = () => Confirmation
+Confirmation.componentName = 'Confirmation'
+
+export default Confirmation
diff --git a/components/onboarding/Error.js b/components/onboarding/Error.js
new file mode 100644
index 0000000..2d6ba24
--- /dev/null
+++ b/components/onboarding/Error.js
@@ -0,0 +1,48 @@
+import styled from 'styled-components'
+
+import logo from '../../public/logos/logo1.svg'
+import emptyplate from '../../public/images/empty-plate.jpg'
+
+const Text = styled.p.attrs({
+ className: 'font-xxl text-center mb-5 mt-5',
+})``
+
+const Error = () => {
+ return (
+
+
+
+
+ Oh no, it looks like something went wrong.
+
+ Please try again.
+
+ Please let us know what happened by{' '}
+
+ filling in this form
+ {' '}
+ and then try again.
+
+ {/*
+ If the problem persists, don't hesitate to{' '}
+
+ contact us.
+
+
*/}
+
+ )
+}
+
+Error.componentName = 'Error'
+
+export default Error
diff --git a/components/onboarding/NumberOfChildren.js b/components/onboarding/NumberOfChildren.js
new file mode 100644
index 0000000..19e8951
--- /dev/null
+++ b/components/onboarding/NumberOfChildren.js
@@ -0,0 +1,56 @@
+import * as Yup from 'yup'
+import { Input, TextInput } from '../Input'
+import keepFieldCleanOnChange from '../../utils/keepFieldCleanOnChange'
+import OnboardingStep from './OnboardingStep'
+
+const tooltipContents = (
+ <>
+
+ At the SnapEat Project we want to learn what children in Lambeth and
+ Southwark are eating when they are at home.
+
+
+ By securely sharing this additional information, you will help us to make
+ sure we are hearing and learning from a range of different households.
+
+ >
+)
+
+const validation = Yup.object().shape({
+ numberOfChildren: Yup.string()
+ .required('Please enter a number')
+ .notOneOf(
+ ['0', '00'],
+ 'You must have children under 18 to sign up to SnapEat',
+ ),
+})
+
+const NumberOfChildren = ({ setFieldValue, values }) => (
+
+
+
+)
+
+NumberOfChildren.componentName = 'NumberOfChildren'
+NumberOfChildren.validation = validation
+
+export default NumberOfChildren
diff --git a/components/onboarding/OnboardingStep.js b/components/onboarding/OnboardingStep.js
new file mode 100644
index 0000000..b993783
--- /dev/null
+++ b/components/onboarding/OnboardingStep.js
@@ -0,0 +1,70 @@
+import React, { useState } from 'react'
+import styled from 'styled-components'
+
+import { Tooltip, TooltipLink, TooltipClose } from '../Tooltip'
+
+const Container = styled.main.attrs({
+ className: 'flex flex-col w-full',
+})``
+const H1 = styled.h1.attrs({
+ className: 'font-xxl font-bold text-center mb-5',
+})``
+
+const H2 = styled.h2.attrs({
+ className: 'font-xl text-center mb-5',
+})``
+
+const Question = styled.label.attrs({
+ className: 'font-xl w-full',
+})``
+
+const SubQuestion = styled.label.attrs({
+ className: 'font-base w-full block',
+})``
+
+const Description = styled.p.attrs({
+ className: 'font-xs w-full mb-10',
+})``
+
+const OnboardingStep = ({
+ children,
+ h1,
+ h2,
+ question,
+ description,
+ tooltipTitle,
+ tooltipContents,
+ className,
+}) => {
+ const [showTooltip, setShowTooltip] = useState(false)
+
+ return (
+
+ {h1}
+ {h2}
+ 1 ? 'div' : 'label'}>{question}
+ {description}
+ {children}
+ setShowTooltip(true)}>
+ Why do you need this?
+
+ {showTooltip && (
+
+ setShowTooltip(false)} />
+ {tooltipTitle}
+ {tooltipContents}
+
+ Something wrong? Tell us here
+
+
+ )}
+
+ )
+}
+
+export { OnboardingStep as default, SubQuestion }
diff --git a/components/onboarding/Phone.js b/components/onboarding/Phone.js
new file mode 100644
index 0000000..2cacc5b
--- /dev/null
+++ b/components/onboarding/Phone.js
@@ -0,0 +1,90 @@
+import axios from 'axios'
+import * as Yup from 'yup'
+import { parsePhoneNumberFromString } from 'libphonenumber-js'
+import { Input, TextInput } from '../Input'
+import keepFieldCleanOnChange from '../../utils/keepFieldCleanOnChange'
+import OnboardingStep from './OnboardingStep'
+
+const validation = Yup.object().shape({
+ phoneNumber: Yup.string().required(
+ 'You need to enter your phone number to continue',
+ ),
+})
+
+const doesPhoneNumberExist = async ({ phoneNumber }) => {
+ const encodedPhoneNumber = encodeURIComponent(phoneNumber)
+ const res = await axios(
+ `${process.env.HOST}/api/does-phonenumber-exist?phonenumber=${encodedPhoneNumber}`,
+ )
+
+ const {
+ data: { phoneNumberExists },
+ } = res
+
+ return phoneNumberExists
+}
+
+const validatePhoneNumber = async value => {
+ const phoneNumber = parsePhoneNumberFromString(value, 'GB')
+ if (!phoneNumber) {
+ return 'Please enter a complete phone number.'
+ }
+ if (phoneNumber.countryCallingCode !== '44') {
+ return 'Please enter a valid UK number'
+ }
+ if (!phoneNumber.isValid()) {
+ return 'Sorry, this phone number is not valid.'
+ }
+
+ const phoneNumberExists = await doesPhoneNumberExist({
+ phoneNumber: phoneNumber.number,
+ })
+
+ if (phoneNumberExists) {
+ return 'This phone number is already registered with SnapEat. Do you already have an account?'
+ }
+}
+
+const tooltipContents = (
+ <>
+
+ We need this information so we can send you regular reminders which will
+ help you stick to schedule.
+
+
+ By securely sharing this additional information, you will help us to make
+ sure we are hearing and learning from a range of different households.
+
+ >
+)
+
+const Phone = ({ setFieldValue }) => {
+ return (
+
+
+
+ )
+}
+Phone.componentName = 'Phone'
+Phone.validation = validation
+
+export default Phone
diff --git a/components/onboarding/PostCode.js b/components/onboarding/PostCode.js
new file mode 100644
index 0000000..9dcfa50
--- /dev/null
+++ b/components/onboarding/PostCode.js
@@ -0,0 +1,73 @@
+import * as Yup from 'yup'
+import axios from 'axios'
+import { Input, TextInput } from '../Input'
+import OnboardingStep from './OnboardingStep'
+
+const validation = Yup.object().shape({
+ postCode: Yup.string().required(
+ 'Please enter the start of your postcode to continue',
+ ),
+})
+
+const isPostCodeValid = async ({ postCode }) => {
+ const {
+ data: { postCodeArray },
+ } = await axios(
+ `${process.env.HOST}/api/is-postcode-valid?postcode=${postCode}`,
+ )
+
+ if (postCodeArray === null) {
+ return { error: 'Not a valid UK postcode' }
+ } else if (postCode.split(' ').join('').length > 5) {
+ return { error: "Please don't enter more than the start of your postcode" }
+ }
+
+ return { error: '' }
+}
+
+const validatePostCode = async value => {
+ const { error } = await isPostCodeValid({ postCode: value })
+
+ return error
+}
+
+const tooltipContents = (
+ <>
+
+ SnapEat uses the start of your postcode to understand where in Lambeth and
+ Southwark our snappers come from.
+
+
+ The start of a postcode only tells us the general area you live in and
+ nothing else.
+
+
+ Fun fact : on average 8,200 buildings
+ will have the same start to their postcode as you.
+
+ >
+)
+
+const PostCode = () => (
+
+
+
+)
+PostCode.componentName = 'PostCode'
+PostCode.validation = validation
+
+export default PostCode
diff --git a/components/onboarding/Projects.js b/components/onboarding/Projects.js
new file mode 100644
index 0000000..30a3085
--- /dev/null
+++ b/components/onboarding/Projects.js
@@ -0,0 +1,86 @@
+import { useState, useEffect } from 'react'
+import axios from 'axios'
+import * as Yup from 'yup'
+import * as R from 'ramda'
+import { Input, SelectInput } from '../Input'
+import OnboardingStep from './OnboardingStep'
+
+const validation = Yup.object().shape({
+ project: Yup.string().required('You need to select a project to continue'),
+})
+
+const tooltipContents = (
+ <>
+
+ At the SnapEat Project we want to learn what children in Lambeth and
+ Southwark are eating when they are at home.
+
+
+ By securely sharing this additional information, you will help us to make
+ sure we are hearing and learning from a range of different households.
+
+ >
+)
+
+const Projects = ({ values, setFieldValue }) => {
+ const [availableProjects, setAvailableProjects] = useState([])
+
+ useEffect(() => {
+ const source = axios.CancelToken.source()
+
+ const getOptions = async () => {
+ try {
+ const {
+ data: { projects },
+ } = await axios.get(`${process.env.HOST}/api/get-available-projects`, {
+ cancelToken: source.token,
+ })
+
+ return R.pipe(
+ R.map(({ name, slug }) => ({
+ label: name,
+ value: slug,
+ })),
+ setAvailableProjects,
+ )(projects)
+ } catch (e) {
+ // eslint-disable-next-line
+ console.log('Inside useEffect catch')
+ }
+ }
+
+ getOptions()
+
+ return () => {
+ source.cancel()
+ }
+ }, [])
+
+ return (
+
+ setFieldValue('project', value)}
+ placeholder="Select a project..."
+ className={values.project ? 'text-navy' : 'text-lightnavy'}
+ />
+
+ )
+}
+Projects.componentName = 'Projects'
+Projects.validation = validation
+
+export default Projects
diff --git a/components/onboarding/Spinner.js b/components/onboarding/Spinner.js
new file mode 100644
index 0000000..ce34323
--- /dev/null
+++ b/components/onboarding/Spinner.js
@@ -0,0 +1,20 @@
+import styled from 'styled-components'
+
+import loading from '../../public/icons/loading.svg'
+
+const Text = styled.p.attrs({
+ className: 'font-xxl text-center text-white mb-5 mt-5',
+})``
+
+const Spinner = () => (
+
+
+
+
Uploading...
+
+
+)
+
+Spinner.componentName = 'Spinner'
+
+export default Spinner
diff --git a/components/onboarding/Success.js b/components/onboarding/Success.js
new file mode 100644
index 0000000..bf9f80f
--- /dev/null
+++ b/components/onboarding/Success.js
@@ -0,0 +1,43 @@
+import { useEffect } from 'react'
+import styled from 'styled-components'
+
+import { useRouteDispatch } from '../../context/routeContext'
+import { CHANGE_VIEW, HOME } from '../../utils/constants'
+
+import logo from '../../public/logos/logo1.svg'
+import successBG from '../../public/backgrounds/success_bg.svg'
+
+const Success = () => {
+ const routeDispatch = useRouteDispatch()
+
+ useEffect(() => {
+ const timeout = window.setTimeout(
+ () => routeDispatch({ type: CHANGE_VIEW, view: HOME }),
+ 2000,
+ )
+ return () => {
+ window.clearTimeout(timeout)
+ }
+ }, [routeDispatch])
+
+ return (
+
+
+ All set
+
+ )
+}
+
+const Container = styled.div.attrs({
+ className: 'pt-5d5 w-screen h-screen font-xxl text-center',
+})`
+ background: url(${successBG}) center 35% / 95% no-repeat;
+`
+
+const Heading = styled.h1.attrs({
+ className: 'font-bold absolute',
+})`
+ bottom: calc(100vh - 35% - (0.95 * 100vw * 0.75));
+ left: calc((100vw - 68px) / 2);
+`
+export default Success
diff --git a/components/onboarding/index.js b/components/onboarding/index.js
new file mode 100644
index 0000000..9b3cc1b
--- /dev/null
+++ b/components/onboarding/index.js
@@ -0,0 +1,6 @@
+export const PostCode = 'PostCode'
+export const NumberOfChildren = 'NumberOfChildren'
+export const Ages = 'Ages'
+export const Projects = 'Projects'
+export const Phone = 'Phone'
+export const Confirmation = 'Confirmation'
diff --git a/context/AuthenicatedAppProviders.js b/context/AuthenicatedAppProviders.js
new file mode 100644
index 0000000..6bb5d9e
--- /dev/null
+++ b/context/AuthenicatedAppProviders.js
@@ -0,0 +1,7 @@
+import { FoodDataProvider } from './foodDataContext'
+
+const AppProviders = ({ children }) => (
+ {children}
+)
+
+export default AppProviders
diff --git a/context/authContext.js b/context/authContext.js
new file mode 100644
index 0000000..31ef539
--- /dev/null
+++ b/context/authContext.js
@@ -0,0 +1,116 @@
+import React, { useState, useEffect } from 'react'
+import axios from 'axios'
+import Spinner from '../views/Spinner'
+
+const AuthContext = React.createContext()
+
+const fetchAuth0User = async () => {
+ if (typeof window !== 'undefined' && window.__user) {
+ return window.__user
+ }
+
+ try {
+ const { data: auth0User } = await axios.get('/api/me')
+
+ if (typeof window !== 'undefined') {
+ window.__user = auth0User
+ }
+ return auth0User
+ } catch (err) {
+ delete window.__user
+ }
+}
+
+const fetchSnapeatUser = async email => {
+ try {
+ const {
+ data: { doesUserExist: snapeatUserExists },
+ } = await axios.get(`/api/does-user-exist?email=${email}`)
+
+ if (snapeatUserExists) {
+ const {
+ data: { user: snapeatUser },
+ } = await axios.get(`/api/get-user?email=${email}`)
+
+ return snapeatUser
+ }
+
+ return null
+ } catch (err) {
+ //eslint-disable-next-line
+ console.log('error fetching user', err)
+ }
+}
+
+const AuthProvider = props => {
+ const [auth0Loading, setAuth0Loading] = useState(
+ () => !(typeof window !== 'undefined' && window.__user),
+ )
+
+ const [auth0User, setAuth0User] = useState(() => {
+ if (typeof window === 'undefined') {
+ return null
+ }
+ return window.__user || null
+ })
+
+ const [snapeatLoading, setSnapeatLoading] = useState(false)
+ const [snapeatUser, setSnapeatUser] = useState(null)
+
+ useEffect(() => {
+ if (!auth0Loading && auth0User) {
+ return
+ }
+ setAuth0Loading(true)
+
+ fetchAuth0User().then(fetchedAuth0User => {
+ if (!fetchedAuth0User) {
+ setAuth0Loading(false)
+ return
+ }
+ setAuth0User(fetchedAuth0User)
+ setAuth0Loading(false)
+ })
+ }, [])
+
+ useEffect(() => {
+ if (!auth0User) return
+ if (!snapeatLoading && snapeatUser) return
+
+ setSnapeatLoading(true)
+
+ fetchSnapeatUser(auth0User.name)
+ .then(fetchedSnapeatUser => {
+ if (!fetchedSnapeatUser) {
+ setSnapeatLoading(false)
+ return
+ }
+ setSnapeatUser(fetchedSnapeatUser)
+ setSnapeatLoading(false)
+ })
+ .catch(e => {
+ // eslint-disable-next-line
+ console.log('Error fetching snapeat user', e)
+ return null
+ })
+ }, [auth0User])
+
+ if (auth0Loading || snapeatLoading) return
+
+ return (
+
+ )
+}
+
+const useAuth = () => {
+ const context = React.useContext(AuthContext)
+ if (context === undefined) {
+ throw new Error(`useAuth must be used within a AuthProvider`)
+ }
+ return context
+}
+
+export { AuthProvider, useAuth }
diff --git a/context/consentContext.js b/context/consentContext.js
new file mode 100644
index 0000000..fb1c6b6
--- /dev/null
+++ b/context/consentContext.js
@@ -0,0 +1,46 @@
+import React from 'react'
+import { NO_CONSENT_FROM_USER, SET_CONSENT } from '../utils/constants'
+
+const initialState = NO_CONSENT_FROM_USER
+
+const ConsentStateContext = React.createContext()
+const ConsentDispatchContext = React.createContext()
+
+const consentReducer = (_, { consent, type }) => {
+ switch (type) {
+ case SET_CONSENT:
+ return consent
+
+ default:
+ throw Error('Consent reducer action not recognised')
+ }
+}
+
+const ConsentProvider = ({ children }) => {
+ const [state, dispatch] = React.useReducer(consentReducer, initialState)
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+const useConsentState = () => {
+ const context = React.useContext(ConsentStateContext)
+ if (context === undefined) {
+ throw new Error(`useConsentState must be used within a ConsentProvider`)
+ }
+ return context
+}
+
+const useConsentDispatch = () => {
+ const context = React.useContext(ConsentDispatchContext)
+ if (context === undefined) {
+ throw new Error(`useConsentDispatch must be used within a ConsentProvider`)
+ }
+ return context
+}
+
+export { ConsentProvider, useConsentState, useConsentDispatch }
diff --git a/context/foodDataContext.js b/context/foodDataContext.js
new file mode 100644
index 0000000..fd1358f
--- /dev/null
+++ b/context/foodDataContext.js
@@ -0,0 +1,52 @@
+import React from 'react'
+import { SET_FOOD_PHOTO } from '../utils/constants'
+
+const initialState = {
+ foodPhoto: {},
+}
+
+const FoodDataStateContext = React.createContext()
+const FoodDataDispatchContext = React.createContext()
+
+const foodDataReducer = (state, { payload, type }) => {
+ switch (type) {
+ case SET_FOOD_PHOTO:
+ return {
+ foodPhoto: payload,
+ }
+
+ default:
+ throw Error('FoodData reducer action not recognised')
+ }
+}
+
+const FoodDataProvider = ({ children }) => {
+ const [state, dispatch] = React.useReducer(foodDataReducer, initialState)
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+const useFoodDataState = () => {
+ const context = React.useContext(FoodDataStateContext)
+ if (context === undefined) {
+ throw new Error(`useFoodDataState must be used within a FoodDataProvider`)
+ }
+ return context
+}
+
+const useFoodDataDispatch = () => {
+ const context = React.useContext(FoodDataDispatchContext)
+ if (context === undefined) {
+ throw new Error(
+ `useFoodDataDispatch must be used within a FoodDataProvider`,
+ )
+ }
+ return context
+}
+
+export { FoodDataProvider, useFoodDataState, useFoodDataDispatch }
diff --git a/context/projectContext.js b/context/projectContext.js
new file mode 100644
index 0000000..02a2b54
--- /dev/null
+++ b/context/projectContext.js
@@ -0,0 +1,46 @@
+import React from 'react'
+
+import { NO_PROJECT, CHANGE_PROJECT } from '../utils/constants'
+
+const initialState = { project: undefined, error: NO_PROJECT }
+
+const ProjectStateContext = React.createContext()
+const ProjectDispatchContext = React.createContext()
+
+const projectReducer = (_, { project, type }) => {
+ switch (type) {
+ case CHANGE_PROJECT:
+ return project
+ default:
+ return { error: NO_PROJECT }
+ }
+}
+
+const ProjectProvider = ({ children }) => {
+ const [state, dispatch] = React.useReducer(projectReducer, initialState)
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+const useProjectState = () => {
+ const context = React.useContext(ProjectStateContext)
+ if (context === undefined) {
+ throw new Error(`useProjectState must be used within a ProjectProvider`)
+ }
+ return context
+}
+
+const useProjectDispatch = () => {
+ const context = React.useContext(ProjectDispatchContext)
+ if (context === undefined) {
+ throw new Error(`useProjectDispatch must be used within a ProjectProvider`)
+ }
+ return context
+}
+
+export { ProjectProvider, useProjectState, useProjectDispatch }
diff --git a/utils/routeContext.js b/context/routeContext.js
similarity index 80%
rename from utils/routeContext.js
rename to context/routeContext.js
index 2b51784..a661bdb 100644
--- a/utils/routeContext.js
+++ b/context/routeContext.js
@@ -1,10 +1,10 @@
-import React from "react"
-import { HOME, CHANGE_VIEW, GO_BACK } from "./constants"
-import * as R from "ramda"
+import React from 'react'
+import * as R from 'ramda'
+import { SPINNER, CHANGE_VIEW, GO_BACK } from '../utils/constants'
const initialState = {
- currentView: HOME,
- history: [HOME],
+ currentView: SPINNER,
+ history: [SPINNER],
}
const RouteStateContext = React.createContext()
@@ -24,6 +24,9 @@ const routeReducer = (state, { view, type }) => {
currentView: lastView,
history: [...state.history, lastView],
}
+
+ default:
+ throw Error('Route reducer action not recognised')
}
}
@@ -54,4 +57,4 @@ const useRouteDispatch = () => {
return context
}
-export { RouteProvider, useRouteState, useRouteDispatch }
\ No newline at end of file
+export { RouteProvider, useRouteState, useRouteDispatch }
diff --git a/context/unauthRouteContext.js b/context/unauthRouteContext.js
new file mode 100644
index 0000000..42ccfd3
--- /dev/null
+++ b/context/unauthRouteContext.js
@@ -0,0 +1,66 @@
+import React from 'react'
+import * as R from 'ramda'
+import { LANDING, CHANGE_VIEW, GO_BACK } from '../utils/constants'
+
+const initialState = {
+ currentView: LANDING,
+ history: [LANDING],
+}
+
+const UnauthRouteStateContext = React.createContext()
+const UnauthRouteDispatchContext = React.createContext()
+
+const unauthRouteReducer = (state, { view, type }) => {
+ const lastView = R.nth(-2)(state.history)
+
+ switch (type) {
+ case CHANGE_VIEW:
+ return {
+ currentView: view,
+ history: [...state.history, view],
+ }
+ case GO_BACK:
+ return {
+ currentView: lastView,
+ history: [...state.history, lastView],
+ }
+
+ default:
+ throw Error(
+ 'Route reducer action not recognised (from unathRouteContext)',
+ )
+ }
+}
+
+const UnauthRouteProvider = ({ children }) => {
+ const [state, dispatch] = React.useReducer(unauthRouteReducer, initialState)
+ return (
+
+
+ {children}
+
+
+ )
+}
+
+const useRouteStateUnauth = () => {
+ const context = React.useContext(UnauthRouteStateContext)
+ if (context === undefined) {
+ throw new Error(
+ `useRouteState must be used within a RouteProvider (from unathRouteContext)`,
+ )
+ }
+ return context
+}
+
+const useRouteDispatchUnauth = () => {
+ const context = React.useContext(UnauthRouteDispatchContext)
+ if (context === undefined) {
+ throw new Error(
+ `useRouteDispatch must be used within a RouteProvider (from unathRouteContext)`,
+ )
+ }
+ return context
+}
+
+export { UnauthRouteProvider, useRouteStateUnauth, useRouteDispatchUnauth }
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..ab0632e
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,30 @@
+version: '3'
+services:
+ prisma:
+ image: prismagraphql/prisma:1.34.10
+ restart: always
+ ports:
+ - '4466:4466'
+ environment:
+ PRISMA_CONFIG: |
+ port: 4466
+ managementApiSecret: ${PRISMA_MANAGEMENT_API_SECRET:?err}
+ databases:
+ default:
+ connector: postgres
+ host: postgres
+ port: 5432
+ user: prisma
+ password: prisma
+ postgres:
+ image: postgres:10.3
+ restart: always
+ ports:
+ - '5432:5432'
+ environment:
+ POSTGRES_USER: prisma
+ POSTGRES_PASSWORD: prisma
+ volumes:
+ - postgres:/var/lib/postgresql/data
+volumes:
+ postgres: ~
diff --git a/jest.config.js b/jest.config.js
new file mode 100644
index 0000000..36b10a3
--- /dev/null
+++ b/jest.config.js
@@ -0,0 +1,4 @@
+module.exports = {
+ ...require('./test/jest-common'),
+ projects: ['./test/jest.lint.js'],
+}
diff --git a/lib/auth0.js b/lib/auth0.js
new file mode 100644
index 0000000..d70d59f
--- /dev/null
+++ b/lib/auth0.js
@@ -0,0 +1,14 @@
+import { initAuth0 } from '@auth0/nextjs-auth0'
+
+export default initAuth0({
+ clientId: process.env.AUTH0_CLIENT_ID,
+ clientSecret: process.env.AUTH0_CLIENT_SECRET,
+ scope: 'openid profile',
+ domain: process.env.AUTH0_DOMAIN,
+ redirectUri: `${process.env.HOST}/api/callback`,
+ postLogoutRedirectUri: process.env.HOST,
+ session: {
+ cookieSecret: process.env.SESSION_COOKIE_SECRET,
+ cookieLifetime: 60 * 60 * 24 * 180,
+ },
+})
diff --git a/lint-staged.config.js b/lint-staged.config.js
new file mode 100644
index 0000000..5df1181
--- /dev/null
+++ b/lint-staged.config.js
@@ -0,0 +1,26 @@
+const escape = require('shell-quote').quote
+const isWin = process.platform === 'win32'
+
+module.exports = {
+ '**/*.{js,jsx,ts,tsx}': filenames => {
+ const escapedFileNames = filenames
+ .map(filename => `"${isWin ? filename : escape([filename])}"`)
+ .join(' ')
+ return [
+ `prettier --write ${escapedFileNames}`,
+ `jest --config test/jest.lint.js --passWithNoTests ${filenames
+ .map(f => `"${f}"`)
+ .join(' ')}`,
+ `git add ${escapedFileNames}`,
+ ]
+ },
+ '**/*.{json,md,mdx,css,html,yml,yaml,scss,sass}': filenames => {
+ const escapedFileNames = filenames
+ .map(filename => `"${isWin ? filename : escape([filename])}"`)
+ .join(' ')
+ return [
+ `prettier --write ${escapedFileNames}`,
+ `git add ${escapedFileNames}`,
+ ]
+ },
+}
diff --git a/next.config.js b/next.config.js
index fabb1b4..6ff7248 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,32 +1,39 @@
-const webpack = require("webpack") //eslint-disable-line
-const path = require("path") //eslint-disable-line
-const withPlugins = require("next-compose-plugins")
-const withImages = require("next-images")
-const withCSS = require("@zeit/next-css")
-const withFonts = require("next-fonts")
-const FRONTEND_ENV_KEYS = ["NODE_ENV", "HOST"]
+const webpack = require('webpack') //eslint-disable-line
+const path = require('path') //eslint-disable-line
+const withPlugins = require('next-compose-plugins')
+const withImages = require('next-images')
+const withOffline = require('next-offline')
+const withCSS = require('@zeit/next-css')
+const withFonts = require('next-fonts')
+
+const FRONTEND_ENV_KEYS = ['NODE_ENV', 'HOST']
+
+if (process.env.HEROKU_APP_NAME) {
+ process.env.HOST = `https://${process.env.HEROKU_APP_NAME}.herokuapp.com`
+}
+
const envPlugin = FRONTEND_ENV_KEYS.reduce(
- (result, key) =>
- Object.assign({}, result, {
- [`process.env.${key}`]: JSON.stringify(process.env[key]),
- }),
- {}
+ (result, key) => ({
+ ...result,
+ [`process.env.${key}`]: JSON.stringify(process.env[key]),
+ }),
+ {},
)
-module.exports = withPlugins([withImages, withCSS, withFonts], {
+module.exports = withPlugins([withImages, withCSS, withFonts, withOffline], {
webpack: (config, { isServer }) => {
// adds access to specific env variables on front end
config.plugins.push(new webpack.DefinePlugin(envPlugin))
// Fixes npm packages that depend on `fs` module
if (!isServer) {
config.node = {
- fs: "empty",
+ fs: 'empty',
}
}
config.plugins.push(
new webpack.ProvidePlugin({
- cssTheme: path.resolve(path.join(__dirname, "utils/cssTheme")),
- })
+ cssTheme: path.resolve(path.join(__dirname, 'utils/cssTheme')),
+ }),
)
return config
},
-})
\ No newline at end of file
+})
diff --git a/package.json b/package.json
index f898487..33c429c 100644
--- a/package.json
+++ b/package.json
@@ -3,26 +3,73 @@
"version": "0.1.0",
"private": true,
"scripts": {
- "dev": "next dev",
- "build": "next build",
- "start": "next start"
+ "==next==": "------------------------------------------------------------------------------------------",
+ "dev:nodemon": " nodemon --exec \"env-cmd -f ./.config/dev.env node --inspect server.js\" -w ./server.js -w ./next.config.js -w ./.config/dev.env",
+ "build": "NODE_ENV=production next build",
+ "start": "NODE_ENV=production node server.js",
+ "==lint&format==": "-----------------------------------------------------------------------------------",
+ "lint": "jest --config test/jest.lint.js",
+ "format": "npm run prettier -- --write",
+ "prettier": "prettier \"**/*.+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|graphql|mdx)\"",
+ "validate": "npm run lint && npm run prettier -- --list-different",
+ "==docker&prisma==": "---------------------------------------------------------------------------------",
+ "up": "docker-compose up -d",
+ "down": "docker-compose down --remove-orphans",
+ "deploy": "prisma deploy",
+ "schema": "prisma generate",
+ "token": "prisma token",
+ "==prisma-dev==": "--------------------------------------------------------------------------------------",
+ "dev:deploy": "env-cmd -f ./.config/dev.env npm run deploy && npm run dev:schema",
+ "dev:schema": "env-cmd -f ./.config/dev.env npm run schema",
+ "dev:token": "env-cmd -f ./.config/dev.env npm run token",
+ "dev:up": "env-cmd -f ./.config/dev.env npm run up",
+ "dev:down": "env-cmd -f ./.config/dev.env npm run down",
+ "dev:seed": "env-cmd -f ./.config/dev.env ts-node -O '{\"module\": \"commonjs\"}' ./prisma/seeds.ts",
+ "dev:flushdb": "env-cmd -f ./.config/dev.env ts-node -O '{\"module\": \"commonjs\"}' ./prisma/flushDB.ts",
+ "==prisma:staging==": "------------------------------------------------------------------------------------",
+ "staging:deploy": "env-cmd -f ./.config/staging.env npm run deploy && npm run staging:schema",
+ "staging:schema": "env-cmd -f ./.config/staging.env npm run schema",
+ "staging:token": "env-cmd -f ./.config/staging.env npm run token",
+ "staging:up": "env-cmd -f ./.config/staging.env npm run up",
+ "staging:down": "env-cmd -f ./.config/staging.env npm run down"
+ },
+ "husky": {
+ "hooks": {
+ "pre-commit": "lint-staged"
+ }
+ },
+ "jest-runner-eslint": {
+ "cliOptions": {
+ "fix": true,
+ "ext": [
+ ".js",
+ ".ts"
+ ]
+ }
},
"dependencies": {
+ "@auth0/nextjs-auth0": "^0.5.0",
"@zeit/next-css": "^1.0.1",
+ "airtable": "^0.8.1",
"axios": "^0.19.0",
+ "cloudinary": "^1.17.0",
"connect-redis": "^4.0.3",
"express": "^4.17.1",
"express-session": "^1.17.0",
"express-sslify": "^1.2.0",
"formidable": "^1.2.1",
- "formik": "^2.0.3",
+ "formik": "^2.0.4",
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1",
- "next": "9.1.3",
+ "libphonenumber-js": "^1.7.29",
+ "moment": "^2.24.0",
+ "next": "^9.1.4",
"next-compose-plugins": "^2.2.0",
"next-cookies": "^2.0.1",
"next-fonts": "^0.19.0",
"next-images": "^1.2.0",
+ "next-offline": "^4.0.6",
+ "node-cron": "^2.0.3",
"passport": "^0.4.0",
"passport-auth0": "^1.2.1",
"prisma": "^1.34.10",
@@ -32,28 +79,33 @@
"react-toastify": "^5.4.1",
"redis": "^2.8.0",
"styled-components": "^4.4.1",
- "tailwindcss": "^1.1.3"
+ "tailwindcss": "^1.1.3",
+ "twilio": "^3.37.1",
+ "yup": "^0.27.0"
},
"devDependencies": {
"@types/formidable": "^1.0.31",
"@types/node": "^12.12.7",
- "@types/ramda": "^0.26.33",
+ "@types/ramda": "^0.26.34",
"@types/react": "^16.9.11",
- "babel-eslint": "^10.0.3",
+ "@typescript-eslint/eslint-plugin": "^2.9.0",
+ "@typescript-eslint/parser": "^2.9.0",
+ "autoprefixer": "^9.7.1",
"concurrently": "^5.0.0",
- "cors": "^2.8.5",
"env-cmd": "^10.0.1",
"eslint": "^6.6.0",
- "eslint-config-esnext": "^4.0.0",
- "eslint-config-node": "^4.0.0",
- "eslint-config-prettier": "^6.5.0",
- "eslint-plugin-babel": "^5.3.0",
- "eslint-plugin-import": "^2.18.2",
- "eslint-plugin-react": "^7.16.0",
- "eslint-plugin-react-hooks": "^2.2.0",
+ "eslint-config-kentcdodds": "^14.6.1",
+ "eslint-config-prettier": "^6.7.0",
+ "eslint-import-resolver-typescript": "^2.0.0",
+ "husky": "^3.0.9",
+ "jest": "^24.9.0",
+ "jest-runner-eslint": "^0.7.5",
+ "lint-staged": "^9.4.3",
"nodemon": "^1.19.4",
"postcss-import": "^12.0.1",
"prettier": "^1.19.1",
+ "shell-quote": "^1.7.2",
+ "tachyons": "^4.11.1",
"typescript": "^3.7.2"
}
}
diff --git a/pages/_app.js b/pages/_app.js
index d123be4..147998a 100644
--- a/pages/_app.js
+++ b/pages/_app.js
@@ -1,15 +1,29 @@
-import React from "react"
+import React from 'react'
+import App from 'next/app'
+import { ThemeProvider } from 'styled-components'
+import { toast } from 'react-toastify'
+import resolveConfig from 'tailwindcss/resolveConfig'
-import App from "next/app"
-import { RouteProvider } from "../utils/routeContext"
+import { AuthProvider } from '../context/authContext'
+
+import tailwindConfig from '../tailwind.config'
+
+import 'react-toastify/dist/ReactToastify.min.css'
+import '../styles/index.css'
+
+toast.configure()
+
+const { theme } = resolveConfig(tailwindConfig)
class Snapeat extends App {
render() {
const { Component, pageProps } = this.props
return (
-
-
-
+
+
+
+
+
)
}
}
diff --git a/pages/_error.js b/pages/_error.js
new file mode 100644
index 0000000..28c45e7
--- /dev/null
+++ b/pages/_error.js
@@ -0,0 +1,58 @@
+import React from 'react'
+import styled from 'styled-components'
+
+import logo from '../public/logos/logo1.svg'
+import robot from '../public/icons/404-robot.svg'
+
+function Error({ statusCode }) {
+ return (
+
+
+
+
+
+
+
+ Oooops, it's {statusCode ? `a ${statusCode}` : 'an error'}!
+
+
+ We noticed you lost your way. Not to worry though!
+
+
+ Go back to the homepage but first, please{' '}
+ {
+
+ tell us what happened{' '}
+
+ }
+ .
+
+
+
+
+ Go to homepage
+
+
+
+ )
+}
+
+const Text = styled.p.attrs({
+ className: 'text-xl text-center m-5 sm:m-8',
+})``
+
+Error.getInitialProps = ({ res, err }) => {
+ const statusCode = res ? res.statusCode : err ? err.statusCode : 404
+ return { statusCode }
+}
+
+export default Error
diff --git a/pages/api/callback.js b/pages/api/callback.js
new file mode 100644
index 0000000..c334427
--- /dev/null
+++ b/pages/api/callback.js
@@ -0,0 +1,11 @@
+import auth0 from '../../lib/auth0'
+
+export default async function callback(req, res) {
+ try {
+ await auth0.handleCallback(req, res, { redirectTo: '/' })
+ } catch (error) {
+ //eslint-disable-next-line no-console
+ console.error(error)
+ res.status(error.status || 500).end(error.message)
+ }
+}
diff --git a/pages/api/create-user.ts b/pages/api/create-user.ts
new file mode 100644
index 0000000..00d0b4a
--- /dev/null
+++ b/pages/api/create-user.ts
@@ -0,0 +1,49 @@
+import * as R from 'ramda'
+import { NextApiRequest, NextApiResponse } from 'next'
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+interface Request {
+ postCode: string
+ ages: string[]
+ project: string
+ phoneNumber: string
+ email: string
+}
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { postCode, ages, project, phoneNumber, email }: Request = req.body
+
+ const ageToChildren = R.map((age: string) => ({ age }))(ages)
+
+ const user = await prisma.createUser({
+ consentGDPR: true,
+ postCode,
+ email,
+ projects: {
+ connect: [
+ {
+ slug: project,
+ },
+ ],
+ },
+ children: {
+ create: ageToChildren,
+ },
+ phoneNumber,
+ })
+
+ if (user) {
+ return res.status(200).json({ user })
+ }
+
+ if (!user) {
+ return res.status(500).end()
+ }
+ } catch (e) {
+ console.log(`Error creating user`, e) //eslint-disable-line no-console
+ return res.status(400).end()
+ }
+}
diff --git a/pages/api/does-phonenumber-exist.ts b/pages/api/does-phonenumber-exist.ts
new file mode 100644
index 0000000..dbb21aa
--- /dev/null
+++ b/pages/api/does-phonenumber-exist.ts
@@ -0,0 +1,15 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+export default async (
+ req: NextApiRequest,
+ res: NextApiResponse,
+): Promise => {
+ const doesPhoneNumberExist = await prisma.$exists.user({
+ phoneNumber: req.query.phonenumber as string,
+ })
+
+ return res.status(200).json({ doesPhoneNumberExist })
+}
diff --git a/pages/api/does-user-exist.ts b/pages/api/does-user-exist.ts
new file mode 100644
index 0000000..133d5b0
--- /dev/null
+++ b/pages/api/does-user-exist.ts
@@ -0,0 +1,10 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import { prisma } from '../../prisma/generated/ts'
+
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ const doesUserExist = await prisma.$exists.user({
+ email: req.query.email as string,
+ })
+
+ return res.status(200).json({ doesUserExist })
+}
diff --git a/pages/api/get-available-projects.ts b/pages/api/get-available-projects.ts
new file mode 100644
index 0000000..8a3212a
--- /dev/null
+++ b/pages/api/get-available-projects.ts
@@ -0,0 +1,19 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+import { PROJECT_NOT_FOUND } from '../../utils/constants'
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const projects = await prisma.projects()
+ if (!projects) {
+ return res.status(404).json({ projects: { error: PROJECT_NOT_FOUND } })
+ }
+
+ return res.status(200).json({ projects })
+ } catch (e) {
+ console.log(`Error getting available projects`, e) //eslint-disable-line no-console
+ return res.status(404).json({ projects: { error: PROJECT_NOT_FOUND } })
+ }
+}
diff --git a/pages/api/get-project-from-slug.ts b/pages/api/get-project-from-slug.ts
new file mode 100644
index 0000000..d8e05c4
--- /dev/null
+++ b/pages/api/get-project-from-slug.ts
@@ -0,0 +1,20 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+import { PROJECT_NOT_FOUND } from '../../utils/constants'
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ const slug = req.query.slug as string
+ try {
+ const project = await prisma.project({ slug })
+ if (!project) {
+ return res.status(404).json({ project: { error: PROJECT_NOT_FOUND } })
+ }
+
+ return res.status(200).json({ project })
+ } catch (e) {
+ console.log(`Error getting project from slug ${slug}`, e) //eslint-disable-line no-console
+ return res.status(404).json({ project: { error: PROJECT_NOT_FOUND } })
+ }
+}
diff --git a/pages/api/get-user.ts b/pages/api/get-user.ts
new file mode 100644
index 0000000..7163a11
--- /dev/null
+++ b/pages/api/get-user.ts
@@ -0,0 +1,34 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+import { USER_NOT_FOUND } from '../../utils/constants'
+
+const fragment = `
+fragment UserWithProjects on User {
+ id
+ consentGDPR
+ email
+ airtableId
+ projects {
+ slug
+ }
+}
+`
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ const email = req.query.email as string
+ try {
+ const user = await prisma.user({ email }).$fragment(fragment)
+ if (!user) {
+ return res.status(404).json({ user: { error: USER_NOT_FOUND } })
+ }
+
+ return res.status(200).json({ user })
+ } catch (e) {
+ console.log(`Error getting user ${email}`, e) //eslint-disable-line no-console
+ return res.status(404).json({ project: { error: USER_NOT_FOUND } })
+ }
+}
diff --git a/pages/api/is-postcode-valid.ts b/pages/api/is-postcode-valid.ts
new file mode 100644
index 0000000..b356541
--- /dev/null
+++ b/pages/api/is-postcode-valid.ts
@@ -0,0 +1,23 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import axios from 'axios'
+
+export default async (
+ req: NextApiRequest,
+ res: NextApiResponse,
+): Promise => {
+ const postcode = req.query.postcode as string
+
+ try {
+ if (postcode) {
+ const {
+ data: { result },
+ } = await axios.get(
+ `https://api.postcodes.io/postcodes/${postcode}/autocomplete`,
+ )
+ return res.status(200).json({ postCodeArray: result })
+ }
+ return res.status(200).json({ postCodeArray: null })
+ } catch (e) {
+ console.log('Error validating postcode') //eslint-disable-line no-console
+ }
+}
diff --git a/pages/api/login.js b/pages/api/login.js
new file mode 100644
index 0000000..6ebf128
--- /dev/null
+++ b/pages/api/login.js
@@ -0,0 +1,11 @@
+import auth0 from '../../lib/auth0'
+
+export default async function login(req, res) {
+ try {
+ await auth0.handleLogin(req, res)
+ } catch (error) {
+ //eslint-disable-next-line no-console
+ console.error(error)
+ res.status(error.status || 500).end(error.message)
+ }
+}
diff --git a/pages/api/logout.js b/pages/api/logout.js
new file mode 100644
index 0000000..8ddb7fa
--- /dev/null
+++ b/pages/api/logout.js
@@ -0,0 +1,11 @@
+import auth0 from '../../lib/auth0'
+
+export default async function logout(req, res) {
+ try {
+ await auth0.handleLogout(req, res)
+ } catch (error) {
+ //eslint-disable-next-line no-console
+ console.error(error)
+ res.status(error.status || 500).end(error.message)
+ }
+}
diff --git a/pages/api/me.js b/pages/api/me.js
new file mode 100644
index 0000000..912b535
--- /dev/null
+++ b/pages/api/me.js
@@ -0,0 +1,14 @@
+import auth0 from '../../lib/auth0'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+export default async function me(req, res) {
+ try {
+ await auth0.handleProfile(req, res)
+ } catch (error) {
+ //eslint-disable-next-line no-console
+ console.error(error)
+ res.status(error.status || 500).end(error.message)
+ }
+}
diff --git a/pages/api/submit-food-data.ts b/pages/api/submit-food-data.ts
new file mode 100644
index 0000000..2c071eb
--- /dev/null
+++ b/pages/api/submit-food-data.ts
@@ -0,0 +1,89 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import * as R from 'ramda'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const {
+ imageURL,
+ categories,
+ proportionFruit,
+ proportionVeg,
+ tags,
+ user,
+ } = req.body
+
+ //create meal
+
+ const connectVeg = proportionVeg
+ ? {
+ proportionVeg: {
+ connect: {
+ name: proportionVeg,
+ },
+ },
+ }
+ : {}
+
+ const connectFruit = proportionFruit
+ ? {
+ proportionFruit: {
+ connect: {
+ name: proportionFruit,
+ },
+ },
+ }
+ : {}
+
+ const meal = await prisma.createMeal({
+ imageURL,
+ user: { connect: { email: user.email } },
+ ...connectVeg,
+ ...connectFruit,
+ })
+
+ //update meal categories
+
+ await Promise.all(
+ R.map((category: string) =>
+ prisma.updateMeal({
+ data: {
+ categories: {
+ connect: {
+ name: category,
+ },
+ },
+ },
+ where: {
+ id: meal.id,
+ },
+ }),
+ )(categories),
+ )
+
+ await Promise.all(
+ R.map((tag: string) =>
+ prisma.updateMeal({
+ data: {
+ tags: {
+ connect: {
+ name: tag,
+ },
+ },
+ },
+ where: {
+ id: meal.id,
+ },
+ }),
+ )(tags),
+ )
+
+ res.status(200).json({ meal })
+ } catch (e) {
+ //eslint-disable-next-line no-console
+ console.log('There was an error in submit food data:', e)
+ }
+}
diff --git a/pages/api/upload-meal-to-airtable.ts b/pages/api/upload-meal-to-airtable.ts
new file mode 100644
index 0000000..c7db77b
--- /dev/null
+++ b/pages/api/upload-meal-to-airtable.ts
@@ -0,0 +1,76 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import Airtable from 'airtable'
+import moment from 'moment'
+import * as R from 'ramda'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+Airtable.configure({
+ endpointUrl: 'https://api.airtable.com',
+ apiKey: process.env.AIRTABLE_API_KEY,
+})
+
+const base = Airtable.base(process.env.AIRTABLE_BASE)
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const {
+ imageURL,
+ categories,
+ proportionFruit,
+ proportionVeg,
+ tags,
+ userAirtableId,
+ mealId,
+ } = req.body
+
+ const dbCategories = await prisma.categories()
+ const dbTags = await prisma.tags()
+ const dbProportions = await prisma.proportions()
+ interface AirtableDbLink {
+ name: string
+ airtableId: string
+ }
+
+ const getAirtableIds = (airtableArray: AirtableDbLink[], prismaArray) =>
+ R.pipe(
+ x => R.filter(({ name }) => R.contains(name)(airtableArray))(x),
+ x => R.map(({ airtableId }) => airtableId)(x as AirtableDbLink[]),
+ )(prismaArray)
+
+ const Categories = getAirtableIds(categories, dbCategories)
+ const Tags = getAirtableIds(tags, dbTags)
+ const ProportionFruit = getAirtableIds([proportionFruit], dbProportions)
+ const ProportionVeg = getAirtableIds([proportionVeg], dbProportions)
+
+ const [{ id: airtableMeal }] = await base('Meals').create([
+ {
+ fields: {
+ Image: [
+ {
+ url: imageURL,
+ },
+ ],
+ 'User ID': [userAirtableId],
+ 'Proportion of Fruit': ProportionFruit,
+ 'Proportion of Veg': ProportionVeg,
+ Categories,
+ Tags,
+ 'Date and Time': moment(),
+ },
+ },
+ ])
+
+ await prisma.updateMeal({
+ data: { airtableId: airtableMeal },
+ where: { id: mealId },
+ })
+ return res.status(200).json({})
+ } catch (e) {
+ //eslint-disable-next-line no-console
+ console.log('There was an error in upload-meal-to-airtable:', e)
+ return res.status(400).json({})
+ }
+}
diff --git a/pages/api/upload-photo.ts b/pages/api/upload-photo.ts
new file mode 100644
index 0000000..505b541
--- /dev/null
+++ b/pages/api/upload-photo.ts
@@ -0,0 +1,50 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import formidable from 'formidable'
+// import axios from 'axios'
+
+// import { prisma } from '../../prisma/generated/ts'
+
+const cloudinary = require('cloudinary').v2
+
+cloudinary.config({
+ //eslint-disable-next-line
+ cloud_name: process.env.CLOUDINARY_CLOUD_NAME,
+ //eslint-disable-next-line
+ api_key: process.env.CLOUDINARY_API_KEY,
+ //eslint-disable-next-line
+ api_secret: process.env.CLOUDINARY_API_SECRET,
+})
+
+//eslint-disable-next-line
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const form = new formidable.IncomingForm()
+ form.parse(req, (err, _fields, files) => {
+ if (err) {
+ //eslint-disable-next-line no-console
+ console.error('form parsing error', err)
+ }
+
+ const { photo } = files
+
+ cloudinary.uploader.upload(`${photo.path}`, (error: any, result: any) => {
+ if (error) {
+ //eslint-disable-next-line no-console
+ console.error('cloudinary error error', error)
+ }
+
+ const { url } = result
+
+ res.status(200).json({ url })
+ })
+ })
+ } catch (e) {
+ console.log('There was an error uploading the photo:', e) //eslint-disable-line no-console
+ }
+}
+
+export const config = {
+ api: {
+ bodyParser: false,
+ },
+}
diff --git a/pages/api/upload-user-to-airtable.ts b/pages/api/upload-user-to-airtable.ts
new file mode 100644
index 0000000..419c615
--- /dev/null
+++ b/pages/api/upload-user-to-airtable.ts
@@ -0,0 +1,58 @@
+import { NextApiRequest, NextApiResponse } from 'next'
+import Airtable from 'airtable'
+import * as R from 'ramda'
+
+//eslint-disable-next-line
+import { prisma } from '../../prisma/generated/ts'
+
+Airtable.configure({
+ endpointUrl: 'https://api.airtable.com',
+ apiKey: process.env.AIRTABLE_API_KEY,
+})
+
+const base = Airtable.base(process.env.AIRTABLE_BASE)
+
+export default async (req: NextApiRequest, res: NextApiResponse) => {
+ try {
+ const { ages, postCode, project, consentGDPR = true, user } = req.body
+
+ const fragment = `
+ fragment ProjectId on Project {
+ airtableId
+ }`
+
+ const { airtableId } = await prisma
+ .project({ slug: project })
+ .$fragment(fragment)
+
+ const [{ id: airtableParent }] = await base('Users').create([
+ {
+ fields: {
+ Project: [airtableId],
+ 'Postcode Area': postCode,
+ 'Consent for Data Usage': consentGDPR,
+ },
+ },
+ ])
+
+ await base('Children').create(
+ R.map(age => ({
+ fields: {
+ Parent: [airtableParent],
+ 'Age Group': age,
+ },
+ }))(ages),
+ )
+
+ const updatedUser = await prisma.updateUser({
+ data: { airtableId: airtableParent },
+ where: { id: user.id },
+ })
+
+ return res.status(200).json({ updatedUser })
+ } catch (e) {
+ //eslint-disable-next-line no-console
+ console.log('There was an error in upload-user-to-airtable:', e)
+ return res.status(400).json({})
+ }
+}
diff --git a/pages/index.js b/pages/index.js
index d070655..210a043 100644
--- a/pages/index.js
+++ b/pages/index.js
@@ -1,22 +1,57 @@
-import React from "react"
-import Head from "next/head"
-import getView from "../views/getView"
-import { useRouteState } from "../utils/routeContext"
+import React, { useEffect } from 'react'
+import Head from 'next/head'
+import styled from 'styled-components'
+
+import { useAuth } from '../context/authContext'
+
+import { RouteProvider } from '../context/routeContext'
+import { UnauthRouteProvider } from '../context/unauthRouteContext'
+import { ConsentProvider } from '../context/consentContext'
+
+import AuthenticatedApp from '../apps/AuthenticatedApp'
+import UnauthenticatedApp from '../apps/UnauthenticatedApp'
+
+const Container = styled.section.attrs({
+ className: 'bg-lightgray w-screen h-screen',
+})``
+
+// TODO: check that everything that was in _app.js and pages/index.js (ie toast and css and ting) is still in new setup
const Index = () => {
- const { currentView } = useRouteState()
+ const { auth0User } = useAuth()
+
+ useEffect(() => {
+ const iOS =
+ !!navigator.platform && /iPad|iPhone|iPod/.test(navigator.platform)
+
+ if (iOS) {
+ const manifestLink = document.getElementById('manifest-link')
+ manifestLink.parentNode.removeChild(manifestLink)
+ }
+ }, [])
return (
<>
- App
+ SnapEat
+
+
-
- Welcome to Snapeat
- {getView(currentView)}
-
+
+
+ {auth0User ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
>
)
}
diff --git a/postcss.config.js b/postcss.config.js
index 7c22d2e..c9dbc3a 100644
--- a/postcss.config.js
+++ b/postcss.config.js
@@ -1,7 +1,7 @@
module.exports = {
plugins: [
- require("postcss-import"),
- require("tailwindcss"),
- require("autoprefixer")
- ]
-};
+ require('postcss-import'),
+ require('tailwindcss'),
+ require('autoprefixer'),
+ ],
+}
diff --git a/prisma/datamodel.prisma b/prisma/datamodel.prisma
new file mode 100644
index 0000000..277fdce
--- /dev/null
+++ b/prisma/datamodel.prisma
@@ -0,0 +1,74 @@
+type User {
+ id: ID! @id
+ airtableId: String @unique
+ consentGDPR: Boolean!
+ postCode: String!
+ email: String! @unique
+ meals: [Meal] @relation(onDelete: CASCADE)
+ projects: [Project!] @relation(onDelete: SET_NULL)
+ children: [Child!] @relation(onDelete: CASCADE)
+ phoneNumber: String! @unique
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
+
+type Child {
+ id: ID! @id
+ airtableId: String @unique
+ Parent: User! @relation(onDelete: SET_NULL)
+ age: String!
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
+
+type Meal {
+ id: ID! @id
+ airtableId: String @unique
+ user: User!
+ imageURL: String!
+ categories: [Category!] @relation(onDelete: SET_NULL)
+ tags: [Tag] @relation(onDelete: SET_NULL)
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+ proportionFruit: Proportion @relation(name: "Fruit")
+ proportionVeg: Proportion @relation(name: "Veg")
+}
+
+type Tag {
+ id: ID! @id
+ airtableId: String @unique
+ name: String! @unique
+ meals: [Meal] @relation(onDelete: SET_NULL)
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
+
+type Category {
+ id: ID! @id
+ airtableId: String @unique
+ name: String! @unique
+ meals: [Meal!] @relation(onDelete: SET_NULL)
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
+
+
+type Project {
+ id: ID! @id
+ airtableId: String @unique
+ name: String!
+ slug: String! @unique
+ users: [User!] @relation(onDelete: CASCADE)
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
+
+type Proportion {
+ id: ID! @id
+ airtableId: String @unique
+ name: String! @unique
+ fruitMeals: [Meal] @relation(name: "Fruit")
+ vegMeals: [Meal] @relation(name: "Veg")
+ updatedAt: DateTime! @updatedAt
+ createdAt: DateTime! @createdAt
+}
\ No newline at end of file
diff --git a/prisma/flushDB.ts b/prisma/flushDB.ts
new file mode 100644
index 0000000..a59f541
--- /dev/null
+++ b/prisma/flushDB.ts
@@ -0,0 +1,40 @@
+import { prisma } from './generated/ts'
+
+const flushDB = async () => {
+ const deleteCategories = await prisma.deleteManyCategories({
+ id_not: 0,
+ })
+
+ const deleteTags = await prisma.deleteManyTags({
+ id_not: 0,
+ })
+
+ const deleteProportions = await prisma.deleteManyProportions({
+ id_not: 0,
+ })
+ const deleteMeals = await prisma.deleteManyMeals({
+ id_not: 0,
+ })
+
+ const deleteUsers = await prisma.deleteManyUsers({
+ id_not: 0,
+ })
+
+ const deleteProjects = await prisma.deleteManyProjects({
+ id_not: 0,
+ })
+
+ const deleteChildren = await prisma.deleteManyChildren({
+ id_not: 0,
+ })
+
+ console.log(JSON.stringify(deleteChildren, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteCategories, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteProjects, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteUsers, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteMeals, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteProportions, undefined, 2)) //eslint-disable-line no-console
+ console.log(JSON.stringify(deleteTags, undefined, 2)) //eslint-disable-line no-console
+}
+
+export default flushDB
diff --git a/prisma/generated/js/index.d.ts b/prisma/generated/js/index.d.ts
new file mode 100644
index 0000000..382d838
--- /dev/null
+++ b/prisma/generated/js/index.d.ts
@@ -0,0 +1,3852 @@
+// Code generated by Prisma (prisma@1.34.10). DO NOT EDIT.
+// Please don't change this file manually but run `prisma generate` to update it.
+// For more information, please read the docs: https://www.prisma.io/docs/prisma-client/
+
+import { DocumentNode } from "graphql";
+import {
+ makePrismaClientClass,
+ BaseClientOptions,
+ Model
+} from "prisma-client-lib";
+import { typeDefs } from "./prisma-schema";
+
+export type AtLeastOne }> = Partial &
+ U[keyof U];
+
+export type Maybe = T | undefined | null;
+
+export interface Exists {
+ category: (where?: CategoryWhereInput) => Promise;
+ child: (where?: ChildWhereInput) => Promise;
+ meal: (where?: MealWhereInput) => Promise;
+ project: (where?: ProjectWhereInput) => Promise;
+ proportion: (where?: ProportionWhereInput) => Promise;
+ tag: (where?: TagWhereInput) => Promise;
+ user: (where?: UserWhereInput) => Promise;
+}
+
+export interface Node {}
+
+export type FragmentableArray = Promise> & Fragmentable;
+
+export interface Fragmentable {
+ $fragment(fragment: string | DocumentNode): Promise;
+}
+
+export interface Prisma {
+ $exists: Exists;
+ $graphql: (
+ query: string,
+ variables?: { [key: string]: any }
+ ) => Promise;
+
+ /**
+ * Queries
+ */
+
+ category: (where: CategoryWhereUniqueInput) => CategoryNullablePromise;
+ categories: (args?: {
+ where?: CategoryWhereInput;
+ orderBy?: CategoryOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ categoriesConnection: (args?: {
+ where?: CategoryWhereInput;
+ orderBy?: CategoryOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => CategoryConnectionPromise;
+ child: (where: ChildWhereUniqueInput) => ChildNullablePromise;
+ children: (args?: {
+ where?: ChildWhereInput;
+ orderBy?: ChildOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ childrenConnection: (args?: {
+ where?: ChildWhereInput;
+ orderBy?: ChildOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => ChildConnectionPromise;
+ meal: (where: MealWhereUniqueInput) => MealNullablePromise;
+ meals: (args?: {
+ where?: MealWhereInput;
+ orderBy?: MealOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ mealsConnection: (args?: {
+ where?: MealWhereInput;
+ orderBy?: MealOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => MealConnectionPromise;
+ project: (where: ProjectWhereUniqueInput) => ProjectNullablePromise;
+ projects: (args?: {
+ where?: ProjectWhereInput;
+ orderBy?: ProjectOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ projectsConnection: (args?: {
+ where?: ProjectWhereInput;
+ orderBy?: ProjectOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => ProjectConnectionPromise;
+ proportion: (where: ProportionWhereUniqueInput) => ProportionNullablePromise;
+ proportions: (args?: {
+ where?: ProportionWhereInput;
+ orderBy?: ProportionOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ proportionsConnection: (args?: {
+ where?: ProportionWhereInput;
+ orderBy?: ProportionOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => ProportionConnectionPromise;
+ tag: (where: TagWhereUniqueInput) => TagNullablePromise;
+ tags: (args?: {
+ where?: TagWhereInput;
+ orderBy?: TagOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ tagsConnection: (args?: {
+ where?: TagWhereInput;
+ orderBy?: TagOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => TagConnectionPromise;
+ user: (where: UserWhereUniqueInput) => UserNullablePromise;
+ users: (args?: {
+ where?: UserWhereInput;
+ orderBy?: UserOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => FragmentableArray;
+ usersConnection: (args?: {
+ where?: UserWhereInput;
+ orderBy?: UserOrderByInput;
+ skip?: Int;
+ after?: String;
+ before?: String;
+ first?: Int;
+ last?: Int;
+ }) => UserConnectionPromise;
+ node: (args: { id: ID_Output }) => Node;
+
+ /**
+ * Mutations
+ */
+
+ createCategory: (data: CategoryCreateInput) => CategoryPromise;
+ updateCategory: (args: {
+ data: CategoryUpdateInput;
+ where: CategoryWhereUniqueInput;
+ }) => CategoryPromise;
+ updateManyCategories: (args: {
+ data: CategoryUpdateManyMutationInput;
+ where?: CategoryWhereInput;
+ }) => BatchPayloadPromise;
+ upsertCategory: (args: {
+ where: CategoryWhereUniqueInput;
+ create: CategoryCreateInput;
+ update: CategoryUpdateInput;
+ }) => CategoryPromise;
+ deleteCategory: (where: CategoryWhereUniqueInput) => CategoryPromise;
+ deleteManyCategories: (where?: CategoryWhereInput) => BatchPayloadPromise;
+ createChild: (data: ChildCreateInput) => ChildPromise;
+ updateChild: (args: {
+ data: ChildUpdateInput;
+ where: ChildWhereUniqueInput;
+ }) => ChildPromise;
+ updateManyChildren: (args: {
+ data: ChildUpdateManyMutationInput;
+ where?: ChildWhereInput;
+ }) => BatchPayloadPromise;
+ upsertChild: (args: {
+ where: ChildWhereUniqueInput;
+ create: ChildCreateInput;
+ update: ChildUpdateInput;
+ }) => ChildPromise;
+ deleteChild: (where: ChildWhereUniqueInput) => ChildPromise;
+ deleteManyChildren: (where?: ChildWhereInput) => BatchPayloadPromise;
+ createMeal: (data: MealCreateInput) => MealPromise;
+ updateMeal: (args: {
+ data: MealUpdateInput;
+ where: MealWhereUniqueInput;
+ }) => MealPromise;
+ updateManyMeals: (args: {
+ data: MealUpdateManyMutationInput;
+ where?: MealWhereInput;
+ }) => BatchPayloadPromise;
+ upsertMeal: (args: {
+ where: MealWhereUniqueInput;
+ create: MealCreateInput;
+ update: MealUpdateInput;
+ }) => MealPromise;
+ deleteMeal: (where: MealWhereUniqueInput) => MealPromise;
+ deleteManyMeals: (where?: MealWhereInput) => BatchPayloadPromise;
+ createProject: (data: ProjectCreateInput) => ProjectPromise;
+ updateProject: (args: {
+ data: ProjectUpdateInput;
+ where: ProjectWhereUniqueInput;
+ }) => ProjectPromise;
+ updateManyProjects: (args: {
+ data: ProjectUpdateManyMutationInput;
+ where?: ProjectWhereInput;
+ }) => BatchPayloadPromise;
+ upsertProject: (args: {
+ where: ProjectWhereUniqueInput;
+ create: ProjectCreateInput;
+ update: ProjectUpdateInput;
+ }) => ProjectPromise;
+ deleteProject: (where: ProjectWhereUniqueInput) => ProjectPromise;
+ deleteManyProjects: (where?: ProjectWhereInput) => BatchPayloadPromise;
+ createProportion: (data: ProportionCreateInput) => ProportionPromise;
+ updateProportion: (args: {
+ data: ProportionUpdateInput;
+ where: ProportionWhereUniqueInput;
+ }) => ProportionPromise;
+ updateManyProportions: (args: {
+ data: ProportionUpdateManyMutationInput;
+ where?: ProportionWhereInput;
+ }) => BatchPayloadPromise;
+ upsertProportion: (args: {
+ where: ProportionWhereUniqueInput;
+ create: ProportionCreateInput;
+ update: ProportionUpdateInput;
+ }) => ProportionPromise;
+ deleteProportion: (where: ProportionWhereUniqueInput) => ProportionPromise;
+ deleteManyProportions: (where?: ProportionWhereInput) => BatchPayloadPromise;
+ createTag: (data: TagCreateInput) => TagPromise;
+ updateTag: (args: {
+ data: TagUpdateInput;
+ where: TagWhereUniqueInput;
+ }) => TagPromise;
+ updateManyTags: (args: {
+ data: TagUpdateManyMutationInput;
+ where?: TagWhereInput;
+ }) => BatchPayloadPromise;
+ upsertTag: (args: {
+ where: TagWhereUniqueInput;
+ create: TagCreateInput;
+ update: TagUpdateInput;
+ }) => TagPromise;
+ deleteTag: (where: TagWhereUniqueInput) => TagPromise;
+ deleteManyTags: (where?: TagWhereInput) => BatchPayloadPromise;
+ createUser: (data: UserCreateInput) => UserPromise;
+ updateUser: (args: {
+ data: UserUpdateInput;
+ where: UserWhereUniqueInput;
+ }) => UserPromise;
+ updateManyUsers: (args: {
+ data: UserUpdateManyMutationInput;
+ where?: UserWhereInput;
+ }) => BatchPayloadPromise;
+ upsertUser: (args: {
+ where: UserWhereUniqueInput;
+ create: UserCreateInput;
+ update: UserUpdateInput;
+ }) => UserPromise;
+ deleteUser: (where: UserWhereUniqueInput) => UserPromise;
+ deleteManyUsers: (where?: UserWhereInput) => BatchPayloadPromise;
+
+ /**
+ * Subscriptions
+ */
+
+ $subscribe: Subscription;
+}
+
+export interface Subscription {
+ category: (
+ where?: CategorySubscriptionWhereInput
+ ) => CategorySubscriptionPayloadSubscription;
+ child: (
+ where?: ChildSubscriptionWhereInput
+ ) => ChildSubscriptionPayloadSubscription;
+ meal: (
+ where?: MealSubscriptionWhereInput
+ ) => MealSubscriptionPayloadSubscription;
+ project: (
+ where?: ProjectSubscriptionWhereInput
+ ) => ProjectSubscriptionPayloadSubscription;
+ proportion: (
+ where?: ProportionSubscriptionWhereInput
+ ) => ProportionSubscriptionPayloadSubscription;
+ tag: (
+ where?: TagSubscriptionWhereInput
+ ) => TagSubscriptionPayloadSubscription;
+ user: (
+ where?: UserSubscriptionWhereInput
+ ) => UserSubscriptionPayloadSubscription;
+}
+
+export interface ClientConstructor {
+ new (options?: BaseClientOptions): T;
+}
+
+/**
+ * Types
+ */
+
+export type MealOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "imageURL_ASC"
+ | "imageURL_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type ProjectOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "name_ASC"
+ | "name_DESC"
+ | "slug_ASC"
+ | "slug_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type UserOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "consentGDPR_ASC"
+ | "consentGDPR_DESC"
+ | "postCode_ASC"
+ | "postCode_DESC"
+ | "email_ASC"
+ | "email_DESC"
+ | "phoneNumber_ASC"
+ | "phoneNumber_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type ChildOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "age_ASC"
+ | "age_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type CategoryOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "name_ASC"
+ | "name_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type TagOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "name_ASC"
+ | "name_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type ProportionOrderByInput =
+ | "id_ASC"
+ | "id_DESC"
+ | "airtableId_ASC"
+ | "airtableId_DESC"
+ | "name_ASC"
+ | "name_DESC"
+ | "updatedAt_ASC"
+ | "updatedAt_DESC"
+ | "createdAt_ASC"
+ | "createdAt_DESC";
+
+export type MutationType = "CREATED" | "UPDATED" | "DELETED";
+
+export interface TagUpdateWithWhereUniqueWithoutMealsInput {
+ where: TagWhereUniqueInput;
+ data: TagUpdateWithoutMealsDataInput;
+}
+
+export type CategoryWhereUniqueInput = AtLeastOne<{
+ id: Maybe;
+ airtableId?: Maybe;
+ name?: Maybe;
+}>;
+
+export interface TagUpdateManyDataInput {
+ airtableId?: Maybe;
+ name?: Maybe;
+}
+
+export interface CategoryWhereInput {
+ id?: Maybe;
+ id_not?: Maybe;
+ id_in?: Maybe;
+ id_not_in?: Maybe;
+ id_lt?: Maybe;
+ id_lte?: Maybe;
+ id_gt?: Maybe;
+ id_gte?: Maybe;
+ id_contains?: Maybe;
+ id_not_contains?: Maybe;
+ id_starts_with?: Maybe;
+ id_not_starts_with?: Maybe;
+ id_ends_with?: Maybe;
+ id_not_ends_with?: Maybe;
+ airtableId?: Maybe;
+ airtableId_not?: Maybe;
+ airtableId_in?: Maybe;
+ airtableId_not_in?: Maybe;
+ airtableId_lt?: Maybe;
+ airtableId_lte?: Maybe;
+ airtableId_gt?: Maybe;
+ airtableId_gte?: Maybe;
+ airtableId_contains?: Maybe;
+ airtableId_not_contains?: Maybe;
+ airtableId_starts_with?: Maybe;
+ airtableId_not_starts_with?: Maybe;
+ airtableId_ends_with?: Maybe;
+ airtableId_not_ends_with?: Maybe;
+ name?: Maybe;
+ name_not?: Maybe;
+ name_in?: Maybe;
+ name_not_in?: Maybe;
+ name_lt?: Maybe;
+ name_lte?: Maybe;
+ name_gt?: Maybe;
+ name_gte?: Maybe;
+ name_contains?: Maybe;
+ name_not_contains?: Maybe;
+ name_starts_with?: Maybe;
+ name_not_starts_with?: Maybe;
+ name_ends_with?: Maybe;
+ name_not_ends_with?: Maybe;
+ meals_every?: Maybe;
+ meals_some?: Maybe;
+ meals_none?: Maybe;
+ updatedAt?: Maybe;
+ updatedAt_not?: Maybe;
+ updatedAt_in?: Maybe;
+ updatedAt_not_in?: Maybe;
+ updatedAt_lt?: Maybe;
+ updatedAt_lte?: Maybe;
+ updatedAt_gt?: Maybe;
+ updatedAt_gte?: Maybe;
+ createdAt?: Maybe;
+ createdAt_not?: Maybe;
+ createdAt_in?: Maybe;
+ createdAt_not_in?: Maybe;
+ createdAt_lt?: Maybe;
+ createdAt_lte?: Maybe;
+ createdAt_gt?: Maybe;
+ createdAt_gte?: Maybe;
+ AND?: Maybe;
+ OR?: Maybe;
+ NOT?: Maybe;
+}
+
+export interface ProportionUpdateOneWithoutFruitMealsInput {
+ create?: Maybe;
+ update?: Maybe;
+ upsert?: Maybe;
+ delete?: Maybe;
+ disconnect?: Maybe;
+ connect?: Maybe;
+}
+
+export interface ProportionWhereInput {
+ id?: Maybe;
+ id_not?: Maybe;
+ id_in?: Maybe;
+ id_not_in?: Maybe;
+ id_lt?: Maybe;
+ id_lte?: Maybe;
+ id_gt?: Maybe;
+ id_gte?: Maybe;
+ id_contains?: Maybe;
+ id_not_contains?: Maybe;
+ id_starts_with?: Maybe;
+ id_not_starts_with?: Maybe;
+ id_ends_with?: Maybe;
+ id_not_ends_with?: Maybe;
+ airtableId?: Maybe;
+ airtableId_not?: Maybe;
+ airtableId_in?: Maybe;
+ airtableId_not_in?: Maybe;
+ airtableId_lt?: Maybe;
+ airtableId_lte?: Maybe;
+ airtableId_gt?: Maybe;
+ airtableId_gte?: Maybe;
+ airtableId_contains?: Maybe;
+ airtableId_not_contains?: Maybe;
+ airtableId_starts_with?: Maybe;
+ airtableId_not_starts_with?: Maybe;
+ airtableId_ends_with?: Maybe;
+ airtableId_not_ends_with?: Maybe;
+ name?: Maybe;
+ name_not?: Maybe;
+ name_in?: Maybe;
+ name_not_in?: Maybe;
+ name_lt?: Maybe;
+ name_lte?: Maybe;
+ name_gt?: Maybe;
+ name_gte?: Maybe;
+ name_contains?: Maybe;
+ name_not_contains?: Maybe;
+ name_starts_with?: Maybe;
+ name_not_starts_with?: Maybe;
+ name_ends_with?: Maybe;
+ name_not_ends_with?: Maybe;
+ fruitMeals_every?: Maybe;
+ fruitMeals_some?: Maybe;
+ fruitMeals_none?: Maybe;
+ vegMeals_every?: Maybe;
+ vegMeals_some?: Maybe;
+ vegMeals_none?: Maybe;
+ updatedAt?: Maybe;
+ updatedAt_not?: Maybe;
+ updatedAt_in?: Maybe;
+ updatedAt_not_in?: Maybe;
+ updatedAt_lt?: Maybe;
+ updatedAt_lte?: Maybe;
+ updatedAt_gt?: Maybe;
+ updatedAt_gte?: Maybe;
+ createdAt?: Maybe;
+ createdAt_not?: Maybe;
+ createdAt_in?: Maybe;
+ createdAt_not_in?: Maybe;
+ createdAt_lt?: Maybe;
+ createdAt_lte?: Maybe;
+ createdAt_gt?: Maybe;
+ createdAt_gte?: Maybe;
+ AND?: Maybe;
+ OR?: Maybe;
+ NOT?: Maybe;
+}
+
+export interface UserUpdateWithoutMealsDataInput {
+ airtableId?: Maybe;
+ consentGDPR?: Maybe;
+ postCode?: Maybe;
+ email?: Maybe;
+ projects?: Maybe;
+ children?: Maybe;
+ phoneNumber?: Maybe;
+}
+
+export interface MealUpdateManyMutationInput {
+ airtableId?: Maybe;
+ imageURL?: Maybe;
+}
+
+export interface ProjectUpdateManyWithoutUsersInput {
+ create?: Maybe<
+ ProjectCreateWithoutUsersInput[] | ProjectCreateWithoutUsersInput
+ >;
+ delete?: Maybe;
+ connect?: Maybe;
+ set?: Maybe;
+ disconnect?: Maybe;
+ update?: Maybe<
+ | ProjectUpdateWithWhereUniqueWithoutUsersInput[]
+ | ProjectUpdateWithWhereUniqueWithoutUsersInput
+ >;
+ upsert?: Maybe<
+ | ProjectUpsertWithWhereUniqueWithoutUsersInput[]
+ | ProjectUpsertWithWhereUniqueWithoutUsersInput
+ >;
+ deleteMany?: Maybe;
+ updateMany?: Maybe<
+ | ProjectUpdateManyWithWhereNestedInput[]
+ | ProjectUpdateManyWithWhereNestedInput
+ >;
+}
+
+export interface ProportionUpdateWithoutFruitMealsDataInput {
+ airtableId?: Maybe;
+ name?: Maybe;
+ vegMeals?: Maybe;
+}
+
+export interface ProjectUpdateWithWhereUniqueWithoutUsersInput {
+ where: ProjectWhereUniqueInput;
+ data: ProjectUpdateWithoutUsersDataInput;
+}
+
+export interface TagSubscriptionWhereInput {
+ mutation_in?: Maybe;
+ updatedFields_contains?: Maybe;
+ updatedFields_contains_every?: Maybe;
+ updatedFields_contains_some?: Maybe;
+ node?: Maybe;
+ AND?: Maybe;
+ OR?: Maybe;
+ NOT?: Maybe;
+}
+
+export interface ProjectUpdateWithoutUsersDataInput {
+ airtableId?: Maybe;
+ name?: Maybe;
+ slug?: Maybe;
+}
+
+export interface ProjectWhereInput {
+ id?: Maybe;
+ id_not?: Maybe;
+ id_in?: Maybe;
+ id_not_in?: Maybe;
+ id_lt?: Maybe;
+ id_lte?: Maybe;
+ id_gt?: Maybe;
+ id_gte?: Maybe;
+ id_contains?: Maybe;
+ id_not_contains?: Maybe;
+ id_starts_with?: Maybe;
+ id_not_starts_with?: Maybe;
+ id_ends_with?: Maybe;
+ id_not_ends_with?: Maybe;
+ airtableId?: Maybe;
+ airtableId_not?: Maybe;
+ airtableId_in?: Maybe;
+ airtableId_not_in?: Maybe;
+ airtableId_lt?: Maybe;
+ airtableId_lte?: Maybe;
+ airtableId_gt?: Maybe;
+ airtableId_gte?: Maybe;
+ airtableId_contains?: Maybe;
+ airtableId_not_contains?: Maybe;
+ airtableId_starts_with?: Maybe;
+ airtableId_not_starts_with?: Maybe;
+ airtableId_ends_with?: Maybe;
+ airtableId_not_ends_with?: Maybe;
+ name?: Maybe;
+ name_not?: Maybe;
+ name_in?: Maybe;
+ name_not_in?: Maybe;
+ name_lt?: Maybe;
+ name_lte?: Maybe;
+ name_gt?: Maybe;
+ name_gte?: Maybe;
+ name_contains?: Maybe;
+ name_not_contains?: Maybe;
+ name_starts_with?: Maybe;
+ name_not_starts_with?: Maybe;
+ name_ends_with?: Maybe;
+ name_not_ends_with?: Maybe;
+ slug?: Maybe;
+ slug_not?: Maybe;
+ slug_in?: Maybe;
+ slug_not_in?: Maybe;
+ slug_lt?: Maybe;
+ slug_lte?: Maybe;
+ slug_gt?: Maybe;
+ slug_gte?: Maybe;
+ slug_contains?: Maybe;
+ slug_not_contains?: Maybe;
+ slug_starts_with?: Maybe;
+ slug_not_starts_with?: Maybe;
+ slug_ends_with?: Maybe;
+ slug_not_ends_with?: Maybe