diff --git a/apps/gnocchi/verdant/src/migrations/skipToV34.ts b/apps/gnocchi/verdant/src/migrations/skipToV34.ts index b584fa21..f81f1032 100644 --- a/apps/gnocchi/verdant/src/migrations/skipToV34.ts +++ b/apps/gnocchi/verdant/src/migrations/skipToV34.ts @@ -1,39 +1,39 @@ import v34Schema, { - MigrationTypes as V34Types, + MigrationTypes as V34Types, } from '../client/schemaVersions/v34.js'; import { createMigration } from '@verdant-web/store'; -import { createMinimalGraphQLClient, graphql } from '@biscuits/client'; +import { createMinimalGraphQLClient, graphql } from '@biscuits/graphql'; import { API_HOST_HTTP } from '../config.js'; export default createMigration(v34Schema, async ({ mutations }) => { - await mutations.collaborationInfo.put({}); - try { - const client = createMinimalGraphQLClient({ - origin: API_HOST_HTTP, - }); - const result = await client.query({ - query: graphql(` - query DefaultCategories { - foodCategories { - id - name - sortKey - } - } - `), - }); - if (!result.data) { - if (result.error) throw result.error; - throw new Error('No data returned'); - } - for (const defaultCategory of result.data.foodCategories) { - await mutations.categories.put({ - id: defaultCategory.id.toString(), - name: defaultCategory.name, - sortKey: defaultCategory.sortKey, - }); - } - } catch (error) { - console.error(error); - } + await mutations.collaborationInfo.put({}); + try { + const client = createMinimalGraphQLClient({ + origin: API_HOST_HTTP, + }); + const result = await client.query({ + query: graphql(` + query DefaultCategories { + foodCategories { + id + name + sortKey + } + } + `), + }); + if (!result.data) { + if (result.error) throw result.error; + throw new Error('No data returned'); + } + for (const defaultCategory of result.data.foodCategories) { + await mutations.categories.put({ + id: defaultCategory.id.toString(), + name: defaultCategory.name, + sortKey: defaultCategory.sortKey, + }); + } + } catch (error) { + console.error(error); + } }); diff --git a/apps/gnocchi/verdant/src/migrations/v5.ts b/apps/gnocchi/verdant/src/migrations/v5.ts index b867639d..155f543b 100644 --- a/apps/gnocchi/verdant/src/migrations/v5.ts +++ b/apps/gnocchi/verdant/src/migrations/v5.ts @@ -1,45 +1,45 @@ import v4Schema, { - MigrationTypes as V4Types, + MigrationTypes as V4Types, } from '../client/schemaVersions/v4.js'; import v5Schema, { - MigrationTypes as V5Types, + MigrationTypes as V5Types, } from '../client/schemaVersions/v5.js'; import { createMigration } from '@verdant-web/store'; -import { createMinimalGraphQLClient, graphql } from '@biscuits/client'; +import { createMinimalGraphQLClient, graphql } from '@biscuits/graphql'; import { API_HOST_HTTP } from '../config.js'; export default createMigration( - v4Schema, - v5Schema, - async ({ mutations }) => { - try { - const client = createMinimalGraphQLClient({ - origin: API_HOST_HTTP, - }); - const result = await client.query({ - query: graphql(` - query DefaultCategories { - foodCategories { - id - name - sortKey - } - } - `), - }); - if (!result.data) { - if (result.error) throw result.error; - throw new Error('No data returned'); - } - for (const defaultCategory of result.data.foodCategories) { - await mutations.categories.put({ - id: defaultCategory.id.toString(), - name: defaultCategory.name, - sortKey: defaultCategory.sortKey, - }); - } - } catch (error) { - console.error(error); - } - }, + v4Schema, + v5Schema, + async ({ mutations }) => { + try { + const client = createMinimalGraphQLClient({ + origin: API_HOST_HTTP, + }); + const result = await client.query({ + query: graphql(` + query DefaultCategories { + foodCategories { + id + name + sortKey + } + } + `), + }); + if (!result.data) { + if (result.error) throw result.error; + throw new Error('No data returned'); + } + for (const defaultCategory of result.data.foodCategories) { + await mutations.categories.put({ + id: defaultCategory.id.toString(), + name: defaultCategory.name, + sortKey: defaultCategory.sortKey, + }); + } + } catch (error) { + console.error(error); + } + }, ); diff --git a/apps/gnocchi/web/src/components/addBar/AddPane.tsx b/apps/gnocchi/web/src/components/addBar/AddPane.tsx index b71ad4c9..542fe21c 100644 --- a/apps/gnocchi/web/src/components/addBar/AddPane.tsx +++ b/apps/gnocchi/web/src/components/addBar/AddPane.tsx @@ -1,37 +1,12 @@ -import { recipeSavePromptState } from '@/components/recipes/savePrompt/state.js'; import { AddToListDialog } from '@/components/recipes/viewer/AddToListDialog.jsx'; import useMergedRef from '@/hooks/useMergedRef.js'; -import { Input } from '@a-type/ui/components/input'; import { useSize } from '@a-type/ui/hooks'; -import { isUrl, preventDefault, stopPropagation } from '@a-type/utils'; -import { - showSubscriptionPromotion, - useHasServerAccess, -} from '@biscuits/client'; -import { Recipe } from '@gnocchi.biscuits/verdant'; +import { preventDefault, stopPropagation } from '@a-type/utils'; import classNames from 'classnames'; -import { - UseComboboxState, - UseComboboxStateChangeOptions, - useCombobox, -} from 'downshift'; -import { - Suspense, - forwardRef, - useCallback, - useEffect, - useRef, - useState, - useTransition, -} from 'react'; +import { Suspense, forwardRef, useEffect, useRef, useState } from 'react'; import { AddInput } from './AddInput.jsx'; import { SuggestionGroup } from './SuggestionGroup.jsx'; -import { - SuggestionData, - suggestionToString, - useAddBarCombobox, - useAddBarSuggestions, -} from './hooks.js'; +import { useAddBarCombobox, useAddBarSuggestions } from './hooks.js'; import { AddBarProps } from './AddBar.jsx'; import { ScrollArea } from '@a-type/ui/components/scrollArea'; @@ -156,31 +131,33 @@ const AddPaneImpl = forwardRef< onScroll={stopPropagation} background="white" > - {showSuggested && ( - - )} - {showExpiring && ( - - )} - {!noSuggestions && ( - - )} - {noSuggestions &&
No suggestions
} +
+ {showSuggested && ( + + )} + {showExpiring && ( + + )} + {!noSuggestions && ( + + )} + {noSuggestions &&
No suggestions
} +
{addingRecipe && ( 1 ? `, ...` : ''); - } - return undefined; + const { comment, textOverride } = hooks.useWatch(item); + hooks.useWatch(item.get('inputs')); + if (comment) { + return comment; + } + // items with a text override show their original as subline + if (textOverride) { + const firstInput = item.get('inputs').get(0); + if (!firstInput) return undefined; + const firstInputText = + firstInput.get('text') + + (firstInput.get('multiplier') + ? ` (x${firstInput.get('multiplier')})` + : ''); + if ( + firstInputText.trim().toLowerCase() === textOverride.trim().toLowerCase() + ) { + return undefined; + } + const numInputs = item.get('inputs').length; + return firstInputText + (numInputs > 1 ? `, ...` : ''); + } + return undefined; } diff --git a/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx b/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx index 007d05d8..68c9281c 100644 --- a/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx +++ b/apps/gnocchi/web/src/components/recipes/collection/RecipeListItem.tsx @@ -6,212 +6,195 @@ import { hooks } from '@/stores/groceries/index.js'; import { Recipe } from '@gnocchi.biscuits/verdant'; import { Button } from '@a-type/ui/components/button'; import { - CardActions, - CardFooter, - CardImage, - CardMain, - CardMenu, - CardRoot, - CardTitle, + CardActions, + CardFooter, + CardImage, + CardMain, + CardMenu, + CardRoot, + CardTitle, } from '@a-type/ui/components/card'; import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuItemRightSlot, - DropdownMenuTrigger, + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuItemRightSlot, + DropdownMenuTrigger, } from '@a-type/ui/components/dropdownMenu'; -import { Suspense, useCallback, useState, memo } from 'react'; +import { Suspense, useState, memo } from 'react'; import { RecipeMainImageViewer } from '../viewer/RecipeMainImageViewer.jsx'; import { RecipeTagsViewer } from '../viewer/RecipeTagsViewer.jsx'; import { Icon } from '@a-type/ui/components/icon'; import addWeeks from 'date-fns/addWeeks'; -import { PinIcon } from './PinIcon.jsx'; import { useGridStyle } from './hooks.js'; import classNames from 'classnames'; import { DrawingPinFilledIcon } from '@radix-ui/react-icons'; import { useNavigate } from '@verdant-web/react-router'; import cuid from 'cuid'; +import { RecipePinToggle } from '../viewer/RecipePinToggle.jsx'; const THREE_WEEKS_AGO = addWeeks(Date.now(), -3).getTime(); export const RecipeListItem = memo(function RecipeListItem({ - recipe, - className, + recipe, + className, }: { - recipe: Recipe; - className?: string; + recipe: Recipe; + className?: string; }) { - const { title, pinnedAt, mainImage } = hooks.useWatch(recipe); - const [gridStyle] = useGridStyle(); + const { title, mainImage } = hooks.useWatch(recipe); + const [gridStyle] = useGridStyle(); - const isPinned = !!pinnedAt && pinnedAt > THREE_WEEKS_AGO; + return ( + + + + + {title} + +
+ + + +
+ +
+ + + + + + - const togglePinned = useCallback(() => { - if (isPinned) { - recipe.set('pinnedAt', null); - } else { - recipe.set('pinnedAt', Date.now()); - } - }, [recipe, isPinned]); - - return ( - - - - - {title} - -
- - - -
- -
- - - - - - - - - - - - - - - -
- ); + + + +
+ + + +
+
+ ); }); export function RecipePlaceholderItem({ className }: { className?: string }) { - return  ; + return  ; } export function RecipeListItemMenu({ - recipe, - ...rest + recipe, + ...rest }: { - recipe: Recipe; - className?: string; + recipe: Recipe; + className?: string; }) { - const deleteRecipe = hooks.useDeleteRecipe(); - const { pinnedAt } = hooks.useWatch(recipe); - const isPinned = pinnedAt && pinnedAt > THREE_WEEKS_AGO; - const client = hooks.useClient(); - const navigate = useNavigate(); - const copyRecipe = async () => { - const copy = await client.recipes.clone(recipe); - copy.update({ - slug: cuid.slug(), - title: `${recipe.get('title')} (copy)`, - pinnedAt: null, - createdAt: Date.now(), - addIntervalGuess: null, - cookCount: 0, - lastAddedAt: null, - lastCookedAt: null, - session: null, - updatedAt: Date.now(), - }); - navigate(makeRecipeLink(copy, '/edit'), { - skipTransition: true, - }); - }; + const deleteRecipe = hooks.useDeleteRecipe(); + const { pinnedAt } = hooks.useWatch(recipe); + const isPinned = pinnedAt && pinnedAt > THREE_WEEKS_AGO; + const client = hooks.useClient(); + const navigate = useNavigate(); + const copyRecipe = async () => { + const copy = await client.recipes.clone(recipe); + copy.update({ + slug: cuid.slug(), + title: `${recipe.get('title')} (copy)`, + pinnedAt: null, + createdAt: Date.now(), + addIntervalGuess: null, + cookCount: 0, + lastAddedAt: null, + lastCookedAt: null, + session: null, + updatedAt: Date.now(), + }); + navigate(makeRecipeLink(copy, '/edit'), { + skipTransition: true, + }); + }; - const [menuOpen, setMenuOpen] = useState(false); + const [menuOpen, setMenuOpen] = useState(false); - return ( - { - if (open) setMenuOpen(true); - }} - modal={false} - > - - - - setMenuOpen(false)}> - setMenuOpen(false)}> - - Tags - - - - - - - - Edit - - - - - - {isPinned && ( - { - recipe.set('pinnedAt', null); - setMenuOpen(false); - }} - > - Remove pin - - - - - )} - - Make a copy - - - - - { - deleteRecipe(recipe.get('id')); - setMenuOpen(false); - }} - > - Delete - - - - - - - ); + return ( + { + if (open) setMenuOpen(true); + }} + modal={false} + > + + + + setMenuOpen(false)}> + setMenuOpen(false)}> + + Tags + + + + + + + + Edit + + + + + + {isPinned && ( + { + recipe.set('pinnedAt', null); + setMenuOpen(false); + }} + > + Remove pin + + + + + )} + + Make a copy + + + + + { + deleteRecipe(recipe.get('id')); + setMenuOpen(false); + }} + > + Delete + + + + + + + ); } diff --git a/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx b/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx index 63693f0a..e87defa3 100644 --- a/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx +++ b/apps/gnocchi/web/src/components/recipes/cook/CookingActionBar.tsx @@ -1,15 +1,15 @@ import { Icon } from '@/components/icons/Icon.jsx'; import { TextLink } from '@/components/nav/Link.jsx'; import { - PromoteSubscriptionButton, - useHasServerAccess, - useIsLoggedIn, - useQuery, + PromoteSubscriptionButton, + useHasServerAccess, + useIsLoggedIn, + ManagePlanButton, } from '@biscuits/client'; import { useActiveCookingSession } from '@/components/recipes/hooks.js'; import { - PeopleList, - PeopleListItem, + PeopleList, + PeopleListItem, } from '@/components/sync/people/People.jsx'; import { PersonAvatar } from '@/components/sync/people/PersonAvatar.jsx'; import { useLocalStorage } from '@/hooks/useLocalStorage.js'; @@ -18,182 +18,182 @@ import { Recipe } from '@gnocchi.biscuits/verdant'; import { ActionBar, ActionButton } from '@a-type/ui/components/actions'; import { Button } from '@a-type/ui/components/button'; import { - Dialog, - DialogActions, - DialogClose, - DialogContent, - DialogTitle, - DialogTrigger, + Dialog, + DialogActions, + DialogClose, + DialogContent, + DialogTitle, + DialogTrigger, } from '@a-type/ui/components/dialog'; import { ErrorBoundary } from '@a-type/ui/components/errorBoundary'; import { Cross2Icon } from '@radix-ui/react-icons'; -import { ManagePlanButton, graphql } from '@biscuits/client'; +import { graphql, useQuery } from '@biscuits/graphql'; import { - Popover, - PopoverContent, - PopoverTrigger, + Popover, + PopoverContent, + PopoverTrigger, } from '@a-type/ui/components/popover'; import { Note } from '@a-type/ui/components/note'; import { CONFIG } from '@biscuits/client'; export interface CookingActionBarProps { - recipe: Recipe; + recipe: Recipe; } export function CookingActionBar({ recipe }: CookingActionBarProps) { - return ( - - - - - - - ); + return ( + + + + + + + ); } function CookingPeople({ recipeId }: { recipeId: string }) { - const self = hooks.useSelf(); - const peers = hooks.useFindPeers( - (peer) => peer.presence.viewingRecipeId === recipeId, - ); + const self = hooks.useSelf(); + const peers = hooks.useFindPeers( + (peer) => peer.presence.viewingRecipeId === recipeId, + ); - const syncing = hooks.useSyncStatus(); + const syncing = hooks.useSyncStatus(); - if (!syncing || peers.length === 0) { - return null; - } + if (!syncing || peers.length === 0) { + return null; + } - return ( - - - - - - {peers.map((peer, index) => ( - - - - ))} - - - ); + return ( + + + + + + {peers.map((peer, index) => ( + + + + ))} + + + ); } const planMembersQuery = graphql(` - query PlanMembersForCookingActionBar { - plan { - members { - id - } - } - } + query PlanMembersForCookingActionBar { + plan { + members { + id + } + } + } `); function AddChefsAction() { - const isLoggedIn = useIsLoggedIn(); - const { data: members } = useQuery(planMembersQuery, { - skip: !isLoggedIn, - }); - const isSubscribed = useHasServerAccess(); - const showTip = - members && (!isLoggedIn || members?.plan?.members.length === 1); - const [dismissed, setDismissed] = useLocalStorage('add-chefs-tip', false); + const isLoggedIn = useIsLoggedIn(); + const { data: members } = useQuery(planMembersQuery, { + skip: !isLoggedIn, + }); + const isSubscribed = useHasServerAccess(); + const showTip = + members && (!isLoggedIn || members?.plan?.members.length === 1); + const [dismissed, setDismissed] = useLocalStorage('add-chefs-tip', false); - const showSubscribe = !isLoggedIn || !isSubscribed; + const showSubscribe = !isLoggedIn || !isSubscribed; - return ( - { - if (!open) setDismissed(true); - }} - > - - } - color="accent" - > - Invite chefs - - - - Invite chefs - {showSubscribe ? ( - <> -

- Subscribe to invite others to help you cook! Recipe progress is - shared for all plan members in real-time. You can have as many - people on your plan as you like. -

-

- Plus, subscribers get a whole lot of other features, including - device sync, grocery collaboration, and web recipe scanning. -

-

- - Learn more about subscription features. - -

- - ) : ( -

- Invite people to your plan to cook together! Recipe progress is - shared for all plan members in real-time. -

- )} - - - - - {showSubscribe ? ( - - Subscribe now - - ) : ( - - )} - -
-
- ); + return ( + { + if (!open) setDismissed(true); + }} + > + + } + color="accent" + > + Invite chefs + + + + Invite chefs + {showSubscribe ? ( + <> +

+ Subscribe to invite others to help you cook! Recipe progress is + shared for all plan members in real-time. You can have as many + people on your plan as you like. +

+

+ Plus, subscribers get a whole lot of other features, including + device sync, grocery collaboration, and web recipe scanning. +

+

+ + Learn more about subscription features. + +

+ + ) : ( +

+ Invite people to your plan to cook together! Recipe progress is + shared for all plan members in real-time. +

+ )} + + + + + {showSubscribe ? ( + + Subscribe now + + ) : ( + + )} + +
+
+ ); } function StopCookingAction({ recipe }: { recipe: Recipe }) { - const session = useActiveCookingSession(recipe); - const stopCooking = () => { - recipe.set('session', null); - }; + const session = useActiveCookingSession(recipe); + const stopCooking = () => { + recipe.set('session', null); + }; - return ( - } - onClick={stopCooking} - > - Stop cooking - - ); + return ( + } + onClick={stopCooking} + > + Stop cooking + + ); } function NoteToggleAction({ recipe }: { recipe: Recipe }) { - const { note } = hooks.useWatch(recipe); + const { note } = hooks.useWatch(recipe); - if (!note) return null; + if (!note) return null; - return ( - - - } color="primary"> - Add note - - - - {note} - - - ); + return ( + + + } color="primary"> + Add note + + + + {note} + + + ); } diff --git a/apps/gnocchi/web/src/components/recipes/editor/InstructionStepNodeView.tsx b/apps/gnocchi/web/src/components/recipes/editor/InstructionStepNodeView.tsx index 7d940df0..96ebff02 100644 --- a/apps/gnocchi/web/src/components/recipes/editor/InstructionStepNodeView.tsx +++ b/apps/gnocchi/web/src/components/recipes/editor/InstructionStepNodeView.tsx @@ -6,15 +6,15 @@ import { Recipe } from '@gnocchi.biscuits/verdant'; import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'; import classNames from 'classnames'; import { - ChangeEvent, - ReactNode, - useCallback, - useContext, - useMemo, + ChangeEvent, + ReactNode, + useCallback, + useContext, + useMemo, } from 'react'; import { - CollapsibleContent, - CollapsibleRoot, + CollapsibleContent, + CollapsibleRoot, } from '@a-type/ui/components/collapsible'; import { TextArea } from '@a-type/ui/components/textArea'; import { Note } from '@a-type/ui/components/note'; @@ -24,228 +24,228 @@ import { Button } from '@a-type/ui/components/button'; import { useToggle } from '@a-type/ui/hooks'; import { InstructionsContext } from '@/components/recipes/editor/InstructionsContext.jsx'; import { - isActiveCookingSession, - useCookSessionAction, + isActiveCookingSession, + useCookSessionAction, } from '@/components/recipes/hooks.js'; import { useHasServerAccess } from '@biscuits/client'; export interface InstructionStepNodeViewProps { - node: { - attrs: { id: string; note?: string }; - }; - extension: { - storage: { recipe?: Recipe }; - }; - updateAttributes: (attrs: { id?: string; note?: string }) => void; + node: { + attrs: { id: string; note?: string }; + }; + extension: { + storage: { recipe?: Recipe }; + }; + updateAttributes: (attrs: { id?: string; note?: string }) => void; } export function InstructionStepNodeView({ - node, - extension, - updateAttributes, - ...rest + node, + extension, + updateAttributes, + ...rest }: InstructionStepNodeViewProps) { - const self = hooks.useSelf(); - const { isEditing, showTools } = useContext(InstructionsContext); - - const { id, note } = node.attrs; - - const [showNote, toggleShowNote] = useToggle(!!note); - - const maybeRecipe = extension.storage.recipe; - hooks.useWatch(maybeRecipe || null); - let maybeSession = maybeRecipe?.get('session') ?? null; - if (!isActiveCookingSession(maybeSession)) { - maybeSession = null; - } - hooks.useWatch(maybeSession || null); - const maybeCompletedSteps = maybeSession - ? maybeSession.get('completedInstructions') - : null; - hooks.useWatch(maybeCompletedSteps); - const maybeAssignments = maybeSession - ? maybeSession.get('instructionAssignments') - : null; - hooks.useWatch(maybeAssignments); - - const completed = maybeCompletedSteps?.has(id); - - const sessionAction = useCookSessionAction(maybeRecipe || null); - - const assignedPersonId = maybeAssignments?.get(id) ?? null; - - const assignPersonId = useCallback( - (personId: string | null) => { - if (!maybeAssignments) { - if (maybeRecipe && personId) { - maybeRecipe.set('session', { - instructionAssignments: { - [id]: personId, - }, - }); - } - return; - } - - sessionAction((session) => { - if (personId) { - session?.get('instructionAssignments').set(id, personId); - } else { - session?.get('instructionAssignments').delete(id); - } - }); - }, - [maybeAssignments, id], - ); - - const isAssignedToMe = assignedPersonId === self.id; - - const updateNote = useCallback( - (event: ChangeEvent) => { - updateAttributes({ note: event.target.value }); - }, - [updateAttributes], - ); - - const onNoteBlur = useCallback(() => { - if (note === '') { - updateAttributes({ note: undefined }); - } - }, [note, updateAttributes]); - - const isSubscribed = useHasServerAccess(); - - return ( - -
- -
- - - -