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

Configure Feed

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

Polls UI refinement maybe

+159 -123
+2
src/auto-imports.d.ts
··· 20 20 const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default 21 21 const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default 22 22 const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default 23 + const IconMdiCheckCircle: typeof import('~icons/mdi/check-circle.jsx').default 24 + const IconMdiCheckboxMultipleMarked: typeof import('~icons/mdi/checkbox-multiple-marked.jsx').default 23 25 const IconMdiClock: typeof import('~icons/mdi/clock.jsx').default 24 26 const IconMdiClockOutline: typeof import('~icons/mdi/clock-outline.jsx').default 25 27 const IconMdiClose: typeof import('~icons/mdi/close.jsx').default
+14 -6
src/components/OGPoll.tsx
··· 45 45 </div> 46 46 47 47 {/* Multiplicity */} 48 - <span className="text-2xl font-normal text-gray-300"> 49 - {multiple || !privateProviderHandle ? 'Select multiple options' : 'Select one option'} 48 + <span className="text-2xl font-normal text-gray-300 flex flex-row gap-2 items-center"> 49 + {multiple || !privateProviderHandle ? (<IconMdiCheckboxMultipleMarked />) : (<IconMdiCheckCircle />)} 50 + {multiple || !privateProviderHandle ? "Select one or more options" : "Select one option"} 51 + </span> 52 + 53 + 54 + <span className="text-3xl font-medium text-gray-100 ml-auto"> 55 + All votes are public 50 56 </span> 51 57 </div> 52 58 ··· 55 61 {options.map((optionText, index) => ( 56 62 <div 57 63 key={index} 58 - className="flex h-[76px] items-center justify-start truncate rounded-2xl bg-gray-800 px-8 text-3xl font-medium text-gray-50" 64 + className="flex h-[76px] items-center justify-start truncate rounded-2xl bg-gray-700 px-8 text-3xl font-medium text-gray-50" 59 65 > 60 66 <span className="truncate">{optionText}</span> 61 67 </div> ··· 84 90 </div> 85 91 </> 86 92 ) : ( 87 - <span className="text-3xl font-medium text-gray-100"> 88 - All votes are public 89 - </span> 93 + <div 94 + className="rounded-full h-16 bg-gray-600 text-gray-200 px-8 py-4 text-2xl" 95 + > 96 + View all votes 97 + </div> 90 98 )} 91 99 </div> 92 100 </div>
+143 -117
src/components/UniversalPostRenderer.tsx
··· 1268 1268 } from "~/routes/profile.$did"; 1269 1269 import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i"; 1270 1270 import { useFastLike } from "~/utils/likeMutationQueue"; 1271 + 1271 1272 // import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed"; 1272 1273 // import type { 1273 1274 // ViewRecord, ··· 2260 2261 2261 2262 2262 2263 2263 - const poll = pollRecord?.value as { 2264 + // todo: hardcoded to multiple for all public polls 2265 + const poll = { 2266 + ...(pollRecord?.value ?? {}), 2267 + multiple: true, 2268 + } as { 2264 2269 a: string; 2265 2270 b: string; 2266 2271 c?: string; ··· 2413 2418 }; 2414 2419 2415 2420 return ( 2416 - <div className="my-4"> 2417 - {/* Header */} 2418 - <div className="mb-4 flex items-center gap-3"> 2419 - {/* Type Pill */} 2420 - <div className="flex items-center gap-2 rounded-lg border border-gray-300 dark:border-gray-600 px-3 py-1 text-sm font-medium uppercase tracking-wide text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800"> 2421 - <IconMdiGlobe /> 2422 - <span>Public Poll</span> 2423 - </div> 2421 + <> 2422 + <div className="my-4"> 2423 + {/* Header */} 2424 + <div className="mb-4 flex items-center gap-3"> 2425 + {/* Type Pill */} 2426 + <div className="flex items-center gap-1.5 rounded-lg border-gray-300 dark:border-gray-600 pl-2 pr-2.5 py-1 text-sm font-medium uppercase tracking-wide text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-800"> 2427 + <IconMdiGlobe /> 2428 + <span>Public Poll</span> 2429 + </div> 2424 2430 2425 - {/* Multiplicity */} 2426 - <span className="text-sm font-normal text-gray-500 dark:text-gray-400"> 2427 - {poll.multiple ? "Select multiple options" : "Select one option"} 2428 - </span> 2431 + {/* Multiplicity */} 2432 + <span className="text-sm font-normal text-gray-500 dark:text-gray-400 flex flex-row items-center gap-1"> 2433 + {poll.multiple ? (<IconMdiCheckboxMultipleMarked />) : (<IconMdiCheckCircle />)} 2434 + {poll.multiple ? "Select one or more options" : "Select one option"} 2435 + </span> 2429 2436 2430 - {/* Total Votes */} 2431 - <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> 2432 - {totalVotes} vote{totalVotes !== 1 ? "s" : ""} 2433 - </span> 2434 - </div> 2437 + </div> 2435 2438 2436 - {/* Options List with Results */} 2437 - <div className="space-y-3"> 2438 - {options.map((optionText, index) => { 2439 - const optionKey = ["a", "b", "c", "d"][index]; 2439 + {/* Options List with Results */} 2440 + <div className="space-y-3"> 2441 + {options.map((optionText, index) => { 2442 + const optionKey = ["a", "b", "c", "d"][index]; 2440 2443 2441 - // Check if user has voted for this option 2442 - const userVotesForOption = (() => { 2443 - switch (optionKey) { 2444 - case "a": 2445 - return userVotesA; 2446 - case "b": 2447 - return userVotesB; 2448 - case "c": 2449 - return userVotesC; 2450 - case "d": 2451 - return userVotesD; 2452 - default: 2453 - return []; 2454 - } 2455 - })(); 2444 + // Check if user has voted for this option 2445 + const userVotesForOption = (() => { 2446 + switch (optionKey) { 2447 + case "a": 2448 + return userVotesA; 2449 + case "b": 2450 + return userVotesB; 2451 + case "c": 2452 + return userVotesC; 2453 + case "d": 2454 + return userVotesD; 2455 + default: 2456 + return []; 2457 + } 2458 + })(); 2456 2459 2457 - const rowData = voteData.find((v) => v.option === optionKey); 2458 - const hasVotedForOption = 2459 - userVotesForOption && userVotesForOption.length > 0; 2460 - const voteCount = 2461 - voteData.find((v) => v.option === optionKey)?.count ?? 0; 2462 - const votePercentage = 2463 - totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0; 2460 + const rowData = voteData.find((v) => v.option === optionKey); 2461 + const hasVotedForOption = 2462 + userVotesForOption && userVotesForOption.length > 0; 2463 + const voteCount = 2464 + voteData.find((v) => v.option === optionKey)?.count ?? 0; 2465 + const votePercentage = 2466 + totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0; 2464 2467 2465 - // Extract just the DIDs we want to show (top 2) 2466 - const topVoters = rowData?.voters 2467 - .filter(v => !!v.did) 2468 - .slice(0, 2) || []; 2468 + // Extract just the DIDs we want to show (top 2) 2469 + const topVoters = rowData?.voters 2470 + .filter(v => !!v.did) 2471 + .slice(0, 5) || []; 2469 2472 2470 - return ( 2471 - <div 2472 - key={index} 2473 - className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${!isExpired 2474 - ? hasVotedForOption 2475 - ? "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer outline-2 outline-gray-500 dark:outline-gray-400" 2476 - : "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer" 2477 - : "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" 2478 - }`} 2479 - onClick={() => !isExpired && handleVote(optionKey)} 2480 - > 2481 - {/* Vote percentage bar - always show */} 2473 + return ( 2482 2474 <div 2483 - className="absolute inset-y-0 left-0 bg-gray-300 dark:bg-gray-700 group-hover:bg-gray-400 dark:group-hover:bg-gray-600" 2484 - style={{ width: `${votePercentage}%` }} 2485 - /> 2475 + key={index} 2476 + className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${!isExpired 2477 + ? hasVotedForOption 2478 + ? "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer outline-2 outline-gray-500 dark:outline-gray-400" 2479 + : "bg-gray-100 dark:bg-gray-950 border-gray-200 dark:border-gray-700 hover:bg-gray-200 dark:hover:bg-gray-900 cursor-pointer" 2480 + : "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700" 2481 + }`} 2482 + onClick={(e) => { 2483 + e.stopPropagation(); 2484 + if (!isExpired) { 2485 + handleVote(optionKey) 2486 + } 2487 + }} 2488 + > 2489 + {/* Vote percentage bar - always show */} 2490 + <div 2491 + className="absolute inset-y-0 left-0 bg-gray-300 dark:bg-gray-700 group-hover:bg-gray-400 dark:group-hover:bg-gray-600" 2492 + style={{ width: `${votePercentage}%` }} 2493 + /> 2486 2494 2487 - {/* Option text */} 2488 - <span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate"> 2489 - {optionText} 2490 - {hasVotedForOption && ( 2491 - <span className="ml-2 text-gray-600 dark:text-gray-400"> 2492 - {poll.multiple ? "✓" : "✓ (click to remove)"} 2493 - </span> 2494 - )} 2495 - </span> 2495 + {/* Option text */} 2496 + <span className="relative z-[2] text-sm font-medium text-gray-900 dark:text-gray-100 truncate"> 2497 + {optionText} 2498 + {hasVotedForOption && ( 2499 + <span className="ml-2 text-gray-600 dark:text-gray-400"> 2500 + {poll.multiple ? "✓" : "✓ (click to remove)"} 2501 + </span> 2502 + )} 2503 + </span> 2496 2504 2497 - {/* Avatar circles and vote count */} 2498 - <div className="relative z-10 flex items-center gap-2"> 2499 - {/* Avatar circles - semi overlapping */} 2505 + {/* Avatar circles and vote count */} 2506 + <div className="relative z-[2] flex items-center gap-2"> 2507 + {/* Avatar circles - semi overlapping */} 2500 2508 2501 - {topVoters.length > 0 && ( 2502 - <div className="flex -space-x-2"> 2503 - {topVoters.map((voter, idx) => ( 2504 - <div 2505 - key={voter.did} // Use DID as key, it's stable 2506 - className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200" 2507 - style={{ zIndex: 2 - idx }} 2508 - > 2509 - {/* The Component handles the async fetch! */} 2510 - <PollOptionAvatar 2511 - did={voter.did} 2512 - /> 2513 - </div> 2514 - ))} 2515 - </div> 2516 - )} 2509 + {topVoters.length > 0 && ( 2510 + <div className="flex -space-x-2"> 2511 + {topVoters.map((voter, idx) => ( 2512 + <div 2513 + key={voter.did} // Use DID as key, it's stable 2514 + className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200" 2515 + style={{ zIndex: 5 - idx }} 2516 + > 2517 + {/* The Component handles the async fetch! */} 2518 + <PollOptionAvatar 2519 + did={voter.did} 2520 + /> 2521 + </div> 2522 + ))} 2523 + </div> 2524 + )} 2517 2525 2518 - {/* Vote count */} 2519 - <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> 2520 - {votePercentage.toFixed(0)}% 2521 - </span> 2526 + {/* Vote count */} 2527 + <span className="text-sm font-medium text-gray-600 dark:text-gray-400"> 2528 + {votePercentage.toFixed(0)}% 2529 + </span> 2530 + </div> 2522 2531 </div> 2523 - </div> 2524 - ); 2525 - })} 2526 - </div> 2532 + ); 2533 + })} 2534 + </div> 2527 2535 2528 - {/* Footer */} 2529 - <div className="mt-4 flex items-center justify-between text-sm text-gray-500 dark:text-gray-400"> 2530 - {/* Expiry */} 2531 - <div className="flex items-center gap-2"> 2532 - <IconMdiClockOutline /> 2533 - {/* <span>Expires {formattedDate}</span> */} 2534 - {formattedDate ? ( 2535 - !isExpired ? ( 2536 - <span>Expires {formattedDate}</span> 2536 + {/* Footer */} 2537 + <div className="mt-4 flex items-center justify-between text-sm text-gray-500 dark:text-gray-400"> 2538 + {/* Expiry */} 2539 + <div className="flex items-center gap-2"> 2540 + <IconMdiClockOutline /> 2541 + {/* <span>Expires {formattedDate}</span> */} 2542 + {formattedDate ? ( 2543 + !isExpired ? ( 2544 + <span>Expires {formattedDate}</span> 2545 + ) : ( 2546 + <span>Expired at {formattedDate}</span> 2547 + ) 2537 2548 ) : ( 2538 - <span>Expired at {formattedDate}</span> 2539 - ) 2540 - ) : ( 2541 - <span>Never expires</span> 2542 - )} 2543 - </div> 2549 + <span>Never expires</span> 2550 + )} 2551 + </div> 2544 2552 2545 - {/* Status */} 2546 - <div className="flex items-center gap-2"> 2553 + {/* Status */} 2554 + {/* <div className="flex items-center gap-2"> 2547 2555 {isExpired ? ( 2548 2556 <span className="text-red-500 dark:text-red-400 font-medium"> 2549 2557 Poll ended ··· 2553 2561 All votes are public 2554 2562 </span> 2555 2563 )} 2564 + </div> */} 2565 + <button 2566 + onClick={(e) => { 2567 + e.stopPropagation(); 2568 + // open the route to the view all stuff 2569 + }} 2570 + className="rounded-full h-10 bg-gray-200 text-gray-700 dark:bg-gray-700 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-gray-600 transition-colors px-4 py-2 text-[14px]" 2571 + > 2572 + View all {totalVotes} votes 2573 + </button> 2556 2574 </div> 2557 2575 </div> 2558 - </div> 2576 + {/* <div className=" scale-[56%] -translate-x-[120px] -translate-y-[80px]"> 2577 + <RawOGC 2578 + multiple 2579 + a={poll.a || ""} 2580 + b={poll.b || ""} 2581 + c={poll.c} 2582 + d={poll.d} /> 2583 + </div> */} 2584 + </> 2559 2585 ); 2560 2586 } 2561 2587