Ilo/design/screen-home.jsx
2026-05-02 20:01:30 +03:30

326 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Dashboard / Home screen.
// Three composition variants controlled by the `variant` prop:
// "classic" — KPI row + activity + side rail
// "focused" — Today-first hero (next live session, top tasks) + KPIs below
// "studio" — left = courses, right = students/inbox; KPIs as compact strip
function Sparkline({ data, color = 'var(--accent)' }) {
const w = 120, h = 32, p = 2;
const min = Math.min(...data), max = Math.max(...data);
const span = Math.max(1, max - min);
const pts = data.map((v, i) => {
const x = p + (i * (w - p * 2)) / (data.length - 1);
const y = h - p - ((v - min) / span) * (h - p * 2);
return [x, y];
});
const d = pts.map((pt, i) => (i === 0 ? `M${pt[0]},${pt[1]}` : `L${pt[0]},${pt[1]}`)).join(' ');
const area = d + ` L${w - p},${h - p} L${p},${h - p} Z`;
return (
<svg width={w} height={h} viewBox={`0 0 ${w} ${h}`} style={{ display: 'block' }}>
<path d={area} fill={color} opacity="0.12" />
<path d={d} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function KPI({ k, compact }) {
return (
<div className="card card-pad" style={{ display: 'flex', flexDirection: 'column', gap: compact ? 6 : 12 }}>
<div className="muted" style={{ fontSize: 12 }}>{k.label}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, flexWrap: 'wrap' }}>
<span className="num" style={{ fontSize: compact ? 20 : 26, fontWeight: 700, letterSpacing: '-0.01em' }}>{k.value}</span>
<span className="muted" style={{ fontSize: 12 }}>{k.unit}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
<span className={`pill ${k.good ? 'ok' : 'warn'} num`}>
<Icon name="trend" className="ic-sm" />
{k.delta}
</span>
<Sparkline data={k.spark} color={k.good ? 'oklch(0.70 0.14 155)' : 'oklch(0.78 0.14 75)'} />
</div>
</div>
);
}
function ActivityFeed() {
const { ACTIVITY } = window.DATA;
const iconFor = { lesson: 'check', chat: 'chat', cert: 'cert', sale: 'star', quiz: 'quiz' };
return (
<div className="card">
<div className="card-h">
<h3>فعالیت اخیر دانشجویان</h3>
<button className="btn ghost sm">نمایش همه</button>
</div>
<div style={{ padding: '6px 0' }}>
{ACTIVITY.map(a => (
<div key={a.id} style={{ display: 'flex', gap: 12, padding: '12px 20px', alignItems: 'flex-start' }}>
<div className="av av-sm" style={{ background: a.color }}>{a.who.split(' ').map(s=>s[0]).join('').slice(0,2)}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13.5, lineHeight: 1.5 }}>
<b style={{ fontWeight: 600 }}>{a.who}</b>
<span className="muted"> · {a.did}</span>
</div>
<div className="muted" style={{ fontSize: 11.5, marginTop: 2 }}>
{a.course} · {a.t}
</div>
</div>
<div className="iconbtn" style={{ width: 28, height: 28, color: a.color }}>
<Icon name={iconFor[a.kind]} className="ic-sm" />
</div>
</div>
))}
</div>
</div>
);
}
function NextLiveCard({ hero }) {
const { LIVE } = window.DATA;
const next = LIVE[0];
return (
<div className="card card-pad" style={{
background: hero
? 'linear-gradient(135deg, var(--accent-soft), color-mix(in oklch, var(--accent-soft) 50%, var(--surface)))'
: 'var(--surface)',
borderColor: hero ? 'transparent' : 'var(--line)',
}}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 10 }}>
<span className="pill accent"><span className="dot-i" />جلسه زندهی بعدی</span>
<span className="muted" style={{ fontSize: 12 }}>{next.at}</span>
</div>
<div style={{ fontSize: hero ? 22 : 18, fontWeight: 700, letterSpacing: '-0.01em', marginBottom: 4 }}>
{next.title}
</div>
<div className="muted" style={{ fontSize: 13, marginBottom: 16 }}>{next.course}</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
<button className="btn primary"><Icon name="video" className="ic-sm" /> شروع جلسه</button>
<button className="btn"><Icon name="link" className="ic-sm" /> کپی لینک</button>
<div style={{ marginInlineStart: 'auto', display: 'flex', alignItems: 'center', gap: 8 }}>
<div style={{ display: 'flex' }}>
{['#e8a07a','#7aa6c4','#a8c47a'].map((c,i) => (
<div key={i} className="av av-sm" style={{ background: c, marginInlineStart: i ? -8 : 0, border: '2px solid var(--surface)' }} />
))}
</div>
<span className="muted num" style={{ fontSize: 12 }}>{next.registered} ثبتنام</span>
</div>
</div>
</div>
);
}
function TodayCard() {
const items = [
{ ic: 'check', t: 'پاسخ به ۳ پرسش جدید در گفت‌وگوی دوره', done: false },
{ ic: 'edit', t: 'ویرایش درس «گرید و سیستم لی‌اوت»', done: false },
{ ic: 'send', t: 'ارسال پیام خوش‌آمد به ۱۲ دانشجوی جدید', done: true },
{ ic: 'star', t: 'مرور تمرین‌های ارسال‌شده فصل دوم', done: false },
];
return (
<div className="card">
<div className="card-h">
<h3>کارهای امروز</h3>
<span className="muted" style={{ fontSize: 12 }}>۳ از ۴ باقیمانده</span>
</div>
<div style={{ padding: '6px 0' }}>
{items.map((it, i) => (
<div key={i} style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 20px' }}>
<input type="checkbox" className="chk" defaultChecked={it.done} />
<Icon name={it.ic} className="ic-sm" style={{ color: 'var(--ink-3)' }} />
<div style={{ flex: 1, fontSize: 13.5, textDecoration: it.done ? 'line-through' : 'none', color: it.done ? 'var(--ink-3)' : 'var(--ink)' }}>
{it.t}
</div>
</div>
))}
</div>
</div>
);
}
function CoursesMini() {
const { COURSES } = window.DATA;
return (
<div className="card">
<div className="card-h">
<h3>دورههای من</h3>
<button className="btn ghost sm">همه</button>
</div>
<div style={{ display: 'flex', flexDirection: 'column' }}>
{COURSES.map((c, i) => (
<div key={c.id} style={{ display: 'flex', alignItems: 'center', gap: 14, padding: '14px 20px', borderTop: i ? '1px solid var(--line-soft)' : 'none' }}>
<div style={{ width: 42, height: 42, borderRadius: 10, background: c.color, flexShrink: 0,
display: 'grid', placeItems: 'center', color: 'white', fontWeight: 700 }}>
<Icon name="book" />
</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ fontSize: 13.5, fontWeight: 600, marginBottom: 4, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
{c.title}
</div>
<div className="muted num" style={{ fontSize: 11.5 }}>
{c.status === 'draft'
? `پیش‌نویس · ${c.progress}٪ آماده`
: `${c.students} دانشجو · ⭐ ${c.rating}`}
</div>
</div>
{c.status === 'draft'
? <span className="pill warn">پیشنویس</span>
: <span className="pill ok">منتشر شده</span>}
</div>
))}
</div>
</div>
);
}
function InboxMini() {
const { INBOX } = window.DATA;
return (
<div className="card">
<div className="card-h">
<h3>پیامهای اخیر</h3>
<span className="pill accent num">۳ خواندهنشده</span>
</div>
<div>
{INBOX.map((m, i) => (
<div key={m.id} style={{ display: 'flex', gap: 12, padding: '12px 20px', borderTop: i ? '1px solid var(--line-soft)' : 'none', alignItems: 'flex-start', cursor: 'pointer' }}>
<div className="av av-sm" style={{ background: m.color }}>{m.name.split(' ').map(s=>s[0]).join('').slice(0,2)}</div>
<div style={{ flex: 1, minWidth: 0 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', gap: 8 }}>
<b style={{ fontSize: 13, fontWeight: m.unread ? 600 : 500 }}>{m.name}</b>
<span className="muted num" style={{ fontSize: 11 }}>{m.t}</span>
</div>
<div style={{ fontSize: 12.5, color: m.unread ? 'var(--ink-2)' : 'var(--ink-3)',
whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', marginTop: 2 }}>
{m.msg}
</div>
</div>
{m.unread && <span style={{ width: 7, height: 7, borderRadius: '50%', background: 'var(--accent)', marginTop: 6 }} />}
</div>
))}
</div>
</div>
);
}
function RevenueChart() {
const months = ['فر', 'ار', 'خر', 'تی', 'مر', 'شه', 'مه', 'آب', 'آذ', 'دی', 'به', 'اس'];
const vals = [8, 11, 14, 12, 16, 18, 22, 20, 24, 26, 25, 28];
const max = Math.max(...vals);
return (
<div className="card">
<div className="card-h">
<div>
<h3>درآمد ماهانه</h3>
<div className="muted" style={{ fontSize: 12, marginTop: 2 }}>۱۲ ماه گذشته · میلیون تومان</div>
</div>
<div style={{ display: 'flex', gap: 4 }}>
<button className="btn ghost sm">ماه</button>
<button className="btn sm" style={{ background: 'var(--surface-2)' }}>سال</button>
</div>
</div>
<div style={{ padding: '24px 20px 16px', display: 'flex', alignItems: 'flex-end', gap: 8, height: 180 }}>
{vals.map((v, i) => (
<div key={i} style={{ flex: 1, display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 6 }}>
<div style={{ width: '100%', height: '100%', display: 'flex', alignItems: 'flex-end' }}>
<div style={{
width: '100%',
height: `${(v / max) * 100}%`,
background: i === vals.length - 1
? 'linear-gradient(180deg, var(--accent), var(--accent-2))'
: 'var(--line)',
borderRadius: '6px 6px 0 0',
transition: 'all .2s',
}} />
</div>
<span className="muted" style={{ fontSize: 10.5 }}>{months[i]}</span>
</div>
))}
</div>
</div>
);
}
function GreetingHero() {
return (
<div style={{ display: 'flex', alignItems: 'flex-end', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
<div>
<div className="muted" style={{ fontSize: 13, marginBottom: 6 }}>سلام آرش 👋</div>
<h1 style={{ fontSize: 28, fontWeight: 700, letterSpacing: '-0.015em', margin: 0 }}>
امروز ۲۴ نفر در دورههایت در حال یادگیریاند
</h1>
</div>
<div style={{ display: 'flex', gap: 8 }}>
<button className="btn"><Icon name="upload" className="ic-sm" /> آپلود محتوا</button>
<button className="btn primary"><Icon name="plus" className="ic-sm" /> دورهی جدید</button>
</div>
</div>
);
}
function HomeScreen({ variant = 'classic' }) {
const { KPIS } = window.DATA;
if (variant === 'focused') {
return (
<div className="page-inner">
<GreetingHero />
<div style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: 'var(--gap-lg)' }}>
<NextLiveCard hero />
<TodayCard />
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 'var(--gap-md)' }}>
{KPIS.map(k => <KPI k={k} compact key={k.id} />)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 'var(--gap-lg)' }}>
<RevenueChart />
<ActivityFeed />
</div>
</div>
);
}
if (variant === 'studio') {
return (
<div className="page-inner">
<GreetingHero />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 'var(--gap-md)' }}>
{KPIS.map(k => <KPI k={k} compact key={k.id} />)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 'var(--gap-lg)' }}>
<div className="col">
<CoursesMini />
<NextLiveCard />
</div>
<div className="col">
<InboxMini />
<TodayCard />
</div>
</div>
<ActivityFeed />
</div>
);
}
// classic
return (
<div className="page-inner">
<GreetingHero />
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 'var(--gap-md)' }}>
{KPIS.map(k => <KPI k={k} key={k.id} />)}
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.6fr 1fr', gap: 'var(--gap-lg)' }}>
<RevenueChart />
<NextLiveCard />
</div>
<div style={{ display: 'grid', gridTemplateColumns: '1.4fr 1fr', gap: 'var(--gap-lg)' }}>
<ActivityFeed />
<div className="col">
<TodayCard />
<InboxMini />
</div>
</div>
</div>
);
}
window.HomeScreen = HomeScreen;