This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

chore: adjust drawer sizing

+308 -411
+13 -274
src/webapp/features/cards/components/addCardDrawer/AddCardDrawer.tsx
··· 1 - import { 2 - Button, 3 - Container, 4 - Drawer, 5 - Flex, 6 - Group, 7 - Input, 8 - ScrollArea, 9 - Stack, 10 - Text, 11 - Textarea, 12 - TextInput, 13 - ThemeIcon, 14 - VisuallyHidden, 15 - } from '@mantine/core'; 16 - import { useForm } from '@mantine/form'; 17 - import { notifications } from '@mantine/notifications'; 18 - import useAddCard from '../../lib/mutations/useAddCard'; 19 - import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector'; 20 - import { Suspense, useEffect, useState } from 'react'; 21 - import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 22 - import { useDisclosure } from '@mantine/hooks'; 23 - import { BiCollection } from 'react-icons/bi'; 24 - import { IoMdCheckmark, IoMdLink } from 'react-icons/io'; 1 + 'use client'; 2 + 3 + import { Container, Drawer } from '@mantine/core'; 25 4 import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 26 - import { track } from '@vercel/analytics'; 27 - import useMyCollections from '@/features/collections/lib/queries/useMyCollections'; 28 - import { isMarginUri, getMarginUrl } from '@/lib/utils/margin'; 29 - import MarginLogo from '@/components/MarginLogo'; 30 - import { Collection, CollectionAccessType } from '@semble/types'; 31 - import { FaSeedling } from 'react-icons/fa6'; 32 - import { CardSaveSource } from '@/features/analytics/types'; 33 - import { usePathname } from 'next/navigation'; 5 + import AddCardForm from './AddCardForm'; 6 + import { Collection } from '@semble/types'; 34 7 35 8 interface Props { 36 9 isOpen: boolean; ··· 40 13 } 41 14 42 15 export default function AddCardDrawer(props: Props) { 43 - const pathname = usePathname(); 44 - const [collectionSelectorOpened, { toggle: toggleCollectionSelector }] = 45 - useDisclosure(false); 46 - const initialCollections = props.selectedCollection 47 - ? [props.selectedCollection] 48 - : []; 49 - const [selectedCollections, setSelectedCollections] = 50 - useState(initialCollections); 51 - 52 - const { data: collections } = useMyCollections({ limit: 30 }); 53 - const allCollections = 54 - collections?.pages.flatMap((page) => page.collections ?? []) ?? []; 55 - 56 - // Put selectedCollection first, then others 57 - const myCollections = props.selectedCollection 58 - ? [ 59 - props.selectedCollection, 60 - ...allCollections.filter((c) => c.id !== props.selectedCollection?.id), 61 - ] 62 - : allCollections; 63 - 64 - const addCard = useAddCard({ 65 - saveSource: CardSaveSource.ADD_CARD_DRAWER, 66 - pagePath: pathname, 67 - }); 68 - 69 - const form = useForm({ 70 - initialValues: { 71 - url: props.initialUrl || '', 72 - note: '', 73 - collections: selectedCollections, 74 - }, 75 - }); 76 - 77 - const MAX_NOTE_LENGTH = 500; 78 - const { note } = form.getValues(); 79 - 80 - useEffect(() => { 81 - if (props.initialUrl) { 82 - form.setValues({ url: props.initialUrl }); 83 - } 84 - }, [props.initialUrl]); 85 - 86 - const handleAddCard = (e: React.FormEvent) => { 87 - e.preventDefault(); 88 - track('add new card'); 89 - 90 - addCard.mutate( 91 - { 92 - url: form.getValues().url, 93 - note: form.getValues().note, 94 - collectionIds: selectedCollections.map((c) => c.id), 95 - }, 96 - { 97 - onSuccess: () => { 98 - setSelectedCollections(initialCollections); 99 - props.onClose(); 100 - window.history.replaceState({}, '', window.location.pathname); 101 - }, 102 - onError: () => { 103 - notifications.show({ 104 - message: 'Could not add card.', 105 - }); 106 - }, 107 - onSettled: () => { 108 - form.reset(); 109 - }, 110 - }, 111 - ); 112 - }; 113 - 114 16 return ( 115 17 <Drawer 116 18 opened={props.isOpen} 117 - onClose={() => { 118 - props.onClose(); 119 - }} 19 + onClose={props.onClose} 120 20 withCloseButton={false} 121 - size={'29rem'} 21 + size={'30.5rem'} 22 + padding={'sm'} 122 23 position="bottom" 123 24 overlayProps={DEFAULT_OVERLAY_PROPS} 124 25 > ··· 128 29 </Drawer.Title> 129 30 </Drawer.Header> 130 31 <Container size={'sm'} p={0}> 131 - <form onSubmit={handleAddCard}> 132 - <Stack gap={'lg'}> 133 - <Stack> 134 - <Stack> 135 - <TextInput 136 - id="url" 137 - label="URL" 138 - type="url" 139 - placeholder="https://www.example.com" 140 - variant="filled" 141 - required 142 - size="md" 143 - leftSection={<IoMdLink size={22} />} 144 - key={form.key('url')} 145 - {...form.getInputProps('url')} 146 - /> 147 - 148 - <Stack gap={0}> 149 - <Flex justify="space-between"> 150 - <Input.Label size="md" htmlFor="note"> 151 - Note 152 - </Input.Label> 153 - <Text c={'gray'} aria-hidden> 154 - {form.getValues().note.length} / {MAX_NOTE_LENGTH} 155 - </Text> 156 - </Flex> 157 - 158 - <Textarea 159 - id="note" 160 - placeholder="Add a note about this card" 161 - variant="filled" 162 - size="md" 163 - rows={3} 164 - maxLength={MAX_NOTE_LENGTH} 165 - aria-describedby="note-char-remaining" 166 - key={form.key('note')} 167 - {...form.getInputProps('note')} 168 - /> 169 - <VisuallyHidden id="note-char-remaining" aria-live="polite"> 170 - {`${MAX_NOTE_LENGTH - form.getValues().note.length} characters remaining`} 171 - </VisuallyHidden> 172 - </Stack> 173 - </Stack> 174 - 175 - <Stack gap={5}> 176 - <Text fw={500}> 177 - Add to collections{' '} 178 - {selectedCollections.length > 0 && 179 - `(${selectedCollections.length})`} 180 - </Text> 181 - <ScrollArea.Autosize 182 - type="hover" 183 - scrollbars="x" 184 - offsetScrollbars={true} 185 - > 186 - <Group gap={'xs'} wrap="nowrap"> 187 - <Button 188 - onClick={toggleCollectionSelector} 189 - variant="light" 190 - color={'blue'} 191 - leftSection={<BiCollection size={22} />} 192 - > 193 - {myCollections.length === 0 194 - ? 'Create a collection' 195 - : 'Manage & Create'} 196 - </Button> 197 - 198 - {myCollections.map((col) => { 199 - const marginUrl = getMarginUrl( 200 - col.uri, 201 - col.author?.handle, 202 - ); 203 - return ( 204 - <Button 205 - key={col.id} 206 - variant="light" 207 - color={ 208 - selectedCollections.some((c) => c.id === col.id) 209 - ? 'grape' 210 - : 'gray' 211 - } 212 - rightSection={ 213 - selectedCollections.some((c) => c.id === col.id) ? ( 214 - <IoMdCheckmark /> 215 - ) : null 216 - } 217 - leftSection={ 218 - isMarginUri(col.uri) ? ( 219 - <MarginLogo size={12} marginUrl={marginUrl} /> 220 - ) : col.accessType === CollectionAccessType.OPEN ? ( 221 - <ThemeIcon 222 - variant="light" 223 - radius={'xl'} 224 - size={'xs'} 225 - color="green" 226 - > 227 - <FaSeedling size={8} /> 228 - </ThemeIcon> 229 - ) : undefined 230 - } 231 - onClick={() => { 232 - setSelectedCollections((prev) => { 233 - // already selected, remove 234 - if (prev.some((c) => c.id === col.id)) { 235 - return prev.filter((c) => c.id !== col.id); 236 - } 237 - // not selected, add it 238 - return [...prev, col]; 239 - }); 240 - }} 241 - > 242 - {col.name} 243 - </Button> 244 - ); 245 - })} 246 - </Group> 247 - </ScrollArea.Autosize> 248 - </Stack> 249 - 250 - <Drawer 251 - opened={collectionSelectorOpened} 252 - onClose={toggleCollectionSelector} 253 - withCloseButton={false} 254 - position="bottom" 255 - size={'29rem'} 256 - overlayProps={DEFAULT_OVERLAY_PROPS} 257 - > 258 - <Drawer.Header> 259 - <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> 260 - Add to collections 261 - </Drawer.Title> 262 - </Drawer.Header> 263 - <Container size={'xs'} p={0}> 264 - <Suspense fallback={<CollectionSelectorSkeleton />}> 265 - <CollectionSelector 266 - isOpen={collectionSelectorOpened} 267 - onCancel={() => { 268 - setSelectedCollections(initialCollections); 269 - toggleCollectionSelector(); 270 - }} 271 - onClose={toggleCollectionSelector} 272 - onSave={toggleCollectionSelector} 273 - selectedCollections={selectedCollections} 274 - onSelectedCollectionsChange={setSelectedCollections} 275 - /> 276 - </Suspense> 277 - </Container> 278 - </Drawer> 279 - </Stack> 280 - <Group justify="space-between" gap={'xs'} grow> 281 - <Button 282 - variant="light" 283 - size="md" 284 - color={'gray'} 285 - onClick={() => { 286 - props.onClose(); 287 - setSelectedCollections(initialCollections); 288 - }} 289 - > 290 - Cancel 291 - </Button> 292 - <Button type="submit" size="md" loading={addCard.isPending}> 293 - Add card 294 - </Button> 295 - </Group> 296 - </Stack> 297 - </form> 32 + <AddCardForm 33 + onClose={props.onClose} 34 + selectedCollection={props.selectedCollection} 35 + initialUrl={props.initialUrl} 36 + /> 298 37 </Container> 299 38 </Drawer> 300 39 );
+282
src/webapp/features/cards/components/addCardDrawer/AddCardForm.tsx
··· 1 + 'use client'; 2 + 3 + import { 4 + Button, 5 + Drawer, 6 + Flex, 7 + Group, 8 + Input, 9 + ScrollArea, 10 + Stack, 11 + Text, 12 + Textarea, 13 + TextInput, 14 + ThemeIcon, 15 + VisuallyHidden, 16 + Container, 17 + } from '@mantine/core'; 18 + import { useForm } from '@mantine/form'; 19 + import { notifications } from '@mantine/notifications'; 20 + import useAddCard from '../../lib/mutations/useAddCard'; 21 + import CollectionSelector from '@/features/collections/components/collectionSelector/CollectionSelector'; 22 + import { Suspense, useEffect, useState } from 'react'; 23 + import CollectionSelectorSkeleton from '@/features/collections/components/collectionSelector/Skeleton.CollectionSelector'; 24 + import { useDisclosure } from '@mantine/hooks'; 25 + import { BiCollection } from 'react-icons/bi'; 26 + import { IoMdCheckmark, IoMdLink } from 'react-icons/io'; 27 + import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 28 + import { track } from '@vercel/analytics'; 29 + import useMyCollections from '@/features/collections/lib/queries/useMyCollections'; 30 + import { isMarginUri, getMarginUrl } from '@/lib/utils/margin'; 31 + import MarginLogo from '@/components/MarginLogo'; 32 + import { Collection, CollectionAccessType } from '@semble/types'; 33 + import { FaSeedling } from 'react-icons/fa6'; 34 + import { CardSaveSource } from '@/features/analytics/types'; 35 + import { usePathname } from 'next/navigation'; 36 + 37 + interface Props { 38 + onClose: () => void; 39 + selectedCollection?: Collection; 40 + initialUrl?: string; 41 + } 42 + 43 + export default function AddCardForm(props: Props) { 44 + const pathname = usePathname(); 45 + const [collectionSelectorOpened, { toggle: toggleCollectionSelector }] = 46 + useDisclosure(false); 47 + const initialCollections = props.selectedCollection 48 + ? [props.selectedCollection] 49 + : []; 50 + const [selectedCollections, setSelectedCollections] = 51 + useState(initialCollections); 52 + 53 + const { data: collections } = useMyCollections({ limit: 30 }); 54 + const allCollections = 55 + collections?.pages.flatMap((page) => page.collections ?? []) ?? []; 56 + 57 + // Put selectedCollection first, then others 58 + const myCollections = props.selectedCollection 59 + ? [ 60 + props.selectedCollection, 61 + ...allCollections.filter((c) => c.id !== props.selectedCollection?.id), 62 + ] 63 + : allCollections; 64 + 65 + const addCard = useAddCard({ 66 + saveSource: CardSaveSource.ADD_CARD_DRAWER, 67 + pagePath: pathname, 68 + }); 69 + 70 + const form = useForm({ 71 + initialValues: { 72 + url: props.initialUrl || '', 73 + note: '', 74 + collections: selectedCollections, 75 + }, 76 + }); 77 + 78 + const MAX_NOTE_LENGTH = 500; 79 + 80 + useEffect(() => { 81 + if (props.initialUrl) { 82 + form.setValues({ url: props.initialUrl }); 83 + } 84 + }, [props.initialUrl]); 85 + 86 + const handleAddCard = (e: React.FormEvent) => { 87 + e.preventDefault(); 88 + track('add new card'); 89 + 90 + addCard.mutate( 91 + { 92 + url: form.getValues().url, 93 + note: form.getValues().note, 94 + collectionIds: selectedCollections.map((c) => c.id), 95 + }, 96 + { 97 + onSuccess: () => { 98 + setSelectedCollections(initialCollections); 99 + props.onClose(); 100 + window.history.replaceState({}, '', window.location.pathname); 101 + }, 102 + onError: () => { 103 + notifications.show({ 104 + message: 'Could not add card.', 105 + }); 106 + }, 107 + onSettled: () => { 108 + form.reset(); 109 + }, 110 + }, 111 + ); 112 + }; 113 + 114 + return ( 115 + <> 116 + <form onSubmit={handleAddCard}> 117 + <Stack gap={'xl'}> 118 + <TextInput 119 + id="url" 120 + label="URL" 121 + type="url" 122 + placeholder="https://www.example.com" 123 + variant="filled" 124 + required 125 + size="md" 126 + leftSection={<IoMdLink size={22} />} 127 + key={form.key('url')} 128 + {...form.getInputProps('url')} 129 + /> 130 + 131 + <Stack gap={0}> 132 + <Flex justify="space-between"> 133 + <Input.Label size="md" htmlFor="note"> 134 + Note 135 + </Input.Label> 136 + <Text c={'gray'} aria-hidden> 137 + {form.getValues().note.length} / {MAX_NOTE_LENGTH} 138 + </Text> 139 + </Flex> 140 + 141 + <Textarea 142 + id="note" 143 + placeholder="Add a note about this card" 144 + variant="filled" 145 + size="md" 146 + rows={3} 147 + maxLength={MAX_NOTE_LENGTH} 148 + aria-describedby="note-char-remaining" 149 + key={form.key('note')} 150 + {...form.getInputProps('note')} 151 + /> 152 + <VisuallyHidden id="note-char-remaining" aria-live="polite"> 153 + {`${MAX_NOTE_LENGTH - form.getValues().note.length} characters remaining`} 154 + </VisuallyHidden> 155 + </Stack> 156 + 157 + <Stack gap={5}> 158 + <Text fw={500}> 159 + Add to collections{' '} 160 + {selectedCollections.length > 0 && 161 + `(${selectedCollections.length})`} 162 + </Text> 163 + <ScrollArea.Autosize 164 + type="hover" 165 + scrollbars="x" 166 + offsetScrollbars={true} 167 + > 168 + <Group gap={'xs'} wrap="nowrap"> 169 + <Button 170 + disabled={addCard.isPending} 171 + onClick={toggleCollectionSelector} 172 + variant="light" 173 + color={'blue'} 174 + leftSection={<BiCollection size={22} />} 175 + > 176 + {myCollections.length === 0 177 + ? 'Create a collection' 178 + : 'Manage & Create'} 179 + </Button> 180 + 181 + {myCollections.map((col) => { 182 + const marginUrl = getMarginUrl(col.uri, col.author?.handle); 183 + return ( 184 + <Button 185 + key={col.id} 186 + disabled={addCard.isPending} 187 + variant="light" 188 + color={ 189 + selectedCollections.some((c) => c.id === col.id) 190 + ? 'grape' 191 + : 'gray' 192 + } 193 + rightSection={ 194 + selectedCollections.some((c) => c.id === col.id) ? ( 195 + <IoMdCheckmark /> 196 + ) : null 197 + } 198 + leftSection={ 199 + isMarginUri(col.uri) ? ( 200 + <MarginLogo size={12} marginUrl={marginUrl} /> 201 + ) : col.accessType === CollectionAccessType.OPEN ? ( 202 + <ThemeIcon 203 + variant="light" 204 + radius={'xl'} 205 + size={'xs'} 206 + color="green" 207 + > 208 + <FaSeedling size={8} /> 209 + </ThemeIcon> 210 + ) : undefined 211 + } 212 + onClick={() => { 213 + setSelectedCollections((prev) => { 214 + // already selected, remove 215 + if (prev.some((c) => c.id === col.id)) { 216 + return prev.filter((c) => c.id !== col.id); 217 + } 218 + // not selected, add it 219 + return [...prev, col]; 220 + }); 221 + }} 222 + > 223 + {col.name} 224 + </Button> 225 + ); 226 + })} 227 + </Group> 228 + </ScrollArea.Autosize> 229 + </Stack> 230 + 231 + <Group justify="space-between" gap={'xs'} grow> 232 + <Button 233 + variant="light" 234 + size="md" 235 + color={'gray'} 236 + onClick={() => { 237 + props.onClose(); 238 + setSelectedCollections(initialCollections); 239 + }} 240 + > 241 + Cancel 242 + </Button> 243 + <Button type="submit" size="md" loading={addCard.isPending}> 244 + Add card 245 + </Button> 246 + </Group> 247 + </Stack> 248 + </form> 249 + 250 + <Drawer 251 + opened={collectionSelectorOpened} 252 + onClose={toggleCollectionSelector} 253 + withCloseButton={false} 254 + position="bottom" 255 + padding={'sm'} 256 + size={'30rem'} 257 + overlayProps={DEFAULT_OVERLAY_PROPS} 258 + > 259 + <Drawer.Header> 260 + <Drawer.Title fz={'xl'} fw={600} mx={'auto'}> 261 + Add to collections 262 + </Drawer.Title> 263 + </Drawer.Header> 264 + <Container size={'xs'} p={0}> 265 + <Suspense fallback={<CollectionSelectorSkeleton />}> 266 + <CollectionSelector 267 + isOpen={collectionSelectorOpened} 268 + onCancel={() => { 269 + setSelectedCollections(initialCollections); 270 + toggleCollectionSelector(); 271 + }} 272 + onClose={toggleCollectionSelector} 273 + onSave={toggleCollectionSelector} 274 + selectedCollections={selectedCollections} 275 + onSelectedCollectionsChange={setSelectedCollections} 276 + /> 277 + </Suspense> 278 + </Container> 279 + </Drawer> 280 + </> 281 + ); 282 + }
+13 -137
src/webapp/features/collections/components/createCollectionDrawer/CreateCollectionDrawer.tsx
··· 1 + 'use client'; 2 + 1 3 import { CollectionAccessType } from '@semble/types'; 2 - import { 3 - Button, 4 - Container, 5 - Drawer, 6 - Group, 7 - Select, 8 - Stack, 9 - Textarea, 10 - TextInput, 11 - ThemeIcon, 12 - } from '@mantine/core'; 13 - import { useForm } from '@mantine/form'; 14 - import useCreateCollection from '../../lib/mutations/useCreateCollection'; 15 - import { notifications } from '@mantine/notifications'; 4 + import { Container, Drawer } from '@mantine/core'; 16 5 import { DEFAULT_OVERLAY_PROPS } from '@/styles/overlays'; 17 - import { FaSeedling } from 'react-icons/fa6'; 6 + import CreateCollectionForm from './CreateCollectionForm'; 18 7 19 8 interface Props { 20 9 isOpen: boolean; ··· 24 13 onCreate?: () => void; 25 14 } 26 15 27 - export default function createCollectionDrawer(props: Props) { 28 - const createCollection = useCreateCollection(); 29 - const form = useForm({ 30 - initialValues: { 31 - name: props.initialName ?? '', 32 - description: '', 33 - accessType: props.initialAccessType ?? CollectionAccessType.CLOSED, 34 - }, 35 - }); 36 - 37 - const handleCreateCollection = (e: React.FormEvent) => { 38 - e.preventDefault(); 39 - e.stopPropagation(); 40 - 41 - createCollection.mutate( 42 - { 43 - name: form.getValues().name, 44 - description: form.getValues().description, 45 - accessType: form.getValues().accessType, 46 - }, 47 - { 48 - onSuccess: () => { 49 - props.onClose(); 50 - if (props.onCreate) { 51 - props.onCreate(); 52 - } 53 - }, 54 - onError: () => { 55 - notifications.show({ 56 - message: 'Could not create collection.', 57 - }); 58 - }, 59 - onSettled: () => { 60 - form.reset(); 61 - }, 62 - }, 63 - ); 64 - }; 65 - 16 + export default function CreateCollectionDrawer(props: Props) { 66 17 return ( 67 18 <Drawer 68 19 opened={props.isOpen} 69 - onClose={() => { 70 - props.onClose(); 71 - form.reset(); 72 - }} 20 + onClose={props.onClose} 73 21 withCloseButton={false} 74 22 size={'31rem'} 23 + padding={'sm'} 75 24 position="bottom" 76 25 overlayProps={DEFAULT_OVERLAY_PROPS} 77 26 > ··· 82 31 </Drawer.Header> 83 32 84 33 <Container size={'sm'} p={0}> 85 - <form onSubmit={handleCreateCollection}> 86 - <Stack> 87 - <Stack gap={'xl'}> 88 - <TextInput 89 - id="name" 90 - label="Name" 91 - type="text" 92 - placeholder="Collection name" 93 - variant="filled" 94 - size="md" 95 - required 96 - maxLength={100} 97 - key={form.key('name')} 98 - {...form.getInputProps('name')} 99 - /> 100 - 101 - <Textarea 102 - id="description" 103 - label="Description" 104 - placeholder="Describe what this collection is about" 105 - variant="filled" 106 - size="md" 107 - rows={3} 108 - maxLength={500} 109 - key={form.key('description')} 110 - {...form.getInputProps('description')} 111 - /> 112 - 113 - <Select 114 - variant="filled" 115 - size="md" 116 - label="Collaboration" 117 - leftSection={ 118 - form.getValues().accessType === CollectionAccessType.OPEN ? ( 119 - <ThemeIcon 120 - size={'md'} 121 - variant="light" 122 - color={'green'} 123 - radius={'xl'} 124 - > 125 - <FaSeedling size={14} /> 126 - </ThemeIcon> 127 - ) : null 128 - } 129 - allowDeselect={false} 130 - defaultValue={CollectionAccessType.CLOSED} 131 - data={[ 132 - { 133 - value: CollectionAccessType.CLOSED, 134 - label: 'Personal — Only you can add', 135 - }, 136 - { 137 - value: CollectionAccessType.OPEN, 138 - label: 'Open — Anyone can add', 139 - }, 140 - ]} 141 - {...form.getInputProps('accessType')} 142 - /> 143 - 144 - <Group justify="space-between" gap={'xs'} grow> 145 - <Button 146 - variant="light" 147 - size="md" 148 - color={'gray'} 149 - onClick={props.onClose} 150 - > 151 - Cancel 152 - </Button> 153 - <Button 154 - type="submit" 155 - size="md" 156 - loading={createCollection.isPending} 157 - > 158 - Create 159 - </Button> 160 - </Group> 161 - </Stack> 162 - </Stack> 163 - </form> 34 + <CreateCollectionForm 35 + onClose={props.onClose} 36 + initialName={props.initialName} 37 + initialAccessType={props.initialAccessType} 38 + onCreate={props.onCreate} 39 + /> 164 40 </Container> 165 41 </Drawer> 166 42 );