A social knowledge tool for researchers built on ATProto

Compare changes

Choose any two refs to compare.

Changed files
+400 -82
.vscode
src
modules
cards
infrastructure
repositories
query-services
tests
webapp
app
(dashboard)
bookmarklet
components
navigation
guestNavbar
features
cards
components
cardToBeAddedPreview
lib
collections
components
collectionCard
lib
home
notes
components
noteCard
noteCardModal
profile
components
profileHoverCard
semble
containers
sembleAside
+1 -1
.vscode/settings.json
··· 1 { 2 "jest.runMode": "on-demand" 3 - }
··· 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 + }
+83 -65
src/webapp/app/bookmarklet/page.tsx
··· 9 Code, 10 Alert, 11 Box, 12 Group, 13 } from '@mantine/core'; 14 - import { useState } from 'react'; 15 - import { BiInfoCircle } from 'react-icons/bi'; 16 17 export default function BookmarkletPage() { 18 - const [copied, setCopied] = useState(false); 19 20 - const appUrl = process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'; 21 - 22 const bookmarkletCode = `javascript:(function(){ 23 const currentUrl = window.location.href; 24 const sembleUrl = '${appUrl}/url?id=' + currentUrl; 25 window.open(sembleUrl, '_blank'); 26 - })();`; 27 - 28 - const handleCopy = async () => { 29 - try { 30 - await navigator.clipboard.writeText(bookmarkletCode); 31 - setCopied(true); 32 - setTimeout(() => setCopied(false), 2000); 33 - } catch (err) { 34 - console.error('Failed to copy bookmarklet:', err); 35 - } 36 - }; 37 38 // Create the bookmarklet link using dangerouslySetInnerHTML to bypass React's security check 39 const createBookmarkletLink = () => { 40 return { 41 - __html: `<a href="${bookmarkletCode}" style="text-decoration: none; padding: 8px 16px; background-color: var(--mantine-color-orange-6); color: white; border-radius: 4px; display: inline-flex; align-items: center; gap: 8px;"><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>` 42 }; 43 }; 44 45 return ( 46 - <Container size="md" py="xl"> 47 <Stack gap="xl"> 48 - <Stack gap="md"> 49 - <Title order={1}>Semble Bookmarklet</Title> 50 - <Text size="lg" c="dimmed"> 51 - Add this bookmarklet to your browser to quickly open any webpage in Semble 52 - and see what your network has shared about it. 53 - </Text> 54 </Stack> 55 56 - <Alert icon={<BiInfoCircle />} title="How to install" color="blue"> 57 <Stack gap="sm"> 58 - <Text> 59 - 1. Copy the bookmarklet code below or drag the button to your bookmarks bar 60 - </Text> 61 - <Text> 62 - 2. When you're on any webpage, click the bookmarklet to open it in Semble 63 - </Text> 64 - <Text> 65 - 3. You'll see who in your network has shared that URL and any notes they've added 66 - </Text> 67 </Stack> 68 </Alert> 69 70 <Stack gap="md"> 71 - <Title order={2} size="h3"> 72 - Method 1: Drag to Bookmarks Bar 73 - </Title> 74 - <Text c="dimmed"> 75 - Drag this button directly to your browser's bookmarks bar: 76 - </Text> 77 <Group> 78 <Box dangerouslySetInnerHTML={createBookmarkletLink()} /> 79 </Group> 80 </Stack> 81 82 <Stack gap="md"> 83 - <Title order={2} size="h3"> 84 - Method 2: Copy Code 85 - </Title> 86 - <Text c="dimmed"> 87 - Copy this code and create a new bookmark with it as the URL: 88 - </Text> 89 <Box pos="relative"> 90 <Code 91 block ··· 98 > 99 {bookmarkletCode} 100 </Code> 101 - <Button 102 - size="xs" 103 - variant="light" 104 - onClick={handleCopy} 105 - style={{ 106 - position: 'absolute', 107 - top: '8px', 108 - right: '8px', 109 - }} 110 - > 111 - {copied ? 'Copied!' : 'Copy'} 112 - </Button> 113 </Box> 114 </Stack> 115 - 116 - <Alert icon={<BiInfoCircle />} title="Note" color="gray"> 117 - <Text> 118 - This bookmarklet will open Semble in a new tab. Make sure you're logged in 119 - to see personalized results from your network. 120 - </Text> 121 - </Alert> 122 </Stack> 123 </Container> 124 );
··· 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 ··· 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 );
+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/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 });
+1 -1
src/webapp/features/collections/components/collectionCard/CollectionCard.tsx
··· 58 size={'sm'} 59 /> 60 61 - <Text fw={500} span> 62 {collection.author.name} 63 </Text> 64 </Group>
··· 58 size={'sm'} 59 /> 60 61 + <Text fw={500} c={'var(--mantine-color-bright)'} span> 62 {collection.author.name} 63 </Text> 64 </Group>
+1 -1
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, limit?: number) => [
··· 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) => [
+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/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) => {
+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/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
+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}>