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}}