@mdguggenbichler/slugbase-core 0.0.30 → 0.0.32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/frontend/src/components/FilterChips.tsx +5 -3
  2. package/frontend/src/components/StatCard.tsx +82 -5
  3. package/frontend/src/components/bookmarks/BookmarkCard.tsx +317 -210
  4. package/frontend/src/components/bookmarks/BookmarkTableView.tsx +47 -23
  5. package/frontend/src/components/collections/CollectionToolbar.tsx +294 -0
  6. package/frontend/src/components/collections/README.md +44 -0
  7. package/frontend/src/components/collections/index.ts +2 -0
  8. package/frontend/src/components/dashboard/DashboardHeader.tsx +16 -0
  9. package/frontend/src/components/dashboard/MostUsedTagsSection.tsx +49 -0
  10. package/frontend/src/components/dashboard/PinnedSection.tsx +110 -0
  11. package/frontend/src/components/dashboard/QuickAccessSection.tsx +120 -0
  12. package/frontend/src/components/dashboard/README.md +35 -0
  13. package/frontend/src/components/dashboard/StatsCardsRow.tsx +78 -0
  14. package/frontend/src/components/dashboard/index.ts +17 -0
  15. package/frontend/src/config/docs.ts +19 -0
  16. package/frontend/src/config/mode.ts +6 -4
  17. package/frontend/src/locales/de.json +2 -0
  18. package/frontend/src/locales/en.json +1 -0
  19. package/frontend/src/locales/es.json +2 -0
  20. package/frontend/src/locales/fr.json +2 -0
  21. package/frontend/src/locales/it.json +2 -0
  22. package/frontend/src/locales/ja.json +2 -0
  23. package/frontend/src/locales/nl.json +2 -0
  24. package/frontend/src/locales/pl.json +2 -0
  25. package/frontend/src/locales/pt.json +2 -0
  26. package/frontend/src/locales/ru.json +2 -0
  27. package/frontend/src/locales/zh.json +2 -0
  28. package/frontend/src/pages/Bookmarks.tsx +97 -214
  29. package/frontend/src/pages/Dashboard.tsx +99 -216
  30. package/frontend/src/pages/Folders.tsx +181 -251
  31. package/frontend/src/pages/Profile.tsx +3 -2
  32. package/frontend/src/pages/Signup.tsx +3 -2
  33. package/frontend/src/pages/Tags.tsx +87 -145
  34. package/frontend/src/pages/admin/AdminLayout.tsx +2 -1
  35. package/package.json +1 -1
@@ -6,20 +6,18 @@ import api from '../api/client';
6
6
  import ConfirmDialog from '../components/ui/ConfirmDialog';
7
7
  import { useConfirmDialog } from '../hooks/useConfirmDialog';
8
8
  import { useToast } from '../components/ui/Toast';
9
- import { Plus, LayoutGrid, List, CheckSquare, Download, Upload, Bookmark as BookmarkIcon, ExternalLink, FolderPlus, Tag as TagIcon, Share2, Trash2, Copy, ChevronLeft, ChevronRight, Pin, Search } from 'lucide-react';
9
+ import { Plus, Upload, Bookmark as BookmarkIcon, ExternalLink, FolderPlus, Tag as TagIcon, Share2, Trash2, Copy, ChevronLeft, ChevronRight } from 'lucide-react';
10
10
  import BookmarkModal from '../components/modals/BookmarkModal';
11
11
  import ImportModal from '../components/modals/ImportModal';
12
12
  import ShareResourceDialog from '../components/sharing/ShareResourceDialog';
13
13
  import Button from '../components/ui/Button';
14
- import Select from '../components/ui/Select';
15
14
  import BookmarkCard from '../components/bookmarks/BookmarkCard';
16
15
  import BookmarkTableView from '../components/bookmarks/BookmarkTableView';
