mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
143 lines
5.6 KiB
TypeScript
143 lines
5.6 KiB
TypeScript
import { FC, useEffect, useState } from 'react';
|
|
import { interpolate, t } from '../utils/i18n';
|
|
import { resolveNewsImage, resolveNewsLink } from '../utils/news';
|
|
|
|
interface NewsItem
|
|
{
|
|
id: number;
|
|
title: string;
|
|
body: string;
|
|
image: string | null;
|
|
linkText: string;
|
|
linkUrl: string;
|
|
}
|
|
|
|
interface RawNewsItem
|
|
{
|
|
id?: number;
|
|
title?: string;
|
|
body?: string;
|
|
image?: string | null;
|
|
link?: string;
|
|
linkUrl?: string;
|
|
linkText?: string;
|
|
}
|
|
|
|
const normalizeNewsItem = (raw: RawNewsItem, fallbackId: number): NewsItem => ({
|
|
id: typeof raw.id === 'number' ? raw.id : fallbackId,
|
|
title: typeof raw.title === 'string' ? raw.title : '',
|
|
body: typeof raw.body === 'string' ? raw.body : '',
|
|
image: typeof raw.image === 'string' && raw.image.length ? interpolate(raw.image) : null,
|
|
linkText: typeof raw.linkText === 'string' ? raw.linkText : '',
|
|
linkUrl: interpolate((typeof raw.linkUrl === 'string' && raw.linkUrl) || (typeof raw.link === 'string' ? raw.link : ''))
|
|
});
|
|
|
|
interface NewsWindowProps { newsUrl: string; }
|
|
|
|
const NEWS_AUTO_ADVANCE_MS = 10000;
|
|
|
|
export const NewsWindow: FC<NewsWindowProps> = ({ newsUrl }) =>
|
|
{
|
|
const [ items, setItems ] = useState<NewsItem[] | null>(null);
|
|
const [ failed, setFailed ] = useState(false);
|
|
const [ index, setIndex ] = useState(0);
|
|
const [ autoTick, setAutoTick ] = useState(0);
|
|
|
|
useEffect(() =>
|
|
{
|
|
if(!newsUrl) { setFailed(true); return; }
|
|
let cancelled = false;
|
|
fetch(newsUrl, { credentials: 'omit' })
|
|
.then(async r =>
|
|
{
|
|
if(!r.ok) throw new Error('status ' + r.status);
|
|
return r.json();
|
|
})
|
|
.then((json: unknown) =>
|
|
{
|
|
if(cancelled) return;
|
|
const rawList = Array.isArray((json as { news?: unknown })?.news)
|
|
? (json as { news: RawNewsItem[] }).news
|
|
: Array.isArray(json) ? (json as RawNewsItem[]) : [];
|
|
setItems(rawList.map((raw, idx) => normalizeNewsItem(raw, idx + 1)));
|
|
})
|
|
.catch(() => { if(!cancelled) setFailed(true); });
|
|
return () => { cancelled = true; };
|
|
}, [ newsUrl ]);
|
|
|
|
useEffect(() =>
|
|
{
|
|
if(!items || items.length < 2) return;
|
|
const id = window.setTimeout(() =>
|
|
{
|
|
setIndex(i => (i + 1) % items.length);
|
|
}, NEWS_AUTO_ADVANCE_MS);
|
|
return () => window.clearTimeout(id);
|
|
}, [ items, index, autoTick ]);
|
|
|
|
if(failed) return null;
|
|
if(!items || !items.length) return null;
|
|
|
|
const current = items[Math.min(index, items.length - 1)];
|
|
const hasMany = items.length > 1;
|
|
const bumpAuto = () => setAutoTick(t => t + 1);
|
|
const prev = () => { setIndex(i => (i - 1 + items.length) % items.length); bumpAuto(); };
|
|
const next = () => { setIndex(i => (i + 1) % items.length); bumpAuto(); };
|
|
|
|
const safeLinkUrl = resolveNewsLink(current.linkUrl);
|
|
const safeImageSrc = resolveNewsImage(current.image);
|
|
const openLink = () =>
|
|
{
|
|
if(!safeLinkUrl) return;
|
|
window.open(safeLinkUrl, '_blank', 'noopener,noreferrer');
|
|
};
|
|
|
|
return (
|
|
<div className="login-news-stack">
|
|
<div className="news-card-wrapper" key={ current.id }>
|
|
<span className="news-sparkle news-sparkle-1" aria-hidden="true">★</span>
|
|
<span className="news-sparkle news-sparkle-2" aria-hidden="true">✦</span>
|
|
<span className="news-sparkle news-sparkle-3" aria-hidden="true">✧</span>
|
|
|
|
<div className="news-new-badge" aria-hidden="true">
|
|
<span>{ t('nitro.login.news.new', 'NEW!') }</span>
|
|
</div>
|
|
|
|
<div className="nitro-login-card nitro-news-card">
|
|
<div className="card-title news-ribbon">
|
|
<span className="news-ribbon-text">{ t('nitro.login.news.title', 'Hotel News') }</span>
|
|
</div>
|
|
<div className="card-body news-body">
|
|
{ safeImageSrc &&
|
|
<div className="news-image">
|
|
<img
|
|
src={ safeImageSrc }
|
|
alt={ current.title || 'news' }
|
|
onError={ e => { (e.currentTarget as HTMLImageElement).style.display = 'none'; } }
|
|
/>
|
|
</div>
|
|
}
|
|
<div className="news-headline">{ current.title }</div>
|
|
{ current.body &&
|
|
<div className="news-text">{ current.body }</div> }
|
|
|
|
<div className="news-footer">
|
|
{ current.linkText && safeLinkUrl
|
|
? <button type="button" className="ok-button news-link-button" onClick={ openLink }>{ current.linkText }</button>
|
|
: <span /> }
|
|
|
|
{ hasMany &&
|
|
<div className="news-pager">
|
|
<button type="button" className="arrow-btn" aria-label="Previous news" onClick={ prev }>‹</button>
|
|
<span className="news-counter">{ index + 1 }/{ items.length }</span>
|
|
<button type="button" className="arrow-btn" aria-label="Next news" onClick={ next }>›</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|