// Page components for the Direction 3 standalone site.
// pages.jsx — site pages
// YouTube sometimes truncates titles with "..." in the API response; strip it
// so CSS line-clamp doesn't add a second ellipsis on top.
function stripTrailingEllipsis(s = '') {
return s.replace(/[….]{2,}\s*$/, '').trimEnd();
}
function LatestSermonBand({ lang, t, setRoute, accent }) {
const [latest, setLatest] = React.useState(null);
React.useEffect(() => {
let cancelled = false;
ytLatestPlayableVideo(YT.uploadsPlaylistId, lang).then((v) => {
if (!cancelled) setLatest(v);
}).catch(() => {});
return () => { cancelled = true; };
}, [lang]);
return (
{latest
?
: }
{lang === 'ko' ? '최근 업로드' : 'Latest uploads'}
{stripTrailingEllipsis(latest?.title) || (lang === 'ko' ? '이번 주 말씀을 들어보세요' : "Listen to this week's message")}
{latest?.publishedAt
? new Date(latest.publishedAt).toLocaleDateString(lang === 'ko' ? 'ko-KR' : 'en-US', { year: 'numeric', month: 'long', day: 'numeric' })
: (lang === 'ko' ? '그리심소망교회 유튜브 채널' : 'Gerizim Hope YouTube channel')}
);
}
function HomePage({ lang, t, setRoute, layout, accent, accentTone }) {
return (
{/* Hero variants */}
{layout === 'fullbleed' &&
{t.indicator} / 2026
{lang === 'ko' ? '의와 영광의 빛.' : 'Light of righteousness.'}
{t.scriptures}
}
{layout === 'split' &&
◆ {t.indicator}
{lang === 'ko' ? '의의 빛을\n발하라' : 'Shine\nthe light.'}
{t.tagline}
{t.scriptures}
}
{layout === 'poster' &&
VOL. 20 — {t.indicator}
NORCROSS, GA — EST. 2006
{lang === 'ko' ? <>의와 영광의 , 빛.> :
<>Right· eous.>}
{t.tagline}
{t.scriptures}
}
{/* Welcome */}
{/* Latest sermon */}
{/* Services */}
◆ 03 — {lang === 'ko' ? '예배' : 'Worship'}
{lang === 'ko' ? '함께 모여' : 'Gather together'}
{t.services.map(([name, time, room, groups], i) =>
0{i + 1}
{groups?.length > 0 && (
{groups.map(([gname, gtime]) => (
{gname}
{gtime}
))}
)}
)}
);
}
function AboutPage({ lang, t, accent, accentTone }) {
return (
◆ {t.aboutKicker} / VOL. 20
{lang === 'ko' ? <>20년의 빛.> : <>Twenty years .>}
{t.aboutLead}
{t.aboutNarrative.intro}
{t.aboutNarrative.origin}
{t.aboutNarrative.pillars.map(([heading, body], i) => (
◆ 0{i + 1}
{heading}
{body}
))}
{t.aboutNarrative.closing}
◆ {t.leadershipKicker}
{t.leadershipTitle}
{t.leadership.map(([name, role, tone, bio, creds, src], i) =>
{role}
{name}
{bio}
{creds && creds.length > 0 &&
{creds.map((c, j) => {c} )}
}
)}
{[
[lang === 'ko' ? '말씀' : 'Word', lang === 'ko' ? '구속사 강해를 통해 성경을 깊이 듣습니다.' : 'Hearing Scripture through redemptive-history exposition.'],
[lang === 'ko' ? '기도' : 'Prayer', lang === 'ko' ? '토요 새벽 기도회로 함께 무릎 꿇습니다.' : 'Kneeling together at the Saturday dawn prayer gathering.'],
[lang === 'ko' ? '교제' : 'Fellowship', lang === 'ko' ? '식탁과 교제 가운데 가족을 이룹니다.' : 'Becoming family around the table.']].
map(([h, body], i) =>
)}
);
}
function SermonsPage({ lang, t, accent }) {
const [activePid, setActivePid] = React.useState(YT.playlists[0].id);
const [videos, setVideos] = React.useState(null); // null = loading, [] = empty/no key
const [activeVideoId, setActiveVideoId] = React.useState(null);
const activeMeta = YT.playlists.find((p) => p.id === activePid) || YT.playlists[0];
React.useEffect(() => {
let cancelled = false;
setVideos(null);
setActiveVideoId(null);
ytFetchPlaylistItems(activePid, 50).then((items) => {
if (cancelled) return;
const filtered = (items || []).filter(
v => lang === 'ko' ? ytIsKorean(v.title) : !ytIsKorean(v.title)
);
setVideos(filtered);
if (filtered.length) setActiveVideoId(filtered[0].videoId);
}).catch(() => { if (!cancelled) setVideos([]); });
return () => { cancelled = true; };
}, [activePid, lang]);
const activeVideo = videos && videos.find((v) => v.videoId === activeVideoId);
const formatDate = (iso) => {
if (!iso) return '';
try {
return new Date(iso).toLocaleDateString(lang === 'ko' ? 'ko-KR' : 'en-US', {
year: 'numeric', month: 'short', day: 'numeric',
});
} catch (_) { return ''; }
};
return (
◆ {lang === 'ko' ? '말씀영상' : 'Sermons'}
{lang === 'ko' ? <>말씀, 깃발.> : <>Word as banner. >}
{/* Featured player */}
{activeVideoId
?
: }
{lang === 'ko' ? '재생목록' : 'Playlist'}
{lang === 'ko' ? activeMeta.ko : activeMeta.en}
{stripTrailingEllipsis(activeVideo?.title) || (lang === 'ko' ? `${activeMeta.ko} 모음` : `${activeMeta.en} sermons`)}
{activeVideo?.publishedAt
? formatDate(activeVideo.publishedAt)
: (lang === 'ko'
? '아래 목록에서 영상을 선택하세요.'
: 'Pick a video from the list below.')}
{/* Playlist tabs */}
{YT.playlists.map((p) => (
setActivePid(p.id)}
className={`chip ${activePid === p.id ? 'on' : ''}`}
aria-pressed={activePid === p.id}>
{lang === 'ko' ? p.ko : p.en}
))}
{videos === null
? (lang === 'ko' ? '불러오는 중…' : 'Loading…')
: (lang === 'ko' ? `${videos.length}개 영상` : `${videos.length} videos`)}
{/* Video grid */}
{videos === null && (
{lang === 'ko' ? '영상 목록을 불러오는 중입니다…' : 'Loading videos…'}
)}
{videos && videos.length === 0 && (
{lang === 'ko' ? '영상을 표시할 수 없습니다.' : 'No videos to display.'}
)}
{videos && videos.length > 0 && (
{videos.map((v) => (
{
setActiveVideoId(v.videoId);
window.scrollTo({ top: 0, behavior: 'smooth' });
}}
className={`yt-video-card ${activeVideoId === v.videoId ? 'is-active' : ''}`}>
{v.thumb &&
}
▶
{stripTrailingEllipsis(v.title)}
{formatDate(v.publishedAt)}
))}
)}
);
}
function EventsPage({ lang, t, accent }) {
return (
◆ {lang === 'ko' ? '교회행사' : 'Events'} / 2026 — APR–JUN
{lang === 'ko' ? <>함께하는 날 .> : <>Come together .>}
{t.eventsList.map(([d, day, name, time, room], i) =>
{d}
{day}
{name}
{time}
{room}
)}
);
}
function WelcomePage({ lang, t, setRoute, accent }) {
return (
◆ {t.welcomePageKicker}
{lang === 'ko' ? <>문은 열려있다. > : <>The door is open.>}
{lang === 'ko' ?
'처음 오시는 길이 어색하지 않도록, 한 걸음씩 함께 안내합니다.' :
'So your first visit feels familiar — we walk through it with you, one step at a time.'}
{t.welcomeSteps.map(([h, body], i) =>
)}
{t.misc.sundayLabel}
{t.misc.sundayTime}
{lang === 'ko' ? '본당' : 'Sanctuary'}
setRoute('contact')} style={{ background: accent }}>
→
{lang === 'ko' ? '오시는 길' : 'Get directions'}
);
}
function Lightbox({ items, index, onClose, onPrev, onNext }) {
React.useEffect(() => {
document.body.style.overflow = 'hidden';
const onKey = (e) => {
if (e.key === 'Escape') onClose();
if (e.key === 'ArrowLeft') onPrev();
if (e.key === 'ArrowRight') onNext();
};
window.addEventListener('keydown', onKey);
return () => {
document.body.style.overflow = '';
window.removeEventListener('keydown', onKey);
};
}, [onClose, onPrev, onNext]);
const [label, , tag, src] = items[index];
return (
✕
{ e.stopPropagation(); onPrev(); }} aria-label="Previous">‹
e.stopPropagation()}>
{tag}
{label}
{ e.stopPropagation(); onNext(); }} aria-label="Next">›
);
}
function GalleryPage({ lang, t, accent }) {
const [lightboxIndex, setLightboxIndex] = React.useState(null);
const count = t.galleryItems.length;
return (
◆ {t.galleryKicker}
{lang === 'ko' ? <>함께한 자리. > : <>Together .>}
{t.galleryItems.map(([label, tone, tag, src], i) =>
setLightboxIndex(i)} aria-label={`Open ${label}`} />
)}
{lightboxIndex !== null && (
setLightboxIndex(null)}
onPrev={() => setLightboxIndex((lightboxIndex - 1 + count) % count)}
onNext={() => setLightboxIndex((lightboxIndex + 1) % count)}
/>
)}
);
}
function ContactPage({ lang, t, accent }) {
return (
◆ {lang === 'ko' ? '오시는 길' : 'Visit'}
{lang === 'ko' ? <>오시는 길 .> : <>Come and visit.>}
{lang === 'ko' ? '주소' : 'Address'}
{t.address}
{lang === 'ko' ? '연락' : 'Contact'}
{t.email}
{lang === 'ko' ? '예배 시간' : 'Service Times'}
{t.services.map(([name, time, room, groups], i) => (
{name}
{room}
{time}
{groups?.length > 0 && (
{groups.map(([gname, gtime]) => (
{gname}
{gtime}
))}
)}
))}
);
}
Object.assign(window, { HomePage, AboutPage, SermonsPage, EventsPage, WelcomePage, GalleryPage, ContactPage });