an appview-less Bluesky client using Constellation and PDS Queries
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
1import { createFileRoute } from "@tanstack/react-router";
2import { useAtom } from "jotai";
3import { Slider } from "radix-ui";
4
5import { Header } from "~/components/Header";
6import Login from "~/components/Login";
7import {
8 constellationURLAtom,
9 defaultconstellationURL,
10 defaulthue,
11 defaultImgCDN,
12 defaultslingshotURL,
13 defaultVideoCDN,
14 hueAtom,
15 imgCDNAtom,
16 slingshotURLAtom,
17 videoCDNAtom,
18} from "~/utils/atoms";
19
20export const Route = createFileRoute("/settings")({
21 component: Settings,
22});
23
24export function Settings() {
25 return (
26 <>
27 <Header
28 title="Settings"
29 backButtonCallback={() => {
30 if (window.history.length > 1) {
31 window.history.back();
32 } else {
33 window.location.assign("/");
34 }
35 }}
36 />
37 <div className="lg:hidden">
38 <Login />
39 </div>
40 <div className="h-4" />
41 <TextInputSetting
42 atom={constellationURLAtom}
43 title={"Constellation"}
44 description={
45 "Customize the Constellation instance to be used by Red Dwarf"
46 }
47 init={defaultconstellationURL}
48 />
49 <TextInputSetting
50 atom={slingshotURLAtom}
51 title={"Slingshot"}
52 description={"Customize the Slingshot instance to be used by Red Dwarf"}
53 init={defaultslingshotURL}
54 />
55 <TextInputSetting
56 atom={imgCDNAtom}
57 title={"Image CDN"}
58 description={
59 "Customize the Constellation instance to be used by Red Dwarf"
60 }
61 init={defaultImgCDN}
62 />
63 <TextInputSetting
64 atom={videoCDNAtom}
65 title={"Video CDN"}
66 description={"Customize the Slingshot instance to be used by Red Dwarf"}
67 init={defaultVideoCDN}
68 />
69
70 <Hue />
71 <p className="text-gray-500 dark:text-gray-400 py-4 px-6 text-sm">
72 please restart/refresh the app if changes arent applying correctly
73 </p>
74 </>
75 );
76}
77function Hue() {
78 const [hue, setHue] = useAtom(hueAtom);
79 return (
80 <div className="flex flex-col px-4 mt-4 ">
81 <span className="z-10">Hue</span>
82 <div className="flex flex-row items-center gap-4">
83 <SliderComponent
84 atom={hueAtom}
85 max={360}
86 />
87 <button
88 onClick={() => setHue(defaulthue ?? 28)}
89 className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
90 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
91 >
92 Reset
93 </button>
94 </div>
95 </div>
96 );
97}
98
99export function TextInputSetting({
100 atom,
101 title,
102 description,
103 init,
104}: {
105 atom: typeof constellationURLAtom;
106 title?: string;
107 description?: string;
108 init?: string;
109}) {
110 const [value, setValue] = useAtom(atom);
111 return (
112 <div className="flex flex-col gap-2 px-4 py-2">
113 {/* <div>
114 {title && (
115 <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100">
116 {title}
117 </h3>
118 )}
119 {description && (
120 <p className="text-sm text-gray-500 dark:text-gray-400">
121 {description}
122 </p>
123 )}
124 </div> */}
125
126 <div className="flex flex-row gap-2 items-center">
127 <div className="m3input-field m3input-label m3input-border size-md flex-1">
128 <input
129 type="text"
130 placeholder=" "
131 value={value}
132 onChange={(e) => setValue(e.target.value)}
133 />
134 <label>{title}</label>
135 </div>
136 {/* <input
137 type="text"
138 value={value}
139 onChange={(e) => setValue(e.target.value)}
140 className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700
141 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400
142 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600"
143 placeholder="Enter value..."
144 /> */}
145 <button
146 onClick={() => setValue(init ?? "")}
147 className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800
148 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition"
149 >
150 Reset
151 </button>
152 </div>
153 </div>
154 );
155}
156
157
158interface SliderProps {
159 atom: typeof hueAtom;
160 min?: number;
161 max?: number;
162 step?: number;
163}
164
165export const SliderComponent: React.FC<SliderProps> = ({
166 atom,
167 min = 0,
168 max = 100,
169 step = 1,
170}) => {
171
172 const [value, setValue] = useAtom(atom)
173
174 return (
175 <Slider.Root
176 className="relative flex items-center w-full h-4"
177 value={[value]}
178 min={min}
179 max={max}
180 step={step}
181 onValueChange={(v: number[]) => setValue(v[0])}
182 >
183 <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full">
184 <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" />
185 </Slider.Track>
186 <Slider.Thumb className="shadow-[0_0_0_8px_var(--color-white)] dark:shadow-[0_0_0_8px_var(--color-gray-950)] block w-[3px] h-12 bg-gray-500 dark:bg-gray-400 rounded-md focus:outline-none" />
187 </Slider.Root>
188 );
189};