+21
-3
README.md
+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
-

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
+

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
public/screenshot.jpg
This is a binary file and will not be displayed.
public/screenshot.png
public/screenshot.png
This is a binary file and will not be displayed.
+12
-1
src/auto-imports.d.ts
+12
-1
src/auto-imports.d.ts
···
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
+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>