Emoji reactions on songs
CI / check (push) Canceled after 0s

React to a friend's current song or any of their recent plays: tap an
emoji to add yours (or take it back), "☺+" for the quick six, and a full
picker with search and categories (Unicode emoji up to 13.1, generated
into the shared package so web and mobile match). One reaction per person
per play; a new one replaces the old. Chips show counts and, on hover/
long label, who reacted; yours is highlighted. Listeners and followers get
the update live. Not on your own plays, not across a block (blocking also
clears reactions between the two people).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MzFywujuH9TiuXbr9vjoX8
This commit is contained in:
2026-10-06 13:39:26 -04:00
co-authored by Claude Opus 5.5
parent edc0c7b999
commit d2edad4b0d
28 changed files with 2200 additions and 30 deletions
+26
View File
@@ -102,6 +102,32 @@ input:focus, textarea:focus, select:focus {
.share-link { font: inherit; cursor: pointer; color: var(--text); }
.song-page { display: flex; flex-direction: column; gap: 1.25rem; align-items: stretch; }
.song-art { width: 100%; max-width: 360px; aspect-ratio: 1; object-fit: cover; border-radius: 1.1rem; align-self: center; box-shadow: 0 30px 60px -30px rgba(0, 0, 0, 0.8); }
.reactions { display: flex; flex-wrap: wrap; align-items: center; gap: 0.35rem; margin-top: 0.7rem; position: relative; }
.recent .reactions { margin: 0.25rem 0 0.4rem 3.4rem; }
.reaction { display: inline-flex; align-items: center; gap: 0.25rem; padding: 0.15rem 0.55rem; border-radius: 999px; border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(255, 255, 255, 0.07); color: var(--text); font: inherit; font-size: 0.9rem; cursor: pointer; transition: transform 0.15s, background 0.2s; }
.reaction:hover:not(:disabled) { transform: translateY(-1px); background: rgba(255, 255, 255, 0.13); }
.reaction:active:not(:disabled) { transform: scale(0.9); }
.reaction:disabled { cursor: default; }
.reaction.mine { border-color: rgba(123, 47, 247, 0.7); background: rgba(123, 47, 247, 0.28); }
.reaction-emoji { font-size: 1.05rem; line-height: 1.3; }
.reaction-count { font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.reaction-add { color: var(--muted); font-size: 1rem; }
.reaction-add span { font-size: 0.75rem; margin-left: -0.15rem; }
.reaction-add-wrap { position: static; } /* the quick menu anchors to the row, so it never runs off the card */
.reaction-quick { position: absolute; bottom: calc(100% + 0.4rem); left: 0; z-index: 30; display: flex; align-items: center; gap: 0.15rem; padding: 0.3rem; border-radius: 999px; background: #1a1535; border: 1px solid rgba(255, 255, 255, 0.14); box-shadow: 0 14px 30px -10px rgba(0, 0, 0, 0.7); animation: pop 0.18s ease-out; white-space: nowrap; }
.reaction-quick button { all: unset; cursor: pointer; font-size: 1.45rem; padding: 0.2rem 0.3rem; border-radius: 999px; transition: transform 0.15s; }
.reaction-quick button:hover { transform: scale(1.25); }
.reaction-quick button.mine { background: rgba(123, 47, 247, 0.35); }
.reaction-quick .reaction-more { font-size: 0.85rem; color: var(--muted); padding: 0.3rem 0.6rem; }
@keyframes pop { from { opacity: 0; transform: translateY(6px) scale(0.92); } to { opacity: 1; transform: none; } }
.emoji-search { width: 100%; margin-bottom: 0.6rem; }
.emoji-tabs { display: flex; gap: 0.15rem; margin-bottom: 0.5rem; overflow-x: auto; }
.emoji-tabs button { all: unset; cursor: pointer; font-size: 1.25rem; padding: 0.25rem 0.4rem; border-radius: 0.5rem; opacity: 0.6; }
.emoji-tabs button[aria-selected='true'] { opacity: 1; background: rgba(255, 255, 255, 0.1); }
.emoji-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(2.4rem, 1fr)); gap: 0.1rem; max-height: 18rem; overflow-y: auto; }
.emoji-grid button { all: unset; cursor: pointer; font-size: 1.55rem; text-align: center; padding: 0.2rem; border-radius: 0.5rem; }
.emoji-grid button:hover { background: rgba(255, 255, 255, 0.1); }
@media (prefers-reduced-motion: reduce) { .reaction-quick { animation: none; } }
.avatar-picker { display: flex; gap: 1rem; align-items: center; }
.avatar-picker-actions { display: flex; flex-direction: column; gap: 0.4rem; }
.grow { flex: 1; min-width: 0; }
@@ -138,7 +138,7 @@ export function ProfileClient() {
<p className="muted">Nothing playing right now.</p>
)}
<RecentPlays plays={profile.recent} preferred={preferred} attribution={profile.attribution} />
<RecentPlays plays={profile.recent} preferred={preferred} attribution={profile.attribution} ownerId={user.id} />
</div>
);
}