Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions src/components/RadiationPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { Panel } from './Panel';
import { t } from '@/services/i18n';
import { fetchRadiationData, type RadiationData, type RadiationSite } from '@/services/radiation';

export class RadiationPanel extends Panel {
private data: RadiationData | null = null;
private loadingEl: HTMLElement | null = null;

constructor() {
super({
id: 'radiation',
title: t('panels.radiation'),
showCount: true,
trackActivity: true,
infoTooltip: 'Safecast + EPA RadNet radiation monitoring',
});
this.init();
}

private async init(): Promise<void> {
this.loadingEl = document.createElement('div');
this.loadingEl.className = 'panel-loading';
this.loadingEl.textContent = 'Loading...';
this.content.appendChild(this.loadingEl);

await this.loadData();
}

private async loadData(): Promise<void> {
try {
this.data = await fetchRadiationData();
this.render();
} catch (e) {
console.error('Radiation error:', e);
this.showError();
}
}

private render(): void {
if (!this.data) return;

this.loadingEl?.remove();

const container = document.createElement('div');
container.className = 'radiation-panel';

// Alert level indicator
const alert = document.createElement('div');
alert.className = `radiation-alert ${this.data.alertLevel}`;
alert.innerHTML = `
<span class="alert-icon">${this.data.alertLevel === 'normal' ? '✅' : this.data.alertLevel === 'elevated' ? '⚠️' : '🚨'}</span>
<span class="alert-text">${this.data.alertLevel.toUpperCase()}</span>
`;
container.appendChild(alert);

// Sites grid
const sites = document.createElement('div');
sites.className = 'radiation-sites';
this.data.sites.forEach(site => {
const card = document.createElement('div');
card.className = `site-card ${site.status}`;
card.innerHTML = `
<div class="site-name">${site.name}</div>
<div class="site-value">${site.latestValue?.toFixed(1) || '--'} cpm</div>
<div class="site-trend">${site.trend === 'stable' ? '➡️' : site.trend === 'rising' ? '📈' : '📉'}</div>
`;
sites.appendChild(card);
});
container.appendChild(sites);

this.content.appendChild(container);
this.updateCount(this.data.sites.length);
}

private showError(): void {
this.loadingEl?.remove();
this.content.innerHTML = '<div class="panel-error">Failed to load radiation data</div>';
}
}
100 changes: 100 additions & 0 deletions src/components/SanctionsPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { fetchSanctionsData, type SanctionsData, type SanctionedEntity } from '@/services/sanctions';

export class SanctionsPanel extends Panel {
private data: SanctionsData | null = null;
private loadingEl: HTMLElement | null = null;

constructor() {
super({
id: 'sanctions',
title: t('panels.sanctions'),
showCount: true,
trackActivity: true,
infoTooltip: 'OFAC SDN + OpenSanctions aggregated tracking',
});
this.init();
}

private async init(): Promise<void> {
this.loadingEl = document.createElement('div');
this.loadingEl.className = 'panel-loading';
this.loadingEl.textContent = 'Loading...';
this.content.appendChild(this.loadingEl);

await this.loadData();
}

private async loadData(): Promise<void> {
try {
this.data = await fetchSanctionsData();
this.render();
} catch (e) {
console.error('Sanctions error:', e);
this.showError();
}
}

private render(): void {
if (!this.data) return;

this.loadingEl?.remove();

const container = document.createElement('div');
container.className = 'sanctions-panel';

// Summary stats
const stats = document.createElement('div');
stats.className = 'sanctions-stats';
stats.innerHTML = `
<div class="stat">
<span class="stat-value">${this.data.totalCount.toLocaleString()}</span>
<span class="stat-label">Total Sanctioned</span>
</div>
<div class="stat">
<span class="stat-value">${this.data.newThisWeek}</span>
<span class="stat-label">New This Week</span>
</div>
`;
container.appendChild(stats);

// Program breakdown
const programs = document.createElement('div');
programs.className = 'sanctions-programs';
const topPrograms = Object.entries(this.data.byProgram)
.sort((a, b) => b[1] - a[1])
.slice(0, 5);

topPrograms.forEach(([program, count]) => {
const item = document.createElement('div');
item.className = 'program-item';
item.innerHTML = `<span class="program-name">${escapeHtml(program)}</span><span class="program-count">${count}</span>`;
programs.appendChild(item);
});
container.appendChild(programs);

// Recent entities
const list = document.createElement('div');
list.className = 'sanctions-list';
this.data.entities.slice(0, 10).forEach(entity => {
const item = document.createElement('div');
item.className = 'sanction-item';
item.innerHTML = `
<span class="entity-name">${escapeHtml(entity.name)}</span>
<span class="entity-type">${escapeHtml(entity.type)}</span>
`;
list.appendChild(item);
});

container.appendChild(list);
this.content.appendChild(container);
this.updateCount(this.data.totalCount);
}

private showError(): void {
this.loadingEl?.remove();
this.content.innerHTML = '<div class="panel-error">Failed to load sanctions data</div>';
}
}
96 changes: 96 additions & 0 deletions src/components/SocialSentimentPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { fetchSentimentData, type SentimentData, type SocialPost } from '@/services/social-sentiment';