17
16
  import { BulkMoveModal, BulkTagModal, BulkShareModal } from '../components/bookmarks/BulkActionModals';
18
- import { FilterChips, type FilterKey } from '../components/bookmarks/FilterChips';
19
- import { ScopeSegmentedControl } from '../components/ScopeSegmentedControl';
17
+ import { type FilterKey } from '../components/bookmarks/FilterChips';
18
+ import { CollectionToolbar } from '../components/collections';
20
19
  import { PageLoadingSkeleton } from '../components/ui/PageLoadingSkeleton';
21
20
  import { Card } from '../components/ui/card';
22
- import { PageHeader } from '../components/PageHeader';
23
21
  import { useSidebar } from '../components/ui/sidebar';
24
22
  import { useAppConfig } from '../contexts/AppConfigContext';
25
23
 
@@ -63,9 +61,6 @@ export default function Bookmarks() {
63
61
  const saved = localStorage.getItem('bookmarks-view-mode');
64
62
  return (saved === 'list' || saved === 'card') ? saved : 'card';
65
63
  });
66
- const [compactMode, setCompactMode] = useState(() => {
67
- return localStorage.getItem('bookmarks-compact-mode') === 'true';
68
- });
69
64
  const [sortBy, setSortBy] = useState<SortOption>('recently_added');
70
65
  const [selectedBookmarks, setSelectedBookmarks] = useState<Set<string>>(new Set());
71
66
  const [allSelectedAcrossPages, setAllSelectedAcrossPages] = useState(false);
@@ -170,10 +165,6 @@ export default function Bookmarks() {
170
165
  localStorage.setItem('bookmarks-view-mode', viewMode);
171
166
  }, [viewMode]);
172
167
 
173
- useEffect(() => {
174
- localStorage.setItem('bookmarks-compact-mode', compactMode.toString());
175
- }, [compactMode]);
176
-
177
168
  useEffect(() => {
178
169
  setSearchInputValue(searchQuery);
179
170
  }, [searchQuery]);
