├── .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 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 | 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 | 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 | 19 | {icon} 20 | {child.props.children} 21 | 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 |