-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Expand file tree
/
Copy pathClimateNewsPanel.ts
More file actions
80 lines (70 loc) · 3.35 KB
/
Copy pathClimateNewsPanel.ts
File metadata and controls
80 lines (70 loc) · 3.35 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
import { Panel } from './Panel';
import { t } from '@/services/i18n';
import { joinSafeHtml, safeHtml, safeUrlAttr, type SafeHtml } from '@/utils/sanitize';
import { getHydratedData } from '@/services/bootstrap';
import { createLazyClient, getRpcBaseUrl, rpcFetch } from '@/services/rpc-client';
import { ClimateServiceClient } from '@/generated/client/worldmonitor/climate/v1/service_client';
import type { ListClimateNewsResponse, ClimateNewsItem } from '@/generated/client/worldmonitor/climate/v1/service_client';
const getClimateClient = createLazyClient(() => new ClimateServiceClient(getRpcBaseUrl(), { fetch: rpcFetch }));
function formatTimeAgo(epochMs: number): string {
const diffMs = Date.now() - epochMs;
const minutes = Math.floor(diffMs / 60_000);
if (minutes < 1) return 'just now';
if (minutes < 60) return `${minutes}m ago`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h ago`;
const days = Math.floor(hours / 24);
if (days === 1) return '1d ago';
return `${days}d ago`;
}
function truncateSummary(text: string, maxLen = 120): string {
if (!text || text.length <= maxLen) return text || '';
return text.slice(0, maxLen).replace(/\s+\S*$/, '') + '...';
}
function renderNewsCard(item: ClimateNewsItem): SafeHtml {
const timeAgo = item.publishedAt ? formatTimeAgo(item.publishedAt) : '';
const summary = truncateSummary(item.summary);
const safeUrl = safeUrlAttr(item.url);
const inner = safeHtml`<div class="climate-news-card__header">
<span class="climate-news-card__source">${item.sourceName}</span>
<span class="climate-news-card__time">${timeAgo}</span>
</div>
<div class="climate-news-card__title">${item.title}</div>
${summary ? safeHtml`<div class="climate-news-card__summary">${summary}</div>` : ''}`;
if (!safeUrl.toString()) return safeHtml`<div class="climate-news-card">${inner}</div>`;
return safeHtml`<a class="climate-news-card" href="${safeUrl}" target="_blank" rel="noopener noreferrer">${inner}</a>`;
}
export class ClimateNewsPanel extends Panel {
constructor() {
super({ id: 'climate-news', title: t('panels.climateNews'), infoTooltip: t('components.climateNews.infoTooltip') });
}
public async fetchData(): Promise<void> {
try {
const hydrated = getHydratedData('climateNews') as ListClimateNewsResponse | undefined;
if (hydrated?.items?.length) {
if (!this.element?.isConnected) return;
this.renderNewsList(hydrated);
void getClimateClient().listClimateNews({}).then(data => {
if (!this.element?.isConnected || !data.items?.length) return;
this.renderNewsList(data);
}).catch(() => {});
return;
}
const data = await getClimateClient().listClimateNews({});
if (!this.element?.isConnected) return;
this.renderNewsList(data);
} catch (err) {
if (this.isAbortError(err)) return;
if (!this.element?.isConnected) return;
this.showError(t('components.climateNews.loadError'), () => void this.fetchData());
}
}
private renderNewsList(data: ListClimateNewsResponse): void {
if (!data.items?.length) {
this.showError(t('components.climateNews.loadError'), () => void this.fetchData());
return;
}
const cards = joinSafeHtml(data.items.map(renderNewsCard));
this.setSafeContent(safeHtml`<div class="climate-news-list">${cards}</div>`);
}
}