A social knowledge tool for researchers built on ATProto

feat: edit note from note card modal

+197 -76
+1 -1
src/webapp/features/cards/components/urlCardActions/UrlCardActions.tsx
··· 130 130 isOpen={showNoteModal} 131 131 onClose={() => setShowNoteModal(false)} 132 132 note={props.note} 133 - urlCardContent={props.cardContent} 133 + cardContent={props.cardContent} 134 134 cardAuthor={props.cardAuthor} 135 135 /> 136 136
+9 -75
src/webapp/features/notes/components/noteCardModal/NoteCardModal.tsx
··· 1 1 import type { UrlCard, User } from '@/api-client'; 2 2 import { getDomain } from '@/lib/utils/link'; 3 3 import { UPDATE_OVERLAY_PROPS } from '@/styles/overlays'; 4 - import { 5 - Anchor, 6 - AspectRatio, 7 - Card, 8 - Group, 9 - Modal, 10 - Stack, 11 - Text, 12 - Image, 13 - Tooltip, 14 - Avatar, 15 - } from '@mantine/core'; 16 - import Link from 'next/link'; 4 + import { Modal, Text } from '@mantine/core'; 5 + import NoteCardModalContent from './NoteCardModalContent'; 6 + import { Suspense } from 'react'; 7 + import NoteCardModalContentSkeleton from './Skeleton.NoteCardModalContent'; 17 8 18 9 interface Props { 19 10 isOpen: boolean; 20 11 onClose: () => void; 21 12 note: UrlCard['note']; 22 - urlCardContent: UrlCard['cardContent']; 13 + cardContent: UrlCard['cardContent']; 23 14 cardAuthor?: User; 24 15 } 25 16 26 17 export default function NoteCardModal(props: Props) { 27 - const domain = getDomain(props.urlCardContent.url); 18 + const domain = getDomain(props.cardContent.url); 28 19 29 20 return ( 30 21 <Modal ··· 35 26 centered 36 27 onClick={(e) => e.stopPropagation()} 37 28 > 38 - <Stack gap={'xs'}> 39 - {props.cardAuthor && ( 40 - <Group gap={5}> 41 - <Avatar 42 - size={'sm'} 43 - component={Link} 44 - href={`/profile/${props.cardAuthor.handle}`} 45 - target="_blank" 46 - src={props.cardAuthor.avatarUrl} 47 - alt={`${props.cardAuthor.name}'s' avatar`} 48 - /> 49 - <Anchor 50 - component={Link} 51 - href={`/profile/${props.cardAuthor.handle}`} 52 - target="_blank" 53 - fw={700} 54 - c="blue" 55 - lineClamp={1} 56 - > 57 - {props.cardAuthor.name} 58 - </Anchor> 59 - </Group> 60 - )} 61 - {props.note && <Text fs={'italic'}>{props.note.text}</Text>} 62 - <Card withBorder p={'xs'} radius={'lg'}> 63 - <Stack> 64 - <Group gap={'sm'}> 65 - {props.urlCardContent.thumbnailUrl && ( 66 - <AspectRatio ratio={1 / 1} flex={0.1}> 67 - <Image 68 - src={props.urlCardContent.thumbnailUrl} 69 - alt={`${props.urlCardContent.url} social preview image`} 70 - radius={'md'} 71 - w={50} 72 - h={50} 73 - /> 74 - </AspectRatio> 75 - )} 76 - <Stack gap={0} flex={0.9}> 77 - <Tooltip label={props.urlCardContent.url}> 78 - <Anchor 79 - component={Link} 80 - href={props.urlCardContent.url} 81 - target="_blank" 82 - c={'gray'} 83 - lineClamp={1} 84 - > 85 - {domain} 86 - </Anchor> 87 - </Tooltip> 88 - {props.urlCardContent.title && ( 89 - <Text fw={500} lineClamp={1}> 90 - {props.urlCardContent.title} 91 - </Text> 92 - )} 93 - </Stack> 94 - </Group> 95 - </Stack> 96 - </Card> 97 - </Stack> 29 + <Suspense fallback={<NoteCardModalContentSkeleton />}> 30 + <NoteCardModalContent {...props} domain={domain} /> 31 + </Suspense> 98 32 </Modal> 99 33 ); 100 34 }
+168
src/webapp/features/notes/components/noteCardModal/NoteCardModalContent.tsx
··· 1 + import useGetCardFromMyLibrary from '@/features/cards/lib/queries/useGetCardFromMyLibrary'; 2 + import { 3 + Anchor, 4 + AspectRatio, 5 + Avatar, 6 + Card, 7 + Group, 8 + Stack, 9 + Tooltip, 10 + Text, 11 + Image, 12 + Textarea, 13 + Button, 14 + } from '@mantine/core'; 15 + import { UrlCard, User } from '@semble/types'; 16 + import Link from 'next/link'; 17 + import { useState } from 'react'; 18 + import useUpdateNote from '../../lib/mutations/useUpdateNote'; 19 + import { notifications } from '@mantine/notifications'; 20 + 21 + interface Props { 22 + note: UrlCard['note']; 23 + cardContent: UrlCard['cardContent']; 24 + cardAuthor?: User; 25 + domain: string; 26 + } 27 + 28 + export default function NoteCardModalContent(props: Props) { 29 + const cardStatus = useGetCardFromMyLibrary({ url: props.cardContent.url }); 30 + const isMyCard = props.cardAuthor?.id === cardStatus.data.card?.author.id; 31 + const [note, setNote] = useState(isMyCard ? props.note?.text : ''); 32 + const [editMode, setEditMode] = useState(false); 33 + 34 + const updateNote = useUpdateNote(); 35 + 36 + const handleUpdateNote = () => { 37 + if (!props.note || !note) return; 38 + 39 + updateNote.mutate( 40 + { 41 + cardId: props.note?.id, 42 + note: note, 43 + }, 44 + { 45 + onError: () => { 46 + notifications.show({ 47 + message: 'Could not update note.', 48 + position: 'top-center', 49 + }); 50 + }, 51 + onSettled: () => { 52 + setEditMode(false); 53 + }, 54 + }, 55 + ); 56 + }; 57 + 58 + if (editMode) { 59 + return ( 60 + <Stack gap={'xs'}> 61 + <Textarea 62 + id="note" 63 + label="Your note" 64 + placeholder="Add a note about this card" 65 + variant="filled" 66 + size="md" 67 + autosize 68 + maxRows={8} 69 + maxLength={500} 70 + value={note} 71 + onChange={(e) => setNote(e.currentTarget.value)} 72 + /> 73 + <Group gap={'xs'} grow> 74 + <Button 75 + variant="light" 76 + color="gray" 77 + onClick={() => { 78 + setEditMode(false); 79 + setNote(props.note?.text); 80 + }} 81 + > 82 + Cancel 83 + </Button> 84 + <Button 85 + onClick={handleUpdateNote} 86 + loading={updateNote.isPending} 87 + disabled={note?.trimEnd() === ''} 88 + > 89 + Save 90 + </Button> 91 + </Group> 92 + </Stack> 93 + ); 94 + } 95 + return ( 96 + <Stack gap={'xs'}> 97 + {props.cardAuthor && ( 98 + <Group gap={5}> 99 + <Avatar 100 + size={'sm'} 101 + component={Link} 102 + href={`/profile/${props.cardAuthor.handle}`} 103 + target="_blank" 104 + src={props.cardAuthor.avatarUrl} 105 + alt={`${props.cardAuthor.name}'s' avatar`} 106 + /> 107 + <Anchor 108 + component={Link} 109 + href={`/profile/${props.cardAuthor.handle}`} 110 + target="_blank" 111 + fw={700} 112 + c="blue" 113 + lineClamp={1} 114 + > 115 + {props.cardAuthor.name} 116 + </Anchor> 117 + </Group> 118 + )} 119 + {props.note && <Text fs={'italic'}>{props.note.text}</Text>} 120 + <Card withBorder component="article" p={'xs'} radius={'lg'}> 121 + <Stack> 122 + <Group gap={'sm'} justify="space-between"> 123 + {props.cardContent.thumbnailUrl && ( 124 + <AspectRatio ratio={1 / 1} flex={0.1}> 125 + <Image 126 + src={props.cardContent.thumbnailUrl} 127 + alt={`${props.cardContent.url} social preview image`} 128 + radius={'md'} 129 + w={50} 130 + h={50} 131 + /> 132 + </AspectRatio> 133 + )} 134 + <Stack gap={0} flex={0.9}> 135 + <Tooltip label={props.cardContent.url}> 136 + <Anchor 137 + component={Link} 138 + href={props.cardContent.url} 139 + target="_blank" 140 + c={'gray'} 141 + lineClamp={1} 142 + onClick={(e) => e.stopPropagation()} 143 + > 144 + {props.domain} 145 + </Anchor> 146 + </Tooltip> 147 + {props.cardContent.title && ( 148 + <Text fw={500} lineClamp={1}> 149 + {props.cardContent.title} 150 + </Text> 151 + )} 152 + </Stack> 153 + <Button 154 + variant="light" 155 + color="gray" 156 + onClick={(e) => { 157 + e.stopPropagation(); 158 + setEditMode(true); 159 + }} 160 + > 161 + {note ? 'Edit note' : 'Add note'} 162 + </Button> 163 + </Group> 164 + </Stack> 165 + </Card> 166 + </Stack> 167 + ); 168 + }
+16
src/webapp/features/notes/components/noteCardModal/Skeleton.NoteCardModalContent.tsx
··· 1 + import { Avatar, Group, Skeleton, Stack } from '@mantine/core'; 2 + 3 + export default function NoteCardModalContentSkeleton() { 4 + return ( 5 + <Stack gap={5}> 6 + <Group gap={5}> 7 + <Avatar size={'md'} /> 8 + <Skeleton w={100} h={14} /> 9 + </Group> 10 + {/* Note */} 11 + <Skeleton w={'100%'} h={14} /> 12 + <Skeleton w={'100%'} h={14} /> 13 + <Skeleton w={'100%'} h={14} /> 14 + </Stack> 15 + ); 16 + }
+3
src/webapp/features/notes/lib/mutations/useUpdateNote.tsx
··· 2 2 import { updateNoteCard } from '../dal'; 3 3 import { cardKeys } from '@/features/cards/lib/cardKeys'; 4 4 import { collectionKeys } from '@/features/collections/lib/collectionKeys'; 5 + import { feedKeys } from '@/features/feeds/lib/feedKeys'; 5 6 6 7 export default function useUpdateNote() { 7 8 const queryClient = useQueryClient(); ··· 14 15 onSuccess: (data) => { 15 16 queryClient.invalidateQueries({ queryKey: cardKeys.card(data.cardId) }); 16 17 queryClient.invalidateQueries({ queryKey: cardKeys.infinite() }); 18 + queryClient.invalidateQueries({ queryKey: cardKeys.infinite() }); 19 + queryClient.invalidateQueries({ queryKey: feedKeys.all() }); 17 20 queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 18 21 }, 19 22 });