This repository has no description
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}