export class SocialSentimentPanel extends Panel {
private data: SentimentData | null = null;
private loadingEl: HTMLElement | null = null;

constructor() {
super({
id: 'social-sentiment',
title: t('panels.socialSentiment'),
showCount: true,
trackActivity: true,
infoTooltip: 'Reddit + Bluesky geopolitical/market sentiment',
});
this.init();
}

private async init(): Promise<void> {
this.loadingEl = document.createElement('div');
this.loadingEl.className = 'panel-loading';
this.loadingEl.textContent = 'Loading...';
this.content.appendChild(this.loadingEl);

await this.loadData();
}

private async loadData(): Promise<void> {
try {
this.data = await fetchSentimentData();
this.render();
} catch (e) {
console.error('Social Sentiment error:', e);
this.showError();
}
}

private render(): void {
if (!this.data) return;

this.loadingEl?.remove();

const container = document.createElement('div');
container.className = 'sentiment-panel';

// Sentiment breakdown
const breakdown = document.createElement('div');
breakdown.className = 'sentiment-breakdown';
breakdown.innerHTML = `
<div class="sentiment-bullish">
<span class="count">${this.data.sentimentBreakdown.bullish}</span>
<span class="label">Bullish 📈</span>
</div>
<div class="sentiment-neutral">
<span class="count">${this.data.sentimentBreakdown.neutral}</span>
<span class="label">Neutral ➡️</span>
</div>
<div class="sentiment-bearish">
<span class="count">${this.data.sentimentBreakdown.bearish}</span>
<span class="label">Bearish 📉</span>
</div>
`;
container.appendChild(breakdown);

// Top topics
const topics = document.createElement('div');
topics.className = 'sentiment-topics';
topics.innerHTML = `<span class="topics-label">Trending:</span> ${this.data.topTopics.slice(0, 5).map(t => `<span class="topic-tag">${escapeHtml(t)}</span>`).join('')}`;
container.appendChild(topics);

// Posts list
const list = document.createElement('div');
list.className = 'sentiment-list';
this.data.posts.slice(0, 10).forEach(post => {
const item = document.createElement('div');
item.className = `sentiment-item ${post.sentiment}`;
item.innerHTML = `
<span class="platform">${post.platform === 'reddit' ? ' Reddit' : ' Bluesky'}</span>
<span class="author">${escapeHtml(post.author)}</span>
<span class="text">${escapeHtml(post.text.slice(0, 100))}</span>
`;
list.appendChild(item);
});
container.appendChild(list);

this.content.appendChild(container);
this.updateCount(this.data.totalPosts);
}

private showError(): void {
this.loadingEl?.remove();
this.content.innerHTML = '<div class="panel-error">Failed to load sentiment data</div>';
}
}
83 changes: 83 additions & 0 deletions src/components/TelegramOsintPanel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Panel } from './Panel';
import { escapeHtml } from '@/utils/sanitize';
import { t } from '@/services/i18n';
import { fetchTelegramOsint, type TelegramOsintData, type TelegramOsintItem } from '@/services/telegram-osint';

