169 lines
7.1 KiB
TypeScript
169 lines
7.1 KiB
TypeScript
import React from 'react';
|
|
import { DOSSIERS } from '../constants';
|
|
import { resolvePublicUrl } from '../lib/urls';
|
|
|
|
const formatBytes = (bytes?: number) => {
|
|
if (!bytes) return null;
|
|
const units = ['B', 'KB', 'MB', 'GB'];
|
|
let b = bytes;
|
|
let i = 0;
|
|
while (b >= 1024 && i < units.length - 1) {
|
|
b /= 1024;
|
|
i += 1;
|
|
}
|
|
return `${b.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
|
};
|
|
|
|
const statusLabel = (status: string) => {
|
|
switch (status) {
|
|
case 'live':
|
|
return 'LIVE';
|
|
case 'scheduled':
|
|
return 'SCHEDULED';
|
|
case 'draft':
|
|
default:
|
|
return 'DRAFT';
|
|
}
|
|
};
|
|
|
|
export const LeakViewer: React.FC = () => {
|
|
return (
|
|
<section id="dossiers" className="py-16 md:py-20 px-6">
|
|
<div className="max-w-6xl mx-auto">
|
|
<div className="mb-10">
|
|
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Dossiers</div>
|
|
<h2 className="text-3xl md:text-4xl font-extrabold tracking-tight text-slate-900">Current publications</h2>
|
|
</div>
|
|
|
|
<div className="space-y-8">
|
|
{DOSSIERS.map((d) => {
|
|
const isLive = d.status === 'live';
|
|
const pdfUrl = resolvePublicUrl(d.pdfPath);
|
|
const evidenceUrl = resolvePublicUrl(d.evidencePath);
|
|
|
|
return (
|
|
<article key={d.id} className="rounded-2xl border hairline bg-white shadow-sm overflow-hidden">
|
|
<div className="grid md:grid-cols-12">
|
|
<div className="md:col-span-4 p-6 md:p-8 bg-paper">
|
|
<div className="rounded-xl border hairline bg-white overflow-hidden">
|
|
<img
|
|
src={d.previewImageUrl}
|
|
alt={`Cover for Shadow Dossier: ${d.title}`}
|
|
className="w-full h-auto block"
|
|
loading="lazy"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="md:col-span-8 p-6 md:p-8">
|
|
<div className="flex flex-wrap items-center gap-2 mb-3">
|
|
<span className="mono text-[11px] text-slate-500">{d.leakDate}</span>
|
|
<span className="text-slate-300">•</span>
|
|
|
|
<span className="inline-flex items-center rounded-full border hairline px-2.5 py-1 mono text-[11px] text-slate-600">
|
|
{d.classification}
|
|
</span>
|
|
|
|
<span className="inline-flex items-center rounded-full border hairline px-2.5 py-1 mono text-[11px] text-slate-600">
|
|
{statusLabel(d.status)}
|
|
</span>
|
|
|
|
<span className="text-slate-300">•</span>
|
|
<span className="mono text-[11px] text-slate-500">{d.id}</span>
|
|
</div>
|
|
|
|
<h3 className="text-2xl md:text-3xl font-extrabold tracking-tight text-slate-900 mb-3">
|
|
{d.title}
|
|
</h3>
|
|
|
|
<p className="text-base md:text-lg text-slate-700 leading-relaxed mb-6">{d.summary}</p>
|
|
|
|
<div className="grid md:grid-cols-2 gap-6">
|
|
<div className="rounded-xl border hairline bg-paper p-4">
|
|
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Verification</div>
|
|
|
|
{d.sha256 && isLive ? (
|
|
<div className="space-y-1">
|
|
<div className="mono text-[11px] text-slate-500">SHA-256</div>
|
|
<div className="mono text-[12px] text-slate-700 break-all">{d.sha256}</div>
|
|
{d.bytes ? (
|
|
<div className="mono text-[11px] text-slate-500">Size: {formatBytes(d.bytes)}</div>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<div className="text-sm text-slate-600 leading-relaxed">
|
|
Integrity details will publish with the drop. Until then, link verification remains staged.
|
|
</div>
|
|
)}
|
|
|
|
{evidenceUrl && isLive ? (
|
|
<div className="mt-3">
|
|
<a
|
|
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
|
href={evidenceUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Verify (evidence index)
|
|
</a>
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
|
|
<div className="rounded-xl border hairline bg-paper p-4">
|
|
<div className="mono text-[11px] uppercase tracking-[0.18em] text-slate-500 mb-2">Links</div>
|
|
|
|
<div className="flex flex-wrap gap-3 items-center">
|
|
{pdfUrl && isLive ? (
|
|
<a
|
|
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
|
href={pdfUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Download PDF
|
|
</a>
|
|
) : (
|
|
<span className="text-sm font-semibold text-slate-400 cursor-not-allowed" title="Not published yet">
|
|
PDF not published
|
|
</span>
|
|
)}
|
|
|
|
{d.gumroadUrl ? (
|
|
<a
|
|
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
|
href={d.gumroadUrl}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Classified edition
|
|
</a>
|
|
) : null}
|
|
|
|
{d.contactEmail ? (
|
|
<a
|
|
className="text-sm font-semibold underline underline-offset-4 hover:no-underline"
|
|
href={`mailto:${d.contactEmail}?subject=${encodeURIComponent(`Shadow Dossier: ${d.title}`)}`}
|
|
>
|
|
Contact
|
|
</a>
|
|
) : null}
|
|
</div>
|
|
|
|
{!isLive ? (
|
|
<div className="mt-3 text-sm text-slate-600 leading-relaxed">
|
|
This dossier is staged. Links will activate when the drop is published.
|
|
</div>
|
|
) : null}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
};
|