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