BlueSky & more on desktop lazurite.stormlightlabs.org/
tauri rust typescript bluesky appview atproto solid
2
fork

Configure Feed

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

at main 210 lines 8.0 kB view raw
1import { Icon } from "$/components/shared/Icon"; 2import { Match, Show, Switch } from "solid-js"; 3import type { EmptyStateReason } from "./types"; 4 5type SearchEmptyStateScope = "local" | "network" | "profiles"; 6 7type SearchEmptyStateProps = { reason: EmptyStateReason | "no-sync"; scope?: SearchEmptyStateScope }; 8 9export function SearchEmptyState(props: SearchEmptyStateProps) { 10 return ( 11 <div class="text-center"> 12 <EmptyStateVisual reason={props.reason} /> 13 <EmptyStateContent reason={props.reason} scope={props.scope ?? "local"} /> 14 </div> 15 ); 16} 17 18function EmptyStateVisual(props: { reason: EmptyStateReason | "no-sync" }) { 19 return ( 20 <Show when={props.reason === "no-sync"} fallback={<EmptyStateIcon />}> 21 <NoSyncIllustration /> 22 </Show> 23 ); 24} 25 26function EmptyStateIcon() { 27 return ( 28 <div class="mb-4 inline-flex h-16 w-16 items-center justify-center rounded-full bg-white/5 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.04)]"> 29 <Icon kind="search" class="text-3xl text-on-surface-variant" /> 30 </div> 31 ); 32} 33 34function NoSyncIllustration() { 35 return ( 36 <div 37 data-testid="no-sync-illustration" 38 class="relative mx-auto mb-6 h-40 w-full max-w-xs overflow-hidden rounded-4xl bg-white/2.5 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.04)]"> 39 <div class="absolute inset-x-6 top-5 h-16 rounded-[1.25rem] bg-primary/10 blur-2xl" /> 40 <div class="absolute left-5 top-7 w-26 rounded-[1.4rem] bg-surface-container p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.04)]"> 41 <div class="mb-2 flex items-center gap-2"> 42 <span class="flex h-8 w-8 items-center justify-center rounded-xl bg-primary/14 text-primary"> 43 <Icon kind="bookmark" class="text-base" /> 44 </span> 45 <span class="h-2.5 w-12 rounded-full bg-white/8" /> 46 </div> 47 <div class="grid gap-1.5"> 48 <span class="h-2 rounded-full bg-white/7" /> 49 <span class="h-2 w-4/5 rounded-full bg-white/5" /> 50 </div> 51 </div> 52 53 <div class="absolute right-5 top-10 w-28 rounded-[1.4rem] bg-surface-container-high p-3 shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)]"> 54 <div class="mb-3 flex items-center justify-between"> 55 <span class="flex h-8 w-8 items-center justify-center rounded-xl bg-white/8 text-on-surface-variant"> 56 <Icon kind="db" class="text-base" /> 57 </span> 58 <span class="rounded-full bg-white/8 px-2 py-0.5 text-[0.62rem] uppercase tracking-[0.12em] text-on-surface-variant"> 59 local 60 </span> 61 </div> 62 <div class="grid gap-1.5"> 63 <span class="h-2 rounded-full bg-white/7" /> 64 <span class="h-2 w-3/4 rounded-full bg-primary/18" /> 65 <span class="h-2 w-2/3 rounded-full bg-white/5" /> 66 </div> 67 </div> 68 69 <div class="absolute bottom-4 left-1/2 flex -translate-x-1/2 items-center gap-2 rounded-full bg-black/35 px-3 py-1.5 text-[0.68rem] text-on-surface-variant shadow-[inset_0_0_0_1px_rgba(255,255,255,0.05)]"> 70 <Icon kind="refresh" class="text-primary" /> 71 <span>Run a sync to fill local search</span> 72 </div> 73 </div> 74 ); 75} 76 77function EmptyStateContent(props: { reason: EmptyStateReason | "no-sync"; scope: SearchEmptyStateScope }) { 78 return ( 79 <Switch> 80 <Match when={props.reason === "initial"}> 81 <InitialContent scope={props.scope} /> 82 </Match> 83 84 <Match when={props.reason === "no-results"}> 85 <NoResultsContent scope={props.scope} /> 86 </Match> 87 88 <Match when={props.reason === "no-sync"}> 89 <NoSyncContent /> 90 </Match> 91 92 <Match when={props.reason === "error"}> 93 <ErrorContent scope={props.scope} /> 94 </Match> 95 </Switch> 96 ); 97} 98 99function InitialContent(props: { scope: SearchEmptyStateScope }) { 100 return ( 101 <> 102 <Switch> 103 <Match when={props.scope === "profiles"}> 104 <h3 class="mb-1 text-base font-medium text-on-surface">Search people across Bluesky</h3> 105 <p class="m-0 text-sm text-on-surface-variant"> 106 Type a handle or display name above to find profiles and jump directly into their profile view. 107 </p> 108 </Match> 109 <Match when={props.scope === "network"}> 110 <h3 class="mb-1 text-base font-medium text-on-surface">Search public posts across the network</h3> 111 <p class="m-0 text-sm text-on-surface-variant"> 112 Type a query above to search Bluesky directly without relying on your local index. 113 </p> 114 </Match> 115 <Match when={props.scope === "local"}> 116 <h3 class="mb-1 text-base font-medium text-on-surface">Search your saved & liked posts</h3> 117 <p class="m-0 text-sm text-on-surface-variant"> 118 Type a query above to search through the posts you liked or bookmarked. 119 </p> 120 </Match> 121 </Switch> 122 <KeyboardShortcuts /> 123 </> 124 ); 125} 126 127function KeyboardShortcuts() { 128 return ( 129 <div class="my-4 space-y-2 flex items-center justify-center flex-col text-xs text-on-surface-variant/60"> 130 <div class="flex items-center gap-2"> 131 <kbd class="rounded bg-white/10 px-1.5 py-0.5">/</kbd> 132 Focus search from anywhere 133 </div> 134 <div class="flex items-center gap-2"> 135 <kbd class="rounded bg-white/10 px-1.5 py-0.5">Tab</kbd> 136 Cycle search modes 137 </div> 138 <div class="flex items-center gap-2"> 139 <kbd class="rounded bg-white/10 px-1.5 py-0.5"></kbd> 140 Navigate profile suggestions 141 </div> 142 </div> 143 ); 144} 145 146function NoResultsContent(props: { scope: SearchEmptyStateScope }) { 147 return ( 148 <> 149 <h3 class="mb-1 text-base font-medium text-on-surface"> 150 {props.scope === "profiles" ? "No profiles found" : "No results found"} 151 </h3> 152 <Switch> 153 <Match when={props.scope === "profiles"}> 154 <p class="m-0 text-sm text-on-surface-variant"> 155 Try a broader handle fragment, a display name, or select one of the suggested profiles as you type. 156 </p> 157 </Match> 158 <Match when={props.scope === "network"}> 159 <p class="m-0 text-sm text-on-surface-variant"> 160 Try a broader query or switch to local search if you want to search your synced posts instead. 161 </p> 162 </Match> 163 <Match when={props.scope === "local"}> 164 <p class="m-0 text-sm text-on-surface-variant"> 165 Try adjusting your search terms or switch to a different search mode. 166 </p> 167 </Match> 168 </Switch> 169 </> 170 ); 171} 172 173function NoSyncContent() { 174 return ( 175 <> 176 <h3 class="mb-1 text-base font-medium text-on-surface">No posts synced yet</h3> 177 <p class="m-0 text-sm text-on-surface-variant"> 178 Sync your liked and bookmarked posts to build the local index for keyword search now, then optionally unlock 179 semantic search later. 180 </p> 181 </> 182 ); 183} 184 185function ErrorContent(props: { scope: SearchEmptyStateScope }) { 186 return ( 187 <> 188 <h3 class="mb-1 text-base font-medium text-on-surface"> 189 {props.scope === "profiles" ? "Profile search failed" : "Search failed"} 190 </h3> 191 <Switch> 192 <Match when={props.scope === "profiles"}> 193 <p class="m-0 text-sm text-on-surface-variant"> 194 The profile lookup did not complete. Retry the query or open a suggested profile if it appears. 195 </p> 196 </Match> 197 <Match when={props.scope === "network"}> 198 <p class="m-0 text-sm text-on-surface-variant"> 199 The network request did not complete. Retry the query or switch to local search while the network recovers. 200 </p> 201 </Match> 202 <Match when={props.scope === "local"}> 203 <p class="m-0 text-sm text-on-surface-variant"> 204 The local index request did not complete. Retry the query or sync again if your index is stale. 205 </p> 206 </Match> 207 </Switch> 208 </> 209 ); 210}