This repository has no description
0

Configure Feed

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

Merge branch 'development'

+290 -193
+12 -31
package-lock.json
··· 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 - "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 - "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 - "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 + } 14199 + }, 14200 + "node_modules/babel-plugin-react-compiler": { 14201 + "version": "1.0.0", 14202 + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", 14203 + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", 14204 + "license": "MIT", 14205 + "peer": true, 14206 + "dependencies": { 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 - "node_modules/web-haptics": { 31503 - "version": "0.0.6", 31504 - "resolved": "https://registry.npmjs.org/web-haptics/-/web-haptics-0.0.6.tgz", 31505 - "integrity": "sha512-eCzcf1LDi20+Fr0x9V3OkX92k0gxEQXaHajmhXHitsnk6SxPeshv8TBtBRqxyst8HI1uf2FyFVE7QS3jo1gkrw==", 31506 - "license": "MIT", 31507 - "peerDependencies": { 31508 - "react": ">=18", 31509 - "react-dom": ">=18", 31510 - "svelte": ">=4", 31511 - "vue": ">=3" 31512 - }, 31513 - "peerDependenciesMeta": { 31514 - "react": { 31515 - "optional": true 31516 - }, 31517 - "react-dom": { 31518 - "optional": true 31519 - }, 31520 - "svelte": { 31521 - "optional": true 31522 - }, 31523 - "vue": { 31524 - "optional": true 31525 - } 31526 - } 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 + "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 - "shiki": "^3.20.0", 32232 - "web-haptics": "^0.0.6" 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
··· 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 + 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 + <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
··· 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 + 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 + </Suspense> 19 + <Suspense fallback={null}> 20 + <ShareHeaderButton /> 17 21 </Suspense> 18 22 </Header> 19 23 {props.children}
+1 -1
src/webapp/components/navigation/backButton/BackButton.tsx
··· 14 14 return ( 15 15 <ActionIcon 16 16 onClick={() => router.back()} 17 - variant="light" 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
··· 14 14 Textarea, 15 15 VisuallyHidden, 16 16 } from '@mantine/core'; 17 - import { BsExclamation } from 'react-icons/bs'; 17 + import { BsExclamation, BsTrash2Fill } from 'react-icons/bs'; 18 + import { MdOutlineStickyNote2 } from 'react-icons/md'; 18 19 19 20 interface Props { 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 + 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 + const removeCard = useRemoveCardFromLibrary(); 33 37 34 38 const handleDeleteNote = () => { 35 39 if (!props.noteId) return; ··· 51 55 }); 52 56 }; 53 57 58 + const handleDeleteCard = () => { 59 + if (!props.cardId) return; 60 + 61 + removeCard.mutate(props.cardId, { 62 + onError: () => { 63 + notifications.show({ 64 + message: 'Could not delete card', 65 + color: 'red', 66 + autoClose: 5000, 67 + withCloseButton: true, 68 + position: 'top-center', 69 + icon: <BsExclamation />, 70 + }); 71 + }, 72 + onSettled: () => { 73 + props.onClose(); 74 + }, 75 + }); 76 + }; 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 + 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 + size="xs" 130 156 onClick={() => setShowDeleteWarning(false)} 131 157 > 132 158 Cancel 133 159 </Button> 134 160 </Group> 135 161 </Group> 162 + ) : showDeleteCardWarning ? ( 163 + <Group justify="space-between" gap={'xs'}> 164 + <Text>Delete card?</Text> 165 + <Group gap={'xs'}> 166 + <Button 167 + color="red" 168 + size="xs" 169 + onClick={handleDeleteCard} 170 + loading={removeCard.isPending} 171 + > 172 + Delete 173 + </Button> 174 + <Button 175 + variant="light" 176 + color="gray" 177 + size="xs" 178 + onClick={() => setShowDeleteCardWarning(false)} 179 + > 180 + Cancel 181 + </Button> 182 + </Group> 183 + </Group> 136 184 ) : ( 137 - <Group gap={'xs'}> 138 - <Button 139 - variant="light" 140 - size="xs" 141 - color="gray" 142 - onClick={(e) => { 143 - e.stopPropagation(); 144 - setNoteMode(true); 145 - }} 146 - > 147 - {note ? 'Edit note' : 'Add note'} 148 - </Button> 149 - {props.noteId && ( 185 + <Group gap={'xs'} justify="space-between"> 186 + <Group gap={'xs'}> 187 + <Button 188 + variant="light" 189 + size="xs" 190 + color="gray" 191 + leftSection={<MdOutlineStickyNote2 />} 192 + onClick={(e) => { 193 + e.stopPropagation(); 194 + setNoteMode(true); 195 + }} 196 + > 197 + {note ? 'Edit note' : 'Add note'} 198 + </Button> 199 + {props.noteId && ( 200 + <Button 201 + variant="light" 202 + color="red" 203 + size="xs" 204 + onClick={(e) => { 205 + e.stopPropagation(); 206 + setShowDeleteWarning(true); 207 + }} 208 + > 209 + Delete note 210 + </Button> 211 + )} 212 + </Group> 213 + {props.cardId && ( 150 214 <Button 151 215 variant="light" 152 216 color="red" 153 217 size="xs" 218 + leftSection={<BsTrash2Fill />} 154 219 onClick={(e) => { 155 220 e.stopPropagation(); 156 - setShowDeleteWarning(true); 221 + setShowDeleteCardWarning(true); 157 222 }} 158 223 > 159 - Delete note 224 + Delete card 160 225 </Button> 161 226 )} 162 227 </Group>
+1
src/webapp/features/cards/components/addCardToModal/AddCardToModalContent.tsx
··· 117 117 return ( 118 118 <Stack> 119 119 <AddCardActions 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
··· 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 + 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 + import { Button, Group, Text } from '@mantine/core'; 16 18 import { BsCheck, BsExclamation } from 'react-icons/bs'; 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 + 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 + const notificationId = variables.notificationId; 45 50 notifications.update({ 46 - id: variables.notificationId, 51 + id: notificationId, 47 52 color: 'green', 48 - title: 'Success!', 49 - message: 'Card added', 53 + title: null, 54 + message: ( 55 + <Group 56 + gap="xs" 57 + justify="space-between" 58 + wrap="nowrap" 59 + align="center" 60 + > 61 + <Text size="sm">Card added</Text> 62 + <Button 63 + size="xs" 64 + variant="light" 65 + color="gray" 66 + onClick={() => { 67 + notifications.hide(notificationId); 68 + router.push(`/url?id=${encodeURIComponent(variables.url)}`); 69 + }} 70 + > 71 + View 72 + </Button> 73 + </Group> 74 + ), 50 75 position: 'top-center', 51 76 loading: false, 52 - autoClose: 2000, 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 + 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
··· 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 + 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 + 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
··· 1 + 'use client'; 2 + 3 + import { ActionIcon, CopyButton, Tooltip } from '@mantine/core'; 4 + import { notifications } from '@mantine/notifications'; 5 + import { MdIosShare } from 'react-icons/md'; 6 + 7 + interface Props { 8 + handle: string; 9 + rkey: string; 10 + } 11 + 12 + export default function CollectionShareHeaderButton(props: Props) { 13 + const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/profile/${props.handle}/collections/${props.rkey}`; 14 + 15 + return ( 16 + <CopyButton value={shareLink}> 17 + {({ copied, copy }) => ( 18 + <Tooltip 19 + label={copied ? 'Link copied!' : 'Share'} 20 + withArrow 21 + position="top" 22 + > 23 + <ActionIcon 24 + variant="light" 25 + color="gray" 26 + size={36} 27 + radius={'xl'} 28 + onClick={(e) => { 29 + e.stopPropagation(); 30 + copy(); 31 + 32 + if (copied) return; 33 + notifications.show({ 34 + message: 'Link copied!', 35 + position: 'top-center', 36 + id: `${props.handle}/${props.rkey}`, 37 + }); 38 + }} 39 + > 40 + <MdIosShare /> 41 + </ActionIcon> 42 + </Tooltip> 43 + )} 44 + </CopyButton> 45 + ); 46 + }
+1 -30
src/webapp/features/collections/components/collectionActions/CollectionActions.tsx
··· 1 1 import { Collection, CollectionAccessType } from '@semble/types'; 2 - import { Group, Menu, ActionIcon, CopyButton } from '@mantine/core'; 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 - 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 - 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 - const shareLink = 181 - typeof window !== 'undefined' 182 - ? `${window.location.origin}/profile/${props.collection.author?.handle}/collections/${props.collection.rkey}` 183 - : ''; 184 - 185 178 return ( 186 179 <Fragment> 187 180 <Group gap={'xs'}> 188 181 {isAuthenticated && ( 189 182 <AuthenticatedCollectionActions collection={props.collection} /> 190 183 )} 191 - 192 - <CopyButton value={shareLink}> 193 - {({ copy }) => ( 194 - <ActionIcon 195 - variant="light" 196 - color="gray" 197 - size={'lg'} 198 - radius={'xl'} 199 - onClick={(e) => { 200 - e.stopPropagation(); 201 - copy(); 202 - notifications.show({ 203 - message: 'Link copied!', 204 - position: 'top-center', 205 - id: props.collection.id, 206 - }); 207 - }} 208 - > 209 - <MdIosShare size={18} /> 210 - </ActionIcon> 211 - )} 212 - </CopyButton> 213 184 214 185 {isAuthor && <AuthorCollectionMenu collection={props.collection} />} 215 186 </Group>
-1
src/webapp/features/collections/components/collectionActions/Skeleton.CollectionActions.tsx
··· 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 - <Skeleton w={34} h={34} radius={'xl'} /> 10 9 </Group> 11 10 ); 12 11 }
+2
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
··· 2 2 import { useMutation, useQueryClient } from '@tanstack/react-query'; 3 3 import { createCollection } from '../dal'; 4 4 import { collectionKeys } from '../collectionKeys'; 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 + 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
··· 1 1 import { useMutation, useQueryClient } from '@tanstack/react-query'; 2 2 import { deleteCollection } from '../dal'; 3 3 import { collectionKeys } from '../collectionKeys'; 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 + queryClient.invalidateQueries({ queryKey: profileKeys.all() }); 15 17 }, 16 18 }); 17 19
-1
src/webapp/features/notes/components/noteCardInline/NoteCardInline.tsx
··· 121 121 <Textarea 122 122 id="note-inline" 123 123 placeholder="Add a note about this card" 124 - variant="filled" 125 124 size="md" 126 125 autosize 127 126 minRows={3}
+3 -3
src/webapp/features/profile/components/profileTabs/ProfileTabs.tsx
··· 32 32 <TabItem 33 33 value="cards" 34 34 href={`${basePath}/cards`} 35 - count={profile.urlCardCount} 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 - count={profile.collectionCount} 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 - count={profile.connectionCount} 49 + count={profile?.connectionCount} 50 50 > 51 51 Connections 52 52 </TabItem>
+31 -7
src/webapp/features/profile/lib/queries/useProfile.tsx
··· 1 - import { useSuspenseQuery } from '@tanstack/react-query'; 1 + import { 2 + useSuspenseQuery, 3 + useQuery, 4 + UseQueryResult, 5 + UseSuspenseQueryResult, 6 + } from '@tanstack/react-query'; 2 7 import { getProfile } from '../dal'; 3 8 import { profileKeys } from '../profileKeys'; 4 9 5 - interface Props { 10 + type ProfileData = Awaited<ReturnType<typeof getProfile>>; 11 + 12 + interface PropsWithStats { 6 13 didOrHandle: string; 7 - includeStats?: boolean; 14 + includeStats: true; 8 15 } 9 16 10 - export default function useProfile(props: Props) { 11 - const profile = useSuspenseQuery({ 17 + interface PropsWithoutStats { 18 + didOrHandle: string; 19 + includeStats?: false; 20 + } 21 + 22 + export default function useProfile( 23 + props: PropsWithStats, 24 + ): UseQueryResult<ProfileData>; 25 + export default function useProfile( 26 + props: PropsWithoutStats, 27 + ): UseSuspenseQueryResult<ProfileData>; 28 + export default function useProfile(props: PropsWithStats | PropsWithoutStats) { 29 + if (props.includeStats) { 30 + // Non-suspense: stats are progressive — tabs render immediately, counts fill in async 31 + return useQuery({ 32 + queryKey: profileKeys.profile(props.didOrHandle, true), 33 + queryFn: () => getProfile(props.didOrHandle, true), 34 + }); 35 + } 36 + 37 + return useSuspenseQuery({ 12 38 queryKey: profileKeys.profile(props.didOrHandle, props.includeStats), 13 39 queryFn: () => getProfile(props.didOrHandle, props.includeStats), 14 40 }); 15 - 16 - return profile; 17 41 }
+46
src/webapp/features/semble/components/ShareHeaderButton/ShareHeaderButton.tsx
··· 1 + 'use client'; 2 + 3 + import { ActionIcon, CopyButton, Tooltip } from '@mantine/core'; 4 + import { notifications } from '@mantine/notifications'; 5 + import { useSearchParams } from 'next/navigation'; 6 + import { MdIosShare } from 'react-icons/md'; 7 + 8 + export default function ShareHeaderButton() { 9 + const searchParams = useSearchParams(); 10 + const rawId = searchParams.get('id'); 11 + 12 + if (!rawId) return null; 13 + 14 + const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/url?id=${rawId}`; 15 + 16 + return ( 17 + <CopyButton value={shareLink}> 18 + {({ copied, copy }) => ( 19 + <Tooltip 20 + label={copied ? 'Link copied!' : 'Share'} 21 + withArrow 22 + position="top" 23 + > 24 + <ActionIcon 25 + variant="light" 26 + color="gray" 27 + size={36} 28 + radius={'xl'} 29 + onClick={() => { 30 + copy(); 31 + 32 + if (copied) return; 33 + notifications.show({ 34 + message: 'Link copied!', 35 + position: 'top-center', 36 + id: copied.toString(), 37 + }); 38 + }} 39 + > 40 + <MdIosShare /> 41 + </ActionIcon> 42 + </Tooltip> 43 + )} 44 + </CopyButton> 45 + ); 46 + }
+20
src/webapp/features/semble/components/sembleActions/GuestSembleActions.tsx
··· 1 + import { Group } from '@mantine/core'; 2 + import { TbPlugConnected } from 'react-icons/tb'; 3 + import { LinkButton } from '@/components/link/MantineLink'; 4 + 5 + export default function GuestSembleActions() { 6 + return ( 7 + <Group gap={'xs'}> 8 + <LinkButton 9 + href={'/login'} 10 + variant="light" 11 + color="green" 12 + radius={'xl'} 13 + leftSection={<TbPlugConnected size={18} />} 14 + > 15 + Log in to connect 16 + </LinkButton> 17 + <LinkButton href={'/login'}>Log in to add</LinkButton> 18 + </Group> 19 + ); 20 + }
-61
src/webapp/features/semble/components/sembleActions/GusetSembleActions.tsx
··· 1 - 'use client'; 2 - 3 - import { ActionIcon, CopyButton, Group, Tooltip } from '@mantine/core'; 4 - import { MdIosShare } from 'react-icons/md'; 5 - import { notifications } from '@mantine/notifications'; 6 - import { TbPlugConnected } from 'react-icons/tb'; 7 - import { LinkButton } from '@/components/link/MantineLink'; 8 - 9 - interface Props { 10 - url: string; 11 - } 12 - 13 - export default function GuestSembleActions(props: Props) { 14 - const shareLink = 15 - typeof window !== 'undefined' 16 - ? `${window.location.origin}/url?id=${props.url}` 17 - : ''; 18 - 19 - return ( 20 - <Group gap={'xs'}> 21 - <CopyButton value={shareLink}> 22 - {({ copied, copy }) => ( 23 - <Tooltip 24 - label={copied ? 'Link copied!' : 'Share'} 25 - withArrow 26 - position="top" 27 - > 28 - <ActionIcon 29 - variant="light" 30 - color="gray" 31 - size={36} 32 - radius={'xl'} 33 - onClick={() => { 34 - copy(); 35 - 36 - if (copied) return; 37 - notifications.show({ 38 - message: 'Link copied!', 39 - position: 'top-center', 40 - id: copied.toString(), 41 - }); 42 - }} 43 - > 44 - <MdIosShare size={22} /> 45 - </ActionIcon> 46 - </Tooltip> 47 - )} 48 - </CopyButton> 49 - <LinkButton 50 - href={'/login'} 51 - variant="light" 52 - color="green" 53 - radius={'xl'} 54 - leftSection={<TbPlugConnected size={18} />} 55 - > 56 - Log in to connect 57 - </LinkButton> 58 - <LinkButton href={'/login'}>Log in to add</LinkButton> 59 - </Group> 60 - ); 61 - }
+1 -33
src/webapp/features/semble/components/sembleActions/SembleActions.tsx
··· 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 - import { ActionIcon, Button, CopyButton, Group, Tooltip } from '@mantine/core'; 7 - import { notifications } from '@mantine/notifications'; 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 - 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 - const shareLink = `${process.env.NEXT_PUBLIC_APP_URL}/url?id=${props.url}`; 40 - 41 37 if (cardStatus.error) { 42 38 return null; 43 39 } ··· 45 41 return ( 46 42 <Fragment> 47 43 <Group gap={'xs'}> 48 - <CopyButton value={shareLink}> 49 - {({ copied, copy }) => ( 50 - <Tooltip 51 - label={copied ? 'Link copied!' : 'Share'} 52 - withArrow 53 - position="top" 54 - > 55 - <ActionIcon 56 - variant="light" 57 - color="gray" 58 - size={36} 59 - radius={'xl'} 60 - onClick={() => { 61 - copy(); 62 - 63 - if (copied) return; 64 - notifications.show({ 65 - message: 'Link copied!', 66 - position: 'top-center', 67 - id: copied.toString(), 68 - }); 69 - }} 70 - > 71 - <MdIosShare /> 72 - </ActionIcon> 73 - </Tooltip> 74 - )} 75 - </CopyButton> 76 44 <Button 77 45 variant="light" 78 46 color="green"
+2 -2
src/webapp/features/semble/containers/sembleActionsContainer/SembleActionsContainer.tsx
··· 1 1 import { verifySessionOnServer } from '@/lib/auth/dal.server'; 2 - import GuestSembleActions from '../../components/sembleActions/GusetSembleActions'; 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 - return <GuestSembleActions url={props.url} />; 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
··· 4 4 return ( 5 5 <Stack gap="sm" align="center"> 6 6 <Group gap="xs"> 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
··· 2 2 3 3 /** @type {import('next').NextConfig} */ 4 4 const nextConfig = { 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
··· 46 46 "@types/mdx": "^2.0.13", 47 47 "@vercel/analytics": "^1.5.0", 48 48 "@vidstack/react": "^1.12.13", 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 - "shiki": "^3.20.0", 63 - "web-haptics": "^0.0.6" 63 + "shiki": "^3.20.0" 64 64 }, 65 65 "devDependencies": { 66 66 "@chromatic-com/storybook": "^5.1.2",