an appview-less Bluesky client using Constellation and PDS Queries reddwarf.app
frontend spa bluesky reddwarf microcosm
at main 10 kB view raw
1import { createFileRoute, useNavigate } from "@tanstack/react-router"; 2import { useAtom, useAtomValue, useSetAtom } from "jotai"; 3import { Slider, Switch } from "radix-ui"; 4import { useEffect, useState } from "react"; 5 6import { Header } from "~/components/Header"; 7import Login from "~/components/Login"; 8import { 9 constellationURLAtom, 10 defaultconstellationURL, 11 defaulthue, 12 defaultImgCDN, 13 defaultLycanURL, 14 defaultslingshotURL, 15 defaultVideoCDN, 16 enableBitesAtom, 17 enableBridgyTextAtom, 18 enableWafrnTextAtom, 19 hueAtom, 20 imgCDNAtom, 21 lycanURLAtom, 22 slingshotURLAtom, 23 videoCDNAtom, 24} from "~/utils/atoms"; 25 26import { MaterialNavItem } from "./__root"; 27 28export const Route = createFileRoute("/settings")({ 29 component: Settings, 30}); 31 32export function Settings() { 33 const navigate = useNavigate(); 34 return ( 35 <> 36 <Header 37 title="Settings" 38 backButtonCallback={() => { 39 if (window.history.length > 1) { 40 window.history.back(); 41 } else { 42 window.location.assign("/"); 43 } 44 }} 45 /> 46 <div className="lg:hidden"> 47 <Login /> 48 </div> 49 <div className="sm:hidden flex flex-col justify-around mt-4"> 50 <SettingHeading title="Other Pages" top /> 51 <MaterialNavItem 52 InactiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 53 ActiveIcon={<IconMaterialSymbolsTag className="w-6 h-6" />} 54 active={false} 55 onClickCallbback={() => 56 navigate({ 57 to: "/feeds", 58 //params: { did: agent.assertDid }, 59 }) 60 } 61 text="Feeds" 62 /> 63 <MaterialNavItem 64 InactiveIcon={<IconMdiShieldOutline className="w-6 h-6" />} 65 ActiveIcon={<IconMdiShield className="w-6 h-6" />} 66 active={false} 67 onClickCallbback={() => 68 navigate({ 69 to: "/moderation", 70 //params: { did: agent.assertDid }, 71 }) 72 } 73 text="Moderation" 74 /> 75 </div> 76 <div className="h-4" /> 77 78 <SettingHeading title="Personalization" top /> 79 <Hue /> 80 81 <SettingHeading title="Network Configuration" /> 82 <div className="flex flex-col px-4 pb-2"> 83 <span className="text-md">Service Endpoints</span> 84 <span className="text-sm text-gray-500 dark:text-gray-400"> 85 Customize the servers to be used by the app 86 </span> 87 </div> 88 <TextInputSetting 89 atom={constellationURLAtom} 90 title={"Constellation"} 91 description={ 92 "Customize the Constellation instance to be used by Red Dwarf" 93 } 94 init={defaultconstellationURL} 95 /> 96 <TextInputSetting 97 atom={slingshotURLAtom} 98 title={"Slingshot"} 99 description={"Customize the Slingshot instance to be used by Red Dwarf"} 100 init={defaultslingshotURL} 101 /> 102 <TextInputSetting 103 atom={imgCDNAtom} 104 title={"Image CDN"} 105 description={ 106 "Customize the Constellation instance to be used by Red Dwarf" 107 } 108 init={defaultImgCDN} 109 /> 110 <TextInputSetting 111 atom={videoCDNAtom} 112 title={"Video CDN"} 113 description={"Customize the Slingshot instance to be used by Red Dwarf"} 114 init={defaultVideoCDN} 115 /> 116 <TextInputSetting 117 atom={lycanURLAtom} 118 title={"Lycan Search"} 119 description={"Enable text search across posts you've interacted with"} 120 init={defaultLycanURL} 121 /> 122 123 <SettingHeading title="Experimental" /> 124 <SwitchSetting 125 atom={enableBitesAtom} 126 title={"Bites"} 127 description={"Enable Wafrn Bites to bite and be bitten by other people"} 128 //init={false} 129 /> 130 <div className="h-4" /> 131 <SwitchSetting 132 atom={enableBridgyTextAtom} 133 title={"Bridgy Text"} 134 description={ 135 "Show the original text of posts bridged from the Fediverse" 136 } 137 //init={false} 138 /> 139 <div className="h-4" /> 140 <SwitchSetting 141 atom={enableWafrnTextAtom} 142 title={"Wafrn Text"} 143 description={"Show the original text of posts from Wafrn instances"} 144 //init={false} 145 /> 146 <p className="text-gray-500 dark:text-gray-400 py-4 px-4 text-sm border rounded-xl mx-4 mt-8 mb-4"> 147 Notice: Please restart/refresh the app if changes arent applying 148 correctly 149 </p> 150 </> 151 ); 152} 153 154export function SettingHeading({ 155 title, 156 top, 157}: { 158 title: string; 159 top?: boolean; 160}) { 161 return ( 162 <div 163 className="px-4" 164 style={{ marginTop: top ? 0 : 18, paddingBottom: 12 }} 165 > 166 <span className=" text-sm font-medium text-gray-500 dark:text-gray-400"> 167 {title} 168 </span> 169 </div> 170 ); 171} 172 173export function SwitchSetting({ 174 atom, 175 title, 176 description, 177}: { 178 atom: typeof enableBitesAtom; 179 title?: string; 180 description?: string; 181}) { 182 const value = useAtomValue(atom); 183 const setValue = useSetAtom(atom); 184 185 const [hydrated, setHydrated] = useState(false); 186 // eslint-disable-next-line react-hooks/set-state-in-effect 187 useEffect(() => setHydrated(true), []); 188 189 if (!hydrated) { 190 // Avoid rendering Switch until we know storage is loaded 191 return null; 192 } 193 194 return ( 195 <div className="flex items-center gap-4 px-4 "> 196 <label htmlFor={`switch-${title}`} className="flex flex-row flex-1"> 197 <div className="flex flex-col"> 198 <span className="text-md">{title}</span> 199 <span className="text-sm text-gray-500 dark:text-gray-400"> 200 {description} 201 </span> 202 </div> 203 </label> 204 205 <Switch.Root 206 id={`switch-${title}`} 207 checked={value} 208 onCheckedChange={(v) => setValue(v)} 209 className="m3switch root" 210 > 211 <Switch.Thumb className="m3switch thumb " /> 212 </Switch.Root> 213 </div> 214 ); 215} 216 217function Hue() { 218 const [hue, setHue] = useAtom(hueAtom); 219 return ( 220 <div className="flex flex-col px-4"> 221 <span className="z-[2] text-md">Hue</span> 222 <span className="z-[2] text-sm text-gray-500 dark:text-gray-400"> 223 Change the colors of the app 224 </span> 225 <div className="z-[1] flex flex-row items-center gap-4"> 226 <SliderComponent atom={hueAtom} max={360} /> 227 <button 228 onClick={() => setHue(defaulthue ?? 28)} 229 className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 230 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 231 > 232 Reset 233 </button> 234 </div> 235 </div> 236 ); 237} 238 239export function TextInputSetting({ 240 atom, 241 title, 242 description, 243 init, 244}: { 245 atom: typeof constellationURLAtom; 246 title?: string; 247 description?: string; 248 init?: string; 249}) { 250 const [value, setValue] = useAtom(atom); 251 return ( 252 <div className="flex flex-col gap-2 px-4 py-2"> 253 {/* <div> 254 {title && ( 255 <h3 className="text-sm font-medium text-gray-900 dark:text-gray-100"> 256 {title} 257 </h3> 258 )} 259 {description && ( 260 <p className="text-sm text-gray-500 dark:text-gray-400"> 261 {description} 262 </p> 263 )} 264 </div> */} 265 266 <div className="flex flex-row gap-2 items-center"> 267 <div className="m3input-field m3input-label m3input-border size-md flex-1"> 268 <input 269 type="text" 270 placeholder=" " 271 value={value} 272 onChange={(e) => setValue(e.target.value)} 273 /> 274 <label>{title}</label> 275 </div> 276 {/* <input 277 type="text" 278 value={value} 279 onChange={(e) => setValue(e.target.value)} 280 className="flex-1 px-3 py-2 rounded-lg bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 281 text-gray-900 dark:text-gray-100 placeholder:text-gray-500 dark:placeholder:text-gray-400 282 focus:outline-none focus:ring-2 focus:ring-gray-400 dark:focus:ring-gray-600" 283 placeholder="Enter value..." 284 /> */} 285 <button 286 onClick={() => setValue(init ?? "")} 287 className="px-6 py-2 h-12 rounded-full bg-gray-100 dark:bg-gray-800 288 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-700 transition" 289 > 290 Reset 291 </button> 292 </div> 293 </div> 294 ); 295} 296 297interface SliderProps { 298 atom: typeof hueAtom; 299 min?: number; 300 max?: number; 301 step?: number; 302} 303 304export const SliderComponent: React.FC<SliderProps> = ({ 305 atom, 306 min = 0, 307 max = 100, 308 step = 1, 309}) => { 310 const [value, setValue] = useAtom(atom); 311 312 return ( 313 <Slider.Root 314 className="relative flex items-center w-full h-4" 315 value={[value]} 316 min={min} 317 max={max} 318 step={step} 319 onValueChange={(v: number[]) => setValue(v[0])} 320 > 321 <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 322 <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 323 </Slider.Track> 324 <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" /> 325 </Slider.Root> 326 ); 327}; 328 329 330interface SliderPProps { 331 value: number; 332 min?: number; 333 max?: number; 334 step?: number; 335} 336 337 338export const SliderPrimitive: React.FC<SliderPProps> = ({ 339 value, 340 min = 0, 341 max = 100, 342 step = 1, 343}) => { 344 345 return ( 346 <Slider.Root 347 className="relative flex items-center w-full h-4" 348 value={[value]} 349 min={min} 350 max={max} 351 step={step} 352 onValueChange={(v: number[]) => {}} 353 > 354 <Slider.Track className="relative flex-grow h-4 bg-gray-300 dark:bg-gray-700 rounded-full"> 355 <Slider.Range className="absolute h-full bg-gray-500 dark:bg-gray-400 rounded-l-full rounded-r-none" /> 356 </Slider.Track> 357 <Slider.Thumb className=" hidden 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" /> 358 </Slider.Root> 359 ); 360};