This repository has no description
0

Configure Feed

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

feat: create collection drawer

+270 -150
-133
src/webapp/app/(authenticated)/collections/create/page.tsx
··· 1 - 'use client'; 2 - 3 - import { useState } from 'react'; 4 - import { useRouter } from 'next/navigation'; 5 - import { getAccessToken } from '@/services/auth'; 6 - import { ApiClient } from '@/api-client/ApiClient'; 7 - import { 8 - Stack, 9 - Title, 10 - Text, 11 - Container, 12 - Card, 13 - Button, 14 - TextInput, 15 - Textarea, 16 - Group, 17 - Alert, 18 - } from '@mantine/core'; 19 - import { useForm } from '@mantine/form'; 20 - 21 - export default function CreateCollectionPage() { 22 - const form = useForm({ 23 - initialValues: { 24 - name: '', 25 - description: '', 26 - }, 27 - }); 28 - 29 - const [loading, setLoading] = useState(false); 30 - const [error, setError] = useState(''); 31 - const router = useRouter(); 32 - 33 - // Create API client instance 34 - const apiClient = new ApiClient( 35 - process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 36 - () => getAccessToken(), 37 - ); 38 - 39 - const handleSubmit = async (e: React.FormEvent) => { 40 - e.preventDefault(); 41 - 42 - if (!form.getValues().name.trim()) { 43 - setError('Collection name is required'); 44 - return; 45 - } 46 - 47 - setLoading(true); 48 - setError(''); 49 - 50 - try { 51 - await apiClient.createCollection({ 52 - name: form.getValues().name.trim(), 53 - description: form.getValues().description.trim() || undefined, 54 - }); 55 - 56 - // Redirect to collections page on success 57 - router.push('/collections'); 58 - } catch (error: any) { 59 - console.error('Error creating collection:', error); 60 - setError( 61 - error.message || 'Failed to create collection. Please try again.', 62 - ); 63 - } finally { 64 - setLoading(false); 65 - } 66 - }; 67 - 68 - return ( 69 - <Container> 70 - <Stack> 71 - <Stack gap={0}> 72 - <Title order={1}>Create Collection</Title> 73 - <Text c={'gray'}> 74 - Create a new collection to organize your cards. 75 - </Text> 76 - </Stack> 77 - 78 - <Card withBorder> 79 - <Stack> 80 - <Title order={3}>Collection Details</Title> 81 - 82 - <Stack> 83 - <form onSubmit={handleSubmit}> 84 - <Stack> 85 - <Stack> 86 - <TextInput 87 - id="name" 88 - label="Name" 89 - type="text" 90 - placeholder="Enter collection name" 91 - disabled={loading} 92 - required 93 - maxLength={100} 94 - key={form.key('name')} 95 - {...form.getInputProps('name')} 96 - /> 97 - 98 - <Textarea 99 - id="description" 100 - label="Description" 101 - placeholder="Describe what this collection is about..." 102 - disabled={loading} 103 - rows={3} 104 - maxLength={500} 105 - key={form.key('description')} 106 - {...form.getInputProps('description')} 107 - /> 108 - </Stack> 109 - 110 - {error && <Alert color="red" title={error} />} 111 - 112 - <Group> 113 - <Button type="submit" loading={loading}> 114 - {loading ? 'Creating...' : 'Create Collection'} 115 - </Button> 116 - <Button 117 - type="button" 118 - variant="outline" 119 - onClick={() => router.back()} 120 - disabled={loading} 121 - > 122 - Cancel 123 - </Button> 124 - </Group> 125 - </Stack> 126 - </form> 127 - </Stack> 128 - </Stack> 129 - </Card> 130 - </Stack> 131 - </Container> 132 - ); 133 - }
+1 -1
src/webapp/components/navigation/appLayout/AppLayout.tsx
··· 23 23 <Navbar /> 24 24 25 25 <AppShell.Main> 26 - {props.children} 26 + {props.children} 27 27 <ComposerDrawer /> 28 28 </AppShell.Main> 29 29 </AppShell>
+5 -10
src/webapp/features/collections/components/collectionsNavList/CollectionsNavList.tsx
··· 1 1 import Link from 'next/link'; 2 - import { Alert, NavLink, Stack } from '@mantine/core'; 3 - import { BiCollection, BiPlus, BiRightArrowAlt } from 'react-icons/bi'; 2 + import { NavLink, Stack } from '@mantine/core'; 3 + import { BiCollection, BiRightArrowAlt } from 'react-icons/bi'; 4 4 import CollectionNavItem from '../collectionNavItem/CollectionNavItem'; 5 5 import useCollections from '../../lib/queries/useCollections'; 6 6 import { useToggle } from '@mantine/hooks'; 7 7 import CollectionsNavListError from './Error.CollectionsNavList'; 8 + import CreateCollectionShortcut from '../createCollectionShortcut/CreateCollectionShortcut'; 8 9 9 10 export default function CollectionsNavList() { 10 11 const { data, error } = useCollections(); ··· 32 33 leftSection={<BiRightArrowAlt size={25} />} 33 34 /> 34 35 35 - <NavLink 36 - component={Link} 37 - href="/collections/create" 38 - label={'Add'} 39 - variant="subtle" 40 - c="blue" 41 - leftSection={<BiPlus size={25} />} 42 - /> 36 + {/* Self-contained shortcut to prevent won't re-render this whole list when interacted with */} 37 + <CreateCollectionShortcut /> 43 38 44 39 <Stack gap={0}> 45 40 {data.collections.map((c) => (
+106
src/webapp/features/collections/components/createCollectionDrawer/CreateCollectionDrawer.tsx
··· 1 + import { 2 + Button, 3 + Container, 4 + Drawer, 5 + Group, 6 + Stack, 7 + Textarea, 8 + TextInput, 9 + } from '@mantine/core'; 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 + } 18 + 19 + export default function createCollectionDrawer(props: Props) { 20 + const createCollection = useCreateCollection(); 21 + const form = useForm({ 22 + initialValues: { 23 + name: '', 24 + description: '', 25 + }, 26 + }); 27 + 28 + const handleCreateCollection = (e: React.FormEvent) => { 29 + e.preventDefault(); 30 + 31 + createCollection.mutate( 32 + { 33 + name: form.getValues().name, 34 + description: form.getValues().description, 35 + }, 36 + { 37 + onSuccess: () => { 38 + notifications.show({ 39 + message: `Created collection "${form.getValues().name}"`, 40 + }); 41 + props.onClose(); 42 + }, 43 + onError: () => { 44 + notifications.show({ 45 + message: 'Could not create collection.', 46 + }); 47 + }, 48 + onSettled: () => { 49 + form.reset(); 50 + }, 51 + }, 52 + ); 53 + }; 54 + 55 + return ( 56 + <Drawer 57 + opened={props.isOpen} 58 + onClose={props.onClose} 59 + withCloseButton={false} 60 + position="bottom" 61 + overlayProps={{ 62 + blur: 3, 63 + gradient: 64 + 'linear-gradient(0deg, rgba(204, 255, 0, 0.5), rgba(255, 255, 255, 0.5))', 65 + }} 66 + size={'sm'} 67 + > 68 + <Container size={'sm'}> 69 + <form onSubmit={handleCreateCollection}> 70 + <Stack> 71 + <TextInput 72 + id="name" 73 + label="Name" 74 + type="text" 75 + placeholder="Collection name" 76 + variant="filled" 77 + required 78 + maxLength={100} 79 + key={form.key('name')} 80 + {...form.getInputProps('name')} 81 + /> 82 + 83 + <Textarea 84 + id="description" 85 + label="Description" 86 + placeholder="Describe what this collection is about" 87 + variant="filled" 88 + rows={8} 89 + maxLength={500} 90 + key={form.key('description')} 91 + {...form.getInputProps('description')} 92 + /> 93 + <Group justify="space-between"> 94 + <Button variant="outline" color={'gray'} onClick={props.onClose}> 95 + Cancel 96 + </Button> 97 + <Button type="submit" loading={createCollection.isPending}> 98 + Create 99 + </Button> 100 + </Group> 101 + </Stack> 102 + </form> 103 + </Container> 104 + </Drawer> 105 + ); 106 + }
+18
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({ drawer: 'createCollection' })} 16 + /> 17 + ); 18 + }
+27
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
··· 1 + import { ApiClient } from '@/api-client/ApiClient'; 2 + import { getAccessToken } from '@/services/auth'; 3 + import { useMutation, useQueryClient } from '@tanstack/react-query'; 4 + 5 + export default function useCreateCollection() { 6 + const apiClient = new ApiClient( 7 + process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:3000', 8 + () => getAccessToken(), 9 + ); 10 + 11 + const queryClient = useQueryClient(); 12 + 13 + const mutation = useMutation({ 14 + mutationFn: (newCollection: { name: string; description: string }) => { 15 + return apiClient.createCollection(newCollection); 16 + }, 17 + 18 + // Do things that are absolutely necessary and logic related (like query invalidation) in the useMutation callbacks 19 + // 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 20 + // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 21 + onSuccess: () => { 22 + queryClient.invalidateQueries({ queryKey: ['collections'] }); 23 + }, 24 + }); 25 + 26 + return mutation; 27 + }
+4 -4
src/webapp/features/composer/components/composerDrawer/ComposerDrawer.tsx
··· 7 7 Stack, 8 8 Transition, 9 9 Card, 10 - AspectRatio, 11 10 } from '@mantine/core'; 11 + import { useContextDrawers } from '@/providers/drawers'; 12 12 import { Fragment, useState } from 'react'; 13 13 import { FiPlus, FiX } from 'react-icons/fi'; 14 14 import { FaRegNoteSticky } from 'react-icons/fa6'; ··· 17 17 18 18 export default function ComposerDrawer() { 19 19 const [opened, setOpened] = useState(false); 20 + const drawers = useContextDrawers(); 20 21 21 22 return ( 22 23 <Fragment> ··· 28 29 radius={'lg'} 29 30 > 30 31 <Menu.Target> 31 - <Affix m={'md'}> 32 + <Affix m={'md'} style={{ zIndex: 10 }}> 32 33 <ActionIcon 33 34 size={'input-lg'} 34 35 radius="xl" ··· 88 89 </Card> 89 90 </Menu.Item> 90 91 <Menu.Item 91 - component={Link} 92 - href="/collections/create" 92 + onClick={() => drawers.open({ drawer: 'createCollection' })} 93 93 p={0} 94 94 style={{ cursor: 'pointer' }} 95 95 >
+12
src/webapp/package-lock.json
··· 13 13 "@mantine/dropzone": "^8.1.3", 14 14 "@mantine/form": "^8.1.3", 15 15 "@mantine/hooks": "^8.1.3", 16 + "@mantine/modals": "^8.1.3", 16 17 "@mantine/notifications": "^8.1.3", 17 18 "@tanstack/react-query": "^5.85.5", 18 19 "@vercel/analytics": "^1.5.0", ··· 2037 2038 "integrity": "sha512-yL4SbyYjrkmtIhscswajNz9RL0iO2+V8CMtOi0KISch2rPNvTAJNumFuZaXgj4UHeDc0JQYSmcZ+EW8NGm7xcQ==", 2038 2039 "peerDependencies": { 2039 2040 "react": "^18.x || ^19.x" 2041 + } 2042 + }, 2043 + "node_modules/@mantine/modals": { 2044 + "version": "8.1.3", 2045 + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-8.1.3.tgz", 2046 + "integrity": "sha512-PTLquO7OuYHrbezhjqf1fNwxU1NKZJmNYDOll6RHp6FPQ80xCVWQqVFsj3R8XsLluu2b5ygTYi+avWrUr1GvGg==", 2047 + "peerDependencies": { 2048 + "@mantine/core": "8.1.3", 2049 + "@mantine/hooks": "8.1.3", 2050 + "react": "^18.x || ^19.x", 2051 + "react-dom": "^18.x || ^19.x" 2040 2052 } 2041 2053 }, 2042 2054 "node_modules/@mantine/notifications": {
+1
src/webapp/package.json
··· 29 29 "@mantine/dropzone": "^8.1.3", 30 30 "@mantine/form": "^8.1.3", 31 31 "@mantine/hooks": "^8.1.3", 32 + "@mantine/modals": "^8.1.3", 32 33 "@mantine/notifications": "^8.1.3", 33 34 "@tanstack/react-query": "^5.85.5", 34 35 "@vercel/analytics": "^1.5.0",
+83
src/webapp/providers/drawers.tsx
··· 1 + import CreateCollectionDrawer from '@/features/collections/components/createCollectionDrawer/CreateCollectionDrawer'; 2 + import React, { 3 + createContext, 4 + useCallback, 5 + useContext, 6 + useState, 7 + ReactNode, 8 + } from 'react'; 9 + 10 + type DrawerName = 'createCollection'; 11 + 12 + type DrawerPropsMap = { 13 + createCollection: {}; 14 + }; 15 + 16 + interface DrawerState<T = any> { 17 + name: DrawerName | null; 18 + innerProps: T; 19 + } 20 + 21 + interface DrawersContextType { 22 + open: <T extends DrawerName>(args: { 23 + drawer: T; 24 + innerProps?: DrawerPropsMap[T]; 25 + }) => void; 26 + close: () => void; 27 + isOpen: (name: DrawerName) => boolean; 28 + innerProps: any; 29 + } 30 + 31 + const DrawersContext = createContext<DrawersContextType | undefined>(undefined); 32 + 33 + export const useContextDrawers = () => { 34 + const ctx = useContext(DrawersContext); 35 + if (!ctx) { 36 + throw new Error('useContextDrawers must be used within a DrawersProvider'); 37 + } 38 + return ctx; 39 + }; 40 + 41 + export const DrawersProvider = ({ children }: { children: ReactNode }) => { 42 + const [state, setState] = useState<DrawerState>({ 43 + name: null, 44 + innerProps: {}, 45 + }); 46 + 47 + const open = useCallback( 48 + <T extends DrawerName>({ 49 + drawer, 50 + innerProps = {}, 51 + }: { 52 + drawer: T; 53 + innerProps?: DrawerPropsMap[T]; 54 + }) => { 55 + setState({ name: drawer, innerProps }); 56 + }, 57 + [], 58 + ); 59 + 60 + const close = useCallback(() => { 61 + setState({ name: null, innerProps: {} }); 62 + }, []); 63 + 64 + const isOpen = useCallback( 65 + (name: DrawerName) => state.name === name, 66 + [state.name], 67 + ); 68 + 69 + return ( 70 + <DrawersContext.Provider 71 + value={{ open, close, isOpen, innerProps: state.innerProps }} 72 + > 73 + {children} 74 + 75 + {/* Render */} 76 + <CreateCollectionDrawer 77 + isOpen={isOpen('createCollection')} 78 + onClose={close} 79 + {...state.innerProps} 80 + /> 81 + </DrawersContext.Provider> 82 + ); 83 + };
+5 -1
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'; 7 9 8 10 interface Props { 9 11 children: React.ReactNode; ··· 14 16 <TanStackQueryProvider> 15 17 <AuthProvider> 16 18 <MantineProvider> 17 - <NavbarProvider>{props.children}</NavbarProvider> 19 + <DrawersProvider> 20 + <NavbarProvider>{props.children}</NavbarProvider> 21 + </DrawersProvider> 18 22 </MantineProvider> 19 23 </AuthProvider> 20 24 </TanStackQueryProvider>
+8 -1
src/webapp/providers/mantine.tsx
··· 3 3 import { theme } from '@/styles/theme'; 4 4 import { MantineProvider as BaseProvider } from '@mantine/core'; 5 5 import '@mantine/core/styles.css'; 6 + import { Notifications } from '@mantine/notifications'; 7 + import '@mantine/notifications/styles.css'; 6 8 7 9 interface Props { 8 10 children: React.ReactNode; 9 11 } 10 12 11 13 export default function MantineProvider(props: Props) { 12 - return <BaseProvider theme={theme}>{props.children}</BaseProvider>; 14 + return ( 15 + <BaseProvider theme={theme}> 16 + <Notifications position="bottom-left" /> 17 + {props.children} 18 + </BaseProvider> 19 + ); 13 20 }