CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
reactions.tsx
Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.
| 6fc53bd | 1 | /** |
| 2 | * Reactions bar — displays current counts + a "+ react" picker. | |
| 3 | * Works without JavaScript: each emoji is its own POST form. | |
| 4 | */ | |
| 5 | ||
| 6 | import type { FC } from "hono/jsx"; | |
| 7 | import { | |
| 8 | ALLOWED_EMOJIS, | |
| 9 | EMOJI_GLYPH, | |
| 10 | type Emoji, | |
| 11 | type ReactionSummary, | |
| 12 | type TargetType, | |
| 13 | } from "../lib/reactions"; | |
| 14 | ||
| 15 | export const ReactionsBar: FC<{ | |
| 16 | targetType: TargetType; | |
| 17 | targetId: string; | |
| 18 | summaries: ReactionSummary[]; | |
| 19 | canReact: boolean; | |
| 20 | }> = ({ targetType, targetId, summaries, canReact }) => { | |
| 21 | const byEmoji = new Map<Emoji, ReactionSummary>(); | |
| 22 | for (const s of summaries) byEmoji.set(s.emoji, s); | |
| 23 | ||
| 24 | const action = (emoji: Emoji) => | |
| 25 | `/api/reactions/${targetType}/${targetId}/${emoji}/toggle`; | |
| 26 | ||
| 27 | const visible = summaries.filter((s) => s.count > 0); | |
| 28 | ||
| 29 | return ( | |
| 30 | <div class="reactions" data-target={`${targetType}:${targetId}`}> | |
| 31 | {visible.map((s) => ( | |
| 001af43 | 32 | <form method="post" action={action(s.emoji)} style="display: inline"> |
| 6fc53bd | 33 | <button |
| 34 | type="submit" | |
| 35 | class={`reaction-btn ${s.reactedByMe ? "active" : ""}`} | |
| 36 | title={s.emoji.replace(/_/g, " ")} | |
| 37 | disabled={!canReact} | |
| 38 | > | |
| 39 | <span>{EMOJI_GLYPH[s.emoji]}</span> | |
| 40 | <span class="reaction-count">{s.count}</span> | |
| 41 | </button> | |
| 42 | </form> | |
| 43 | ))} | |
| 44 | {canReact && ( | |
| 45 | <details class="reaction-picker"> | |
| 46 | <summary class="reaction-btn" title="Add reaction"> | |
| 47 | {"\u271A"} | |
| 48 | </summary> | |
| 49 | <div style="display: flex; gap: 4px; padding: 4px"> | |
| 50 | {ALLOWED_EMOJIS.filter((e) => !byEmoji.get(e)?.reactedByMe).map( | |
| 51 | (emoji) => ( | |
| 001af43 | 52 | <form method="post" action={action(emoji)} style="display: inline"> |
| 6fc53bd | 53 | <button |
| 54 | type="submit" | |
| 55 | class="reaction-btn" | |
| 56 | title={emoji.replace(/_/g, " ")} | |
| 57 | > | |
| 58 | <span>{EMOJI_GLYPH[emoji]}</span> | |
| 59 | </button> | |
| 60 | </form> | |
| 61 | ) | |
| 62 | )} | |
| 63 | </div> | |
| 64 | </details> | |
| 65 | )} | |
| 66 | </div> | |
| 67 | ); | |
| 68 | }; |