A social knowledge tool for researchers built on ATProto

Compare changes

Choose any two refs to compare.

Changed files
+484 -28
.vscode
src
modules
cards
application
useCases
infrastructure
repositories
query-services
tests
webapp
app
(dashboard)
bookmarklet
components
navigation
guestNavbar
features
cards
components
cardToBeAddedPreview
urlCard
lib
collections
feeds
components
feedActivityStatus
home
notes
components
noteCard
noteCardModal
profile
components
profileHeader
profileHoverCard
profileMenu
semble
containers
sembleAside
-1
.gitignore
··· 9 9 build 10 10 *storybook.log 11 11 storybook-static 12 - .vscode/settings.json
+3
.vscode/settings.json
··· 1 + { 2 + "jest.runMode": "on-demand" 3 + }
+1 -1
src/modules/cards/application/useCases/queries/GetLibrariesForUrlUseCase.ts
··· 63 63 // Set defaults 64 64 const page = query.page || 1; 65 65 const limit = Math.min(query.limit || 20, 100); // Cap at 100 66 - const sortBy = query.sortBy || CardSortField.UPDATED_AT; 66 + const sortBy = query.sortBy || CardSortField.CREATED_AT; 67 67 const sortOrder = query.sortOrder || SortOrder.DESC; 68 68 69 69 try {
+5 -1
src/modules/cards/infrastructure/repositories/query-services/UrlCardQueryService.ts
··· 359 359 options: CardQueryOptions, 360 360 ): Promise<PaginatedQueryResult<LibraryForUrlDTO>> { 361 361 try { 362 - const { page, limit } = options; 362 + const { page, limit, sortBy, sortOrder } = options; 363 363 const offset = (page - 1) * limit; 364 + 365 + // Build the sort order 366 + const orderDirection = sortOrder === SortOrder.ASC ? asc : desc; 364 367 365 368 // Get all URL cards with this URL and their library memberships 366 369 const librariesQuery = this.db ··· 376 379 .from(libraryMemberships) 377 380 .innerJoin(cards, eq(libraryMemberships.cardId, cards.id)) 378 381 .where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL))) 382 + .orderBy(orderDirection(this.getSortColumn(sortBy))) 379 383 .limit(limit) 380 384 .offset(offset); 381 385
+1 -1
src/modules/cards/tests/application/GetLibrariesForUrlUseCase.test.ts
··· 319 319 expect(result.isOk()).toBe(true); 320 320 const response = result.unwrap(); 321 321 322 - expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT); 322 + expect(response.sorting.sortBy).toBe(CardSortField.CREATED_AT); 323 323 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 324 324 }); 325 325
+276
src/modules/cards/tests/infrastructure/DrizzleCardQueryRepository.getLibrariesForUrl.integration.test.ts
··· 15 15 import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 16 16 import { createTestSchema } from '../test-utils/createTestSchema'; 17 17 import { CardTypeEnum } from '../../domain/value-objects/CardType'; 18 + import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; 18 19 19 20 describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => { 20 21 let container: StartedPostgreSqlContainer; ··· 281 282 // Should return empty since card is not in any library 282 283 expect(result.items).toHaveLength(0); 283 284 expect(result.totalCount).toBe(0); 285 + }); 286 + }); 287 + 288 + describe('sorting', () => { 289 + it('should sort by createdAt in descending order by default', async () => { 290 + const testUrl = 'https://example.com/sort-test'; 291 + const url = URL.create(testUrl).unwrap(); 292 + 293 + // Create cards with different creation times 294 + const card1 = new CardBuilder() 295 + .withCuratorId(curator1.value) 296 + .withType(CardTypeEnum.URL) 297 + .withUrl(url) 298 + .buildOrThrow(); 299 + 300 + await new Promise((resolve) => setTimeout(resolve, 1000)); 301 + const card2 = new CardBuilder() 302 + .withCuratorId(curator2.value) 303 + .withType(CardTypeEnum.URL) 304 + .withUrl(url) 305 + .buildOrThrow(); 306 + 307 + await new Promise((resolve) => setTimeout(resolve, 1000)); 308 + const card3 = new CardBuilder() 309 + .withCuratorId(curator3.value) 310 + .withType(CardTypeEnum.URL) 311 + .withUrl(url) 312 + .buildOrThrow(); 313 + 314 + card1.addToLibrary(curator1); 315 + card2.addToLibrary(curator2); 316 + card3.addToLibrary(curator3); 317 + 318 + // Save cards with slight delays to ensure different timestamps 319 + await cardRepository.save(card1); 320 + await new Promise((resolve) => setTimeout(resolve, 10)); 321 + await cardRepository.save(card2); 322 + await new Promise((resolve) => setTimeout(resolve, 10)); 323 + await cardRepository.save(card3); 324 + 325 + const result = await queryRepository.getLibrariesForUrl(testUrl, { 326 + page: 1, 327 + limit: 10, 328 + sortBy: CardSortField.CREATED_AT, 329 + sortOrder: SortOrder.DESC, 330 + }); 331 + 332 + expect(result.items).toHaveLength(3); 333 + 334 + // Should be sorted by creation time, newest first 335 + const cardIds = result.items.map((lib) => lib.card.id); 336 + expect(cardIds[0]).toBe(card3.cardId.getStringValue()); // Most recent 337 + expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Middle 338 + expect(cardIds[2]).toBe(card1.cardId.getStringValue()); // Oldest 339 + }); 340 + 341 + it('should sort by createdAt in ascending order when specified', async () => { 342 + const testUrl = 'https://example.com/sort-asc-test'; 343 + const url = URL.create(testUrl).unwrap(); 344 + 345 + // Create cards with different creation times 346 + const card1 = new CardBuilder() 347 + .withCuratorId(curator1.value) 348 + .withType(CardTypeEnum.URL) 349 + .withUrl(url) 350 + .buildOrThrow(); 351 + 352 + const card2 = new CardBuilder() 353 + .withCuratorId(curator2.value) 354 + .withType(CardTypeEnum.URL) 355 + .withUrl(url) 356 + .buildOrThrow(); 357 + 358 + card1.addToLibrary(curator1); 359 + card2.addToLibrary(curator2); 360 + 361 + // Save cards with slight delay to ensure different timestamps 362 + await cardRepository.save(card1); 363 + await new Promise((resolve) => setTimeout(resolve, 10)); 364 + await cardRepository.save(card2); 365 + 366 + const result = await queryRepository.getLibrariesForUrl(testUrl, { 367 + page: 1, 368 + limit: 10, 369 + sortBy: CardSortField.CREATED_AT, 370 + sortOrder: SortOrder.ASC, 371 + }); 372 + 373 + expect(result.items).toHaveLength(2); 374 + 375 + // Should be sorted by creation time, oldest first 376 + const cardIds = result.items.map((lib) => lib.card.id); 377 + expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Oldest 378 + expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Newest 379 + }); 380 + 381 + it('should sort by updatedAt in descending order', async () => { 382 + const testUrl = 'https://example.com/sort-updated-test'; 383 + const url = URL.create(testUrl).unwrap(); 384 + 385 + // Create cards 386 + const card1 = new CardBuilder() 387 + .withCuratorId(curator1.value) 388 + .withType(CardTypeEnum.URL) 389 + .withUrl(url) 390 + .buildOrThrow(); 391 + 392 + const card2 = new CardBuilder() 393 + .withCuratorId(curator2.value) 394 + .withType(CardTypeEnum.URL) 395 + .withUrl(url) 396 + .buildOrThrow(); 397 + 398 + card1.addToLibrary(curator1); 399 + card2.addToLibrary(curator2); 400 + 401 + // Save cards 402 + await cardRepository.save(card1); 403 + await cardRepository.save(card2); 404 + 405 + // Update card1 to have a more recent updatedAt 406 + await new Promise((resolve) => setTimeout(resolve, 1000)); 407 + card1.markAsPublished( 408 + PublishedRecordId.create({ 409 + uri: 'at://did:plc:publishedrecord1', 410 + cid: 'bafyreicpublishedrecord1', 411 + }), 412 + ); 413 + await cardRepository.save(card1); // This should update the updatedAt timestamp 414 + 415 + const result = await queryRepository.getLibrariesForUrl(testUrl, { 416 + page: 1, 417 + limit: 10, 418 + sortBy: CardSortField.UPDATED_AT, 419 + sortOrder: SortOrder.DESC, 420 + }); 421 + 422 + expect(result.items).toHaveLength(2); 423 + 424 + // card1 should be first since it was updated more recently 425 + const cardIds = result.items.map((lib) => lib.card.id); 426 + expect(cardIds[0]).toBe(card1.cardId.getStringValue()); // Most recently updated 427 + expect(cardIds[1]).toBe(card2.cardId.getStringValue()); // Less recently updated 428 + }); 429 + 430 + it('should sort by libraryCount in descending order', async () => { 431 + const testUrl = 'https://example.com/sort-library-count-test'; 432 + const url = URL.create(testUrl).unwrap(); 433 + 434 + // Create cards 435 + const card1 = new CardBuilder() 436 + .withCuratorId(curator1.value) 437 + .withType(CardTypeEnum.URL) 438 + .withUrl(url) 439 + .buildOrThrow(); 440 + 441 + const card2 = new CardBuilder() 442 + .withCuratorId(curator2.value) 443 + .withType(CardTypeEnum.URL) 444 + .withUrl(url) 445 + .buildOrThrow(); 446 + 447 + const card3 = new CardBuilder() 448 + .withCuratorId(curator3.value) 449 + .withType(CardTypeEnum.URL) 450 + .withUrl(url) 451 + .buildOrThrow(); 452 + 453 + // Add cards to libraries with different counts 454 + card1.addToLibrary(curator1); 455 + 456 + card2.addToLibrary(curator2); 457 + card2.addToLibrary(curator1); // card2 has 2 library memberships 458 + 459 + card3.addToLibrary(curator3); 460 + card3.addToLibrary(curator1); // card3 has 3 library memberships 461 + card3.addToLibrary(curator2); 462 + 463 + await cardRepository.save(card1); 464 + await cardRepository.save(card2); 465 + await cardRepository.save(card3); 466 + 467 + const result = await queryRepository.getLibrariesForUrl(testUrl, { 468 + page: 1, 469 + limit: 10, 470 + sortBy: CardSortField.LIBRARY_COUNT, 471 + sortOrder: SortOrder.DESC, 472 + }); 473 + 474 + // Should return all library memberships, but sorted by the card's library count 475 + expect(result.items.length).toBeGreaterThan(0); 476 + 477 + // Group by card ID to check sorting 478 + const cardGroups = new Map<string, any[]>(); 479 + result.items.forEach((item) => { 480 + const cardId = item.card.id; 481 + if (!cardGroups.has(cardId)) { 482 + cardGroups.set(cardId, []); 483 + } 484 + cardGroups.get(cardId)!.push(item); 485 + }); 486 + 487 + // Get the first occurrence of each card to check library count ordering 488 + const uniqueCards = Array.from(cardGroups.entries()).map( 489 + ([cardId, items]) => ({ 490 + cardId, 491 + libraryCount: items[0]!.card.libraryCount, 492 + }), 493 + ); 494 + 495 + // Should be sorted by library count descending 496 + for (let i = 0; i < uniqueCards.length - 1; i++) { 497 + expect(uniqueCards[i]!.libraryCount).toBeGreaterThanOrEqual( 498 + uniqueCards[i + 1]!.libraryCount, 499 + ); 500 + } 501 + }); 502 + 503 + it('should sort by libraryCount in ascending order when specified', async () => { 504 + const testUrl = 'https://example.com/sort-library-count-asc-test'; 505 + const url = URL.create(testUrl).unwrap(); 506 + 507 + // Create cards with different library counts 508 + const card1 = new CardBuilder() 509 + .withCuratorId(curator1.value) 510 + .withType(CardTypeEnum.URL) 511 + .withUrl(url) 512 + .buildOrThrow(); 513 + 514 + const card2 = new CardBuilder() 515 + .withCuratorId(curator2.value) 516 + .withType(CardTypeEnum.URL) 517 + .withUrl(url) 518 + .buildOrThrow(); 519 + 520 + // card1 has 1 library membership, card2 has 2 521 + card1.addToLibrary(curator1); 522 + card2.addToLibrary(curator2); 523 + card2.addToLibrary(curator1); 524 + 525 + await cardRepository.save(card1); 526 + await cardRepository.save(card2); 527 + 528 + const result = await queryRepository.getLibrariesForUrl(testUrl, { 529 + page: 1, 530 + limit: 10, 531 + sortBy: CardSortField.LIBRARY_COUNT, 532 + sortOrder: SortOrder.ASC, 533 + }); 534 + 535 + expect(result.items.length).toBeGreaterThan(0); 536 + 537 + // Group by card ID and check ascending order 538 + const cardGroups = new Map<string, any[]>(); 539 + result.items.forEach((item) => { 540 + const cardId = item.card.id; 541 + if (!cardGroups.has(cardId)) { 542 + cardGroups.set(cardId, []); 543 + } 544 + cardGroups.get(cardId)!.push(item); 545 + }); 546 + 547 + const uniqueCards = Array.from(cardGroups.entries()).map( 548 + ([cardId, items]) => ({ 549 + cardId, 550 + libraryCount: items[0]!.card.libraryCount, 551 + }), 552 + ); 553 + 554 + // Should be sorted by library count ascending 555 + for (let i = 0; i < uniqueCards.length - 1; i++) { 556 + expect(uniqueCards[i]!.libraryCount).toBeLessThanOrEqual( 557 + uniqueCards[i + 1]!.libraryCount, 558 + ); 559 + } 284 560 }); 285 561 }); 286 562
+1 -1
src/webapp/app/(dashboard)/error.tsx
··· 88 88 component={Link} 89 89 href="/login" 90 90 size="lg" 91 - color="dark" 91 + color="var(--mantine-color-dark-filled)" 92 92 rightSection={<BiRightArrowAlt size={22} />} 93 93 > 94 94 Log in
+15
src/webapp/app/bookmarklet/layout.tsx
··· 1 + import type { Metadata } from 'next'; 2 + 3 + export const metadata: Metadata = { 4 + title: 'Semble bookmarklet', 5 + description: 6 + 'Learn how to add our bookmarklet to your browser to quickly open any webpage in Semble.', 7 + }; 8 + 9 + interface Props { 10 + children: React.ReactNode; 11 + } 12 + 13 + export default function Layout(props: Props) { 14 + return props.children; 15 + }
+143
src/webapp/app/bookmarklet/page.tsx
··· 1 + 'use client'; 2 + 3 + import { 4 + Container, 5 + Title, 6 + Text, 7 + Stack, 8 + Button, 9 + Code, 10 + Alert, 11 + Box, 12 + Badge, 13 + Image, 14 + Group, 15 + Anchor, 16 + CopyButton, 17 + } from '@mantine/core'; 18 + import SembleLogo from '@/assets/semble-logo.svg'; 19 + import Link from 'next/link'; 20 + 21 + export default function BookmarkletPage() { 22 + const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://127.0.0.1:4000'; 23 + 24 + const bookmarkletCode = `javascript:(function(){ 25 + const currentUrl = window.location.href; 26 + const sembleUrl = '${appUrl}/url?id=' + currentUrl; 27 + window.open(sembleUrl, '_blank'); 28 + })();`; 29 + 30 + // Create the bookmarklet link using dangerouslySetInnerHTML to bypass React's security check 31 + const createBookmarkletLink = () => { 32 + return { 33 + __html: `<a href="${bookmarkletCode}" style="text-decoration: none; padding: 8px 16px; background-color: var(--mantine-color-tangerine-6); color: white; border-radius: 100px; display: inline-flex; align-items: center; gap: 8px; font-weight: 600;"><svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor"><path d="M17 3H7c-1.1 0-1.99.9-1.99 2L5 21l7-3 7 3V5c0-1.1-.9-2-2-2z"/></svg>Open in Semble</a>`, 34 + }; 35 + }; 36 + 37 + return ( 38 + <Container size="sm" p="md"> 39 + <Stack gap="xl"> 40 + <Stack gap="xs" align="center"> 41 + <Stack align="center" gap={'xs'}> 42 + <Anchor component={Link} href={'/'}> 43 + <Image 44 + src={SembleLogo.src} 45 + alt="Semble logo" 46 + w={48} 47 + h={64.5} 48 + mx={'auto'} 49 + /> 50 + <Badge size="sm">Alpha</Badge> 51 + </Anchor> 52 + </Stack> 53 + <Stack gap={'xs'} align="center"> 54 + <Title order={1}>Semble Bookmarklet</Title> 55 + <Title 56 + order={2} 57 + size="xl" 58 + c="dimmed" 59 + fw={600} 60 + maw={500} 61 + ta={'center'} 62 + > 63 + Add this bookmarklet to your browser to quickly open any webpage 64 + in Semble. 65 + </Title> 66 + </Stack> 67 + </Stack> 68 + 69 + <Alert title="How to install" color="grape"> 70 + <Stack gap="sm"> 71 + <Group gap={'xs'}> 72 + <Badge size="md" color="grape" circle> 73 + 1 74 + </Badge> 75 + <Text fw={500} c="grape"> 76 + Copy the bookmarklet code below or drag the button to your 77 + bookmarks bar 78 + </Text> 79 + </Group> 80 + <Group gap={'xs'}> 81 + <Badge size="md" color="grape" circle> 82 + 2 83 + </Badge> 84 + 85 + <Text fw={500} c={'grape'}> 86 + { 87 + "When you're on any webpage, click the bookmarklet to open it in Semble" 88 + } 89 + </Text> 90 + </Group> 91 + </Stack> 92 + </Alert> 93 + 94 + <Stack gap="md"> 95 + <Stack gap={'xs'}> 96 + <Title order={3}>Method 1: Drag to Bookmarks Bar</Title> 97 + <Text c="dimmed" fw={500}> 98 + {"Drag this button directly to your browser's bookmarks bar:"} 99 + </Text> 100 + </Stack> 101 + <Group> 102 + <Box dangerouslySetInnerHTML={createBookmarkletLink()} /> 103 + </Group> 104 + </Stack> 105 + 106 + <Stack gap="md"> 107 + <Stack gap={'xs'}> 108 + <Title order={3}>Method 2: Copy Code</Title> 109 + <Text c="dimmed" fw={500}> 110 + Copy this code and create a new bookmark with it as the URL: 111 + </Text> 112 + </Stack> 113 + <Box pos="relative"> 114 + <Code 115 + block 116 + p="md" 117 + style={{ 118 + wordBreak: 'break-all', 119 + whiteSpace: 'pre-wrap', 120 + fontSize: '12px', 121 + }} 122 + > 123 + {bookmarkletCode} 124 + </Code> 125 + <CopyButton value={bookmarkletCode}> 126 + {({ copied, copy }) => ( 127 + <Button 128 + color="dark" 129 + pos={'absolute'} 130 + top={12} 131 + right={12} 132 + onClick={copy} 133 + > 134 + {copied ? 'Copied!' : 'Copy'} 135 + </Button> 136 + )} 137 + </CopyButton> 138 + </Box> 139 + </Stack> 140 + </Stack> 141 + </Container> 142 + ); 143 + }
+1 -1
src/webapp/components/navigation/guestNavbar/GuestNavbar.tsx
··· 46 46 <Button 47 47 component={Link} 48 48 href="/login" 49 - color="dark" 49 + color="var(--mantine-color-dark-filled)" 50 50 rightSection={<BiRightArrowAlt size={22} />} 51 51 > 52 52 Log in
+1 -1
src/webapp/features/cards/components/cardToBeAddedPreview/CardToBeAddedPreview.tsx
··· 127 127 </Anchor> 128 128 </Tooltip> 129 129 {props.title && ( 130 - <Text fw={500} lineClamp={1}> 130 + <Text fw={500} lineClamp={1} c="var(--mantine-color-bright)"> 131 131 {props.title} 132 132 </Text> 133 133 )}
+1 -1
src/webapp/features/cards/components/urlCard/UrlCard.tsx
··· 67 67 </Anchor> 68 68 </Tooltip> 69 69 {props.cardContent.title && ( 70 - <Text fw={500} lineClamp={2}> 70 + <Text fw={500} lineClamp={2} c={'var(--mantine-color-bright)'}> 71 71 {props.cardContent.title} 72 72 </Text> 73 73 )}
+3 -2
src/webapp/features/cards/lib/cardKeys.ts
··· 2 2 all: () => ['cards'] as const, 3 3 card: (id: string) => [...cardKeys.all(), id] as const, 4 4 byUrl: (url: string) => [...cardKeys.all(), url] as const, 5 - mine: () => [...cardKeys.all(), 'mine'] as const, 5 + mine: (limit?: number) => [...cardKeys.all(), 'mine', limit] as const, 6 6 search: (query: string) => [...cardKeys.all(), 'search', query], 7 7 bySembleUrl: (url: string) => [...cardKeys.all(), url], 8 8 libraries: (id: string) => [...cardKeys.all(), 'libraries', id], 9 - infinite: (didOrHandle?: string) => [ 9 + infinite: (didOrHandle?: string, limit?: number) => [ 10 10 ...cardKeys.all(), 11 11 'infinite', 12 12 didOrHandle, 13 + limit, 13 14 ], 14 15 };
+1 -1
src/webapp/features/cards/lib/queries/useCards.tsx
··· 11 11 const limit = props?.limit ?? 16; 12 12 13 13 const cards = useSuspenseInfiniteQuery({ 14 - queryKey: cardKeys.infinite(props.didOrHandle), 14 + queryKey: cardKeys.infinite(props.didOrHandle, props.limit), 15 15 initialPageParam: 1, 16 16 queryFn: ({ pageParam = 1 }) => { 17 17 return getUrlCards(props.didOrHandle, {
+1 -1
src/webapp/features/cards/lib/queries/useMyCards.tsx
··· 10 10 const limit = props?.limit ?? 16; 11 11 12 12 const myCards = useSuspenseInfiniteQuery({ 13 - queryKey: cardKeys.mine(), 13 + queryKey: cardKeys.mine(props?.limit), 14 14 initialPageParam: 1, 15 15 queryFn: ({ pageParam = 1 }) => { 16 16 return getMyUrlCards({ page: pageParam, limit });
+2 -2
src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
··· 34 34 > 35 35 <Stack justify="space-between" h={'100%'}> 36 36 <Stack gap={0}> 37 - <Text fw={500} lineClamp={1}> 37 + <Text fw={500} lineClamp={1} c={'var(--mantine-color-bright)'}> 38 38 {collection.name} 39 39 </Text> 40 40 {collection.description && ( ··· 58 58 size={'sm'} 59 59 /> 60 60 61 - <Text fw={500} span> 61 + <Text fw={500} c={'var(--mantine-color-bright)'} span> 62 62 {collection.author.name} 63 63 </Text> 64 64 </Group>
+7 -2
src/webapp/features/collections/lib/collectionKeys.ts
··· 1 1 export const collectionKeys = { 2 2 all: () => ['collections'] as const, 3 3 collection: (id: string) => [...collectionKeys.all(), id] as const, 4 - mine: () => [...collectionKeys.all(), 'mine'] as const, 4 + mine: (limit?: number) => [...collectionKeys.all(), 'mine', limit] as const, 5 5 search: (query: string) => [...collectionKeys.all(), 'search', query], 6 6 bySembleUrl: (url: string) => [...collectionKeys.all(), url], 7 - infinite: (id?: string) => [...collectionKeys.all(), 'infinite', id], 7 + infinite: (id?: string, limit?: number) => [ 8 + ...collectionKeys.all(), 9 + 'infinite', 10 + id, 11 + limit, 12 + ], 8 13 };
+1 -1
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
··· 14 14 // Do UI related things like redirects or showing toast notifications in mutate callbacks. If the user navigated away from the current screen before the mutation finished, those will purposefully not fire 15 15 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 16 16 onSuccess: () => { 17 - queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() }); 17 + queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 18 18 queryClient.refetchQueries({ queryKey: collectionKeys.mine() }); 19 19 }, 20 20 });
+1 -1
src/webapp/features/collections/lib/queries/useCollection.tsx
··· 12 12 const limit = props.limit ?? 20; 13 13 14 14 return useSuspenseInfiniteQuery({ 15 - queryKey: collectionKeys.infinite(props.rkey), 15 + queryKey: collectionKeys.infinite(props.rkey, props.limit), 16 16 initialPageParam: 1, 17 17 queryFn: ({ pageParam }) => 18 18 getCollectionPageByAtUri({
+1 -1
src/webapp/features/collections/lib/queries/useCollections.tsx
··· 11 11 const limit = props?.limit ?? 15; 12 12 13 13 return useSuspenseInfiniteQuery({ 14 - queryKey: collectionKeys.infinite(props.didOrHandle), 14 + queryKey: collectionKeys.infinite(props.didOrHandle, props.limit), 15 15 initialPageParam: 1, 16 16 queryFn: ({ pageParam }) => 17 17 getCollections(props.didOrHandle, {
+1 -1
src/webapp/features/collections/lib/queries/useMyCollections.tsx
··· 10 10 const limit = props?.limit ?? 15; 11 11 12 12 return useSuspenseInfiniteQuery({ 13 - queryKey: collectionKeys.mine(), 13 + queryKey: collectionKeys.mine(props?.limit), 14 14 initialPageParam: 1, 15 15 queryFn: ({ pageParam }) => getMyCollections({ limit, page: pageParam }), 16 16 getNextPageParam: (lastPage) => {
+6 -1
src/webapp/features/feeds/components/feedActivityStatus/FeedActivityStatus.tsx
··· 41 41 42 42 return ( 43 43 <Text fw={500}> 44 - <Text component={Link} href={`/profile/${props.user.handle}`} fw={600}> 44 + <Text 45 + component={Link} 46 + href={`/profile/${props.user.handle}`} 47 + fw={600} 48 + c={'var(--mantine-color-bright)'} 49 + > 45 50 {sanitizeText(props.user.name)} 46 51 </Text>{' '} 47 52 {collections.length === 0 ? (
+1 -1
src/webapp/features/home/containers/homeContainer/HomeContainer.tsx
··· 26 26 import { useNavbarContext } from '@/providers/navbar'; 27 27 28 28 export default function HomeContainer() { 29 - const { data: collectionsData } = useMyCollections({ limit: 8 }); 29 + const { data: collectionsData } = useMyCollections({ limit: 4 }); 30 30 const { data: myCardsData } = useMyCards({ limit: 8 }); 31 31 const { data: profile } = useMyProfile(); 32 32
+1 -1
src/webapp/features/home/containers/homeContainer/Skeleton.HomeContainer.tsx
··· 48 48 </Group> 49 49 50 50 <Grid gutter="md"> 51 - {Array.from({ length: 4 }).map((_, i) => ( 51 + {Array.from({ length: 6 }).map((_, i) => ( 52 52 <GridCol key={i} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}> 53 53 <UrlCardSkeleton /> 54 54 </GridCol>
+1
src/webapp/features/notes/components/noteCard/NoteCard.tsx
··· 34 34 <Text 35 35 component={Link} 36 36 href={`/profile/${props.author.handle}`} 37 + c={'var(--mantine-color-bright)'} 37 38 fw={500} 38 39 span 39 40 >
+1 -1
src/webapp/features/notes/components/noteCardModal/NoteCardModalContent.tsx
··· 174 174 </Anchor> 175 175 </Tooltip> 176 176 {props.cardContent.title && ( 177 - <Text fw={500} lineClamp={1}> 177 + <Text fw={500} lineClamp={1} c="var(--mantine-color-bright)"> 178 178 {props.cardContent.title} 179 179 </Text> 180 180 )}
+1 -1
src/webapp/features/profile/components/profileHeader/ProfileHeader.tsx
··· 31 31 /> 32 32 <Stack gap={'sm'} p={'xs'}> 33 33 <Stack gap={'xl'}> 34 - <Grid gutter={'md'} align={'center'} grow> 34 + <Grid gutter={'md'} grow> 35 35 <GridCol span={'auto'}> 36 36 <Avatar 37 37 src={profile.avatarUrl}
+1 -1
src/webapp/features/profile/components/profileHoverCard/ProfileHoverCard.tsx
··· 39 39 <Button 40 40 component={Link} 41 41 href={`/profile/${profile.handle}/cards`} 42 - color="dark" 42 + color="var(--mantine-color-dark-filled)" 43 43 leftSection={<FaRegNoteSticky />} 44 44 > 45 45 Cards
+1 -1
src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
··· 70 70 <Menu.Target> 71 71 <Button 72 72 variant="subtle" 73 - color={computedColorScheme === 'dark' ? 'gray' : 'dark'} 73 + color={'var(--mantine-color-bright)'} 74 74 fz="md" 75 75 radius="md" 76 76 size="lg"
+5 -1
src/webapp/features/semble/containers/sembleAside/SembleAside.tsx
··· 41 41 alt={`${lib.user.name}'s avatar`} 42 42 /> 43 43 <Stack gap={0}> 44 - <Text fw={600} lineClamp={1}> 44 + <Text 45 + fw={600} 46 + lineClamp={1} 47 + c={'var(--mantine-color-bright)'} 48 + > 45 49 {lib.user.name} 46 50 </Text> 47 51 <Text fw={600} c={'blue'} lineClamp={1}>