Subscription store, paywall on locked units, profile subscription bar, Tadrisino branding

- Store opens with two choices (subscription / question packs); 3- and 6-month cards
  show per-month price and saving.
- Units that need a subscription show a crown badge and open the store; a 402 from
  the quiz goes there too. «ادامه بده» skips locked units.
- Profile: subscription card with an animated days-left bar (planDurationDays).
- BrandMark (SVG) on splash, home header and footer; copyright footer; floating math
  glyphs on splash. CSS transform/opacity only; honours reduced motion.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-10 01:19:09 +03:30
parent 8879b42b1c
commit a8a5351862
15 changed files with 320 additions and 71 deletions

View File

@@ -1,6 +1,6 @@
import { useState } from 'react';
import { motion } from 'motion/react';
import { Brain, Check, ChevronDown, Lock, PartyPopper, Play, Sparkles } from 'lucide-react';
import { Brain, Check, ChevronDown, Crown, Lock, PartyPopper, Play, Sparkles } from 'lucide-react';
import { api, type HomeNode, type HomeUnit } from '@/api';
import { useAppData } from '@/state/AppData';
import { findContinue, unitColor, unitProgress } from '@/state/curriculum';
@@ -115,6 +115,8 @@ function UnitCard({
const complete = p.total > 0 && p.done === p.total;
const untouched = unit.nodes.every((n) => n.status === 'locked');
const Icon = iconFor(unit.nodes[0]?.icon, index);
const nav = useNav();
const premium = unit.requiresSubscription;
return (
<motion.div
initial={{ opacity: 0, y: 12 }}
@@ -123,7 +125,7 @@ function UnitCard({
className="overflow-hidden rounded-2xl bg-white"
style={{ border: `2px solid ${open ? color : '#e9e6f7'}`, boxShadow: open ? `0 4px 0 ${color}40` : '0 2px 0 #e9e6f7' }}
>
<button type="button" onClick={onToggle} aria-expanded={open} className="flex w-full items-center gap-3 px-3.5 py-3.5 text-right">
<button type="button" onClick={premium ? () => nav.push({ name: 'subscription', view: 'subscription' }) : onToggle} aria-expanded={open} className="flex w-full items-center gap-3 px-3.5 py-3.5 text-right">
<span
className="flex h-[52px] w-[52px] flex-shrink-0 items-center justify-center rounded-2xl"
style={{ backgroundColor: complete ? color : `${color}22`, color: complete ? '#fff' : color, opacity: untouched ? 0.55 : 1 }}
@@ -133,6 +135,7 @@ function UnitCard({
<span className="min-w-0 flex-1">
<span className="block text-[11px] font-bold" style={{ color }}>
{complete ? <><PartyPopper size={12} className="mb-0.5 ml-1 inline" />تموم شد</> : `${toFa(p.total)} جزیره`}
{premium && <span className="mr-1.5 inline-flex items-center gap-0.5 rounded-full bg-[#FFF4D6] px-1.5 text-[10px] text-[#A86A00]"><Crown size={10} /> با اشتراک</span>}
</span>
<span className="block text-sm font-black leading-6 text-ink"><MathText text={unit.title} /></span>
<span className="mt-1 flex items-center gap-2">
@@ -140,9 +143,9 @@ function UnitCard({
<span className="text-[11px] text-muted">{toFa(p.done)}/{toFa(p.total)}</span>
</span>
</span>
<ChevronDown size={18} className={`flex-shrink-0 text-muted transition-transform ${open ? 'rotate-180' : ''}`} />
{premium ? <Crown size={18} className="flex-shrink-0 text-[#E0A200]" /> : <ChevronDown size={18} className={`flex-shrink-0 text-muted transition-transform ${open ? 'rotate-180' : ''}`} />}
</button>
{open && (
{open && !premium && (
<ul className="flex flex-col gap-1.5 border-t border-line bg-[#fbfaff] px-3 py-3">
{unit.nodes.map((node, ni) => (
<NodeRow