NotesTabs.svelte
1 <script lang="ts"> 2 import { onMount } from "svelte"; 3 import ArticleCard from "./ArticleCard.svelte"; 4 import MusicCard from "./MusicCard.svelte"; 5 import PeopleCard from "./PeopleCard.svelte"; 6 import VaultCard from "./VaultCard.svelte"; 7 8 interface Note { 9 id: string; 10 title: string; 11 description?: string; 12 category: string; 13 tags: string[]; 14 created_at: string; 15 modified_at: string; 16 url?: string; 17 author?: string; 18 artist?: string; 19 album?: string; 20 year?: string[]; 21 links?: string[]; 22 backlink_count?: number; 23 } 24 25 interface Props { 26 notes: Note[]; 27 filteredIds?: string[] | null; 28 } 29 30 let { notes, filteredIds = null }: Props = $props(); 31 32 const categories = ["article", "vault", "person", "music"] as const; 33 type Category = (typeof categories)[number]; 34 35 const categoryLabels: Record<Category, string> = { 36 article: "Articles", 37 vault: "Vault", 38 person: "People", 39 music: "Music", 40 }; 41 42 const categoryHashMap: Record<string, Category> = { 43 articles: "article", 44 vault: "vault", 45 people: "person", 46 music: "music", 47 }; 48 49 const hashToCategoryMap: Record<Category, string> = { 50 article: "articles", 51 vault: "vault", 52 person: "people", 53 music: "music", 54 }; 55 56 let activeTab = $state<Category>("article"); 57 58 let displayedNotes = $derived.by(() => { 59 let filtered = notes; 60 if (filteredIds !== null) { 61 filtered = notes.filter((n) => filteredIds?.includes(n.id)); 62 } 63 return filtered.filter((n) => n.category === activeTab); 64 }); 65 66 let categoryCounts = $derived.by(() => { 67 const counts: Record<Category, number> = { 68 article: 0, 69 vault: 0, 70 person: 0, 71 music: 0, 72 }; 73 let filtered = notes; 74 if (filteredIds !== null) { 75 filtered = notes.filter((n) => filteredIds?.includes(n.id)); 76 } 77 for (const note of filtered) { 78 if (counts[note.category as Category] !== undefined) { 79 counts[note.category as Category]++; 80 } 81 } 82 return counts; 83 }); 84 85 function setTab(category: Category) { 86 activeTab = category; 87 const hash = hashToCategoryMap[category]; 88 window.history.replaceState(null, "", `#${hash}`); 89 } 90 91 onMount(() => { 92 const hash = window.location.hash.slice(1); 93 if (hash && categoryHashMap[hash]) { 94 activeTab = categoryHashMap[hash]; 95 } 96 }); 97 </script> 98 99 <div class="space-y-4"> 100 <!-- Tab bar --> 101 <div class="relative flex flex-wrap gap-4 border-b border-pink-300/50 pb-2"> 102 <!-- floating indicator --> 103 <span class="tab-indicator"></span> 104 {#each categories as cat} 105 {@const count = categoryCounts[cat]} 106 {@const isActive = activeTab === cat} 107 <button 108 onclick={() => setTab(cat)} 109 class="tab-btn relative flex items-center gap-2 text-sm font-medium transition-colors pb-1 cursor-pointer 110 outline-none focus-visible:ring-2 focus-visible:ring-pink-400/40 rounded px-2 py-1 111 {isActive ? 'text-pink-600' : 'text-pink-950/50 hover:text-pink-950/80'}" 112 aria-pressed={isActive} 113 style={isActive ? 'anchor-name: --active-tab' : ''} 114 > 115 {categoryLabels[cat]} 116 <span 117 class="text-xs px-1.5 py-0.5 rounded 118 {isActive ? 'bg-pink-100 text-pink-600' : 'bg-pink-50 text-pink-400'}" 119 > 120 {count} 121 </span> 122 </button> 123 {/each} 124 </div> 125 126 <!-- Notes grid --> 127 {#if displayedNotes.length === 0} 128 <div class="text-center py-12 text-pink-950/50"> 129 <p>No notes found in this category.</p> 130 {#if filteredIds !== null} 131 <p class="text-sm mt-1">Try adjusting your search.</p> 132 {/if} 133 </div> 134 {:else if activeTab === "article"} 135 <div class="grid gap-2"> 136 {#each displayedNotes as note (note.id)} 137 <ArticleCard 138 id={note.id} 139 title={note.title} 140 description={note.description} 141 tags={note.tags} 142 created_at={note.created_at} 143 url={note.url} 144 author={note.author} 145 /> 146 {/each} 147 </div> 148 {:else if activeTab === "vault"} 149 <div class="grid gap-2"> 150 {#each displayedNotes as note (note.id)} 151 <VaultCard 152 id={note.id} 153 title={note.title} 154 description={note.description} 155 tags={note.tags} 156 created_at={note.created_at} 157 /> 158 {/each} 159 </div> 160 {:else if activeTab === "music"} 161 <div class="grid grid-cols-1 sm:grid-cols-2 gap-2"> 162 {#each displayedNotes as note (note.id)} 163 <MusicCard 164 id={note.id} 165 title={note.title} 166 tags={note.tags} 167 artist={note.artist} 168 album={note.album} 169 year={note.year} 170 description={note.description} 171 /> 172 {/each} 173 </div> 174 {:else if activeTab === "person"} 175 <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 gap-2"> 176 {#each displayedNotes as note (note.id)} 177 <PeopleCard 178 id={note.id} 179 title={note.title} 180 tags={note.tags} 181 backlink_count={note.backlink_count} 182 /> 183 {/each} 184 </div> 185 {/if} 186 </div> 187 188 <style> 189 .tab-indicator { 190 position: absolute; 191 position-anchor: --active-tab; 192 bottom: -1px; 193 left: anchor(left); 194 width: anchor-size(width); 195 height: 2px; 196 background-color: var(--color-pink-500); 197 transition: 198 left 0.2s ease, 199 width 0.2s ease; 200 } 201 </style>