an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm

autoimported icons and readme

rimar1337 0bbbb07d 9bed298b

+21 -3
README.md
··· 1 # Red Dwarf 2 Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS. 3 4 - ![screenshot of red dwarf](/public/screenshot.png) 5 6 huge thanks to [Microcosm](https://microcosm.blue/) for making this possible 7 ··· 52 and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed 53 54 ## Tanstack Router 55 - it does the job, nothing very specific was used here 56 57 - im planning to use the loader system on select pages to prevent loss of scroll positon and state though its really complex so i havent done it yet but the migration to tanstack query is a huge first step towards this goal
··· 1 # Red Dwarf 2 Red Dwarf is a Bluesky client that does not use any AppView servers, instead it gathers the data from [Constellation](https://constellation.microcosm.blue/) and each users' PDS. 3 4 + ![screenshot of red dwarf](/public/screenshot.jpg) 5 6 huge thanks to [Microcosm](https://microcosm.blue/) for making this possible 7 ··· 52 and for list feeds, you can just use something like graze or skyfeed to input a list of users and output a custom feed 53 54 ## Tanstack Router 55 + something specific was used here 56 + 57 + so tanstack router is used as the base, but the home route is using tanstack-router-keepalive to preserve the route for better responsiveness, and it also saves scroll position of feeds into jotai (persistent) 58 + 59 + i previously used a tanstack router loader to ensure the tanstack query cache is ready to prevent scroll jumps but it is way too slow so i replaced it with tanstack-router-keepalive 60 + 61 + ## Icons 62 + this project uses Material icons. do not the light variant. sometimes i use `Mdi` if the icon needed doesnt exist in `MaterialSymbols` 63 64 + the project uses unplugin icon auto import, so you can just use the component and itll just work! 65 + 66 + the format is: 67 + ```tsx 68 + <IconMaterialSymbols{icon name here} /> 69 + // or 70 + <IconMdi{icon name here} /> 71 + ``` 72 + 73 + you can get the full list of icon names from iconify ([Material Symbols](https://icon-sets.iconify.design/material-symbols/) or [MDI](https://icon-sets.iconify.design/mdi/)) 74 + 75 + while it is nice to keep everything consistent by using material icons, if the icon you need is not provided by either material symbols nor mdi, you are allowed to just grab any icon from any pack (please do prioritize icons that fit in)
public/screenshot.jpg

This is a binary file and will not be displayed.

public/screenshot.png

This is a binary file and will not be displayed.

