Skip to content
Open
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
1,171 changes: 1,071 additions & 100 deletions app/file-browser.tsx

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions app/file/[...path]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ import { useFiles } from "@/lib/files-context";
import { CELEBRITY_DATA } from "@/lib/celebrity-data";
import { CelebrityDisclaimer } from "@/components/celebrity-disclaimer";

const WORKER_URL = process.env.NODE_ENV === "development"
? "http://localhost:8787"
: "https://epstein-files.rhys-669.workers.dev";
const WORKER_URL = "https://epstein-files.rhys-669.workers.dev";

// Track in-progress prefetch operations to avoid duplicates
const prefetchingSet = new Set<string>();
Expand Down
132 changes: 110 additions & 22 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,39 +48,42 @@

:root {
--radius: 0.625rem;
--background: oklch(0.09 0.005 260);
--foreground: oklch(0.95 0 0);
--card: oklch(0.13 0.005 260);
--card-foreground: oklch(0.95 0 0);
--popover: oklch(0.13 0.005 260);
--popover-foreground: oklch(0.95 0 0);
/* Light theme (default) */
--background: oklch(0.985 0 0);
--foreground: oklch(0.22 0 0);
--card: oklch(0.985 0 0);
--card-foreground: oklch(0.22 0 0);
--popover: oklch(0.99 0 0);
--popover-foreground: oklch(0.22 0 0);
--primary: oklch(0.65 0.2 250);
--primary-foreground: oklch(0.98 0 0);
--secondary: oklch(0.18 0.005 260);
--secondary-foreground: oklch(0.95 0 0);
--muted: oklch(0.18 0.005 260);
--muted-foreground: oklch(0.65 0 0);
--accent: oklch(0.22 0.01 260);
--accent-foreground: oklch(0.95 0 0);
--destructive: oklch(0.577 0.245 27.325);
--border: oklch(0.25 0.01 260);
--input: oklch(0.20 0.01 260);
--secondary: oklch(0.96 0.005 260);
--secondary-foreground: oklch(0.22 0 0);
--muted: oklch(0.96 0.005 260);
--muted-foreground: oklch(0.45 0 0);
--accent: oklch(0.97 0.01 260);
--accent-foreground: oklch(0.22 0 0);
--destructive: oklch(0.6 0.25 27.3);
--border: oklch(0.9 0.005 260);
--input: oklch(0.95 0.005 260);
--ring: oklch(0.65 0.2 250);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--sidebar: oklch(0.11 0.005 260);
--sidebar-foreground: oklch(0.95 0 0);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.22 0 0);
--sidebar-primary: oklch(0.65 0.2 250);
--sidebar-primary-foreground: oklch(0.98 0 0);
--sidebar-accent: oklch(0.18 0.005 260);
--sidebar-accent-foreground: oklch(0.95 0 0);
--sidebar-border: oklch(0.25 0.01 260);
--sidebar-accent: oklch(0.96 0.005 260);
--sidebar-accent-foreground: oklch(0.22 0 0);
--sidebar-border: oklch(0.9 0.005 260);
--sidebar-ring: oklch(0.65 0.2 250);
}

/* System preference is applied via .dark class in JS for consistency */

.dark {
--background: oklch(0.09 0.005 260);
--foreground: oklch(0.95 0 0);
Expand Down Expand Up @@ -116,6 +119,14 @@
}

@layer base {
html {
color-scheme: light;
}

html.dark {
color-scheme: dark;
}

* {
@apply border-border outline-ring/50;
}
Expand All @@ -124,26 +135,103 @@
}
}

@keyframes theme-fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}

@keyframes theme-fade-out {
from {
opacity: 1;
}
to {
opacity: 0;
}
}

/* Theme transition overlay */
#theme-transition-overlay {
position: fixed;
inset: 0;
pointer-events: none;
background-color: var(--background);
opacity: 0;
z-index: 9999;
}

#theme-transition-overlay.fade-in {
animation: theme-fade-in 0.25s ease-in;
}

#theme-transition-overlay.fade-out {
animation: theme-fade-out 0.25s ease-out;
}

/* Custom scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}

/* Light default */
::-webkit-scrollbar-track {
background: oklch(0.12 0.005 260);
background: oklch(0.95 0.005 260);
}

::-webkit-scrollbar-thumb {
background: oklch(0.28 0.01 260);
background: oklch(0.75 0.02 260);
border-radius: 4px;
}

::-webkit-scrollbar-thumb:hover {
background: oklch(0.7 0.03 260);
}

/* Dark overrides */
.dark ::-webkit-scrollbar-track {
background: oklch(0.12 0.005 260);
}

.dark ::-webkit-scrollbar-thumb {
background: oklch(0.28 0.01 260);
}

.dark ::-webkit-scrollbar-thumb:hover {
background: oklch(0.35 0.01 260);
}

/* Selection styling */
::selection {
background: oklch(0.65 0.2 250 / 0.3);
}

/* Minimap animations */
@keyframes expandMinimap {
from {
max-height: 0;
opacity: 0;
transform: scaleY(0);
}
to {
max-height: calc(100vh - 14rem);
opacity: 1;
transform: scaleY(1);
}
}