export class TelegramOsintPanel extends Panel {
private data: TelegramOsintData | null = null;
private loadingEl: HTMLElement | null = null;

constructor() {
super({
id: 'telegram-osint',
title: t('panels.telegramOsint'),
showCount: true,
trackActivity: true,
infoTooltip: 'Telegram OSINT channels for conflict zones and geopolitics',
});
this.init();
}

private async init(): Promise<void> {
this.loadingEl = document.createElement('div');
this.loadingEl.className = 'panel-loading';
this.loadingEl.textContent = 'Loading...';
this.content.appendChild(this.loadingEl);

await this.loadData();
}

private async loadData(): Promise<void> {
try {
this.data = await fetchTelegramOsint();
this.render();
} catch (e) {
console.error('Telegram OSINT error:', e);
this.showError();
}
}

private render(): void {
if (!this.data) return;

this.loadingEl?.remove();

if (this.data.items.length === 0) {
this.content.innerHTML = '<div class="panel-empty">No data available. Configure Telegram bot token for full access.</div>';
return;
}

const list = document.createElement('div');
list.className = 'telegram-osint-list';

this.data.items.slice(0, 20).forEach(item => {
const card = document.createElement('div');
card.className = `osint-card ${item.urgent ? 'urgent' : ''}`;

const header = document.createElement('div');
header.className = 'osint-header';
header.innerHTML = `<span class="channel">${escapeHtml(item.label)}</span><span class="topic">${escapeHtml(item.topic)}</span>`;

const text = document.createElement('div');
text.className = 'osint-text';
text.textContent = item.text.slice(0, 200);

const footer = document.createElement('div');
footer.className = 'osint-footer';
footer.innerHTML = `<span class="views">👁 ${item.views}</span><span class="date">${item.date}</span>`;

card.appendChild(header);
card.appendChild(text);
card.appendChild(footer);
list.appendChild(card);
});

this.content.appendChild(list);
this.updateCount(this.data.items.length);
}

private showError(): void {
this.loadingEl?.remove();
this.content.innerHTML = '<div class="panel-error">Failed to load Telegram OSINT data</div>';
}
}
4 changes: 4 additions & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,7 @@ export * from './ClimateAnomalyPanel';
export * from './InvestmentsPanel';
export * from './LanguageSelector';
export * from './PentagonPizzaPanel';
export * from './TelegramOsintPanel';
export * from './SanctionsPanel';
export * from './RadiationPanel';
export * from './SocialSentimentPanel';
8 changes: 6 additions & 2 deletions src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@
"techReadiness": "Tech Readiness Index",
"gccInvestments": "GCC Investments",
"geoHubs": "Geopolitical Hubs",
"liveWebcams": "Live Webcams"
"liveWebcams": "Live Webcams",
"telegramOsint": "Telegram OSINT",
"sanctions": "Sanctions Tracker",
"radiation": "Radiation Monitor",
"socialSentiment": "Social Sentiment"
},
"modals": {
"search": {
Expand Down Expand Up @@ -1506,4 +1510,4 @@
"close": "Close",
"currentVariant": "(current)"
}
}
}
4 changes: 4 additions & 0 deletions src/services/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,7 @@ export * from './usa-spending';
export * from './oil-analytics';
export { generateSummary, translateText } from './summarization';
export * from './cached-theater-posture';
export * from './telegram-osint';
export * from './sanctions';
export * from './radiation';
export * from './social-sentiment';
Loading