From cac42c9b29dca8d7e00b2e37065e94a043b6a13a Mon Sep 17 00:00:00 2001 From: yohlo Date: Wed, 17 Sep 2025 10:30:10 -0500 Subject: [PATCH] reactions --- package.json | 1 + .../matches/components/match-card.tsx | 195 ++++++++++-------- .../reactions/components/emoji-bar.tsx | 160 ++++++++++++++ .../reactions/components/emoji-picker.tsx | 89 ++++++++ .../components/upcoming-tournament/index.tsx | 4 + 5 files changed, 360 insertions(+), 89 deletions(-) create mode 100644 src/features/reactions/components/emoji-bar.tsx create mode 100644 src/features/reactions/components/emoji-picker.tsx diff --git a/package.json b/package.json index 7f60937..21b8698 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "@mantine/hooks": "^8.2.4", "@mantine/tiptap": "^8.2.4", "@phosphor-icons/react": "^2.1.10", + "@svgmoji/noto": "^3.2.0", "@tanstack/react-query": "^5.66.0", "@tanstack/react-query-devtools": "^5.66.0", "@tanstack/react-router": "^1.130.12", diff --git a/src/features/matches/components/match-card.tsx b/src/features/matches/components/match-card.tsx index 4e41628..095ba27 100644 --- a/src/features/matches/components/match-card.tsx +++ b/src/features/matches/components/match-card.tsx @@ -3,16 +3,14 @@ import { Group, Stack, Paper, - ThemeIcon, Indicator, Box, - Badge, - Skeleton, } from "@mantine/core"; -import { TrophyIcon, CrownIcon } from "@phosphor-icons/react"; +import { CrownIcon } from "@phosphor-icons/react"; import { useNavigate } from "@tanstack/react-router"; import { Match } from "../types"; import Avatar from "@/components/avatar"; +import EmojiBar from "@/features/reactions/components/emoji-bar"; interface MatchCardProps { match: Match; @@ -47,98 +45,117 @@ const MatchCard = ({ match }: MatchCardProps) => { position="top-end" offset={2} > - - - - - {match.tournament.name} - - - - - Round {match.round + 1} - {match.is_losers_bracket && " (Losers)"} - - - - - - - - {isHomeWin && ( - - - - )} - - - {match.home?.name!} + + + + + + {match.tournament.name} + + - + + Round {match.round + 1} + {match.is_losers_bracket && " (Losers)"} - - {match.home_cups} - - - - - - - {isAwayWin && ( - - - - )} - + + + + + {isHomeWin && ( + + + + )} + + + {match.home?.name!} + + - {match.away?.name} + {match.home_cups} - - {match.away_cups} - - - - + + + + + + {isAwayWin && ( + + + + )} + + + {match.away?.name} + + + + {match.away_cups} + + + + + + + + + ); }; diff --git a/src/features/reactions/components/emoji-bar.tsx b/src/features/reactions/components/emoji-bar.tsx new file mode 100644 index 0000000..815d0aa --- /dev/null +++ b/src/features/reactions/components/emoji-bar.tsx @@ -0,0 +1,160 @@ +import { + Group, + UnstyledButton, + Text, + Tabs, +} from "@mantine/core"; +import { useDisclosure } from "@mantine/hooks"; +import { useState, useRef } from "react"; +import Sheet from "@/components/sheet/sheet"; +import { PlayerInfo } from "@/features/players/types"; +import PlayerList from "@/features/players/components/player-list"; +import EmojiPicker from "./emoji-picker"; + +interface Reaction { + emoji: string; + count: number; + players: PlayerInfo[]; + hasReacted: boolean; +} + +interface EmojiBarProps { + reactions?: Reaction[]; + onReactionPress?: (emoji: string) => void; +} + +const EXAMPLE_DATA: Reaction[] = [ + { + emoji: "👍", + count: 1, + players: [{ id: "dasfasdf", first_name: "Kyle", last_name: "Yohler" }], + hasReacted: true, + }, + { + emoji: "❤️", + count: 1, + players: [{ id: "f3234f", first_name: "Salah", last_name: "Atiyeh" }], + hasReacted: false, + }, +]; + +const EmojiBar = ({ + reactions = EXAMPLE_DATA, + onReactionPress, +}: EmojiBarProps) => { + const [opened, { open, close }] = useDisclosure(false); + const [activeTab, setActiveTab] = useState(null); + const longPressTimeout = useRef(null); + + const handleLongPressStart = (emoji: string) => { + longPressTimeout.current = setTimeout(() => { + setActiveTab(emoji); + open(); + }, 500); + }; + + const handleLongPressEnd = () => { + if (longPressTimeout.current) { + clearTimeout(longPressTimeout.current); + } + }; + + const handleReactionClick = (emoji: string) => { + handleLongPressEnd(); + onReactionPress?.(emoji); + }; + + if (!reactions.length) return null; + + return ( + <> + + + {reactions.map((reaction) => ( + handleLongPressStart(reaction.emoji)} + onMouseUp={handleLongPressEnd} + onMouseLeave={handleLongPressEnd} + onTouchStart={() => handleLongPressStart(reaction.emoji)} + onTouchEnd={handleLongPressEnd} + onClick={() => handleReactionClick(reaction.emoji)} + px="8px" + py="10px" + style={{ + borderRadius: "var(--mantine-radius-xl)", + border: reaction.hasReacted + ? "1px solid var(--mantine-primary-color-filled)" + : "1px solid var(--mantine-color-default-border)", + backgroundColor: reaction.hasReacted + ? "var(--mantine-primary-color-light)" + : "transparent", + transition: "all 0.15s ease", + userSelect: "none", + WebkitUserSelect: "none", + MozUserSelect: "none", + msUserSelect: "none", + }} + styles={{ + root: { + "&:hover": { + backgroundColor: reaction.hasReacted + ? "var(--mantine-primary-color-light)" + : "var(--mantine-color-gray-1)", + transform: "scale(1.05)", + }, + "&:active": { + transform: "scale(0.95)", + }, + }, + }} + > + + + {reaction.emoji} + + + {reaction.count} + + + + ))} + + {})} /> + + + close()}> + + + {reactions.map((reaction) => ( + + + {reaction.emoji} + + {reaction.count} + + + + ))} + + + {reactions.map((reaction) => ( + + + + ))} + + + + ); +}; + +export default EmojiBar; diff --git a/src/features/reactions/components/emoji-picker.tsx b/src/features/reactions/components/emoji-picker.tsx new file mode 100644 index 0000000..a4ce88b --- /dev/null +++ b/src/features/reactions/components/emoji-picker.tsx @@ -0,0 +1,89 @@ +import { ActionIcon, Popover, SimpleGrid, UnstyledButton, Text } from "@mantine/core"; +import { SmileyStickerIcon } from "@phosphor-icons/react"; +import { useState } from "react"; + +interface EmojiPickerProps { + onSelect: (emoji: string) => void; + disabled?: boolean; +} + +const EMOJIS = [ + { emoji: "😊", label: "smile" }, + { emoji: "😢", label: "cry" }, + { emoji: "👀", label: "eyes" }, + { emoji: "🔥", label: "fire" }, + { emoji: "❤️", label: "heart" }, + { emoji: "👑", label: "crown" }, +]; + +const EmojiPicker = ({ + onSelect, + disabled = false +}: EmojiPickerProps) => { + const [opened, setOpened] = useState(false); + + const handleEmojiSelect = (emoji: string) => { + onSelect(emoji); + setOpened(false); + }; + + return ( + + + setOpened((o) => !o)} + disabled={disabled} + aria-label="Select emoji" + > + + + + + + + {EMOJIS.map(({ emoji, label }) => ( + handleEmojiSelect(emoji)} + style={{ + borderRadius: "var(--mantine-radius-sm)", + display: "flex", + alignItems: "center", + justifyContent: "center", + minHeight: 36, + minWidth: 36, + }} + styles={{ + root: { + '&:hover': { + backgroundColor: 'var(--mantine-color-gray-1)', + }, + '&:active': { + transform: 'scale(0.95)', + }, + }, + }} + aria-label={label} + > + + {emoji} + + + ))} + + + + ); +}; + +export default EmojiPicker; \ No newline at end of file diff --git a/src/features/tournaments/components/upcoming-tournament/index.tsx b/src/features/tournaments/components/upcoming-tournament/index.tsx index 7d61828..6949d6e 100644 --- a/src/features/tournaments/components/upcoming-tournament/index.tsx +++ b/src/features/tournaments/components/upcoming-tournament/index.tsx @@ -20,6 +20,8 @@ import EnrollTeam from "./enroll-team"; import EnrollFreeAgent from "./enroll-free-agent"; import TeamListButton from "./team-list-button"; import Header from "./header"; +import EmojiPicker from "@/features/reactions/components/emoji-picker"; +import EmojiBar from "@/features/reactions/components/emoji-bar"; const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({ tournament, @@ -46,6 +48,8 @@ const UpcomingTournament: React.FC<{ tournament: Tournament }> = ({
+ + {tournament.desc && {tournament.desc}}