From a924ece13afbb7b672f74c55499a7244b42a8cbd Mon Sep 17 00:00:00 2001 From: Grant Forrest Date: Sun, 25 Aug 2024 15:00:14 -0400 Subject: [PATCH] [ww] convert list privacy, improve cards, qol --- .../web/src/components/addBar/AddPane.tsx | 326 +++++------ .../web/src/pages/groceries/GroceriesPage.tsx | 128 ++--- .../src/components/items/ItemEditDialog.tsx | 528 +++++++++--------- .../web/src/components/items/ListItem.tsx | 217 ++++--- .../src/components/lists/CreateListButton.tsx | 157 +++--- .../components/lists/ListDetailsDialog.tsx | 200 ++++--- .../web/src/components/lists/ListHero.tsx | 24 +- .../web/src/components/lists/ListView.tsx | 162 +++++- .../web/src/components/lists/ListsList.tsx | 184 +++--- apps/wish-wash/web/src/pages/HomePage.tsx | 105 ++-- biscuits.code-workspace | 150 ++--- packages/db/src/index.ts | 41 +- packages/error/src/index.ts | 124 ++-- server/src/graphql/builder.ts | 386 ++++++------- server/src/graphql/types/wishWash/hub.ts | 220 ++++---- server/src/routers/gnocchi.ts | 118 ++-- server/src/routers/graphql.ts | 207 +++---- server/src/routers/wishWash.ts | 167 +++--- server/src/server.ts | 103 ++-- server/src/tasks/writeSchema.ts | 10 +- web/src/pages/LoginPage.tsx | 309 +++++----- web/src/pages/SettingsPage.tsx | 277 +++++---- 22 files changed, 2186 insertions(+), 1957 deletions(-) diff --git a/apps/gnocchi/web/src/components/addBar/AddPane.tsx b/apps/gnocchi/web/src/components/addBar/AddPane.tsx index 5d8e3ce8..b71ad4c9 100644 --- a/apps/gnocchi/web/src/components/addBar/AddPane.tsx +++ b/apps/gnocchi/web/src/components/addBar/AddPane.tsx @@ -3,191 +3,203 @@ 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, stopPropagation } from '@a-type/utils'; +import { isUrl, preventDefault, stopPropagation } from '@a-type/utils'; import { - showSubscriptionPromotion, - useHasServerAccess, + showSubscriptionPromotion, + useHasServerAccess, } from '@biscuits/client'; import { Recipe } from '@gnocchi.biscuits/verdant'; import classNames from 'classnames'; import { - UseComboboxState, - UseComboboxStateChangeOptions, - useCombobox, + UseComboboxState, + UseComboboxStateChangeOptions, + useCombobox, } from 'downshift'; import { - Suspense, - forwardRef, - useCallback, - useEffect, - useRef, - useState, - useTransition, + Suspense, + forwardRef, + useCallback, + useEffect, + useRef, + useState, + useTransition, } from 'react'; import { AddInput } from './AddInput.jsx'; import { SuggestionGroup } from './SuggestionGroup.jsx'; import { - SuggestionData, - suggestionToString, - useAddBarCombobox, - useAddBarSuggestions, + SuggestionData, + suggestionToString, + useAddBarCombobox, + useAddBarSuggestions, } from './hooks.js'; import { AddBarProps } from './AddBar.jsx'; import { ScrollArea } from '@a-type/ui/components/scrollArea'; const AddPaneImpl = forwardRef< - HTMLDivElement, - AddBarProps & { disabled?: boolean } + HTMLDivElement, + AddBarProps & { disabled?: boolean } >(function AddPaneImpl( - { - onAdd, - showRichSuggestions = false, - open, - onOpenChange, - className, - disabled, - ...rest - }, - ref, + { + onAdd, + showRichSuggestions = false, + open, + onOpenChange, + className, + disabled, + ...rest + }, + ref, ) { - const [suggestionPrompt, setSuggestionPrompt] = useState(''); + const [suggestionPrompt, setSuggestionPrompt] = useState(''); - const { - allSuggestions, - placeholder, - expiresSoonSuggestions, - showExpiring, - showSuggested, - mainSuggestions, - matchSuggestions, - } = useAddBarSuggestions({ - showRichSuggestions, - suggestionPrompt, - }); + const { + allSuggestions, + placeholder, + expiresSoonSuggestions, + showExpiring, + showSuggested, + mainSuggestions, + matchSuggestions, + } = useAddBarSuggestions({ + showRichSuggestions, + suggestionPrompt, + }); - const contentRef = useRef(null); - const innerRef = useSize(({ width }) => { - if (contentRef.current) { - contentRef.current.style.width = width + 'px'; - } - }); + const contentRef = useRef(null); + const innerRef = useSize(({ width }) => { + if (contentRef.current) { + contentRef.current.style.width = width + 'px'; + } + }); - const inputRef = useRef(null); - const { - combobox: { - isOpen, - getMenuProps, - getInputProps, - highlightedIndex, - getItemProps, - inputValue, - setInputValue, - selectItem, - openMenu, - }, - addingRecipe, - clearAddingRecipe, - onInputPaste, - } = useAddBarCombobox({ - setSuggestionPrompt, - allSuggestions, - onAdd: (items) => { - onAdd(items); - inputRef.current?.blur(); - }, - onOpenChange, - open, - }); + const inputRef = useRef(null); + const { + combobox: { + isOpen, + getMenuProps, + getInputProps, + highlightedIndex, + getItemProps, + inputValue, + setInputValue, + selectItem, + openMenu, + }, + addingRecipe, + clearAddingRecipe, + onInputPaste, + } = useAddBarCombobox({ + setSuggestionPrompt, + allSuggestions, + onAdd: (items) => { + onAdd(items); + inputRef.current?.blur(); + }, + onOpenChange, + open, + }); - const mergedRef = useMergedRef(ref, innerRef); + const mergedRef = useMergedRef(ref, innerRef); - useEffect(() => { - if (disabled) { - inputRef.current?.blur(); - } else { - inputRef.current?.focus(); - } - }, [disabled]); + useEffect(() => { + if (disabled) { + inputRef.current?.blur(); + } else { + inputRef.current?.focus(); + } + }, [disabled]); - const noSuggestions = allSuggestions.length === 0; + useEffect(() => { + if (open) { + visualViewport?.addEventListener('scroll', preventDefault, true); + const original = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { + visualViewport?.removeEventListener('scroll', preventDefault); + document.body.style.overflow = original; + }; + } + }, [open]); - const menuProps = getMenuProps({ - ref: contentRef, - }); + const noSuggestions = allSuggestions.length === 0; - return ( -
- setInputValue('')} - ref={mergedRef} - disableInteraction={disabled} - {...rest} - /> - - {showSuggested && ( - - )} - {showExpiring && ( - - )} - {!noSuggestions && ( - - )} - {noSuggestions &&
No suggestions
} -
- {addingRecipe && ( - - )} -
- ); + const menuProps = getMenuProps({ + ref: contentRef, + }); + + return ( +
+ setInputValue('')} + ref={mergedRef} + disableInteraction={disabled} + {...rest} + /> + + {showSuggested && ( + + )} + {showExpiring && ( + + )} + {!noSuggestions && ( + + )} + {noSuggestions &&
No suggestions
} +
+ {addingRecipe && ( + + )} +
+ ); }); export const AddPane = forwardRef< - HTMLDivElement, - AddBarProps & { disabled?: boolean } + HTMLDivElement, + AddBarProps & { disabled?: boolean } >(function AddBar(props, ref) { - return ( - - - - ); + return ( + + + + ); }); diff --git a/apps/gnocchi/web/src/pages/groceries/GroceriesPage.tsx b/apps/gnocchi/web/src/pages/groceries/GroceriesPage.tsx index 2fc5b940..6ba5aefa 100644 --- a/apps/gnocchi/web/src/pages/groceries/GroceriesPage.tsx +++ b/apps/gnocchi/web/src/pages/groceries/GroceriesPage.tsx @@ -10,81 +10,81 @@ import { ListContext } from '@/contexts/ListContext.jsx'; import { usePageTitle } from '@/hooks/usePageTitle.jsx'; import { firstTimeOnboarding } from '@/onboarding/firstTimeOnboarding.js'; import { - MainActions, - List, - ListSelectWrapper, - ThemedPageContent, - TopControls, - UnknownListRedirect, + MainActions, + List, + ListSelectWrapper, + ThemedPageContent, + TopControls, + UnknownListRedirect, } from '@/pages/groceries/layout.jsx'; import { PageNowPlaying } from '@a-type/ui/components/layouts'; import { - ChangelogDisplay, - InstallButton, - SubscribedOnly, - SubscriptionExpiredDialog, - UserMenu, - PresencePeople, + ChangelogDisplay, + InstallButton, + SubscribedOnly, + SubscriptionExpiredDialog, + UserMenu, + PresencePeople, } from '@biscuits/client'; import { useNavigate, useParams } from '@verdant-web/react-router'; import { Suspense, useCallback, useEffect } from 'react'; export function GroceriesPage() { - const navigate = useNavigate(); + const navigate = useNavigate(); - usePageTitle('Groceries'); + usePageTitle('Groceries'); - const onListChange = useCallback( - (listId: string | null | undefined) => { - if (listId === undefined) { - navigate('/'); - } else if (listId === null) { - navigate('/list/null'); - } else { - navigate(`/list/${listId}`); - } - }, - [navigate], - ); - const { listId: listIdParam } = useParams(); - const listId = listIdParam === 'null' ? null : listIdParam; + const onListChange = useCallback( + (listId: string | null | undefined) => { + if (listId === undefined) { + navigate('/'); + } else if (listId === null) { + navigate('/list/null'); + } else { + navigate(`/list/${listId}`); + } + }, + [navigate], + ); + const { listId: listIdParam } = useParams(); + const listId = listIdParam === 'null' ? null : listIdParam; - const start = firstTimeOnboarding.useBegin(); - useEffect(() => { - start(); - }, [start]); + const start = firstTimeOnboarding.useBegin(); + useEffect(() => { + start(); + }, [start]); - return ( - - - - - - - {listId && } - + return ( + + + + + + + {listId && } + -
- - - - - - - -
-
- - - - - - - -
-
- ); +
+ + + + + + + +
+
+ + + + + + + +
+
+ ); } diff --git a/apps/wish-wash/web/src/components/items/ItemEditDialog.tsx b/apps/wish-wash/web/src/components/items/ItemEditDialog.tsx index cf5d88c7..be35058b 100644 --- a/apps/wish-wash/web/src/components/items/ItemEditDialog.tsx +++ b/apps/wish-wash/web/src/components/items/ItemEditDialog.tsx @@ -2,11 +2,11 @@ import { hooks } from '@/hooks.js'; import { clsx } from '@a-type/ui'; import { Button } from '@a-type/ui/components/button'; import { - Dialog, - DialogActions, - DialogClose, - DialogContent, - DialogTitle, + Dialog, + DialogActions, + DialogClose, + DialogContent, + DialogTitle, } from '@a-type/ui/components/dialog'; import { Icon } from '@a-type/ui/components/icon'; import { ImageUploader } from '@a-type/ui/components/imageUploader'; @@ -19,322 +19,336 @@ import { preventDefault } from '@a-type/utils'; import { graphql, useHasServerAccess, useLazyQuery } from '@biscuits/client'; import { useSearchParams } from '@verdant-web/react-router'; import { - Item, - List, - ListItemsItemImageFilesDestructured, + Item, + List, + ListItemsItemImageFilesDestructured, } from '@wish-wash.biscuits/verdant'; import { ReactNode, useCallback } from 'react'; export interface ItemEditDialogProps { - list: List; + list: List; } export function ItemEditDialog({ list }: ItemEditDialogProps) { - const [search, setSearch] = useSearchParams(); + const [search, setSearch] = useSearchParams(); - const itemId = search.get('itemId'); - const { items } = hooks.useWatch(list); - hooks.useWatch(items); + const itemId = search.get('itemId'); + const { items } = hooks.useWatch(list); + hooks.useWatch(items); - const item = items.find((i) => i.get('id') === itemId); + const item = items.find((i) => i.get('id') === itemId); - return ( - { - if (!o) { - setSearch((p) => { - p.delete('itemId'); - return p; - }); - } - }} - > - - Edit item - - All fields save automatically - - {item && } - - - - - - - - ); + return ( + { + if (!o) { + setSearch((p) => { + p.delete('itemId'); + return p; + }); + } + }} + > + + Edit item + + All fields save automatically + + {item && } + + + + + + + + ); } function ItemEditor({ item }: { item: Item }) { - const { type } = hooks.useWatch(item); - let content: ReactNode = null; - switch (type) { - case 'idea': - content = ; - break; - case 'product': - content = ; - break; - case 'vibe': - content = ; - break; - } + const { type } = hooks.useWatch(item); + let content: ReactNode = null; + switch (type) { + case 'idea': + content = ; + break; + case 'product': + content = ; + break; + case 'vibe': + content = ; + break; + } - return
{content}
; + return
{content}
; } function IdeaEditor({ item }: { item: Item }) { - return ( - <> - - - - - - ); + return ( + <> + + + + + + ); } function ProductEditor({ item }: { item: Item }) { - return ( - <> - - - - - - - ); + return ( + <> + + + + + + + ); } function VibeEditor({ item }: { item: Item }) { - return ( - <> - - - - ); + return ( + <> + + + + ); } function ImagesField({ item }: { item: Item }) { - const { imageFiles } = hooks.useWatch(item); - hooks.useWatch(imageFiles); + const { imageFiles } = hooks.useWatch(item); + hooks.useWatch(imageFiles); - return ( -
- -
- {imageFiles.map((file, index) => ( - { - imageFiles.removeAll(file); - }} - /> - ))} -
-
- - { - if (v) { - imageFiles.push(v); - } - }} - className="w-full h-200px rounded-lg" - /> -
- ); + return ( +
+ +
+ {imageFiles.map((file, index) => ( + { + imageFiles.removeAll(file); + }} + /> + ))} +
+
+ + { + if (v) { + imageFiles.push(v); + } + }} + className="w-full h-200px rounded-lg" + /> +
+ ); } function ImageField({ - file, - onRemove, - className, + file, + onRemove, + className, }: { - file: ListItemsItemImageFilesDestructured[number]; - onRemove: () => void; - className?: string; + file: ListItemsItemImageFilesDestructured[number]; + onRemove: () => void; + className?: string; }) { - hooks.useWatch(file); + hooks.useWatch(file); - return ( -
- - -
- ); + return ( +
+ + +
+ ); } function DescriptionField({ - item, - label, - placeholder, + item, + label, + placeholder, + autoFocus, }: { - item: Item; - label?: string; - placeholder?: string; + item: Item; + label?: string; + placeholder?: string; + autoFocus?: boolean; }) { - const descriptionField = hooks.useField(item, 'description'); + const descriptionField = hooks.useField(item, 'description'); - return ( - <> - -