search and/or read your saved and liked bluesky posts
wails go svelte sqlite desktop bluesky
at main 209 lines 7.5 kB view raw
1<script lang="ts"> 2 import type { main } from "../../../wailsjs/go/models"; 3 import { formatShortDate } from "../date"; 4 import PostText from "./PostText.svelte"; 5 6 interface Props { 7 posts: main.SearchResult[]; 8 sortColumn: string; 9 sortDirection: "asc" | "desc"; 10 onSort: (column: string) => void; 11 onOpenPost: (post: main.SearchResult) => void; 12 selectedPostURI?: string | null; 13 } 14 15 let { posts, sortColumn, sortDirection, onSort, onOpenPost, selectedPostURI = null }: Props = $props(); 16 17 const columns = [ 18 { key: "author_handle", label: "Author", width: "w-36" }, 19 { key: "text", label: "Text", width: "min-w-[32rem]" }, 20 { key: "created_at", label: "Created", width: "w-36" }, 21 { key: "like_count", label: "LIKE", width: "w-20" }, 22 { key: "repost_count", label: "REPOST", width: "w-20" }, 23 { key: "reply_count", label: "REPLY", width: "w-20" }, 24 { key: "source", label: "Source", width: "w-28" }, 25 ]; 26 27 const pageSize = 12; 28 let currentPage = $state(1); 29 30 let totalPages = $derived(Math.max(1, Math.ceil(posts.length / pageSize))); 31 let paginatedPosts = $derived(posts.slice((currentPage - 1) * pageSize, currentPage * pageSize)); 32 let pageStart = $derived(posts.length === 0 ? 0 : (currentPage - 1) * pageSize + 1); 33 let pageEnd = $derived(Math.min(currentPage * pageSize, posts.length)); 34 let visiblePages = $derived.by(() => { 35 const pages: number[] = []; 36 const start = Math.max(1, currentPage - 2); 37 const end = Math.min(totalPages, currentPage + 2); 38 39 for (let page = start; page <= end; page += 1) { 40 pages.push(page); 41 } 42 43 return pages; 44 }); 45 46 $effect(() => { 47 posts; 48 currentPage = 1; 49 }); 50 51 $effect(() => { 52 if (currentPage > totalPages) { 53 currentPage = totalPages; 54 } 55 }); 56 57 function getSortIcon(column: string): string { 58 if (sortColumn !== column) return "↕"; 59 return sortDirection === "asc" ? "↑" : "↓"; 60 } 61</script> 62 63{#snippet columnLabel(label: string)} 64 {#if label === "LIKE"} 65 <span class="flex-items-center"> 66 <i class="i-ri-heart-line text-red-500"></i> 67 </span> 68 {:else if label === "REPOST"} 69 <span class="flex-items-center"> 70 <i class="i-ri-repeat-line text-blue-500"></i> 71 </span> 72 {:else if label === "REPLY"} 73 <span class="flex-items-center"> 74 <i class="i-ri-message-2-line text-green-500"></i> 75 </span> 76 {:else} 77 <span>{label}</span> 78 {/if} 79{/snippet} 80 81{#snippet sortIcon(column: string)} 82 <span class="flex items-center"> 83 {#if sortColumn !== column} 84 <i class="i-ri-arrow-up-down-line"></i> 85 {:else if sortDirection === "asc"} 86 <i class="i-ri-arrow-up-line"></i> 87 {:else} 88 <i class="i-ri-arrow-down-line"></i> 89 {/if} 90 </span> 91{/snippet} 92 93<div 94 class="border-outline bg-surface flex h-full min-h-0 flex-col overflow-hidden rounded-[1.25rem] border shadow-[0_18px_60px_rgba(0,0,0,0.35)]"> 95 <div class="min-h-0 flex-1 overflow-auto"> 96 <table class="w-full min-w-296 border-separate border-spacing-0"> 97 <thead class="sticky top-0 z-10 bg-black/95 backdrop-blur"> 98 <tr> 99 {#each columns as column} 100 <th 101 class="border-outline text-muted hover:text-bright cursor-pointer border-b px-4 py-3 text-left font-sans text-xs tracking-[0.16em] uppercase select-none {column.width}" 102 onclick={() => onSort(column.key)}> 103 <div class="flex items-center gap-1"> 104 {@render columnLabel(column.label)} 105 {@render sortIcon(column.key)} 106 </div> 107 </th> 108 {/each} 109 </tr> 110 </thead> 111 112 <tbody class="divide-outline divide-y"> 113 {#each paginatedPosts as post} 114 <tr 115 class="group cursor-pointer transition-colors {selectedPostURI === post.uri 116 ? 'bg-primary/10' 117 : 'hover:bg-black/50'}" 118 onclick={() => onOpenPost(post)}> 119 <td class="text-muted truncate px-4 py-3 font-mono text-xs"> 120 @{post.author_handle} 121 </td> 122 123 <td class="text-bright px-4 py-3 font-mono text-sm"> 124 <div class="line-clamp-2"> 125 <PostText text={post.text} facetsJson={post.facets} maxLength={120} /> 126 </div> 127 </td> 128 129 <td class="text-muted px-4 py-3 font-mono text-xs"> 130 {formatShortDate(post.created_at)} 131 </td> 132 133 <td class="text-bright px-4 py-3 text-center font-mono text-xs"> 134 {post.like_count || 0} 135 </td> 136 137 <td class="text-bright px-4 py-3 text-center font-mono text-xs"> 138 {post.repost_count || 0} 139 </td> 140 141 <td class="text-bright px-4 py-3 text-center font-mono text-xs"> 142 {post.reply_count || 0} 143 </td> 144 145 <td class="px-4 py-3"> 146 <span 147 class="rounded-full px-2 py-0.5 font-sans text-xs {post.source === 'saved' 148 ? 'bg-primary/20 text-primary' 149 : 'bg-secondary/20 text-secondary'}"> 150 {post.source} 151 </span> 152 </td> 153 </tr> 154 {:else} 155 <tr> 156 <td colspan={columns.length} class="px-4 py-12 text-center"> 157 <p class="font-sans text-muted">No posts found</p> 158 <p class="mt-2 font-mono text-xs text-[#333]">Try searching or refreshing your data</p> 159 </td> 160 </tr> 161 {/each} 162 </tbody> 163 164 {#if posts.length > 0} 165 <tfoot class="sticky bottom-0 z-10 bg-black/95 backdrop-blur"> 166 <tr> 167 <td colspan={columns.length} class="border-outline border-t px-4 py-3"> 168 <div class="flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between"> 169 <p class="text-muted font-mono text-xs tracking-[0.14em] uppercase"> 170 Showing {pageStart}-{pageEnd} of {posts.length} 171 </p> 172 173 <div class="flex flex-wrap items-center gap-2"> 174 <button 175 type="button" 176 class="border-outline text-muted hover:text-bright rounded-full border px-3 py-1.5 font-mono text-xs transition-colors disabled:opacity-40" 177 onclick={() => (currentPage = Math.max(1, currentPage - 1))} 178 disabled={currentPage === 1}> 179 Prev 180 </button> 181 182 {#each visiblePages as page} 183 <button 184 type="button" 185 class="min-w-9 rounded-full border px-3 py-1.5 font-mono text-xs transition-colors {page === 186 currentPage 187 ? 'border-primary bg-primary/15 text-primary' 188 : 'border-outline text-muted hover:text-bright'}" 189 onclick={() => (currentPage = page)}> 190 {page} 191 </button> 192 {/each} 193 194 <button 195 type="button" 196 class="border-outline text-muted hover:text-bright rounded-full border px-3 py-1.5 font-mono text-xs transition-colors disabled:opacity-40" 197 onclick={() => (currentPage = Math.min(totalPages, currentPage + 1))} 198 disabled={currentPage === totalPages}> 199 Next 200 </button> 201 </div> 202 </div> 203 </td> 204 </tr> 205 </tfoot> 206 {/if} 207 </table> 208 </div> 209</div>