This repository has no description
0

Configure Feed

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

semble / src / webapp / features / notes / components / noteCardInline / NoteCardInline.tsx
8.2 kB 288 lines
1'use client'; 2 3import { 4 ActionIcon, 5 Anchor, 6 Button, 7 Card, 8 Flex, 9 Group, 10 Input, 11 Menu, 12 Stack, 13 Text, 14 Textarea, 15 VisuallyHidden, 16} from '@mantine/core'; 17import { IoEyeOffOutline } from 'react-icons/io5'; 18import { BsThreeDots, BsTrash2Fill } from 'react-icons/bs'; 19import { MdOutlineEdit } from 'react-icons/md'; 20import { notifications } from '@mantine/notifications'; 21import type { UrlCard, User } from '@/api-client'; 22import { useState } from 'react'; 23import { BsExclamation } from 'react-icons/bs'; 24import { LinkAvatar } from '@/components/link/MantineLink'; 25import { isBotAccount } from '@/features/platforms/bluesky/lib/utils/account'; 26import BotLabel from '@/features/profile/components/botLabel/BotLabel'; 27import useUpdateNote from '../../lib/mutations/useUpdateNote'; 28import useRemoveCardFromLibrary from '@/features/cards/lib/mutations/useRemoveCardFromLibrary'; 29import styles from './NoteCardInline.module.css'; 30 31interface Props { 32 note: UrlCard['note']; 33 cardContent: UrlCard['cardContent']; 34 cardAuthor?: User; 35 isOwner: boolean; 36 onClose: () => void; 37} 38 39export default function NoteCardInline(props: Props) { 40 const [noteText, setNoteText] = useState(props.note?.text ?? ''); 41 const [editMode, setEditMode] = useState(false); 42 const [showDeleteWarning, setShowDeleteWarning] = useState(false); 43 const MAX_NOTE_LENGTH = 500; 44 45 const removeNote = useRemoveCardFromLibrary(); 46 const updateNote = useUpdateNote(); 47 48 const handleDeleteNote = () => { 49 if (!props.isOwner || !props.note) return; 50 51 removeNote.mutate(props.note.id, { 52 onError: () => { 53 notifications.show({ 54 message: 'Could not delete note', 55 position: 'top-center', 56 color: 'red', 57 title: 'Error', 58 loading: false, 59 autoClose: false, 60 withCloseButton: true, 61 icon: <BsExclamation />, 62 }); 63 }, 64 onSettled: () => { 65 props.onClose(); 66 }, 67 }); 68 }; 69 70 const handleUpdateNote = () => { 71 if (!props.note || !noteText) { 72 props.onClose(); 73 return; 74 } 75 76 if (props.note.text === noteText) { 77 setEditMode(false); 78 return; 79 } 80 81 updateNote.mutate( 82 { cardId: props.note.id, note: noteText }, 83 { 84 onError: () => { 85 notifications.show({ 86 message: 'Could not update note', 87 position: 'top-center', 88 color: 'red', 89 title: 'Error', 90 loading: false, 91 autoClose: false, 92 withCloseButton: true, 93 icon: <BsExclamation />, 94 }); 95 }, 96 onSettled: () => { 97 setEditMode(false); 98 }, 99 }, 100 ); 101 }; 102 103 if (editMode) { 104 return ( 105 <Card 106 className={styles.root} 107 radius="md" 108 p="sm" 109 onClick={(e) => e.stopPropagation()} 110 > 111 <Stack gap={'xs'}> 112 <Stack gap={0}> 113 <Flex justify="space-between"> 114 <Input.Label size="md" htmlFor="note-inline"> 115 Your note 116 </Input.Label> 117 <Text c={'gray'} aria-hidden> 118 {noteText.length} / {MAX_NOTE_LENGTH} 119 </Text> 120 </Flex> 121 <Textarea 122 id="note-inline" 123 placeholder="Add a note about this card" 124 variant="filled" 125 size="md" 126 autosize 127 minRows={3} 128 maxRows={8} 129 maxLength={MAX_NOTE_LENGTH} 130 value={noteText} 131 onChange={(e) => setNoteText(e.currentTarget.value)} 132 /> 133 <VisuallyHidden id="note-inline-char-remaining" aria-live="polite"> 134 {`${MAX_NOTE_LENGTH - noteText.length} characters remaining`} 135 </VisuallyHidden> 136 </Stack> 137 <Group gap={'xs'} grow> 138 <Button 139 variant="light" 140 color="gray" 141 onClick={() => { 142 setEditMode(false); 143 setNoteText(props.note?.text ?? ''); 144 }} 145 > 146 Cancel 147 </Button> 148 <Button 149 onClick={handleUpdateNote} 150 loading={updateNote.isPending} 151 disabled={noteText.trimEnd() === ''} 152 > 153 Save 154 </Button> 155 </Group> 156 </Stack> 157 </Card> 158 ); 159 } 160 161 return ( 162 <Card 163 className={styles.root} 164 radius="md" 165 p="sm" 166 onClick={(e) => e.stopPropagation()} 167 > 168 <Stack gap={'xs'}> 169 <Group justify="space-between" wrap="nowrap"> 170 {props.cardAuthor ? ( 171 <Group gap={'5'}> 172 <LinkAvatar 173 href={`/profile/${props.cardAuthor.handle}`} 174 src={props.cardAuthor.avatarUrl?.replace( 175 'avatar', 176 'avatar_thumbnail', 177 )} 178 alt={`${props.cardAuthor.handle}'s avatar`} 179 size={'xs'} 180 radius={'sm'} 181 /> 182 <Anchor 183 href={`/profile/${props.cardAuthor.handle}`} 184 fz={'xs'} 185 fw={600} 186 c={'bright'} 187 underline="never" 188 onClick={(e) => e.stopPropagation()} 189 > 190 {props.cardAuthor.name || `@${props.cardAuthor.handle}`} 191 </Anchor> 192 {isBotAccount(props.cardAuthor) && <BotLabel />} 193 </Group> 194 ) : ( 195 <span /> 196 )} 197 <Group gap={'xs'}> 198 {props.isOwner && ( 199 <Menu shadow="sm" position="bottom-end"> 200 <Menu.Target> 201 <ActionIcon 202 variant="light" 203 color="gray" 204 size="md" 205 radius="xl" 206 onClick={(e) => e.stopPropagation()} 207 > 208 <BsThreeDots size={18} /> 209 </ActionIcon> 210 </Menu.Target> 211 <Menu.Dropdown onClick={(e) => e.stopPropagation()}> 212 <Menu.Item 213 leftSection={<MdOutlineEdit size={14} />} 214 onClick={() => setEditMode(true)} 215 > 216 Edit note 217 </Menu.Item> 218 <Menu.Item 219 color="red" 220 leftSection={<BsTrash2Fill size={14} />} 221 onClick={() => setShowDeleteWarning(true)} 222 > 223 Delete note 224 </Menu.Item> 225 </Menu.Dropdown> 226 </Menu> 227 )} 228 <ActionIcon 229 variant="light" 230 color="gray" 231 size="md" 232 radius="xl" 233 onClick={(e) => { 234 e.stopPropagation(); 235 props.onClose(); 236 }} 237 > 238 <IoEyeOffOutline size={18} /> 239 </ActionIcon> 240 </Group> 241 </Group> 242 243 {props.note && ( 244 <Text 245 fw={500} 246 fz={'sm'} 247 fs={'italic'} 248 c={'gray'} 249 style={{ cursor: 'pointer' }} 250 onClick={(e) => { 251 e.stopPropagation(); 252 props.onClose(); 253 }} 254 > 255 {props.note.text} 256 </Text> 257 )} 258 259 {showDeleteWarning && ( 260 <Group justify="space-between" gap={'xs'}> 261 <Text fz="sm">Delete note?</Text> 262 <Group gap={'xs'}> 263 <Button 264 color="red" 265 size="xs" 266 onClick={handleDeleteNote} 267 loading={removeNote.isPending} 268 > 269 Delete 270 </Button> 271 <Button 272 variant="light" 273 color="gray" 274 size="xs" 275 onClick={(e) => { 276 e.stopPropagation(); 277 setShowDeleteWarning(false); 278 }} 279 > 280 Cancel 281 </Button> 282 </Group> 283 </Group> 284 )} 285 </Stack> 286 </Card> 287 ); 288}