steelforesight/src/components/ui/EventCalendar.tsx

157 lines
7.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState } from 'react'
const NAVY = '#032340'
const GOLD = '#CD9E53'
const NEON = '#d4ff2e' // neon yellow for conference-date markers
type EventItem = {
g: Date // Gregorian date of the event
title: string
city: string
time: string
type: string
color: string
}
/* steel conferences during اردیبهشت ۱۴۰۵ (anchor month below) */
const EVENTS: EventItem[] = [
{ g: new Date(2026, 3, 28), title: 'کنفرانس فولاد خاورمیانه', city: 'دبی، امارات', time: '۰۹:۰۰ ۱۷:۰۰', type: 'کنفرانس بین‌المللی', color: '#9b1c1c' },
{ g: new Date(2026, 4, 5), title: 'نمایشگاه متالکس ۱۴۰۵', city: 'تهران، ایران', time: '۱۰:۰۰ ۱۸:۰۰', type: 'نمایشگاه', color: GOLD },
{ g: new Date(2026, 4, 12), title: 'نشست تخصصی زنجیره فولاد', city: 'اصفهان، ایران', time: '۱۴:۰۰ ۱۸:۰۰', type: 'نشست تخصصی', color: '#1e8449' },
{ g: new Date(2026, 4, 18), title: 'وبینار فولاد سبز و کربن‌زدایی', city: 'آنلاین', time: '۱۶:۰۰ ۱۸:۰۰', type: 'وبینار', color: '#2471a3' },
]
const ANCHOR = new Date(2026, 4, 10) // a day inside اردیبهشت ۱۴۰۵
const J_MONTHS = ['فروردین', 'اردیبهشت', 'خرداد', 'تیر', 'مرداد', 'شهریور', 'مهر', 'آبان', 'آذر', 'دی', 'بهمن', 'اسفند']
const WEEKDAYS = ['ش', 'ی', 'د', 'س', 'چ', 'پ', 'ج'] // شنبه … جمعه
const faDigits = (n: number) => n.toLocaleString('fa-IR', { useGrouping: false })
/* Jalali year/month/day for a Gregorian Date, via the browser's Persian calendar */
function jParts(d: Date) {
const parts = new Intl.DateTimeFormat('en-US-u-ca-persian', { day: 'numeric', month: 'numeric', year: 'numeric' }).formatToParts(d)
const get = (t: string) => Number(parts.find((p) => p.type === t)?.value)
return { day: get('day'), month: get('month'), year: get('year') }
}
const sameDay = (a: Date, b: Date) =>
a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
function buildMonth(anchor: Date) {
const { month, year } = jParts(anchor)
// walk back to the 1st of this Jalali month
const first = new Date(anchor)
while (jParts(first).day !== 1) first.setDate(first.getDate() - 1)
// collect every day until the Jalali month rolls over
const days: Date[] = []
const cur = new Date(first)
while (jParts(cur).month === month && jParts(cur).year === year) {
days.push(new Date(cur))
cur.setDate(cur.getDate() + 1)
}
const firstCol = (first.getDay() + 1) % 7 // Sat→0 … Fri→6
return { month, year, days, firstCol }
}
export default function EventCalendar() {
const { month, year, days, firstCol } = buildMonth(ANCHOR)
const [hover, setHover] = useState<number | null>(null)
const eventFor = (d: Date) => EVENTS.findIndex((e) => sameDay(e.g, d))
return (
<div style={{
background: '#fff', borderRadius: 18, padding: '22px 22px 18px',
boxShadow: '0 24px 60px -34px rgba(3,35,64,0.4)', border: '1px solid rgba(3,35,64,0.08)',
direction: 'rtl', maxWidth: 360, margin: '0 auto', width: '100%',
}}>
{/* header */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 16 }}>
<div style={{ fontSize: 9, fontWeight: 700, letterSpacing: '2px', color: GOLD }}>تقویم رویدادها</div>
<div style={{ fontSize: 16, fontWeight: 900, color: NAVY }}>{J_MONTHS[month - 1]} {faDigits(year)}</div>
</div>
{/* weekday row */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 2, marginBottom: 6 }}>
{WEEKDAYS.map((w, i) => (
<div key={i} style={{ textAlign: 'center', fontSize: 10, fontWeight: 700, color: 'rgba(3,35,64,0.45)', padding: '4px 0' }}>{w}</div>
))}
</div>
{/* day grid */}
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(7,1fr)', gap: 2 }}>
{Array.from({ length: firstCol }).map((_, i) => <div key={`b${i}`} />)}
{days.map((d, i) => {
const j = jParts(d)
const ev = eventFor(d)
const marked = ev >= 0
const isHover = hover === ev && marked
return (
<div key={i} style={{ position: 'relative', aspectRatio: '1', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<button
className={marked ? 'ev-breathe' : undefined}
onMouseEnter={() => marked && setHover(ev)}
onMouseLeave={() => setHover(null)}
style={{
position: 'relative', zIndex: 2,
width: 34, height: 34, borderRadius: '50%', border: 'none', cursor: marked ? 'pointer' : 'default',
fontFamily: 'inherit', fontSize: 13, fontWeight: marked ? 900 : 500,
color: marked ? NAVY : 'rgba(3,35,64,0.7)',
background: marked ? NEON : 'transparent',
transition: 'transform 160ms', transform: isHover ? 'scale(1.15)' : 'none',
}}
>
{faDigits(j.day)}
</button>
{/* breathing neon ring on conference days */}
{marked && (
<span style={{
position: 'absolute', width: 34, height: 34, borderRadius: '50%', pointerEvents: 'none',
border: `2px solid ${NEON}`, opacity: 0.6,
animation: 'evPulse 2s ease-out infinite',
}} />
)}
{/* hover card */}
{isHover && (
<div style={{
position: 'absolute', bottom: 'calc(100% + 8px)', left: '50%', transform: 'translateX(-50%)',
width: 220, background: NAVY, color: '#fff', borderRadius: 12, padding: '12px 14px', zIndex: 30,
boxShadow: '0 20px 50px -16px rgba(3,35,64,0.55)', textAlign: 'right',
}}>
<div style={{ display: 'inline-block', fontSize: 9, fontWeight: 700, color: NAVY, background: EVENTS[ev].color, borderRadius: 999, padding: '2px 8px', marginBottom: 8 }}>
{EVENTS[ev].type}
</div>
<div style={{ fontSize: 13, fontWeight: 800, lineHeight: 1.5, marginBottom: 8 }}>{EVENTS[ev].title}</div>
<div style={{ fontSize: 11, color: 'rgba(255,255,255,0.7)', lineHeight: 1.9 }}>
<div>📅 {faDigits(j.day)} {J_MONTHS[month - 1]} {faDigits(year)}</div>
<div>📍 {EVENTS[ev].city}</div>
<div>🕐 {EVENTS[ev].time}</div>
</div>
{/* arrow */}
<span style={{ position: 'absolute', top: '100%', left: '50%', transform: 'translateX(-50%)', width: 0, height: 0, borderLeft: '7px solid transparent', borderRight: '7px solid transparent', borderTop: `7px solid ${NAVY}` }} />
</div>
)}
</div>
)
})}
</div>
{/* legend */}
<div style={{ display: 'flex', alignItems: 'center', gap: 6, marginTop: 16, paddingTop: 12, borderTop: '1px solid rgba(3,35,64,0.08)', fontSize: 10, color: 'rgba(3,35,64,0.5)' }}>
<span style={{ width: 8, height: 8, borderRadius: '50%', background: NEON, boxShadow: `0 0 8px 1px ${NEON}` }} />
روزهای دارای رویداد نشانگر را نگه دارید
</div>
<style>{`
@keyframes evPulse { 0%{transform:scale(1);opacity:.6} 70%{transform:scale(1.7);opacity:0} 100%{opacity:0} }
.ev-breathe { animation: evBreathe 1.8s ease-in-out infinite; }
@keyframes evBreathe {
0%,100% { box-shadow: 0 0 0 0 ${NEON}00, 0 0 10px 1px ${NEON}cc; }
50% { box-shadow: 0 0 0 5px ${NEON}33, 0 0 18px 4px ${NEON}; }
}
`}</style>
</div>
)
}