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
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
22
const IconMdiCheck: typeof import('~icons/mdi/check.jsx').default
0
0
23
const IconMdiClock: typeof import('~icons/mdi/clock.jsx').default
24
const IconMdiClockOutline: typeof import('~icons/mdi/clock-outline.jsx').default
25
const IconMdiClose: typeof import('~icons/mdi/close.jsx').default
···
20
const IconMdiAccountCircle: typeof import('~icons/mdi/account-circle.jsx').default
21
const IconMdiAccountPlus: typeof import('~icons/mdi/account-plus.jsx').default
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
25
const IconMdiClock: typeof import('~icons/mdi/clock.jsx').default
26
const IconMdiClockOutline: typeof import('~icons/mdi/clock-outline.jsx').default
27
const IconMdiClose: typeof import('~icons/mdi/close.jsx').default
+14
-6
src/components/OGPoll.tsx
···
45
</div>
46
47
{/* Multiplicity */}
48
-
<span className="text-2xl font-normal text-gray-300">
49
-
{multiple || !privateProviderHandle ? 'Select multiple options' : 'Select one option'}
0
0
0
0
0
0
50
</span>
51
</div>
52
···
55
{options.map((optionText, index) => (
56
<div
57
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"
59
>
60
<span className="truncate">{optionText}</span>
61
</div>
···
84
</div>
85
</>
86
) : (
87
-
<span className="text-3xl font-medium text-gray-100">
88
-
All votes are public
89
-
</span>
0
0
90
)}
91
</div>
92
</div>
···
45
</div>
46
47
{/* Multiplicity */}
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
56
</span>
57
</div>
58
···
61
{options.map((optionText, index) => (
62
<div
63
key={index}
64
+
className="flex h-[76px] items-center justify-start truncate rounded-2xl bg-gray-700 px-8 text-3xl font-medium text-gray-50"
65
>
66
<span className="truncate">{optionText}</span>
67
</div>
···
90
</div>
91
</>
92
) : (
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>
98
)}
99
</div>
100
</div>
+143
-117
src/components/UniversalPostRenderer.tsx
···
1268
} from "~/routes/profile.$did";
1269
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1270
import { useFastLike } from "~/utils/likeMutationQueue";
0
1271
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1272
// import type {
1273
// ViewRecord,
···
2260
2261
2262
2263
-
const poll = pollRecord?.value as {
0
0
0
0
2264
a: string;
2265
b: string;
2266
c?: string;
···
2413
};
2414
2415
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>
0
2424
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>
0
2429
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>
2435
2436
-
{/* Options List with Results */}
2437
-
<div className="space-y-3">
2438
-
{options.map((optionText, index) => {
2439
-
const optionKey = ["a", "b", "c", "d"][index];
2440
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
-
})();
2456
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;
2464
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) || [];
2469
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 */}
2482
<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
-
/>
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
2486
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>
2496
2497
-
{/* Avatar circles and vote count */}
2498
-
<div className="relative z-10 flex items-center gap-2">
2499
-
{/* Avatar circles - semi overlapping */}
2500
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
-
)}
2517
2518
-
{/* Vote count */}
2519
-
<span className="text-sm font-medium text-gray-600 dark:text-gray-400">
2520
-
{votePercentage.toFixed(0)}%
2521
-
</span>
0
2522
</div>
2523
-
</div>
2524
-
);
2525
-
})}
2526
-
</div>
2527
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>
0
0
0
2537
) : (
2538
-
<span>Expired at {formattedDate}</span>
2539
-
)
2540
-
) : (
2541
-
<span>Never expires</span>
2542
-
)}
2543
-
</div>
2544
2545
-
{/* Status */}
2546
-
<div className="flex items-center gap-2">
2547
{isExpired ? (
2548
<span className="text-red-500 dark:text-red-400 font-medium">
2549
Poll ended
···
2553
All votes are public
2554
</span>
2555
)}
0
0
0
0
0
0
0
0
0
0
2556
</div>
2557
</div>
2558
-
</div>
0
0
0
0
0
0
0
0
2559
);
2560
}
2561
···
1268
} from "~/routes/profile.$did";
1269
import type { LightboxProps } from "~/routes/profile.$did/post.$rkey.image.$i";
1270
import { useFastLike } from "~/utils/likeMutationQueue";
1271
+
1272
// import type { OutputSchema } from "@atproto/api/dist/client/types/app/bsky/feed/getFeed";
1273
// import type {
1274
// ViewRecord,
···
2261
2262
2263
2264
+
// todo: hardcoded to multiple for all public polls
2265
+
const poll = {
2266
+
...(pollRecord?.value ?? {}),
2267
+
multiple: true,
2268
+
} as {
2269
a: string;
2270
b: string;
2271
c?: string;
···
2418
};
2419
2420
return (
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>
2430
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>
2436
2437
+
</div>
0
0
0
0
2438
2439
+
{/* Options List with Results */}
2440
+
<div className="space-y-3">
2441
+
{options.map((optionText, index) => {
2442
+
const optionKey = ["a", "b", "c", "d"][index];
2443
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
+
})();
2459
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;
2467
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) || [];
2472
2473
+
return (
0
0
0
0
0
0
0
0
0
0
0
2474
<div
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
+
/>
2494
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>
2504
2505
+
{/* Avatar circles and vote count */}
2506
+
<div className="relative z-[2] flex items-center gap-2">
2507
+
{/* Avatar circles - semi overlapping */}
2508
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
+
)}
2525
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>
2531
</div>
2532
+
);
2533
+
})}
2534
+
</div>
0
2535
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
+
)
2548
) : (
2549
+
<span>Never expires</span>
2550
+
)}
2551
+
</div>
0
0
0
2552
2553
+
{/* Status */}
2554
+
{/* <div className="flex items-center gap-2">
2555
{isExpired ? (
2556
<span className="text-red-500 dark:text-red-400 font-medium">
2557
Poll ended
···
2561
All votes are public
2562
</span>
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>
2574
</div>
2575
</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
+
</>
2585
);
2586
}
2587