alpha
Login
or
Join now
nandi.uk
/
semble
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
Merge branch 'development'
author
pdelfan
date
2 months ago
(May 21, 2026, 5:23 PM -0700)
commit
6669a4f2
6669a4f23b6f029aba9b000abdef38609b61325e
parent
4631c3ae
4631c3aef2d83cc1b25d97be74e07260b6b80338
+290
-193
24 changed files
Expand all
Collapse all
Unified
Split
package-lock.json
src
webapp
app
(dashboard)
profile
[handle]
(withoutHeader)
collections
[rkey]
layout.tsx
url
layout.tsx
components
navigation
backButton
BackButton.tsx
features
cards
components
addCardActions
AddCardActions.tsx
addCardToModal
AddCardToModalContent.tsx
lib
mutations
useAddCard.tsx
useRemoveCardFromLibrary.tsx
collections
components
CollectionShareHeaderButton
CollectionShareHeaderButton.tsx
collectionActions
CollectionActions.tsx
Skeleton.CollectionActions.tsx
lib
mutations
useCreateCollection.tsx
useDeleteCollection.tsx
notes
components
noteCardInline
NoteCardInline.tsx
profile
components
profileTabs
ProfileTabs.tsx
lib
queries
useProfile.tsx
semble
components
ShareHeaderButton
ShareHeaderButton.tsx
sembleActions
GuestSembleActions.tsx
GusetSembleActions.tsx
SembleActions.tsx
containers
sembleActionsContainer
SembleActionsContainer.tsx
Skeleton.SembleActionsContainer.tsx
next.config.mjs
package.json
+12
-31
package-lock.json
View file
Reviewed
···
1264
1264
"version": "7.27.1",
1265
1265
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
1266
1266
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
1267
1267
-
"devOptional": true,
1268
1267
"engines": {
1269
1268
"node": ">=6.9.0"
1270
1269
}
···
1273
1272
"version": "7.28.5",
1274
1273
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
1275
1274
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
1276
1276
-
"devOptional": true,
1277
1275
"license": "MIT",
1278
1276
"engines": {
1279
1277
"node": ">=6.9.0"
···
1583
1581
"version": "7.29.0",
1584
1582
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
1585
1583
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
1586
1586
-
"devOptional": true,
1587
1584
"license": "MIT",
1588
1585
"dependencies": {
1589
1586
"@babel/helper-string-parser": "^7.27.1",
···
14198
14195
},
14199
14196
"engines": {
14200
14197
"node": "^14.15.0 || ^16.10.0 || >=18.0.0"
14198
14198
+
}
14199
14199
+
},
14200
14200
+
"node_modules/babel-plugin-react-compiler": {
14201
14201
+
"version": "1.0.0",
14202
14202
+
"resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz",
14203
14203
+
"integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==",
14204
14204
+
"license": "MIT",
14205
14205
+
"peer": true,
14206
14206
+
"dependencies": {
14207
14207
+
"@babel/types": "^7.26.0"
14201
14208
}
14202
14209
},
14203
14210
"node_modules/babel-preset-current-node-syntax": {
···
31499
31506
"integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==",
31500
31507
"dev": true
31501
31508
},
31502
31502
-
"node_modules/web-haptics": {
31503
31503
-
"version": "0.0.6",
31504
31504
-
"resolved": "https://registry.npmjs.org/web-haptics/-/web-haptics-0.0.6.tgz",
31505
31505
-
"integrity": "sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==",
31506
31506
-
"license": "MIT",
31507
31507
-
"peerDependencies": {
31508
31508
-
"react": ">=18",
31509
31509
-
"react-dom": ">=18",
31510
31510
-
"svelte": ">=4",
31511
31511
-
"vue": ">=3"
31512
31512
-
},
31513
31513
-
"peerDependenciesMeta": {
31514
31514
-
"react": {
31515
31515
-
"optional": true
31516
31516
-
},
31517
31517
-
"react-dom": {
31518
31518
-
"optional": true
31519
31519
-
},
31520
31520
-
"svelte": {
31521
31521
-
"optional": true
31522
31522
-
},
31523
31523
-
"vue": {
31524
31524
-
"optional": true
31525
31525
-
}
31526
31526
-
}
31527
31527
-
},
31528
31509
"node_modules/web-vitals": {
31529
31510
"version": "5.1.0",
31530
31511
"resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-5.1.0.tgz",
···
32215
32196
"@types/mdx": "^2.0.13",
32216
32197
"@vercel/analytics": "^1.5.0",
32217
32198
"@vidstack/react": "^1.12.13",
32199
32199
+
"babel-plugin-react-compiler": "^1.0.0",
32218
32200
"capnweb": "^0.6.1",
32219
32201
"date-fns": "^4.1.0",
32220
32202
"dayjs": "^1.11.13",
···
32228
32210
"react-force-graph": "^1.48.2",
32229
32211
"react-force-graph-2d": "^1.29.1",
32230
32212
"react-icons": "^5.5.0",
32231
32231
-
"shiki": "^3.20.0",
32232
32232
-
"web-haptics": "^0.0.6"
32213
32213
+
"shiki": "^3.20.0"
32233
32214
},
32234
32215
"devDependencies": {
32235
32216
"@chromatic-com/storybook": "^5.1.2",
+2
src/webapp/app/(dashboard)/profile/[handle]/(withoutHeader)/collections/[rkey]/layout.tsx
View file
Reviewed
···
5
5
import { Fragment, Suspense } from 'react';
6
6
import CollectionHeader from '@/features/collections/components/collectionHeader/CollectionHeader';
7
7
import CollectionHeaderSkeleton from '@/features/collections/components/collectionHeader/Skeleton.CollectionHeader';
8
8
+
import CollectionShareHeaderButton from '@/features/collections/components/CollectionShareHeaderButton/CollectionShareHeaderButton';
8
9
import CollectionTabs from '@/features/collections/components/collectionTabs/CollectionTabs';
9
10
import { Container } from '@mantine/core';
10
11
···
50
51
<Fragment>
51
52
<Header>
52
53
<BackButton />
54
54
+
<CollectionShareHeaderButton handle={handle} rkey={rkey} />
53
55
</Header>
54
56
<Suspense fallback={<CollectionHeaderSkeleton />}>
55
57
<CollectionHeader handle={handle} rkey={rkey} />
+4
src/webapp/app/(dashboard)/url/layout.tsx
View file
Reviewed
···
1
1
import BackButton from '@/components/navigation/backButton/BackButton';
2
2
import Header from '@/components/navigation/header/Header';
3
3
import ReaderHeaderButton from '@/features/reader/components/ReaderHeaderButton/ReaderHeaderButton';
4
4
+
import ShareHeaderButton from '@/features/semble/components/ShareHeaderButton/ShareHeaderButton';
4
5
import { Fragment, Suspense } from 'react';
5
6
6
7
interface Props {
···
14
15
<BackButton />
15
16
<Suspense fallback={null}>
16
17
<ReaderHeaderButton />
18
18
+
</Suspense>
19
19
+
<Suspense fallback={null}>
20
20
+
<ShareHeaderButton />
17
21
</Suspense>
18
22
</Header>
19
23
{props.children}
+1
-1
src/webapp/components/navigation/backButton/BackButton.tsx
View file
Reviewed
···
14
14
return (
15
15
<ActionIcon
16
16
onClick={() => router.back()}
17
17
-
variant="light"
17
17
+
variant="subtle"
18
18
size="lg"
19
19
color="gray"
20
20
radius={'xl'}
+81
-16
src/webapp/features/cards/components/addCardActions/AddCardActions.tsx
View file
Reviewed
···
14
14
Textarea,
15
15
VisuallyHidden,
16
16
} from '@mantine/core';
17
17
-
import { BsExclamation } from 'react-icons/bs';
17
17
+
import { BsExclamation, BsTrash2Fill } from 'react-icons/bs';
18
18
+
import { MdOutlineStickyNote2 } from 'react-icons/md';
18
19
19
20
interface Props {
21
21
+
cardId?: string;
20
22
note?: string;
21
23
noteId?: string;
22
24
onUpdateNote: Dispatch<SetStateAction<string | undefined>>;
···
25
27
26
28
export default function AddCardActions(props: Props) {
27
29
const [showDeleteWarning, setShowDeleteWarning] = useState(false);
30
30
+
const [showDeleteCardWarning, setShowDeleteCardWarning] = useState(false);
28
31
const [noteMode, setNoteMode] = useState(false);
29
32
const [note, setNote] = useState(props.note);
30
33
const MAX_NOTE_LENGTH = 500;
31
34
32
35
const removeNote = useRemoveCardFromLibrary();
36
36
+
const removeCard = useRemoveCardFromLibrary();
33
37
34
38
const handleDeleteNote = () => {
35
39
if (!props.noteId) return;
···
51
55
});
52
56
};
53
57
58
58
+
const handleDeleteCard = () => {
59
59
+
if (!props.cardId) return;
60
60
+
61
61
+
removeCard.mutate(props.cardId, {
62
62
+
onError: () => {
63
63
+
notifications.show({
64
64
+
message: 'Could not delete card',
65
65
+
color: 'red',
66
66
+
autoClose: 5000,
67
67
+
withCloseButton: true,
68
68
+
position: 'top-center',
69
69
+
icon: <BsExclamation />,
70
70
+
});
71
71
+
},
72
72
+
onSettled: () => {
73
73
+
props.onClose();
74
74
+
},
75
75
+
});
76
76
+
};
77
77
+
54
78
if (noteMode) {
55
79
return (
56
80
<Card
···
118
142
<Text>Delete note?</Text>
119
143
<Group gap={'xs'}>
120
144
<Button
145
145
+
size="xs"
121
146
color="red"
122
147
onClick={handleDeleteNote}
123
148
loading={removeNote.isPending}
···
127
152
<Button
128
153
variant="light"
129
154
color="gray"
155
155
+
size="xs"
130
156
onClick={() => setShowDeleteWarning(false)}
131
157
>
132
158
Cancel
133
159
</Button>
134
160
</Group>
135
161
</Group>
162
162
+
) : showDeleteCardWarning ? (
163
163
+
<Group justify="space-between" gap={'xs'}>
164
164
+
<Text>Delete card?</Text>
165
165
+
<Group gap={'xs'}>
166
166
+
<Button
167
167
+
color="red"
168
168
+
size="xs"
169
169
+
onClick={handleDeleteCard}
170
170
+
loading={removeCard.isPending}
171
171
+
>
172
172
+
Delete
173
173
+
</Button>
174
174
+
<Button
175
175
+
variant="light"
176
176
+
color="gray"
177
177
+
size="xs"
178
178
+
onClick={() => setShowDeleteCardWarning(false)}
179
179
+
>
180
180
+
Cancel
181
181
+
</Button>
182
182
+
</Group>
183
183
+
</Group>
136
184
) : (
137
137
-
<Group gap={'xs'}>
138
138
-
<Button
139
139
-
variant="light"
140
140
-
size="xs"
141
141
-
color="gray"
142
142
-
onClick={(e) => {
143
143
-
e.stopPropagation();
144
144
-
setNoteMode(true);
145
145
-
}}
146
146
-
>
147
147
-
{note ? 'Edit note' : 'Add note'}
148
148
-
</Button>
149
149
-
{props.noteId && (
185
185
+
<Group gap={'xs'} justify="space-between">
186
186
+
<Group gap={'xs'}>
187
187
+
<Button
188
188
+
variant="light"
189
189
+
size="xs"
190
190
+
color="gray"
191
191
+
leftSection={<MdOutlineStickyNote2 />}
192
192
+
onClick={(e) => {
193
193
+
e.stopPropagation();
194
194
+
setNoteMode(true);
195
195
+
}}
196
196
+
>
197
197
+
{note ? 'Edit note' : 'Add note'}
198
198
+
</Button>
199
199
+
{props.noteId && (
200
200
+
<Button
201
201
+
variant="light"
202
202
+
color="red"
203
203
+
size="xs"
204
204
+
onClick={(e) => {
205
205
+
e.stopPropagation();
206
206
+
setShowDeleteWarning(true);
207
207
+
}}
208
208
+
>
209
209
+
Delete note
210
210
+
</Button>
211
211
+
)}
212
212
+
</Group>
213
213
+
{props.cardId && (
150
214
<Button
151
215
variant="light"
152
216
color="red"
153
217
size="xs"
218
218
+
leftSection={<BsTrash2Fill />}
154
219
onClick={(e) => {
155
220
e.stopPropagation();
156
156
-
setShowDeleteWarning(true);
221
221
+
setShowDeleteCardWarning(true);
157
222
}}
158
223
>
159
159
-
Delete note
224
224
+
Delete card
160
225
</Button>
161
226
)}
162
227
</Group>
+1
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
View file
Reviewed
···
117
117
return (
118
118
<Stack>
119
119
<AddCardActions
120
120
+
cardId={cardStatus.data.card?.id}
120
121
note={isMyCard ? note : cardStatus.data.card?.note?.text}
121
122
noteId={cardStatus.data.card?.note?.id}
122
123
onUpdateNote={setNote}
+30
-4
src/webapp/features/cards/lib/mutations/useAddCard.tsx
View file
Reviewed
···
6
6
import { noteKeys } from '@/features/notes/lib/noteKeys';
7
7
import { sembleKeys } from '@/features/semble/lib/sembleKeys';
8
8
import { connectionKeys } from '@/features/connections/lib/connectionKeys';
9
9
+
import { profileKeys } from '@/features/profile/lib/profileKeys';
9
10
import posthog from 'posthog-js';
10
11
import {
11
12
CardSaveAnalyticsContext,
···
13
14
} from '@/features/analytics/types';
14
15
import { shouldCaptureAnalytics } from '@/features/analytics/utils';
15
16
import { notifications } from '@mantine/notifications';
17
17
+
import { Button, Group, Text } from '@mantine/core';
16
18
import { BsCheck, BsExclamation } from 'react-icons/bs';
19
19
+
import { useRouter } from 'next/navigation';
17
20
18
21
export default function useAddCard(
19
22
analyticsContext?: CardSaveAnalyticsContext,
20
23
) {
21
24
const queryClient = useQueryClient();
25
25
+
const router = useRouter();
22
26
23
27
const mutation = useMutation({
24
28
mutationFn: async (newCard: {
···
42
46
// https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire
43
47
onSuccess: (_data, variables) => {
44
48
if (variables.notificationId) {
49
49
+
const notificationId = variables.notificationId;
45
50
notifications.update({
46
46
-
id: variables.notificationId,
51
51
+
id: notificationId,
47
52
color: 'green',
48
48
-
title: 'Success!',
49
49
-
message: 'Card added',
53
53
+
title: null,
54
54
+
message: (
55
55
+
<Group
56
56
+
gap="xs"
57
57
+
justify="space-between"
58
58
+
wrap="nowrap"
59
59
+
align="center"
60
60
+
>
61
61
+
<Text size="sm">Card added</Text>
62
62
+
<Button
63
63
+
size="xs"
64
64
+
variant="light"
65
65
+
color="gray"
66
66
+
onClick={() => {
67
67
+
notifications.hide(notificationId);
68
68
+
router.push(`/url?id=${encodeURIComponent(variables.url)}`);
69
69
+
}}
70
70
+
>
71
71
+
View
72
72
+
</Button>
73
73
+
</Group>
74
74
+
),
50
75
position: 'top-center',
51
76
loading: false,
52
52
-
autoClose: 2000,
77
77
+
autoClose: 5000,
53
78
icon: <BsCheck />,
54
79
});
55
80
}
···
62
87
queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() });
63
88
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
64
89
queryClient.invalidateQueries({ queryKey: connectionKeys.all() });
90
90
+
queryClient.invalidateQueries({ queryKey: profileKeys.all() });
65
91
queryClient.invalidateQueries({
66
92
queryKey: collectionKeys.bySembleUrl(variables.url),
67
93
});
+2
src/webapp/features/cards/lib/mutations/useRemoveCardFromLibrary.tsx
View file
Reviewed
···
6
6
import { feedKeys } from '@/features/feeds/lib/feedKeys';
7
7
import { sembleKeys } from '@/features/semble/lib/sembleKeys';
8
8
import { connectionKeys } from '@/features/connections/lib/connectionKeys';
9
9
+
import { profileKeys } from '@/features/profile/lib/profileKeys';
9
10
10
11
export default function useRemoveCardFromLibrary() {
11
12
const queryClient = useQueryClient();
···
22
23
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
23
24
queryClient.invalidateQueries({ queryKey: sembleKeys.all() });
24
25
queryClient.invalidateQueries({ queryKey: connectionKeys.all() });
26
26
+
queryClient.invalidateQueries({ queryKey: profileKeys.all() });
25
27
// Invalidate all URL metadata queries with stats to update tab counts
26
28
queryClient.invalidateQueries({
27
29
predicate: (query): boolean => {
+46
src/webapp/features/collections/components/CollectionShareHeaderButton/CollectionShareHeaderButton.tsx
View file
Reviewed
···
1
1
+
'use client';
2
2
+
3
3
+
import { ActionIcon, CopyButton, Tooltip } from '@mantine/core';
4
4
+
import { notifications } from '@mantine/notifications';
5
5
+
import { MdIosShare } from 'react-icons/md';
6
6
+
7
7
+
interface Props {
8
8
+
handle: string;
9
9
+
rkey: string;
10
10
+
}
11
11
+
12
12
+
export default function CollectionShareHeaderButton(props: Props) {
13
13
+
const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/profile/${props.handle}/collections/${props.rkey}`;
14
14
+
15
15
+
return (
16
16
+
<CopyButton value={shareLink}>
17
17
+
{({ copied, copy }) => (
18
18
+
<Tooltip
19
19
+
label={copied ? 'Link copied!' : 'Share'}
20
20
+
withArrow
21
21
+
position="top"
22
22
+
>
23
23
+
<ActionIcon
24
24
+
variant="light"
25
25
+
color="gray"
26
26
+
size={36}
27
27
+
radius={'xl'}
28
28
+
onClick={(e) => {
29
29
+
e.stopPropagation();
30
30
+
copy();
31
31
+
32
32
+
if (copied) return;
33
33
+
notifications.show({
34
34
+
message: 'Link copied!',
35
35
+
position: 'top-center',
36
36
+
id: `${props.handle}/${props.rkey}`,
37
37
+
});
38
38
+
}}
39
39
+
>
40
40
+
<MdIosShare />
41
41
+
</ActionIcon>
42
42
+
</Tooltip>
43
43
+
)}
44
44
+
</CopyButton>
45
45
+
);
46
46
+
}
+1
-30
src/webapp/features/collections/components/collectionActions/CollectionActions.tsx
View file
Reviewed
···
1
1
import { Collection, CollectionAccessType } from '@semble/types';
2
2
-
import { Group, Menu, ActionIcon, CopyButton } from '@mantine/core';
2
2
+
import { Group, Menu, ActionIcon } from '@mantine/core';
3
3
import EditCollectionModal from '../editCollectionModal/EditCollectionModal';
4
4
import DeleteCollectionModal from '../deleteCollectionModal/DeleteCollectionModal';
5
5
import { BsThreeDots, BsPencilFill, BsTrash2Fill } from 'react-icons/bs';
6
6
-
import { MdIosShare } from 'react-icons/md';
7
6
import { Fragment, useState } from 'react';
8
7
import { useAuth } from '@/hooks/useAuth';
9
8
import { FiPlus } from 'react-icons/fi';
10
9
import AddCardDrawer from '@/features/cards/components/addCardDrawer/AddCardDrawer';
11
10
import AddCardToModal from '@/features/cards/components/addCardToModal/AddCardToModal';
12
12
-
import { notifications } from '@mantine/notifications';
13
11
import FollowButton from '@/features/follows/components/followButton/FollowButton';
14
12
import useGetCardFromMyLibrary from '@/features/cards/lib/queries/useGetCardFromMyLibrary';
15
13
import useSembleLibraries from '@/features/semble/lib/queries/useSembleLibraries';
···
177
175
const isAuthor =
178
176
isAuthenticated && user?.handle === props.collection.author?.handle;
179
177
180
180
-
const shareLink =
181
181
-
typeof window !== 'undefined'
182
182
-
? `${window.location.origin}/profile/${props.collection.author?.handle}/collections/${props.collection.rkey}`
183
183
-
: '';
184
184
-
185
178
return (
186
179
<Fragment>
187
180
<Group gap={'xs'}>
188
181
{isAuthenticated && (
189
182
<AuthenticatedCollectionActions collection={props.collection} />
190
183
)}
191
191
-
192
192
-
<CopyButton value={shareLink}>
193
193
-
{({ copy }) => (
194
194
-
<ActionIcon
195
195
-
variant="light"
196
196
-
color="gray"
197
197
-
size={'lg'}
198
198
-
radius={'xl'}
199
199
-
onClick={(e) => {
200
200
-
e.stopPropagation();
201
201
-
copy();
202
202
-
notifications.show({
203
203
-
message: 'Link copied!',
204
204
-
position: 'top-center',
205
205
-
id: props.collection.id,
206
206
-
});
207
207
-
}}
208
208
-
>
209
209
-
<MdIosShare size={18} />
210
210
-
</ActionIcon>
211
211
-
)}
212
212
-
</CopyButton>
213
184
214
185
{isAuthor && <AuthorCollectionMenu collection={props.collection} />}
215
186
</Group>
-1
src/webapp/features/collections/components/collectionActions/Skeleton.CollectionActions.tsx
View file
Reviewed
···
6
6
<Skeleton w={34} h={34} radius={'xl'} />
7
7
<Skeleton w={34} h={34} radius={'xl'} />
8
8
<Skeleton w={34} h={34} radius={'xl'} />
9
9
-
<Skeleton w={34} h={34} radius={'xl'} />
10
9
</Group>
11
10
);
12
11
}
+2
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
View file
Reviewed
···
2
2
import { useMutation, useQueryClient } from '@tanstack/react-query';
3
3
import { createCollection } from '../dal';
4
4
import { collectionKeys } from '../collectionKeys';
5
5
+
import { profileKeys } from '@/features/profile/lib/profileKeys';
5
6
6
7
export default function useCreateCollection() {
7
8
const queryClient = useQueryClient();
···
20
21
// https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire
21
22
onSuccess: () => {
22
23
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
24
24
+
queryClient.invalidateQueries({ queryKey: profileKeys.all() });
23
25
queryClient.refetchQueries({ queryKey: collectionKeys.mine() });
24
26
},
25
27
});
+2
src/webapp/features/collections/lib/mutations/useDeleteCollection.tsx
View file
Reviewed
···
1
1
import { useMutation, useQueryClient } from '@tanstack/react-query';
2
2
import { deleteCollection } from '../dal';
3
3
import { collectionKeys } from '../collectionKeys';
4
4
+
import { profileKeys } from '@/features/profile/lib/profileKeys';
4
5
5
6
export default function useDeleteCollection() {
6
7
const queryClient = useQueryClient();
···
12
13
13
14
onSuccess: () => {
14
15
queryClient.invalidateQueries({ queryKey: collectionKeys.all() });
16
16
+
queryClient.invalidateQueries({ queryKey: profileKeys.all() });
15
17
},
16
18
});
17
19
-1
src/webapp/features/notes/components/noteCardInline/NoteCardInline.tsx
View file
Reviewed
···
121
121
<Textarea
122
122
id="note-inline"
123
123
placeholder="Add a note about this card"
124
124
-
variant="filled"
125
124
size="md"
126
125
autosize
127
126
minRows={3}
+3
-3
src/webapp/features/profile/components/profileTabs/ProfileTabs.tsx
View file
Reviewed
···
32
32
<TabItem
33
33
value="cards"
34
34
href={`${basePath}/cards`}
35
35
-
count={profile.urlCardCount}
35
35
+
count={profile?.urlCardCount}
36
36
>
37
37
Cards
38
38
</TabItem>
39
39
<TabItem
40
40
value="collections"
41
41
href={`${basePath}/collections`}
42
42
-
count={profile.collectionCount}
42
42
+
count={profile?.collectionCount}
43
43
>
44
44
Collections
45
45
</TabItem>
46
46
<TabItem
47
47
value="connections"
48
48
href={`${basePath}/connections`}
49
49
-
count={profile.connectionCount}
49
49
+
count={profile?.connectionCount}
50
50
>
51
51
Connections
52
52
</TabItem>
+31
-7
src/webapp/features/profile/lib/queries/useProfile.tsx
View file
Reviewed
···
1
1
-
import { useSuspenseQuery } from '@tanstack/react-query';
1
1
+
import {
2
2
+
useSuspenseQuery,
3
3
+
useQuery,
4
4
+
UseQueryResult,
5
5
+
UseSuspenseQueryResult,
6
6
+
} from '@tanstack/react-query';
2
7
import { getProfile } from '../dal';
3
8
import { profileKeys } from '../profileKeys';
4
9
5
5
-
interface Props {
10
10
+
type ProfileData = Awaited<ReturnType<typeof getProfile>>;
11
11
+
12
12
+
interface PropsWithStats {
6
13
didOrHandle: string;
7
7
-
includeStats?: boolean;
14
14
+
includeStats: true;
8
15
}
9
16
10
10
-
export default function useProfile(props: Props) {
11
11
-
const profile = useSuspenseQuery({
17
17
+
interface PropsWithoutStats {
18
18
+
didOrHandle: string;
19
19
+
includeStats?: false;
20
20
+
}
21
21
+
22
22
+
export default function useProfile(
23
23
+
props: PropsWithStats,
24
24
+
): UseQueryResult<ProfileData>;
25
25
+
export default function useProfile(
26
26
+
props: PropsWithoutStats,
27
27
+
): UseSuspenseQueryResult<ProfileData>;
28
28
+
export default function useProfile(props: PropsWithStats | PropsWithoutStats) {
29
29
+
if (props.includeStats) {
30
30
+
// Non-suspense: stats are progressive — tabs render immediately, counts fill in async
31
31
+
return useQuery({
32
32
+
queryKey: profileKeys.profile(props.didOrHandle, true),
33
33
+
queryFn: () => getProfile(props.didOrHandle, true),
34
34
+
});
35
35
+
}
36
36
+
37
37
+
return useSuspenseQuery({
12
38
queryKey: profileKeys.profile(props.didOrHandle, props.includeStats),
13
39
queryFn: () => getProfile(props.didOrHandle, props.includeStats),
14
40
});
15
15
-
16
16
-
return profile;
17
41
}
+46
src/webapp/features/semble/components/ShareHeaderButton/ShareHeaderButton.tsx
View file
Reviewed
···
1
1
+
'use client';
2
2
+
3
3
+
import { ActionIcon, CopyButton, Tooltip } from '@mantine/core';
4
4
+
import { notifications } from '@mantine/notifications';
5
5
+
import { useSearchParams } from 'next/navigation';
6
6
+
import { MdIosShare } from 'react-icons/md';
7
7
+
8
8
+
export default function ShareHeaderButton() {
9
9
+
const searchParams = useSearchParams();
10
10
+
const rawId = searchParams.get('id');
11
11
+
12
12
+
if (!rawId) return null;
13
13
+
14
14
+
const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/url?id=${rawId}`;
15
15
+
16
16
+
return (
17
17
+
<CopyButton value={shareLink}>
18
18
+
{({ copied, copy }) => (
19
19
+
<Tooltip
20
20
+
label={copied ? 'Link copied!' : 'Share'}
21
21
+
withArrow
22
22
+
position="top"
23
23
+
>
24
24
+
<ActionIcon
25
25
+
variant="light"
26
26
+
color="gray"
27
27
+
size={36}
28
28
+
radius={'xl'}
29
29
+
onClick={() => {
30
30
+
copy();
31
31
+
32
32
+
if (copied) return;
33
33
+
notifications.show({
34
34
+
message: 'Link copied!',
35
35
+
position: 'top-center',
36
36
+
id: copied.toString(),
37
37
+
});
38
38
+
}}
39
39
+
>
40
40
+
<MdIosShare />
41
41
+
</ActionIcon>
42
42
+
</Tooltip>
43
43
+
)}
44
44
+
</CopyButton>
45
45
+
);
46
46
+
}
+20
src/webapp/features/semble/components/sembleActions/GuestSembleActions.tsx
View file
Reviewed
···
1
1
+
import { Group } from '@mantine/core';
2
2
+
import { TbPlugConnected } from 'react-icons/tb';
3
3
+
import { LinkButton } from '@/components/link/MantineLink';
4
4
+
5
5
+
export default function GuestSembleActions() {
6
6
+
return (
7
7
+
<Group gap={'xs'}>
8
8
+
<LinkButton
9
9
+
href={'/login'}
10
10
+
variant="light"
11
11
+
color="green"
12
12
+
radius={'xl'}
13
13
+
leftSection={<TbPlugConnected size={18} />}
14
14
+
>
15
15
+
Log in to connect
16
16
+
</LinkButton>
17
17
+
<LinkButton href={'/login'}>Log in to add</LinkButton>
18
18
+
</Group>
19
19
+
);
20
20
+
}
-61
src/webapp/features/semble/components/sembleActions/GusetSembleActions.tsx
Reviewed
···
1
1
-
'use client';
2
2
-
3
3
-
import { ActionIcon, CopyButton, Group, Tooltip } from '@mantine/core';
4
4
-
import { MdIosShare } from 'react-icons/md';
5
5
-
import { notifications } from '@mantine/notifications';
6
6
-
import { TbPlugConnected } from 'react-icons/tb';
7
7
-
import { LinkButton } from '@/components/link/MantineLink';
8
8
-
9
9
-
interface Props {
10
10
-
url: string;
11
11
-
}
12
12
-
13
13
-
export default function GuestSembleActions(props: Props) {
14
14
-
const shareLink =
15
15
-
typeof window !== 'undefined'
16
16
-
? `${window.location.origin}/url?id=${props.url}`
17
17
-
: '';
18
18
-
19
19
-
return (
20
20
-
<Group gap={'xs'}>
21
21
-
<CopyButton value={shareLink}>
22
22
-
{({ copied, copy }) => (
23
23
-
<Tooltip
24
24
-
label={copied ? 'Link copied!' : 'Share'}
25
25
-
withArrow
26
26
-
position="top"
27
27
-
>
28
28
-
<ActionIcon
29
29
-
variant="light"
30
30
-
color="gray"
31
31
-
size={36}
32
32
-
radius={'xl'}
33
33
-
onClick={() => {
34
34
-
copy();
35
35
-
36
36
-
if (copied) return;
37
37
-
notifications.show({
38
38
-
message: 'Link copied!',
39
39
-
position: 'top-center',
40
40
-
id: copied.toString(),
41
41
-
});
42
42
-
}}
43
43
-
>
44
44
-
<MdIosShare size={22} />
45
45
-
</ActionIcon>
46
46
-
</Tooltip>
47
47
-
)}
48
48
-
</CopyButton>
49
49
-
<LinkButton
50
50
-
href={'/login'}
51
51
-
variant="light"
52
52
-
color="green"
53
53
-
radius={'xl'}
54
54
-
leftSection={<TbPlugConnected size={18} />}
55
55
-
>
56
56
-
Log in to connect
57
57
-
</LinkButton>
58
58
-
<LinkButton href={'/login'}>Log in to add</LinkButton>
59
59
-
</Group>
60
60
-
);
61
61
-
}
+1
-33
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
View file
Reviewed
···
3
3
import AddCardToModal from '@/features/cards/components/addCardToModal/AddCardToModal';
4
4
import RemoveCardFromLibraryModal from '@/features/cards/components/removeCardFromLibraryModal/RemoveCardFromLibraryModal';
5
5
import useGetCardFromMyLibrary from '@/features/cards/lib/queries/useGetCardFromMyLibrary';
6
6
-
import { ActionIcon, Button, CopyButton, Group, Tooltip } from '@mantine/core';
7
7
-
import { notifications } from '@mantine/notifications';
6
6
+
import { ActionIcon, Button, Group } from '@mantine/core';
8
7
import { Fragment, useState } from 'react';
9
8
import { FiPlus } from 'react-icons/fi';
10
9
import { IoMdCheckmark } from 'react-icons/io';
11
11
-
import { MdIosShare } from 'react-icons/md';
12
10
import useSembleLibraries from '../../lib/queries/useSembleLibraries';
13
11
import { track } from '@vercel/analytics';
14
12
import { CardSaveSource } from '@/features/analytics/types';
···
36
34
37
35
const urlLibraryCount = allLibraries.length ?? 0;
38
36
39
39
-
const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/url?id=${props.url}`;
40
40
-
41
37
if (cardStatus.error) {
42
38
return null;
43
39
}
···
45
41
return (
46
42
<Fragment>
47
43
<Group gap={'xs'}>
48
48
-
<CopyButton value={shareLink}>
49
49
-
{({ copied, copy }) => (
50
50
-
<Tooltip
51
51
-
label={copied ? 'Link copied!' : 'Share'}
52
52
-
withArrow
53
53
-
position="top"
54
54
-
>
55
55
-
<ActionIcon
56
56
-
variant="light"
57
57
-
color="gray"
58
58
-
size={36}
59
59
-
radius={'xl'}
60
60
-
onClick={() => {
61
61
-
copy();
62
62
-
63
63
-
if (copied) return;
64
64
-
notifications.show({
65
65
-
message: 'Link copied!',
66
66
-
position: 'top-center',
67
67
-
id: copied.toString(),
68
68
-
});
69
69
-
}}
70
70
-
>
71
71
-
<MdIosShare />
72
72
-
</ActionIcon>
73
73
-
</Tooltip>
74
74
-
)}
75
75
-
</CopyButton>
76
44
<Button
77
45
variant="light"
78
46
color="green"
+2
-2
src/webapp/features/semble/containers/sembleActionsContainer/SembleActionsContainer.tsx
View file
Reviewed
···
1
1
import { verifySessionOnServer } from '@/lib/auth/dal.server';
2
2
-
import GuestSembleActions from '../../components/sembleActions/GusetSembleActions';
2
2
+
import GuestSembleActions from '../../components/sembleActions/GuestSembleActions';
3
3
import SembleActions from '../../components/sembleActions/SembleActions';
4
4
5
5
interface Props {
···
11
11
const session = await verifySessionOnServer();
12
12
13
13
if (!session) {
14
14
-
return <GuestSembleActions url={props.url} />;
14
14
+
return <GuestSembleActions />;
15
15
}
16
16
17
17
return <SembleActions url={props.url} viaCardId={props.viaCardId} />;
-1
src/webapp/features/semble/containers/sembleActionsContainer/Skeleton.SembleActionsContainer.tsx
View file
Reviewed
···
4
4
return (
5
5
<Stack gap="sm" align="center">
6
6
<Group gap="xs">
7
7
-
<Skeleton w={36} h={36} circle />
8
7
<Skeleton w={114} h={36} radius="xl" />
9
8
<Skeleton w={113} h={36} radius="xl" />
10
9
<Skeleton w={36} h={36} circle />
+1
src/webapp/next.config.mjs
View file
Reviewed
···
2
2
3
3
/** @type {import('next').NextConfig} */
4
4
const nextConfig = {
5
5
+
reactCompiler: true,
5
6
reactStrictMode: false,
6
7
serverExternalPackages: ['jsdom', '@mozilla/readability', 'dompurify'],
7
8
// Configure `pageExtensions` to include markdown and MDX files
+2
-2
src/webapp/package.json
View file
Reviewed
···
46
46
"@types/mdx": "^2.0.13",
47
47
"@vercel/analytics": "^1.5.0",
48
48
"@vidstack/react": "^1.12.13",
49
49
+
"babel-plugin-react-compiler": "^1.0.0",
49
50
"capnweb": "^0.6.1",
50
51
"date-fns": "^4.1.0",
51
52
"dayjs": "^1.11.13",
···
59
60
"react-force-graph": "^1.48.2",
60
61
"react-force-graph-2d": "^1.29.1",
61
62
"react-icons": "^5.5.0",
62
62
-
"shiki": "^3.20.0",
63
63
-
"web-haptics": "^0.0.6"
63
63
+
"shiki": "^3.20.0"
64
64
},
65
65
"devDependencies": {
66
66
"@chromatic-com/storybook": "^5.1.2",