Add designed sticker packs (SVG art) to the reactions system

- 15 hand-designed inline-SVG stickers (faces, Hokm: حکم/کُت/crown/ace,
  Persian: chai/آفرین/rose, taunts: clown/zzz/ضعیف) in components/online/Sticker.tsx
- Sticker packs: faces (free), hokm (earned @rating 1300), persian & taunt (buy)
- In-game tray now tabbed Emoji | Stickers; stickers broadcast as "sticker:<id>"
  and render as large animated bubbles per seat
- Shop sells sticker packs; profile.ownedStickerPacks; gamification helpers
  ownedStickers/ownedStickerPackIds; mock opponents send stickers too

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-04 11:15:28 +03:30
parent f9425dea01
commit db4eade619
8 changed files with 359 additions and 20 deletions
+59 -12
View File
@@ -17,10 +17,11 @@ import {
} from "@/lib/hokm/types";
import { useI18n } from "@/lib/i18n";
import { useSessionStore } from "@/lib/session-store";
import { cardStyleById, ownedReactions } from "@/lib/online/gamification";
import { cardStyleById, ownedReactions, ownedStickers } from "@/lib/online/gamification";
import { getService } from "@/lib/online/service";
import { cn } from "@/lib/cn";
import { PlayingCard } from "./PlayingCard";
import { Sticker } from "./online/Sticker";
function useCountdown(deadline: number | null) {
const [now, setNow] = useState(() => Date.now());
@@ -492,12 +493,21 @@ interface Bubble {
emoji: string;
}
function ReactionBubble({ value }: { value: string }) {
if (value.startsWith("sticker:")) {
return <Sticker id={value.slice(8)} size={72} className="drop-shadow-xl" />;
}
return <span className="text-4xl drop-shadow-lg">{value}</span>;
}
function Reactions() {
const profile = useSessionStore((s) => s.profile);
const { t } = useI18n();
const [open, setOpen] = useState(false);
const [tab, setTab] = useState<"emoji" | "sticker">("emoji");
const [bubbles, setBubbles] = useState<Bubble[]>([]);
const list = profile ? ownedReactions(profile) : [];
const emojis = profile ? ownedReactions(profile) : [];
const stickers = profile ? ownedStickers(profile) : [];
useEffect(() => {
const unsub = getService().onReaction((seat, emoji) => {
@@ -508,8 +518,8 @@ function Reactions() {
return unsub;
}, []);
const send = (emoji: string) => {
getService().sendReaction(emoji);
const send = (value: string) => {
getService().sendReaction(value);
setOpen(false);
};
@@ -524,7 +534,7 @@ function Reactions() {
exit={{ opacity: 0 }}
className={cn("absolute z-40 pointer-events-none", REACTION_POS[b.seat])}
>
<span className="text-4xl drop-shadow-lg">{b.emoji}</span>
<ReactionBubble value={b.emoji} />
</motion.div>
))}
@@ -535,17 +545,54 @@ function Reactions() {
initial={{ opacity: 0, y: 12, scale: 0.95 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 12, scale: 0.95 }}
className="absolute bottom-20 ltr:right-4 rtl:left-4 z-50 glass rounded-2xl p-2 grid grid-cols-5 gap-1 max-w-[260px]"
className="absolute bottom-20 ltr:right-4 rtl:left-4 z-50 glass rounded-2xl p-2 w-[270px]"
>
{list.map((emoji, i) => (
<div className="flex gap-1 p-1 rounded-xl bg-navy-900/70 mb-2">
<button
key={`${emoji}-${i}`}
onClick={() => send(emoji)}
className="size-10 rounded-xl hover:bg-navy-800 transition flex items-center justify-center text-2xl"
onClick={() => setTab("emoji")}
className={cn(
"flex-1 rounded-lg py-1.5 text-xs font-bold transition",
tab === "emoji" ? "btn-gold" : "text-cream/60"
)}
>
{emoji}
{t("reactions.title")}
</button>
))}
<button
onClick={() => setTab("sticker")}
className={cn(
"flex-1 rounded-lg py-1.5 text-xs font-bold transition",
tab === "sticker" ? "btn-gold" : "text-cream/60"
)}
>
{t("stickers.title")}
</button>
</div>
{tab === "emoji" ? (
<div className="grid grid-cols-5 gap-1">
{emojis.map((emoji, i) => (
<button
key={`${emoji}-${i}`}
onClick={() => send(emoji)}
className="size-10 rounded-xl hover:bg-navy-800 transition flex items-center justify-center text-2xl"
>
{emoji}
</button>
))}
</div>
) : (
<div className="grid grid-cols-4 gap-1">
{stickers.map((id) => (
<button
key={id}
onClick={() => send(`sticker:${id}`)}
className="rounded-xl hover:bg-navy-800 transition flex items-center justify-center p-1"
>
<Sticker id={id} size={48} />
</button>
))}
</div>
)}
</motion.div>
)}
</AnimatePresence>