Add gamification, micro-animations, and a mini-game for empty states

Applies a few Hook Model (trigger/action/reward/investment) patterns on top
of the existing swipe/match loop:

- Daily streak: a flame badge in the home header, backed by the new
  checkin endpoint — an external trigger to return daily.
- Profile completeness meter + badge collection on the profile page
  (client-computed from existing data) — the investment loop nudging
  users to fill out bio/photos/answers.
- Bio editor on the profile page, shown on swipe cards and candidate
  detail so profiles read richer.
- Catch the Hearts: a small tap mini-game (falling hearts, bonus items,
  20s timer, persisted high score) shown instead of a dead end when the
  discover feed runs out, so there's still a reason to stay in the app.
- Micro-animations: heart-burst on like, confetti on match, tap feedback
  on all buttons (active:scale-95).

Fixed a React "purity" lint violation along the way (Math.random() can't
be called inside useMemo under this project's rules — moved to useState
lazy initializers, which run once on mount as intended).
This commit is contained in:
2026-09-20 12:26:41 +03:30
parent 4bd7b93b4a
commit cbbfb145dc
11 changed files with 524 additions and 21 deletions
+25
View File
@@ -0,0 +1,25 @@
"use client";
import { motion, AnimatePresence } from "framer-motion";
import { Flame } from "lucide-react";
// نشان استریک روزانه؛ تریگر بیرونی کلاسیک برای برگشت روزانه‌ی کاربر (Hook Model).
// وقتی عدد عوض می‌شه یه بار کوچیک pop می‌زنه تا حس جایزه گرفتن بده.
export function StreakBadge({ count }: { count: number }) {
if (count <= 0) return null;
return (
<AnimatePresence mode="popLayout">
<motion.div
key={count}
initial={{ scale: 0.6, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ type: "spring", stiffness: 400, damping: 15 }}
className="flex items-center gap-1 rounded-(--radius-full) bg-amber-50 border border-amber-200 px-2.5 py-1 text-amber-600"
>
<Flame size={13} fill="currentColor" className="text-amber-500" />
<span className="text-[12px] font-extrabold">{count.toLocaleString("fa-IR")}</span>
</motion.div>
</AnimatePresence>
);
}