my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: tags

+73 -12
+21 -5
src/lib/components/ui/BlogPostCard.svelte
··· 17 17 18 18 <InternalCard href={post.url}> 19 19 {#snippet children()} 20 - <div class="min-w-0 flex-1 space-y-2"> 20 + <div class="relative min-w-0 flex-1 space-y-2"> 21 21 <!-- Badges: Platform and Publication --> 22 22 {#if badges.length > 0} 23 23 <div class="flex flex-wrap items-center gap-2"> ··· 45 45 </p> 46 46 {/if} 47 47 48 - <!-- Timestamp --> 49 - <p class="text-xs font-medium text-ink-800 dark:text-ink-100"> 50 - {formatLocalizedDate(post.createdAt, locale)} 51 - </p> 48 + <!-- Timestamp and Tags row --> 49 + <div class="flex items-end justify-between gap-3 pt-1"> 50 + <!-- Timestamp (left) --> 51 + <p class="text-xs font-medium text-ink-800 dark:text-ink-100"> 52 + {formatLocalizedDate(post.createdAt, locale)} 53 + </p> 54 + 55 + <!-- Tags (right) --> 56 + {#if post.tags && post.tags.length > 0} 57 + <div class="flex flex-wrap items-center justify-end gap-1.5"> 58 + {#each post.tags as tag} 59 + <span 60 + class="rounded bg-ink-100 px-2 py-0.5 text-xs font-medium text-ink-800 dark:bg-ink-800 dark:text-ink-100" 61 + > 62 + #{tag} 63 + </span> 64 + {/each} 65 + </div> 66 + {/if} 67 + </div> 52 68 </div> 53 69 54 70 <!-- External Link Icon -->
+13 -1
src/lib/helper/posts.ts
··· 20 20 const descMatch = post.description?.toLowerCase().includes(lowerQuery); 21 21 const platformMatch = post.platform.toLowerCase().includes(lowerQuery); 22 22 const pubMatch = post.publicationName?.toLowerCase().includes(lowerQuery); 23 - return titleMatch || descMatch || platformMatch || pubMatch; 23 + const tagsMatch = post.tags?.some((tag) => tag.toLowerCase().includes(lowerQuery)); 24 + return titleMatch || descMatch || platformMatch || pubMatch || tagsMatch; 24 25 }); 25 26 } 26 27 ··· 74 75 export function getSortedYears(groupedPosts: GroupedPosts): number[] { 75 76 return Array.from(groupedPosts.keys()).sort((a, b) => b - a); 76 77 } 78 + 79 + /** 80 + * Extract all unique tags from posts 81 + */ 82 + export function getAllTags(posts: BlogPost[]): string[] { 83 + const tagsSet = new Set<string>(); 84 + posts.forEach((post) => { 85 + post.tags?.forEach((tag) => tagsSet.add(tag)); 86 + }); 87 + return Array.from(tagsSet).sort(); 88 + }
+2 -1
src/lib/services/atproto/posts.ts
··· 187 187 description: value.description, 188 188 rkey, 189 189 publicationName: publication?.name, 190 - publicationRkey 190 + publicationRkey, 191 + tags: value.tags || undefined 191 192 }); 192 193 } 193 194 } catch (error) {
+1
src/lib/services/atproto/types.ts
··· 107 107 rkey: string; 108 108 publicationName?: string; 109 109 publicationRkey?: string; 110 + tags?: string[]; 110 111 } 111 112 112 113 export interface BlogPostsData {
+34 -4
src/routes/archive/+page.svelte
··· 9 9 } from '$lib/components/ui'; 10 10 import type { BlogPost } from '$lib/services/atproto'; 11 11 import { getUserLocale } from '$lib/utils/locale'; 12 - import { filterPosts, getSortedYears, groupPostsByDate } from '$lib/helper/posts'; 12 + import { filterPosts, getSortedYears, groupPostsByDate, getAllTags } from '$lib/helper/posts'; 13 13 14 14 interface Props { 15 15 data: { ··· 26 26 let searchQuery = $state(''); 27 27 let selectedYear = $state('all'); 28 28 let selectedPublication = $state(''); 29 + let selectedTag = $state(''); 29 30 let currentPage = $state(1); 30 31 const postsPerPage = 50; 31 32 ··· 54 55 })); 55 56 }); 56 57 58 + // Get unique tags 59 + const allTags = $derived(getAllTags(data.allPosts)); 60 + const tagOptions = $derived( 61 + allTags.map((tag) => ({ 62 + value: tag, 63 + label: `#${tag}` 64 + })) 65 + ); 66 + 57 67 // Filter posts by search, year, and publication 58 68 const filteredBySearch = $derived(filterPosts(data.allPosts, searchQuery)); 59 69 ··· 65 75 }); 66 76 }); 67 77 68 - const filteredPosts = $derived.by(() => { 78 + const filteredByPublication = $derived.by(() => { 69 79 if (!selectedPublication) return filteredByYear; 70 80 return filteredByYear.filter((post: BlogPost) => { 71 81 if (post.platform === 'WhiteWind' && selectedPublication === 'whitewind') return true; ··· 77 87 }); 78 88 }); 79 89 90 + const filteredPosts = $derived.by(() => { 91 + if (!selectedTag) return filteredByPublication; 92 + return filteredByPublication.filter((post: BlogPost) => { 93 + return post.tags?.includes(selectedTag); 94 + }); 95 + }); 96 + 80 97 // Add WhiteWind to publication options if there are WhiteWind posts 81 98 const hasWhiteWind = $derived(data.allPosts.some((p) => p.platform === 'WhiteWind')); 82 99 const publicationOptions = $derived.by(() => [ ··· 95 112 searchQuery; 96 113 selectedYear; 97 114 selectedPublication; 115 + selectedTag; 98 116 currentPage = 1; 99 117 }); 100 118 ··· 122 140 <div class="mb-6"> 123 141 <SearchBar 124 142 bind:value={searchQuery} 125 - placeholder="Search posts by title, description, platform, or publication..." 143 + placeholder="Search posts by title, description, platform, publication, or tags..." 126 144 resultCount={searchQuery ? filteredPosts.length : undefined} 127 145 /> 128 146 </div> ··· 140 158 /> 141 159 </div> 142 160 {/if} 161 + 162 + <!-- Tag Dropdown --> 163 + {#if tagOptions.length > 0} 164 + <div class="flex-1 sm:max-w-xs"> 165 + <Dropdown 166 + bind:value={selectedTag} 167 + options={tagOptions} 168 + label="Filter by Tag" 169 + placeholder="All Tags" 170 + /> 171 + </div> 172 + {/if} 143 173 </div> 144 174 145 175 <!-- Year Tabs (Pills) --> ··· 150 180 <Card variant="flat" padding="lg"> 151 181 {#snippet children()} 152 182 <div class="text-center"> 153 - {#if searchQuery || selectedPublication} 183 + {#if searchQuery || selectedPublication || selectedTag} 154 184 <p class="text-ink-700 dark:text-ink-300"> 155 185 No posts found matching your filters. Try adjusting your search or filters. 156 186 </p>
+2 -1
src/routes/archive/+page.ts
··· 81 81 description: value.description, 82 82 rkey, 83 83 publicationName: publication?.name, 84 - publicationRkey: publicationRkey || undefined 84 + publicationRkey: publicationRkey || undefined, 85 + tags: value.tags || undefined 85 86 }); 86 87 } 87 88 } catch (error) {