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

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