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 1 # Red Dwarf 2 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 3 4 - ![screenshot of red dwarf](/public/screenshot.png) 4 + ![screenshot of red dwarf](/public/screenshot.jpg) 5 5 6 6 huge thanks to [Microcosm](https://microcosm.blue/) for making this possible 7 7 ··· 52 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 53 54 54 ## Tanstack Router 55 - it does the job, nothing very specific was used here 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` 56 63 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 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 6 // biome-ignore lint: disable 7 7 export {} 8 8 declare global { 9 - 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 10 21 }
+27 -32
src/routes/__root.tsx
··· 21 21 import { NotFound } from "~/components/NotFound"; 22 22 import { UnifiedAuthProvider, useAuth } from "~/providers/UnifiedAuthProvider"; 23 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 24 35 25 export const Route = createRootRouteWithContext<{ 36 26 queryClient: QueryClient; ··· 204 194 } 205 195 > 206 196 {!isHome ? ( 207 - <IconHomeOutline width={28} height={28} /> 197 + <IconMaterialSymbolsHomeOutline width={28} height={28} /> 208 198 ) : ( 209 - <IconHome width={28} height={28} /> 199 + <IconMaterialSymbolsHome width={28} height={28} /> 210 200 )} 211 201 <span>Home</span> 212 202 </Link> ··· 218 208 } 219 209 > 220 210 {!isNotifications ? ( 221 - <IconNotificationsOutline width={28} height={28} /> 211 + <IconMaterialSymbolsNotificationsOutline width={28} height={28} /> 222 212 ) : ( 223 - <IconNotifications width={28} height={28} /> 213 + <IconMaterialSymbolsNotifications width={28} height={28} /> 224 214 )} 225 215 <span>Notifications</span> 226 216 </Link> ··· 231 221 }`} 232 222 > 233 223 {location.pathname.startsWith("/feeds") ? ( 234 - <IconTag width={28} height={28} /> 224 + <IconMaterialSymbolsTag width={28} height={28} /> 235 225 ) : ( 236 - <IconTag width={28} height={28} /> 226 + <IconMaterialSymbolsTag width={28} height={28} /> 237 227 )} 238 228 <span>Feeds</span> 239 229 </Link> ··· 245 235 }`} 246 236 > 247 237 {location.pathname.startsWith("/search") ? ( 248 - <IconSearch width={28} height={28} /> 238 + <IconMaterialSymbolsSearch width={28} height={28} /> 249 239 ) : ( 250 - <IconSearch width={28} height={28} /> 240 + <IconMaterialSymbolsSearch width={28} height={28} /> 251 241 )} 252 242 <span>Search</span> 253 243 </Link> ··· 266 256 }} 267 257 type="button" 268 258 > 269 - <IconAccountCircleOutline width={28} height={28} /> 259 + {!isProfile ? ( 260 + <IconMaterialSymbolsAccountCircleOutline width={28} height={28} /> 261 + ) : ( 262 + <IconMaterialSymbolsAccountCircle width={28} height={28} /> 263 + ) 264 + } 270 265 <span>Profile</span> 271 266 </button> 272 267 <Link ··· 276 271 }`} 277 272 > 278 273 {!location.pathname.startsWith("/settings") ? ( 279 - <IconSettingsOutline width={28} height={28} /> 274 + <IconMaterialSymbolsSettingsOutline width={28} height={28} /> 280 275 ) : ( 281 - <IconSettings width={28} height={28} /> 276 + <IconMaterialSymbolsSettings width={28} height={28} /> 282 277 )} 283 278 <span>Settings</span> 284 279 </Link> ··· 287 282 onClick={() => setPostOpen(true)} 288 283 type="button" 289 284 > 290 - <IconPencilOutline 285 + <IconMdiPencilOutline 291 286 width={24} 292 287 height={24} 293 288 className="text-gray-600 dark:text-gray-400" ··· 331 326 type="button" 332 327 aria-label="Create Post" 333 328 > 334 - <IconPencilOutline 329 + <IconMdiPencilOutline 335 330 width={24} 336 331 height={24} 337 332 className="text-gray-600 dark:text-gray-400" ··· 384 379 }`} 385 380 > 386 381 {!isHome ? ( 387 - <IconHomeOutline width={24} height={24} /> 382 + <IconMaterialSymbolsHomeOutline width={24} height={24} /> 388 383 ) : ( 389 - <IconHome width={24} height={24} /> 384 + <IconMaterialSymbolsHome width={24} height={24} /> 390 385 )} 391 386 <span className="text-xs mt-1">Home</span> 392 387 </Link> ··· 399 394 }`} 400 395 > 401 396 {!location.pathname.startsWith("/search") ? ( 402 - <IconSearch width={24} height={24} /> 397 + <IconMaterialSymbolsSearch width={24} height={24} /> 403 398 ) : ( 404 - <IconSearch width={24} height={24} /> 399 + <IconMaterialSymbolsSearch width={24} height={24} /> 405 400 )} 406 401 <span className="text-xs mt-1">Search</span> 407 402 </Link> ··· 414 409 }`} 415 410 > 416 411 {!isNotifications ? ( 417 - <IconNotificationsOutline width={24} height={24} /> 412 + <IconMaterialSymbolsNotificationsOutline width={24} height={24} /> 418 413 ) : ( 419 - <IconNotifications width={24} height={24} /> 414 + <IconMaterialSymbolsNotifications width={24} height={24} /> 420 415 )} 421 416 <span className="text-xs mt-1">Notifications</span> 422 417 </Link> ··· 437 432 }} 438 433 type="button" 439 434 > 440 - <IconAccountCircleOutline width={24} height={24} /> 435 + <IconMaterialSymbolsAccountCircleOutline width={24} height={24} /> 441 436 <span className="text-xs mt-1">Profile</span> 442 437 </button> 443 438 <Link ··· 449 444 }`} 450 445 > 451 446 {!location.pathname.startsWith("/settings") ? ( 452 - <IconSettingsOutline width={24} height={24} /> 447 + <IconMaterialSymbolsSettingsOutline width={24} height={24} /> 453 448 ) : ( 454 - <IconSettings width={24} height={24} /> 449 + <IconMaterialSymbolsSettings width={24} height={24} /> 455 450 )} 456 451 <span className="text-xs mt-1">Settings</span> 457 452 </Link>