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 </Text> 144 )} 145 146 - <Group grow> 147 <Button 148 onClick={handleSubmit} 149 disabled={submitting || selectedCollectionIds.length === 0}
··· 143 </Text> 144 )} 145 146 + <Group gap={"xs"} grow> 147 <Button 148 onClick={handleSubmit} 149 disabled={submitting || selectedCollectionIds.length === 0}
+36 -44
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
··· 2 Button, 3 Container, 4 Drawer, 5 - Text, 6 Group, 7 Stack, 8 Textarea, 9 TextInput, 10 - Anchor, 11 } from '@mantine/core'; 12 import { useForm } from '@mantine/form'; 13 14 import { notifications } from '@mantine/notifications'; 15 import useAddCard from '../../lib/mutations/useAddCard'; 16 import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector'; 17 - import { Fragment, Suspense, useState } from 'react'; 18 import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 19 - import { IoMdLink } from 'react-icons/io'; 20 import { useDisclosure } from '@mantine/hooks'; 21 import { BiCollection } from 'react-icons/bi'; 22 - import Link from 'next/link'; 23 24 interface Props { 25 isOpen: boolean; ··· 37 useState<{ id: string; name: string; cardCount: number }[]>( 38 initialCollections, 39 ); 40 const addCard = useAddCard(); 41 42 const form = useForm({ ··· 47 }, 48 }); 49 50 - const handleCreateCollection = (e: React.FormEvent) => { 51 e.preventDefault(); 52 53 addCard.mutate( ··· 84 }} 85 withCloseButton={false} 86 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 - }} 93 > 94 <Drawer.Header> 95 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 97 </Drawer.Title> 98 </Drawer.Header> 99 <Container size={'sm'}> 100 - <form onSubmit={handleCreateCollection}> 101 - <Stack> 102 <Stack> 103 <Stack> 104 <TextInput ··· 129 130 <Group> 131 <Stack align="start" gap={'xs'}> 132 - <Button 133 - onClick={toggleCollectionSelector} 134 - variant="light" 135 - color="gray" 136 - leftSection={<BiCollection size={22} />} 137 > 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 - )} 160 </Stack> 161 </Group> 162 <Suspense fallback={<CollectionSelectorSkeleton />}> ··· 168 /> 169 </Suspense> 170 </Stack> 171 - <Group justify="space-between"> 172 - <Button variant="outline" color={'gray'} onClick={props.onClose}> 173 Cancel 174 </Button> 175 - <Button type="submit" loading={addCard.isPending}> 176 Add card 177 </Button> 178 </Group>
··· 2 Button, 3 Container, 4 Drawer, 5 Group, 6 Stack, 7 Textarea, 8 TextInput, 9 + Tooltip, 10 } from '@mantine/core'; 11 import { useForm } from '@mantine/form'; 12 13 import { notifications } from '@mantine/notifications'; 14 import useAddCard from '../../lib/mutations/useAddCard'; 15 import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector'; 16 + import { Suspense, useState } from 'react'; 17 import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 18 import { useDisclosure } from '@mantine/hooks'; 19 import { BiCollection } from 'react-icons/bi'; 20 + import { IoMdLink } from 'react-icons/io'; 21 + 22 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 23 24 interface Props { 25 isOpen: boolean; ··· 37 useState<{ id: string; name: string; cardCount: number }[]>( 38 initialCollections, 39 ); 40 + 41 + const hasNoCollections = selectedCollections.length === 0; 42 + const hasOneCollection = selectedCollections.length === 1; 43 + 44 const addCard = useAddCard(); 45 46 const form = useForm({ ··· 51 }, 52 }); 53 54 + const handleAddCard = (e: React.FormEvent) => { 55 e.preventDefault(); 56 57 addCard.mutate( ··· 88 }} 89 withCloseButton={false} 90 position="bottom" 91 + size={'26rem'} 92 + overlayProps={DEFAULT_OVERLAY_PROPS} 93 > 94 <Drawer.Header> 95 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 97 </Drawer.Title> 98 </Drawer.Header> 99 <Container size={'sm'}> 100 + <form onSubmit={handleAddCard}> 101 + <Stack gap={'xl'}> 102 <Stack> 103 <Stack> 104 <TextInput ··· 129 130 <Group> 131 <Stack align="start" gap={'xs'}> 132 + <Tooltip 133 + label={selectedCollections.map((c) => c.name).join(', ')} 134 + disabled={hasNoCollections} 135 > 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> 147 </Stack> 148 </Group> 149 <Suspense fallback={<CollectionSelectorSkeleton />}> ··· 155 /> 156 </Suspense> 157 </Stack> 158 + <Group justify="space-between" gap={'xs'} grow> 159 + <Button 160 + variant="light" 161 + size="md" 162 + color={'gray'} 163 + onClick={props.onClose} 164 + > 165 Cancel 166 </Button> 167 + <Button type="submit" size="md" loading={addCard.isPending}> 168 Add card 169 </Button> 170 </Group>
+8 -1
src/webapp/features/cards/lib/mutations/useAddCard.tsx
··· 26 // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks 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 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 29 - onSuccess: () => { 30 queryClient.invalidateQueries({ queryKey: ['my cards'] }); 31 }, 32 }); 33
··· 26 // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks 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 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 29 + onSuccess: (_data, variables) => { 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 + }); 38 }, 39 }); 40
+158 -123
src/webapp/features/collections/components/collectionSelector/CollectionSelector.tsx
··· 10 Button, 11 Drawer, 12 Container, 13 - SimpleGrid, 14 Group, 15 } from '@mantine/core'; 16 - import { IoSearch } from 'react-icons/io5'; 17 - import { useState } from 'react'; 18 import useCollections from '../../lib/queries/useCollections'; 19 import useCollectionSearch from '../../lib/queries/useCollectionSearch'; 20 - import { useDebouncedValue } from '@mantine/hooks'; 21 - 22 import CollectionSelectorError from './Error.CollectionSelector'; 23 import CollectionSelectorItem from '../collectionSelectorItem/CollectionSelectorItem'; 24 - import CollectionSelectorNewCollection from '../collectionSelectorNewCollection/CollectionSelectorNewCollection'; 25 import { BiPlus } from 'react-icons/bi'; 26 - import { useContextDrawers } from '@/providers/drawers'; 27 28 interface Props { 29 isOpen: boolean; ··· 39 const [search, setSearch] = useState<string>(''); 40 const [debouncedSearch] = useDebouncedValue(search, 200); 41 const searchedCollections = useCollectionSearch({ query: debouncedSearch }); 42 - const drawers = useContextDrawers(); 43 44 const handleCollectionChange = ( 45 checked: boolean, ··· 76 } 77 78 const hasCollections = data?.collections?.length > 0; 79 80 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> 123 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 - /> 144 )} 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> 169 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> 199 ); 200 }
··· 10 Button, 11 Drawer, 12 Container, 13 Group, 14 } from '@mantine/core'; 15 + import { Fragment, useState } from 'react'; 16 + import { useDebouncedValue } from '@mantine/hooks'; 17 import useCollections from '../../lib/queries/useCollections'; 18 import useCollectionSearch from '../../lib/queries/useCollectionSearch'; 19 + import CreateCollectionDrawer from '../createCollectionDrawer/CreateCollectionDrawer'; 20 import CollectionSelectorError from './Error.CollectionSelector'; 21 import CollectionSelectorItem from '../collectionSelectorItem/CollectionSelectorItem'; 22 import { BiPlus } from 'react-icons/bi'; 23 + import { IoSearch } from 'react-icons/io5'; 24 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 25 26 interface Props { 27 isOpen: boolean; ··· 37 const [search, setSearch] = useState<string>(''); 38 const [debouncedSearch] = useDebouncedValue(search, 200); 39 const searchedCollections = useCollectionSearch({ query: debouncedSearch }); 40 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 41 42 const handleCollectionChange = ( 43 checked: boolean, ··· 74 } 75 76 const hasCollections = data?.collections?.length > 0; 77 + const hasSelectedCollections = props.selectedCollections.length > 0; 78 79 return ( 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> 136 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> 174 )} 175 + </Stack> 176 + </ScrollArea> 177 + </Tabs.Panel> 178 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> 234 ); 235 }
+3
src/webapp/features/collections/components/collectionSelectorItem/CollectionSelectorItem.tsx
··· 39 <Text fw={500} lineClamp={1}> 40 {props.name} 41 </Text> 42 </Stack> 43 <CheckboxIndicator /> 44 </Group>
··· 39 <Text fw={500} lineClamp={1}> 40 {props.name} 41 </Text> 42 + <Text c={'gray'}> 43 + {props.cardCount} {props.cardCount === 1 ? 'card' : 'cards'} 44 + </Text> 45 </Stack> 46 <CheckboxIndicator /> 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 import { useForm } from '@mantine/form'; 11 import useCreateCollection from '../../lib/mutations/useCreateCollection'; 12 import { notifications } from '@mantine/notifications'; 13 14 interface Props { 15 isOpen: boolean; 16 onClose: () => void; 17 initialName?: string; 18 } 19 20 export default function createCollectionDrawer(props: Props) { ··· 35 description: form.getValues().description, 36 }, 37 { 38 - onSuccess: () => { 39 notifications.show({ 40 message: `Created collection "${form.getValues().name}".`, 41 }); 42 props.onClose(); 43 }, 44 onError: () => { 45 notifications.show({ ··· 59 onClose={props.onClose} 60 withCloseButton={false} 61 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 - }} 67 > 68 <Drawer.Header> 69 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 72 </Drawer.Header> 73 74 <Container size={'sm'}> 75 - <form onSubmit={handleCreateCollection}> 76 <Stack> 77 <TextInput 78 id="name" ··· 80 type="text" 81 placeholder="Collection name" 82 variant="filled" 83 required 84 maxLength={100} 85 key={form.key('name')} ··· 91 label="Description" 92 placeholder="Describe what this collection is about" 93 variant="filled" 94 rows={8} 95 maxLength={500} 96 key={form.key('description')} 97 {...form.getInputProps('description')} 98 /> 99 - <Group justify="space-between"> 100 - <Button variant="outline" color={'gray'} onClick={props.onClose}> 101 Cancel 102 </Button> 103 - <Button type="submit" loading={createCollection.isPending}> 104 Create 105 </Button> 106 </Group>
··· 10 import { useForm } from '@mantine/form'; 11 import useCreateCollection from '../../lib/mutations/useCreateCollection'; 12 import { notifications } from '@mantine/notifications'; 13 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 14 15 interface Props { 16 isOpen: boolean; 17 onClose: () => void; 18 initialName?: string; 19 + onCreate?: (newCollection: { 20 + id: string; 21 + name: string; 22 + cardCount: number; 23 + }) => void; 24 } 25 26 export default function createCollectionDrawer(props: Props) { ··· 41 description: form.getValues().description, 42 }, 43 { 44 + onSuccess: (newCollection) => { 45 notifications.show({ 46 message: `Created collection "${form.getValues().name}".`, 47 }); 48 + 49 props.onClose(); 50 + props.onCreate && 51 + props.onCreate({ 52 + id: newCollection.collectionId, 53 + name: form.getValues().name, 54 + cardCount: 0, 55 + }); 56 }, 57 onError: () => { 58 notifications.show({ ··· 72 onClose={props.onClose} 73 withCloseButton={false} 74 position="bottom" 75 + size={'30rem'} 76 + overlayProps={DEFAULT_OVERLAY_PROPS} 77 > 78 <Drawer.Header> 79 <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> ··· 82 </Drawer.Header> 83 84 <Container size={'sm'}> 85 + <form> 86 <Stack> 87 <TextInput 88 id="name" ··· 90 type="text" 91 placeholder="Collection name" 92 variant="filled" 93 + size="md" 94 required 95 maxLength={100} 96 key={form.key('name')} ··· 102 label="Description" 103 placeholder="Describe what this collection is about" 104 variant="filled" 105 + size="md" 106 rows={8} 107 maxLength={500} 108 key={form.key('description')} 109 {...form.getInputProps('description')} 110 /> 111 + <Group justify="space-between" gap={'xs'} grow> 112 + <Button 113 + variant="light" 114 + size="md" 115 + color={'gray'} 116 + onClick={props.onClose} 117 + > 118 Cancel 119 </Button> 120 + <Button 121 + onClick={handleCreateCollection} 122 + size="md" 123 + loading={createCollection.isPending} 124 + > 125 Create 126 </Button> 127 </Group>
+17 -10
src/webapp/features/collections/components/createCollectionShortcut/CreateCollectionShortcut.tsx
··· 1 - import { useContextDrawers } from '@/providers/drawers'; 2 import { NavLink } from '@mantine/core'; 3 import { BiPlus } from 'react-icons/bi'; 4 5 export default function CreateCollectionShortcut() { 6 - const drawers = useContextDrawers(); 7 8 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 - /> 17 ); 18 }
··· 1 import { NavLink } from '@mantine/core'; 2 + import { Fragment, useState } from 'react'; 3 import { BiPlus } from 'react-icons/bi'; 4 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 5 6 export default function CreateCollectionShortcut() { 7 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 8 9 return ( 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> 24 ); 25 }
+17 -8
src/webapp/features/collections/components/editCollectionDrawer/EditCollectionDrawer.tsx
··· 10 import { useForm } from '@mantine/form'; 11 import { notifications } from '@mantine/notifications'; 12 import useUpdateCollection from '../../lib/mutations/useUpdateCollection'; 13 14 interface Props { 15 isOpen: boolean; ··· 65 onClose={props.onClose} 66 withCloseButton={false} 67 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 - }} 73 > 74 <Drawer.Header> 75 <Drawer.Title fz="xl" fw={600} mx="auto"> ··· 85 label="Name" 86 placeholder="Collection name" 87 variant="filled" 88 required 89 maxLength={100} 90 key={form.key('name')} ··· 96 label="Description" 97 placeholder="Describe what this collection is about" 98 variant="filled" 99 rows={8} 100 maxLength={500} 101 key={form.key('description')} 102 {...form.getInputProps('description')} 103 /> 104 105 - <Group justify="space-between"> 106 - <Button variant="outline" color="gray" onClick={props.onClose}> 107 Cancel 108 </Button> 109 - <Button type="submit" loading={updateCollection.isPending}> 110 Update 111 </Button> 112 </Group>
··· 10 import { useForm } from '@mantine/form'; 11 import { notifications } from '@mantine/notifications'; 12 import useUpdateCollection from '../../lib/mutations/useUpdateCollection'; 13 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 14 15 interface Props { 16 isOpen: boolean; ··· 66 onClose={props.onClose} 67 withCloseButton={false} 68 position="bottom" 69 + size={'30rem'} 70 + overlayProps={DEFAULT_OVERLAY_PROPS} 71 > 72 <Drawer.Header> 73 <Drawer.Title fz="xl" fw={600} mx="auto"> ··· 83 label="Name" 84 placeholder="Collection name" 85 variant="filled" 86 + size="md" 87 required 88 maxLength={100} 89 key={form.key('name')} ··· 95 label="Description" 96 placeholder="Describe what this collection is about" 97 variant="filled" 98 + size="md" 99 rows={8} 100 maxLength={500} 101 key={form.key('description')} 102 {...form.getInputProps('description')} 103 /> 104 105 + <Group justify="space-between" gap={'xs'} grow> 106 + <Button 107 + variant="light" 108 + size="md" 109 + color="gray" 110 + onClick={props.onClose} 111 + > 112 Cancel 113 </Button> 114 + <Button 115 + type="submit" 116 + size="md" 117 + loading={updateCollection.isPending} 118 + > 119 Update 120 </Button> 121 </Group>
+8 -3
src/webapp/features/collections/containers/collectionsContainer/CollectionsContainer.tsx
··· 11 import useCollections from '../../lib/queries/useCollections'; 12 import { BiPlus } from 'react-icons/bi'; 13 import CollectionCard from '../../components/collectionCard/CollectionCard'; 14 - import { useContextDrawers } from '@/providers/drawers'; 15 16 export default function CollectionsContainer() { 17 const { data } = useCollections(); 18 - const drawers = useContextDrawers(); 19 20 return ( 21 <Container p={'xs'} size={'xl'}> ··· 34 No collections 35 </Text> 36 <Button 37 - onClick={() => drawers.open('createCollection')} 38 variant="light" 39 color={'gray'} 40 size="md" ··· 42 > 43 Create your first collection 44 </Button> 45 </Stack> 46 )} 47 </Stack>
··· 11 import useCollections from '../../lib/queries/useCollections'; 12 import { BiPlus } from 'react-icons/bi'; 13 import CollectionCard from '../../components/collectionCard/CollectionCard'; 14 + import { useState } from 'react'; 15 + import CreateCollectionDrawer from '../../components/createCollectionDrawer/CreateCollectionDrawer'; 16 17 export default function CollectionsContainer() { 18 const { data } = useCollections(); 19 + const [isDrawerOpen, setIsDrawerOpen] = useState(false); 20 21 return ( 22 <Container p={'xs'} size={'xl'}> ··· 35 No collections 36 </Text> 37 <Button 38 + onClick={() => setIsDrawerOpen(true)} 39 variant="light" 40 color={'gray'} 41 size="md" ··· 43 > 44 Create your first collection 45 </Button> 46 + <CreateCollectionDrawer 47 + isOpen={isDrawerOpen} 48 + onClose={() => setIsDrawerOpen(false)} 49 + /> 50 </Stack> 51 )} 52 </Stack>
+22 -12
src/webapp/features/composer/components/composerDrawer/ComposerDrawer.tsx
··· 2 ActionIcon, 3 Affix, 4 Text, 5 - Box, 6 Menu, 7 Stack, 8 Transition, 9 Card, 10 } from '@mantine/core'; 11 - import { useContextDrawers } from '@/providers/drawers'; 12 import { Fragment, useState } from 'react'; 13 import { FiPlus, FiX } from 'react-icons/fi'; 14 import { FaRegNoteSticky } from 'react-icons/fa6'; 15 import { BiCollection } from 'react-icons/bi'; 16 - import Link from 'next/link'; 17 18 export default function ComposerDrawer() { 19 const [opened, setOpened] = useState(false); 20 - const drawers = useContextDrawers(); 21 22 return ( 23 <Fragment> ··· 73 py={0} 74 > 75 <Menu.Item 76 - onClick={() => drawers.open('addCard')} 77 p={0} 78 style={{ cursor: 'pointer' }} 79 > ··· 88 </Card> 89 </Menu.Item> 90 <Menu.Item 91 - onClick={() => drawers.open('createCollection')} 92 p={0} 93 style={{ cursor: 'pointer' }} 94 > ··· 101 </Menu.Item> 102 </Menu.Dropdown> 103 </Menu> 104 <Transition 105 mounted={opened} 106 transition="fade" ··· 108 timingFunction="ease" 109 > 110 {(styles) => ( 111 - <Box 112 style={{ 113 ...styles, 114 - position: 'fixed', 115 - inset: 0, 116 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 }} 121 /> 122 )}
··· 2 ActionIcon, 3 Affix, 4 Text, 5 Menu, 6 Stack, 7 Transition, 8 Card, 9 + Overlay, 10 } from '@mantine/core'; 11 import { Fragment, useState } from 'react'; 12 import { FiPlus, FiX } from 'react-icons/fi'; 13 import { FaRegNoteSticky } from 'react-icons/fa6'; 14 import { BiCollection } from 'react-icons/bi'; 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'; 18 19 export default function ComposerDrawer() { 20 const [opened, setOpened] = useState(false); 21 + const [activeDrawer, setActiveDrawer] = useState< 22 + 'addCard' | 'createCollection' | null 23 + >(); 24 25 return ( 26 <Fragment> ··· 76 py={0} 77 > 78 <Menu.Item 79 + onClick={() => setActiveDrawer('addCard')} 80 p={0} 81 style={{ cursor: 'pointer' }} 82 > ··· 91 </Card> 92 </Menu.Item> 93 <Menu.Item 94 + onClick={() => setActiveDrawer('createCollection')} 95 p={0} 96 style={{ cursor: 'pointer' }} 97 > ··· 104 </Menu.Item> 105 </Menu.Dropdown> 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 + 117 <Transition 118 mounted={opened} 119 transition="fade" ··· 121 timingFunction="ease" 122 > 123 {(styles) => ( 124 + <Overlay 125 + blur={DEFAULT_OVERLAY_PROPS.blur} 126 + gradient={DEFAULT_OVERLAY_PROPS.gradient} 127 style={{ 128 ...styles, 129 zIndex: 101, 130 }} 131 /> 132 )}
+8 -3
src/webapp/features/library/containers/libraryContainer/LibraryContainer.tsx
··· 4 import useMyCards from '@/features/cards/lib/queries/useMyCards'; 5 import CollectionCard from '@/features/collections/components/collectionCard/CollectionCard'; 6 import useCollections from '@/features/collections/lib/queries/useCollections'; 7 - import { useContextDrawers } from '@/providers/drawers'; 8 import { 9 Anchor, 10 Container, ··· 17 Button, 18 } from '@mantine/core'; 19 import Link from 'next/link'; 20 import { BiCollection, BiPlus } from 'react-icons/bi'; 21 import { FaRegNoteSticky } from 'react-icons/fa6'; 22 23 export default function LibraryContainer() { 24 const { data: CollectionsData } = useCollections({ limit: 4 }); 25 const { data: myCardsData } = useMyCards({ limit: 4 }); 26 - const drawers = useContextDrawers(); 27 28 return ( 29 <Container p={'xs'} size={'xl'}> ··· 54 No collections 55 </Text> 56 <Button 57 - onClick={() => drawers.open('createCollection')} 58 variant="light" 59 color={'gray'} 60 size="md" ··· 113 </Stack> 114 </Stack> 115 </Stack> 116 </Container> 117 ); 118 }
··· 4 import useMyCards from '@/features/cards/lib/queries/useMyCards'; 5 import CollectionCard from '@/features/collections/components/collectionCard/CollectionCard'; 6 import useCollections from '@/features/collections/lib/queries/useCollections'; 7 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 8 import { 9 Anchor, 10 Container, ··· 17 Button, 18 } from '@mantine/core'; 19 import Link from 'next/link'; 20 + import { useState } from 'react'; 21 import { BiCollection, BiPlus } from 'react-icons/bi'; 22 import { FaRegNoteSticky } from 'react-icons/fa6'; 23 24 export default function LibraryContainer() { 25 const { data: CollectionsData } = useCollections({ limit: 4 }); 26 const { data: myCardsData } = useMyCards({ limit: 4 }); 27 + const [isCollectionDrawerOpen, setIsCollectionDrawerOpen] = useState(false); 28 29 return ( 30 <Container p={'xs'} size={'xl'}> ··· 55 No collections 56 </Text> 57 <Button 58 + onClick={() => setIsCollectionDrawerOpen(true)} 59 variant="light" 60 color={'gray'} 61 size="md" ··· 114 </Stack> 115 </Stack> 116 </Stack> 117 + <CreateCollectionDrawer 118 + isOpen={isCollectionDrawerOpen} 119 + onClose={() => setIsCollectionDrawerOpen(false)} 120 + /> 121 </Container> 122 ); 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 import MantineProvider from './mantine'; 5 import TanStackQueryProvider from './tanstack'; 6 import { NavbarProvider } from './navbar'; 7 - import { DrawersProvider } from './drawers'; 8 - import { Notifications } from '@mantine/notifications'; 9 10 interface Props { 11 children: React.ReactNode; ··· 16 <TanStackQueryProvider> 17 <AuthProvider> 18 <MantineProvider> 19 - <DrawersProvider> 20 - <NavbarProvider>{props.children}</NavbarProvider> 21 - </DrawersProvider> 22 </MantineProvider> 23 </AuthProvider> 24 </TanStackQueryProvider>
··· 4 import MantineProvider from './mantine'; 5 import TanStackQueryProvider from './tanstack'; 6 import { NavbarProvider } from './navbar'; 7 8 interface Props { 9 children: React.ReactNode; ··· 14 <TanStackQueryProvider> 15 <AuthProvider> 16 <MantineProvider> 17 + <NavbarProvider>{props.children}</NavbarProvider> 18 </MantineProvider> 19 </AuthProvider> 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 'use client'; 2 3 import { 4 - Anchor, 5 Avatar, 6 Button, 7 - Checkbox, 8 - CheckboxCard, 9 CheckboxIndicator, 10 createTheme, 11 MenuItem, 12 NavLink, 13 - TextInput, 14 } from '@mantine/core'; 15 16 export const theme = createTheme({
··· 1 'use client'; 2 3 import { 4 Avatar, 5 Button, 6 CheckboxIndicator, 7 createTheme, 8 MenuItem, 9 NavLink, 10 } from '@mantine/core'; 11 12 export const theme = createTheme({