A social knowledge tool for researchers built on ATProto

Compare changes

Choose any two refs to compare.

Changed files
+480 -25
.vscode
src
modules
cards
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 build 10 *storybook.log 11 storybook-static 12 - .vscode/settings.json
··· 9 build 10 *storybook.log 11 storybook-static
+3
.vscode/settings.json
···
··· 1 + { 2 + "jest.runMode": "on-demand" 3 + }
+5 -1
src/modules/cards/infrastructure/repositories/query-services/UrlCardQueryService.ts
··· 359 options: CardQueryOptions, 360 ): Promise<PaginatedQueryResult<LibraryForUrlDTO>> { 361 try { 362 - const { page, limit } = options; 363 const offset = (page - 1) * limit; 364 365 // Get all URL cards with this URL and their library memberships 366 const librariesQuery = this.db ··· 376 .from(libraryMemberships) 377 .innerJoin(cards, eq(libraryMemberships.cardId, cards.id)) 378 .where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL))) 379 .limit(limit) 380 .offset(offset); 381
··· 359 options: CardQueryOptions, 360 ): Promise<PaginatedQueryResult<LibraryForUrlDTO>> { 361 try { 362 + const { page, limit, sortBy, sortOrder } = options; 363 const offset = (page - 1) * limit; 364 + 365 + // Build the sort order 366 + const orderDirection = sortOrder === SortOrder.ASC ? asc : desc; 367 368 // Get all URL cards with this URL and their library memberships 369 const librariesQuery = this.db ··· 379 .from(libraryMemberships) 380 .innerJoin(cards, eq(libraryMemberships.cardId, cards.id)) 381 .where(and(eq(cards.url, url), eq(cards.type, CardTypeEnum.URL))) 382 + .orderBy(orderDirection(this.getSortColumn(sortBy))) 383 .limit(limit) 384 .offset(offset); 385
+1 -1
src/modules/cards/tests/application/GetLibrariesForUrlUseCase.test.ts
··· 319 expect(result.isOk()).toBe(true); 320 const response = result.unwrap(); 321 322 - expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT); 323 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 324 }); 325
··· 319 expect(result.isOk()).toBe(true); 320 const response = result.unwrap(); 321 322 + expect(response.sorting.sortBy).toBe(CardSortField.CREATED_AT); 323 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 324 }); 325
+276
src/modules/cards/tests/infrastructure/DrizzleCardQueryRepository.getLibrariesForUrl.integration.test.ts
··· 15 import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 16 import { createTestSchema } from '../test-utils/createTestSchema'; 17 import { CardTypeEnum } from '../../domain/value-objects/CardType'; 18 19 describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => { 20 let container: StartedPostgreSqlContainer; ··· 281 // Should return empty since card is not in any library 282 expect(result.items).toHaveLength(0); 283 expect(result.totalCount).toBe(0); 284 }); 285 }); 286
··· 15 import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 16 import { createTestSchema } from '../test-utils/createTestSchema'; 17 import { CardTypeEnum } from '../../domain/value-objects/CardType'; 18 + import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; 19 20 describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => { 21 let container: StartedPostgreSqlContainer; ··· 282 // Should return empty since card is not in any library 283 expect(result.items).toHaveLength(0); 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 + } 560 }); 561 }); 562
+1 -1
src/webapp/app/(dashboard)/error.tsx
··· 88 component={Link} 89 href="/login" 90 size="lg" 91 - color="dark" 92 rightSection={<BiRightArrowAlt size={22} />} 93 > 94 Log in
··· 88 component={Link} 89 href="/login" 90 size="lg" 91 + color="var(--mantine-color-dark-filled)" 92 rightSection={<BiRightArrowAlt size={22} />} 93 > 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 <Button 47 component={Link} 48 href="/login" 49 - color="dark" 50 rightSection={<BiRightArrowAlt size={22} />} 51 > 52 Log in
··· 46 <Button 47 component={Link} 48 href="/login" 49 + color="var(--mantine-color-dark-filled)" 50 rightSection={<BiRightArrowAlt size={22} />} 51 > 52 Log in
+1 -1
src/webapp/features/cards/components/cardToBeAddedPreview/CardToBeAddedPreview.tsx
··· 127 </Anchor> 128 </Tooltip> 129 {props.title && ( 130 - <Text fw={500} lineClamp={1}> 131 {props.title} 132 </Text> 133 )}
··· 127 </Anchor> 128 </Tooltip> 129 {props.title && ( 130 + <Text fw={500} lineClamp={1} c="var(--mantine-color-bright)"> 131 {props.title} 132 </Text> 133 )}
+1 -1
src/webapp/features/cards/components/urlCard/UrlCard.tsx
··· 67 </Anchor> 68 </Tooltip> 69 {props.cardContent.title && ( 70 - <Text fw={500} lineClamp={2}> 71 {props.cardContent.title} 72 </Text> 73 )}
··· 67 </Anchor> 68 </Tooltip> 69 {props.cardContent.title && ( 70 + <Text fw={500} lineClamp={2} c={'var(--mantine-color-bright)'}> 71 {props.cardContent.title} 72 </Text> 73 )}
+1 -1
src/webapp/features/cards/lib/cardKeys.ts
··· 2 all: () => ['cards'] as const, 3 card: (id: string) => [...cardKeys.all(), id] as const, 4 byUrl: (url: string) => [...cardKeys.all(), url] as const, 5 - mine: () => [...cardKeys.all(), 'mine'] as const, 6 search: (query: string) => [...cardKeys.all(), 'search', query], 7 bySembleUrl: (url: string) => [...cardKeys.all(), url], 8 libraries: (id: string) => [...cardKeys.all(), 'libraries', id],
··· 2 all: () => ['cards'] as const, 3 card: (id: string) => [...cardKeys.all(), id] as const, 4 byUrl: (url: string) => [...cardKeys.all(), url] as const, 5 + mine: (limit?: number) => [...cardKeys.all(), 'mine', limit] as const, 6 search: (query: string) => [...cardKeys.all(), 'search', query], 7 bySembleUrl: (url: string) => [...cardKeys.all(), url], 8 libraries: (id: string) => [...cardKeys.all(), 'libraries', id],
+1 -1
src/webapp/features/cards/lib/queries/useMyCards.tsx
··· 10 const limit = props?.limit ?? 16; 11 12 const myCards = useSuspenseInfiniteQuery({ 13 - queryKey: cardKeys.mine(), 14 initialPageParam: 1, 15 queryFn: ({ pageParam = 1 }) => { 16 return getMyUrlCards({ page: pageParam, limit });
··· 10 const limit = props?.limit ?? 16; 11 12 const myCards = useSuspenseInfiniteQuery({ 13 + queryKey: cardKeys.mine(props?.limit), 14 initialPageParam: 1, 15 queryFn: ({ pageParam = 1 }) => { 16 return getMyUrlCards({ page: pageParam, limit });
+2 -2
src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
··· 34 > 35 <Stack justify="space-between" h={'100%'}> 36 <Stack gap={0}> 37 - <Text fw={500} lineClamp={1}> 38 {collection.name} 39 </Text> 40 {collection.description && ( ··· 58 size={'sm'} 59 /> 60 61 - <Text fw={500} span> 62 {collection.author.name} 63 </Text> 64 </Group>
··· 34 > 35 <Stack justify="space-between" h={'100%'}> 36 <Stack gap={0}> 37 + <Text fw={500} lineClamp={1} c={'var(--mantine-color-bright)'}> 38 {collection.name} 39 </Text> 40 {collection.description && ( ··· 58 size={'sm'} 59 /> 60 61 + <Text fw={500} c={'var(--mantine-color-bright)'} span> 62 {collection.author.name} 63 </Text> 64 </Group>
+7 -2
src/webapp/features/collections/lib/collectionKeys.ts
··· 1 export const collectionKeys = { 2 all: () => ['collections'] as const, 3 collection: (id: string) => [...collectionKeys.all(), id] as const, 4 - mine: () => [...collectionKeys.all(), 'mine'] as const, 5 search: (query: string) => [...collectionKeys.all(), 'search', query], 6 bySembleUrl: (url: string) => [...collectionKeys.all(), url], 7 - infinite: (id?: string) => [...collectionKeys.all(), 'infinite', id], 8 };
··· 1 export const collectionKeys = { 2 all: () => ['collections'] as const, 3 collection: (id: string) => [...collectionKeys.all(), id] as const, 4 + mine: (limit?: number) => [...collectionKeys.all(), 'mine', limit] as const, 5 search: (query: string) => [...collectionKeys.all(), 'search', query], 6 bySembleUrl: (url: string) => [...collectionKeys.all(), url], 7 + infinite: (id?: string, limit?: number) => [ 8 + ...collectionKeys.all(), 9 + 'infinite', 10 + id, 11 + limit, 12 + ], 13 };
+1 -1
src/webapp/features/collections/lib/mutations/useCreateCollection.tsx
··· 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 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 16 onSuccess: () => { 17 - queryClient.invalidateQueries({ queryKey: collectionKeys.infinite() }); 18 queryClient.refetchQueries({ queryKey: collectionKeys.mine() }); 19 }, 20 });
··· 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 // https://tkdodo.eu/blog/mastering-mutations-in-react-query#some-callbacks-might-not-fire 16 onSuccess: () => { 17 + queryClient.invalidateQueries({ queryKey: collectionKeys.all() }); 18 queryClient.refetchQueries({ queryKey: collectionKeys.mine() }); 19 }, 20 });
+1 -1
src/webapp/features/collections/lib/queries/useCollection.tsx
··· 12 const limit = props.limit ?? 20; 13 14 return useSuspenseInfiniteQuery({ 15 - queryKey: collectionKeys.infinite(props.rkey), 16 initialPageParam: 1, 17 queryFn: ({ pageParam }) => 18 getCollectionPageByAtUri({
··· 12 const limit = props.limit ?? 20; 13 14 return useSuspenseInfiniteQuery({ 15 + queryKey: collectionKeys.infinite(props.rkey, props.limit), 16 initialPageParam: 1, 17 queryFn: ({ pageParam }) => 18 getCollectionPageByAtUri({
+1 -1
src/webapp/features/collections/lib/queries/useCollections.tsx
··· 11 const limit = props?.limit ?? 15; 12 13 return useSuspenseInfiniteQuery({ 14 - queryKey: collectionKeys.infinite(props.didOrHandle), 15 initialPageParam: 1, 16 queryFn: ({ pageParam }) => 17 getCollections(props.didOrHandle, {
··· 11 const limit = props?.limit ?? 15; 12 13 return useSuspenseInfiniteQuery({ 14 + queryKey: collectionKeys.infinite(props.didOrHandle, props.limit), 15 initialPageParam: 1, 16 queryFn: ({ pageParam }) => 17 getCollections(props.didOrHandle, {
+1 -1
src/webapp/features/collections/lib/queries/useMyCollections.tsx
··· 10 const limit = props?.limit ?? 15; 11 12 return useSuspenseInfiniteQuery({ 13 - queryKey: collectionKeys.mine(), 14 initialPageParam: 1, 15 queryFn: ({ pageParam }) => getMyCollections({ limit, page: pageParam }), 16 getNextPageParam: (lastPage) => {
··· 10 const limit = props?.limit ?? 15; 11 12 return useSuspenseInfiniteQuery({ 13 + queryKey: collectionKeys.mine(props?.limit), 14 initialPageParam: 1, 15 queryFn: ({ pageParam }) => getMyCollections({ limit, page: pageParam }), 16 getNextPageParam: (lastPage) => {
+6 -1
src/webapp/features/feeds/components/feedActivityStatus/FeedActivityStatus.tsx
··· 41 42 return ( 43 <Text fw={500}> 44 - <Text component={Link} href={`/profile/${props.user.handle}`} fw={600}> 45 {sanitizeText(props.user.name)} 46 </Text>{' '} 47 {collections.length === 0 ? (
··· 41 42 return ( 43 <Text fw={500}> 44 + <Text 45 + component={Link} 46 + href={`/profile/${props.user.handle}`} 47 + fw={600} 48 + c={'var(--mantine-color-bright)'} 49 + > 50 {sanitizeText(props.user.name)} 51 </Text>{' '} 52 {collections.length === 0 ? (
+1 -1
src/webapp/features/home/containers/homeContainer/HomeContainer.tsx
··· 26 import { useNavbarContext } from '@/providers/navbar'; 27 28 export default function HomeContainer() { 29 - const { data: collectionsData } = useMyCollections({ limit: 8 }); 30 const { data: myCardsData } = useMyCards({ limit: 8 }); 31 const { data: profile } = useMyProfile(); 32
··· 26 import { useNavbarContext } from '@/providers/navbar'; 27 28 export default function HomeContainer() { 29 + const { data: collectionsData } = useMyCollections({ limit: 4 }); 30 const { data: myCardsData } = useMyCards({ limit: 8 }); 31 const { data: profile } = useMyProfile(); 32
+1 -1
src/webapp/features/home/containers/homeContainer/Skeleton.HomeContainer.tsx
··· 48 </Group> 49 50 <Grid gutter="md"> 51 - {Array.from({ length: 4 }).map((_, i) => ( 52 <GridCol key={i} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}> 53 <UrlCardSkeleton /> 54 </GridCol>
··· 48 </Group> 49 50 <Grid gutter="md"> 51 + {Array.from({ length: 6 }).map((_, i) => ( 52 <GridCol key={i} span={{ base: 12, xs: 6, sm: 4, lg: 3 }}> 53 <UrlCardSkeleton /> 54 </GridCol>
+1
src/webapp/features/notes/components/noteCard/NoteCard.tsx
··· 34 <Text 35 component={Link} 36 href={`/profile/${props.author.handle}`} 37 fw={500} 38 span 39 >
··· 34 <Text 35 component={Link} 36 href={`/profile/${props.author.handle}`} 37 + c={'var(--mantine-color-bright)'} 38 fw={500} 39 span 40 >
+1 -1
src/webapp/features/notes/components/noteCardModal/NoteCardModalContent.tsx
··· 174 </Anchor> 175 </Tooltip> 176 {props.cardContent.title && ( 177 - <Text fw={500} lineClamp={1}> 178 {props.cardContent.title} 179 </Text> 180 )}
··· 174 </Anchor> 175 </Tooltip> 176 {props.cardContent.title && ( 177 + <Text fw={500} lineClamp={1} c="var(--mantine-color-bright)"> 178 {props.cardContent.title} 179 </Text> 180 )}
+1 -1
src/webapp/features/profile/components/profileHeader/ProfileHeader.tsx
··· 31 /> 32 <Stack gap={'sm'} p={'xs'}> 33 <Stack gap={'xl'}> 34 - <Grid gutter={'md'} align={'center'} grow> 35 <GridCol span={'auto'}> 36 <Avatar 37 src={profile.avatarUrl}
··· 31 /> 32 <Stack gap={'sm'} p={'xs'}> 33 <Stack gap={'xl'}> 34 + <Grid gutter={'md'} grow> 35 <GridCol span={'auto'}> 36 <Avatar 37 src={profile.avatarUrl}
+1 -1
src/webapp/features/profile/components/profileHoverCard/ProfileHoverCard.tsx
··· 39 <Button 40 component={Link} 41 href={`/profile/${profile.handle}/cards`} 42 - color="dark" 43 leftSection={<FaRegNoteSticky />} 44 > 45 Cards
··· 39 <Button 40 component={Link} 41 href={`/profile/${profile.handle}/cards`} 42 + color="var(--mantine-color-dark-filled)" 43 leftSection={<FaRegNoteSticky />} 44 > 45 Cards
+1 -1
src/webapp/features/profile/components/profileMenu/ProfileMenu.tsx
··· 70 <Menu.Target> 71 <Button 72 variant="subtle" 73 - color={computedColorScheme === 'dark' ? 'gray' : 'dark'} 74 fz="md" 75 radius="md" 76 size="lg"
··· 70 <Menu.Target> 71 <Button 72 variant="subtle" 73 + color={'var(--mantine-color-bright)'} 74 fz="md" 75 radius="md" 76 size="lg"
+5 -1
src/webapp/features/semble/containers/sembleAside/SembleAside.tsx
··· 41 alt={`${lib.user.name}'s avatar`} 42 /> 43 <Stack gap={0}> 44 - <Text fw={600} lineClamp={1}> 45 {lib.user.name} 46 </Text> 47 <Text fw={600} c={'blue'} lineClamp={1}>
··· 41 alt={`${lib.user.name}'s avatar`} 42 /> 43 <Stack gap={0}> 44 + <Text 45 + fw={600} 46 + lineClamp={1} 47 + c={'var(--mantine-color-bright)'} 48 + > 49 {lib.user.name} 50 </Text> 51 <Text fw={600} c={'blue'} lineClamp={1}>