Files
slot/next-app/apps/web/src/components/home/StatStrip.tsx
T
chpark 59001dbc5f Add 201 React deploy + admin catch-all + redesign + tests
Stack on 201: PG 17 + Next.js 15 (Docker) + nginx (/, /php-ref/)
Home: StatStrip (8 metrics), LiveActivity feed, refined Hero aurora
Admin: 80+ menu catch-all renderer + read-only legacy table queries
Auth/CRUD: fix narrowing in 6 action routes, fix wr_last varchar(19),
  fix back() new URL on missing referer
Verify: 50/50 PASS across 5 iterations of login + comment + good + scrap

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 02:44:18 +09:00

61 lines
3.2 KiB
TypeScript

'use client';
import { motion } from 'framer-motion';
import { Users, FileText, MessageSquare, Eye, ShieldCheck, AlertTriangle, Coins, Activity } from 'lucide-react';
export interface StatStripProps {
members: number;
posts: number;
comments: number;
visitsToday: number;
visitsTotal: number;
guarantees: number;
muktiReports: number;
pointsCirculating: number;
}
const fmt = (n: number) => n.toLocaleString();
const compact = (n: number) =>
n >= 1_000_000_000 ? (n / 1_000_000_000).toFixed(1) + 'B' :
n >= 1_000_000 ? (n / 1_000_000).toFixed(1) + 'M' :
n >= 1_000 ? (n / 1_000).toFixed(1) + 'K' : String(n);
export default function StatStrip({ members, posts, comments, visitsToday, visitsTotal, guarantees, muktiReports, pointsCirculating }: StatStripProps) {
const stats = [
{ icon: Users, label: '활성 회원', value: fmt(members), sub: '실가입', tone: 'from-violet-500 to-fuchsia-600' },
{ icon: FileText, label: '누적 게시글', value: compact(posts), sub: `자유게시판`, tone: 'from-sky-500 to-blue-700' },
{ icon: MessageSquare, label: '전체 댓글', value: compact(comments), sub: '커뮤니티', tone: 'from-emerald-500 to-teal-600' },
{ icon: Eye, label: '오늘 방문', value: compact(visitsToday), sub: `누적 ${compact(visitsTotal)}`, tone: 'from-amber-500 to-orange-600' },
{ icon: ShieldCheck, label: '보증 사이트', value: fmt(guarantees), sub: '검수 완료', tone: 'from-green-500 to-emerald-700' },
{ icon: AlertTriangle, label: '먹튀 신고', value: fmt(muktiReports), sub: '신고 누적', tone: 'from-rose-500 to-red-700' },
{ icon: Coins, label: '유통 포인트', value: compact(pointsCirculating), sub: 'p', tone: 'from-yellow-500 to-amber-700' },
{ icon: Activity, label: '실시간', value: 'LIVE', sub: '24/7 모니터링', tone: 'from-pink-500 to-rose-600' },
];
return (
<section className="grid grid-cols-2 gap-2.5 sm:grid-cols-4 lg:grid-cols-8">
{stats.map((s, i) => {
const Icon = s.icon;
return (
<motion.div
key={s.label}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: i * 0.04, duration: 0.4 }}
className="lift relative overflow-hidden rounded-2xl bg-white p-3.5 ring-1 ring-neutral-100"
>
<div className={`absolute -top-6 -right-6 h-20 w-20 rounded-full bg-gradient-to-br ${s.tone} opacity-20 blur-2xl`} />
<div className="flex items-start justify-between">
<span className={`grid h-9 w-9 place-items-center rounded-xl bg-gradient-to-br ${s.tone} text-white shadow-[0_6px_14px_rgba(0,0,0,0.10)]`}>
<Icon size={16} />
</span>
<span className="text-[10px] font-semibold uppercase tracking-wider text-neutral-text-soft">{s.sub}</span>
</div>
<div className="mt-2 text-[20px] font-extrabold tabular text-neutral-900">{s.value}</div>
<div className="text-[11px] font-medium text-neutral-text-soft">{s.label}</div>
</motion.div>
);
})}
</section>
);
}