@keyframes collapseMinimap {
from {
max-height: calc(100vh - 14rem);
opacity: 1;
transform: scaleY(1);
}
to {
max-height: 0;
opacity: 0;
transform: scaleY(0);
}
}
118 changes: 106 additions & 12 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import Script from "next/script";
import { Geist, Geist_Mono } from "next/font/google";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import { Analytics } from "@vercel/analytics/next";
import { FilesProvider } from "@/lib/files-context";
import { FileItem, PdfManifest } from "@/lib/cache";
import "./globals.css";
import ScrollToTopButton from "@/components/scroll-to-top-button";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -19,6 +21,24 @@ const geistMono = Geist_Mono({
export const metadata: Metadata = {
title: "Epstein Files Browser",
description: "Browse and view the released Epstein files",
manifest: "/manifest.json",
icons: {
icon: "/favicon.ico",
shortcut: "/favicon.ico",
apple: "/favicon.ico",
},
appleWebApp: {
capable: true,
statusBarStyle: "black-translucent",
title: "Files Browser",
},
};

export const viewport: Viewport = {
width: "device-width",
initialScale: 1,
maximumScale: 1,
themeColor: "#1f2937",
};

const WORKER_URL = "https://epstein-files.rhys-669.workers.dev";
Expand All @@ -29,20 +49,29 @@ interface AllFilesResponse {
}

async function fetchAllFiles(): Promise<FileItem[]> {
const response = await fetch(`${WORKER_URL}/api/all-files`, {
next: { revalidate: 3600 }, // Revalidate every hour
});
try {
console.log('Fetching files from:', `${WORKER_URL}/api/all-files`);
const response = await fetch(`${WORKER_URL}/api/all-files`, {
next: { revalidate: 3600 }, // Revalidate every hour
});

if (!response.ok) {
throw new Error("Failed to fetch files");
}
if (!response.ok) {
console.error('Failed to fetch files:', response.status, response.statusText);
throw new Error("Failed to fetch files");
}

const data: AllFilesResponse = await response.json();
return data.files;
const data: AllFilesResponse = await response.json();
console.log('Files loaded:', data.totalReturned);
return data.files;
} catch (error) {
console.error('Error fetching files:', error);
throw error;
}
}

async function fetchPdfManifest(): Promise<PdfManifest> {
try {
console.log('Fetching PDF manifest from:', `${WORKER_URL}/api/pdf-manifest`);
const response = await fetch(`${WORKER_URL}/api/pdf-manifest`, {
next: { revalidate: 3600 }, // Revalidate every hour
});
Expand All @@ -52,9 +81,11 @@ async function fetchPdfManifest(): Promise<PdfManifest> {
return {};
}

return await response.json();
} catch {
console.warn("Failed to fetch PDF manifest, falling back to PDF rendering");
const manifest: PdfManifest = await response.json();
console.log('PDF manifest loaded, entries:', Object.keys(manifest).length);
return manifest;
} catch (error) {
console.warn("Failed to fetch PDF manifest:", error);
return {};
}
}
Expand All @@ -71,12 +102,75 @@ export default async function RootLayout({

return (
<html lang="en">
<head>
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Files Browser" />
<meta name="theme-color" content="#ffffff" />
<link rel="manifest" href="/manifest.json" />
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="apple-touch-icon" href="/favicon.ico" />
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<div id="theme-transition-overlay" />
<Script id="theme-init" strategy="beforeInteractive">
{`(() => {try {var s = localStorage.getItem('theme'); var m = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; var r = document.documentElement; var isDark = s === 'dark' || ((s === 'system' || !s) && m); if (isDark) { r.classList.add('dark'); } else { r.classList.remove('dark'); } var themeColor = document.querySelector('meta[name="theme-color"]'); if (themeColor) { themeColor.setAttribute('content', isDark ? '#000000' : '#ffffff'); }} catch (e) { /* noop */ }})();`}
</Script>
<Script id="theme-color-sync" strategy="afterInteractive">
{`
function updateThemeColor() {
const isDark = document.documentElement.classList.contains('dark');
const themeColor = document.querySelector('meta[name="theme-color"]');
if (themeColor) {
themeColor.setAttribute('content', isDark ? '#000000' : '#ffffff');
}
}

// Update on load
updateThemeColor();

// Watch for theme changes
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === 'class') {
updateThemeColor();
}
});
});

observer.observe(document.documentElement, { attributes: true });

// Also sync with system preference changes
if (window.matchMedia) {
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', updateThemeColor);
}
`}
</Script>
<Script id="sw-register" strategy="afterInteractive">
{`
if ('serviceWorker' in navigator) {
window.addEventListener('load', function() {
navigator.serviceWorker.register('/sw.js', { scope: '/' })
.then(function(registration) {
console.log('Service Worker registered successfully:', registration);
})
.catch(function(error) {
console.error('Service Worker registration failed:', error);
console.error('Error details:', error.message);
});
});
} else {
console.warn('Service Workers are not supported in this browser');
}
`}
</Script>
<FilesProvider files={files} pdfManifest={pdfManifest}>
<NuqsAdapter>{children}</NuqsAdapter>
</FilesProvider>
<ScrollToTopButton />
<Analytics />
</body>
</html>
Expand Down
2 changes: 1 addition & 1 deletion components/celebrity-combobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export function CelebrityCombobox({
variant="outline"
role="combobox"
aria-expanded={open}
className="w-[220px] sm:w-[260px] justify-between bg-secondary border-border text-foreground hover:bg-accent hover:text-foreground rounded-xl h-auto py-2.5 px-4 transition-all duration-200"
className="w-full sm:w-[260px] justify-between bg-secondary border-border text-foreground hover:bg-accent hover:text-foreground rounded-xl h-auto py-2.5 px-4 transition-all duration-200 cursor-pointer"
>
<div className="flex items-center gap-2 min-w-0">
<User className="h-4 w-4 text-muted-foreground flex-shrink-0" />
Expand Down
Loading