diff --git a/src/components/online/NavRail.tsx b/src/components/online/NavRail.tsx index 5671143..f0c2d11 100644 --- a/src/components/online/NavRail.tsx +++ b/src/components/online/NavRail.tsx @@ -1,12 +1,14 @@ "use client"; import { Home, ShoppingBag, Star, Trophy, User, Users } from "lucide-react"; +import { useEffect } from "react"; import { useUIStore, type Screen } from "@/lib/ui-store"; import { useSessionStore } from "@/lib/session-store"; +import { useOnlineStore } from "@/lib/online-store"; import { useI18n } from "@/lib/i18n"; import { cn } from "@/lib/cn"; -type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean }; +type Item = { key: Screen; icon: typeof Home; label: string; authed?: boolean; badge?: number }; /** * UNO-style primary navigation. A vertical rail pinned to the side in landscape @@ -17,13 +19,26 @@ export function NavRail({ bottom = false }: { bottom?: boolean }) { const screen = useUIStore((s) => s.screen); const go = useUIStore((s) => s.go); const isAuthed = useSessionStore((s) => s.isAuthed); + const unread = useOnlineStore((s) => s.unread); + const requests = useOnlineStore((s) => s.requests); + const refreshUnread = useOnlineStore((s) => s.refreshUnread); const { t } = useI18n(); + // Keep the Friends badge live (unread chats + friend requests) on every screen. + useEffect(() => { + if (!isAuthed) return; + refreshUnread(); + const id = setInterval(refreshUnread, 12000); + return () => clearInterval(id); + }, [isAuthed, refreshUnread]); + + const friendsBadge = unread + requests.length; + const all: Item[] = [ { key: "home", icon: Home, label: t("nav.home") }, { key: "profile", icon: User, label: t("menu.profile") }, { key: "shop", icon: ShoppingBag, label: t("menu.shop") }, - { key: "friends", icon: Users, label: t("menu.friends"), authed: true }, + { key: "friends", icon: Users, label: t("menu.friends"), authed: true, badge: friendsBadge }, { key: "leaderboard", icon: Trophy, label: t("menu.leaderboard") }, { key: "achievements", icon: Star, label: t("achv.title") }, ]; @@ -52,11 +67,16 @@ export function NavRail({ bottom = false }: { bottom?: boolean }) { key={it.key} onClick={() => go(it.authed && !isAuthed ? "auth" : it.key)} className={cn( - "flex min-w-[54px] flex-col items-center justify-center gap-1 rounded-2xl px-1.5 py-2 transition", + "relative flex min-w-[54px] flex-col items-center justify-center gap-1 rounded-2xl px-1.5 py-2 transition", !bottom && "landscape:w-full landscape:py-2.5", active ? "btn-gold shadow-lg" : "text-cream/55 hover:bg-navy-800/60 hover:text-cream" )} > + {!!it.badge && it.badge > 0 && ( + + {it.badge > 9 ? "9+" : it.badge} + + )} {it.label} diff --git a/src/components/online/PostMatchRewardsModal.tsx b/src/components/online/PostMatchRewardsModal.tsx index 3126b91..4cdba41 100644 --- a/src/components/online/PostMatchRewardsModal.tsx +++ b/src/components/online/PostMatchRewardsModal.tsx @@ -77,13 +77,13 @@ export function PostMatchRewardsModal({ {/* radiating bg glow */}
{won ? "🏆" : "🎴"} -

{t("reward.title")}

-

+

{t("reward.title")}

+

{won ? t("reward.win") : t("reward.lose")}

