A social knowledge tool for researchers built on ATProto

feat: fully featured add card drawer

+315 -304
+1 -1
src/webapp/components/AddToCollectionModal.tsx
··· 143 143 </Text> 144 144 )} 145 145 146 - <Group grow> 146 + <Group gap={"xs"} grow> 147 147 <Button 148 148 onClick={handleSubmit} 149 149 disabled={submitting || selectedCollectionIds.length === 0}
+36 -44
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
··· 2 2 Button, 3 3 Container, 4 4 Drawer, 5 - Text, 6 5 Group, 7 6 Stack, 8 7 Textarea, 9 8 TextInput, 10 - Anchor, 9 + Tooltip, 11 10 } from '@mantine/core'; 12 11 import { useForm } from '@mantine/form'; 13 12 14 13 import { notifications } from '@mantine/notifications'; 15 14 import useAddCard from '../../lib/mutations/useAddCard'; 16 15 import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector'; 17 - import { Fragment, Suspense, useState } from 'react'; 16 + import { Suspense, useState } from 'react'; 18 17 import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 19 - import { IoMdLink } from 'react-icons/io'; 20 18 import { useDisclosure } from '@mantine/hooks'; 21 19 import { BiCollection } from 'react-icons/bi'; 22 - import Link from 'next/link'; 20 + import { IoMdLink } from 'react-icons/io'; 21 + 22 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 23 23 24 24 interface Props { 25 25 isOpen: boolean; ··· 37 37 useState<{ id: string; name: string; cardCount: number }[]>( 38 38 initialCollections, 39 39 ); 40 + 41 + const hasNoCollections = selectedCollections.length === 0; 42 + const hasOneCollection = selectedCollections.length === 1; 43 + 40 44 const addCard = useAddCard(); 41 45 42 46 const form = useForm({ ··· 47 51 }, 48 52 }); 49 53 50 - const handleCreateCollection = (e: React.FormEvent) => { 54 + const handleAddCard = (e: React.FormEvent) => { 51 55 e.preventDefault(); 52 56 53 57 addCard.mutate( ··· 84 88 }} 85 89 withCloseButton={false} 86 90 position="bottom" 87 - size={'lg'} 88 - overlayProps={{ 89 - blur: 3, 90 - gradient: 91 - 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 92 - }} 91 + size={'26rem'} 92 + overlayProps={DEFAULT_OVERLAY_PROPS} 93 93 > 94 94 <Drawer.Header> 95 95 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 97 97 </Drawer.Title> 98 98 </Drawer.Header> 99 99 <Container size={'sm'}> 100 - <form onSubmit={handleCreateCollection}> 101 - <Stack> 100 + <form onSubmit={handleAddCard}> 101 + <Stack gap={'xl'}> 102 102 <Stack> 103 103 <Stack> 104 104 <TextInput ··· 129 129 130 130 <Group> 131 131 <Stack align="start" gap={'xs'}> 132 - <Button 133 - onClick={toggleCollectionSelector} 134 - variant="light" 135 - color="gray" 136 - leftSection={<BiCollection size={22} />} 132 + <Tooltip 133 + label={selectedCollections.map((c) => c.name).join(', ')} 134 + disabled={hasNoCollections} 137 135 > 138 - Add to collections 139 - </Button> 140 - {selectedCollections.length > 0 && ( 141 - <Text fw={500}> 142 - Selected collections:{' '} 143 - <Text fw={500} span> 144 - {selectedCollections.map((c, index) => ( 145 - <Fragment key={c.id}> 146 - <Anchor 147 - component={Link} 148 - href={`/collections/${c.id}`} 149 - target="_blank" 150 - c="blue" 151 - > 152 - {c.name} 153 - </Anchor> 154 - {index < selectedCollections.length - 1 && ', '} 155 - </Fragment> 156 - ))} 157 - </Text> 158 - </Text> 159 - )} 136 + <Button 137 + onClick={toggleCollectionSelector} 138 + variant="light" 139 + color="gray" 140 + leftSection={<BiCollection size={22} />} 141 + > 142 + {!hasNoCollections 143 + ? `${selectedCollections.length} ${hasOneCollection ? 'collection' : 'collections'}` 144 + : 'Add to collections'} 145 + </Button> 146 + </Tooltip> 160 147 </Stack> 161 148 </Group> 162 149 <Suspense fallback={<CollectionSelectorSkeleton />}> ··· 168 155 /> 169 156 </Suspense> 170 157 </Stack> 171 - <Group justify="space-between"> 172 - <Button variant="outline" color={'gray'} onClick={props.onClose}> 158 + <Group justify="space-between" gap={'xs'} grow> 159 + <Button 160 + variant="light" 161 + size="md" 162 + color={'gray'} 163 + onClick={props.onClose} 164 + > 173 165 Cancel 174 166 </Button> 175 - <Button type="submit" loading={addCard.isPending}> 167 + <Button type="submit" size="md" loading={addCard.isPending}> 176 168 Add card 177 169 </Button> 178 170 </Group>
+8 -1
src/webapp/features/cards/lib/mutations/useAddCard.tsx
··· 26 26 // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks 27 27 // Do UI related things like redirects or showing toast notifications in mutate callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire 28 28 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 29 - onSuccess: () => { 29 + onSuccess: (_data, variables) => { 30 30 queryClient.invalidateQueries({ queryKey: ['my cards'] }); 31 + queryClient.invalidateQueries({ queryKey: ['library'] }); 32 + queryClient.invalidateQueries({ queryKey: ['collections'] }); 33 + 34 + // invalidate each collection query individually 35 + variables.collectionIds?.forEach((id) => { 36 + queryClient.invalidateQueries({ queryKey: ['collection', id] }); 37 + }); 31 38 }, 32 39 }); 33 40
+158 -123
src/webapp/features/collections/components/collectionSelector/CollectionSelector.tsx
··· 10 10 Button, 11 11 Drawer, 12 12 Container, 13 - SimpleGrid, 14 13 Group, 15 14 } from '@mantine/core'; 16 - import { IoSearch } from 'react-icons/io5'; 17 - import { useState } from 'react'; 15 + import { Fragment, useState } from 'react'; 16 + import { useDebouncedValue } from '@mantine/hooks'; 18 17 import useCollections from '../../lib/queries/useCollections'; 19 18 import useCollectionSearch from '../../lib/queries/useCollectionSearch'; 20 - import { useDebouncedValue } from '@mantine/hooks'; 21 - 19 + import CreateCollectionDrawer from '../createCollectionDrawer/CreateCollectionDrawer'; 22 20 import CollectionSelectorError from './Error.CollectionSelector'; 23 21 import CollectionSelectorItem from '../collectionSelectorItem/CollectionSelectorItem'; 24 - import CollectionSelectorNewCollection from '../collectionSelectorNewCollection/CollectionSelectorNewCollection'; 25 22 import { BiPlus } from 'react-icons/bi'; 26 - import { useContextDrawers } from '@/providers/drawers'; 23 + import { IoSearch } from 'react-icons/io5'; 24 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 27 25 28 26 interface Props { 29 27 isOpen: boolean; ··· 39 37 const [search, setSearch] = useState<string>(''); 40 38 const [debouncedSearch] = useDebouncedValue(search, 200); 41 39 const searchedCollections = useCollectionSearch({ query: debouncedSearch }); 42 - const drawers = useContextDrawers(); 40 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 43 41 44 42 const handleCollectionChange = ( 45 43 checked: boolean, ··· 76 74 } 77 75 78 76 const hasCollections = data?.collections?.length > 0; 77 + const hasSelectedCollections = props.selectedCollections.length > 0; 79 78 80 79 return ( 81 - <Drawer 82 - opened={props.isOpen} 83 - onClose={props.onClose} 84 - withCloseButton={false} 85 - position="bottom" 86 - size={'33rem'} 87 - overlayProps={{ 88 - blur: 3, 89 - gradient: 90 - 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 91 - }} 92 - > 93 - <Drawer.Header> 94 - <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> 95 - Add to collections 96 - </Drawer.Title> 97 - </Drawer.Header> 98 - <Container size={'sm'}> 99 - <Stack> 100 - <TextInput 101 - placeholder="Search for collections" 102 - value={search} 103 - onChange={(e) => setSearch(e.currentTarget.value)} 104 - size="md" 105 - variant="filled" 106 - id="search" 107 - leftSection={<IoSearch size={22} />} 108 - rightSection={ 109 - <CloseButton 110 - aria-label="Clear input" 111 - onClick={() => setSearch('')} 112 - style={{ display: search ? undefined : 'none' }} 113 - /> 114 - } 115 - /> 116 - <Tabs defaultValue="collections"> 117 - <Tabs.List grow> 118 - <Tabs.Tab value="collections">Collections</Tabs.Tab> 119 - <Tabs.Tab value="selected"> 120 - Selected ({props.selectedCollections.length}) 121 - </Tabs.Tab> 122 - </Tabs.List> 80 + <Fragment> 81 + <Drawer 82 + opened={props.isOpen} 83 + onClose={props.onClose} 84 + withCloseButton={false} 85 + position="bottom" 86 + size={'40rem'} 87 + overlayProps={DEFAULT_OVERLAY_PROPS} 88 + > 89 + <Drawer.Header> 90 + <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> 91 + Add to collections 92 + </Drawer.Title> 93 + </Drawer.Header> 94 + <Container size={'xs'}> 95 + <Stack gap={'xl'}> 96 + <TextInput 97 + placeholder="Search for collections" 98 + value={search} 99 + onChange={(e) => setSearch(e.currentTarget.value)} 100 + size="md" 101 + variant="filled" 102 + id="search" 103 + leftSection={<IoSearch size={22} />} 104 + rightSection={ 105 + <CloseButton 106 + aria-label="Clear input" 107 + onClick={() => setSearch('')} 108 + style={{ display: search ? undefined : 'none' }} 109 + /> 110 + } 111 + /> 112 + <Tabs defaultValue="collections"> 113 + <Tabs.List grow> 114 + <Tabs.Tab value="collections">Collections</Tabs.Tab> 115 + <Tabs.Tab value="selected"> 116 + Selected ({props.selectedCollections.length}) 117 + </Tabs.Tab> 118 + </Tabs.List> 119 + 120 + {/* collections Panel */} 121 + <Tabs.Panel value="collections" my="xs" w="100%"> 122 + <ScrollArea h={340} type="auto"> 123 + <Stack gap="xs"> 124 + {search ? ( 125 + <Fragment> 126 + <Button 127 + variant="light" 128 + size="md" 129 + color={'grape'} 130 + radius={'lg'} 131 + leftSection={<BiPlus size={22} />} 132 + onClick={() => setIsDrawerOpen(true)} 133 + > 134 + Create new collection "{search}" 135 + </Button> 123 136 124 - {/* Collections Panel */} 125 - <Tabs.Panel value="collections" my="xs" w="100%"> 126 - <ScrollArea h={245}> 127 - <Stack gap="xs"> 128 - {search && <CollectionSelectorNewCollection name={search} />} 129 - {search && searchedCollections.isPending && ( 130 - <Stack align="center"> 131 - <Text fw={500} c="gray"> 132 - Searching collections... 133 - </Text> 134 - <Loader color="gray" /> 135 - </Stack> 136 - )} 137 - {search && 138 - searchedCollections.data && 139 - searchedCollections.data.collections.length === 0 && ( 140 - <Alert 141 - color="gray" 142 - title={`No results found for "${search}"`} 143 - /> 137 + {searchedCollections.isPending && ( 138 + <Stack align="center"> 139 + <Text fw={500} c="gray"> 140 + Searching collections... 141 + </Text> 142 + <Loader color="gray" /> 143 + </Stack> 144 + )} 145 + 146 + {searchedCollections.data && 147 + (searchedCollections.data.collections.length === 0 ? ( 148 + <Alert 149 + color="gray" 150 + title={`No results found for "${search}"`} 151 + /> 152 + ) : ( 153 + renderCollectionItems( 154 + searchedCollections.data.collections, 155 + ) 156 + ))} 157 + </Fragment> 158 + ) : hasCollections ? ( 159 + renderCollectionItems(data.collections) 160 + ) : ( 161 + <Stack align="center" gap="xs"> 162 + <Text fz="lg" fw={600} c="gray"> 163 + No collections 164 + </Text> 165 + <Button 166 + onClick={() => setIsDrawerOpen(true)} 167 + variant="light" 168 + color="gray" 169 + rightSection={<BiPlus size={22} />} 170 + > 171 + Create a collection 172 + </Button> 173 + </Stack> 144 174 )} 145 - {search && 146 - searchedCollections.data && 147 - renderCollectionItems(searchedCollections.data.collections)} 148 - {!search && !hasCollections && ( 149 - <Stack align="center" gap={'xs'}> 150 - <Text fz={'lg'} fw={600} c={'gray'}> 151 - No collections 152 - </Text> 153 - <Button 154 - onClick={() => drawers.open('createCollection')} 155 - variant="light" 156 - color={'gray'} 157 - rightSection={<BiPlus size={22} />} 158 - > 159 - Create a collection 160 - </Button> 161 - </Stack> 162 - )} 163 - {!search && 164 - hasCollections && 165 - renderCollectionItems(data.collections)} 166 - </Stack> 167 - </ScrollArea> 168 - </Tabs.Panel> 175 + </Stack> 176 + </ScrollArea> 177 + </Tabs.Panel> 169 178 170 - {/* Selected Collections Panel */} 171 - <Tabs.Panel value="selected" my="xs"> 172 - <ScrollArea h={245}> 173 - <Stack gap="xs"> 174 - {props.selectedCollections.length > 0 ? ( 175 - renderCollectionItems(props.selectedCollections) 176 - ) : ( 177 - <Alert color="gray" title="No collections selected" /> 178 - )} 179 - </Stack> 180 - </ScrollArea> 181 - </Tabs.Panel> 182 - </Tabs> 183 - <Group justify="space-between"> 184 - <Button 185 - variant="outline" 186 - color={'gray'} 187 - onClick={() => { 188 - props.onSelectedCollectionsChange([]); 189 - props.onClose(); 190 - }} 191 - > 192 - Cancel 193 - </Button> 194 - <Button onClick={props.onClose}>Save</Button> 195 - </Group> 196 - </Stack> 197 - </Container> 198 - </Drawer> 179 + {/* selected Collections Panel */} 180 + <Tabs.Panel value="selected" my="xs"> 181 + <ScrollArea h={340} type="auto"> 182 + <Stack gap="xs"> 183 + {props.selectedCollections.length > 0 ? ( 184 + renderCollectionItems(props.selectedCollections) 185 + ) : ( 186 + <Alert color="gray" title="No collections selected" /> 187 + )} 188 + </Stack> 189 + </ScrollArea> 190 + </Tabs.Panel> 191 + </Tabs> 192 + <Group justify="space-between" gap={'xs'} grow> 193 + <Button 194 + variant="light" 195 + color={'gray'} 196 + size="md" 197 + onClick={() => { 198 + props.onSelectedCollectionsChange([]); 199 + props.onClose(); 200 + }} 201 + > 202 + Cancel 203 + </Button> 204 + {hasSelectedCollections && ( 205 + <Button 206 + variant="light" 207 + color="grape" 208 + size="md" 209 + onClick={() => props.onSelectedCollectionsChange([])} 210 + > 211 + Clear all 212 + </Button> 213 + )} 214 + <Button size="md" onClick={props.onClose}> 215 + Save 216 + </Button> 217 + </Group> 218 + </Stack> 219 + </Container> 220 + </Drawer> 221 + <CreateCollectionDrawer 222 + key={search} 223 + isOpen={isDrawerOpen} 224 + onClose={() => setIsDrawerOpen(false)} 225 + initialName={search} 226 + onCreate={(newCollection) => { 227 + props.onSelectedCollectionsChange([ 228 + ...props.selectedCollections, 229 + newCollection, 230 + ]); 231 + }} 232 + /> 233 + </Fragment> 199 234 ); 200 235 }
+3
src/webapp/features/collections/components/collectionSelectorItem/CollectionSelectorItem.tsx
··· 39 39 <Text fw={500} lineClamp={1}> 40 40 {props.name} 41 41 </Text> 42 + <Text c={'gray'}> 43 + {props.cardCount} {props.cardCount === 1 ? 'card' : 'cards'} 44 + </Text> 42 45 </Stack> 43 46 <CheckboxIndicator /> 44 47 </Group>
-24
src/webapp/features/collections/components/collectionSelectorNewCollection/CollectionSelectorNewCollection.tsx
··· 1 - import { useContextDrawers } from '@/providers/drawers'; 2 - import { Button } from '@mantine/core'; 3 - import { BiPlus } from 'react-icons/bi'; 4 - 5 - interface Props { 6 - name: string; 7 - } 8 - 9 - export default function CollectionSelectorNewCollection(props: Props) { 10 - const drawers = useContextDrawers(); 11 - 12 - return ( 13 - <Button 14 - variant="light" 15 - size="md" 16 - color={'grape'} 17 - radius={'lg'} 18 - leftSection={<BiPlus size={22} />} 19 - onClick={() => drawers.open('createCollection')} 20 - > 21 - Create new collection "{props.name}" 22 - </Button> 23 - ); 24 - }
+31 -10
src/webapp/features/collections/components/createCollectionDrawer/CreateCollectionDrawer.tsx
··· 10 10 import { useForm } from '@mantine/form'; 11 11 import useCreateCollection from '../../lib/mutations/useCreateCollection'; 12 12 import { notifications } from '@mantine/notifications'; 13 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 13 14 14 15 interface Props { 15 16 isOpen: boolean; 16 17 onClose: () => void; 17 18 initialName?: string; 19 + onCreate?: (newCollection: { 20 + id: string; 21 + name: string; 22 + cardCount: number; 23 + }) => void; 18 24 } 19 25 20 26 export default function createCollectionDrawer(props: Props) { ··· 35 41 description: form.getValues().description, 36 42 }, 37 43 { 38 - onSuccess: () => { 44 + onSuccess: (newCollection) => { 39 45 notifications.show({ 40 46 message: `Created collection "${form.getValues().name}".`, 41 47 }); 48 + 42 49 props.onClose(); 50 + props.onCreate && 51 + props.onCreate({ 52 + id: newCollection.collectionId, 53 + name: form.getValues().name, 54 + cardCount: 0, 55 + }); 43 56 }, 44 57 onError: () => { 45 58 notifications.show({ ··· 59 72 onClose={props.onClose} 60 73 withCloseButton={false} 61 74 position="bottom" 62 - overlayProps={{ 63 - blur: 3, 64 - gradient: 65 - 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 66 - }} 75 + size={'30rem'} 76 + overlayProps={DEFAULT_OVERLAY_PROPS} 67 77 > 68 78 <Drawer.Header> 69 79 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 72 82 </Drawer.Header> 73 83 74 84 <Container size={'sm'}> 75 - <form onSubmit={handleCreateCollection}> 85 + <form> 76 86 <Stack> 77 87 <TextInput 78 88 id="name" ··· 80 90 type="text" 81 91 placeholder="Collection name" 82 92 variant="filled" 93 + size="md" 83 94 required 84 95 maxLength={100} 85 96 key={form.key('name')} ··· 91 102 label="Description" 92 103 placeholder="Describe what this collection is about" 93 104 variant="filled" 105 + size="md" 94 106 rows={8} 95 107 maxLength={500} 96 108 key={form.key('description')} 97 109 {...form.getInputProps('description')} 98 110 /> 99 - <Group justify="space-between"> 100 - <Button variant="outline" color={'gray'} onClick={props.onClose}> 111 + <Group justify="space-between" gap={'xs'} grow> 112 + <Button 113 + variant="light" 114 + size="md" 115 + color={'gray'} 116 + onClick={props.onClose} 117 + > 101 118 Cancel 102 119 </Button> 103 - <Button type="submit" loading={createCollection.isPending}> 120 + <Button 121 + onClick={handleCreateCollection} 122 + size="md" 123 + loading={createCollection.isPending} 124 + > 104 125 Create 105 126 </Button> 106 127 </Group>
+17 -10
src/webapp/features/collections/components/createCollectionShortcut/CreateCollectionShortcut.tsx
··· 1 - import { useContextDrawers } from '@/providers/drawers'; 2 1 import { NavLink } from '@mantine/core'; 2 + import { Fragment, useState } from 'react'; 3 3 import { BiPlus } from 'react-icons/bi'; 4 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 4 5 5 6 export default function CreateCollectionShortcut() { 6 - const drawers = useContextDrawers(); 7 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 7 8 8 9 return ( 9 - <NavLink 10 - component="button" 11 - label={'Create'} 12 - variant="subtle" 13 - c="blue" 14 - leftSection={<BiPlus size={25} />} 15 - onClick={() => drawers.open('createCollection')} 16 - /> 10 + <Fragment> 11 + <NavLink 12 + component="button" 13 + label={'Create'} 14 + variant="subtle" 15 + c="blue" 16 + leftSection={<BiPlus size={25} />} 17 + onClick={() => setIsDrawerOpen(true)} 18 + /> 19 + <CreateCollectionDrawer 20 + isOpen={isDrawerOpen} 21 + onClose={() => setIsDrawerOpen(false)} 22 + /> 23 + </Fragment> 17 24 ); 18 25 }
+17 -8
src/webapp/features/collections/components/editCollectionDrawer/EditCollectionDrawer.tsx
··· 10 10 import { useForm } from '@mantine/form'; 11 11 import { notifications } from '@mantine/notifications'; 12 12 import useUpdateCollection from '../../lib/mutations/useUpdateCollection'; 13 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 13 14 14 15 interface Props { 15 16 isOpen: boolean; ··· 65 66 onClose={props.onClose} 66 67 withCloseButton={false} 67 68 position="bottom" 68 - overlayProps={{ 69 - blur: 3, 70 - gradient: 71 - 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 72 - }} 69 + size={'30rem'} 70 + overlayProps={DEFAULT_OVERLAY_PROPS} 73 71 > 74 72 <Drawer.Header> 75 73 <Drawer.Title fz="xl" fw={600} mx="auto"> ··· 85 83 label="Name" 86 84 placeholder="Collection name" 87 85 variant="filled" 86 + size="md" 88 87 required 89 88 maxLength={100} 90 89 key={form.key('name')} ··· 96 95 label="Description" 97 96 placeholder="Describe what this collection is about" 98 97 variant="filled" 98 + size="md" 99 99 rows={8} 100 100 maxLength={500} 101 101 key={form.key('description')} 102 102 {...form.getInputProps('description')} 103 103 /> 104 104 105 - <Group justify="space-between"> 106 - <Button variant="outline" color="gray" onClick={props.onClose}> 105 + <Group justify="space-between" gap={'xs'} grow> 106 + <Button 107 + variant="light" 108 + size="md" 109 + color="gray" 110 + onClick={props.onClose} 111 + > 107 112 Cancel 108 113 </Button> 109 - <Button type="submit" loading={updateCollection.isPending}> 114 + <Button 115 + type="submit" 116 + size="md" 117 + loading={updateCollection.isPending} 118 + > 110 119 Update 111 120 </Button> 112 121 </Group>
+8 -3
src/webapp/features/collections/containers/collectionsContainer/CollectionsContainer.tsx
··· 11 11 import useCollections from '../../lib/queries/useCollections'; 12 12 import { BiPlus } from 'react-icons/bi'; 13 13 import CollectionCard from '../../components/collectionCard/CollectionCard'; 14 - import { useContextDrawers } from '@/providers/drawers'; 14 + import { useState } from 'react'; 15 + import CreateCollectionDrawer from '../../components/createCollectionDrawer/CreateCollectionDrawer'; 15 16 16 17 export default function CollectionsContainer() { 17 18 const { data } = useCollections(); 18 - const drawers = useContextDrawers(); 19 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 19 20 20 21 return ( 21 22 <Container p={'xs'} size={'xl'}> ··· 34 35 No collections 35 36 </Text> 36 37 <Button 37 - onClick={() => drawers.open('createCollection')} 38 + onClick={() => setIsDrawerOpen(true)} 38 39 variant="light" 39 40 color={'gray'} 40 41 size="md" ··· 42 43 > 43 44 Create your first collection 44 45 </Button> 46 + <CreateCollectionDrawer 47 + isOpen={isDrawerOpen} 48 + onClose={() => setIsDrawerOpen(false)} 49 + /> 45 50 </Stack> 46 51 )} 47 52 </Stack>
+22 -12
src/webapp/features/composer/components/composerDrawer/ComposerDrawer.tsx
··· 2 2 ActionIcon, 3 3 Affix, 4 4 Text, 5 - Box, 6 5 Menu, 7 6 Stack, 8 7 Transition, 9 8 Card, 9 + Overlay, 10 10 } from '@mantine/core'; 11 - import { useContextDrawers } from '@/providers/drawers'; 12 11 import { Fragment, useState } from 'react'; 13 12 import { FiPlus, FiX } from 'react-icons/fi'; 14 13 import { FaRegNoteSticky } from 'react-icons/fa6'; 15 14 import { BiCollection } from 'react-icons/bi'; 16 - import Link from 'next/link'; 15 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 16 + import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer'; 17 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 17 18 18 19 export default function ComposerDrawer() { 19 20 const [opened, setOpened] = useState(false); 20 - const drawers = useContextDrawers(); 21 + const [activeDrawer, setActiveDrawer] = useState< 22 + 'addCard' | 'createCollection' | null 23 + >(); 21 24 22 25 return ( 23 26 <Fragment> ··· 73 76 py={0} 74 77 > 75 78 <Menu.Item 76 - onClick={() => drawers.open('addCard')} 79 + onClick={() => setActiveDrawer('addCard')} 77 80 p={0} 78 81 style={{ cursor: 'pointer' }} 79 82 > ··· 88 91 </Card> 89 92 </Menu.Item> 90 93 <Menu.Item 91 - onClick={() => drawers.open('createCollection')} 94 + onClick={() => setActiveDrawer('createCollection')} 92 95 p={0} 93 96 style={{ cursor: 'pointer' }} 94 97 > ··· 101 104 </Menu.Item> 102 105 </Menu.Dropdown> 103 106 </Menu> 107 + 108 + <AddCardDrawer 109 + isOpen={activeDrawer === 'addCard'} 110 + onClose={() => setActiveDrawer(null)} 111 + /> 112 + <CreateCollectionDrawer 113 + isOpen={activeDrawer === 'createCollection'} 114 + onClose={() => setActiveDrawer(null)} 115 + /> 116 + 104 117 <Transition 105 118 mounted={opened} 106 119 transition="fade" ··· 108 121 timingFunction="ease" 109 122 > 110 123 {(styles) => ( 111 - <Box 124 + <Overlay 125 + blur={DEFAULT_OVERLAY_PROPS.blur} 126 + gradient={DEFAULT_OVERLAY_PROPS.gradient} 112 127 style={{ 113 128 ...styles, 114 - position: 'fixed', 115 - inset: 0, 116 129 zIndex: 101, 117 - backdropFilter: 'blur(3px)', 118 - background: 119 - 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 120 130 }} 121 131 /> 122 132 )}
+8 -3
src/webapp/features/library/containers/libraryContainer/LibraryContainer.tsx
··· 4 4 import useMyCards from '@/features/cards/lib/queries/useMyCards'; 5 5 import CollectionCard from '@/features/collections/components/collectionCard/CollectionCard'; 6 6 import useCollections from '@/features/collections/lib/queries/useCollections'; 7 - import { useContextDrawers } from '@/providers/drawers'; 7 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 8 8 import { 9 9 Anchor, 10 10 Container, ··· 17 17 Button, 18 18 } from '@mantine/core'; 19 19 import Link from 'next/link'; 20 + import { useState } from 'react'; 20 21 import { BiCollection, BiPlus } from 'react-icons/bi'; 21 22 import { FaRegNoteSticky } from 'react-icons/fa6'; 22 23 23 24 export default function LibraryContainer() { 24 25 const { data: CollectionsData } = useCollections({ limit: 4 }); 25 26 const { data: myCardsData } = useMyCards({ limit: 4 }); 26 - const drawers = useContextDrawers(); 27 + const [isCollectionDrawerOpen, setIsCollectionDrawerOpen] = useState(false); 27 28 28 29 return ( 29 30 <Container p={'xs'} size={'xl'}> ··· 54 55 No collections 55 56 </Text> 56 57 <Button 57 - onClick={() => drawers.open('createCollection')} 58 + onClick={() => setIsCollectionDrawerOpen(true)} 58 59 variant="light" 59 60 color={'gray'} 60 61 size="md" ··· 113 114 </Stack> 114 115 </Stack> 115 116 </Stack> 117 + <CreateCollectionDrawer 118 + isOpen={isCollectionDrawerOpen} 119 + onClose={() => setIsCollectionDrawerOpen(false)} 120 + /> 116 121 </Container> 117 122 ); 118 123 }
-56
src/webapp/providers/drawers.tsx
··· 1 - import React, { createContext, useContext, ReactNode } from 'react'; 2 - import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer'; 3 - import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 4 - import { Drawer, useDrawersStack } from '@mantine/core'; // Importing Mantine's hook 5 - 6 - type DrawerName = 'createCollection' | 'addCard'; 7 - 8 - interface DrawersContextType { 9 - open: <T extends DrawerName>(drawer: T) => void; 10 - close: (drawer: DrawerName) => void; 11 - closeAll: () => void; 12 - toggle: (name: DrawerName) => void; 13 - isOpen: (name: DrawerName) => boolean; 14 - } 15 - 16 - const DrawersContext = createContext<DrawersContextType | undefined>(undefined); 17 - 18 - export const useContextDrawers = () => { 19 - const ctx = useContext(DrawersContext); 20 - if (!ctx) { 21 - throw new Error('useContextDrawers must be used within a DrawersProvider'); 22 - } 23 - return ctx; 24 - }; 25 - 26 - export const DrawersProvider = ({ children }: { children: ReactNode }) => { 27 - // Initialize the drawer stack with the available drawers 28 - // https://mantine.dev/core/drawer/#usedrawersstack-hook 29 - const stack = useDrawersStack<DrawerName>(['addCard', 'createCollection']); 30 - 31 - return ( 32 - <DrawersContext.Provider 33 - value={{ 34 - open: stack.open, 35 - close: stack.close, 36 - closeAll: stack.closeAll, 37 - isOpen: (name: DrawerName) => stack.state[name], // Access the state object to check if the drawer is open 38 - toggle: stack.toggle, 39 - }} 40 - > 41 - {children} 42 - 43 - <Drawer.Stack> 44 - {/* Render Drawers */} 45 - <AddCardDrawer 46 - {...stack.register('addCard')} 47 - isOpen={stack.state['addCard']} 48 - /> 49 - <CreateCollectionDrawer 50 - {...stack.register('createCollection')} 51 - isOpen={stack.state['createCollection']} 52 - /> 53 - </Drawer.Stack> 54 - </DrawersContext.Provider> 55 - ); 56 - };
+1 -5
src/webapp/providers/index.tsx
··· 4 4 import MantineProvider from './mantine'; 5 5 import TanStackQueryProvider from './tanstack'; 6 6 import { NavbarProvider } from './navbar'; 7 - import { DrawersProvider } from './drawers'; 8 - import { Notifications } from '@mantine/notifications'; 9 7 10 8 interface Props { 11 9 children: React.ReactNode; ··· 16 14 <TanStackQueryProvider> 17 15 <AuthProvider> 18 16 <MantineProvider> 19 - <DrawersProvider> 20 - <NavbarProvider>{props.children}</NavbarProvider> 21 - </DrawersProvider> 17 + <NavbarProvider>{props.children}</NavbarProvider> 22 18 </MantineProvider> 23 19 </AuthProvider> 24 20 </TanStackQueryProvider>
+5
src/webapp/styles/overlays.ts
··· 1 + export const DEFAULT_OVERLAY_PROPS = { 2 + blur: 3, 3 + gradient: 4 + 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 5 + };
-4
src/webapp/styles/theme.tsx
··· 1 1 'use client'; 2 2 3 3 import { 4 - Anchor, 5 4 Avatar, 6 5 Button, 7 - Checkbox, 8 - CheckboxCard, 9 6 CheckboxIndicator, 10 7 createTheme, 11 8 MenuItem, 12 9 NavLink, 13 - TextInput, 14 10 } from '@mantine/core'; 15 11 16 12 export const theme = createTheme({