@@ -499,202 +490,98 @@ export default function Bookmarks() {
499
490
 
500
491
  return (
501
492
  <div className="space-y-6 pb-24">
502
- {/* Sticky controls bar: header + filters/toolbar - stays visible when scrolling */}
503
- <div className="sticky top-0 z-40 space-y-4 pb-4 -mx-4 sm:-mx-6 lg:-mx-8 px-4 sm:px-6 lg:px-8 pt-0 -mt-8 bg-background shadow-sm">
504
- <PageHeader
505
- className="pt-4"
506
- title={`${t('bookmarks.title')} (${total})`}
507
- subtitle={
508
- hasActiveFilters
509
- ? t('bookmarks.showingXOfY', { x: displayedBookmarks.length, y: total })
510
- : undefined
511
- }
512
- actions={
513
- <div className="flex flex-wrap items-center gap-2">
514
- <ScopeSegmentedControl
515
- value={scope}
516
- onChange={(s) => updateParams({ scope: s === 'all' ? undefined : s })}
517
- options={[
518
- { value: 'all', label: t('bookmarks.scopeAll') },
519
- { value: 'mine', label: t('bookmarks.scopeMine') },
520
- { value: 'shared_with_me', label: t('common.scopeSharedWithMe') },
521
- { value: 'shared_by_me', label: t('common.scopeSharedByMe') },
522
- ]}
523
- ariaLabel={t('bookmarks.scopeAll')}
524
- />
525
- <Button
526
- variant={pinnedFilter ? 'secondary' : 'ghost'}
527
- size="sm"
528
- icon={Pin}
529
- onClick={() => updateParams({ pinned: pinnedFilter ? undefined : 'true' })}
530
- title={t('bookmarks.pinned')}
531
- aria-pressed={pinnedFilter}
532
- >
533
- <span className="hidden sm:inline">{t('bookmarks.pinned')}</span>
534
- </Button>
535
- <Button
536
- variant="ghost"
537
- size="sm"
538
- icon={Upload}
539
- onClick={() => setImportModalOpen(true)}
540
- title={t('bookmarks.import')}
541
- >
542
- <span className="hidden sm:inline">{t('bookmarks.import')}</span>
543
- </Button>
544
- <Button
545
- variant="ghost"
546
- size="sm"
547
- icon={Download}
548
- onClick={handleExport}
549
- title={t('bookmarks.export')}
550
- >
551
- <span className="hidden sm:inline">{t('bookmarks.export')}</span>
552
- </Button>
553
- <Button onClick={handleCreate} icon={Plus}>
554
- {t('bookmarks.create')}
555
- </Button>
556
- </div>
493
+ <CollectionToolbar
494
+ title={t('bookmarks.title')}
495
+ count={total}
496
+ subtitle={
497
+ hasActiveFilters
498
+ ? t('bookmarks.showingXOfY', { x: displayedBookmarks.length, y: total })
499
+ : undefined
557
500
  }
558
- />
559
-
560
- <FilterChips
561
- chips={filterChips}
562
- onRemove={(key) => handleRemoveFilter(key as FilterKey)}
563
- onClearAll={handleResetFilters}
564
- clearAllLabel={t('bookmarks.clearAllFilters')}
565
- clearAllAriaLabel={t('bookmarks.clearAllFilters')}
566
- />
567
-
568
- {/* Toolbar: Search, Filters, Sort, View Modes */}
569
- <div className="flex flex-wrap items-center gap-3 bg-card rounded-lg border p-4 shadow-sm">
570
- <div className="flex items-center gap-2 min-w-[200px] flex-1">
571
- <Search className="h-4 w-4 text-muted-foreground flex-shrink-0" aria-hidden />
572
- <input
573
- type="search"
574
- value={searchInputValue}
575
- onChange={(e) => setSearchInputValue(e.target.value)}
576
- onKeyDown={(e) => {
577
- if (e.key === 'Enter') updateParams({ q: (e.target as HTMLInputElement).value.trim() || undefined });
578
- }}
579
- placeholder={t('common.searchPlaceholder')}
580
- className="flex-1 min-w-0 rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"
581
- aria-label={t('common.searchPlaceholder')}
582
- />
583
- </div>
584
- {/* Filters */}
585
- <div className="flex flex-wrap gap-3 flex-1 min-w-[200px]">
586
- <div className="flex-1 min-w-[180px]">
587
- <Select
588
- value={selectedFolder || ALL_FILTER}
589
- onChange={(value) => {
590
- const params = new URLSearchParams(searchParams);
591
- if (value && value !== ALL_FILTER) {
592
- params.set('folder_id', value);
593
- } else {
594
- params.delete('folder_id');
595
- }
596
- setSearchParams(params);
597
- }}
598
- options={folderOptions}
599
- placeholder={t('bookmarks.filterByFolder')}
600
- />
601
- </div>
602
- <div className="flex-1 min-w-[180px]">
603
- <Select
604
- value={selectedTag || ALL_FILTER}
605
- onChange={(value) => {
606
- const params = new URLSearchParams(searchParams);
607
- if (value && value !== ALL_FILTER) {
608
- params.set('tag_id', value);
609
- } else {
610
- params.delete('tag_id');
611
- }
612
- setSearchParams(params);
613
- }}
614
- options={tagOptions}
615
- placeholder={t('bookmarks.filterByTag')}
616
- />
617
- </div>
618
- </div>
619
-
620
- {/* Sort */}
621
- <div className="flex items-center gap-2">
622
- <Select
623
- value={sortBy}
624
- onChange={(value) => setSortBy(value as SortOption)}
625
- options={sortOptions}
626
- className="min-w-[160px]"
627
- />
628
- </div>
629
-
630
- {/* Page size */}
631
- <div className="flex items-center gap-2">
632
- <Select
633
- value={String(pageSize)}
634
- onChange={(value) => {
635
- updateParams({ limit: value });
636
- setPage(0);
637
- }}
638
- options={PAGE_SIZE_OPTIONS.map((n) => ({ value: String(n), label: String(n) }))}
639
- className="min-w-[80px]"
640
- />
641
- <span className="text-sm text-muted-foreground whitespace-nowrap">{t('bookmarks.perPage')}</span>
642
- </div>
643
-
644
- {/* View Mode Toggle */}
645
- <div className="flex items-center gap-2 border-l border-gray-200 dark:border-gray-700 pl-3">
646
- <div className="flex items-center gap-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
647
- <button
648
- onClick={() => setViewMode('card')}
649
- className={`p-1.5 rounded transition-colors ${
650
- viewMode === 'card'
651
- ? 'bg-card text-primary shadow-sm'
652
- : 'text-muted-foreground hover:text-foreground'
653
- }`}
654
- title={t('bookmarks.viewCard')}
655
- >
656
- <LayoutGrid className="h-4 w-4" />
657
- </button>
658
- <button
659
- onClick={() => setViewMode('list')}
660
- className={`p-1.5 rounded transition-colors ${
661
- viewMode === 'list'
662
- ? 'bg-card text-primary shadow-sm'
663
- : 'text-muted-foreground hover:text-foreground'
664
- }`}
665
- title={t('bookmarks.viewList')}
666
- >
667
- <List className="h-4 w-4" />
668
- </button>
669
- </div>
670
- <button
671
- onClick={() => setCompactMode(!compactMode)}
672
- className={`px-3 py-1.5 text-sm rounded-lg transition-colors ${
673
- compactMode
674
- ? 'bg-primary/20 text-primary'
675
- : 'bg-muted text-muted-foreground hover:bg-accent'
676
- }`}
677
- title={t('bookmarks.compactMode')}
678
- >
679
- {t('bookmarks.compactMode')}
680
- </button>
681
- </div>
682
-
683
- {/* Bulk Select Toggle */}
684
- {!bulkMode && displayedBookmarks.length > 0 && (
685
- <div className="flex items-center gap-2 border-l border-gray-200 dark:border-gray-700 pl-3">
686
- <Button
687
- variant="ghost"
688
- size="sm"
689
- icon={CheckSquare}
690
- onClick={() => setBulkMode(true)}
691
- >
692
- {t('bookmarks.bulkSelect')}
693
- </Button>
694
- </div>
695
- )}
696
- </div>
697
- </div>
501
+ tabs={{
502
+ value: scope,
503
+ onChange: (s) => updateParams({ scope: s === 'all' ? undefined : s }),
504
+ options: [
505
+ { value: 'all', label: t('bookmarks.scopeAll') },
506
+ { value: 'mine', label: t('bookmarks.scopeMine') },
507
+ { value: 'shared_with_me', label: t('common.scopeSharedWithMe') },
508
+ { value: 'shared_by_me', label: t('common.scopeSharedByMe') },
509
+ ],
510
+ ariaLabel: t('bookmarks.scopeAll'),
511
+ }}
512
+ createButton={{ label: t('bookmarks.create'), onClick: handleCreate }}
513
+ filterChips={{
514
+ chips: filterChips,
515
+ onRemove: (key) => handleRemoveFilter(key as FilterKey),
516
+ onClearAll: handleResetFilters,
517
+ clearAllLabel: t('bookmarks.clearAllFilters'),
518
+ clearAllAriaLabel: t('bookmarks.clearAllFilters'),
519
+ }}
520
+ search={{
521
+ value: searchInputValue,
522
+ onChange: setSearchInputValue,
523
+ onSubmit: (value) => updateParams({ q: value || undefined }),
524
+ placeholder: t('common.searchPlaceholder'),
525
+ ariaLabel: t('common.searchPlaceholder'),
526
+ }}
527
+ folderFilter={{
528
+ value: selectedFolder || ALL_FILTER,
529
+ onChange: (value) => {
530
+ const params = new URLSearchParams(searchParams);
531
+ if (value && value !== ALL_FILTER) params.set('folder_id', value);
532
+ else params.delete('folder_id');
533
+ setSearchParams(params);
534
+ },
535
+ options: folderOptions,
536
+ placeholder: t('bookmarks.filterByFolder'),
537
+ }}
538
+ tagFilter={{
539
+ value: selectedTag || ALL_FILTER,
540
+ onChange: (value) => {
541
+ const params = new URLSearchParams(searchParams);
542
+ if (value && value !== ALL_FILTER) params.set('tag_id', value);
543
+ else params.delete('tag_id');
544
+ setSearchParams(params);
545
+ },
546
+ options: tagOptions,
547
+ placeholder: t('bookmarks.filterByTag'),
548
+ }}
549
+ sort={{
550
+ value: sortBy,
551
+ onChange: (value) => setSortBy(value as SortOption),
552
+ options: sortOptions,
553
+ className: 'min-w-[160px]',
554
+ }}
555
+ perPage={{
556
+ value: pageSize,
557
+ onChange: (value) => {
558
+ updateParams({ limit: String(value) });
559
+ setPage(0);
560
+ },
561
+ options: [...PAGE_SIZE_OPTIONS],
562
+ label: t('bookmarks.perPage'),
563
+ }}
564
+ viewMode={{
565
+ value: viewMode,
566
+ onChange: setViewMode,
567
+ cardLabel: t('bookmarks.viewCard'),
568
+ listLabel: t('bookmarks.viewList'),
569
+ }}
570
+ pinnedToggle={{
571
+ active: pinnedFilter,
572
+ onClick: () => updateParams({ pinned: pinnedFilter ? undefined : 'true' }),
573
+ label: t('bookmarks.pinned'),
574
+ }}
575
+ onImport={() => setImportModalOpen(true)}
576
+ importLabel={t('bookmarks.import')}
577
+ onExport={handleExport}
578
+ exportLabel={t('bookmarks.export')}
579
+ bulkSelect={
580
+ !bulkMode && displayedBookmarks.length > 0
581
+ ? { onClick: () => setBulkMode(true), label: t('bookmarks.bulkSelect') }
582
+ : { onClick: () => setBulkMode(true), label: t('bookmarks.bulkSelect'), disabled: true }
583
+ }
584
+ />
698
585
 
699
586
  {/* Bulk Actions Bar - sticky bottom, visible when selecting */}
700
587
  {bulkMode && (
@@ -836,16 +723,12 @@ export default function Bookmarks() {
836
723
  </div>
837
724
  </Card>
838
725
  ) : viewMode === 'card' ? (
839
- <div className={`grid grid-cols-1 gap-3 items-stretch ${
840
- compactMode
841
- ? 'sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 2xl:grid-cols-8'
842
- : 'sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 2xl:grid-cols-6'
843
- }`}>
726
+ <div className="grid gap-3 items-stretch [grid-template-columns:repeat(auto-fill,minmax(300px,1fr))]">
844
727
  {displayedBookmarks.map((bookmark) => (
845
728
  <BookmarkCard
846
729
  key={bookmark.id}
847
730
  bookmark={bookmark}
848
- compact={compactMode}
731
+ compact={false}
849
732
  selected={selectedBookmarks.has(bookmark.id)}
850
733
  onSelect={() => toggleSelectBookmark(bookmark.id)}
851
734
  onEdit={() => handleEdit(bookmark)}
@@ -873,7 +756,7 @@ export default function Bookmarks() {
873
756
  bulkMode={bulkMode}
874
757
  user={user}
875
758
  t={t}
876
- compact={compactMode}
759
+ compact={false}
877
760
  />
878
761
  ) : null}
879
762