search and/or read your saved and liked bluesky posts
wails
go
svelte
sqlite
desktop
bluesky
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>