tangled
alpha
login
or
join now
whey.party
/
red-dwarf
82
fork
atom
an independent Bluesky client using Constellation, PDS Queries, and other services
reddwarf.app
frontend
spa
bluesky
reddwarf
microcosm
client
app
82
fork
atom
overview
issues
25
pulls
pipelines
Polls UI refinement maybe
whey.party
1 month ago
76d6a758
c647b290
+159
-123
3 changed files
expand all
collapse all
unified
split
src
auto-imports.d.ts
components
OGPoll.tsx
UniversalPostRenderer.tsx
+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
23
+
const IconMdiCheckCircle: typeof import('~icons/mdi/check-circle.jsx').default
24
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
48
-
<span className="text-2xl font-normal text-gray-300">
49
49
-
{multiple || !privateProviderHandle ? 'Select multiple options' : 'Select one option'}
48
48
+
<span className="text-2xl font-normal text-gray-300 flex flex-row gap-2 items-center">
49
49
+
{multiple || !privateProviderHandle ? (<IconMdiCheckboxMultipleMarked />) : (<IconMdiCheckCircle />)}
50
50
+
{multiple || !privateProviderHandle ? "Select one or more options" : "Select one option"}
51
51
+
</span>
52
52
+
53
53
+
54
54
+
<span className="text-3xl font-medium text-gray-100 ml-auto">
55
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
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
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
87
-
<span className="text-3xl font-medium text-gray-100">
88
88
-
All votes are public
89
89
-
</span>
93
93
+
<div
94
94
+
className="rounded-full h-16 bg-gray-600 text-gray-200 px-8 py-4 text-2xl"
95
95
+
>
96
96
+
View all votes
97
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
+
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
2263
-
const poll = pollRecord?.value as {
2264
2264
+
// todo: hardcoded to multiple for all public polls
2265
2265
+
const poll = {
2266
2266
+
...(pollRecord?.value ?? {}),
2267
2267
+
multiple: true,
2268
2268
+
} as {
2264
2269
a: string;
2265
2270
b: string;
2266
2271
c?: string;
···
2413
2418
};
2414
2419
2415
2420
return (
2416
2416
-
<div className="my-4">
2417
2417
-
{/* Header */}
2418
2418
-
<div className="mb-4 flex items-center gap-3">
2419
2419
-
{/* Type Pill */}
2420
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
2421
-
<IconMdiGlobe />
2422
2422
-
<span>Public Poll</span>
2423
2423
-
</div>
2421
2421
+
<>
2422
2422
+
<div className="my-4">
2423
2423
+
{/* Header */}
2424
2424
+
<div className="mb-4 flex items-center gap-3">
2425
2425
+
{/* Type Pill */}
2426
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
2427
+
<IconMdiGlobe />
2428
2428
+
<span>Public Poll</span>
2429
2429
+
</div>
2424
2430
2425
2425
-
{/* Multiplicity */}
2426
2426
-
<span className="text-sm font-normal text-gray-500 dark:text-gray-400">
2427
2427
-
{poll.multiple ? "Select multiple options" : "Select one option"}
2428
2428
-
</span>
2431
2431
+
{/* Multiplicity */}
2432
2432
+
<span className="text-sm font-normal text-gray-500 dark:text-gray-400 flex flex-row items-center gap-1">
2433
2433
+
{poll.multiple ? (<IconMdiCheckboxMultipleMarked />) : (<IconMdiCheckCircle />)}
2434
2434
+
{poll.multiple ? "Select one or more options" : "Select one option"}
2435
2435
+
</span>
2429
2436
2430
2430
-
{/* Total Votes */}
2431
2431
-
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
2432
2432
-
{totalVotes} vote{totalVotes !== 1 ? "s" : ""}
2433
2433
-
</span>
2434
2434
-
</div>
2437
2437
+
</div>
2435
2438
2436
2436
-
{/* Options List with Results */}
2437
2437
-
<div className="space-y-3">
2438
2438
-
{options.map((optionText, index) => {
2439
2439
-
const optionKey = ["a", "b", "c", "d"][index];
2439
2439
+
{/* Options List with Results */}
2440
2440
+
<div className="space-y-3">
2441
2441
+
{options.map((optionText, index) => {
2442
2442
+
const optionKey = ["a", "b", "c", "d"][index];
2440
2443
2441
2441
-
// Check if user has voted for this option
2442
2442
-
const userVotesForOption = (() => {
2443
2443
-
switch (optionKey) {
2444
2444
-
case "a":
2445
2445
-
return userVotesA;
2446
2446
-
case "b":
2447
2447
-
return userVotesB;
2448
2448
-
case "c":
2449
2449
-
return userVotesC;
2450
2450
-
case "d":
2451
2451
-
return userVotesD;
2452
2452
-
default:
2453
2453
-
return [];
2454
2454
-
}
2455
2455
-
})();
2444
2444
+
// Check if user has voted for this option
2445
2445
+
const userVotesForOption = (() => {
2446
2446
+
switch (optionKey) {
2447
2447
+
case "a":
2448
2448
+
return userVotesA;
2449
2449
+
case "b":
2450
2450
+
return userVotesB;
2451
2451
+
case "c":
2452
2452
+
return userVotesC;
2453
2453
+
case "d":
2454
2454
+
return userVotesD;
2455
2455
+
default:
2456
2456
+
return [];
2457
2457
+
}
2458
2458
+
})();
2456
2459
2457
2457
-
const rowData = voteData.find((v) => v.option === optionKey);
2458
2458
-
const hasVotedForOption =
2459
2459
-
userVotesForOption && userVotesForOption.length > 0;
2460
2460
-
const voteCount =
2461
2461
-
voteData.find((v) => v.option === optionKey)?.count ?? 0;
2462
2462
-
const votePercentage =
2463
2463
-
totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0;
2460
2460
+
const rowData = voteData.find((v) => v.option === optionKey);
2461
2461
+
const hasVotedForOption =
2462
2462
+
userVotesForOption && userVotesForOption.length > 0;
2463
2463
+
const voteCount =
2464
2464
+
voteData.find((v) => v.option === optionKey)?.count ?? 0;
2465
2465
+
const votePercentage =
2466
2466
+
totalVotes > 0 ? (voteCount / totalVotes) * 100 : 0;
2464
2467
2465
2465
-
// Extract just the DIDs we want to show (top 2)
2466
2466
-
const topVoters = rowData?.voters
2467
2467
-
.filter(v => !!v.did)
2468
2468
-
.slice(0, 2) || [];
2468
2468
+
// Extract just the DIDs we want to show (top 2)
2469
2469
+
const topVoters = rowData?.voters
2470
2470
+
.filter(v => !!v.did)
2471
2471
+
.slice(0, 5) || [];
2469
2472
2470
2470
-
return (
2471
2471
-
<div
2472
2472
-
key={index}
2473
2473
-
className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${!isExpired
2474
2474
-
? hasVotedForOption
2475
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
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
2477
-
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
2478
2478
-
}`}
2479
2479
-
onClick={() => !isExpired && handleVote(optionKey)}
2480
2480
-
>
2481
2481
-
{/* Vote percentage bar - always show */}
2473
2473
+
return (
2482
2474
<div
2483
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
2484
-
style={{ width: `${votePercentage}%` }}
2485
2485
-
/>
2475
2475
+
key={index}
2476
2476
+
className={`group relative h-12 items-center justify-between rounded-lg border px-4 flex overflow-hidden ${!isExpired
2477
2477
+
? hasVotedForOption
2478
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
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
2480
+
: "bg-white dark:bg-gray-900 border-gray-200 dark:border-gray-700"
2481
2481
+
}`}
2482
2482
+
onClick={(e) => {
2483
2483
+
e.stopPropagation();
2484
2484
+
if (!isExpired) {
2485
2485
+
handleVote(optionKey)
2486
2486
+
}
2487
2487
+
}}
2488
2488
+
>
2489
2489
+
{/* Vote percentage bar - always show */}
2490
2490
+
<div
2491
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
2492
+
style={{ width: `${votePercentage}%` }}
2493
2493
+
/>
2486
2494
2487
2487
-
{/* Option text */}
2488
2488
-
<span className="relative z-10 text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
2489
2489
-
{optionText}
2490
2490
-
{hasVotedForOption && (
2491
2491
-
<span className="ml-2 text-gray-600 dark:text-gray-400">
2492
2492
-
{poll.multiple ? "✓" : "✓ (click to remove)"}
2493
2493
-
</span>
2494
2494
-
)}
2495
2495
-
</span>
2495
2495
+
{/* Option text */}
2496
2496
+
<span className="relative z-[2] text-sm font-medium text-gray-900 dark:text-gray-100 truncate">
2497
2497
+
{optionText}
2498
2498
+
{hasVotedForOption && (
2499
2499
+
<span className="ml-2 text-gray-600 dark:text-gray-400">
2500
2500
+
{poll.multiple ? "✓" : "✓ (click to remove)"}
2501
2501
+
</span>
2502
2502
+
)}
2503
2503
+
</span>
2496
2504
2497
2497
-
{/* Avatar circles and vote count */}
2498
2498
-
<div className="relative z-10 flex items-center gap-2">
2499
2499
-
{/* Avatar circles - semi overlapping */}
2505
2505
+
{/* Avatar circles and vote count */}
2506
2506
+
<div className="relative z-[2] flex items-center gap-2">
2507
2507
+
{/* Avatar circles - semi overlapping */}
2500
2508
2501
2501
-
{topVoters.length > 0 && (
2502
2502
-
<div className="flex -space-x-2">
2503
2503
-
{topVoters.map((voter, idx) => (
2504
2504
-
<div
2505
2505
-
key={voter.did} // Use DID as key, it's stable
2506
2506
-
className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200"
2507
2507
-
style={{ zIndex: 2 - idx }}
2508
2508
-
>
2509
2509
-
{/* The Component handles the async fetch! */}
2510
2510
-
<PollOptionAvatar
2511
2511
-
did={voter.did}
2512
2512
-
/>
2513
2513
-
</div>
2514
2514
-
))}
2515
2515
-
</div>
2516
2516
-
)}
2509
2509
+
{topVoters.length > 0 && (
2510
2510
+
<div className="flex -space-x-2">
2511
2511
+
{topVoters.map((voter, idx) => (
2512
2512
+
<div
2513
2513
+
key={voter.did} // Use DID as key, it's stable
2514
2514
+
className="w-5 h-5 rounded-full border-2 border-white dark:border-gray-900 overflow-hidden bg-gray-200"
2515
2515
+
style={{ zIndex: 5 - idx }}
2516
2516
+
>
2517
2517
+
{/* The Component handles the async fetch! */}
2518
2518
+
<PollOptionAvatar
2519
2519
+
did={voter.did}
2520
2520
+
/>
2521
2521
+
</div>
2522
2522
+
))}
2523
2523
+
</div>
2524
2524
+
)}
2517
2525
2518
2518
-
{/* Vote count */}
2519
2519
-
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
2520
2520
-
{votePercentage.toFixed(0)}%
2521
2521
-
</span>
2526
2526
+
{/* Vote count */}
2527
2527
+
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
2528
2528
+
{votePercentage.toFixed(0)}%
2529
2529
+
</span>
2530
2530
+
</div>
2522
2531
</div>
2523
2523
-
</div>
2524
2524
-
);
2525
2525
-
})}
2526
2526
-
</div>
2532
2532
+
);
2533
2533
+
})}
2534
2534
+
</div>
2527
2535
2528
2528
-
{/* Footer */}
2529
2529
-
<div className="mt-4 flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
2530
2530
-
{/* Expiry */}
2531
2531
-
<div className="flex items-center gap-2">
2532
2532
-
<IconMdiClockOutline />
2533
2533
-
{/* <span>Expires {formattedDate}</span> */}
2534
2534
-
{formattedDate ? (
2535
2535
-
!isExpired ? (
2536
2536
-
<span>Expires {formattedDate}</span>
2536
2536
+
{/* Footer */}
2537
2537
+
<div className="mt-4 flex items-center justify-between text-sm text-gray-500 dark:text-gray-400">
2538
2538
+
{/* Expiry */}
2539
2539
+
<div className="flex items-center gap-2">
2540
2540
+
<IconMdiClockOutline />
2541
2541
+
{/* <span>Expires {formattedDate}</span> */}
2542
2542
+
{formattedDate ? (
2543
2543
+
!isExpired ? (
2544
2544
+
<span>Expires {formattedDate}</span>
2545
2545
+
) : (
2546
2546
+
<span>Expired at {formattedDate}</span>
2547
2547
+
)
2537
2548
) : (
2538
2538
-
<span>Expired at {formattedDate}</span>
2539
2539
-
)
2540
2540
-
) : (
2541
2541
-
<span>Never expires</span>
2542
2542
-
)}
2543
2543
-
</div>
2549
2549
+
<span>Never expires</span>
2550
2550
+
)}
2551
2551
+
</div>
2544
2552
2545
2545
-
{/* Status */}
2546
2546
-
<div className="flex items-center gap-2">
2553
2553
+
{/* Status */}
2554
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
2564
+
</div> */}
2565
2565
+
<button
2566
2566
+
onClick={(e) => {
2567
2567
+
e.stopPropagation();
2568
2568
+
// open the route to the view all stuff
2569
2569
+
}}
2570
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
2571
+
>
2572
2572
+
View all {totalVotes} votes
2573
2573
+
</button>
2556
2574
</div>
2557
2575
</div>
2558
2558
-
</div>
2576
2576
+
{/* <div className=" scale-[56%] -translate-x-[120px] -translate-y-[80px]">
2577
2577
+
<RawOGC
2578
2578
+
multiple
2579
2579
+
a={poll.a || ""}
2580
2580
+
b={poll.b || ""}
2581
2581
+
c={poll.c}
2582
2582
+
d={poll.d} />
2583
2583
+
</div> */}
2584
2584
+
</>
2559
2585
);
2560
2586
}
2561
2587