| import { useState, useMemo, useEffect } from 'react'; |
| import { Link } from 'react-router-dom'; |
| import { getAdminPrograms, type AdminProgramSummary } from '../../lib/admin'; |
| import { CAMPAIGN_STATUS } from '../../lib/campaignStatusConfig'; |
|
|
| const formatKRW = (n: number) => |
| n === 0 ? 'โ' : new Intl.NumberFormat('ko-KR').format(n) + '์'; |
|
|
| const formatNum = (n: number) => |
| n === 0 ? 'โ' : n.toLocaleString('ko-KR'); |
|
|
| function ProgressBar({ current, target }: { current: number; target: number }) { |
| const pct = target === 0 ? 0 : Math.min(100, (current / target) * 100); |
| return ( |
| <div className="flex items-center gap-2"> |
| <div className="flex-1 h-1.5 bg-paper-gray"> |
| <div className="h-full bg-ink-black transition-all" style={{ width: `${pct}%` }} /> |
| </div> |
| <span className="text-xs text-ink-medium w-8 text-right">{pct.toFixed(0)}%</span> |
| </div> |
| ); |
| } |
|
|
| export default function ProgramsPage() { |
| const [searchTerm, setSearchTerm] = useState(''); |
| const [statusFilter, setStatusFilter] = useState('all'); |
| const [programs, setPrograms] = useState<AdminProgramSummary[]>([]); |
| const [loading, setLoading] = useState(true); |
|
|
| useEffect(() => { |
| getAdminPrograms() |
| .then(setPrograms) |
| .finally(() => setLoading(false)); |
| }, []); |
|
|
| const filteredPrograms = useMemo(() => { |
| return programs.filter((p) => { |
| const matchSearch = p.title.toLowerCase().includes(searchTerm.toLowerCase()) || |
| p.sponsorName.toLowerCase().includes(searchTerm.toLowerCase()); |
| const matchStatus = statusFilter === 'all' || p.status === statusFilter; |
| return matchSearch && matchStatus; |
| }); |
| }, [programs, searchTerm, statusFilter]); |
|
|
| if (loading) { |
| return <div className="p-12 text-center text-ink-light font-mono uppercase animate-pulse">ํ๋ก๊ทธ๋จ ๋ฐ์ดํฐ ๋๊ธฐํ ์ค... [SYNCHRONIZING...]</div>; |
| } |
|
|
| return ( |
| <div className="space-y-6"> |
| <div className="flex items-center justify-between border-b border-border-strong pb-4"> |
| <div> |
| <h2 className="text-2xl font-bold text-ink-black italic tracking-tight uppercase">์บ ํ์ธ ์ด์ ๊ด๋ฆฌ [CAMPAIGN OPERATIONS]</h2> |
| <p className="text-sm text-ink-medium mt-1">ํ๋ซํผ ์ ์ฒด ํ์ ๊ฐ์ถ ์บ ํ์ธ ๋ผ์ดํ์ฌ์ดํด ๊ด๋ฆฌ</p> |
| </div> |
| <Link |
| to="/admin/programs/new" |
| className="px-4 py-2 bg-ink-black text-paper-white text-xs font-bold hover:bg-accent-teal transition-colors uppercase tracking-widest rounded-sm border-2 border-ink-black" |
| > |
| + ์บ ํ์ธ ์์ฑ [CREATE] |
| </Link> |
| </div> |
| |
| {/* Filters */} |
| <div className="flex flex-wrap gap-4 items-center bg-paper-cream p-4 border-2 border-border-default rounded-sm"> |
| <div className="flex-1 min-w-[200px]"> |
| <input |
| type="text" |
| placeholder="์บ ํ์ธ๋ช
๋๋ ์คํฐ์ ๊ฒ์..." |
| value={searchTerm} |
| onChange={(e) => setSearchTerm(e.target.value)} |
| className="w-full px-3 py-2 text-xs border border-border-default bg-paper-white focus:outline-none focus:border-ink-black" |
| /> |
| </div> |
| <select |
| value={statusFilter} |
| onChange={(e) => setStatusFilter(e.target.value)} |
| className="px-3 py-2 text-xs border border-border-default bg-paper-white focus:outline-none" |
| > |
| <option value="all">์ ์ฒด ์ํ [ALL]</option> |
| <option value="active">์งํ์ค [ACTIVE]</option> |
| <option value="draft">์ค๋น์ค [DRAFT]</option> |
| <option value="completed">์ข
๋ฃ [COMPLETED]</option> |
| </select> |
| </div> |
| |
| {/* Program Grid */} |
| <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> |
| {filteredPrograms.map((p) => { |
| const st = CAMPAIGN_STATUS[p.status as keyof typeof CAMPAIGN_STATUS] || { label: p.status, cls: 'border-border-default' }; |
| return ( |
| <div key={p.id} className="border-2 border-ink-black bg-paper-white p-6 shadow-sm hover:shadow-md transition-shadow relative group rounded-sm"> |
| <div className="flex items-start justify-between mb-4"> |
| <div> |
| <span className={`text-[9px] font-bold px-1.5 py-0.5 border ${st.cls || 'border-border-default'} uppercase tracking-tighter mb-2 inline-block`}> |
| {st.label} |
| </span> |
| <span className="ml-1 text-[9px] font-bold px-1.5 py-0.5 border border-border-default text-ink-light uppercase tracking-tighter mb-2 inline-block"> |
| {p.source === 'db' ? 'DB' : 'DEMO'} |
| </span> |
| <h3 className="text-base font-bold text-ink-black group-hover:text-accent-teal transition-colors">{p.title}</h3> |
| <p className="text-xs text-ink-medium mt-0.5">{p.sponsorName}</p> |
| </div> |
| </div> |
| |
| <div className="space-y-4"> |
| <div> |
| <div className="flex justify-between text-[10px] text-ink-light uppercase font-bold mb-1 font-mono"> |
| <span>์งํ ํํฉ [PROGRESS]</span> |
| <span>{formatNum(p.currentCo2Kg)} / {formatNum(p.targetCo2Kg)} kg</span> |
| </div> |
| <ProgressBar current={p.currentCo2Kg} target={p.targetCo2Kg} /> |
| </div> |
| |
| <div className="grid grid-cols-2 gap-4 border-t border-border-default pt-4"> |
| <div> |
| <p className="text-[9px] text-ink-light uppercase font-bold">์ฐธ์ฌ์ [PARTICIPANTS]</p> |
| <p className="text-sm font-bold text-ink-black">{formatNum(p.participantCount)}</p> |
| </div> |
| <div> |
| <p className="text-[9px] text-ink-light uppercase font-bold">์์ฐ [BUDGET]</p> |
| <p className="text-sm font-bold text-ink-black font-mono">{formatKRW(p.budgetKrw)}</p> |
| </div> |
| </div> |
| </div> |
| |
| <div className="mt-6 pt-4 border-t border-dotted border-border-default flex justify-between items-center"> |
| <p className="text-[10px] text-ink-light font-mono italic"> |
| {p.startDate} ~ {p.endDate || 'Ongoing'} |
| </p> |
| <Link |
| to={`/admin/programs/${p.id}`} |
| className="text-[10px] font-bold text-ink-black hover:text-accent-teal underline underline-offset-2 uppercase" |
| > |
| ๊ด๋ฆฌํ๊ธฐ [MANAGE] → |
| </Link> |
| </div> |
| </div> |
| ); |
| })} |
| </div> |
| </div> |
| ); |
| } |
|
|