steelforesight/frontend/src/pages/Home/sections/VideocastPodcastSection.tsx

502 lines
26 KiB
TypeScript

import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useLang } from '@/context/LangContext'
import { Bookmark, Podcast, Clock, Calendar, Play, ChevronRight } from 'lucide-react'
import { bootstrapMedia, videos as VIDEOS, podcasts as PODCASTS } from '@/data/media'
import MediaWavePlayer from '@/components/MediaWavePlayer'
import { authFetch, useAuth } from '@/context/AuthContext'
const NAVY = '#032340'
const GOLD = '#CD9E53'
const T = {
fa: {
videoLabel: 'ویدیوکست', videoSub: 'آخرین گفتگوهای تصویری اندیشکده',
podLabel: 'پادکست‌ها', podSub: 'شنیدنی‌های اندیشکده فولاد آینده',
watchAll: 'مشاهده همه', listenAll: 'مشاهده همه',
},
en: {
videoLabel: 'Videocast', videoSub: 'Latest video conversations from the Institute',
podLabel: 'Podcasts', podSub: 'Audio episodes from Future Steel Institute',
watchAll: 'View All', listenAll: 'View All',
},
}
export default function VideocastPodcastSection({ only }: { only?: 'video' | 'pod' }) {
const { lang } = useLang()
const { member } = useAuth()
const navigate = useNavigate()
const t = T[lang]
const [activeVideo, setActiveVideo] = useState(0)
const [activePod, setActivePod] = useState(() => {
const playableIndex = PODCASTS.findIndex(item => Boolean(item.mediaUrl?.trim()))
return playableIndex >= 0 ? playableIndex : 0
})
const [podcasts, setPodcasts] = useState(() => [...PODCASTS])
const [bookmarkedIds, setBookmarkedIds] = useState<Set<string>>(new Set())
const [bookmarkBusy, setBookmarkBusy] = useState(false)
const pod = podcasts[activePod] ?? podcasts[0]
useEffect(() => {
let active = true
bootstrapMedia().then(() => {
if (!active) return
setPodcasts([...PODCASTS])
const playableIndex = PODCASTS.findIndex(item => Boolean(item.mediaUrl?.trim()))
setActivePod(playableIndex >= 0 ? playableIndex : 0)
})
return () => { active = false }
}, [])
useEffect(() => {
if (!member) { setBookmarkedIds(new Set()); return }
let active = true
authFetch('/api/members/me/bookmarks')
.then(async response => response.ok ? response.json() : [])
.then(items => {
if (!active || !Array.isArray(items)) return
setBookmarkedIds(new Set(items.filter(item => item.contentType === 'podcast').map(item => String(item.contentId))))
})
.catch(() => {})
return () => { active = false }
}, [member])
const togglePodcastBookmark = async () => {
if (!member) { navigate('/login'); return }
if (!pod || bookmarkBusy) return
const contentId = String(pod.id)
const saved = bookmarkedIds.has(contentId)
setBookmarkBusy(true)
try {
const response = saved
? await authFetch(`/api/members/me/bookmarks/podcast/${encodeURIComponent(contentId)}`, { method: 'DELETE' })
: await authFetch('/api/members/me/bookmarks', {
method: 'POST',
body: JSON.stringify({
contentId,
contentType: 'podcast',
title: pod[lang].title,
summary: pod[lang].body,
coverImage: pod.cover,
path: '/podcast',
}),
})
if (!response.ok) throw new Error('bookmark_failed')
setBookmarkedIds(current => {
const next = new Set(current)
if (saved) next.delete(contentId)
else next.add(contentId)
return next
})
} catch { /* keep the previous state when the request fails */ }
finally { setBookmarkBusy(false) }
}
const showVideo = only !== 'pod'
const showPod = only !== 'video'
// when split into one column, give it full width instead of 60/40
const gridCols = only ? '1fr' : '60% 40%'
return (
<section style={{ direction: lang === 'fa' ? 'rtl' : 'ltr', background: 'var(--paper)' }}>
<div className="max-w-7xl mx-auto px-12 max-md:px-5">
<div style={{ display: 'grid', gridTemplateColumns: gridCols }} className="max-lg:!grid-cols-1">
{/* ── VIDEOCAST (full-width featured player + playlist + description) ── */}
{showVideo && (
<div style={{ padding: '40px 40px 44px', display: 'flex', flexDirection: 'column', gridColumn: '1 / -1', background: 'linear-gradient(180deg, #f6f8fa 0%, #edf0f4 100%)', borderRadius: 24, margin: '36px 0', border: '1px solid rgba(3,35,64,0.06)', boxShadow: '0 24px 64px -40px rgba(3,35,64,0.35)' }} className="max-md:!p-5">
{/* heading */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 12, marginBottom: 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 28, background: GOLD, borderRadius: 2 }} />
<h2 style={{ fontSize: 'clamp(20px,2.2vw,28px)', fontWeight: 900, letterSpacing: '-0.5px', color: NAVY, margin: 0 }}>{t.videoLabel}</h2>
</div>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 8, direction: 'ltr',
fontSize: 13, fontWeight: 700, color: GOLD,
background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
}}>
<div style={{ width: 24, height: 24, borderRadius: '50%', border: `1px solid ${GOLD}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ChevronRight size={14} strokeWidth={2.5} />
</div>
<span style={{ direction: 'rtl' }}>{t.watchAll}</span>
</button>
</div>
{/* featured player + playlist — all in one row */}
<div style={{ display: 'flex', gap: 16, alignItems: 'stretch', marginBottom: 32 }} className="max-lg:!flex-col">
<div style={{ flex: '1.2 1 0', minWidth: 0, display: 'flex', flexDirection: 'column', gap: 14 }} className="max-lg:!w-full">
<div style={{ flex: '1 1 0', minWidth: 0, position: 'relative', overflow: 'hidden', background: '#000', borderRadius: 16, minHeight: 0, boxShadow: '0 30px 70px -34px rgba(3,35,64,0.5)' }} className="max-lg:!aspect-video max-lg:!flex-none">
<img src={VIDEOS[activeVideo].thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block', opacity: 0.9, position: 'absolute', inset: 0 }} />
<div style={{ position: 'absolute', inset: 0, background: 'linear-gradient(to top, rgba(3,35,64,0.55) 0%, transparent 45%)' }} />
{/* center play */}
<button aria-label="play" style={{
position: 'absolute', top: '50%', left: '50%', transform: 'translate(-50%,-50%)',
width: 64, height: 64, borderRadius: '50%', border: '2px solid rgba(255,255,255,0.6)',
background: 'rgba(255,255,255,0.14)', backdropFilter: 'blur(4px)', cursor: 'pointer',
display: 'flex', alignItems: 'center', justifyContent: 'center',
}}>
<div style={{ width: 0, height: 0, borderTop: '11px solid transparent', borderBottom: '11px solid transparent', borderLeft: '19px solid #fff', marginLeft: 4 }} />
</button>
{/* controls bar */}
<div style={{ position: 'absolute', left: 0, right: 0, bottom: 0, padding: '0 16px 14px', direction: 'ltr' }}>
<div style={{ height: 4, borderRadius: 2, background: 'rgba(255,255,255,0.3)', marginBottom: 12 }}>
<div style={{ width: '22%', height: '100%', borderRadius: 2, background: GOLD, position: 'relative' }}>
<span style={{ position: 'absolute', right: -5, top: '50%', transform: 'translateY(-50%)', width: 11, height: 11, borderRadius: '50%', background: '#fff' }} />
</div>
</div>
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', color: '#fff' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<span style={{ fontSize: 18 }}></span>
<span style={{ fontSize: 15, opacity: 0.85 }}></span>
<span style={{ fontSize: 15, opacity: 0.85 }}>🔊</span>
<span style={{ fontSize: 11, opacity: 0.8, direction: 'ltr' }}>۰۹:۲۷ / {VIDEOS[activeVideo].duration}</span>
</div>
<div style={{ display: 'flex', alignItems: 'center', gap: 14, opacity: 0.85 }}>
<span style={{ fontSize: 14 }}></span>
<span style={{ fontSize: 14 }}></span>
</div>
</div>
</div>
</div>
{/* title — directly below the video, inside the right column */}
<div style={{ direction: lang === 'fa' ? 'rtl' : 'ltr' }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
<span style={{ fontSize: 16 }}>🎙</span>
<h3 style={{ fontSize: 'clamp(15px,1.5vw,20px)', fontWeight: 900, color: NAVY, margin: 0, lineHeight: 1.4 }}>{VIDEOS[activeVideo][lang].title}</h3>
</div>
<p style={{ fontSize: 13, lineHeight: 1.9, color: 'rgba(3,35,64,0.7)', margin: 0, textAlign: 'justify', display: '-webkit-box', WebkitLineClamp: 2, WebkitBoxOrient: 'vertical', overflow: 'hidden' }}>{VIDEOS[activeVideo][lang].body}</p>
</div>
</div>
{/* playlist column — beside the featured player */}
<div style={{ flex: '1 1 0', minWidth: 0 }} className="max-lg:!w-full">
{/* video playlist — 3 thumbnail cards */}
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10, alignContent: 'start', direction: lang === 'fa' ? 'rtl' : 'ltr' }} className="max-md:!grid-cols-2">
{VIDEOS.slice(0, 3).map((v, i) => (
<button
key={v.id}
onClick={() => setActiveVideo(i)}
style={{
textAlign: 'inherit', background: i === activeVideo ? '#efe9e1' : '#fff',
border: `1px solid ${i === activeVideo ? GOLD : 'rgba(3,35,64,0.08)'}`,
borderRadius: 12, overflow: 'hidden', cursor: 'pointer',
display: 'flex', flexDirection: 'column', transition: 'box-shadow .2s',
gridColumn: i === 2 ? '1 / -1' : undefined,
}}
className="hover:!shadow-md"
>
<div style={{ position: 'relative', aspectRatio: '16/9', overflow: 'hidden' }}>
<img src={v.thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
<div style={{ position: 'absolute', inset: 0, background: 'rgba(3,35,64,0.28)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 36, height: 36, borderRadius: '50%', background: 'rgba(255,255,255,0.85)', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<div style={{ width: 0, height: 0, borderTop: '7px solid transparent', borderBottom: '7px solid transparent', borderLeft: `12px solid ${NAVY}`, marginLeft: 3 }} />
</div>
</div>
<span style={{ position: 'absolute', bottom: 8, left: 8, fontSize: 10, fontWeight: 800, color: '#fff', background: 'rgba(0,0,0,0.55)', padding: '2px 7px', borderRadius: 4, direction: 'ltr' }}>{v.duration}</span>
</div>
<div style={{ padding: '12px 14px', direction: lang === 'fa' ? 'rtl' : 'ltr', textAlign: 'right' }}>
<div style={{ fontSize: 11, color: GOLD, fontWeight: 800, marginBottom: 4 }}>{v.ep}</div>
<div style={{ fontSize: 13, fontWeight: 800, color: NAVY, lineHeight: 1.4 }}>{v[lang].title}</div>
</div>
</button>
))}
</div>
</div>
</div>
</div>
)}
{/* ── PODCAST (full width) ── */}
{showPod && (
<div style={{ padding: '56px 0 72px', background: 'var(--paper)', gridColumn: '1 / -1' }} className="max-md:!px-5">
<div className="max-w-7xl mx-auto px-12 max-md:!px-0">
{/* heading */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: 24, gap: 12 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
<div style={{ width: 4, height: 28, background: GOLD, borderRadius: 2 }} />
<h2 style={{ fontSize: 'clamp(20px,2.2vw,28px)', fontWeight: 900, letterSpacing: '-0.5px', color: NAVY, margin: 0 }}>{t.podLabel}</h2>
</div>
<button style={{
display: 'inline-flex', alignItems: 'center', gap: 8,
fontSize: 13, fontWeight: 700, color: GOLD,
background: 'none', border: 'none', cursor: 'pointer', fontFamily: 'inherit',
}}>
<span>{t.listenAll}</span>
<div style={{ width: 24, height: 24, borderRadius: '50%', border: `1px solid ${GOLD}`, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<ChevronRight size={14} strokeWidth={2.5} />
</div>
</button>
</div>
{/* ── featured player card ── */}
<div style={{
display: 'flex',
flexDirection: lang === 'fa' ? 'row' : 'row-reverse',
alignItems: 'stretch',
background: '#efe9e1',
borderRadius: 18,
marginBottom: 32,
overflow: 'hidden',
minHeight: 200,
}} className="max-md:!flex-col">
{/* cover — full height, flush to right edge */}
<div style={{ flexShrink: 0, width: 200, position: 'relative' }} className="max-md:!w-full max-md:!h-48">
<img src={pod.cover} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} />
{/* rating badge overlaid at bottom of cover */}
<div style={{
position: 'absolute', bottom: 0, left: 0, right: 0,
background: 'rgba(3,35,64,0.75)', backdropFilter: 'blur(4px)',
padding: '6px 10px', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: 6,
}}>
<span style={{ color: GOLD, fontSize: 12, letterSpacing: 1 }}></span>
<span style={{ fontSize: 11, fontWeight: 700, color: '#fff' }}>{pod.rating}</span>
</div>
</div>
{/* player body — directly beside cover, no gap */}
<div style={{
flex: 1, minWidth: 0, padding: 24,
display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
direction: lang === 'fa' ? 'rtl' : 'ltr',
}}>
{/* bookmark */}
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'flex-start', marginBottom: 12, direction: 'ltr' }}>
<div style={{ display: 'flex', gap: 10 }}>
<button
type="button"
onClick={togglePodcastBookmark}
disabled={bookmarkBusy}
aria-label={bookmarkedIds.has(String(pod.id)) ? 'حذف از علاقه‌مندی‌ها' : 'افزودن به علاقه‌مندی‌ها'}
title={bookmarkedIds.has(String(pod.id)) ? 'حذف از علاقه‌مندی‌ها' : 'ذخیره در علاقه‌مندی‌ها'}
style={{
width: 36, height: 36, borderRadius: '50%',
border: `1px solid ${bookmarkedIds.has(String(pod.id)) ? GOLD : 'rgba(3,35,64,0.18)'}`,
display: 'flex', alignItems: 'center', justifyContent: 'center',
background: bookmarkedIds.has(String(pod.id)) ? `${GOLD}22` : 'transparent',
color: bookmarkedIds.has(String(pod.id)) ? GOLD : NAVY,
cursor: bookmarkBusy ? 'wait' : 'pointer', transition: 'all 0.2s',
}}
>
<Bookmark size={16} strokeWidth={2} fill={bookmarkedIds.has(String(pod.id)) ? 'currentColor' : 'none'} />
</button>
</div>
</div>
{/* title */}
<h3 style={{ fontSize: 'clamp(16px,1.8vw,22px)', fontWeight: 900, color: NAVY, margin: '0 0 8px', lineHeight: 1.4, textAlign: lang === 'fa' ? 'right' : 'left' }}>
{pod[lang].title}
</h3>
{/* date */}
<div style={{ display: 'inline-flex', alignItems: 'center', gap: 6, fontSize: 12, color: 'rgba(3,35,64,0.65)', fontWeight: 600, marginBottom: 16, flexDirection: lang === 'fa' ? 'row' : 'row-reverse' }}>
<Calendar size={14} color={GOLD} />
<span>{pod.date}</span>
</div>
<div style={{ height: 1, background: 'rgba(3,35,64,0.1)', marginBottom: 16 }} />
{/* play + waveform */}
{pod.mediaUrl ? (
<div style={{ marginInlineStart: -16, minWidth: 0 }}>
<MediaWavePlayer src={pod.mediaUrl} episodeKey={pod.id} tone="light" compact />
</div>
) : (
<div style={{ display: 'flex', alignItems: 'center', gap: 16, flexDirection: lang === 'fa' ? 'row-reverse' : 'row', minWidth: 0 }}>
<button aria-label="play" style={{ flexShrink: 0, width: 44, height: 44, borderRadius: '50%', border: 'none', cursor: 'pointer', background: GOLD, display: 'flex', alignItems: 'center', justifyContent: 'center', transition: 'transform 0.2s, background-color 0.2s' }} className="hover:!scale-105 hover:!bg-[#b88d46]">
<Play size={18} fill="#fff" color="#fff" style={{ marginLeft: 2 }} />
</button>
<div style={{ flex: 1, minWidth: 0, display: 'flex', alignItems: 'center', gap: 10, direction: 'ltr' }}>
<span style={{ fontSize: 11, color: 'rgba(3,35,64,0.6)', fontWeight: 700, flexShrink: 0 }}>۰۲:۳۱</span>
<div style={{ flex: 1, display: 'flex', alignItems: 'center', gap: 3, height: 26, overflow: 'hidden' }}>
{[4,6,8,10,7,5,8,12,15,18,14,10,8,12,16,20,22,17,12,9,12,15,18,22,24,20,15,11,8,12,15,17,20,22,18,14,10,8,12,15,19,21,16,12,9,11,14,18,20,16,11,8,6,9,12,15,17,13,9,7,10,13,16,18,14,10,8,11,14,16,13,9,7,10,13,16,18,14,10,7].map((h, i) => (
<span key={i} style={{ flex: 1, height: h, borderRadius: 1.5, background: i < 20 ? GOLD : 'rgba(3,35,64,0.18)', minWidth: 2, transition: 'background-color 0.2s' }} />
))}
</div>
<span style={{ fontSize: 11, color: 'rgba(3,35,64,0.6)', fontWeight: 700, flexShrink: 0 }}>{pod.duration}</span>
</div>
</div>
)}
</div>
</div>
{/* ── episode list (left) + description (right) ── */}
<div style={{
display: 'grid',
gridTemplateColumns: '1fr 1.2fr',
gap: 40,
direction: lang === 'fa' ? 'rtl' : 'ltr'
}} className="max-lg:!grid-cols-1 max-lg:!gap-8">
{/* episode list — first DOM → left in LTR, left in RTL via order */}
<div style={{
order: lang === 'fa' ? 2 : 1,
display: 'flex',
flexDirection: 'column',
gap: 16
}}>
{podcasts.filter((_, idx) => idx !== activePod).slice(0, 2).map((p) => {
const originalIndex = podcasts.findIndex(item => item.id === p.id);
return (
<button
key={p.id}
onClick={() => setActivePod(originalIndex)}
style={{
display: 'flex',
alignItems: 'center',
gap: 16,
width: '100%',
cursor: 'pointer',
background: '#FAF8F5',
border: '1px solid rgba(3, 35, 64, 0.04)',
borderRadius: 16,
padding: 16,
flexDirection: lang === 'fa' ? 'row-reverse' : 'row',
textAlign: lang === 'fa' ? 'right' : 'left',
transition: 'all 0.2s'
}}
className="hover:!bg-[#fff] hover:!border-transparent hover:!shadow-[0_8px_20px_-8px_rgba(3,35,64,0.15)]"
>
<img
src={p.cover}
alt=""
style={{
width: 120,
height: 80,
borderRadius: 12,
objectFit: 'cover',
flexShrink: 0,
display: 'block'
}}
/>
<div style={{
flex: 1,
minWidth: 0,
display: 'flex',
flexDirection: 'column',
gap: 8,
direction: lang === 'fa' ? 'rtl' : 'ltr'
}}>
{/* Top Row inside card text: Title & Duration */}
<div style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%'
}}>
<div style={{
fontSize: 14.5,
fontWeight: 900,
color: NAVY,
margin: 0
}}>
{p[lang].title}
</div>
<span style={{
display: 'inline-flex',
alignItems: 'center',
gap: 4,
fontSize: 11,
color: GOLD,
fontWeight: 700,
flexShrink: 0,
direction: 'ltr'
}}>
<Clock size={12} strokeWidth={2.5} />
<span>{p.duration}</span>
</span>
</div>
<div style={{
fontSize: 12,
color: 'rgba(3, 35, 64, 0.55)',
lineHeight: 1.8,
display: '-webkit-box',
WebkitLineClamp: 2,
WebkitBoxOrient: 'vertical',
overflow: 'hidden',
textAlign: 'justify'
}}>
{p[lang].body}
</div>
</div>
</button>
)
})}
</div>
{/* description — second DOM → right in LTR, right in RTL via order */}
<div style={{
order: lang === 'fa' ? 1 : 2,
direction: lang === 'fa' ? 'rtl' : 'ltr'
}}>
<div style={{
display: 'flex',
alignItems: 'center',
gap: 12,
marginBottom: 16,
justifyContent: 'flex-start'
}}>
{lang === 'fa' ? (
<>
<h3 style={{ fontSize: 'clamp(16px, 1.6vw, 22px)', fontWeight: 900, color: NAVY, margin: 0, lineHeight: 1.4, flex: 1, textAlign: 'right' }}>
{pod[lang].title}
</h3>
<div style={{ width: 34, height: 34, borderRadius: '50%', border: `1.5px solid ${GOLD}`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: GOLD, flexShrink: 0 }}>
<Podcast size={16} strokeWidth={2.5} />
</div>
</>
) : (
<>
<div style={{ width: 34, height: 34, borderRadius: '50%', border: `1.5px solid ${GOLD}`, display: 'flex', alignItems: 'center', justifyContent: 'center', color: GOLD, flexShrink: 0 }}>
<Podcast size={16} strokeWidth={2.5} />
</div>
<h3 style={{ fontSize: 'clamp(16px, 1.6vw, 22px)', fontWeight: 900, color: NAVY, margin: 0, lineHeight: 1.4, flex: 1, textAlign: 'left' }}>
{pod[lang].title}
</h3>
</>
)}
</div>
<p style={{
fontSize: 13.5,
lineHeight: 2.1,
color: 'rgba(3, 35, 64, 0.75)',
margin: '0 0 18px',
textAlign: 'justify',
direction: lang === 'fa' ? 'rtl' : 'ltr'
}}>
{pod[lang].body}
</p>
<div style={{
fontSize: 13.5,
fontWeight: 800,
color: GOLD,
marginBottom: 12,
textAlign: lang === 'fa' ? 'right' : 'left'
}}>
{lang === 'fa' ? 'مهمان برنامه: ' : 'Guest: '}{pod[lang].guest}
</div>
</div>
</div>
</div>
</div>
)}
</div>
</div>
<style>{`
@keyframes spinRing {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
`}</style>
</section>
)
}