├── .husky
├── .gitignore
└── pre-commit
├── src
├── react-app-env.d.ts
├── icons
│ ├── index.ts
│ └── CalendarPlus.tsx
├── theme
│ ├── components
│ │ ├── Button.ts
│ │ ├── Input.ts
│ │ ├── Textarea.ts
│ │ ├── Alert.ts
│ │ ├── Divider.ts
│ │ └── index.ts
│ ├── colors.ts
│ ├── styles.ts
│ ├── index.ts
│ └── getComputedColorFromChakra.ts
├── foods-filters
│ ├── persistence
│ │ ├── index.ts
│ │ └── loadFoodsFilter.ts
│ ├── index.ts
│ ├── foodsFilter.ts
│ ├── FoodsFilterPopoverOrModal
│ │ ├── index.tsx
│ │ ├── Footer.tsx
│ │ ├── Trigger.tsx
│ │ ├── Modal.tsx
│ │ ├── Popover.tsx
│ │ └── Content.tsx
│ └── useFilterFoods.ts
├── form
│ ├── types.ts
│ ├── index.ts
│ ├── useSelectInputText.ts
│ ├── duplicate.ts
│ ├── useFormError.ts
│ └── names.ts
├── stats
│ ├── StatsFormFields
│ │ ├── StatFormField
│ │ │ ├── types.ts
│ │ │ └── ReadOnlyInput.tsx
│ │ ├── useGetDailyValuePercent.ts
│ │ ├── useGetValue.ts
│ │ ├── ReavealButton.tsx
│ │ └── index.tsx
│ ├── calculations
│ │ ├── index.ts
│ │ ├── getStatsTree.ts
│ │ ├── aggregateStats.ts
│ │ ├── getMacrosPercents.ts
│ │ ├── getDailyValuePercent.ts
│ │ └── getEnergiesEstimates.ts
│ ├── amountAsNumber.ts
│ ├── StatValueDetail.tsx
│ ├── useUpdateMealStats.ts
│ ├── index.ts
│ ├── types.ts
│ ├── getUnit.ts
│ ├── PdfStatsLayout.tsx
│ ├── objectFromNutritionDataKeys.ts
│ ├── EnergyStat.tsx
│ └── statsVariants.ts
├── notes
│ ├── index.ts
│ ├── notesForm.ts
│ └── EditNotesModal
│ │ ├── Content
│ │ ├── notesForm.ts
│ │ ├── Form
│ │ │ └── Header.tsx
│ │ ├── NotesFormProvider.tsx
│ │ └── index.tsx
│ │ └── index.tsx
├── foods-categories
│ ├── types.ts
│ ├── index.ts
│ ├── FoodCategoriesSelect.tsx
│ └── categories.json
├── general
│ ├── MenuOrDrawer
│ │ ├── MenuOrDrawerSeparator.tsx
│ │ ├── MenuOrDrawerItem.tsx
│ │ ├── Trigger.tsx
│ │ ├── Menu
│ │ │ ├── index.tsx
│ │ │ └── getMenuItems.tsx
│ │ ├── Drawer
│ │ │ ├── index.tsx
│ │ │ └── getDrawerButtons.tsx
│ │ └── index.tsx
│ ├── deepCopy.ts
│ ├── getCtrlKeyName.ts
│ ├── useSameOrPreviousValue.ts
│ ├── HFadeScroll
│ │ ├── ScrollContainer.tsx
│ │ └── FadeBox.tsx
│ ├── minDelay.ts
│ ├── TooltipCommandLabel.tsx
│ ├── useElementHeight.ts
│ ├── Loader.tsx
│ ├── useRunIfNotUnmounted.ts
│ ├── RightAligned.tsx
│ ├── ScreenSizeProvider
│ │ ├── context.ts
│ │ └── index.tsx
│ ├── Menu.tsx
│ ├── ResponsiveButton.tsx
│ ├── ResponsiveIconButton.tsx
│ ├── useOneTimeCheckStore.ts
│ ├── Badge.tsx
│ ├── Tooltip.tsx
│ ├── index.ts
│ └── useSelection.ts
├── meals
│ ├── types.ts
│ ├── index.ts
│ ├── MealsList
│ │ ├── MealItem
│ │ │ ├── Notes.tsx
│ │ │ ├── Header
│ │ │ │ ├── MenuOrDrawer.tsx
│ │ │ │ └── Name.tsx
│ │ │ ├── PresenceAnimation.tsx
│ │ │ └── useGetAndUpdateStats.ts
│ │ ├── MealsControls.tsx
│ │ ├── useScrollToAndFocusMeal.ts
│ │ └── EmptyList.tsx
│ ├── useGetMealFormStatsTree.ts
│ ├── mealForm.ts
│ └── PdfMealsList
│ │ ├── Notes.tsx
│ │ └── index.tsx
├── diets
│ ├── persistence
│ │ ├── ExportModal
│ │ │ ├── Content
│ │ │ │ └── Exporter
│ │ │ │ │ ├── worker
│ │ │ │ │ ├── index.ts
│ │ │ │ │ ├── custom.d.ts
│ │ │ │ │ ├── types.ts
│ │ │ │ │ └── worker.tsx
│ │ │ │ │ └── index.tsx
│ │ │ └── index.tsx
│ │ ├── canExportDietForm.ts
│ │ ├── index.ts
│ │ ├── loadLastOrDefaultDietForm.ts
│ │ ├── hasMissingFoods.ts
│ │ ├── useDietImportErrors.tsx
│ │ └── parseDietForm.ts
│ ├── types.ts
│ ├── index.ts
│ ├── dietForm.ts
│ ├── useGetDietFormStatsTree.ts
│ └── DietEditor
│ │ ├── Form
│ │ ├── useVariantFormActions.ts
│ │ ├── Footer.tsx
│ │ ├── Controls
│ │ │ └── ExportButton.tsx
│ │ └── useDietFormEvents.ts
│ │ ├── index.tsx
│ │ └── DndContextProvider.tsx
├── persistence
│ ├── fixWhiteSpace.tsx
│ ├── getUntitledFileName.ts
│ ├── index.ts
│ ├── useBlobUrl.ts
│ ├── useSaveValue.ts
│ ├── DownloadButton.tsx
│ ├── file.ts
│ └── useImportFileError.ts
├── ingredients
│ ├── types.ts
│ ├── index.ts
│ ├── IngredientsList
│ │ ├── IngredientItem
│ │ │ ├── MenuOrDrawer.tsx
│ │ │ ├── MissingStatsLayout.tsx
│ │ │ ├── Notes.tsx
│ │ │ ├── PresenceAnimation.tsx
│ │ │ └── getMenuOrDrawerItems.tsx
│ │ └── EmptyList.tsx
│ ├── getIngredient.ts
│ └── ingredientForm.ts
├── undoRedo
│ ├── index.ts
│ ├── UndoRedoButtons
│ │ ├── index.tsx
│ │ ├── RedoButton.tsx
│ │ └── UndoButton.tsx
│ ├── appLocation.ts
│ └── useKeyboard.ts
├── layout
│ ├── index.tsx
│ ├── useHasSideNavigation.ts
│ ├── Page
│ │ ├── PageBody.tsx
│ │ ├── index.tsx
│ │ ├── ElementContainer.tsx
│ │ ├── PageFooter.tsx
│ │ └── PageHeader.tsx
│ ├── RightAligned.tsx
│ └── MainLayout.tsx
├── portions
│ ├── types.ts
│ ├── getPortionDescription.ts
│ ├── index.ts
│ ├── getAmountFromPortionsToGrams.ts
│ ├── useGetToGramsConversionFactor.ts
│ ├── getToGramsConversionFactor.ts
│ ├── PortionsSelect.tsx
│ ├── formatAmount.ts
│ ├── PortionsMenuOrDrawer
│ │ ├── Trigger.tsx
│ │ ├── Drawer
│ │ │ ├── PortionItem.tsx
│ │ │ └── index.tsx
│ │ └── index.tsx
│ ├── defaultPortions.ts
│ ├── getIngredientPortionDescription.ts
│ └── usePortionsStore.ts
├── setupTests.ts
├── dom
│ ├── index.ts
│ ├── isElementInViewport.ts
│ ├── animateScrollLeft.ts
│ ├── useGetRefForId.ts
│ └── useScrollTo.ts
├── foods
│ ├── persistence
│ │ ├── index.ts
│ │ ├── loadFoods.ts
│ │ ├── useImportFoods.ts
│ │ └── MissingFoodsModal.tsx
│ ├── FoodsList
│ │ └── VirtualizedList
│ │ │ ├── Inner.tsx
│ │ │ ├── FoodItem
│ │ │ ├── DisappearingBox.tsx
│ │ │ └── AnimateAppear.tsx
│ │ │ └── FoodItemRenderer.tsx
│ ├── index.ts
│ ├── types.ts
│ ├── foodVolumeForm.ts
│ ├── FoodsDrawer
│ │ ├── Content
│ │ │ ├── SelectedFoodsList
│ │ │ │ ├── SelectedFoodItem.tsx
│ │ │ │ └── index.tsx
│ │ │ ├── Header.tsx
│ │ │ ├── MenuButtons.tsx
│ │ │ └── useFoodEvents.ts
│ │ └── index.tsx
│ ├── FoodModal
│ │ ├── Content
│ │ │ ├── Form
│ │ │ │ ├── Footer.tsx
│ │ │ │ ├── Header.tsx
│ │ │ │ ├── useTabs.tsx
│ │ │ │ └── Tabs
│ │ │ │ │ └── NutritionFactsFormFields.tsx
│ │ │ ├── FoodFormProvider.tsx
│ │ │ ├── useDeleteFood.ts
│ │ │ └── DeleteConfirmationModal.tsx
│ │ └── index.tsx
│ ├── builtIn
│ │ └── index.ts
│ └── FoodInfo.tsx
├── variants
│ ├── getVariantFormIndexAfterRemove.ts
│ ├── index.ts
│ ├── VariantsList
│ │ ├── VariantItem
│ │ │ ├── useScrollIntoView.ts
│ │ │ ├── PresenceAnimation.tsx
│ │ │ ├── getMenuOrDrawerItems.tsx
│ │ │ └── useVariantFormEvents.ts
│ │ ├── useScrollState.ts
│ │ ├── VariantNameModal
│ │ │ ├── useSubmitVariantNameForm.ts
│ │ │ ├── index.tsx
│ │ │ ├── VariantNameFormProvider.tsx
│ │ │ └── variantNameForm.ts
│ │ ├── VariantsMenuOrDrawer
│ │ │ ├── Drawer
│ │ │ │ └── VariantItem.tsx
│ │ │ ├── Trigger.tsx
│ │ │ ├── Menu.tsx
│ │ │ └── index.tsx
│ │ └── AddVariantButton.tsx
│ ├── variantForm.ts
│ ├── VariantsDetailsModal
│ │ ├── index.tsx
│ │ └── Content
│ │ │ ├── VariantsDetailsFormProvider.tsx
│ │ │ ├── variantsDetailsForm.ts
│ │ │ └── useVariantFormEvents.ts
│ ├── VariantStats
│ │ └── EnergyStat.tsx
│ ├── useGetVariantFormStatsTree.ts
│ └── PdfVariantsList
│ │ └── index.tsx
├── reportWebVitals.ts
├── index.tsx
└── App.tsx
├── public
├── favicon.ico
├── robots.txt
├── favicon-16x16.png
├── favicon-32x32.png
├── mstile-150x150.png
├── apple-touch-icon.png
├── android-chrome-192x192.png
├── android-chrome-512x512.png
├── browserconfig.xml
└── site.webmanifest
├── devices-preview.png
├── .prettierrc
├── .prettierignore
├── .vscode
└── settings.json
├── .gitignore
├── tsconfig.json
└── LICENSE
/.husky/.gitignore:
--------------------------------------------------------------------------------
1 | _
2 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/icons/index.ts:
--------------------------------------------------------------------------------
1 | export { default as CalendarPlus } from './CalendarPlus'
2 |
--------------------------------------------------------------------------------
/src/theme/components/Button.ts:
--------------------------------------------------------------------------------
1 | const Button = {}
2 |
3 | export default Button
4 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx lint-staged
5 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/devices-preview.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/devices-preview.png
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/public/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/favicon-16x16.png
--------------------------------------------------------------------------------
/public/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/favicon-32x32.png
--------------------------------------------------------------------------------
/public/mstile-150x150.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/mstile-150x150.png
--------------------------------------------------------------------------------
/src/foods-filters/persistence/index.ts:
--------------------------------------------------------------------------------
1 | export { default as loadFoodsFilter } from './loadFoodsFilter'
2 |
--------------------------------------------------------------------------------
/public/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/apple-touch-icon.png
--------------------------------------------------------------------------------
/src/form/types.ts:
--------------------------------------------------------------------------------
1 | type Form = {
2 | fieldId: string
3 | name: string
4 | }
5 |
6 | export type { Form }
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "arrowParens": "avoid",
3 | "singleQuote": true,
4 | "semi": false,
5 | "printWidth": 80
6 | }
--------------------------------------------------------------------------------
/public/android-chrome-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/android-chrome-192x192.png
--------------------------------------------------------------------------------
/public/android-chrome-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vangelov/calories-in/HEAD/public/android-chrome-512x512.png
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/node_modules
2 | **/dist
3 | **/package.json
4 | **/yarn.lock
5 | **/package-lock.json
6 | **/.eslintrc.json
7 | **/tsconfig.json
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/StatFormField/types.ts:
--------------------------------------------------------------------------------
1 | type InputType = 'text' | 'nutritionValue' | 'foodCategory'
2 |
3 | export type { InputType }
4 |
--------------------------------------------------------------------------------
/src/notes/index.ts:
--------------------------------------------------------------------------------
1 | export { default as EditNotesModal } from './EditNotesModal'
2 | export * from './EditNotesModal'
3 | export * from './notesForm'
4 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.formatOnPaste": true,
3 | "editor.formatOnSave": true,
4 | "editor.defaultFormatter": "esbenp.prettier-vscode"
5 | }
--------------------------------------------------------------------------------
/src/foods-categories/types.ts:
--------------------------------------------------------------------------------
1 | type FoodCategory = {
2 | id: number
3 | name: string
4 | color: string
5 | }
6 |
7 | export type { FoodCategory }
8 |
--------------------------------------------------------------------------------
/src/theme/components/Input.ts:
--------------------------------------------------------------------------------
1 | const Input = {
2 | defaultProps: {
3 | focusBorderColor: 'teal.400',
4 | },
5 | }
6 |
7 | export default Input
8 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/MenuOrDrawerSeparator.tsx:
--------------------------------------------------------------------------------
1 | function MenuOrDrawerSeparator() {
2 | return null
3 | }
4 |
5 | export default MenuOrDrawerSeparator
6 |
--------------------------------------------------------------------------------
/src/theme/components/Textarea.ts:
--------------------------------------------------------------------------------
1 | const Textarea = {
2 | defaultProps: {
3 | focusBorderColor: 'teal.400',
4 | },
5 | }
6 |
7 | export default Textarea
8 |
--------------------------------------------------------------------------------
/src/meals/types.ts:
--------------------------------------------------------------------------------
1 | import { Ingredient } from 'ingredients'
2 |
3 | type Meal = {
4 | name: string
5 | ingredients: Ingredient[]
6 | }
7 |
8 | export type { Meal }
9 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/Content/Exporter/worker/index.ts:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | import Worker from 'comlink-loader!./worker'
3 |
4 | export default Worker
5 |
--------------------------------------------------------------------------------
/src/diets/types.ts:
--------------------------------------------------------------------------------
1 | import { Meal } from 'meals'
2 |
3 | type Diet = {
4 | id: number
5 | name: string
6 | meals: Meal[]
7 | }
8 |
9 | export type { Diet }
10 |
--------------------------------------------------------------------------------
/src/general/deepCopy.ts:
--------------------------------------------------------------------------------
1 | const deepCopy = (value: any, replacer?: (key: string, value: any) => any) =>
2 | JSON.parse(JSON.stringify(value, replacer))
3 |
4 | export default deepCopy
5 |
--------------------------------------------------------------------------------
/src/theme/colors.ts:
--------------------------------------------------------------------------------
1 | const colors = {
2 | custom: {
3 | '50': '#74CFD1',
4 |
5 | '500': '#74CFD1',
6 | '600': '#38a8aa',
7 | },
8 | }
9 |
10 | export default colors
11 |
--------------------------------------------------------------------------------
/src/foods-categories/index.ts:
--------------------------------------------------------------------------------
1 | export { default as FoodCategoriesSelect } from './FoodCategoriesSelect'
2 | export * from './types'
3 | export { default as foodCategories } from './categories.json'
4 |
--------------------------------------------------------------------------------
/src/persistence/fixWhiteSpace.tsx:
--------------------------------------------------------------------------------
1 | function fixWhiteSpace(text: string) {
2 | return text.replace(/\\n/g, '\n').replace(/\r/g, '\r').replace(/\t/g, '\t')
3 | }
4 |
5 | export default fixWhiteSpace
6 |
--------------------------------------------------------------------------------
/src/ingredients/types.ts:
--------------------------------------------------------------------------------
1 | import { FoodId } from 'foods'
2 |
3 | type Ingredient = {
4 | foodId: FoodId
5 | amount: number
6 | portionId: string
7 | }
8 |
9 | export type { Ingredient }
10 |
--------------------------------------------------------------------------------
/src/undoRedo/index.ts:
--------------------------------------------------------------------------------
1 | export * from './useDietFormVersionsStore'
2 | export { default as useKeyboard } from './useKeyboard'
3 | export { default as UndoRedoButtons } from './UndoRedoButtons'
4 | export * from './appLocation'
5 |
--------------------------------------------------------------------------------
/src/form/index.ts:
--------------------------------------------------------------------------------
1 | export { default as duplicate } from './duplicate'
2 | export * from './names'
3 | export { default as useFormError } from './useFormError'
4 | export { default as useSelectInputText } from './useSelectInputText'
5 |
--------------------------------------------------------------------------------
/src/layout/index.tsx:
--------------------------------------------------------------------------------
1 | export { default as Page } from './Page'
2 | export * from './Page'
3 | export { default as MainLayout } from './MainLayout'
4 | export * from './MainLayout'
5 | export { default as RightAligned } from './RightAligned'
6 |
--------------------------------------------------------------------------------
/src/portions/types.ts:
--------------------------------------------------------------------------------
1 | type Portion = {
2 | id: string
3 | unit: string
4 | singular: string
5 | description?: string
6 | gramsPerAmount?: number
7 | millilitersPerAmount?: number
8 | }
9 |
10 | export type { Portion }
11 |
--------------------------------------------------------------------------------
/src/theme/components/Alert.ts:
--------------------------------------------------------------------------------
1 | const Alert = {
2 | variants: {
3 | subtle: {
4 | container: {
5 | bg: 'gray.100',
6 | borderRadius: 6,
7 | },
8 | },
9 | },
10 | }
11 |
12 | export default Alert
13 |
--------------------------------------------------------------------------------
/src/theme/components/Divider.ts:
--------------------------------------------------------------------------------
1 | const Divider = {
2 | sizes: {
3 | md: {
4 | borderBottomWidth: '8px',
5 | },
6 | lg: {
7 | borderBottomWidth: '12px',
8 | },
9 | },
10 | }
11 |
12 | export default Divider
13 |
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/theme/components/index.ts:
--------------------------------------------------------------------------------
1 | export { default as Input } from './Input'
2 | export { default as Button } from './Button'
3 | export { default as Divider } from './Divider'
4 | export { default as Textarea } from './Textarea'
5 | export { default as Alert } from './Alert'
6 |
--------------------------------------------------------------------------------
/src/general/getCtrlKeyName.ts:
--------------------------------------------------------------------------------
1 | function isMac() {
2 | return navigator.platform.indexOf('Mac') > -1
3 | }
4 |
5 | function getCtrlKeyName() {
6 | if (isMac()) {
7 | return 'Cmd'
8 | }
9 |
10 | return 'Ctrl'
11 | }
12 |
13 | export default getCtrlKeyName
14 |
--------------------------------------------------------------------------------
/src/dom/index.ts:
--------------------------------------------------------------------------------
1 | export { default as animateScrollLeft } from './animateScrollLeft'
2 | export { default as isElementInViewport } from './isElementInViewport'
3 | export { default as useGetRefForId } from './useGetRefForId'
4 | export { default as useScrollTo } from './useScrollTo'
5 |
--------------------------------------------------------------------------------
/src/layout/useHasSideNavigation.ts:
--------------------------------------------------------------------------------
1 | // import { useScreenSize } from 'general'
2 |
3 | function useHasSideNavigation() {
4 | /*const screenSize = useScreenSize()
5 | return screenSize >= ScreenSize.Large*/
6 |
7 | return false
8 | }
9 |
10 | export default useHasSideNavigation
11 |
--------------------------------------------------------------------------------
/public/browserconfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | #da532c
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/foods/persistence/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MissingFoodsModal } from './MissingFoodsModal'
2 | export { default as useImportFoods } from './useImportFoods'
3 | export { default as FoodsListModal } from './FoodsListModal'
4 | export * from './FoodsListModal'
5 | export { default as loadFoods } from './loadFoods'
6 |
--------------------------------------------------------------------------------
/src/foods-filters/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useFilterFoods } from './useFilterFoods'
2 | export * from './foodsFilter'
3 | export { default as useFoodsFilterStore } from './useFoodsFilterStore'
4 | export * from './useFoodsFilterStore'
5 | export { default as FoodsFilterPopoverOrModal } from './FoodsFilterPopoverOrModal'
6 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/Content/Exporter/worker/custom.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'comlink-loader!*' {
2 | import { Params } from './types'
3 |
4 | class WebpackWorker extends Worker {
5 | constructor()
6 |
7 | getDietPdfBlob(data: Params): Promise
8 | }
9 |
10 | export = WebpackWorker
11 | }
12 |
--------------------------------------------------------------------------------
/src/meals/index.ts:
--------------------------------------------------------------------------------
1 | export { default as MealsList } from './MealsList'
2 | export { default as useMealsFormsActions } from './useMealsFormsActions'
3 | export * from './useMealsFormsActions'
4 | export * from './mealForm'
5 | export * from './types'
6 | export { default as useGetMealFormStatsTree } from './useGetMealFormStatsTree'
7 |
--------------------------------------------------------------------------------
/src/diets/persistence/canExportDietForm.ts:
--------------------------------------------------------------------------------
1 | import { DietForm } from 'diets'
2 |
3 | function canExportDietForm(dietForm: DietForm) {
4 | const { variantsForms } = dietForm
5 |
6 | return variantsForms.some(({ mealsForms }) => {
7 | return mealsForms.length > 0
8 | })
9 | }
10 |
11 | export default canExportDietForm
12 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/MenuOrDrawerItem.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, ReactNode } from 'react'
2 |
3 | type Props = {
4 | onClick?: () => void
5 | children: ReactNode
6 | icon: ReactElement
7 | isDisabled?: boolean
8 | }
9 |
10 | function MenuItem(props: Props) {
11 | return null
12 | }
13 |
14 | export default MenuItem
15 |
--------------------------------------------------------------------------------
/src/general/useSameOrPreviousValue.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef } from 'react'
2 |
3 | function useSameOrPreviousValue(value: T) {
4 | const previous = useRef(value)
5 |
6 | useEffect(() => {
7 | previous.current = value
8 | }, [value])
9 |
10 | return previous.current
11 | }
12 |
13 | export default useSameOrPreviousValue
14 |
--------------------------------------------------------------------------------
/src/general/HFadeScroll/ScrollContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from '@chakra-ui/react'
2 | import styled from '@emotion/styled'
3 |
4 | const ScrollContainer = styled(Flex)`
5 | -ms-overflow-style: none;
6 | scrollbar-width: none;
7 |
8 | ::-webkit-scrollbar {
9 | display: none;
10 | }
11 | `
12 |
13 | export default ScrollContainer
14 |
--------------------------------------------------------------------------------
/src/portions/getPortionDescription.ts:
--------------------------------------------------------------------------------
1 | import { Portion } from 'portions'
2 |
3 | function getPortionDescription(portion: Portion) {
4 | const { id, millilitersPerAmount } = portion
5 |
6 | if (millilitersPerAmount && id !== 'milliliters') {
7 | return `(${millilitersPerAmount} ml)`
8 | }
9 |
10 | return ''
11 | }
12 |
13 | export default getPortionDescription
14 |
--------------------------------------------------------------------------------
/src/form/useSelectInputText.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react'
2 |
3 | function useSelectInputText(inputRef: RefObject) {
4 | useEffect(() => {
5 | const input = inputRef.current
6 |
7 | if (input) {
8 | input.setSelectionRange(0, input.value.length)
9 | }
10 | }, [inputRef])
11 | }
12 |
13 | export default useSelectInputText
14 |
--------------------------------------------------------------------------------
/src/foods/persistence/loadFoods.ts:
--------------------------------------------------------------------------------
1 | import { builtInFoods } from 'foods'
2 |
3 | function loadFoods() {
4 | const userFoodsString = localStorage.getItem('userFoods')
5 |
6 | if (userFoodsString) {
7 | const userFoods = JSON.parse(userFoodsString)
8 | return [...userFoods, ...builtInFoods]
9 | }
10 |
11 | return builtInFoods
12 | }
13 |
14 | export default loadFoods
15 |
--------------------------------------------------------------------------------
/src/persistence/getUntitledFileName.ts:
--------------------------------------------------------------------------------
1 | type Params = {
2 | prefix?: string
3 | }
4 |
5 | function getUntitledFileName({ prefix = 'Untitled' }: Params = {}) {
6 | const date = new Date()
7 | const dateString = date.toISOString()
8 | const dateStringParts = dateString.split('.')
9 |
10 | return `${prefix}-${dateStringParts[0]}`
11 | }
12 |
13 | export default getUntitledFileName
14 |
--------------------------------------------------------------------------------
/src/notes/notesForm.ts:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup'
2 |
3 | type NotesForm = {
4 | notes: string
5 | }
6 |
7 | function getNotesForm(notes?: string): NotesForm {
8 | return {
9 | notes: notes || '',
10 | }
11 | }
12 |
13 | const notesFormSchema = object().shape({
14 | name: string(),
15 | })
16 |
17 | export { getNotesForm, notesFormSchema }
18 |
19 | export type { NotesForm }
20 |
--------------------------------------------------------------------------------
/src/undoRedo/UndoRedoButtons/index.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonGroup } from '@chakra-ui/react'
2 | import UndoButton from './UndoButton'
3 | import RedoButton from './RedoButton'
4 |
5 | function UndoRedoButtons() {
6 | return (
7 |
8 |
9 |
10 |
11 | )
12 | }
13 |
14 | export default UndoRedoButtons
15 |
--------------------------------------------------------------------------------
/src/diets/index.ts:
--------------------------------------------------------------------------------
1 | export * from './dietForm'
2 | export { default as useDietFormStore } from './useDietFormStore'
3 | export * from './useDietFormStore'
4 | export { default as DietEditor } from './DietEditor'
5 | export * from './types'
6 | export { default as useGetDietFormStatsTree } from './useGetDietFormStatsTree'
7 | export { default as useScrollManager } from './useScrollManager'
8 | export * from './useScrollManager'
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
--------------------------------------------------------------------------------
/src/notes/EditNotesModal/Content/notesForm.ts:
--------------------------------------------------------------------------------
1 | import { object, string } from 'yup'
2 |
3 | type NotesForm = {
4 | notes: string
5 | }
6 |
7 | function getNotesForm(notes?: string): NotesForm {
8 | return {
9 | notes: notes || '',
10 | }
11 | }
12 |
13 | const notesFormSchema = object().shape({
14 | name: string(),
15 | })
16 |
17 | export { getNotesForm, notesFormSchema }
18 |
19 | export type { NotesForm }
20 |
--------------------------------------------------------------------------------
/src/diets/persistence/index.ts:
--------------------------------------------------------------------------------
1 | export { default as ExportModal } from './ExportModal'
2 | export { default as useImportDietForm } from './useImportDietForm'
3 | export { default as parseDietForm } from './parseDietForm'
4 | export { default as hasMissingFoods } from './hasMissingFoods'
5 | export { default as loadLastOrDefaultDietForm } from './loadLastOrDefaultDietForm'
6 | export { default as canExportDietForm } from './canExportDietForm'
7 |
--------------------------------------------------------------------------------
/src/foods/FoodsList/VirtualizedList/Inner.tsx:
--------------------------------------------------------------------------------
1 | import { forwardRef } from 'react'
2 |
3 | const TOP_PADDING = 12
4 |
5 | const Inner = forwardRef(({ style, ...rest }, ref) => (
6 |
14 | ))
15 |
16 | export { TOP_PADDING }
17 |
18 | export default Inner
19 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealItem/Notes.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Text } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | notes: string
5 | }
6 |
7 | function Notes({ notes }: Props) {
8 | return (
9 |
10 |
11 | {notes}
12 |
13 |
14 | )
15 | }
16 |
17 | export default Notes
18 |
--------------------------------------------------------------------------------
/src/persistence/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useSaveValue } from './useSaveValue'
2 | export * from './file'
3 | export { default as useBlobUrl } from './useBlobUrl'
4 | export { default as DownloadButton } from './DownloadButton'
5 | export { default as getUntitledFileName } from './getUntitledFileName'
6 | export { default as useFileImportError } from './useImportFileError'
7 | export { default as fixWhiteSpace } from './fixWhiteSpace'
8 |
--------------------------------------------------------------------------------
/src/dom/isElementInViewport.ts:
--------------------------------------------------------------------------------
1 | function isElementInViewport(element: Element) {
2 | const { top, left, bottom, right } = element.getBoundingClientRect()
3 |
4 | return (
5 | top >= 0 &&
6 | left >= 0 &&
7 | bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
8 | right <= (window.innerWidth || document.documentElement.clientWidth)
9 | )
10 | }
11 |
12 | export default isElementInViewport
13 |
--------------------------------------------------------------------------------
/src/ingredients/index.ts:
--------------------------------------------------------------------------------
1 | export { default as IngredientsList } from './IngredientsList'
2 | export * from './ingredientForm'
3 | export { default as useIngredientsFormsActions } from './useIngredientsFormsActions'
4 | export * from './useIngredientsFormsActions'
5 | export * from './types'
6 | export { default as useGetIngredientFormStatsTree } from './useGetIngredientFormStatsTree'
7 | export { default as getIngredient } from './getIngredient'
8 |
--------------------------------------------------------------------------------
/src/foods-filters/persistence/loadFoodsFilter.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_FILTER, FoodsFilter } from 'foods-filters'
2 |
3 | function loadFoodsFilter() {
4 | const foodsFilterString = localStorage.getItem('foodsFilter')
5 |
6 | if (foodsFilterString) {
7 | const foodsFilter = JSON.parse(foodsFilterString) as FoodsFilter
8 | return foodsFilter
9 | }
10 |
11 | return DEFAULT_FILTER
12 | }
13 |
14 | export default loadFoodsFilter
15 |
--------------------------------------------------------------------------------
/src/general/minDelay.ts:
--------------------------------------------------------------------------------
1 | function minDelay(startDate: Date, minDelayInMs = 500) {
2 | return new Promise(resolve => {
3 | const endDate = new Date()
4 | const timeDiffInMs = endDate.getTime() - startDate.getTime()
5 |
6 | if (timeDiffInMs >= minDelayInMs) {
7 | resolve(true)
8 | } else {
9 | setTimeout(() => resolve(true), minDelayInMs - timeDiffInMs)
10 | }
11 | })
12 | }
13 |
14 | export default minDelay
15 |
--------------------------------------------------------------------------------
/src/variants/getVariantFormIndexAfterRemove.ts:
--------------------------------------------------------------------------------
1 | function getVariantFormIndexAfterRemove(
2 | selectedIndex: number,
3 | indexToRemove: number
4 | ) {
5 | if (indexToRemove < selectedIndex) {
6 | return selectedIndex - 1
7 | }
8 |
9 | if (indexToRemove === selectedIndex && indexToRemove > 0) {
10 | return indexToRemove - 1
11 | }
12 |
13 | return selectedIndex
14 | }
15 |
16 | export default getVariantFormIndexAfterRemove
17 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/Content/Exporter/worker/types.ts:
--------------------------------------------------------------------------------
1 | import { DietForm } from 'diets/dietForm'
2 | import { Food } from 'foods/types'
3 | import { Portion } from 'portions/types'
4 | import { StatsTree } from 'stats/calculations'
5 |
6 | type Params = {
7 | dietForm: DietForm
8 | foodsById: Record
9 | portionsById: Record
10 | dietFormStatsTree: StatsTree
11 | }
12 |
13 | export type { Params }
14 |
--------------------------------------------------------------------------------
/src/diets/persistence/loadLastOrDefaultDietForm.ts:
--------------------------------------------------------------------------------
1 | import { getDietForm } from 'diets'
2 |
3 | function loadLastOrDefaultDietForm() {
4 | const savedValue = localStorage.getItem('lastDietForm')
5 |
6 | if (savedValue) {
7 | try {
8 | return JSON.parse(savedValue)
9 | } catch (error) {
10 | return getDietForm()
11 | }
12 | }
13 |
14 | return getDietForm()
15 | }
16 |
17 | export default loadLastOrDefaultDietForm
18 |
--------------------------------------------------------------------------------
/src/foods/index.ts:
--------------------------------------------------------------------------------
1 | export * from './foodForm'
2 | export * from './useFoodsStore'
3 | export { default as FoodInfo } from './FoodInfo'
4 | export { default as FoodModal } from './FoodModal'
5 | export * from './FoodModal'
6 | export { default as FoodsList } from './FoodsList'
7 | export * from './FoodsList'
8 | export { default as FoodsDrawer } from './FoodsDrawer'
9 | export * from './types'
10 | export { default as builtInFoods } from './builtIn'
11 |
--------------------------------------------------------------------------------
/src/foods/types.ts:
--------------------------------------------------------------------------------
1 | import { NutritionData } from 'stats'
2 |
3 | type FoodId = string | number
4 |
5 | type FoodVolume = {
6 | portionId: string
7 | weightInGrams: number
8 | }
9 |
10 | type Food = {
11 | id: FoodId
12 | categoryId: number
13 | name: string
14 | addedByUser?: boolean
15 | servingSizeInGrams?: number
16 | volume?: FoodVolume
17 | url?: string
18 | } & NutritionData
19 |
20 | export type { Food, FoodId, FoodVolume }
21 |
--------------------------------------------------------------------------------
/src/form/duplicate.ts:
--------------------------------------------------------------------------------
1 | import { deepCopy } from 'general'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { Form } from './types'
4 |
5 | function duplicate(originalForm: T) {
6 | const copiedForm = deepCopy(originalForm, (key: string, value: any) => {
7 | if (key === 'fieldId') {
8 | return uuidv4()
9 | }
10 |
11 | return value
12 | }) as T
13 |
14 | return copiedForm
15 | }
16 |
17 | export default duplicate
18 |
--------------------------------------------------------------------------------
/src/stats/calculations/index.ts:
--------------------------------------------------------------------------------
1 | export { default as roundMacrosPercents } from './roundMacrosPercents'
2 | export { default as getMacrosPercents } from './getMacrosPercents'
3 | export * from './getMacrosPercents'
4 | export * from './getStatsTree'
5 | export { default as getStatsTree } from './getStatsTree'
6 | export * from './aggregateStats'
7 | export * from './getEnergiesEstimates'
8 | export { default as getDailyValuePercent } from './getDailyValuePercent'
9 |
--------------------------------------------------------------------------------
/src/stats/amountAsNumber.ts:
--------------------------------------------------------------------------------
1 | import numericQuantity from 'numeric-quantity'
2 |
3 | function amountAsNumber(amount: string) {
4 | if (amount === '') {
5 | return 0
6 | }
7 |
8 | const t = parseInt(amount, 10)
9 |
10 | if (Number.isNaN(t)) {
11 | return 0
12 | }
13 |
14 | const q = numericQuantity(amount)
15 |
16 | if (Number.isNaN(q)) {
17 | return t
18 | }
19 |
20 | return q
21 | }
22 |
23 | export default amountAsNumber
24 |
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/useGetDailyValuePercent.ts:
--------------------------------------------------------------------------------
1 | import { getDailyValuePercent, NutritionData } from 'stats'
2 | import useGetValue from './useGetValue'
3 |
4 | function useGetDailyValuePercent() {
5 | const getValue = useGetValue()
6 |
7 | function get(name: keyof NutritionData) {
8 | const value = getValue(name)
9 | return getDailyValuePercent(name, value)
10 | }
11 |
12 | return get
13 | }
14 |
15 | export default useGetDailyValuePercent
16 |
--------------------------------------------------------------------------------
/src/general/TooltipCommandLabel.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | command: string
5 | kbdCombo: string
6 | }
7 |
8 | function TooltipCommandLabel({ command, kbdCombo }: Props) {
9 | return (
10 |
11 | {command}{' '}
12 |
13 | {kbdCombo}
14 |
15 |
16 | )
17 | }
18 |
19 | export default TooltipCommandLabel
20 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalOverlay } from '@chakra-ui/react'
2 | import Content from './Content'
3 |
4 | type Props = {
5 | isOpen: boolean
6 | onClose: () => void
7 | }
8 |
9 | function ExportModal({ isOpen, onClose }: Props) {
10 | return (
11 |
12 |
13 |
14 |
15 | )
16 | }
17 |
18 | export default ExportModal
19 |
--------------------------------------------------------------------------------
/src/layout/Page/PageBody.tsx:
--------------------------------------------------------------------------------
1 | import { Flex } from '@chakra-ui/react'
2 | import { ReactNode } from 'react'
3 | import ElementContainer from './ElementContainer'
4 |
5 | type Props = {
6 | children: ReactNode
7 | }
8 |
9 | function PageBody({ children }: Props) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | export default PageBody
18 |
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/useGetValue.ts:
--------------------------------------------------------------------------------
1 | import { FoodForm } from 'foods'
2 | import { useFormContext } from 'react-hook-form'
3 | import { NutritionData } from 'stats'
4 |
5 | function useGetValue() {
6 | const { getValues } = useFormContext()
7 | const values = getValues()
8 |
9 | function get(name: keyof NutritionData) {
10 | const valueAsNumber = Number(values[name])
11 | return valueAsNumber
12 | }
13 |
14 | return get
15 | }
16 |
17 | export default useGetValue
18 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/form/useFormError.ts:
--------------------------------------------------------------------------------
1 | import { useFormContext } from 'react-hook-form'
2 |
3 | function useFormError(name: string) {
4 | const { formState } = useFormContext()
5 | const { errors, touchedFields } = formState
6 |
7 | const isInvalid =
8 | errors[name] && (touchedFields[name] || formState.isSubmitted)
9 |
10 | const errorMessage = isInvalid ? errors[name]?.message : undefined
11 |
12 | return {
13 | isInvalid,
14 | errorMessage,
15 | }
16 | }
17 |
18 | export default useFormError
19 |
--------------------------------------------------------------------------------
/src/dom/animateScrollLeft.ts:
--------------------------------------------------------------------------------
1 | import { animate } from 'framer-motion'
2 | import { RefObject } from 'react'
3 |
4 | function animateScrollLeft(nodeRef: RefObject, delta: number) {
5 | if (!nodeRef.current) {
6 | return
7 | }
8 |
9 | const node = nodeRef.current
10 |
11 | animate(node.scrollLeft, node.scrollLeft + delta, {
12 | duration: 0.2,
13 | onUpdate: value => {
14 | node.scrollLeft = value
15 | },
16 | })
17 | }
18 |
19 | export default animateScrollLeft
20 |
--------------------------------------------------------------------------------
/src/portions/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './usePortionsStore'
3 | export { default as PortionsMenuOrDrawer } from './PortionsMenuOrDrawer'
4 | export { default as formatAmount } from './formatAmount'
5 | export { default as PortionsSelect } from './PortionsSelect'
6 | export { default as useGetAmount } from './useGetAmount'
7 | export { default as getPortionDescription } from './getPortionDescription'
8 | export { default as getIngredientPortionDescription } from './getIngredientPortionDescription'
9 |
--------------------------------------------------------------------------------
/src/general/useElementHeight.ts:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react'
2 | import useResizeObserver from '@react-hook/resize-observer'
3 |
4 | function useElementHeight() {
5 | const elementRef = useRef(null)
6 | const [elementHeight, setElementHeight] = useState(0)
7 |
8 | useResizeObserver(elementRef, entry =>
9 | setElementHeight(entry.contentRect.height)
10 | )
11 |
12 | return {
13 | elementRef,
14 | elementHeight,
15 | }
16 | }
17 |
18 | export default useElementHeight
19 |
--------------------------------------------------------------------------------
/public/site.webmanifest:
--------------------------------------------------------------------------------
1 | {
2 | "name": "",
3 | "short_name": "",
4 | "icons": [
5 | {
6 | "src": "/android-chrome-192x192.png",
7 | "sizes": "192x192",
8 | "type": "image/png"
9 | },
10 | {
11 | "src": "/android-chrome-512x512.png",
12 | "sizes": "512x512",
13 | "type": "image/png"
14 | }
15 | ],
16 | "theme_color": "#319795",
17 | "background_color": "#319795",
18 | "display": "standalone"
19 | }
20 |
--------------------------------------------------------------------------------
/src/general/Loader.tsx:
--------------------------------------------------------------------------------
1 | import { Spinner, Text, Center, Flex } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | label: string
5 | }
6 |
7 | function Loader({ label }: Props) {
8 | return (
9 |
10 |
11 |
12 |
13 | {label}
14 |
15 |
16 |
17 | )
18 | }
19 |
20 | export default Loader
21 |
--------------------------------------------------------------------------------
/src/general/useRunIfNotUnmounted.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react'
2 |
3 | function useRunIfNotUnmounted() {
4 | const isUnmountedRef = useRef(false)
5 |
6 | useEffect(() => {
7 | return () => {
8 | isUnmountedRef.current = true
9 | }
10 | }, [])
11 |
12 | const callIfNotUnmounted = useCallback((callback: Function) => {
13 | if (!isUnmountedRef.current) {
14 | callback()
15 | }
16 | }, [])
17 |
18 | return callIfNotUnmounted
19 | }
20 |
21 | export default useRunIfNotUnmounted
22 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import ReactDOM from 'react-dom'
3 | import App from './App'
4 | import reportWebVitals from './reportWebVitals'
5 |
6 | ReactDOM.render(
7 |
8 |
9 | ,
10 | document.getElementById('root')
11 | )
12 |
13 | // If you want to start measuring performance in your app, pass a function
14 | // to log results (for example: reportWebVitals(console.log))
15 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
16 | reportWebVitals()
17 |
--------------------------------------------------------------------------------
/src/layout/RightAligned.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, FlexProps } from '@chakra-ui/react'
2 | import { ReactNode } from 'react'
3 |
4 | type Props = {
5 | children: ReactNode
6 | } & FlexProps
7 |
8 | function RightAligned({ children, ...rest }: Props) {
9 | return (
10 |
18 | {children}
19 |
20 | )
21 | }
22 |
23 | export default RightAligned
24 |
--------------------------------------------------------------------------------
/src/persistence/useBlobUrl.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 |
3 | type Params = {
4 | blob?: Blob
5 | }
6 |
7 | function useBlobUrl({ blob }: Params) {
8 | const [url, setUrl] = useState()
9 |
10 | useEffect(() => {
11 | if (blob) {
12 | const blobUrl = URL.createObjectURL(blob)
13 | setUrl(blobUrl)
14 |
15 | return () => {
16 | URL.revokeObjectURL(blobUrl)
17 | }
18 | }
19 | }, [blob])
20 |
21 | return {
22 | url,
23 | }
24 | }
25 |
26 | export default useBlobUrl
27 |
--------------------------------------------------------------------------------
/src/general/RightAligned.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, FlexProps } from '@chakra-ui/react'
2 | import { ReactNode } from 'react'
3 |
4 | type Props = {
5 | children: ReactNode
6 | } & FlexProps
7 |
8 | function RightAligned({ children, ...rest }: Props) {
9 | return (
10 |
18 | {children}
19 |
20 | )
21 | }
22 |
23 | export default RightAligned
24 |
--------------------------------------------------------------------------------
/src/variants/index.ts:
--------------------------------------------------------------------------------
1 | export { default as useVariantsFormsActions } from './useVariantsFormsActions'
2 | export * from './useVariantsFormsActions'
3 | export * from './variantForm'
4 | export { default as VariantsList } from './VariantsList'
5 | export { default as VariantsDetailsModal } from './VariantsDetailsModal'
6 | export { default as useGetVariantFormStatsTree } from './useGetVariantFormStatsTree'
7 | export { default as getVariantFormIndexAfterRemove } from './getVariantFormIndexAfterRemove'
8 | export { default as VariantStats } from './VariantStats'
9 |
--------------------------------------------------------------------------------
/src/layout/Page/index.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react'
2 | import { ReactNode } from 'react'
3 |
4 | type Props = {
5 | children: ReactNode
6 | }
7 |
8 | function Page({ children }: Props) {
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 | )
16 | }
17 |
18 | export { default as PageHeader } from './PageHeader'
19 | export { default as PageBody } from './PageBody'
20 | export { default as PageFooter } from './PageFooter'
21 |
22 | export default Page
23 |
--------------------------------------------------------------------------------
/src/theme/styles.ts:
--------------------------------------------------------------------------------
1 | const styles = {
2 | global: {
3 | body: {
4 | overflowX: 'hidden',
5 | fontFamily: 'Roboto',
6 | background: 'gray.50',
7 | },
8 | '.js-focus-visible :focus:not([data-focus-visible-added])': {
9 | outline: 'none',
10 | boxShadow: 'none',
11 | },
12 | '.rc-menu__item--hover': {
13 | backgroundColor: '#EDF2F7',
14 | },
15 | '.rc-menu__item--active': {
16 | color: 'black',
17 | backgroundColor: '#E2E8F0',
18 | },
19 | },
20 | }
21 |
22 | export default styles
23 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/IngredientItem/MenuOrDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { RightAligned } from 'layout'
2 | import { MenuOrDrawer as MenuOrDrawerBase } from 'general'
3 | import { ReactElement } from 'react'
4 |
5 | type Props = {
6 | children: ReactElement[]
7 | }
8 |
9 | function MenuOrDrawer({ children }: Props) {
10 | return (
11 |
12 |
13 | {children}
14 |
15 |
16 | )
17 | }
18 |
19 | export default MenuOrDrawer
20 |
--------------------------------------------------------------------------------
/src/notes/EditNotesModal/Content/Form/Header.tsx:
--------------------------------------------------------------------------------
1 | import { ModalHeader, Text } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | ownerName: string
5 | notes?: string
6 | }
7 |
8 | function Header({ ownerName, notes }: Props) {
9 | const titlePrefix = notes ? 'Edit notes of' : 'Add notes to'
10 |
11 | return (
12 |
13 | {titlePrefix}{' '}
14 |
15 | {ownerName}
16 |
17 |
18 | )
19 | }
20 |
21 | export default Header
22 |
--------------------------------------------------------------------------------
/src/foods/foodVolumeForm.ts:
--------------------------------------------------------------------------------
1 | import { FoodVolume } from 'foods'
2 |
3 | type FoodVolumeForm = {
4 | portionId: string
5 | weightInGrams: string
6 | }
7 |
8 | function getFoodVolumeForm(foodVolume?: FoodVolume): FoodVolumeForm {
9 | if (foodVolume) {
10 | return {
11 | portionId: foodVolume.portionId,
12 | weightInGrams: foodVolume.weightInGrams.toString(),
13 | }
14 | }
15 |
16 | return {
17 | portionId: 'milliliters',
18 | weightInGrams: '',
19 | }
20 | }
21 |
22 | export type { FoodVolumeForm }
23 |
24 | export { getFoodVolumeForm }
25 |
--------------------------------------------------------------------------------
/src/general/ScreenSizeProvider/context.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react'
2 |
3 | enum ScreenSize {
4 | Base = 0,
5 | Small,
6 | Medium,
7 | Large,
8 | ExtraLarge,
9 | }
10 |
11 | const ScreenSizeContext = createContext(undefined)
12 |
13 | function useScreenSize() {
14 | const screenSize = useContext(ScreenSizeContext)
15 |
16 | if (screenSize === undefined) {
17 | throw new Error('Provider missing')
18 | }
19 |
20 | return screenSize
21 | }
22 |
23 | export { useScreenSize, ScreenSizeContext, ScreenSize }
24 |
--------------------------------------------------------------------------------
/src/portions/getAmountFromPortionsToGrams.ts:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import { Portion } from 'portions'
3 | import getToGramsConversionFactor from './getToGramsConversionFactor'
4 |
5 | function getAmountFromPortionToGrams(
6 | amountInGrams: number,
7 | portionId: string,
8 | food: Food,
9 | portionsById: Record
10 | ) {
11 | const portion = portionsById[portionId]
12 | const factor = getToGramsConversionFactor(portion, food, portionsById)
13 | return amountInGrams * factor
14 | }
15 |
16 | export default getAmountFromPortionToGrams
17 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantItem/useScrollIntoView.ts:
--------------------------------------------------------------------------------
1 | import { RefObject, useEffect } from 'react'
2 |
3 | type Params = {
4 | isSelected: boolean
5 | ref: RefObject
6 | }
7 |
8 | function useScrollIntoView({ ref, isSelected }: Params) {
9 | useEffect(() => {
10 | setTimeout(() => {
11 | if (isSelected) {
12 | ref.current?.scrollIntoView({
13 | block: 'nearest',
14 | behavior: 'smooth',
15 | })
16 | }
17 | }, 200)
18 | }, [ref, isSelected])
19 | }
20 |
21 | export default useScrollIntoView
22 |
--------------------------------------------------------------------------------
/src/theme/index.ts:
--------------------------------------------------------------------------------
1 | import { extendTheme } from '@chakra-ui/react'
2 | import styles from './styles'
3 | import colors from './colors'
4 | import { Input, Button, Divider, Textarea, Alert } from './components'
5 |
6 | const theme = extendTheme({
7 | styles,
8 | colors,
9 | config: { initialColorMode: 'light', useSystemColorMode: false },
10 | components: {
11 | Input,
12 | Button,
13 | Divider,
14 | Textarea,
15 | Alert,
16 | },
17 | })
18 |
19 | export { default as getComputedColorFromChakra } from './getComputedColorFromChakra'
20 |
21 | export default theme
22 |
--------------------------------------------------------------------------------
/src/dom/useGetRefForId.ts:
--------------------------------------------------------------------------------
1 | import { createRef, RefObject, useCallback, useRef } from 'react'
2 |
3 | type RefsCache = {
4 | [id: number]: RefObject
5 | [id: string]: RefObject
6 | }
7 |
8 | function useGetRefForId() {
9 | const cacheRef = useRef>({})
10 |
11 | const getRef = useCallback((id: number | string) => {
12 | if (!cacheRef.current[id]) {
13 | cacheRef.current[id] = createRef()
14 | }
15 |
16 | return cacheRef.current[id]
17 | }, [])
18 |
19 | return getRef
20 | }
21 |
22 | export default useGetRefForId
23 |
--------------------------------------------------------------------------------
/src/variants/variantForm.ts:
--------------------------------------------------------------------------------
1 | import { MealForm } from 'meals'
2 | import { v4 as uuidv4 } from 'uuid'
3 |
4 | type VariantForm = {
5 | fieldId: string
6 | name: string
7 | mealsForms: MealForm[]
8 | }
9 |
10 | function getVariantForm(name: string): VariantForm {
11 | const fieldId = uuidv4()
12 |
13 | return {
14 | fieldId,
15 | name,
16 | mealsForms: [],
17 | }
18 | }
19 |
20 | function getInsertVariantFormAnimationKey(fieldId: string) {
21 | return `insert-variant-animmation-${fieldId}`
22 | }
23 |
24 | export type { VariantForm }
25 |
26 | export { getVariantForm, getInsertVariantFormAnimationKey }
27 |
--------------------------------------------------------------------------------
/src/form/names.ts:
--------------------------------------------------------------------------------
1 | import { Form } from './types'
2 |
3 | function getDuplicatedName(index: number, forms: Form[]) {
4 | const form = forms[index]
5 | const originalNameOrUntitled = form.name || 'Untitled'
6 | return getEnumeratedName(`Copy of ${originalNameOrUntitled}`, forms)
7 | }
8 |
9 | function getEnumeratedName(currentName: string, forms: Form[]) {
10 | const presentCount = forms.filter(({ name }) => name === currentName).length
11 |
12 | if (presentCount > 0) {
13 | return `${currentName} (${presentCount})`
14 | }
15 |
16 | return currentName
17 | }
18 |
19 | export { getDuplicatedName, getEnumeratedName }
20 |
--------------------------------------------------------------------------------
/src/layout/Page/ElementContainer.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps } from '@chakra-ui/react'
2 | import { ForwardedRef, ReactNode, forwardRef } from 'react'
3 |
4 | type Props = {
5 | children: ReactNode
6 | forwardedRef?: ForwardedRef
7 | } & BoxProps
8 |
9 | function ElementContainer({ children, forwardedRef, ...rest }: Props) {
10 | return (
11 |
12 | {children}
13 |
14 | )
15 | }
16 |
17 | export default forwardRef((props, ref) => (
18 |
19 | ))
20 |
--------------------------------------------------------------------------------
/src/portions/useGetToGramsConversionFactor.ts:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import { Portion, usePortions } from 'portions'
3 | import { useCallback } from 'react'
4 | import getToGramsConversionFactor from './getToGramsConversionFactor'
5 |
6 | function useGetToGramsConversionFactor() {
7 | const { portionsById } = usePortions()
8 |
9 | const getToGramsConversionFactorCallback = useCallback(
10 | (portion: Portion, food: Food) =>
11 | getToGramsConversionFactor(portion, food, portionsById),
12 | [portionsById]
13 | )
14 |
15 | return getToGramsConversionFactorCallback
16 | }
17 |
18 | export default useGetToGramsConversionFactor
19 |
--------------------------------------------------------------------------------
/src/theme/getComputedColorFromChakra.ts:
--------------------------------------------------------------------------------
1 | /* React-pdf does not have access to the Chakra context so we
2 | use the css variables to get the actual color values*/
3 |
4 | const map: Record = {
5 | 'gray.50': '#F7FAFC',
6 | 'teal.500': '#319795',
7 | 'teal.400': '#38B2AC',
8 | 'gray.400': '#A0AEC0',
9 | 'gray.600': '#4A5568',
10 | 'gray.500': '#718096',
11 | 'gray.100': '#EDF2F7',
12 | 'gray.200': '#E2E8F0',
13 | 'gray.300': '#CBD5E0',
14 | 'teal.600': '#2C7A7B',
15 | }
16 | function getComputedColorFromChakra(chakraColor: string) {
17 | return map[chakraColor]
18 | }
19 |
20 | export default getComputedColorFromChakra
21 |
--------------------------------------------------------------------------------
/src/general/Menu.tsx:
--------------------------------------------------------------------------------
1 | import styled from '@emotion/styled'
2 | import {
3 | Menu as MenuBase,
4 | MenuButton,
5 | MenuItem,
6 | ControlledMenu as ControlledMenuBase,
7 | MenuDivider,
8 | MenuHeader,
9 | } from '@szhsin/react-menu'
10 | import '@szhsin/react-menu/dist/index.css'
11 |
12 | const bowShadow =
13 | 'box-shadow: 0 3px 7px rgb(0 0 0 / 13%), 0 0.6px 2px rgb(0 0 0 / 10%) !important'
14 |
15 | const Menu = styled(MenuBase)`
16 | ${bowShadow}
17 | `
18 |
19 | const ControlledMenu = styled(ControlledMenuBase)`
20 | ${bowShadow}
21 | `
22 |
23 | export { MenuButton, MenuItem, ControlledMenu, MenuDivider, MenuHeader }
24 |
25 | export default Menu
26 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealItem/Header/MenuOrDrawer.tsx:
--------------------------------------------------------------------------------
1 | import { RightAligned } from 'layout'
2 | import { MenuOrDrawer as MenuOrDrawerBase } from 'general'
3 | import { ReactElement } from 'react'
4 |
5 | type Props = {
6 | children: ReactElement[]
7 | }
8 |
9 | function MenuOrDrawer({ children }: Props) {
10 | return (
11 |
12 |
19 | {children}
20 |
21 |
22 | )
23 | }
24 |
25 | export default MenuOrDrawer
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx",
22 | "baseUrl": "src"
23 | },
24 | "include": [
25 | "src"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------
/src/general/ResponsiveButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from '@chakra-ui/react'
2 | import { useScreenSize, ScreenSize } from 'general'
3 | import { ForwardedRef, forwardRef } from 'react'
4 |
5 | type Props = {
6 | forwardedRef?: ForwardedRef
7 | } & ButtonProps
8 |
9 | function ResponsiveButton({ forwardedRef, ...rest }: Props) {
10 | const screenSize = useScreenSize()
11 | const size = screenSize >= ScreenSize.Medium ? 'sm' : 'sm'
12 |
13 | return
14 | }
15 |
16 | export default forwardRef((props, ref) => (
17 |
18 | ))
19 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/EmptyList.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Text, Button } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | onAddIngredients: () => void
5 | }
6 |
7 | function EmptyList({ onAddIngredients }: Props) {
8 | return (
9 |
10 |
11 | You haven't added any foods
12 |
13 |
14 |
22 |
23 | )
24 | }
25 |
26 | export default EmptyList
27 |
--------------------------------------------------------------------------------
/src/ingredients/getIngredient.ts:
--------------------------------------------------------------------------------
1 | import { DEFAULT_SERVING_SIZE_IN_GRAMS, Food } from 'foods'
2 | import { Ingredient } from './types'
3 |
4 | function getIngredient(food: Food): Ingredient {
5 | const { volume } = food
6 |
7 | if (
8 | volume &&
9 | ['tablespoons', 'teaspoons', 'cups'].includes(volume.portionId)
10 | ) {
11 | const { portionId } = volume
12 |
13 | return {
14 | foodId: food.id,
15 | amount: 1,
16 | portionId,
17 | }
18 | }
19 |
20 | return {
21 | foodId: food.id,
22 | amount: food.servingSizeInGrams || DEFAULT_SERVING_SIZE_IN_GRAMS,
23 | portionId: 'grams',
24 | }
25 | }
26 |
27 | export default getIngredient
28 |
--------------------------------------------------------------------------------
/src/foods-filters/foodsFilter.ts:
--------------------------------------------------------------------------------
1 | const DEFAULT_FILTER: FoodsFilter = {
2 | query: '',
3 | onlyFoodsAddedByUser: false,
4 | categoryId: 0,
5 | }
6 |
7 | type FoodsFilter = {
8 | categoryId?: number
9 | onlyFoodsAddedByUser?: boolean
10 | query: string
11 | }
12 |
13 | function nonQueryChangesCount(filter: FoodsFilter) {
14 | let count = 0
15 | const { categoryId, onlyFoodsAddedByUser } = filter
16 |
17 | if (categoryId !== DEFAULT_FILTER.categoryId) {
18 | count++
19 | }
20 |
21 | if (onlyFoodsAddedByUser !== DEFAULT_FILTER.onlyFoodsAddedByUser) {
22 | count++
23 | }
24 |
25 | return count
26 | }
27 |
28 | export type { FoodsFilter }
29 |
30 | export { nonQueryChangesCount, DEFAULT_FILTER }
31 |
--------------------------------------------------------------------------------
/src/diets/persistence/hasMissingFoods.ts:
--------------------------------------------------------------------------------
1 | import { DietForm } from 'diets'
2 | import { Food, FoodId } from 'foods'
3 |
4 | function hasMissingFoods(dietForm: DietForm, foodsById: Record) {
5 | const { variantsForms } = dietForm
6 |
7 | for (const variantForm of variantsForms) {
8 | const { mealsForms } = variantForm
9 |
10 | for (const mealForm of mealsForms) {
11 | const { ingredientsForms } = mealForm
12 |
13 | for (const ingredientForm of ingredientsForms) {
14 | const food = foodsById[ingredientForm.foodId]
15 |
16 | if (!food) {
17 | return true
18 | }
19 | }
20 | }
21 | }
22 |
23 | return false
24 | }
25 |
26 | export default hasMissingFoods
27 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/Content/SelectedFoodsList/SelectedFoodItem.tsx:
--------------------------------------------------------------------------------
1 | import { Tag, TagLabel, TagCloseButton, Fade } from '@chakra-ui/react'
2 | import { Food } from 'foods'
3 |
4 | type Props = {
5 | food: Food
6 | onUnselect: (food: Food) => void
7 | }
8 |
9 | function SelectedFoodItem({ food, onUnselect }: Props) {
10 | return (
11 |
12 |
19 | {food.name}
20 | onUnselect(food)} />
21 |
22 |
23 | )
24 | }
25 |
26 | export default SelectedFoodItem
27 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton, ButtonProps } from '@chakra-ui/react'
2 | import { MoreHorizontal } from 'react-feather'
3 | import { ForwardedRef, forwardRef } from 'react'
4 |
5 | type Props = {
6 | forwardedRef?: ForwardedRef
7 | } & ButtonProps
8 |
9 | function Trigger({ forwardedRef, ...rest }: Props) {
10 | return (
11 | }
14 | variant="ghost"
15 | size="sm"
16 | ref={forwardedRef}
17 | {...rest}
18 | />
19 | )
20 | }
21 | export default forwardRef((props, ref) => (
22 |
23 | ))
24 |
--------------------------------------------------------------------------------
/src/undoRedo/appLocation.ts:
--------------------------------------------------------------------------------
1 | import { DietForm } from 'diets'
2 | import { RefObject } from 'react'
3 |
4 | type AppLocation = {
5 | scrollTop: number
6 | scrollLeft: number
7 | variantIndex: number
8 | }
9 |
10 | type Params = {
11 | horizontalScrollRef: RefObject
12 | dietForm: DietForm
13 | }
14 |
15 | function getAppLocation({
16 | horizontalScrollRef,
17 | dietForm,
18 | }: Params): AppLocation {
19 | return {
20 | scrollTop: window.scrollY,
21 | scrollLeft: horizontalScrollRef.current
22 | ? horizontalScrollRef.current.scrollLeft
23 | : 0,
24 | variantIndex: dietForm.selectedVariantFormIndex,
25 | }
26 | }
27 |
28 | export type { AppLocation }
29 |
30 | export default getAppLocation
31 |
--------------------------------------------------------------------------------
/src/stats/StatValueDetail.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Text } from '@chakra-ui/react'
2 | import { Tooltip } from 'general'
3 | import { ReactNode } from 'react'
4 |
5 | type Props = {
6 | label: string
7 | tooltipLabel?: string
8 | leftIcon?: ReactNode
9 | isLarge?: boolean
10 | }
11 |
12 | function StatValueDetail({
13 | label,
14 | tooltipLabel,
15 | leftIcon,
16 | isLarge = false,
17 | }: Props) {
18 | return (
19 |
20 | {leftIcon}
21 |
22 |
23 |
24 | {label}
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default StatValueDetail
32 |
--------------------------------------------------------------------------------
/src/notes/EditNotesModal/Content/NotesFormProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FormProvider, useForm } from 'react-hook-form'
2 | import { yupResolver } from '@hookform/resolvers/yup'
3 | import { ReactNode } from 'react'
4 | import { getNotesForm, NotesForm, notesFormSchema } from 'notes'
5 |
6 | type Props = {
7 | children: ReactNode
8 | notes?: string
9 | }
10 |
11 | function NotesFormProvider({ children, notes }: Props) {
12 | const defaultValues = getNotesForm(notes)
13 |
14 | const formMethods = useForm({
15 | defaultValues,
16 | mode: 'onChange',
17 |
18 | resolver: yupResolver(notesFormSchema),
19 | })
20 |
21 | return {children}
22 | }
23 |
24 | export default NotesFormProvider
25 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/Form/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { ModalFooter, Button, HStack } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | onClose: () => void
5 | onSubmit: () => void
6 | isEditing: boolean
7 | }
8 |
9 | function Footer({ onClose, onSubmit, isEditing }: Props) {
10 | return (
11 |
12 |
13 |
14 | {isEditing && (
15 |
23 | )}
24 |
25 |
26 | )
27 | }
28 |
29 | export default Footer
30 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/Form/Header.tsx:
--------------------------------------------------------------------------------
1 | import { ModalHeader, Button } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | onClose: () => void
5 | title: string
6 | canEdit: boolean
7 | onToggleEdit: () => void
8 | isEditing: boolean
9 | }
10 |
11 | function Header({ title, canEdit, isEditing, onToggleEdit }: Props) {
12 | return (
13 |
14 | {title}
15 | {canEdit && (
16 |
25 | )}
26 |
27 | )
28 | }
29 |
30 | export default Header
31 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/Content/Exporter/worker/worker.tsx:
--------------------------------------------------------------------------------
1 | import { pdf } from '@react-pdf/renderer'
2 |
3 | if (process.env.NODE_ENV !== 'production') {
4 | const t: any = global
5 | t.$RefreshReg$ = () => {}
6 | t.$RefreshSig$ = () => () => {}
7 | }
8 |
9 | async function getDietPdfBlob(data: any) {
10 | const PdfDietEditor = require('diets/PdfDietEditor').default
11 |
12 | const document = (
13 |
20 | )
21 |
22 | return pdf(document).toBlob()
23 | }
24 |
25 | export { getDietPdfBlob }
26 |
--------------------------------------------------------------------------------
/src/general/ResponsiveIconButton.tsx:
--------------------------------------------------------------------------------
1 | import { IconButtonProps, IconButton } from '@chakra-ui/react'
2 | import { useScreenSize, ScreenSize } from 'general'
3 | import { ForwardedRef, forwardRef } from 'react'
4 |
5 | type Props = {
6 | forwardedRef?: ForwardedRef
7 | tooltip?: string
8 | } & IconButtonProps
9 |
10 | function ResponsiveIconButton({
11 | forwardedRef,
12 |
13 | ...rest
14 | }: Props) {
15 | const screenSize = useScreenSize()
16 | const size = screenSize >= ScreenSize.Medium ? 'sm' : 'sm'
17 |
18 | return
19 | }
20 |
21 | export default forwardRef((props, ref) => (
22 |
23 | ))
24 |
--------------------------------------------------------------------------------
/src/persistence/useSaveValue.ts:
--------------------------------------------------------------------------------
1 | import { useEffect } from 'react'
2 |
3 | const TIMEOUT = 150
4 |
5 | type Params = {
6 | value: any
7 | key: string
8 | isEnabled?: boolean
9 | }
10 |
11 | function useSaveValue({ value, key, isEnabled = true }: Params) {
12 | useEffect(() => {
13 | if (isEnabled) {
14 | const timeoutId = window.setTimeout(() => {
15 | try {
16 | const valueString = JSON.stringify(value)
17 | localStorage.setItem(key, valueString)
18 | } catch (error) {
19 | // Do nothing
20 | }
21 | }, TIMEOUT)
22 |
23 | return () => {
24 | window.clearTimeout(timeoutId)
25 | }
26 | }
27 | }, [value, key, isEnabled])
28 | }
29 |
30 | export default useSaveValue
31 |
--------------------------------------------------------------------------------
/src/foods/FoodsList/VirtualizedList/FoodItem/DisappearingBox.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react'
2 | import { useEffect, useState } from 'react'
3 |
4 | type Props = {
5 | shouldAnimate: boolean
6 | }
7 |
8 | function DisappearingBox({ shouldAnimate }: Props) {
9 | const [opacity, setOpacity] = useState(shouldAnimate ? 1 : 0)
10 |
11 | useEffect(() => {
12 | if (shouldAnimate) {
13 | setOpacity(0)
14 | }
15 | }, [shouldAnimate])
16 |
17 | return (
18 |
28 | )
29 | }
30 |
31 | export default DisappearingBox
32 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/useScrollState.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | function useScrollState() {
4 | const [showsScrollButtons, setShowsScrollButtons] = useState(false)
5 | const [canScrollLeft, setCanScrollLeft] = useState(false)
6 | const [canScrollRight, setCanScrollRight] = useState(false)
7 |
8 | function onScrollStateChange(
9 | isScrollable: boolean,
10 | canScrollLeft: boolean,
11 | canScrollRight: boolean
12 | ) {
13 | setCanScrollLeft(canScrollLeft)
14 | setCanScrollRight(canScrollRight)
15 | setShowsScrollButtons(isScrollable)
16 | }
17 |
18 | return {
19 | showsScrollButtons,
20 | canScrollLeft,
21 | canScrollRight,
22 | onScrollStateChange,
23 | }
24 | }
25 |
26 | export default useScrollState
27 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenSize, ScreenSize } from 'general'
2 | import Modal from './Modal'
3 | import Trigger from './Trigger'
4 | import Popover from './Popover'
5 | import { useDisclosure } from '@chakra-ui/hooks'
6 |
7 | function FoodsFilterPopoverOrModal() {
8 | const screenSize = useScreenSize()
9 | const modalDisclosure = useDisclosure()
10 |
11 | if (screenSize < ScreenSize.Medium) {
12 | return (
13 | <>
14 |
15 |
19 | >
20 | )
21 | }
22 |
23 | return
24 | }
25 |
26 | export default FoodsFilterPopoverOrModal
27 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealsControls.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, chakra, Button } from '@chakra-ui/react'
2 | import { MealForm } from 'meals'
3 | import { Plus } from 'react-feather'
4 |
5 | const PlusStyled = chakra(Plus)
6 |
7 | type Props = {
8 | mealsForms: MealForm[]
9 | onAddMeal: () => void
10 | }
11 |
12 | function MealsControls({ mealsForms, onAddMeal }: Props) {
13 | return (
14 |
15 | }
21 | >
22 | Add meal
23 |
24 |
25 | )
26 | }
27 |
28 | export default MealsControls
29 |
--------------------------------------------------------------------------------
/src/layout/Page/PageFooter.tsx:
--------------------------------------------------------------------------------
1 | import { Box } from '@chakra-ui/react'
2 | import { ReactNode, RefObject } from 'react'
3 | import ElementContainer from './ElementContainer'
4 |
5 | type Props = {
6 | children: ReactNode
7 | footerContainerRef?: RefObject
8 | }
9 |
10 | function PageFooter({ children, footerContainerRef }: Props) {
11 | return (
12 |
21 |
22 |
23 | {children}
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default PageFooter
31 |
--------------------------------------------------------------------------------
/src/variants/VariantsDetailsModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalOverlay } from '@chakra-ui/react'
2 | import { VariantForm } from 'variants'
3 | import Content from './Content'
4 |
5 | type Props = {
6 | onClose: () => void
7 | isOpen: boolean
8 | initialVariantForm: VariantForm
9 | }
10 |
11 | function VariantsDetailsModal({ onClose, isOpen, initialVariantForm }: Props) {
12 | return (
13 |
20 |
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export type { Props }
28 |
29 | export default VariantsDetailsModal
30 |
--------------------------------------------------------------------------------
/src/stats/useUpdateMealStats.ts:
--------------------------------------------------------------------------------
1 | import { Stats } from './types'
2 | import { useEffect } from 'react'
3 | import { useMealsStatsActions } from './useMealsStatsStore'
4 |
5 | type Params = {
6 | stats: Stats
7 | selectedVariantFormFieldId: string
8 | index: number
9 | }
10 |
11 | function useUpdateMealStats({
12 | stats,
13 | selectedVariantFormFieldId,
14 | index,
15 | }: Params) {
16 | const mealsStatsActions = useMealsStatsActions()
17 |
18 | useEffect(() => {
19 | mealsStatsActions.setMealStats(selectedVariantFormFieldId, index, stats)
20 |
21 | return () => {
22 | mealsStatsActions.deleteMealStats(selectedVariantFormFieldId, index)
23 | }
24 | }, [stats, mealsStatsActions, index, selectedVariantFormFieldId])
25 | }
26 |
27 | export default useUpdateMealStats
28 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantNameModal/useSubmitVariantNameForm.ts:
--------------------------------------------------------------------------------
1 | import { useFormContext } from 'react-hook-form'
2 | import { VariantNameForm } from './variantNameForm'
3 | import { useDietFormActions } from 'diets'
4 |
5 | type Params = {
6 | onComplete: (variantNameForm: VariantNameForm) => void
7 | variantFormIndex: number
8 | }
9 |
10 | function useSubmitVariantNameForm({ onComplete, variantFormIndex }: Params) {
11 | const dietFormActions = useDietFormActions()
12 | const { handleSubmit } = useFormContext()
13 |
14 | const onSubmit = handleSubmit((form: VariantNameForm) => {
15 | onComplete(form)
16 |
17 | dietFormActions.updateVariantForm(variantFormIndex, { name: form.name })
18 | })
19 |
20 | return onSubmit
21 | }
22 |
23 | export default useSubmitVariantNameForm
24 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/Menu/index.tsx:
--------------------------------------------------------------------------------
1 | import { ButtonProps } from '@chakra-ui/react'
2 | import { Menu as MenuBase, MenuHeader } from 'general'
3 | import Trigger from '../Trigger'
4 | import { ReactElement } from 'react'
5 | import getMenuItems from './getMenuItems'
6 |
7 | type Props = {
8 | children: ReactElement | ReactElement[]
9 | title: string
10 | } & ButtonProps
11 |
12 | function Menu({ children, title, ...rest }: Props) {
13 | return (
14 | }
20 | >
21 | {title}
22 | {getMenuItems(children)}
23 |
24 | )
25 | }
26 |
27 | export { getMenuItems }
28 |
29 | export default Menu
30 |
--------------------------------------------------------------------------------
/src/stats/calculations/getStatsTree.ts:
--------------------------------------------------------------------------------
1 | import { sumStats, avgStats } from './aggregateStats'
2 | import { Stats } from '../types'
3 |
4 | type StatsTree = {
5 | id: string
6 | stats: Stats
7 | avg?: Stats
8 | subtrees: StatsTree[]
9 | }
10 |
11 | type Params = {
12 | id: string
13 | subtrees: StatsTree[]
14 | calculateAvg?: boolean
15 | }
16 |
17 | function getStatsTree({ id, subtrees, calculateAvg = false }: Params) {
18 | const subtreesStats = subtrees.map(({ stats }) => stats)
19 |
20 | const result: StatsTree = {
21 | id,
22 | stats: sumStats(subtreesStats),
23 | subtrees,
24 | }
25 |
26 | if (calculateAvg) {
27 | result.avg = avgStats(subtreesStats)
28 | }
29 |
30 | return result
31 | }
32 |
33 | export type { StatsTree }
34 |
35 | export default getStatsTree
36 |
--------------------------------------------------------------------------------
/src/meals/useGetMealFormStatsTree.ts:
--------------------------------------------------------------------------------
1 | import { getStatsTree, StatsTree } from 'stats'
2 | import { useGetIngredientFormStatsTree } from 'ingredients'
3 | import { MealForm } from 'meals'
4 | import { useCallback } from 'react'
5 |
6 | function useGetMealFormStatsTree() {
7 | const getIngredientFormStatsTree = useGetIngredientFormStatsTree()
8 |
9 | const getMealFormStatsTree = useCallback(
10 | (mealForm: MealForm): StatsTree => {
11 | return getStatsTree({
12 | id: mealForm.fieldId,
13 | subtrees: mealForm.ingredientsForms.map(ingredientForm =>
14 | getIngredientFormStatsTree(ingredientForm)
15 | ),
16 | })
17 | },
18 | [getIngredientFormStatsTree]
19 | )
20 |
21 | return getMealFormStatsTree
22 | }
23 |
24 | export default useGetMealFormStatsTree
25 |
--------------------------------------------------------------------------------
/src/diets/dietForm.ts:
--------------------------------------------------------------------------------
1 | import { Diet } from 'diets'
2 | import { getVariantForm, VariantForm } from 'variants'
3 | import { v4 as uuidv4 } from 'uuid'
4 |
5 | type DietForm = {
6 | fieldId: string
7 | name: string
8 | selectedVariantFormIndex: number
9 | variantsForms: VariantForm[]
10 | }
11 |
12 | function getDietForm(diet?: Diet): DietForm {
13 | const variantsForms = [getVariantForm('Day 1')]
14 | const fieldId = uuidv4()
15 |
16 | if (diet) {
17 | return {
18 | fieldId,
19 | name: diet.name,
20 | variantsForms,
21 | selectedVariantFormIndex: 0,
22 | }
23 | }
24 |
25 | return {
26 | fieldId,
27 | name: 'Untitled',
28 | variantsForms,
29 | selectedVariantFormIndex: 0,
30 | }
31 | }
32 |
33 | export type { DietForm }
34 |
35 | export { getDietForm }
36 |
--------------------------------------------------------------------------------
/src/variants/VariantStats/EnergyStat.tsx:
--------------------------------------------------------------------------------
1 | import { BoxProps } from '@chakra-ui/layout'
2 | import VariantStat from './VariantStat'
3 |
4 | type Props = {
5 | energy: number
6 | energyDiff: number
7 | hasAtLeastOneMeal: boolean
8 | } & BoxProps
9 |
10 | function getEnergyDiffDetail(energyDiff: number) {
11 | const sign = energyDiff > 0 ? '+' : '-'
12 | return `${sign}${Math.abs(energyDiff)}kcal`
13 | }
14 |
15 | function EnergyStat({ energy, energyDiff, hasAtLeastOneMeal, ...rest }: Props) {
16 | return (
17 |
25 | )
26 | }
27 |
28 | export default EnergyStat
29 |
--------------------------------------------------------------------------------
/src/variants/useGetVariantFormStatsTree.ts:
--------------------------------------------------------------------------------
1 | import { StatsTree, getStatsTree } from 'stats'
2 | import { VariantForm } from './variantForm'
3 | import { useGetMealFormStatsTree } from 'meals'
4 | import { useCallback } from 'react'
5 |
6 | function useGetVariantFormStatsTree() {
7 | const getMealFormStatsTree = useGetMealFormStatsTree()
8 |
9 | const getVariantFormStatsTree = useCallback(
10 | (variantForm: VariantForm): StatsTree => {
11 | const subtrees = variantForm.mealsForms.map(mealForm =>
12 | getMealFormStatsTree(mealForm)
13 | )
14 |
15 | return getStatsTree({
16 | id: variantForm.fieldId,
17 | subtrees,
18 | })
19 | },
20 | [getMealFormStatsTree]
21 | )
22 |
23 | return getVariantFormStatsTree
24 | }
25 |
26 | export default useGetVariantFormStatsTree
27 |
--------------------------------------------------------------------------------
/src/general/ScreenSizeProvider/index.tsx:
--------------------------------------------------------------------------------
1 | import { useBreakpointValue } from '@chakra-ui/media-query'
2 | import { ReactNode } from 'react'
3 | import { ScreenSizeContext, ScreenSize } from './context'
4 |
5 | type Props = {
6 | children: ReactNode
7 | }
8 |
9 | function ScreenSizeProvider({ children }: Props) {
10 | const screenSize = useBreakpointValue({
11 | base: ScreenSize.Base,
12 | sm: ScreenSize.Small,
13 | md: ScreenSize.Medium,
14 | lg: ScreenSize.Large,
15 | xl: ScreenSize.ExtraLarge,
16 | })
17 |
18 | if (screenSize === undefined) {
19 | return null
20 | }
21 |
22 | return (
23 |
24 | {children}
25 |
26 | )
27 | }
28 |
29 | export * from './context'
30 |
31 | export default ScreenSizeProvider
32 |
--------------------------------------------------------------------------------
/src/stats/index.ts:
--------------------------------------------------------------------------------
1 | export * from './types'
2 | export * from './useMealsStatsStore'
3 | export { default as useUpdateMealStats } from './useUpdateMealStats'
4 | export { default as useVariantStats } from './useVariantStats'
5 | export * from './calculations'
6 | export { default as Stat } from './Stat'
7 | export { default as StatsLayout } from './StatsLayout'
8 | export { default as objectFromNutritionDataKeys } from './objectFromNutritionDataKeys'
9 | export * from './objectFromNutritionDataKeys'
10 | export { default as StatsFormFields } from './StatsFormFields'
11 | export * from './StatsFormFields'
12 | export { default as AmountInput } from './AmountInput'
13 | export { default as EnergyStat } from './EnergyStat'
14 | export { default as getUnit } from './getUnit'
15 | export { default as StatValueDetail } from './StatValueDetail'
16 |
--------------------------------------------------------------------------------
/src/foods/FoodsList/VirtualizedList/FoodItem/AnimateAppear.tsx:
--------------------------------------------------------------------------------
1 | import { motion } from 'framer-motion'
2 | import { ReactNode } from 'react'
3 |
4 | type Props = {
5 | shouldAnimate: boolean
6 | children: ReactNode
7 | }
8 |
9 | const variants = {
10 | open: {
11 | opacity: 1,
12 | scale: 1,
13 | },
14 | hidden: { opacity: 0, scale: 0.9 },
15 | }
16 |
17 | function AnimateAppear({ shouldAnimate, children }: Props) {
18 | return (
19 |
29 | {children}
30 |
31 | )
32 | }
33 |
34 | export default AnimateAppear
35 |
--------------------------------------------------------------------------------
/src/foods-categories/FoodCategoriesSelect.tsx:
--------------------------------------------------------------------------------
1 | import { SelectProps, Select } from '@chakra-ui/select'
2 | import foodCategories from './categories.json'
3 | import { ForwardedRef, forwardRef } from 'react'
4 |
5 | type Props = { forwardedRef?: ForwardedRef } & SelectProps
6 |
7 | function FoodCategoriesSelect({ children, forwardedRef, ...rest }: Props) {
8 | return (
9 |
17 | )
18 | }
19 |
20 | export default forwardRef((props, ref) => (
21 |
22 | ))
23 |
--------------------------------------------------------------------------------
/src/diets/useGetDietFormStatsTree.ts:
--------------------------------------------------------------------------------
1 | import { StatsTree, getStatsTree } from 'stats'
2 | import { DietForm } from './dietForm'
3 | import { useGetVariantFormStatsTree } from 'variants'
4 | import { useCallback } from 'react'
5 |
6 | function useGetDietFormStatsTree() {
7 | const getVariantFormStatsTree = useGetVariantFormStatsTree()
8 |
9 | const getDietFormStatsTree2 = useCallback(
10 | (dietForm: DietForm): StatsTree => {
11 | const subtrees = dietForm.variantsForms.map(variantForm =>
12 | getVariantFormStatsTree(variantForm)
13 | )
14 |
15 | return getStatsTree({
16 | id: dietForm.fieldId,
17 | subtrees,
18 | calculateAvg: true,
19 | })
20 | },
21 | [getVariantFormStatsTree]
22 | )
23 |
24 | return getDietFormStatsTree2
25 | }
26 |
27 | export default useGetDietFormStatsTree
28 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/IngredientItem/MissingStatsLayout.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Text, Button } from '@chakra-ui/react'
2 | import { Trash2 } from 'react-feather'
3 |
4 | type Props = {
5 | onRemoveRequest: () => void
6 | }
7 |
8 | function MissingStatsLayout({ onRemoveRequest }: Props) {
9 | return (
10 |
11 |
12 | Food not found
13 |
14 |
15 | }
23 | >
24 | Remove
25 |
26 |
27 | )
28 | }
29 |
30 | export default MissingStatsLayout
31 |
--------------------------------------------------------------------------------
/src/portions/getToGramsConversionFactor.ts:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import { Portion } from 'portions'
3 |
4 | function getToGramsConversionFactor(
5 | portion: Portion,
6 | food: Food,
7 | portionsById: Record
8 | ): number {
9 | const { gramsPerAmount, millilitersPerAmount } = portion
10 |
11 | if (gramsPerAmount) {
12 | return gramsPerAmount
13 | }
14 |
15 | if (millilitersPerAmount && food.volume) {
16 | const { portionId, weightInGrams } = food.volume
17 | const portion = portionsById[portionId]
18 |
19 | if (portion.millilitersPerAmount) {
20 | const gramsPerMilliliter = weightInGrams / portion.millilitersPerAmount
21 |
22 | return millilitersPerAmount * gramsPerMilliliter
23 | }
24 | }
25 |
26 | throw new Error()
27 | }
28 |
29 | export default getToGramsConversionFactor
30 |
--------------------------------------------------------------------------------
/src/layout/Page/PageHeader.tsx:
--------------------------------------------------------------------------------
1 | import { Box, Flex } from '@chakra-ui/react'
2 | import { ForwardedRef, forwardRef, ReactNode } from 'react'
3 | import ElementContainer from './ElementContainer'
4 |
5 | type Props = {
6 | children: ReactNode
7 | forwardedRef?: ForwardedRef
8 | }
9 |
10 | function PageHeader({ children, forwardedRef }: Props) {
11 | return (
12 |
22 |
23 | {children}
24 |
25 |
26 | )
27 | }
28 |
29 | export default forwardRef((props, ref) => (
30 |
31 | ))
32 |
--------------------------------------------------------------------------------
/src/foods-categories/categories.json:
--------------------------------------------------------------------------------
1 | [
2 | { "name": "Poultry", "id": 100 },
3 | { "name": "Beef", "id": 200 },
4 | { "name": "Pork", "id": 300 },
5 | { "name": "Finfish & Shellfish", "id": 400 },
6 | { "name": "Dairy and eggs", "id": 500},
7 | { "name": "Grains & Pasta", "id": 600},
8 | { "name": "Vegetables", "id": 700 },
9 | { "name": "Legumes & Legume Products", "id": 800 },
10 | { "name": "Fruits & Juices", "id": 900 },
11 | { "name": "Nut and Seed Products", "id": 1000 },
12 | { "name": "Fats & Oils", "id": 2000 },
13 | { "name": "Baked Products", "id": 3000 },
14 | { "name": "Sauces & Soups", "id": 4000 },
15 | { "name": "Spices & Herbs", "id": 5000 },
16 | { "name": "Sweets & Snacks", "id": 6000 },
17 | { "name": "Beverages", "id": 7000 },
18 | { "name": "Other", "id": 8000 }
19 | ]
20 |
21 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/Form/useVariantFormActions.ts:
--------------------------------------------------------------------------------
1 | import { VariantForm } from 'variants'
2 | import { useCallback } from 'react'
3 | import { ScrollManager } from 'diets/useScrollManager'
4 |
5 | type Params = {
6 | scrollManager: ScrollManager
7 | }
8 |
9 | function useVariantFormEvents({ scrollManager }: Params) {
10 | const { setScrollState, getCachedScrollTop } = scrollManager
11 |
12 | const onVariantFormSelect = useCallback(
13 | (variantForm: VariantForm) => {
14 | setScrollState({ top: getCachedScrollTop(variantForm.fieldId) })
15 | },
16 | [setScrollState, getCachedScrollTop]
17 | )
18 |
19 | const onVariantFormCopy = useCallback(() => {
20 | setScrollState({ top: 0 })
21 | }, [setScrollState])
22 |
23 | return {
24 | onVariantFormCopy,
25 | onVariantFormSelect,
26 | }
27 | }
28 |
29 | export default useVariantFormEvents
30 |
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/ReavealButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps, chakra } from '@chakra-ui/react'
2 | import { ChevronDown, ChevronUp } from 'react-feather'
3 |
4 | const ChevronDownStyled = chakra(ChevronDown)
5 | const ChevronUpStyled = chakra(ChevronUp)
6 |
7 | type Props = {
8 | isContentShown: boolean
9 | showContentLabel: string
10 | hideContentLabel: string
11 | } & ButtonProps
12 |
13 | function RevealButton({
14 | isContentShown,
15 | showContentLabel,
16 | hideContentLabel,
17 | ...rest
18 | }: Props) {
19 | return (
20 |
28 | )
29 | }
30 |
31 | export default RevealButton
32 |
--------------------------------------------------------------------------------
/src/ingredients/ingredientForm.ts:
--------------------------------------------------------------------------------
1 | import { Ingredient } from './types'
2 | import { v4 as uuidv4 } from 'uuid'
3 | import { FoodId } from 'foods'
4 | import { formatAmount } from 'portions'
5 |
6 | type IngredientForm = {
7 | fieldId: string
8 | foodId: FoodId
9 | amount: string
10 | notes?: string
11 | portionId: string
12 | }
13 |
14 | function getIngredientForm(ingredient: Ingredient): IngredientForm {
15 | const fieldId = uuidv4()
16 |
17 | return {
18 | fieldId,
19 | foodId: ingredient.foodId,
20 | amount: formatAmount(ingredient.amount, ingredient.portionId),
21 | portionId: ingredient.portionId,
22 | }
23 | }
24 |
25 | function getInsertIngredientFormAnimationKey(fieldId: string) {
26 | return `insert-ingredient-animation-${fieldId}`
27 | }
28 |
29 | export type { IngredientForm }
30 |
31 | export { getIngredientForm, getInsertIngredientFormAnimationKey }
32 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/IngredientItem/Notes.tsx:
--------------------------------------------------------------------------------
1 | import PresenceAnimation from './PresenceAnimation'
2 | import { Box, Text } from '@chakra-ui/react'
3 | import { NotesEvents } from './useNotesEvents'
4 | import { IngredientForm } from 'ingredients'
5 |
6 | type Props = {
7 | notesEvents: NotesEvents
8 | ingredientForm: IngredientForm
9 | }
10 |
11 | function Notes({ notesEvents, ingredientForm }: Props) {
12 | return (
13 |
18 |
19 |
20 | {ingredientForm.notes}
21 |
22 |
23 |
24 | )
25 | }
26 |
27 | export default Notes
28 |
--------------------------------------------------------------------------------
/src/stats/calculations/aggregateStats.ts:
--------------------------------------------------------------------------------
1 | import { objectFromNutritionDataKeys, NUTRITION_STATS_KEYS } from 'stats'
2 | import { Stats } from '../types'
3 |
4 | function sumStats(stats: Stats[]): Stats {
5 | const result: Stats = {
6 | amountInGrams: 0,
7 | ...objectFromNutritionDataKeys(key => 0),
8 | }
9 |
10 | for (const stat of stats) {
11 | result.amountInGrams += stat.amountInGrams
12 |
13 | for (const key of NUTRITION_STATS_KEYS) {
14 | result[key] += stat[key]
15 | }
16 | }
17 |
18 | return result
19 | }
20 |
21 | function avgStats(
22 | stats: Stats[],
23 | round: (x: number) => number = Math.round
24 | ): Stats {
25 | const result = sumStats(stats)
26 | let key: keyof typeof result
27 |
28 | for (key in result) {
29 | result[key] = round(result[key] / stats.length)
30 | }
31 |
32 | return result
33 | }
34 |
35 | export { sumStats, avgStats }
36 |
--------------------------------------------------------------------------------
/src/persistence/DownloadButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, ButtonProps } from '@chakra-ui/react'
2 | import { useBlobUrl } from 'persistence'
3 | import prettyBytes from 'pretty-bytes'
4 |
5 | type Props = {
6 | blob?: Blob
7 | onClose: () => void
8 | fileName: string
9 | label: string
10 | } & ButtonProps
11 |
12 | function DownloadButton({ blob, onClose, fileName, label, ...rest }: Props) {
13 | const { url } = useBlobUrl({ blob })
14 |
15 | return (
16 |
31 | )
32 | }
33 |
34 | export default DownloadButton
35 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/Menu/getMenuItems.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | MenuItem,
3 | MenuOrDrawerItem,
4 | MenuOrDrawerSeparator,
5 | MenuDivider,
6 | } from 'general'
7 | import { cloneElement, ReactElement, Children } from 'react'
8 |
9 | function getMenuItems(children: ReactElement | ReactElement[]) {
10 | return Children.map(children, (child: ReactElement) => {
11 | if (child.type === MenuOrDrawerItem) {
12 | const icon = cloneElement(child.props.icon, { size: 16, mr: 3 })
13 |
14 | return (
15 |
22 | )
23 | } else if (child.type === MenuOrDrawerSeparator) {
24 | return
25 | }
26 |
27 | return null
28 | })
29 | }
30 |
31 | export default getMenuItems
32 |
--------------------------------------------------------------------------------
/src/portions/PortionsSelect.tsx:
--------------------------------------------------------------------------------
1 | import { SelectProps, Select } from '@chakra-ui/select'
2 | import { Portion } from 'portions'
3 | import { ForwardedRef, forwardRef } from 'react'
4 |
5 | type Props = {
6 | forwardedRef?: ForwardedRef
7 | portions: Portion[]
8 | } & SelectProps
9 |
10 | function PortionsSelect({ children, forwardedRef, portions, ...rest }: Props) {
11 | return (
12 |
21 | )
22 | }
23 |
24 | export default forwardRef((props, ref) => (
25 |
26 | ))
27 |
--------------------------------------------------------------------------------
/src/meals/mealForm.ts:
--------------------------------------------------------------------------------
1 | import { Meal } from './types'
2 | import { getIngredientForm, IngredientForm } from 'ingredients'
3 | import { v4 as uuidv4 } from 'uuid'
4 |
5 | type MealForm = {
6 | fieldId: string
7 | name: string
8 | notes?: string
9 | ingredientsForms: IngredientForm[]
10 | }
11 |
12 | function getMealForm(meal?: Meal): MealForm {
13 | const fieldId = uuidv4()
14 |
15 | if (meal) {
16 | return {
17 | fieldId,
18 | name: meal.name,
19 | ingredientsForms: meal.ingredients.map(ingredient =>
20 | getIngredientForm(ingredient)
21 | ),
22 | }
23 | }
24 |
25 | return {
26 | fieldId,
27 | name: '',
28 | ingredientsForms: [],
29 | }
30 | }
31 |
32 | function getInsertMealFormAnimationKey(fieldId: string) {
33 | return `insert-meal-animmation-${fieldId}`
34 | }
35 |
36 | export type { MealForm }
37 |
38 | export { getMealForm, getInsertMealFormAnimationKey }
39 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { HStack, Button } from '@chakra-ui/react'
2 | import {
3 | nonQueryChangesCount,
4 | useFoodsFilter,
5 | useFoodsFilterActions,
6 | } from 'foods-filters'
7 |
8 | type Props = {
9 | onClose: () => void
10 | }
11 |
12 | function Footer({ onClose }: Props) {
13 | const filter = useFoodsFilter()
14 | const foodsFilterActions = useFoodsFilterActions()
15 |
16 | const changesCount = nonQueryChangesCount(filter)
17 |
18 | function onReset() {
19 | foodsFilterActions.resetFilter()
20 | onClose()
21 | }
22 |
23 | return (
24 |
25 |
28 |
31 |
32 | )
33 | }
34 |
35 | export default Footer
36 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/index.tsx:
--------------------------------------------------------------------------------
1 | import { DietFormStoreProvider } from 'diets'
2 | import Form from './Form'
3 | import { useOneTimeCheckActions } from 'general'
4 | import DndContextProvider from './DndContextProvider'
5 | import { MealsStatsStoreProvider } from 'stats'
6 | import { useState } from 'react'
7 | import { loadLastOrDefaultDietForm } from 'diets/persistence'
8 |
9 | function DietEditor() {
10 | const oneTimeCheckActions = useOneTimeCheckActions()
11 | const [dietForm] = useState(loadLastOrDefaultDietForm)
12 |
13 | return (
14 | <>
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | >
26 | )
27 | }
28 |
29 | export default DietEditor
30 |
--------------------------------------------------------------------------------
/src/portions/formatAmount.ts:
--------------------------------------------------------------------------------
1 | import formatQuantity from 'format-quantity'
2 |
3 | function formatAsNumber(amount: number) {
4 | const amountStringFixedTo1 = amount.toFixed(1)
5 | const amountFixedTo1 = Number(amountStringFixedTo1)
6 |
7 | return Number.isInteger(amountFixedTo1)
8 | ? amountFixedTo1.toString()
9 | : amountStringFixedTo1
10 | }
11 |
12 | function formatAmount(amount: number, portionId: string): string {
13 | if (portionId === 'grams') {
14 | return Math.round(amount).toString()
15 | }
16 |
17 | if (
18 | portionId === 'ounces' ||
19 | portionId === 'milliliters' ||
20 | portionId === 'fluid ounces'
21 | ) {
22 | return formatAsNumber(amount)
23 | }
24 |
25 | const formattedAsFractions = formatQuantity(amount) || ''
26 |
27 | if (formattedAsFractions.includes('.')) {
28 | return formatAsNumber(amount)
29 | }
30 |
31 | return formattedAsFractions
32 | }
33 |
34 | export default formatAmount
35 |
--------------------------------------------------------------------------------
/src/variants/VariantsDetailsModal/Content/VariantsDetailsFormProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FormProvider, useForm } from 'react-hook-form'
2 | import { ReactNode } from 'react'
3 | import {
4 | getVariantsDetailsForm,
5 | VariantsDetailsForm,
6 | } from './variantsDetailsForm'
7 | import { VariantForm } from 'variants'
8 | import { Stats } from 'stats'
9 |
10 | type Props = {
11 | children: ReactNode
12 | initialVariantForm: VariantForm
13 | initialVariantStats: Stats
14 | }
15 |
16 | function VariantsDetailsFormProvider({
17 | children,
18 | initialVariantForm,
19 | initialVariantStats,
20 | }: Props) {
21 | const defaultValues = getVariantsDetailsForm(
22 | initialVariantForm.fieldId,
23 | initialVariantStats
24 | )
25 |
26 | const formMethods = useForm({
27 | defaultValues,
28 | })
29 |
30 | return {children}
31 | }
32 |
33 | export default VariantsDetailsFormProvider
34 |
--------------------------------------------------------------------------------
/src/meals/PdfMealsList/Notes.tsx:
--------------------------------------------------------------------------------
1 | import { Text, StyleSheet, View } from '@react-pdf/renderer'
2 | import getComputedColorFromChakra from 'theme/getComputedColorFromChakra'
3 |
4 | type Props = {
5 | notes: string
6 | }
7 |
8 | function Notes({ notes }: Props) {
9 | return (
10 |
18 |
26 | {notes}
27 |
28 |
29 | )
30 | }
31 |
32 | const styles = StyleSheet.create({
33 | root: {
34 | padding: 12,
35 | borderTopWidth: 1,
36 | fontSize: 14,
37 | },
38 | text: {
39 | flexDirection: 'row',
40 | flex: 1,
41 | },
42 | })
43 |
44 | export default Notes
45 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantItem/PresenceAnimation.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { motion } from 'framer-motion'
3 |
4 | type Props = {
5 | children: ReactNode
6 | shouldAnimate: boolean
7 | onAnimationComplete: () => void
8 | isVisible: boolean
9 | }
10 |
11 | const variants = {
12 | open: {
13 | opacity: 1,
14 | width: 'auto',
15 | x: 0,
16 | },
17 | collapsed: { opacity: 0, width: 0, x: 0 },
18 | }
19 |
20 | function PresenceAnimation({
21 | shouldAnimate,
22 | isVisible,
23 | onAnimationComplete,
24 | children,
25 | }: Props) {
26 | return (
27 |
34 | {children}
35 |
36 | )
37 | }
38 |
39 | export default PresenceAnimation
40 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/FoodFormProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useForm } from 'react-hook-form'
2 | import { yupResolver } from '@hookform/resolvers/yup'
3 | import { ReactNode } from 'react'
4 | import { FormProvider } from 'react-hook-form'
5 | import {
6 | Food,
7 | FoodForm,
8 | foodFormSchema,
9 | FoodFormSchemaContext,
10 | getFoodForm,
11 | useFoods,
12 | } from 'foods'
13 |
14 | type Props = {
15 | food?: Food
16 | children: ReactNode
17 | }
18 |
19 | function FoodFormProvider({ food, children }: Props) {
20 | const defaultValues = getFoodForm(food)
21 | const { allFoods } = useFoods()
22 |
23 | const formMethods = useForm({
24 | defaultValues,
25 | resolver: yupResolver(foodFormSchema),
26 | context: {
27 | allFoods,
28 | food,
29 | },
30 | mode: 'onChange',
31 | })
32 |
33 | return {children}
34 | }
35 |
36 | export default FoodFormProvider
37 |
--------------------------------------------------------------------------------
/src/stats/types.ts:
--------------------------------------------------------------------------------
1 | type NutritionData = {
2 | energy: number
3 |
4 | fat: number
5 | saturatedFat: number
6 | monounsaturatedFat: number
7 | polyunsaturatedFat: number
8 |
9 | carbs: number
10 | sugar: number
11 | fiber: number
12 |
13 | protein: number
14 |
15 | sodium: number
16 | cholesterol: number
17 |
18 | vitaminA: number
19 | vitaminB1: number
20 | vitaminB2: number
21 | vitaminB3: number
22 | vitaminB5: number
23 | vitaminB6: number
24 | vitaminB9: number
25 | vitaminB12: number
26 | vitaminC: number
27 | vitaminD: number
28 | vitaminE: number
29 | vitaminK: number
30 |
31 | magnesium: number
32 | calcium: number
33 | phosphorus: number
34 | potassium: number
35 | iron: number
36 | selenium: number
37 | zinc: number
38 | manganese: number
39 | copper: number
40 | choline: number
41 | }
42 |
43 | type Stats = {
44 | amountInGrams: number
45 | } & NutritionData
46 |
47 | export type { Stats, NutritionData }
48 |
--------------------------------------------------------------------------------
/src/notes/EditNotesModal/Content/index.tsx:
--------------------------------------------------------------------------------
1 | import { RefObject } from 'react'
2 | import NotesFormProvider from './NotesFormProvider'
3 | import Form from './Form'
4 |
5 | type Props = {
6 | onClose: () => void
7 | initialRef: RefObject
8 | onEditNotes: (notes?: string) => void
9 | fieldId: string
10 | ownerName: string
11 | notes?: string
12 | textAreaHeight?: string | number
13 | }
14 |
15 | function Content({
16 | ownerName,
17 | onClose,
18 | initialRef,
19 | onEditNotes,
20 | fieldId,
21 | notes,
22 | textAreaHeight,
23 | }: Props) {
24 | return (
25 |
26 |
35 |
36 | )
37 | }
38 |
39 | export default Content
40 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from 'general'
2 | import { IconButton } from '@chakra-ui/react'
3 | import { Filter } from 'react-feather'
4 | import { nonQueryChangesCount, useFoodsFilter } from 'foods-filters'
5 | import { ForwardedRef, forwardRef } from 'react'
6 |
7 | type Props = {
8 | onClick?: () => void
9 | forwardedRef?: ForwardedRef
10 | }
11 |
12 | function Trigger({ onClick, forwardedRef }: Props) {
13 | const filter = useFoodsFilter()
14 |
15 | const changesCount = nonQueryChangesCount(filter)
16 |
17 | return (
18 |
19 | }
23 | variant="outline"
24 | onClick={onClick}
25 | />
26 |
27 | )
28 | }
29 | export default forwardRef((props, ref) => (
30 |
31 | ))
32 |
--------------------------------------------------------------------------------
/src/foods/persistence/useImportFoods.ts:
--------------------------------------------------------------------------------
1 | import { UseDisclosureReturn } from '@chakra-ui/hooks'
2 | import { Food } from 'foods'
3 | import { selectFile, readFile, useFileImportError } from 'persistence'
4 | import { useState } from 'react'
5 |
6 | type Params = {
7 | foodsListModalDisclosure: UseDisclosureReturn
8 | }
9 |
10 | function useImportFoods({ foodsListModalDisclosure }: Params) {
11 | const fileImportError = useFileImportError()
12 | const [foodsToImport, setFoodsToImport] = useState()
13 |
14 | async function onImport() {
15 | const file = await selectFile('text/json')
16 |
17 | try {
18 | const text = await readFile(file)
19 | const foodsToImport = JSON.parse(text) as Food[]
20 | setFoodsToImport(foodsToImport)
21 | foodsListModalDisclosure.onOpen()
22 | } catch (error: any) {
23 | fileImportError.onError({ error, file })
24 | }
25 | }
26 |
27 | return {
28 | onImport,
29 | foodsToImport,
30 | }
31 | }
32 |
33 | export default useImportFoods
34 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/Content/SelectedFoodsList/index.tsx:
--------------------------------------------------------------------------------
1 | import { Flex, Text, Wrap } from '@chakra-ui/react'
2 | import { Selection } from 'general'
3 | import SelectedFoodItem from './SelectedFoodItem'
4 | import { Food, useFoods } from 'foods'
5 |
6 | type Props = {
7 | selection: Selection
8 | }
9 |
10 | function SelectedFoods({ selection }: Props) {
11 | const { selectedItems: selectedFoods } = selection
12 | const { foodsById } = useFoods()
13 |
14 | return (
15 |
16 | {selectedFoods.length > 0 ? (
17 |
18 | {selectedFoods.map(({ id }) => (
19 |
24 | ))}
25 |
26 | ) : (
27 | Select one or more foods
28 | )}
29 |
30 | )
31 | }
32 |
33 | export default SelectedFoods
34 |
--------------------------------------------------------------------------------
/src/portions/PortionsMenuOrDrawer/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import { ForwardedRef, forwardRef } from 'react'
2 | import { Button } from '@chakra-ui/react'
3 | import { usePortions } from 'portions'
4 |
5 | type Props = {
6 | selectedPortionId: string
7 | forwardedRef?: ForwardedRef
8 | onClick?: () => void
9 | }
10 |
11 | function Trigger({ forwardedRef, selectedPortionId, ...rest }: Props) {
12 | const { portionsById } = usePortions()
13 | const portion = portionsById[selectedPortionId]
14 |
15 | return (
16 |
30 | )
31 | }
32 | export default forwardRef((props, ref) => (
33 |
34 | ))
35 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/Modal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal as ModalBase,
3 | ModalOverlay,
4 | ModalContent,
5 | ModalHeader,
6 | ModalFooter,
7 | ModalBody,
8 | ModalCloseButton,
9 | } from '@chakra-ui/react'
10 | import { useRef } from 'react'
11 | import Content from './Content'
12 | import Footer from './Footer'
13 |
14 | type Props = {
15 | isOpen: boolean
16 | onClose: () => void
17 | }
18 |
19 | function Modal({ isOpen, onClose }: Props) {
20 | const selectRef = useRef(null)
21 |
22 | return (
23 |
24 |
25 |
26 |
27 | Filters
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
41 | export default Modal
42 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/Content/Header.tsx:
--------------------------------------------------------------------------------
1 | import { DrawerHeader, Text } from '@chakra-ui/react'
2 | import { MealForm } from 'meals'
3 |
4 | type Props = {
5 | mealName?: string
6 | canSelect: boolean
7 | mealForm?: MealForm
8 | }
9 |
10 | function getTitlePrefix(props: Props) {
11 | const { mealName, canSelect, mealForm } = props
12 |
13 | if (!mealForm && mealName) {
14 | return 'Select Foods for '
15 | }
16 |
17 | if (canSelect) {
18 | return mealName ? 'Add Foods to ' : 'Add Foods'
19 | }
20 |
21 | return 'Foods'
22 | }
23 |
24 | function Header(props: Props) {
25 | const { mealName } = props
26 | const fontWeight = mealName ? 'normal' : 'bold'
27 | let titlePrefix = getTitlePrefix(props)
28 |
29 | return (
30 |
31 | {titlePrefix}
32 |
33 | {mealName && (
34 |
35 | {mealName}
36 |
37 | )}
38 |
39 | )
40 | }
41 |
42 | export default Header
43 |
--------------------------------------------------------------------------------
/src/portions/defaultPortions.ts:
--------------------------------------------------------------------------------
1 | import { Portion } from './types'
2 |
3 | const defaultPortions: Portion[] = [
4 | {
5 | id: 'grams',
6 | unit: 'g',
7 | gramsPerAmount: 1,
8 | singular: 'gram',
9 | },
10 | {
11 | id: 'ounces',
12 | unit: 'oz',
13 | gramsPerAmount: 28.34952,
14 | singular: 'ounce',
15 | },
16 | {
17 | id: 'milliliters',
18 | unit: 'ml',
19 | millilitersPerAmount: 1,
20 | singular: 'milliliter',
21 | },
22 | {
23 | id: 'teaspoons',
24 | unit: 'tsp',
25 | millilitersPerAmount: 5,
26 | singular: 'teaspoon',
27 | },
28 | {
29 | id: 'tablespoons',
30 | unit: 'tbsp',
31 | millilitersPerAmount: 15,
32 | singular: 'tablespoon',
33 | },
34 | {
35 | id: 'fluid ounces',
36 | unit: 'fl oz',
37 | millilitersPerAmount: 30,
38 | singular: 'fluid ounce',
39 | },
40 | {
41 | id: 'cups',
42 | unit: 'cup',
43 | millilitersPerAmount: 240,
44 | singular: 'cup',
45 | },
46 | ]
47 |
48 | export default defaultPortions
49 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealItem/PresenceAnimation.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { motion } from 'framer-motion'
3 |
4 | type Props = {
5 | children: ReactNode
6 | shouldAnimate: boolean
7 | onAnimationComplete: () => void
8 | isVisible: boolean
9 | isDragging: boolean
10 | }
11 |
12 | const variants = {
13 | open: {
14 | opacity: 1,
15 | height: 'auto',
16 | },
17 |
18 | collapsed: { opacity: 0, height: 0, x: 0 },
19 | }
20 |
21 | function PresenceAnimation({
22 | children,
23 | onAnimationComplete,
24 | shouldAnimate,
25 | isVisible,
26 | isDragging,
27 | }: Props) {
28 | return (
29 |
39 | {children}
40 |
41 | )
42 | }
43 |
44 | export default PresenceAnimation
45 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantsMenuOrDrawer/Drawer/VariantItem.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps, Flex, Text } from '@chakra-ui/react'
2 | import { Check } from 'react-feather'
3 |
4 | type Props = {
5 | name: string
6 | isSelected: boolean
7 | } & BoxProps
8 |
9 | function VariantItem({ name, isSelected, ...rest }: Props) {
10 | return (
11 |
26 |
27 |
28 | {name}
29 |
30 | {isSelected && (
31 |
32 |
33 |
34 | )}
35 |
36 |
37 | )
38 | }
39 |
40 | export default VariantItem
41 |
--------------------------------------------------------------------------------
/src/variants/VariantsDetailsModal/Content/variantsDetailsForm.ts:
--------------------------------------------------------------------------------
1 | import {
2 | getFatEnergyEstimate,
3 | getMacroEnergyPercent,
4 | getStatsEnergiesEstimates,
5 | MappedNutritionData,
6 | objectFromNutritionDataKeys,
7 | Stats,
8 | } from 'stats'
9 |
10 | type VariantsDetailsForm = {
11 | variantFormFieldId: string
12 | saturatedFatEnergyPercent: string
13 | } & MappedNutritionData
14 |
15 | function getVariantsDetailsForm(
16 | variantFormFieldId: string,
17 | variantStats: Stats
18 | ) {
19 | const { saturatedFat } = variantStats
20 | const saturatedFatEnergyEstimate = getFatEnergyEstimate(saturatedFat)
21 | const { energyEstimate } = getStatsEnergiesEstimates(variantStats)
22 |
23 | return {
24 | variantFormFieldId,
25 | saturatedFatEnergyPercent: Math.round(
26 | getMacroEnergyPercent(saturatedFatEnergyEstimate, energyEstimate)
27 | ).toString(),
28 | ...objectFromNutritionDataKeys(key => variantStats[key].toString()),
29 | }
30 | }
31 |
32 | export type { VariantsDetailsForm }
33 |
34 | export { getVariantsDetailsForm }
35 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/AddVariantButton.tsx:
--------------------------------------------------------------------------------
1 | import { Button, IconButton, ButtonProps } from '@chakra-ui/react'
2 | import { ScreenSize, useScreenSize } from 'general'
3 | import { CalendarPlus } from 'icons'
4 |
5 | type Props = {} & ButtonProps
6 |
7 | function AddVariantButton({ ...rest }: Props) {
8 | const screenSize = useScreenSize()
9 |
10 | if (screenSize >= ScreenSize.Medium) {
11 | return (
12 | }
17 | variant="outline"
18 | mr={2}
19 | flexShrink={0}
20 | {...rest}
21 | >
22 | Add day
23 |
24 | )
25 | }
26 |
27 | return (
28 | }
34 | variant="outline"
35 | mr={2}
36 | flexShrink={0}
37 | {...rest}
38 | />
39 | )
40 | }
41 |
42 | export default AddVariantButton
43 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/IngredientItem/PresenceAnimation.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { motion } from 'framer-motion'
3 |
4 | type Props = {
5 | children: ReactNode
6 | shouldAnimate: boolean
7 | onAnimationComplete: () => void
8 | isVisible: boolean
9 | isDraggingOver?: boolean
10 | }
11 |
12 | const variants = {
13 | open: {
14 | opacity: 1,
15 | height: 'auto',
16 | },
17 | collapsed: { opacity: 0, height: 0 },
18 | }
19 |
20 | function PresenceAnimation({
21 | shouldAnimate,
22 | onAnimationComplete,
23 | isVisible,
24 | children,
25 | isDraggingOver = false,
26 | }: Props) {
27 | return (
28 |
36 | {children}
37 |
38 | )
39 | }
40 |
41 | export default PresenceAnimation
42 |
--------------------------------------------------------------------------------
/src/stats/calculations/getMacrosPercents.ts:
--------------------------------------------------------------------------------
1 | import { Stats } from '../types'
2 | import { getStatsEnergiesEstimates } from './getEnergiesEstimates'
3 |
4 | type MacrosPercents = {
5 | proteinPercent: number
6 | carbsPercent: number
7 | fatPercent: number
8 | }
9 |
10 | function getMacroEnergyPercent(energyFromMacro: number, energyTotal: number) {
11 | return energyFromMacro === 0 ? 0 : (energyFromMacro / energyTotal) * 100
12 | }
13 |
14 | function getMacrosPercents(stats: Stats): MacrosPercents {
15 | const {
16 | energyEstimate,
17 | proteinEnergyEstimate,
18 | carbsEnergyEstimate,
19 | fatEnergyEstimate,
20 | } = getStatsEnergiesEstimates(stats)
21 |
22 | return {
23 | proteinPercent: getMacroEnergyPercent(
24 | proteinEnergyEstimate,
25 | energyEstimate
26 | ),
27 | carbsPercent: getMacroEnergyPercent(carbsEnergyEstimate, energyEstimate),
28 | fatPercent: getMacroEnergyPercent(fatEnergyEstimate, energyEstimate),
29 | }
30 | }
31 |
32 | export type { MacrosPercents }
33 |
34 | export { getMacroEnergyPercent }
35 |
36 | export default getMacrosPercents
37 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/Form/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { Box, HStack, Button, Divider, Link, BoxProps } from '@chakra-ui/react'
2 |
3 | type Props = {
4 | onAbout: () => void
5 | } & BoxProps
6 |
7 | function Footer({ onAbout, ...rest }: Props) {
8 | return (
9 |
10 |
11 |
12 |
21 |
22 |
27 | Terms
28 |
29 |
30 |
35 | Disclaimer
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | export default Footer
43 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/Drawer/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Drawer as DrawerBase,
3 | DrawerBody,
4 | DrawerHeader,
5 | DrawerOverlay,
6 | DrawerContent,
7 | DrawerCloseButton,
8 | VStack,
9 | } from '@chakra-ui/react'
10 | import { ReactElement } from 'react'
11 | import getDrawerButtons from './getDrawerButtons'
12 |
13 | type Props = {
14 | isOpen: boolean
15 | onClose: () => void
16 | children: ReactElement | ReactElement[]
17 | title: string
18 | }
19 |
20 | function Drawer({ title, isOpen, onClose, children }: Props) {
21 | return (
22 |
23 |
24 |
25 |
26 | {title}
27 |
28 |
29 |
30 | {getDrawerButtons(children, onClose)}
31 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export { getDrawerButtons }
39 |
40 | export default Drawer
41 |
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import { ChakraProvider } from '@chakra-ui/react'
2 | import { MainLayout } from 'layout'
3 | import 'focus-visible/dist/focus-visible'
4 | import theme from 'theme'
5 | import { FoodsStoreProvider } from 'foods'
6 | import { loadFoods } from 'foods/persistence'
7 | import { OneTimeCheckStoreProvider, ScreenSizeProvider } from 'general'
8 | import { DietEditor } from 'diets'
9 | import { useState } from 'react'
10 | import { PortionsStoreProvider } from 'portions'
11 |
12 | import 'scroll-polyfill/auto'
13 |
14 | function App() {
15 | const [foods] = useState(loadFoods)
16 |
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | )
32 | }
33 |
34 | export default App
35 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Vladimir Angelov
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/src/stats/getUnit.ts:
--------------------------------------------------------------------------------
1 | import { NutritionData } from 'stats'
2 |
3 | type Unit = 'mcg' | 'mg' | 'g'
4 | type UnitsRecord = Record
5 |
6 | const unitsRecord: UnitsRecord = {
7 | energy: 'g',
8 |
9 | fat: 'g',
10 | saturatedFat: 'g',
11 | monounsaturatedFat: 'g',
12 | polyunsaturatedFat: 'g',
13 |
14 | carbs: 'g',
15 | sugar: 'g',
16 | fiber: 'g',
17 |
18 | protein: 'g',
19 |
20 | sodium: 'mg',
21 | cholesterol: 'mg',
22 |
23 | vitaminA: 'mcg',
24 | vitaminB1: 'mg',
25 | vitaminB2: 'mg',
26 | vitaminB3: 'mg',
27 | vitaminB5: 'mg',
28 | vitaminB6: 'mg',
29 | vitaminB9: 'mcg',
30 | vitaminB12: 'mcg',
31 | vitaminC: 'mg',
32 | vitaminD: 'mcg',
33 | vitaminE: 'mg',
34 | vitaminK: 'mcg',
35 |
36 | magnesium: 'mg',
37 | calcium: 'mg',
38 | phosphorus: 'mg',
39 | potassium: 'mg',
40 | iron: 'mg',
41 | selenium: 'mcg',
42 | zinc: 'mcg',
43 | manganese: 'mg',
44 | copper: 'mg',
45 | choline: 'g',
46 | }
47 |
48 | function getUnit(name: keyof NutritionData) {
49 | const unit = unitsRecord[name]
50 |
51 | return unit
52 | }
53 |
54 | export default getUnit
55 |
--------------------------------------------------------------------------------
/src/stats/calculations/getDailyValuePercent.ts:
--------------------------------------------------------------------------------
1 | import { NutritionData } from 'stats'
2 |
3 | type DailyValuesRecord = Partial>
4 |
5 | // From: https://www.fda.gov/food/new-nutrition-facts-label/daily-value-new-nutrition-and-supplement-facts-labels
6 | const DAILY_VALUES_RECORD: DailyValuesRecord = {
7 | calcium: 1300,
8 | fiber: 28,
9 | magnesium: 420,
10 | manganese: 2.3,
11 | phosphorus: 1250,
12 | potassium: 4700,
13 | vitaminA: 900,
14 | vitaminB1: 1.5,
15 | vitaminB2: 1.3,
16 | vitaminB5: 5,
17 | vitaminB6: 1.7,
18 | vitaminB12: 2.4,
19 | vitaminC: 90,
20 | vitaminD: 20,
21 | vitaminE: 15,
22 | vitaminK: 120,
23 | copper: 0.9,
24 | selenium: 55,
25 | sodium: 2300,
26 | cholesterol: 300,
27 | iron: 18,
28 | saturatedFat: 20,
29 | choline: 550,
30 | }
31 |
32 | function getDailyValuePercent(name: keyof NutritionData, value: number) {
33 | const dailyValue = DAILY_VALUES_RECORD[name]
34 |
35 | if (dailyValue === undefined) {
36 | return undefined
37 | }
38 |
39 | return Math.round((100 * value) / dailyValue)
40 | }
41 |
42 | export default getDailyValuePercent
43 |
--------------------------------------------------------------------------------
/src/foods/FoodsList/VirtualizedList/FoodItemRenderer.tsx:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import FoodItem, { UsageType } from './FoodItem'
3 | import { TOP_PADDING } from './Inner'
4 |
5 | type Data = {
6 | getFood: (index: number) => Food
7 | isFoodSelected: (food: Food) => boolean
8 | onFoodSelect: (food: Food) => void
9 | onFoodPreview: (food: Food) => void
10 | usageType: UsageType
11 | }
12 |
13 | type Props = {
14 | style: any
15 | index: number
16 | data: Data
17 | }
18 |
19 | function FoodItemRenderer({ style, index, data }: Props) {
20 | const {
21 | getFood,
22 | onFoodSelect,
23 | onFoodPreview,
24 | isFoodSelected,
25 | usageType,
26 | } = data
27 | const food = getFood(index)
28 |
29 | return (
30 |
42 | )
43 | }
44 |
45 | export default FoodItemRenderer
46 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/DndContextProvider.tsx:
--------------------------------------------------------------------------------
1 | import { useDietFormActions } from 'diets'
2 | import { ReactNode } from 'react'
3 |
4 | import { DragDropContext, DropResult } from 'react-beautiful-dnd'
5 |
6 | type Props = {
7 | children: ReactNode
8 | }
9 |
10 | function DndContextProvider({ children }: Props) {
11 | const dietFormActions = useDietFormActions()
12 |
13 | const onDragEnd = (dropResult: DropResult) => {
14 | const { source, destination, type } = dropResult
15 |
16 | if (!destination) {
17 | return
18 | }
19 |
20 | if (type === 'variantsList') {
21 | dietFormActions.moveVariantForm(source.index, destination.index)
22 | } else if (type === 'mealsList') {
23 | dietFormActions.moveMealForm(source.index, destination.index)
24 | } else if (type === 'ingredientsList') {
25 | dietFormActions.moveIngredientForm(
26 | source.droppableId,
27 | source.index,
28 | destination.droppableId,
29 | destination.index
30 | )
31 | }
32 | }
33 |
34 | return {children}
35 | }
36 |
37 | export default DndContextProvider
38 |
--------------------------------------------------------------------------------
/src/persistence/file.ts:
--------------------------------------------------------------------------------
1 | function createFileInput(accept: string) {
2 | const input = document.createElement('input')
3 | input.type = 'file'
4 | input.multiple = false
5 | input.accept = accept
6 |
7 | return input
8 | }
9 |
10 | function selectFile(accept: string) {
11 | return new Promise(resolve => {
12 | const input = createFileInput(accept)
13 |
14 | input.addEventListener('change', function onChange(this: HTMLInputElement) {
15 | const { files } = this
16 |
17 | if (files && files.length > 0) {
18 | resolve(files[0])
19 | }
20 | })
21 | input.dispatchEvent(new MouseEvent('click'))
22 | })
23 | }
24 |
25 | function readFile(file: File) {
26 | return new Promise((resolve, reject) => {
27 | const fileReader = new FileReader()
28 |
29 | fileReader.onload = () => {
30 | const { result } = fileReader
31 | resolve(result as string)
32 | }
33 |
34 | fileReader.onerror = () => {
35 | const { error } = fileReader
36 | fileReader.abort()
37 | reject(error)
38 | }
39 |
40 | fileReader.readAsText(file)
41 | })
42 | }
43 |
44 | export { selectFile, readFile }
45 |
--------------------------------------------------------------------------------
/src/diets/persistence/useDietImportErrors.tsx:
--------------------------------------------------------------------------------
1 | import { useToast } from '@chakra-ui/toast'
2 | import { Text, Button, UseDisclosureReturn } from '@chakra-ui/react'
3 |
4 | type Params = {
5 | missingFoodsModalDisclosure: UseDisclosureReturn
6 | }
7 |
8 | function useDietImportErrors({ missingFoodsModalDisclosure }: Params) {
9 | const toast = useToast()
10 |
11 | function onLearnAboutMissingFoods() {
12 | toast.closeAll()
13 | missingFoodsModalDisclosure.onOpen()
14 | }
15 |
16 | function onMissingFoods() {
17 | toast({
18 | isClosable: true,
19 | position: 'top',
20 | title: 'File imported',
21 | description: (
22 |
23 | Warning: Foods missing.{' '}
24 |
31 |
32 | ),
33 | status: 'warning',
34 | duration: null,
35 | })
36 | }
37 |
38 | return {
39 | onMissingFoods,
40 | missingFoodsModalDisclosure,
41 | }
42 | }
43 |
44 | export default useDietImportErrors
45 |
--------------------------------------------------------------------------------
/src/meals/MealsList/useScrollToAndFocusMeal.ts:
--------------------------------------------------------------------------------
1 | import { MealForm } from 'meals'
2 | import { useScrollTo } from 'dom'
3 | import { RefObject, useCallback } from 'react'
4 | import { isMobile } from 'react-device-detect'
5 |
6 | type Params = {
7 | getMealNameInputRefById: (id: string) => RefObject
8 | scrollTargetRef: RefObject
9 | }
10 |
11 | function useScrollToAndFocusMeal({ getMealNameInputRefById }: Params) {
12 | const scrollTo = useScrollTo()
13 |
14 | const onScrollToMeal = useCallback(
15 | async (mealForm: MealForm) => {
16 | const mealNameInputRef = getMealNameInputRefById(mealForm.fieldId)
17 |
18 | if (mealNameInputRef.current) {
19 | await scrollTo(mealNameInputRef.current)
20 | }
21 |
22 | if (mealNameInputRef.current && !isMobile) {
23 | mealNameInputRef.current.setSelectionRange(
24 | 0,
25 | mealNameInputRef.current.value.length
26 | )
27 | mealNameInputRef.current.focus()
28 | }
29 | },
30 | [getMealNameInputRefById, scrollTo]
31 | )
32 |
33 | return { onScrollToMeal }
34 | }
35 |
36 | export default useScrollToAndFocusMeal
37 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantsMenuOrDrawer/Trigger.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@chakra-ui/react'
2 | import { ChevronDown } from 'react-feather'
3 | import { ForwardedRef, forwardRef } from 'react'
4 | import { useScreenSize, Tooltip, ScreenSize } from 'general'
5 |
6 | type Props = {
7 | forwardedRef?: ForwardedRef
8 | onClick?: () => void
9 | }
10 |
11 | function Trigger({ forwardedRef, onClick, ...rest }: Props) {
12 | const screenSize = useScreenSize()
13 | const isPhone = screenSize <= ScreenSize.Small
14 |
15 | return (
16 |
17 | }
23 | variant="outline"
24 | mr={isPhone ? 0 : 2}
25 | ml={isPhone ? 2 : 0}
26 | flexShrink={0}
27 | ref={forwardedRef}
28 | onClick={onClick}
29 | {...rest}
30 | />
31 |
32 | )
33 | }
34 | export default forwardRef((props, ref) => (
35 |
36 | ))
37 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/Drawer/getDrawerButtons.tsx:
--------------------------------------------------------------------------------
1 | import { cloneElement, ReactElement, Children } from 'react'
2 | import { Button, Divider } from '@chakra-ui/react'
3 | import MenuOrDrawerItem from '../MenuOrDrawerItem'
4 | import MenuOrDrawerSeparator from '../MenuOrDrawerSeparator'
5 |
6 | function getDrawerButtons(
7 | children: ReactElement | ReactElement[],
8 | onClose: () => void
9 | ) {
10 | return Children.map(children, child => {
11 | if (child.type === MenuOrDrawerItem) {
12 | const icon = cloneElement(child.props.icon, {
13 | size: 20,
14 | })
15 |
16 | return (
17 |
29 | )
30 | } else if (child.type === MenuOrDrawerSeparator) {
31 | return
32 | }
33 |
34 | return null
35 | })
36 | }
37 |
38 | export default getDrawerButtons
39 |
--------------------------------------------------------------------------------
/src/general/HFadeScroll/FadeBox.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Box } from '@chakra-ui/react'
3 |
4 | type ShadowProps = {
5 | hasStartFade: boolean
6 | hasEndFade: boolean
7 | children: ReactNode
8 | }
9 |
10 | function getLinearGradient(direction: 'left' | 'right') {
11 | return `linear-gradient(to ${direction},#F7FAFC, rgba(255, 255, 255, 0))`
12 | }
13 |
14 | const shadowElementBase = {
15 | content: '""',
16 | position: 'absolute',
17 | top: 0,
18 | bottom: 0,
19 | width: '50px',
20 | zIndex: 1,
21 | }
22 |
23 | function FadeBox({ hasStartFade, hasEndFade, children }: ShadowProps) {
24 | const before = {
25 | ...shadowElementBase,
26 | left: 0,
27 | background: getLinearGradient('right'),
28 | }
29 |
30 | const after = {
31 | ...shadowElementBase,
32 | right: 0,
33 | background: getLinearGradient('left'),
34 | }
35 |
36 | return (
37 |
44 | {children}
45 |
46 | )
47 | }
48 |
49 | export default FadeBox
50 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/Content/MenuButtons.tsx:
--------------------------------------------------------------------------------
1 | import { chakra, IconButton } from '@chakra-ui/react'
2 | import { MoreHorizontal, Download, Share } from 'react-feather'
3 | import { Menu, MenuItem } from 'general'
4 |
5 | const DownloadStyled = chakra(Download)
6 | const ShareStyled = chakra(Share)
7 | const MoreHorizontalStyled = chakra(MoreHorizontal)
8 |
9 | type Props = {
10 | onImport: () => void
11 | onExport: () => void
12 | }
13 |
14 | function MenuButtons({ onImport, onExport }: Props) {
15 | return (
16 | }
24 | variant="outline"
25 | />
26 | }
27 | >
28 |
32 |
36 |
37 | )
38 | }
39 |
40 | export default MenuButtons
41 |
--------------------------------------------------------------------------------
/src/meals/PdfMealsList/index.tsx:
--------------------------------------------------------------------------------
1 | import { View } from '@react-pdf/renderer'
2 | import { Food } from 'foods'
3 | import { MealForm } from 'meals/mealForm'
4 | import { Portion } from 'portions'
5 | import { StatsTree } from 'stats'
6 | import PdfMealItem from './PdfMealItem'
7 |
8 | type Props = {
9 | mealsForms: MealForm[]
10 | mealsFormsStatsTrees: StatsTree[]
11 | foodsById: Record
12 | portionsById: Record
13 | }
14 |
15 | function PdfMealsList({
16 | mealsForms,
17 | mealsFormsStatsTrees,
18 | foodsById,
19 | portionsById,
20 | }: Props) {
21 | return (
22 |
23 | {mealsForms.map((mealForm, index) => {
24 | const { stats, subtrees } = mealsFormsStatsTrees[index]
25 |
26 | return (
27 | stats)}
33 | foodsById={foodsById}
34 | portionsById={portionsById}
35 | />
36 | )
37 | })}
38 |
39 | )
40 | }
41 |
42 | export default PdfMealsList
43 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/useDeleteFood.ts:
--------------------------------------------------------------------------------
1 | import { useDisclosure } from '@chakra-ui/hooks'
2 | import { useFoodsActions } from 'foods'
3 | import { Food } from 'foods'
4 | import { useToast } from '@chakra-ui/react'
5 |
6 | type Params = {
7 | food?: Food
8 | onClose: () => void
9 | onFoodDeleted?: (food: Food) => void
10 | }
11 |
12 | function useDeleteFood({ food, onClose, onFoodDeleted }: Params) {
13 | const deleteConfirmationDisclosure = useDisclosure()
14 | const foodsActions = useFoodsActions()
15 | const toast = useToast()
16 |
17 | function onDelete() {
18 | deleteConfirmationDisclosure.onOpen()
19 | }
20 |
21 | function onConfirmDelete() {
22 | if (food) {
23 | foodsActions.removeFood(food.id)
24 | toast({
25 | position: 'top',
26 | title: 'Food deleted',
27 | status: 'success',
28 | duration: 2000,
29 | isClosable: true,
30 | })
31 | deleteConfirmationDisclosure.onClose()
32 | onFoodDeleted && onFoodDeleted(food)
33 | onClose()
34 | }
35 | }
36 |
37 | return {
38 | deleteConfirmationDisclosure,
39 | onDelete,
40 | onConfirmDelete,
41 | }
42 | }
43 |
44 | export default useDeleteFood
45 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/Form/useTabs.tsx:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import { useEffect, useState } from 'react'
3 | import { TabName } from './Tabs'
4 |
5 | type Params = {
6 | food?: Food
7 | isEditing: boolean
8 | }
9 |
10 | function getTabNames(isEditing: boolean, food?: Food): TabName[] {
11 | if (isEditing) {
12 | return ['nutrition', 'volume', 'link']
13 | }
14 |
15 | const result: TabName[] = ['nutrition']
16 |
17 | if (food?.volume) {
18 | result.push('volume')
19 | }
20 |
21 | if (food?.url) {
22 | result.push('link')
23 | }
24 |
25 | return result
26 | }
27 |
28 | function useTabs({ food, isEditing }: Params) {
29 | const [selectedTabName, setSelectedTabName] = useState('nutrition')
30 |
31 | function onTabNameChange(newTabName: TabName) {
32 | setSelectedTabName(newTabName)
33 | }
34 |
35 | const tabNames = getTabNames(isEditing, food)
36 |
37 | useEffect(() => {
38 | if (!tabNames.includes(selectedTabName)) {
39 | setSelectedTabName('nutrition')
40 | }
41 | }, [food, isEditing, selectedTabName, tabNames])
42 |
43 | return {
44 | onTabNameChange,
45 | selectedTabName,
46 | tabNames,
47 | }
48 | }
49 |
50 | export default useTabs
51 |
--------------------------------------------------------------------------------
/src/diets/persistence/parseDietForm.ts:
--------------------------------------------------------------------------------
1 | import { DietForm } from 'diets'
2 | import { fixWhiteSpace } from 'persistence'
3 |
4 | function getLocation(text: string) {
5 | const subject = '/Subject'
6 | const startIndex = text.indexOf('/Subject')
7 | const endIndex = text.indexOf('R', startIndex)
8 |
9 | if (startIndex < 0 || endIndex < 0) {
10 | throw new SyntaxError()
11 | }
12 | const locatioPrefix = text.slice(startIndex + subject.length, endIndex).trim()
13 | return `${locatioPrefix} obj`
14 | }
15 |
16 | function getData(location: string, text: string) {
17 | const startIndex = text.indexOf(location)
18 | const endIndex = text.indexOf('endobj', startIndex)
19 |
20 | if (startIndex < 0 || endIndex < 0) {
21 | throw new SyntaxError()
22 | }
23 |
24 | return text.slice(startIndex + location.length + 2, endIndex - 2)
25 | }
26 |
27 | function parseDietForm(text: string) {
28 | const location = getLocation(text)
29 | const data = getData(location, text)
30 | const dietForm = JSON.parse(data, (key: string, value) => {
31 | if (key === 'notes') {
32 | return fixWhiteSpace(value)
33 | }
34 |
35 | return value
36 | }) as DietForm
37 |
38 | return dietForm
39 | }
40 |
41 | export default parseDietForm
42 |
--------------------------------------------------------------------------------
/src/general/useOneTimeCheckStore.ts:
--------------------------------------------------------------------------------
1 | import { makeStoreProvider, useCallbacksMemo } from 'general'
2 | import { useRef, useCallback } from 'react'
3 |
4 | type KeysMap = {
5 | [key: string]: boolean | undefined
6 | }
7 |
8 | function useOneTimeCheckStore() {
9 | const keysMapRef = useRef({})
10 |
11 | const checkAndReset = useCallback((key: string) => {
12 | if (keysMapRef.current[key] === true) {
13 | setTimeout(() => {
14 | keysMapRef.current[key] = undefined
15 | }, 0)
16 |
17 | return true
18 | }
19 |
20 | return false
21 | }, [])
22 |
23 | const set = useCallback((key: string) => {
24 | keysMapRef.current[key] = true
25 | }, [])
26 |
27 | const actions = useCallbacksMemo({
28 | checkAndReset,
29 | set,
30 | })
31 |
32 | return [keysMapRef, actions] as const
33 | }
34 |
35 | const [
36 | OneTimeCheckStoreProvider,
37 | useOneTimeCheck,
38 | useOneTimeCheckActions,
39 | ] = makeStoreProvider(useOneTimeCheckStore)
40 |
41 | type OneTimeCheckActions = ReturnType
42 |
43 | export { OneTimeCheckStoreProvider, useOneTimeCheck, useOneTimeCheckActions }
44 |
45 | export type { OneTimeCheckActions }
46 |
47 | export default useOneTimeCheckStore
48 |
--------------------------------------------------------------------------------
/src/stats/calculations/getEnergiesEstimates.ts:
--------------------------------------------------------------------------------
1 | import { Stats } from 'stats/types'
2 |
3 | const CALORIES_PER_GRAM_PROTEIN = 4
4 | const CALORIES_PER_GRAM_CARBS = 4
5 | const CALORIES_PER_GRAM_FAT = 9
6 |
7 | function getProteinEnergyEstimate(gramsProtein: number) {
8 | return gramsProtein * CALORIES_PER_GRAM_PROTEIN
9 | }
10 |
11 | function getCarbsEnergyEstimate(gramsCarbs: number) {
12 | return gramsCarbs * CALORIES_PER_GRAM_CARBS
13 | }
14 |
15 | function getFatEnergyEstimate(gramsFat: number) {
16 | return gramsFat * CALORIES_PER_GRAM_FAT
17 | }
18 |
19 | function getStatsEnergiesEstimates(stats: Stats) {
20 | const { protein, carbs, fat } = stats
21 |
22 | const proteinEnergyEstimate = getProteinEnergyEstimate(protein)
23 | const carbsEnergyEstimate = getCarbsEnergyEstimate(carbs)
24 | const fatEnergyEstimate = getFatEnergyEstimate(fat)
25 |
26 | const energyEstimate =
27 | proteinEnergyEstimate + carbsEnergyEstimate + fatEnergyEstimate
28 |
29 | return {
30 | energyEstimate,
31 | proteinEnergyEstimate,
32 | carbsEnergyEstimate,
33 | fatEnergyEstimate,
34 | }
35 | }
36 |
37 | export {
38 | getProteinEnergyEstimate,
39 | getCarbsEnergyEstimate,
40 | getFatEnergyEstimate,
41 | getStatsEnergiesEstimates,
42 | }
43 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantNameModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalOverlay } from '@chakra-ui/react'
2 | import { useRef } from 'react'
3 | import Content from './Content'
4 | import VariantNameFormProvider from './VariantNameFormProvider'
5 |
6 | type Props = {
7 | onClose: () => void
8 | isOpen: boolean
9 | variantFormIndex: number
10 | }
11 |
12 | function VariantNameModal({ onClose, isOpen, variantFormIndex }: Props) {
13 | const initialRef = useRef(null)
14 | const finalFocusRef = useRef(null)
15 |
16 | return (
17 |
25 |
26 |
27 |
33 |
34 |
35 | )
36 | }
37 |
38 | export * from './variantNameForm'
39 | export * from './useSubmitVariantNameForm'
40 |
41 | export default VariantNameModal
42 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantsMenuOrDrawer/Menu.tsx:
--------------------------------------------------------------------------------
1 | import { chakra } from '@chakra-ui/react'
2 | import { Check } from 'react-feather'
3 | import { Menu as MenuBase, MenuItem } from 'general'
4 | import Trigger from './Trigger'
5 | import { useDietForm } from 'diets'
6 | import { VariantForm } from 'variants'
7 |
8 | const CheckStyled = chakra(Check)
9 |
10 | type Props = {
11 | onSelect: (variantForm: VariantForm, index: number) => void
12 | }
13 |
14 | function Menu({ onSelect }: Props) {
15 | const { variantsForms, selectedVariantFormIndex } = useDietForm()
16 |
17 | return (
18 | }>
19 | {variantsForms.map((variantForm, index) => {
20 | const { fieldId, name } = variantForm
21 | const isSelected = index === selectedVariantFormIndex
22 |
23 | return (
24 |
33 | )
34 | })}
35 |
36 | )
37 | }
38 |
39 | export default Menu
40 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/Form/Controls/ExportButton.tsx:
--------------------------------------------------------------------------------
1 | import { ScreenSize, useScreenSize } from 'general'
2 | import { Button, ButtonProps, IconButton } from '@chakra-ui/react'
3 | import { Share } from 'react-feather'
4 | import { canExportDietForm } from 'diets/persistence'
5 | import { useDietForm } from 'diets'
6 |
7 | type Props = {} & ButtonProps
8 |
9 | function ExportButton({ ...rest }: Props) {
10 | const screenSize = useScreenSize()
11 | const dietForm = useDietForm()
12 | const canExport = canExportDietForm(dietForm)
13 |
14 | const commonProps: ButtonProps = {
15 | isDisabled: !canExport,
16 | ...rest,
17 | }
18 |
19 | if (screenSize >= ScreenSize.Medium) {
20 | return (
21 | }
23 | variant="solid"
24 | colorScheme="teal"
25 | size="md"
26 | {...commonProps}
27 | >
28 | Export
29 |
30 | )
31 | }
32 |
33 | return (
34 | }
40 | {...commonProps}
41 | />
42 | )
43 | }
44 |
45 | export default ExportButton
46 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantsMenuOrDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenSize, ScreenSize } from 'general'
2 | import Drawer from './Drawer'
3 | import Trigger from './Trigger'
4 | import { useDisclosure } from '@chakra-ui/hooks'
5 | import Menu from './Menu'
6 | import { useDietFormActions } from 'diets'
7 | import { VariantForm } from 'variants'
8 |
9 | type Props = {
10 | onVariantFormSelect: (variantForm: VariantForm, index: number) => void
11 | }
12 |
13 | function VariantsMenuOrDrawer({ onVariantFormSelect }: Props) {
14 | const screenSize = useScreenSize()
15 | const modalDisclosure = useDisclosure()
16 | const dietActions = useDietFormActions()
17 |
18 | function onSelect(variantForm: VariantForm, index: number) {
19 | dietActions.setSelectedVariantFormIndex(index)
20 | onVariantFormSelect(variantForm, index)
21 | }
22 |
23 | if (screenSize < ScreenSize.Medium) {
24 | return (
25 | <>
26 |
27 |
32 | >
33 | )
34 | }
35 |
36 | return
37 | }
38 |
39 | export default VariantsMenuOrDrawer
40 |
--------------------------------------------------------------------------------
/src/variants/VariantsDetailsModal/Content/useVariantFormEvents.ts:
--------------------------------------------------------------------------------
1 | import { useFormContext, useWatch } from 'react-hook-form'
2 | import { getVariantsDetailsForm } from './variantsDetailsForm'
3 | import { Stats, StatsTree } from 'stats'
4 |
5 | type Params = {
6 | dietFormStatsTree: StatsTree
7 | }
8 |
9 | function useVariantFormEvents({ dietFormStatsTree }: Params) {
10 | const { reset } = useFormContext()
11 | const variantFormFieldId = useWatch({ name: 'variantFormFieldId' })
12 |
13 | function getVariantStatsForForFieldId(value: string) {
14 | if (value) {
15 | const stats = dietFormStatsTree.subtrees.find(({ id }) => id === value)
16 |
17 | if (!stats) {
18 | throw new Error()
19 | }
20 |
21 | return stats.stats
22 | }
23 |
24 | return dietFormStatsTree.avg as Stats
25 | }
26 |
27 | const variantStats = getVariantStatsForForFieldId(variantFormFieldId)
28 |
29 | function onVariantFormFieldIdChange(value: string) {
30 | const variantStats = getVariantStatsForForFieldId(value)
31 | const newVaraintsDetailsForm = getVariantsDetailsForm(value, variantStats)
32 | reset(newVaraintsDetailsForm)
33 | }
34 |
35 | return { onVariantFormFieldIdChange, variantStats }
36 | }
37 |
38 | export default useVariantFormEvents
39 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import { Drawer, DrawerOverlay } from '@chakra-ui/react'
2 | import { Food } from 'foods'
3 | import { MealForm } from 'meals'
4 | import { useRef } from 'react'
5 | import { isMobile } from 'react-device-detect'
6 | import Content from './Content'
7 |
8 | type Props = {
9 | onClose: () => void
10 | isOpen: boolean
11 | mealName?: string
12 | mealForm?: MealForm
13 | canSelect?: boolean
14 | onSelectedFoods?: (foods: Food[], mealName?: string) => void
15 | }
16 |
17 | function FoodsDrawer({
18 | onClose,
19 | isOpen,
20 | mealName,
21 | mealForm,
22 | canSelect = true,
23 | onSelectedFoods,
24 | }: Props) {
25 | const searchInputRef = useRef(null)
26 |
27 | return (
28 |
35 |
36 |
44 |
45 | )
46 | }
47 |
48 | export default FoodsDrawer
49 |
--------------------------------------------------------------------------------
/src/general/Badge.tsx:
--------------------------------------------------------------------------------
1 | import { ForwardedRef, ReactNode } from 'react'
2 | import { Box, BoxProps, Center, Fade, Text } from '@chakra-ui/react'
3 | import { forwardRef } from 'react'
4 | import { useSameOrPreviousValue } from 'general'
5 |
6 | type Props = {
7 | children: ReactNode
8 | count: number
9 | forwardedRef?: ForwardedRef
10 | } & BoxProps
11 |
12 | function Badge({ children, count, forwardedRef, ...rest }: Props) {
13 | const prevCount = useSameOrPreviousValue(count)
14 |
15 | return (
16 |
17 | {children}
18 |
19 | 0}>
20 |
31 |
32 | {count === 0 ? prevCount : count}
33 |
34 |
35 |
36 |
37 | )
38 | }
39 |
40 | export default forwardRef((props, ref) => (
41 |
42 | ))
43 |
--------------------------------------------------------------------------------
/src/foods/builtIn/index.ts:
--------------------------------------------------------------------------------
1 | import poultry from './poultry.json'
2 | import beef from './beef.json'
3 | import pork from './pork.json'
4 | import finfishAndShellfish from './finfishAndShellFish.json'
5 | import dairyAndEggs from './dairyAndEggs.json'
6 | import grainsAndPasta from './grainsAndPasta.json'
7 | import vegetables from './vegetables.json'
8 | import legumesAndLegumeProducts from './legumesAndLegumeProducts.json'
9 | import fruitsAndJuices from './fruitsAndJuices.json'
10 | import nutAndSeedProducts from './nutAndSeedProducts.json'
11 | import fatsAndOils from './fatsAndOils.json'
12 | import bakedProducts from './bakedProducts.json'
13 | import saucesAndSoups from './saucesAndSoups.json'
14 | import spicesAndHerbs from './spicesAndHerbs.json'
15 | import sweetsAndSnacks from './sweetsAndSnacks.json'
16 | import beverages from './beverages.json'
17 |
18 | const foods = [
19 | ...poultry,
20 | ...beef,
21 | ...pork,
22 | ...finfishAndShellfish,
23 | ...dairyAndEggs,
24 | ...grainsAndPasta,
25 | ...vegetables,
26 | ...legumesAndLegumeProducts,
27 | ...fruitsAndJuices,
28 | ...nutAndSeedProducts,
29 | ...fatsAndOils,
30 | ...bakedProducts,
31 | ...saucesAndSoups,
32 | ...spicesAndHerbs,
33 | ...sweetsAndSnacks,
34 | ...beverages,
35 | ]
36 |
37 | export default foods
38 |
--------------------------------------------------------------------------------
/src/meals/MealsList/EmptyList.tsx:
--------------------------------------------------------------------------------
1 | import { Text, Center, chakra, Button, VStack } from '@chakra-ui/react'
2 | import { Plus } from 'react-feather'
3 |
4 | const PlusStyled = chakra(Plus)
5 |
6 | type Props = {
7 | onAddMeal: () => void
8 | }
9 |
10 | function EmptyList({ onAddMeal }: Props) {
11 | return (
12 |
21 |
22 |
23 | You haven't added any meals to this day yet
24 |
25 |
26 | Days can be specific weekdays or just types of days. For example: a
27 | training or a rest day.
28 |
29 | }
36 | >
37 | Add meal
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default EmptyList
45 |
--------------------------------------------------------------------------------
/src/portions/getIngredientPortionDescription.ts:
--------------------------------------------------------------------------------
1 | import { Food, FoodId } from 'foods'
2 | import { IngredientForm } from 'ingredients'
3 | import amountAsNumber from 'stats/amountAsNumber'
4 | import { Portion } from './types'
5 | import getAmountFromPortionToGrams from './getAmountFromPortionsToGrams'
6 |
7 | const UNITS_WITH_DISTANCE = ['oz', 'tsp', 'tbsp', 'fl oz', 'cup']
8 |
9 | function getIngredientPortionDescription(
10 | ingredientForm: IngredientForm,
11 | foodsById: Record,
12 | portionsById: Record
13 | ) {
14 | const { portionId, foodId, amount } = ingredientForm
15 | const portion = portionsById[portionId]
16 |
17 | const distance = UNITS_WITH_DISTANCE.includes(portion.unit) ? ' ' : ''
18 | const mainPart = `${amount || 0}${distance}${portion.unit}`
19 | const { gramsPerAmount, millilitersPerAmount } = portion
20 |
21 | if (gramsPerAmount !== 1 || millilitersPerAmount) {
22 | const weightInGrams = Math.round(
23 | getAmountFromPortionToGrams(
24 | amountAsNumber(amount),
25 | portion.id,
26 | foodsById[foodId],
27 | portionsById
28 | )
29 | )
30 |
31 | return `${mainPart} (${weightInGrams}g)`
32 | }
33 |
34 | return mainPart
35 | }
36 |
37 | export default getIngredientPortionDescription
38 |
--------------------------------------------------------------------------------
/src/dom/useScrollTo.ts:
--------------------------------------------------------------------------------
1 | import { useEffect, useRef, useCallback } from 'react'
2 |
3 | function useScrollTo() {
4 | const scrollTimeoutRef = useRef()
5 | const didScrollCheckTimeoutRef = useRef()
6 |
7 | useEffect(() => {
8 | return () => {
9 | window.clearTimeout(scrollTimeoutRef.current)
10 | }
11 | }, [])
12 |
13 | const scrollTo = useCallback((node: HTMLElement) => {
14 | return new Promise(resolve => {
15 | const resolveWihoutScrollTimeout = window.setTimeout(() => {
16 | window.clearTimeout(didScrollCheckTimeoutRef.current)
17 |
18 | window.removeEventListener('scroll', listener)
19 | resolve()
20 | }, 100)
21 |
22 | function listener() {
23 | window.clearTimeout(scrollTimeoutRef.current)
24 | window.clearTimeout(resolveWihoutScrollTimeout)
25 |
26 | scrollTimeoutRef.current = window.setTimeout(() => {
27 | window.removeEventListener('scroll', listener)
28 | resolve()
29 | }, 50)
30 | }
31 |
32 | window.addEventListener('scroll', listener)
33 |
34 | node.scrollIntoView({
35 | behavior: 'smooth',
36 | block: 'center',
37 | })
38 | })
39 | }, [])
40 |
41 | return scrollTo
42 | }
43 |
44 | export default useScrollTo
45 |
--------------------------------------------------------------------------------
/src/stats/PdfStatsLayout.tsx:
--------------------------------------------------------------------------------
1 | import { View, StyleSheet } from '@react-pdf/renderer'
2 | import { ReactElement } from 'react'
3 |
4 | type Props = {
5 | nameElement: ReactElement
6 |
7 | energyElement: ReactElement
8 | proteinElement: ReactElement
9 | carbsElement: ReactElement
10 | fatElement: ReactElement
11 | }
12 |
13 | function PdfStatsLayout({
14 | nameElement,
15 |
16 | energyElement,
17 | proteinElement,
18 | carbsElement,
19 | fatElement,
20 | }: Props) {
21 | return (
22 |
23 | {nameElement}
24 | {energyElement}
25 | {proteinElement}
26 | {carbsElement}
27 | {fatElement}
28 |
29 | )
30 | }
31 |
32 | const NAME_WIDTH = '50%'
33 | const MACROS_COUNT = 4
34 | const MACRO_WIDTH = `${(100 - parseInt(NAME_WIDTH, 10)) / MACROS_COUNT}%`
35 |
36 | const styles = StyleSheet.create({
37 | root: {
38 | flexDirection: 'row',
39 | },
40 | name: {
41 | width: NAME_WIDTH,
42 | justifyContent: 'center',
43 | },
44 | macro: {
45 | width: MACRO_WIDTH,
46 | marginRight: 12,
47 | },
48 | })
49 |
50 | export default PdfStatsLayout
51 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalOverlay } from '@chakra-ui/react'
2 | import { Food } from 'foods'
3 | import { useRef } from 'react'
4 | import Content from './Content'
5 |
6 | type Props = {
7 | onClose: () => void
8 | isOpen: boolean
9 | food?: Food
10 | onFoodCreatedOrUpdated?: (newFood: Food, oldFood?: Food) => void
11 | onFoodDeleted?: (food: Food) => void
12 | }
13 |
14 | function FoodModal({
15 | onClose,
16 | isOpen,
17 | food,
18 | onFoodCreatedOrUpdated,
19 | onFoodDeleted,
20 | }: Props) {
21 | const nameInputRef = useRef(null)
22 | const title = food ? 'Food Details' : 'Create Food'
23 |
24 | return (
25 |
33 |
34 |
35 |
43 |
44 | )
45 | }
46 |
47 | export type { Props }
48 |
49 | export default FoodModal
50 |
--------------------------------------------------------------------------------
/src/general/MenuOrDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenSize, ScreenSize } from 'general'
2 | import Drawer from './Drawer'
3 | import Trigger from './Trigger'
4 | import { useDisclosure, IconButtonProps } from '@chakra-ui/react'
5 | import Menu from './Menu'
6 | import { ReactElement } from 'react'
7 |
8 | type Props = {
9 | children: ReactElement | ReactElement[]
10 | title: string
11 | } & IconButtonProps
12 |
13 | function MenuOrDrawer({ children, title, ...rest }: Props) {
14 | const screenSize = useScreenSize()
15 | const modalDisclosure = useDisclosure()
16 |
17 | if (screenSize < ScreenSize.Medium) {
18 | return (
19 | <>
20 |
21 |
26 | {children}
27 |
28 | >
29 | )
30 | }
31 |
32 | return (
33 |
36 | )
37 | }
38 |
39 | export { default as MenuOrDrawerItem } from './MenuOrDrawerItem'
40 | export { default as MenuOrDrawerSeparator } from './MenuOrDrawerSeparator'
41 | export * from './Menu'
42 | export * from './Drawer'
43 |
44 | export default MenuOrDrawer
45 |
--------------------------------------------------------------------------------
/src/stats/objectFromNutritionDataKeys.ts:
--------------------------------------------------------------------------------
1 | import { NutritionData } from './types'
2 |
3 | type MappedNutritionData = { [k in keyof NutritionData]: T }
4 |
5 | const NUTRITION_STATS_KEYS: (keyof NutritionData)[] = [
6 | 'protein',
7 | 'carbs',
8 | 'fat',
9 | 'saturatedFat',
10 | 'monounsaturatedFat',
11 | 'polyunsaturatedFat',
12 | 'energy',
13 | 'sugar',
14 | 'fiber',
15 | 'sodium',
16 | 'cholesterol',
17 |
18 | 'vitaminA',
19 | 'vitaminD',
20 | 'vitaminE',
21 | 'vitaminK',
22 | 'vitaminB1',
23 | 'vitaminB12',
24 | 'vitaminB2',
25 | 'vitaminB5',
26 | 'vitaminB6',
27 | 'vitaminB3',
28 | 'vitaminB9',
29 | 'vitaminC',
30 | 'vitaminD',
31 | 'vitaminE',
32 | 'vitaminK',
33 |
34 | 'magnesium',
35 | 'calcium',
36 | 'phosphorus',
37 | 'potassium',
38 | 'iron',
39 | 'selenium',
40 | 'zinc',
41 | 'manganese',
42 | 'copper',
43 | 'choline',
44 | ]
45 |
46 | function objectFromNutritionDataKeys(
47 | f: (key: keyof NutritionData) => T
48 | ): MappedNutritionData {
49 | const entries = NUTRITION_STATS_KEYS.map(key => {
50 | return [key, f(key)]
51 | })
52 |
53 | return Object.fromEntries(entries)
54 | }
55 |
56 | export { NUTRITION_STATS_KEYS }
57 |
58 | export type { MappedNutritionData }
59 |
60 | export default objectFromNutritionDataKeys
61 |
--------------------------------------------------------------------------------
/src/notes/EditNotesModal/index.tsx:
--------------------------------------------------------------------------------
1 | import { Modal, ModalOverlay, ModalProps } from '@chakra-ui/react'
2 | import { useRef } from 'react'
3 | import Content from './Content'
4 |
5 | type Props = {
6 | onClose: () => void
7 | isOpen: boolean
8 | notes?: string
9 | onEditNotes: (notes?: string) => void
10 | fieldId: string
11 | ownerName: string
12 | textAreaHeight?: string | number
13 | } & Omit
14 |
15 | function EditNotesModal({
16 | onClose,
17 | isOpen,
18 | notes,
19 | onEditNotes,
20 | fieldId,
21 | ownerName,
22 | textAreaHeight,
23 | ...rest
24 | }: Props) {
25 | const initialRef = useRef(null)
26 | const finalFocusRef = useRef(null)
27 |
28 | return (
29 |
37 |
38 |
39 |
48 |
49 | )
50 | }
51 |
52 | export default EditNotesModal
53 |
--------------------------------------------------------------------------------
/src/general/Tooltip.tsx:
--------------------------------------------------------------------------------
1 | import { ReactElement, useState, cloneElement, useRef, ReactNode } from 'react'
2 | import { Tooltip as TooltipBase } from '@chakra-ui/react'
3 |
4 | type Props = {
5 | children: ReactElement
6 | delay?: number
7 | label?: ReactNode
8 | isActive?: boolean
9 | }
10 |
11 | function Tooltip({ children, label, isActive = true, delay = 500 }: Props) {
12 | const [isHovered, setIsHovered] = useState(false)
13 | const timeoutIdRef = useRef()
14 |
15 | function onMouseEnter() {
16 | window.clearTimeout(timeoutIdRef.current)
17 | timeoutIdRef.current = window.setTimeout(() => setIsHovered(true), delay)
18 | }
19 |
20 | function hideTooltip() {
21 | window.clearTimeout(timeoutIdRef.current)
22 | setIsHovered(false)
23 | }
24 |
25 | function onMouseLeave() {
26 | hideTooltip()
27 | }
28 |
29 | function onClick(...rest: any) {
30 | children.props.onClick && children.props.onClick(...rest)
31 | hideTooltip()
32 | }
33 |
34 | if (!isActive) {
35 | return children
36 | }
37 |
38 | return (
39 |
40 | {cloneElement(children, {
41 | onMouseEnter,
42 | onMouseLeave,
43 | onClick,
44 | })}
45 |
46 | )
47 | }
48 |
49 | export default Tooltip
50 |
--------------------------------------------------------------------------------
/src/layout/MainLayout.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | createContext,
3 | ReactElement,
4 | ReactNode,
5 | RefObject,
6 | useRef,
7 | } from 'react'
8 | import { Box } from '@chakra-ui/react'
9 | import useHasSideNavigation from './useHasSideNavigation'
10 |
11 | export type MainLayoutProps = {
12 | sidebarElement?: ReactElement
13 | children: ReactNode
14 | }
15 |
16 | const ContentBoxRefContext = createContext>({
17 | current: null,
18 | })
19 |
20 | function MainLayout({ children }: MainLayoutProps) {
21 | const hasSideNavigation = useHasSideNavigation()
22 | const contentBoxRef = useRef(null)
23 |
24 | return (
25 |
26 | {hasSideNavigation && (
27 |
34 | )}
35 |
36 |
37 |
42 | {children}
43 |
44 |
45 |
46 | )
47 | }
48 |
49 | export { ContentBoxRefContext }
50 |
51 | export default MainLayout
52 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealItem/useGetAndUpdateStats.ts:
--------------------------------------------------------------------------------
1 | import { MealForm } from 'meals'
2 | import { useGetIngredientFormStatsTree } from 'ingredients'
3 | import { getStatsTree, useUpdateMealStats } from 'stats'
4 | import { useMemo } from 'react'
5 |
6 | type Params = {
7 | mealForm: MealForm
8 | index: number
9 | selectedVariantFormFieldId: string
10 | }
11 |
12 | function useGetAndUpdateStats({
13 | mealForm,
14 | index,
15 | selectedVariantFormFieldId,
16 | }: Params) {
17 | const getIngredientFormStatsTree = useGetIngredientFormStatsTree()
18 |
19 | const mealFormStatsTree = useMemo(
20 | () =>
21 | getStatsTree({
22 | id: mealForm.fieldId,
23 | subtrees: mealForm.ingredientsForms.map(ingredientForm =>
24 | getIngredientFormStatsTree(ingredientForm)
25 | ),
26 | }),
27 | [mealForm.fieldId, mealForm.ingredientsForms, getIngredientFormStatsTree]
28 | )
29 |
30 | const ingredientsStats = useMemo(
31 | () => mealFormStatsTree.subtrees.map(({ stats }) => stats),
32 | [mealFormStatsTree]
33 | )
34 |
35 | useUpdateMealStats({
36 | stats: mealFormStatsTree.stats,
37 | selectedVariantFormFieldId,
38 | index,
39 | })
40 |
41 | return {
42 | ingredientsStats,
43 | mealFormStatsTree,
44 | }
45 | }
46 |
47 | export default useGetAndUpdateStats
48 |
--------------------------------------------------------------------------------
/src/variants/PdfVariantsList/index.tsx:
--------------------------------------------------------------------------------
1 | import { Food } from 'foods'
2 | import { Portion } from 'portions'
3 | import { ReactElement } from 'react'
4 | import { StatsTree } from 'stats'
5 | import { VariantForm } from 'variants/variantForm'
6 | import PdfVariantItem from './PdfVariantItem'
7 |
8 | type Props = {
9 | variantsForms: VariantForm[]
10 | variantsFormsStatsTrees: StatsTree[]
11 | foodsById: Record
12 | portionsById: Record
13 | }
14 |
15 | function PdfVariantsList({
16 | variantsForms,
17 | variantsFormsStatsTrees,
18 | foodsById,
19 | portionsById,
20 | }: Props) {
21 | const variantItemsElements: ReactElement[] = []
22 |
23 | variantsForms.forEach((variantForm, index) => {
24 | const { mealsForms } = variantForm
25 | const { stats, subtrees } = variantsFormsStatsTrees[index]
26 |
27 | if (mealsForms.length > 0) {
28 | variantItemsElements.push(
29 |
38 | )
39 | }
40 | })
41 |
42 | return variantItemsElements
43 | }
44 |
45 | export default PdfVariantsList
46 |
--------------------------------------------------------------------------------
/src/meals/MealsList/MealItem/Header/Name.tsx:
--------------------------------------------------------------------------------
1 | import { BoxProps, Input, Flex } from '@chakra-ui/react'
2 | import { useDietFormActions } from 'diets'
3 | import { MealForm } from 'meals'
4 | import { RefObject, ChangeEvent } from 'react'
5 |
6 | type Props = {
7 | variantIndex: number
8 | mealForm: MealForm
9 | index: number
10 | getMealNameInputRefById: (id: string) => RefObject
11 | } & BoxProps
12 |
13 | function Name({
14 | variantIndex,
15 | mealForm,
16 | index,
17 | getMealNameInputRefById,
18 | ...rest
19 | }: Props) {
20 | const dietFormActions = useDietFormActions()
21 |
22 | function onNameChange(event: ChangeEvent) {
23 | const { value } = event.target
24 |
25 | dietFormActions.updateMealForm(variantIndex, index, {
26 | name: value,
27 | })
28 | }
29 |
30 | return (
31 |
32 |
45 |
46 | )
47 | }
48 |
49 | export default Name
50 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/Popover.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Popover as PopoverBase,
3 | PopoverTrigger,
4 | PopoverContent,
5 | PopoverHeader,
6 | PopoverBody,
7 | PopoverFooter,
8 | PopoverArrow,
9 | PopoverCloseButton,
10 | } from '@chakra-ui/react'
11 | import { useRef } from 'react'
12 | import Content from './Content'
13 | import Trigger from './Trigger'
14 | import Footer from './Footer'
15 |
16 | function Popover() {
17 | const selectRef = useRef(null)
18 |
19 | return (
20 |
21 | {({ onClose }) => {
22 | return (
23 | <>
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | Filters
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | >
42 | )
43 | }}
44 |
45 | )
46 | }
47 |
48 | export default Popover
49 |
--------------------------------------------------------------------------------
/src/foods/FoodsDrawer/Content/useFoodEvents.ts:
--------------------------------------------------------------------------------
1 | import { useDisclosure } from '@chakra-ui/react'
2 | import { Food } from 'foods'
3 | import { useState, RefObject } from 'react'
4 | import { FoodsListMethods } from 'foods'
5 | import { Selection } from 'general'
6 |
7 | type Params = {
8 | listRef: RefObject
9 | selection: Selection
10 | }
11 |
12 | function useFoodEvents({ listRef, selection }: Params) {
13 | const foodModalDisclosure = useDisclosure()
14 | const [food, setFood] = useState()
15 |
16 | function onCreateFood() {
17 | setFood(undefined)
18 | foodModalDisclosure.onOpen()
19 | }
20 |
21 | function onPreviewFood(food: Food) {
22 | setFood(food)
23 | foodModalDisclosure.onOpen()
24 | }
25 |
26 | function onFoodDeleted(food: Food) {
27 | selection.removeItem(food)
28 | }
29 |
30 | function onFoodCreatedOrUpdated(newFood: Food, oldFood?: Food) {
31 | if (!listRef.current) {
32 | return
33 | }
34 |
35 | if (!oldFood || (oldFood && newFood.categoryId !== oldFood.categoryId)) {
36 | listRef.current.scrollToFood(newFood)
37 | }
38 | }
39 |
40 | return {
41 | onCreateFood,
42 | onPreviewFood,
43 | onFoodCreatedOrUpdated,
44 | food,
45 | foodModalDisclosure,
46 | onFoodDeleted,
47 | }
48 | }
49 |
50 | export default useFoodEvents
51 |
--------------------------------------------------------------------------------
/src/ingredients/IngredientsList/IngredientItem/getMenuOrDrawerItems.tsx:
--------------------------------------------------------------------------------
1 | import { chakra } from '@chakra-ui/react'
2 | import { IngredientForm } from 'ingredients'
3 | import { Trash2, Info, Edit } from 'react-feather'
4 | import { MenuOrDrawerItem, MenuOrDrawerSeparator } from 'general'
5 |
6 | const InfoStyled = chakra(Info)
7 | const Trash2Styled = chakra(Trash2)
8 | const EditStyled = chakra(Edit)
9 |
10 | type Props = {
11 | onEditNotes: () => void
12 | onRemove: () => void
13 | onViewFoodDetails: () => void
14 | ingredientForm: IngredientForm
15 | }
16 |
17 | function getMenuOrDrawerItems({
18 | ingredientForm,
19 | onRemove,
20 | onViewFoodDetails,
21 | onEditNotes,
22 | }: Props) {
23 | return [
24 | }
27 | onClick={onViewFoodDetails}
28 | >
29 | View details
30 | ,
31 | ,
32 | }
35 | onClick={onEditNotes}
36 | >
37 | {ingredientForm.notes ? 'Edit notes' : 'Add notes'}
38 | ,
39 | } onClick={onRemove}>
40 | Remove
41 | ,
42 | ]
43 | }
44 |
45 | export default getMenuOrDrawerItems
46 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/DeleteConfirmationModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalOverlay,
4 | ModalContent,
5 | ModalHeader,
6 | ModalFooter,
7 | ModalBody,
8 | ModalCloseButton,
9 | Button,
10 | Text,
11 | } from '@chakra-ui/react'
12 |
13 | type Props = {
14 | isOpen: boolean
15 | onCancel: () => void
16 | onConfirm: () => void
17 | text: string
18 | confirmButtonLabel: String
19 | }
20 |
21 | function DeleteConfirmationModal({
22 | isOpen,
23 | onCancel,
24 | onConfirm,
25 | text,
26 | confirmButtonLabel,
27 | }: Props) {
28 | return (
29 |
30 |
31 |
32 | Delete food
33 |
34 |
35 | {text}
36 |
37 | This action cannot be undone.
38 |
39 |
40 |
41 |
44 |
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | export default DeleteConfirmationModal
54 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantNameModal/VariantNameFormProvider.tsx:
--------------------------------------------------------------------------------
1 | import { FormProvider, useForm } from 'react-hook-form'
2 | import { yupResolver } from '@hookform/resolvers/yup'
3 | import { ReactNode } from 'react'
4 | import { useDietForm } from 'diets'
5 | import {
6 | getVariantNameForm,
7 | VariantNameForm,
8 | variantNameFormSchema,
9 | } from './variantNameForm'
10 | import { VariantNameFormSchemaContext } from './variantNameForm'
11 |
12 | type Props = {
13 | children: ReactNode
14 | variantFormIndex?: number
15 | }
16 |
17 | function VariantNameFormProvider({ children, variantFormIndex }: Props) {
18 | const dietForm = useDietForm()
19 | const { variantsForms } = dietForm
20 | const variantForm =
21 | variantFormIndex !== undefined ? variantsForms[variantFormIndex] : undefined
22 |
23 | const defaultValues = getVariantNameForm(
24 | variantFormIndex !== undefined
25 | ? dietForm.variantsForms[variantFormIndex].name
26 | : ''
27 | )
28 |
29 | const formMethods = useForm({
30 | defaultValues,
31 | mode: 'onChange',
32 | context: {
33 | variantsForms,
34 | variantForm,
35 | },
36 | resolver: yupResolver(variantNameFormSchema),
37 | })
38 |
39 | return {children}
40 | }
41 |
42 | export default VariantNameFormProvider
43 |
--------------------------------------------------------------------------------
/src/diets/persistence/ExportModal/Content/Exporter/index.tsx:
--------------------------------------------------------------------------------
1 | import { Loader } from 'general'
2 | import {
3 | Alert,
4 | AlertIcon,
5 | AlertTitle,
6 | AlertDescription,
7 | } from '@chakra-ui/react'
8 | import usePdfExport from './usePdfExport'
9 |
10 | type Props = {
11 | onUpdate: (blob: Blob, url: string) => void
12 | }
13 |
14 | function Exporter({ onUpdate }: Props) {
15 | const { isLoading, error } = usePdfExport({ onUpdate })
16 |
17 | if (isLoading) {
18 | return
19 | }
20 |
21 | return (
22 |
32 |
33 |
34 | {error
35 | ? 'Something went wrong while creating your pdf file'
36 | : 'Your PDF file is ready'}
37 |
38 | {!error && (
39 |
40 | Downloading this plan will allow you to import it later if you need to
41 | update it.
42 |
43 | )}
44 |
45 | )
46 | }
47 |
48 | export default Exporter
49 |
--------------------------------------------------------------------------------
/src/undoRedo/useKeyboard.ts:
--------------------------------------------------------------------------------
1 | import { useContext, useEffect } from 'react'
2 | import { ContentBoxRefContext } from 'layout'
3 | import { useDietFormVersionsActions } from './useDietFormVersionsStore'
4 |
5 | function useKeyboard() {
6 | const formVersionsActions = useDietFormVersionsActions()
7 | const contentBoxRef = useContext(ContentBoxRefContext)
8 | const node = contentBoxRef.current
9 |
10 | useEffect(() => {
11 | if (!node) {
12 | return
13 | }
14 |
15 | function onNodeKeyDown(event: KeyboardEvent) {
16 | const { ctrlKey, metaKey, shiftKey, code } = event
17 |
18 | if (code === 'KeyZ' && (ctrlKey || metaKey)) {
19 | event.preventDefault()
20 |
21 | if (shiftKey) {
22 | formVersionsActions.redo()
23 | } else {
24 | formVersionsActions.undo()
25 | }
26 | }
27 | }
28 |
29 | function onBodyKeyDown(event: KeyboardEvent) {
30 | if (event.target === document.body) {
31 | onNodeKeyDown(event)
32 | }
33 | }
34 |
35 | node.addEventListener('keydown', onNodeKeyDown)
36 | document.body.addEventListener('keydown', onBodyKeyDown)
37 |
38 | return () => {
39 | node.removeEventListener('keydown', onNodeKeyDown)
40 | document.body.removeEventListener('keydown', onBodyKeyDown)
41 | }
42 | }, [formVersionsActions, node])
43 | }
44 |
45 | export default useKeyboard
46 |
--------------------------------------------------------------------------------
/src/foods/FoodModal/Content/Form/Tabs/NutritionFactsFormFields.tsx:
--------------------------------------------------------------------------------
1 | import { Box, FlexProps, VStack } from '@chakra-ui/react'
2 | import { RefObject } from 'react'
3 | import { StatsFormFields, StatFormField } from 'stats'
4 |
5 | type Props = {
6 | nameInputRef: RefObject
7 | canEdit: boolean
8 | } & FlexProps
9 |
10 | function NutritionFactsFormFields({ nameInputRef, canEdit, ...rest }: Props) {
11 | return (
12 |
13 |
14 |
15 |
24 |
25 |
36 |
37 |
38 |
39 |
40 |
41 | )
42 | }
43 |
44 | export default NutritionFactsFormFields
45 |
--------------------------------------------------------------------------------
/src/portions/PortionsMenuOrDrawer/Drawer/PortionItem.tsx:
--------------------------------------------------------------------------------
1 | import { Box, BoxProps, Flex, Text } from '@chakra-ui/react'
2 | import { Portion, getPortionDescription } from 'portions'
3 | import { Check } from 'react-feather'
4 |
5 | type Props = {
6 | portion: Portion
7 | isSelected: boolean
8 | } & BoxProps
9 |
10 | function PortionItem({ portion, isSelected, ...rest }: Props) {
11 | const { unit } = portion
12 |
13 | return (
14 |
29 |
30 |
35 | {unit}{' '}
36 |
37 | {getPortionDescription(portion)}
38 |
39 |
40 | {isSelected && (
41 |
42 |
43 |
44 | )}
45 |
46 |
47 | )
48 | }
49 |
50 | export default PortionItem
51 |
--------------------------------------------------------------------------------
/src/stats/EnergyStat.tsx:
--------------------------------------------------------------------------------
1 | import Stat, { StatProps } from './Stat'
2 | import { ArrowUpCircle, ArrowDownCircle } from 'react-feather'
3 | import { useSameOrPreviousValue } from 'general'
4 | import StatValueDetail from './StatValueDetail'
5 |
6 | type Props = {
7 | energy: number
8 | energyDiff: number
9 | } & StatProps
10 |
11 | function EnergyStat({ energy, energyDiff, ...rest }: Props) {
12 | const energyValueDetail = `${Math.abs(energyDiff)}kcal`
13 | const previousOrSameEnergyValueDetail = useSameOrPreviousValue(
14 | energyValueDetail
15 | )
16 |
17 | return (
18 | 0 ? (
34 |
35 | ) : (
36 |
37 | )
38 | }
39 | />
40 | ) : undefined
41 | }
42 | {...rest}
43 | />
44 | )
45 | }
46 |
47 | export default EnergyStat
48 |
--------------------------------------------------------------------------------
/src/general/index.ts:
--------------------------------------------------------------------------------
1 | export { default as HFadeScroll } from './HFadeScroll'
2 | export * from './HFadeScroll'
3 | export { default as Badge } from './Badge'
4 | export { default as deepCopy } from './deepCopy'
5 | export { default as Menu } from './Menu'
6 | export * from './Menu'
7 | export { default as ResponsiveButton } from './ResponsiveButton'
8 | export { default as ResponsiveIconButton } from './ResponsiveIconButton'
9 | export { default as RightAligned } from './RightAligned'
10 | export * from './stores'
11 | export { default as useSelection } from './useSelection'
12 | export * from './useSelection'
13 | export { default as useSameOrPreviousValue } from './useSameOrPreviousValue'
14 | export { default as ContextMenuFlex } from './ContextMenuFlex'
15 | export { default as Loader } from './Loader'
16 | export { default as useElementHeight } from './useElementHeight'
17 | export { default as Tooltip } from './Tooltip'
18 | export { default as getCtrlKeyName } from './getCtrlKeyName'
19 | export { default as TooltipCommandLabel } from './TooltipCommandLabel'
20 | export { default as ScreenSizeProvider } from './ScreenSizeProvider'
21 | export * from './ScreenSizeProvider'
22 | export * from './useOneTimeCheckStore'
23 | export { default as MenuOrDrawer } from './MenuOrDrawer'
24 | export * from './MenuOrDrawer'
25 | export { default as minDelay } from './minDelay'
26 | export { default as useRunIfNotUnmounted } from './useRunIfNotUnmounted'
27 |
--------------------------------------------------------------------------------
/src/foods/FoodInfo.tsx:
--------------------------------------------------------------------------------
1 | import { Text, Box, Flex, BoxProps, Link } from '@chakra-ui/react'
2 | import { Food } from 'foods'
3 | import { ReactNode } from 'react'
4 | import { DEFAULT_SERVING_SIZE_IN_GRAMS } from './foodForm'
5 |
6 | type Props = {
7 | food: Food
8 | nameNoOfLines?: number
9 | canBeLink?: boolean
10 | notes?: string
11 |
12 | energy?: number
13 | children?: ReactNode
14 | } & BoxProps
15 |
16 | function FoodInfo({
17 | food,
18 |
19 | fontSize,
20 | nameNoOfLines,
21 | energy,
22 | notes,
23 | children,
24 | canBeLink = false,
25 | ...rest
26 | }: Props) {
27 | return (
28 |
29 |
30 | {food.url && canBeLink ? (
31 |
32 | {food.name}
33 |
34 | ) : (
35 |
36 | {food.name}
37 |
38 | )}
39 |
40 | {energy !== undefined && (
41 |
42 |
43 | {`${Math.round(energy as number)}kcal`}
44 | {' '}
45 | / {food.servingSizeInGrams || DEFAULT_SERVING_SIZE_IN_GRAMS}g
46 |
47 | )}
48 |
49 | {children}
50 |
51 |
52 | )
53 | }
54 |
55 | export default FoodInfo
56 |
--------------------------------------------------------------------------------
/src/persistence/useImportFileError.ts:
--------------------------------------------------------------------------------
1 | import { useToast, UseToastOptions } from '@chakra-ui/toast'
2 |
3 | const COMMON_TOAST_OPTIONS: UseToastOptions = {
4 | isClosable: true,
5 | position: 'top',
6 | duration: null,
7 | status: 'error',
8 | }
9 |
10 | type OnErrorParams = {
11 | file: File
12 | error: any
13 | }
14 |
15 | function useFileImportError() {
16 | const toast = useToast()
17 |
18 | function showCouldNotLoadFileToast(file: File) {
19 | toast({
20 | ...COMMON_TOAST_OPTIONS,
21 | title: `File ${file.name} could not be loaded`,
22 | })
23 | }
24 |
25 | function showCouldNotParseFileToast(file: File) {
26 | toast({
27 | ...COMMON_TOAST_OPTIONS,
28 | title: `File ${file.name} contains invalid data`,
29 | })
30 | }
31 |
32 | function showErrorToast(file: File, error: any) {
33 | const { message } = error
34 |
35 | toast({
36 | ...COMMON_TOAST_OPTIONS,
37 | title: `File ${file.name} could not be imported`,
38 | description: message,
39 | })
40 | }
41 |
42 | function onError({ error, file }: OnErrorParams) {
43 | if (error instanceof DOMException) {
44 | showCouldNotLoadFileToast(file)
45 | } else if (error instanceof SyntaxError) {
46 | showCouldNotParseFileToast(file)
47 | } else {
48 | showErrorToast(file, error)
49 | }
50 | }
51 |
52 | return {
53 | onError,
54 | }
55 | }
56 |
57 | export default useFileImportError
58 |
--------------------------------------------------------------------------------
/src/stats/statsVariants.ts:
--------------------------------------------------------------------------------
1 | type StatVariant =
2 | | 'ingredient'
3 | | 'ingredientAmount'
4 | | 'ingredientEnergy'
5 | | 'meal'
6 | | 'mealEnergy'
7 | | 'diet'
8 | | 'dietEnergy'
9 |
10 | function isForDiet(statVariant: StatVariant) {
11 | return statVariant.startsWith('diet')
12 | }
13 |
14 | function isForEnergy(statVariant: StatVariant) {
15 | return statVariant.endsWith('Energy')
16 | }
17 |
18 | function isForIngredient(statVaraint: StatVariant) {
19 | return statVaraint.startsWith('ingredient')
20 | }
21 |
22 | function isForMeal(statVaraint: StatVariant) {
23 | return statVaraint.startsWith('meal')
24 | }
25 |
26 | function getValueTextColor(statVariant: StatVariant) {
27 | if (isForIngredient(statVariant)) {
28 | return 'gray.500'
29 | }
30 |
31 | return 'gray.800'
32 | }
33 |
34 | function getValueFontWeight(statVariant: StatVariant) {
35 | if (statVariant === 'dietEnergy') {
36 | return 'bold'
37 | }
38 |
39 | if (statVariant === 'diet' || statVariant === 'mealEnergy') {
40 | return 'medium'
41 | }
42 |
43 | return undefined
44 | }
45 |
46 | function getLabelColor(statVariant: StatVariant) {
47 | if (isForDiet(statVariant)) {
48 | return 'gray.800'
49 | }
50 |
51 | return 'gray.500'
52 | }
53 |
54 | export {
55 | getValueTextColor,
56 | getValueFontWeight,
57 | isForDiet,
58 | isForEnergy,
59 | isForIngredient,
60 | isForMeal,
61 | getLabelColor,
62 | }
63 |
64 | export type { StatVariant }
65 |
--------------------------------------------------------------------------------
/src/undoRedo/UndoRedoButtons/RedoButton.tsx:
--------------------------------------------------------------------------------
1 | import { chakra, IconButton, Button } from '@chakra-ui/react'
2 | import { useDietFormVersionsActions, useDietFormVersions } from 'undoRedo'
3 | import { RotateCw } from 'react-feather'
4 | import {
5 | getCtrlKeyName,
6 | TooltipCommandLabel,
7 | Tooltip,
8 | useScreenSize,
9 | ScreenSize,
10 | } from 'general'
11 |
12 | const RotateCwStyled = chakra(RotateCw)
13 | const ctrlKeyName = getCtrlKeyName()
14 |
15 | function RedoButton() {
16 | const { redo } = useDietFormVersionsActions()
17 | const { canRedo } = useDietFormVersions()
18 | const screenSize = useScreenSize()
19 |
20 | if (screenSize >= ScreenSize.Medium) {
21 | return (
22 |
28 | }
29 | >
30 | }
33 | isDisabled={!canRedo}
34 | onClick={() => redo()}
35 | >
36 | Redo
37 |
38 |
39 | )
40 | }
41 |
42 | return (
43 | }
47 | isDisabled={!canRedo}
48 | onClick={() => redo()}
49 | />
50 | )
51 | }
52 |
53 | export default RedoButton
54 |
--------------------------------------------------------------------------------
/src/undoRedo/UndoRedoButtons/UndoButton.tsx:
--------------------------------------------------------------------------------
1 | import { chakra, IconButton, Button } from '@chakra-ui/react'
2 | import { useDietFormVersionsActions, useDietFormVersions } from 'undoRedo'
3 | import { RotateCcw } from 'react-feather'
4 | import {
5 | getCtrlKeyName,
6 | TooltipCommandLabel,
7 | Tooltip,
8 | useScreenSize,
9 | ScreenSize,
10 | } from 'general'
11 |
12 | const RotateCcwStyled = chakra(RotateCcw)
13 | const ctrlKeyName = getCtrlKeyName()
14 |
15 | function UndoButton() {
16 | const { undo } = useDietFormVersionsActions()
17 | const { canUndo } = useDietFormVersions()
18 | const screenSize = useScreenSize()
19 |
20 | if (screenSize >= ScreenSize.Medium) {
21 | return (
22 |
28 | }
29 | >
30 | }
33 | isDisabled={!canUndo}
34 | onClick={() => undo()}
35 | >
36 | Undo
37 |
38 |
39 | )
40 | }
41 |
42 | return (
43 | }
47 | isDisabled={!canUndo}
48 | onClick={() => undo()}
49 | />
50 | )
51 | }
52 |
53 | export default UndoButton
54 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantItem/getMenuOrDrawerItems.tsx:
--------------------------------------------------------------------------------
1 | import { chakra } from '@chakra-ui/react'
2 | import { Trash2, Edit, Copy, Info } from 'react-feather'
3 | import { MenuOrDrawerItem, MenuOrDrawerSeparator } from 'general'
4 |
5 | const Trash2Styled = chakra(Trash2)
6 | const EditStyled = chakra(Edit)
7 | const CopyStyled = chakra(Copy)
8 | const InfoStyled = chakra(Info)
9 |
10 | type Props = {
11 | onClone: () => void
12 | onEditName: () => void
13 | onDelete: () => void
14 | onViewDetails: () => void
15 | canRemove: boolean
16 | }
17 |
18 | function getMenuOrDrawerItems({
19 | onClone,
20 | onEditName,
21 | canRemove,
22 | onDelete,
23 | onViewDetails,
24 | }: Props) {
25 | return [
26 | } key="rename" onClick={onEditName}>
27 | Rename
28 | ,
29 |
30 | ,
31 |
32 | }
34 | key="viewDetails"
35 | onClick={onViewDetails}
36 | >
37 | View details
38 | ,
39 |
40 | } onClick={onClone}>
41 | Duplicate
42 | ,
43 |
44 | }
48 | onClick={onDelete}
49 | >
50 | Remove
51 | ,
52 | ]
53 | }
54 |
55 | export default getMenuOrDrawerItems
56 |
--------------------------------------------------------------------------------
/src/icons/CalendarPlus.tsx:
--------------------------------------------------------------------------------
1 | import { chakra } from '@chakra-ui/react'
2 |
3 | function CalendarPlus({ size = 24, ...rest }) {
4 | return (
5 |
50 | )
51 | }
52 |
53 | export default chakra(CalendarPlus)
54 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantNameModal/variantNameForm.ts:
--------------------------------------------------------------------------------
1 | import { VariantForm } from 'variants'
2 | import { object, string } from 'yup'
3 |
4 | type VariantNameForm = {
5 | name: string
6 | }
7 |
8 | function getVariantNameForm(name: string): VariantNameForm {
9 | return {
10 | name,
11 | }
12 | }
13 |
14 | type VariantNameFormSchemaContext = {
15 | variantsForms: VariantForm[]
16 | variantForm?: VariantForm
17 | }
18 |
19 | const variantNameFormSchema = object().shape({
20 | name: string()
21 | .required('Please add a name')
22 | .test(
23 | 'uniqueName',
24 | 'This name has already been used',
25 | (currentName, { options }) => {
26 | if (currentName === undefined) {
27 | return true
28 | }
29 |
30 | const {
31 | variantsForms,
32 | variantForm,
33 | } = options.context as VariantNameFormSchemaContext
34 |
35 | const sameVariantFormExists = variantsForms.some(
36 | ({ name, fieldId }) => {
37 | const haveSameNames =
38 | currentName.toLowerCase() === name.toLowerCase()
39 | return variantForm
40 | ? fieldId !== variantForm.fieldId && haveSameNames
41 | : haveSameNames
42 | }
43 | )
44 |
45 | return !sameVariantFormExists
46 | }
47 | ),
48 | })
49 |
50 | export { getVariantNameForm, variantNameFormSchema }
51 |
52 | export type { VariantNameForm, VariantNameFormSchemaContext }
53 |
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/StatFormField/ReadOnlyInput.tsx:
--------------------------------------------------------------------------------
1 | import { Text } from '@chakra-ui/react'
2 | import { Controller } from 'react-hook-form'
3 | import { foodCategories } from 'foods-categories'
4 | import { InputType } from './types'
5 |
6 | type Props = {
7 | name: string
8 | inputType: InputType
9 | nutritionValueUnit: string
10 | isBold?: boolean
11 | }
12 |
13 | function formatNutritionValue(value: string) {
14 | const number = Number(value)
15 |
16 | if (Number.isInteger(number)) {
17 | return value
18 | }
19 |
20 | return number.toFixed(2)
21 | }
22 |
23 | function ReadOnlyInput({
24 | name,
25 | inputType,
26 | nutritionValueUnit,
27 | isBold = false,
28 | }: Props) {
29 | return (
30 | {
33 | let { value } = field
34 |
35 | if (inputType === 'foodCategory') {
36 | const foodCategory = foodCategories.find(({ id }) => id === value)
37 | if (foodCategory) {
38 | value = foodCategory.name
39 | }
40 | }
41 |
42 | return (
43 |
47 | {inputType === 'nutritionValue'
48 | ? `${formatNutritionValue(value)}${nutritionValueUnit}`
49 | : value}
50 |
51 | )
52 | }}
53 | />
54 | )
55 | }
56 |
57 | export default ReadOnlyInput
58 |
--------------------------------------------------------------------------------
/src/foods/persistence/MissingFoodsModal.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Modal,
3 | ModalOverlay,
4 | ModalContent,
5 | ModalHeader,
6 | ModalFooter,
7 | ModalBody,
8 | ModalCloseButton,
9 | Button,
10 | Text,
11 | } from '@chakra-ui/react'
12 |
13 | type Props = {
14 | isOpen: boolean
15 | onClose: () => void
16 | onImport: () => void
17 | }
18 |
19 | function MissingFoodsModal({ isOpen, onClose, onImport }: Props) {
20 | return (
21 |
22 |
23 |
24 | Missing foods
25 |
26 |
27 |
28 | The meal plan you contains foods that are not part of your list.
29 |
30 |
31 |
32 | You can try to import the missing foods or continue without them.
33 |
34 |
35 |
36 |
37 |
40 |
41 |
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default MissingFoodsModal
58 |
--------------------------------------------------------------------------------
/src/diets/DietEditor/Form/useDietFormEvents.ts:
--------------------------------------------------------------------------------
1 | import { UseDisclosureReturn } from '@chakra-ui/hooks'
2 | import { DietForm, useDietFormActions, ScrollManager } from 'diets'
3 | import { Food } from 'foods'
4 | import { getIngredient } from 'ingredients'
5 | import { getMealForm } from 'meals'
6 | import { AppLocation } from 'undoRedo'
7 |
8 | type Params = {
9 | scrollManager: ScrollManager
10 | foodsDrawerDisclosure: UseDisclosureReturn
11 | }
12 |
13 | function useDietFormEvents({ scrollManager, foodsDrawerDisclosure }: Params) {
14 | const dietFormActions = useDietFormActions()
15 | const { setScrollState } = scrollManager
16 |
17 | function onMealAdded(foods: Food[], mealName?: string) {
18 | foodsDrawerDisclosure.onClose()
19 | const ingredients = foods.map(getIngredient)
20 | const mealForm = getMealForm({ name: mealName as string, ingredients })
21 | dietFormActions.appendMealForm(mealForm)
22 | }
23 |
24 | function onUndoOrRedo(
25 | form: DietForm,
26 | { scrollTop, scrollLeft, variantIndex }: AppLocation
27 | ) {
28 | const finalVariantIndex = form.variantsForms[variantIndex]
29 | ? variantIndex
30 | : form.selectedVariantFormIndex
31 |
32 | dietFormActions.updateDietForm({
33 | ...form,
34 | selectedVariantFormIndex: finalVariantIndex,
35 | })
36 |
37 | setScrollState({ top: scrollTop, left: scrollLeft })
38 | }
39 |
40 | return {
41 | onUndoOrRedo,
42 | onMealAdded,
43 | }
44 | }
45 |
46 | export default useDietFormEvents
47 |
--------------------------------------------------------------------------------
/src/foods-filters/useFilterFoods.ts:
--------------------------------------------------------------------------------
1 | import Fuse from 'fuse.js'
2 | import { useMemo } from 'react'
3 | import { Food } from 'foods'
4 | import { FoodsFilter } from './foodsFilter'
5 |
6 | const OPTIONS = { keys: ['name'] }
7 |
8 | function groupFoodsByCategoryId(foods: Food[]) {
9 | const foodsByCategoryIdMap: Record = {}
10 |
11 | for (const food of foods) {
12 | const { categoryId } = food
13 |
14 | if (!foodsByCategoryIdMap[categoryId]) {
15 | foodsByCategoryIdMap[categoryId] = []
16 | }
17 |
18 | foodsByCategoryIdMap[categoryId].push(food)
19 | }
20 |
21 | return foodsByCategoryIdMap
22 | }
23 |
24 | function useFilterFoods(
25 | allFoods: Food[],
26 | userFoods: Food[],
27 | filter: FoodsFilter
28 | ) {
29 | const foodsToFilter = filter.onlyFoodsAddedByUser ? userFoods : allFoods
30 |
31 | const fuse = useMemo(() => new Fuse(foodsToFilter, OPTIONS), [foodsToFilter])
32 | const foodsByCategoryId = useMemo(
33 | () => groupFoodsByCategoryId(foodsToFilter),
34 | [foodsToFilter]
35 | )
36 |
37 | const { query, categoryId } = filter
38 | if (!query) {
39 | return categoryId ? foodsByCategoryId[categoryId] || [] : foodsToFilter
40 | }
41 |
42 | const foodsForQuery = fuse.search(query, { limit: 5 }).map(({ item }) => item)
43 | if (!categoryId) {
44 | return foodsForQuery
45 | }
46 |
47 | return foodsForQuery.filter(food => food.categoryId === categoryId)
48 | }
49 |
50 | export type { FoodsFilter }
51 |
52 | export default useFilterFoods
53 |
--------------------------------------------------------------------------------
/src/portions/PortionsMenuOrDrawer/index.tsx:
--------------------------------------------------------------------------------
1 | import { useScreenSize, ScreenSize } from 'general'
2 | import Drawer from './Drawer'
3 | import Trigger from './Trigger'
4 | import { useDisclosure } from '@chakra-ui/hooks'
5 | import Menu from './Menu'
6 | import { Portion, usePortions } from 'portions'
7 | import { Food } from 'foods'
8 |
9 | type Props = {
10 | onPortionChange: (portion: Portion) => void
11 | selectedPortionId: string
12 | food: Food
13 | }
14 |
15 | function PortionsMenuOrDrawer({
16 | food,
17 | onPortionChange,
18 | selectedPortionId,
19 | }: Props) {
20 | const screenSize = useScreenSize()
21 | const modalDisclosure = useDisclosure()
22 | const { allPortions, weightBasedPortions } = usePortions()
23 | const portions = food.volume ? allPortions : weightBasedPortions
24 |
25 | if (screenSize < ScreenSize.Medium) {
26 | return (
27 | <>
28 |
32 |
39 | >
40 | )
41 | }
42 | return (
43 |
48 | )
49 | }
50 |
51 | export default PortionsMenuOrDrawer
52 |
--------------------------------------------------------------------------------
/src/foods-filters/FoodsFilterPopoverOrModal/Content.tsx:
--------------------------------------------------------------------------------
1 | import { VStack, Checkbox } from '@chakra-ui/react'
2 | import { FoodCategoriesSelect } from 'foods-categories'
3 | import { useFoodsFilter, useFoodsFilterActions } from 'foods-filters'
4 | import { ChangeEvent, RefObject } from 'react'
5 |
6 | type Props = {
7 | selectRef: RefObject
8 | }
9 |
10 | function Content({ selectRef }: Props) {
11 | const filter = useFoodsFilter()
12 | const foodsFilterActions = useFoodsFilterActions()
13 |
14 | function onSelectChange(event: ChangeEvent) {
15 | const { value } = event.target
16 | foodsFilterActions.updateFilter({ categoryId: Number(value) })
17 | }
18 |
19 | function onCheckboxChange(event: ChangeEvent) {
20 | const { checked } = event.target
21 | foodsFilterActions.updateFilter({ onlyFoodsAddedByUser: checked })
22 | }
23 |
24 | return (
25 |
26 |
32 |
33 |
34 |
35 |
40 | Only items added by me
41 |
42 |
43 | )
44 | }
45 |
46 | export default Content
47 |
--------------------------------------------------------------------------------
/src/portions/PortionsMenuOrDrawer/Drawer/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Drawer as DrawerBase,
3 | DrawerBody,
4 | DrawerHeader,
5 | DrawerOverlay,
6 | DrawerContent,
7 | DrawerCloseButton,
8 | } from '@chakra-ui/react'
9 | import { Portion } from 'portions'
10 | import PortionItem from './PortionItem'
11 |
12 | type Props = {
13 | isOpen: boolean
14 | onClose: () => void
15 | onChange: (portion: Portion) => void
16 | selectedPortionId: string
17 | portions: Portion[]
18 | }
19 |
20 | function Drawer({
21 | portions,
22 | isOpen,
23 | onClose,
24 | onChange,
25 | selectedPortionId,
26 | }: Props) {
27 | return (
28 |
29 |
30 |
31 |
32 | Portions
33 |
34 |
35 | {portions.map(portion => {
36 | const { id } = portion
37 | const isSelected = id === selectedPortionId
38 |
39 | return (
40 | {
45 | onClose()
46 | onChange(portion)
47 | }}
48 | />
49 | )
50 | })}
51 |
52 |
53 |
54 | )
55 | }
56 |
57 | export default Drawer
58 |
--------------------------------------------------------------------------------
/src/portions/usePortionsStore.ts:
--------------------------------------------------------------------------------
1 | import { makeStoreProvider, useCallbacksMemo } from 'general'
2 | import { useMemo, useState } from 'react'
3 | import { Portion } from './types'
4 | import defaultPortions from './defaultPortions'
5 |
6 | type PortionsMap = Record
7 |
8 | function usePortionsStore() {
9 | const [portionsById, setPortionsById] = useState(() => {
10 | const initialMap: PortionsMap = {}
11 |
12 | for (const portion of defaultPortions) {
13 | initialMap[portion.id] = portion
14 | }
15 |
16 | return initialMap
17 | })
18 |
19 | const allPortions = useMemo(() => Object.values(portionsById), [portionsById])
20 |
21 | const weightBasedPortions = useMemo(
22 | () =>
23 | allPortions.filter(({ gramsPerAmount }) => gramsPerAmount !== undefined),
24 | [allPortions]
25 | )
26 |
27 | const volumeBasedPortions = useMemo(
28 | () =>
29 | allPortions.filter(
30 | ({ millilitersPerAmount }) => millilitersPerAmount !== undefined
31 | ),
32 | [allPortions]
33 | )
34 |
35 | const state = useCallbacksMemo({
36 | portionsById,
37 | allPortions,
38 | volumeBasedPortions,
39 | weightBasedPortions,
40 | })
41 |
42 | return [state, setPortionsById] as const
43 | }
44 |
45 | const [
46 | PortionsStoreProvider,
47 | usePortions,
48 | usePortionsActions,
49 | ] = makeStoreProvider(usePortionsStore)
50 |
51 | export { PortionsStoreProvider, usePortions, usePortionsActions }
52 |
53 | export default usePortionsStore
54 |
--------------------------------------------------------------------------------
/src/stats/StatsFormFields/index.tsx:
--------------------------------------------------------------------------------
1 | import { FlexProps, Text, Flex, Box, Divider } from '@chakra-ui/react'
2 | import MacrosFormFields from './MacrosFormFields'
3 | import VitaminsAndMineralsFormFields from './VitaminsAndMineralsFormFields'
4 |
5 | type Props = {
6 | canEdit: boolean
7 | showsEnergyPrecentFromFat?: boolean
8 | } & FlexProps
9 |
10 | function StatsFormFields({
11 | canEdit,
12 | showsEnergyPrecentFromFat = false,
13 | }: Props) {
14 | return (
15 |
16 | {!canEdit && (
17 | <>
18 |
19 |
20 |
21 | % Daily Value *
22 |
23 |
24 | >
25 | )}
26 |
27 |
31 |
32 |
33 |
34 | {!canEdit && (
35 |
36 |
37 |
38 | * The % Daily Value (DV) tells you how much a nutrient in a serving
39 | of food contributes to a daily diet. 2000 calories a day is used for
40 | general nutrition advise.{' '}
41 |
42 |
43 | )}
44 |
45 | )
46 | }
47 |
48 | export { default as StatFormField } from './StatFormField'
49 |
50 | export default StatsFormFields
51 |
--------------------------------------------------------------------------------
/src/general/useSelection.ts:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 |
3 | type Id = number | string
4 | type Item = { id: Id }
5 | type SelectionMap = { [id in Id]: boolean | undefined }
6 |
7 | type Selection = {
8 | isIdSelected: (id: Id) => boolean
9 | toggleItem: (item: T) => void
10 | addItem: (item: T) => void
11 | removeItem: (item: T) => void
12 | selectedItems: T[]
13 | selectionMap: SelectionMap
14 | }
15 |
16 | function useSelection(): Selection {
17 | const [selectionMap, setSelectionMap] = useState({})
18 | const [selectedItems, setSelectedItems] = useState([])
19 |
20 | function toggleItem(item: T) {
21 | const { id } = item
22 | const isSelected = Boolean(selectionMap[id])
23 |
24 | if (isSelected) {
25 | removeItem(item)
26 | } else {
27 | addItem(item)
28 | }
29 |
30 | setSelectionMap({ ...selectionMap, [id]: !isSelected })
31 | }
32 |
33 | function removeItem(item: T) {
34 | setSelectedItems(selectedItems.filter(({ id }) => item.id !== id))
35 | }
36 |
37 | function addItem(item: T) {
38 | setSelectedItems([...selectedItems, item])
39 | }
40 |
41 | function isIdSelected(id: Id) {
42 | const isSelected = Boolean(selectionMap[id])
43 | return isSelected
44 | }
45 |
46 | return {
47 | isIdSelected,
48 | toggleItem,
49 | selectionMap,
50 | selectedItems,
51 | removeItem,
52 | addItem,
53 | }
54 | }
55 |
56 | export type { Selection, Item }
57 |
58 | export default useSelection
59 |
--------------------------------------------------------------------------------
/src/variants/VariantsList/VariantItem/useVariantFormEvents.ts:
--------------------------------------------------------------------------------
1 | import { getInsertVariantFormAnimationKey, VariantForm } from 'variants'
2 | import { RefObject, useState } from 'react'
3 | import { useOneTimeCheckActions } from 'general'
4 | import { MouseEvent } from 'react'
5 |
6 | type Params = {
7 | onDelete: (index: number) => void
8 | onSelect: (variantForm: VariantForm, index: number) => void
9 | variantForm: VariantForm
10 | index: number
11 | ref: RefObject
12 | }
13 |
14 | function useVariantFormEvents({
15 | onDelete,
16 | onSelect,
17 | variantForm,
18 |
19 | index,
20 | }: Params) {
21 | const oneTimeCheckActions = useOneTimeCheckActions()
22 | const [isVisible, setIsVisible] = useState(true)
23 |
24 | const shouldAnimate = oneTimeCheckActions.checkAndReset(
25 | getInsertVariantFormAnimationKey(variantForm.fieldId)
26 | )
27 |
28 | function onAnimationComplete() {
29 | if (!isVisible) {
30 | onDelete(index)
31 | }
32 | }
33 |
34 | function onClick(event: MouseEvent) {
35 | const anyTarget: any = event.target
36 |
37 | if (
38 | anyTarget.type !== 'button' &&
39 | anyTarget.getAttribute('role') !== 'menuitem'
40 | ) {
41 | onSelect(variantForm, index)
42 | }
43 | }
44 |
45 | function onRemoveRequest() {
46 | setIsVisible(false)
47 | }
48 |
49 | return {
50 | onClick,
51 | shouldAnimate,
52 | onAnimationComplete,
53 | isVisible,
54 | onRemoveRequest,
55 | }
56 | }
57 |
58 | export default useVariantFormEvents
59 |
--------------------------------------------------------------------------------