@@ -114,9 +114,9 @@ export function PostMatchRewardsModal({ initial={{ scale: 0.5, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} transition={{ type: "spring", stiffness: 200, damping: 14, delay: 0.18 }} - className="relative mt-4 flex items-center justify-center gap-2" + className="relative mt-4 short:mt-2 flex items-center justify-center gap-2" > - + + )} -
+
{reward.ratingDelta !== 0 && ( 0 diff --git a/src/components/screens/ChatScreen.tsx b/src/components/screens/ChatScreen.tsx index 7d37052..2db26a4 100644 --- a/src/components/screens/ChatScreen.tsx +++ b/src/components/screens/ChatScreen.tsx @@ -1,12 +1,13 @@ "use client"; -import { ChevronLeft, ChevronRight, MessageCircle, Send } from "lucide-react"; +import { ChevronLeft, ChevronRight, MessageCircle, Send, Smile } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { useOnlineStore } from "@/lib/online-store"; import { useSessionStore } from "@/lib/session-store"; import { useUIStore } from "@/lib/ui-store"; import { useI18n } from "@/lib/i18n"; import { sound } from "@/lib/sound"; +import { ownedReactions } from "@/lib/online/gamification"; import { avatarEmoji } from "@/lib/online/types"; import { cn } from "@/lib/cn"; @@ -16,10 +17,13 @@ export function ChatScreen() { const messages = useOnlineStore((s) => s.chatMessages); const sendChat = useOnlineStore((s) => s.sendChat); const closeChat = useOnlineStore((s) => s.closeChat); - const isPro = useSessionStore((s) => s.profile?.plan === "pro"); + const profile = useSessionStore((s) => s.profile); + const isPro = profile?.plan === "pro"; const navBack = useUIStore((s) => s.back); const viewProfile = useUIStore((s) => s.viewProfile); const [text, setText] = useState(""); + const [showEmoji, setShowEmoji] = useState(false); + const emojis = profile ? ownedReactions(profile) : []; const endRef = useRef(null); const prevLen = useRef(0); const Chevron = locale === "fa" ? ChevronRight : ChevronLeft; @@ -49,6 +53,11 @@ export function ChatScreen() { await sendChat(v); }; + const sendEmoji = async (e: string) => { + setShowEmoji(false); + await sendChat(e); + }; + return (
@@ -106,21 +115,45 @@ export function ChatScreen() {
{/* input */} -
- setText(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && send()} - placeholder={t("chat.placeholder")} - className="flex-1 rounded-full bg-navy-900/70 gold-border px-4 py-2.5 text-cream placeholder:text-cream/30 outline-none focus:ring-2 focus:ring-gold-500/40" - /> - +
+ {/* emoji / sticker picker */} + {showEmoji && emojis.length > 0 && ( +
+ {emojis.map((e, i) => ( + + ))} +
+ )} +
+ + setShowEmoji(false)} + onChange={(e) => setText(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && send()} + placeholder={t("chat.placeholder")} + className="flex-1 min-w-0 rounded-full bg-navy-900/70 gold-border px-4 py-2.5 text-cream placeholder:text-cream/30 outline-none focus:ring-2 focus:ring-gold-500/40" + /> + +
diff --git a/src/components/screens/FriendsScreen.tsx b/src/components/screens/FriendsScreen.tsx index 29b71cc..ccba1cd 100644 --- a/src/components/screens/FriendsScreen.tsx +++ b/src/components/screens/FriendsScreen.tsx @@ -42,12 +42,15 @@ type Tab = "friends" | "discover" | "messages"; export function FriendsScreen() { const { t } = useI18n(); const requests = useOnlineStore((s) => s.requests); + const unread = useOnlineStore((s) => s.unread); const load = useOnlineStore((s) => s.loadFriends); + const refreshUnread = useOnlineStore((s) => s.refreshUnread); const [tab, setTab] = useState("friends"); useEffect(() => { load(); - }, [load]); + refreshUnread(); + }, [load, refreshUnread]); return ( @@ -57,7 +60,7 @@ export function FriendsScreen() {
setTab("friends")} icon={} label={t("social.tabFriends")} badge={requests.length} /> setTab("discover")} icon={} label={t("social.tabDiscover")} /> - setTab("messages")} icon={} label={t("social.tabMessages")} /> + setTab("messages")} icon={} label={t("social.tabMessages")} badge={unread} />
diff --git a/src/lib/i18n.tsx b/src/lib/i18n.tsx index 79427fe..2305799 100644 --- a/src/lib/i18n.tsx +++ b/src/lib/i18n.tsx @@ -151,6 +151,7 @@ const fa: Dict = { "chat.title": "گفتگو", "chat.placeholder": "پیام بنویسید…", "chat.send": "ارسال", + "chat.emoji": "ایموجی", "chat.empty": "گفتگو را شروع کنید", "friends.message": "پیام", @@ -315,7 +316,7 @@ const fa: Dict = { "shop.titles": "عناوین", "shop.titlesHint": "عنوان شما زیر نامتان در بازی و لیست‌ها نمایش داده می‌شود", "shop.xp": "امتیاز تجربه (XP)", - "shop.xpHint": "افزایش سریع سطح — XP گران است", + "shop.xpHint": "افزایش سریع سطح", "shop.includes": "شامل", "shop.reqLevel": "سطح", "shop.reqRating": "امتیاز", @@ -501,6 +502,7 @@ const en: Dict = { "chat.title": "Chat", "chat.placeholder": "Type a message…", "chat.send": "Send", + "chat.emoji": "Emoji", "chat.empty": "Start the conversation", "friends.message": "Message", @@ -662,7 +664,7 @@ const en: Dict = { "shop.titles": "Titles", "shop.titlesHint": "Your title shows under your name in games & lists", "shop.xp": "XP packs", - "shop.xpHint": "Level up faster — XP is expensive", + "shop.xpHint": "Level up faster", "shop.includes": "Includes", "shop.reqLevel": "Level", "shop.reqRating": "Rating", diff --git a/src/lib/online-store.ts b/src/lib/online-store.ts index f1221d7..1107781 100644 --- a/src/lib/online-store.ts +++ b/src/lib/online-store.ts @@ -41,9 +41,11 @@ interface OnlineStore { // chat activeChatFriend: Friend | null; chatMessages: ChatMessage[]; + unread: number; // total unread messages across conversations (for nav badge) openChat: (friend: Friend) => Promise; sendChat: (text: string) => Promise; closeChat: () => void; + refreshUnread: () => Promise; } let roomUnsub: (() => void) | null = null; @@ -154,11 +156,22 @@ export const useOnlineStore = create((set, get) => ({ activeChatFriend: null, chatMessages: [], + unread: 0, + + refreshUnread: async () => { + try { + const convs = await getService().listConversations(); + set({ unread: convs.reduce((n, c) => n + (c.unread ?? 0), 0) }); + } catch { + /* ignore */ + } + }, openChat: async (friend) => { const svc = getService(); set({ activeChatFriend: friend, chatMessages: await svc.getMessages(friend.id) }); await svc.markRead(friend.id); + get().refreshUnread(); if (chatUnsub) chatUnsub(); chatUnsub = svc.onChat((friendId, msgs) => { const active = get().activeChatFriend; @@ -182,5 +195,6 @@ export const useOnlineStore = create((set, get) => ({ chatUnsub = null; } set({ activeChatFriend: null, chatMessages: [] }); + get().refreshUnread(); }, }));