Polish: daily reward via celebration overlay + premium chat to recipient
CI/CD / CI - API (dotnet build + engine sim) (push) Has been cancelled
CI/CD / CI - Web (tsc + next build) (push) Has been cancelled
CI/CD / Deploy - local stack (db + server + web) (push) Has been cancelled

- Daily reward now routes through the global CelebrationOverlay: new "daily"
  variant + coins count-up; claiming closes the daily modal and fires
  celebrate({variant:"daily", coins}). Unifies the "you earned X" moment.
- Premium (pro) gold chat is now visible to the OTHER player: ChatMessage gains
  senderPro; server resolves each participant's plan once (SocialService.IsPro)
  and stamps it on ChatMessageDto; ChatScreen styles incoming bubbles with
  .premium-chat when senderPro. Mock marks ~half its friends pro so it's visible
  offline too.

Verified: tsc + next build + dotnet build all pass.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
soroush.asadi
2026-06-06 22:26:28 +03:30
parent 03dfbe1e67
commit 82b2bc0648
8 changed files with 45 additions and 7 deletions
+15 -1
View File
@@ -56,6 +56,7 @@ function Card() {
const dismiss = useCelebrationStore((s) => s.dismiss);
const leveled = (current.levelAfter ?? 0) > (current.levelBefore ?? 0);
const xp = useCountUp(current.xpGained ?? 0, 900, current.variant === "xp");
const coins = useCountUp(current.coins ?? 0, 1000, (current.coins ?? 0) > 0);
return (
<motion.div
@@ -82,13 +83,26 @@ function Card() {
transition={{ type: "spring", stiffness: 180, delay: 0.1 }}
className="relative text-6xl mb-1"
>
{current.icon ?? (current.variant === "xp" ? "⚡" : "🛍️")}
{current.icon ?? (current.variant === "xp" ? "⚡" : current.variant === "daily" ? "🎁" : "🛍️")}
</motion.div>
{current.title && (
<h2 className="relative gold-text text-xl font-black mt-1">{current.title}</h2>
)}
{/* coins gained (daily reward, etc.) */}
{(current.coins ?? 0) > 0 && (
<motion.div
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 200, delay: 0.15 }}
className="relative mt-3 inline-flex items-center gap-2 btn-gold rounded-full px-5 py-2 font-black text-lg"
>
<Coins className="size-5" />
+{coins.toLocaleString()}
</motion.div>
)}
{/* XP gain */}
{current.variant === "xp" && (current.xpGained ?? 0) > 0 && (
<div className="relative mt-3">
+4 -1
View File
@@ -8,6 +8,7 @@ import { useUIStore } from "@/lib/ui-store";
import { useI18n } from "@/lib/i18n";
import { DAILY_REWARDS } from "@/lib/online/gamification";
import { getService } from "@/lib/online/service";
import { celebrate } from "@/lib/celebration-store";
import { sound } from "@/lib/sound";
import { DailyRewardState } from "@/lib/online/types";
import { cn } from "@/lib/cn";
@@ -48,7 +49,9 @@ export function DailyRewardModal() {
setClaimed(res.reward);
sound.play("award");
await refreshProfile();
setState(await getService().getDailyState());
// Hand off to the global celebration overlay (animated coin count-up).
close();
celebrate({ variant: "daily", title: t("daily.title"), coins: res.reward });
};
const isMegaDay = state?.day === 7;
+1 -1
View File
@@ -95,7 +95,7 @@ export function ChatScreen() {
"max-w-[78%] rounded-2xl px-3.5 py-2 text-sm",
m.fromMe
? cn("ms-auto rounded-ee-sm", isPro ? "premium-chat" : "btn-gold")
: "me-auto glass text-cream rounded-es-sm"
: cn("me-auto rounded-es-sm", m.senderPro ? "premium-chat" : "glass text-cream")
)}
>
{m.text}