+12 -1
src/auto-imports.d.ts
··· 6 // biome-ignore lint: disable 7 export {} 8 declare global { 9 - 10 }
··· 6 // biome-ignore lint: disable 7 export {} 8 declare global { 9 + const IconMaterialSymbolsAccountCircle: typeof import('~icons/material-symbols/account-circle.jsx').default 10 + const IconMaterialSymbolsAccountCircleOutline: typeof import('~icons/material-symbols/account-circle-outline.jsx').default 11 + const IconMaterialSymbolsHome: typeof import('~icons/material-symbols/home.jsx').default 12 + const IconMaterialSymbolsHomeOutline: typeof import('~icons/material-symbols/home-outline.jsx').default 13 + const IconMaterialSymbolsNotifications: typeof import('~icons/material-symbols/notifications.jsx').default 14 + const IconMaterialSymbolsNotificationsOutline: typeof import('~icons/material-symbols/notifications-outline.jsx').default 15 + const IconMaterialSymbolsSearch: typeof import('~icons/material-symbols/search.jsx').default 16 + const IconMaterialSymbolsSettings: typeof import('~icons/material-symbols/settings.jsx').default 17 + const IconMaterialSymbolsSettingsOutline: typeof import('~icons/material-symbols/settings-outline.jsx').default 18 + const IconMaterialSymbolsTag: typeof import('~icons/material-symbols/tag.jsx').default 19 + const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default 20 + const IconMdiPencilOutline: typeof import('~icons/mdi/pencil-outline.jsx').default 21 }
+27 -32
src/routes/__root.tsx
··· 21 import { NotFound } from "~/components/NotFound"; 22 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 23 import { seo } from "~/utils/seo"; 24 - import IconHome from "~icons/material-symbols/home" 25 - import IconHomeOutline from "~icons/material-symbols/home-outline" 26 - import IconNotifications from "~icons/material-symbols/notifications" 27 - import IconNotificationsOutline from "~icons/material-symbols/notifications-outline" 28 - import IconSearch from "~icons/material-symbols/search" 29 - import IconSettings from "~icons/material-symbols/settings" 30 - import IconSettingsOutline from "~icons/material-symbols/settings-outline" 31 - import IconTag from "~icons/material-symbols/tag" 32 - import IconAccountCircleOutline from "~icons/mdi/account-circle-outline" 33 - import IconPencilOutline from "~icons/mdi/pencil-outline" 34 35 export const Route = createRootRouteWithContext<{ 36 queryClient: QueryClient; ··· 204 } 205 > 206 {!isHome ? ( 207 - <IconHomeOutline width={28} height={28} /> 208 ) : ( 209 - <IconHome width={28} height={28} /> 210 )} 211 <span>Home</span> 212 </Link> ··· 218 } 219 > 220 {!isNotifications ? ( 221 - <IconNotificationsOutline width={28} height={28} /> 222 ) : ( 223 - <IconNotifications width={28} height={28} /> 224 )} 225 <span>Notifications</span> 226 </Link> ··· 231 }`} 232 > 233 {location.pathname.startsWith("/feeds") ? ( 234 - <IconTag width={28} height={28} /> 235 ) : ( 236 - <IconTag width={28} height={28} /> 237 )} 238 <span>Feeds</span> 239 </Link> ··· 245 }`} 246 > 247 {location.pathname.startsWith("/search") ? ( 248 - <IconSearch width={28} height={28} /> 249 ) : ( 250 - <IconSearch width={28} height={28} /> 251 )} 252 <span>Search</span> 253 </Link> ··· 266 }} 267 type="button" 268 > 269 - <IconAccountCircleOutline width={28} height={28} /> 270 <span>Profile</span> 271 </button> 272 <Link ··· 276 }`} 277 > 278 {!location.pathname.startsWith("/settings") ? ( 279 - <IconSettingsOutline width={28} height={28} /> 280 ) : ( 281 - <IconSettings width={28} height={28} /> 282 )} 283 <span>Settings</span> 284 </Link> ··· 287 onClick={() => setPostOpen(true)} 288 type="button" 289 > 290 - <IconPencilOutline 291 width={24} 292 height={24} 293 className="text-gray-600 dark:text-gray-400" ··· 331 type="button" 332 aria-label="Create Post" 333 > 334 - <IconPencilOutline 335 width={24} 336 height={24} 337 className="text-gray-600 dark:text-gray-400" ··· 384 }`} 385 > 386 {!isHome ? ( 387 - <IconHomeOutline width={24} height={24} /> 388 ) : ( 389 - <IconHome width={24} height={24} /> 390 )} 391 <span className="text-xs mt-1">Home</span> 392 </Link> ··· 399 }`} 400 > 401 {!location.pathname.startsWith("/search") ? ( 402 - <IconSearch width={24} height={24} /> 403 ) : ( 404 - <IconSearch width={24} height={24} /> 405 )} 406 <span className="text-xs mt-1">Search</span> 407 </Link> ··· 414 }`} 415 > 416 {!isNotifications ? ( 417 - <IconNotificationsOutline width={24} height={24} /> 418 ) : ( 419 - <IconNotifications width={24} height={24} /> 420 )} 421 <span className="text-xs mt-1">Notifications</span> 422 </Link> ··· 437 }} 438 type="button" 439 > 440 - <IconAccountCircleOutline width={24} height={24} /> 441 <span className="text-xs mt-1">Profile</span> 442 </button> 443 <Link ··· 449 }`} 450 > 451 {!location.pathname.startsWith("/settings") ? ( 452 - <IconSettingsOutline width={24} height={24} /> 453 ) : ( 454 - <IconSettings width={24} height={24} /> 455 )} 456 <span className="text-xs mt-1">Settings</span> 457 </Link>
··· 21 import { NotFound } from "~/components/NotFound"; 22 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 23 import { seo } from "~/utils/seo"; 24 25 export const Route = createRootRouteWithContext<{ 26 queryClient: QueryClient; ··· 194 } 195 > 196 {!isHome ? ( 197 + <IconMaterialSymbolsHomeOutline width={28} height={28} /> 198 ) : ( 199 + <IconMaterialSymbolsHome width={28} height={28} /> 200 )} 201 <span>Home</span> 202 </Link> ··· 208 } 209 > 210 {!isNotifications ? ( 211 + <IconMaterialSymbolsNotificationsOutline width={28} height={28} /> 212 ) : ( 213 + <IconMaterialSymbolsNotifications width={28} height={28} /> 214 )} 215 <span>Notifications</span> 216 </Link> ··· 221 }`} 222 > 223 {location.pathname.startsWith("/feeds") ? ( 224 + <IconMaterialSymbolsTag width={28} height={28} /> 225 ) : ( 226 + <IconMaterialSymbolsTag width={28} height={28} /> 227 )} 228 <span>Feeds</span> 229 </Link> ··· 235 }`} 236 > 237 {location.pathname.startsWith("/search") ? ( 238 + <IconMaterialSymbolsSearch width={28} height={28} /> 239 ) : ( 240 + <IconMaterialSymbolsSearch width={28} height={28} /> 241 )} 242 <span>Search</span> 243 </Link> ··· 256 }} 257 type="button" 258 > 259 + {!isProfile ? ( 260 + <IconMaterialSymbolsAccountCircleOutline width={28} height={28} /> 261 + ) : ( 262 + <IconMaterialSymbolsAccountCircle width={28} height={28} /> 263 + ) 264 + } 265 <span>Profile</span> 266 </button> 267 <Link ··· 271 }`} 272 > 273 {!location.pathname.startsWith("/settings") ? ( 274 + <IconMaterialSymbolsSettingsOutline width={28} height={28} /> 275 ) : ( 276 + <IconMaterialSymbolsSettings width={28} height={28} /> 277 )} 278 <span>Settings</span> 279 </Link> ··· 282 onClick={() => setPostOpen(true)} 283 type="button" 284 > 285 + <IconMdiPencilOutline 286 width={24} 287 height={24} 288 className="text-gray-600 dark:text-gray-400" ··· 326 type="button" 327 aria-label="Create Post" 328 > 329 + <IconMdiPencilOutline 330 width={24} 331 height={24} 332 className="text-gray-600 dark:text-gray-400" ··· 379 }`} 380 > 381 {!isHome ? ( 382 + <IconMaterialSymbolsHomeOutline width={24} height={24} /> 383 ) : ( 384 + <IconMaterialSymbolsHome width={24} height={24} /> 385 )} 386 <span className="text-xs mt-1">Home</span> 387 </Link> ··· 394 }`} 395 > 396 {!location.pathname.startsWith("/search") ? ( 397 + <IconMaterialSymbolsSearch width={24} height={24} /> 398 ) : ( 399 + <IconMaterialSymbolsSearch width={24} height={24} /> 400 )} 401 <span className="text-xs mt-1">Search</span> 402 </Link> ··· 409 }`} 410 > 411 {!isNotifications ? ( 412 + <IconMaterialSymbolsNotificationsOutline width={24} height={24} /> 413 ) : ( 414 + <IconMaterialSymbolsNotifications width={24} height={24} /> 415 )} 416 <span className="text-xs mt-1">Notifications</span> 417 </Link> ··· 432 }} 433 type="button" 434 > 435 + <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 436 <span className="text-xs mt-1">Profile</span> 437 </button> 438 <Link ··· 444 }`} 445 > 446 {!location.pathname.startsWith("/settings") ? ( 447 + <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 448 ) : ( 449 + <IconMaterialSymbolsSettings width={24} height={24} /> 450 )} 451 <span className="text-xs mt-1">Settings</span> 452 </Link>