an independent Bluesky client using Constellation, PDS Queries, and other services reddwarf.app
frontend spa bluesky reddwarf microcosm client app
99
fork

Configure Feed

Select the types of activity you want to include in your feed.

button

rimar1337 665adb3f 79f5809f

+238 -131
+164 -117
src/components/Composer.tsx
··· 8 8 import { useQueryPost } from "~/utils/useQuery"; 9 9 10 10 import { ProfileThing } from "./Login"; 11 + import { Button } from "./radix-m3-rd/Button"; 11 12 import { UniversalPostRendererATURILoader } from "./UniversalPostRenderer"; 12 13 13 14 const MAX_POST_LENGTH = 300; 14 15 15 16 export function Composer() { 16 17 const [composerState, setComposerState] = useAtom(composerAtom); 18 + const [closeConfirmState, setCloseConfirmState] = useState<boolean>(false); 17 19 const { agent } = useAuth(); 18 20 19 21 const [postText, setPostText] = useState(""); ··· 112 114 setPosting(false); 113 115 } 114 116 } 115 - // if (composerState.kind === "closed") { 116 - // return null; 117 - // } 118 117 119 118 const getPlaceholder = () => { 120 119 switch (composerState.kind) { ··· 132 131 const isPostButtonDisabled = 133 132 posting || !postText.trim() || isParentLoading || charsLeft < 0; 134 133 134 + function handleAttemptClose() { 135 + if (postText.trim() && !posting) { 136 + setCloseConfirmState(true); 137 + } else { 138 + setComposerState({ kind: "closed" }); 139 + } 140 + } 141 + 142 + function handleConfirmClose() { 143 + setComposerState({ kind: "closed" }); 144 + setCloseConfirmState(false); 145 + setPostText(""); 146 + } 147 + 135 148 return ( 136 - <Dialog.Root 137 - open={composerState.kind !== "closed"} 138 - onOpenChange={(open) => { 139 - if (!open) setComposerState({ kind: "closed" }); 140 - }} 141 - > 142 - <Dialog.Portal> 143 - <Dialog.Overlay className="fixed disablegutter inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 149 + <> 150 + <Dialog.Root 151 + open={composerState.kind !== "closed"} 152 + onOpenChange={(open) => { 153 + if (!open) handleAttemptClose(); 154 + }} 155 + > 156 + <Dialog.Portal> 157 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 144 158 145 - <Dialog.Content className="fixed gutter overflow-y-scroll inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]"> 146 - <div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4"> 147 - <div className="flex flex-row justify-between p-2"> 148 - <Dialog.Close asChild> 149 - <button 150 - className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800" 151 - disabled={posting} 152 - aria-label="Close" 153 - > 154 - <svg 155 - xmlns="http://www.w3.org/2000/svg" 156 - width="20" 157 - height="20" 158 - viewBox="0 0 24 24" 159 - fill="none" 160 - stroke="currentColor" 161 - strokeWidth="2.5" 162 - strokeLinecap="round" 163 - strokeLinejoin="round" 159 + <Dialog.Content className="fixed overflow-y-auto gutter inset-0 z-50 flex items-start justify-center pt-10 sm:pt-20 pb-[50dvh] sm:pb-[50dvh]"> 160 + <div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-xl relative mx-4"> 161 + <div className="flex flex-row justify-between p-2"> 162 + <Dialog.Close asChild> 163 + <button 164 + className="h-8 w-8 flex items-center justify-center rounded-full text-gray-600 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800" 165 + disabled={posting} 166 + aria-label="Close" 167 + onClick={handleAttemptClose} 164 168 > 165 - <line x1="18" y1="6" x2="6" y2="18"></line> 166 - <line x1="6" y1="6" x2="18" y2="18"></line> 167 - </svg> 168 - </button> 169 - </Dialog.Close> 169 + <svg 170 + xmlns="http://www.w3.org/2000/svg" 171 + width="20" 172 + height="20" 173 + viewBox="0 0 24 24" 174 + fill="none" 175 + stroke="currentColor" 176 + strokeWidth="2.5" 177 + strokeLinecap="round" 178 + strokeLinejoin="round" 179 + > 180 + <line x1="18" y1="6" x2="6" y2="18"></line> 181 + <line x1="6" y1="6" x2="18" y2="18"></line> 182 + </svg> 183 + </button> 184 + </Dialog.Close> 170 185 171 - <div className="flex-1" /> 172 - <div className="flex items-center gap-4"> 173 - <span 174 - className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 175 - > 176 - {charsLeft} 177 - </span> 178 - <button 179 - className="bg-gray-600 hover:bg-gray-700 text-white font-bold py-1 px-4 rounded-full disabled:opacity-50 disabled:cursor-not-allowed transition-colors" 180 - onClick={handlePost} 181 - disabled={isPostButtonDisabled} 182 - > 183 - {posting ? "Posting..." : "Post"} 184 - </button> 186 + <div className="flex-1" /> 187 + <div className="flex items-center gap-4"> 188 + <span 189 + className={`text-sm ${charsLeft < 0 ? "text-red-500" : "text-gray-500"}`} 190 + > 191 + {charsLeft} 192 + </span> 193 + <Button 194 + onClick={handlePost} 195 + disabled={isPostButtonDisabled} 196 + > 197 + {posting ? "Posting..." : "Post"} 198 + </Button> 199 + </div> 185 200 </div> 186 - </div> 201 + 202 + {postSuccess ? ( 203 + <div className="flex flex-col items-center justify-center py-16"> 204 + <span className="text-gray-500 text-6xl mb-4">✓</span> 205 + <span className="text-xl font-bold text-black dark:text-white"> 206 + Posted! 207 + </span> 208 + </div> 209 + ) : ( 210 + <div className="px-4"> 211 + {composerState.kind === "reply" && ( 212 + <div className="mb-1 -mx-4"> 213 + {isParentLoading ? ( 214 + <div className="text-sm text-gray-500 animate-pulse"> 215 + Loading parent post... 216 + </div> 217 + ) : parentUri ? ( 218 + <UniversalPostRendererATURILoader 219 + atUri={parentUri} 220 + bottomReplyLine 221 + bottomBorder={false} 222 + /> 223 + ) : ( 224 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 225 + Could not load parent post. 226 + </div> 227 + )} 228 + </div> 229 + )} 187 230 188 - {postSuccess ? ( 189 - <div className="flex flex-col items-center justify-center py-16"> 190 - <span className="text-gray-500 text-6xl mb-4">✓</span> 191 - <span className="text-xl font-bold text-black dark:text-white"> 192 - Posted! 193 - </span> 194 - </div> 195 - ) : ( 196 - <div className="px-4"> 197 - {composerState.kind === "reply" && ( 198 - <div className="mb-1 -mx-4"> 199 - {isParentLoading ? ( 200 - <div className="text-sm text-gray-500 animate-pulse"> 201 - Loading parent post... 202 - </div> 203 - ) : parentUri ? ( 204 - <UniversalPostRendererATURILoader 205 - atUri={parentUri} 206 - bottomReplyLine 207 - bottomBorder={false} 231 + <div className="flex w-full gap-1 flex-col"> 232 + <ProfileThing agent={agent} large /> 233 + <div className="flex pl-[50px]"> 234 + <AutoGrowTextarea 235 + className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 236 + rows={5} 237 + placeholder={getPlaceholder()} 238 + value={postText} 239 + onChange={(e) => setPostText(e.target.value)} 240 + disabled={posting} 241 + autoFocus 208 242 /> 209 - ) : ( 210 - <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 211 - Could not load parent post. 212 - </div> 213 - )} 243 + </div> 214 244 </div> 215 - )} 216 245 217 - <div className="flex w-full gap-1 flex-col"> 218 - <ProfileThing agent={agent} large /> 219 - <div className="flex pl-[50px]"> 220 - <AutoGrowTextarea 221 - className="w-full text-lg bg-transparent focus:outline-none resize-none placeholder:text-gray-500 text-black dark:text-white pb-2" 222 - rows={5} 223 - placeholder={getPlaceholder()} 224 - value={postText} 225 - onChange={(e) => setPostText(e.target.value)} 226 - disabled={posting} 227 - autoFocus 228 - /> 229 - </div> 246 + {composerState.kind === "quote" && ( 247 + <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 248 + {isParentLoading ? ( 249 + <div className="text-sm text-gray-500 animate-pulse"> 250 + Loading parent post... 251 + </div> 252 + ) : parentUri ? ( 253 + <UniversalPostRendererATURILoader 254 + atUri={parentUri} 255 + isQuote 256 + /> 257 + ) : ( 258 + <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 259 + Could not load parent post. 260 + </div> 261 + )} 262 + </div> 263 + )} 264 + 265 + {postError && ( 266 + <div className="text-red-500 text-sm my-2 text-center"> 267 + {postError} 268 + </div> 269 + )} 230 270 </div> 271 + )} 272 + </div> 273 + </Dialog.Content> 274 + </Dialog.Portal> 275 + </Dialog.Root> 231 276 232 - {composerState.kind === "quote" && ( 233 - <div className="mb-4 ml-[50px] rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden"> 234 - {isParentLoading ? ( 235 - <div className="text-sm text-gray-500 animate-pulse"> 236 - Loading parent post... 237 - </div> 238 - ) : parentUri ? ( 239 - <UniversalPostRendererATURILoader 240 - atUri={parentUri} 241 - isQuote 242 - /> 243 - ) : ( 244 - <div className="text-sm text-red-500 rounded-lg border border-red-500/50 p-3"> 245 - Could not load parent post. 246 - </div> 247 - )} 248 - </div> 249 - )} 277 + {/* Close confirmation dialog */} 278 + <Dialog.Root open={closeConfirmState} onOpenChange={setCloseConfirmState}> 279 + <Dialog.Portal> 280 + 281 + <Dialog.Overlay className="disablegutter fixed inset-0 z-50 bg-black/40 dark:bg-black/50 data-[state=open]:animate-fadeIn" /> 250 282 251 - {postError && ( 252 - <div className="text-red-500 text-sm my-2 text-center"> 253 - {postError} 254 - </div> 255 - )} 283 + <Dialog.Content className="fixed gutter inset-0 z-50 flex items-start justify-center pt-30 sm:pt-40"> 284 + <div className="bg-gray-50 dark:bg-gray-950 border border-gray-200 dark:border-gray-700 rounded-2xl shadow-xl w-full max-w-md relative mx-4 py-6"> 285 + <div className="text-xl mb-4 text-center"> 286 + Discard your post? 287 + </div> 288 + <div className="text-md mb-4 text-center"> 289 + You will lose your draft 290 + </div> 291 + <div className="flex justify-end gap-2 px-6"> 292 + <Button 293 + onClick={handleConfirmClose} 294 + > 295 + Discard 296 + </Button> 297 + <Button 298 + variant={"outlined"} 299 + onClick={() => setCloseConfirmState(false)} 300 + > 301 + Cancel 302 + </Button> 256 303 </div> 257 - )} 258 - </div> 259 - </Dialog.Content> 260 - </Dialog.Portal> 261 - </Dialog.Root> 304 + </div> 305 + </Dialog.Content> 306 + </Dialog.Portal> 307 + </Dialog.Root> 308 + </> 262 309 ); 263 310 } 264 311
+5 -3
src/components/Login.tsx
··· 7 7 import { imgCDNAtom } from "~/utils/atoms"; 8 8 import { useQueryIdentity, useQueryProfile } from "~/utils/useQuery"; 9 9 10 + import { Button } from "./radix-m3-rd/Button"; 11 + 10 12 // --- 1. The Main Component (Orchestrator with `compact` prop) --- 11 13 export default function Login({ 12 14 compact = false, ··· 49 51 You are logged in! 50 52 </p> 51 53 <ProfileThing agent={agent} large /> 52 - <button 54 + <Button 53 55 onClick={logout} 54 - className="bg-gray-600 mt-4 hover:bg-gray-700 text-white rounded-full px-6 py-2 font-semibold text-base transition-colors" 56 + className="mt-4" 55 57 > 56 58 Log out 57 - </button> 59 + </Button> 58 60 </div> 59 61 </div> 60 62 );
+2
src/components/UniversalPostRenderer.tsx
··· 1557 1557 className="rounded-full w-[58px] h-[58px] object-cover border border-gray-300 dark:border-gray-800 bg-gray-300 dark:bg-gray-600" 1558 1558 /> 1559 1559 <div className=" flex-1 flex flex-row align-middle justify-end"> 1560 + <div className=" flex flex-col justify-start"> 1560 1561 <FollowButton targetdidorhandle={post.author.did} /> 1562 + </div> 1561 1563 </div> 1562 1564 </div> 1563 1565 <div className="flex flex-col gap-3">
+59
src/components/radix-m3-rd/Button.tsx
··· 1 + import { Slot } from "@radix-ui/react-slot"; 2 + import clsx from "clsx"; 3 + import * as React from "react"; 4 + 5 + export type ButtonVariant = "filled" | "outlined" | "text" | "secondary"; 6 + export type ButtonSize = "sm" | "md" | "lg"; 7 + 8 + const variantClasses: Record<ButtonVariant, string> = { 9 + filled: 10 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 11 + secondary: 12 + "bg-gray-300 text-gray-900 hover:bg-gray-400 dark:bg-gray-600 dark:text-white dark:hover:bg-gray-500", 13 + outlined: 14 + "border border-gray-800 text-gray-800 hover:bg-gray-100 dark:border-gray-200 dark:text-gray-200 dark:hover:bg-gray-800/10", 15 + text: "bg-transparent text-gray-800 hover:bg-gray-100 dark:text-gray-200 dark:hover:bg-gray-800/10", 16 + }; 17 + 18 + const sizeClasses: Record<ButtonSize, string> = { 19 + sm: "px-3 py-1.5 text-sm", 20 + md: "px-4 py-2 text-base", 21 + lg: "px-6 py-3 text-lg", 22 + }; 23 + 24 + export function Button({ 25 + variant = "filled", 26 + size = "md", 27 + asChild = false, 28 + ref, 29 + className, 30 + children, 31 + ...props 32 + }: { 33 + variant?: ButtonVariant; 34 + size?: ButtonSize; 35 + asChild?: boolean; 36 + className?: string; 37 + children?: React.ReactNode; 38 + ref?: React.Ref<HTMLButtonElement>; 39 + } & React.ComponentPropsWithoutRef<"button">) { 40 + const Comp = asChild ? Slot : "button"; 41 + 42 + return ( 43 + <Comp 44 + ref={ref} 45 + className={clsx( 46 + //focus:outline-none focus:ring-1 focus:ring-offset-1 focus:ring-gray-500 dark:focus:ring-gray-300 47 + "inline-flex items-center justify-center rounded-full transition-colors disabled:opacity-50 disabled:cursor-not-allowed", 48 + variantClasses[variant], 49 + sizeClasses[size], 50 + className 51 + )} 52 + {...props} 53 + > 54 + {children} 55 + </Comp> 56 + ); 57 + } 58 + 59 + Button.displayName = "Button";
+8 -11
src/routes/profile.$did/index.tsx
··· 5 5 import React, { type ReactNode, useEffect, useState } from "react"; 6 6 7 7 import { Header } from "~/components/Header"; 8 + import { Button } from "~/components/radix-m3-rd/Button"; 8 9 import { 9 10 renderTextWithFacets, 10 11 UniversalPostRendererATURILoader, ··· 170 171 also save it persistently 171 172 */} 172 173 <FollowButton targetdidorhandle={did} /> 173 - <button className="rounded-full dark:bg-gray-600 bg-gray-300 px-3 py-2 text-[14px]"> 174 + <Button className="rounded-full" variant={"secondary"}> 174 175 ... {/* todo: icon */} 175 - </button> 176 + </Button> 176 177 </div> 177 178 178 179 {/* Info Card */} ··· 248 249 {identity?.did !== agent?.did ? ( 249 250 <> 250 251 {!(followRecords?.length && followRecords?.length > 0) ? ( 251 - <button 252 + <Button 252 253 onClick={(e) => { 253 254 e.stopPropagation(); 254 255 toggleFollow({ ··· 258 259 queryClient: queryClient, 259 260 }); 260 261 }} 261 - className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 262 262 > 263 263 Follow 264 - </button> 264 + </Button> 265 265 ) : ( 266 - <button 266 + <Button 267 267 onClick={(e) => { 268 268 e.stopPropagation(); 269 269 toggleFollow({ ··· 273 273 queryClient: queryClient, 274 274 }); 275 275 }} 276 - className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]" 277 276 > 278 277 Unfollow 279 - </button> 278 + </Button> 280 279 )} 281 280 </> 282 281 ) : ( 283 - <button className="rounded-full h-10 bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 dark:hover:bg-gray-500 transition-colors px-4 py-2 text-[14px]"> 284 - Edit Profile 285 - </button> 282 + <Button variant={"secondary"}>Edit Profile</Button> 286 283 )} 287 284 </> 288 285 );