diff --git a/nginx.conf b/nginx.conf
index 6ed80b2..6837e93 100644
--- a/nginx.conf
+++ b/nginx.conf
@@ -13,6 +13,7 @@ server {
}
location / {
+ add_header Cache-Control "no-cache" always;
try_files $uri $uri/ /index.html;
}
}
diff --git a/src/App.tsx b/src/App.tsx
index cd69c24..b98b291 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,7 +1,7 @@
import '@mantine/core/styles.css';
-import { Center, createTheme, Loader, MantineProvider } from '@mantine/core';
-import { lazy, Suspense } from 'react';
+import { Button, Center, createTheme, Loader, MantineProvider, Stack, Text, Title } from '@mantine/core';
+import { Component, lazy, Suspense, type ReactNode } from 'react';
import { Route, Routes } from 'react-router-dom';
import { ScrollToTop } from './components/ScrollToTop/ScrollToTop';
import './style.css';
@@ -39,27 +39,60 @@ const theme = createTheme({
},
});
+class PageErrorBoundary extends Component<
+ { children: ReactNode },
+ { hasError: boolean }
+> {
+ state = { hasError: false };
+
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+
+ This page could not be loaded
+
+
+ Rosetta may have been updated while this tab was open. Reload the page to continue.
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
+
export default function App() {
return (
-
-
-
-
- }
- >
-
- } />
- } />
- } />
- } />
- } />
- } />
- } />
-
-
+
+
+
+
+
+ }
+ >
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
);
}
diff --git a/src/components/LegalPage/LegalPage.module.css b/src/components/LegalPage/LegalPage.module.css
index 8a8f27c..c9ab85d 100644
--- a/src/components/LegalPage/LegalPage.module.css
+++ b/src/components/LegalPage/LegalPage.module.css
@@ -1,18 +1,22 @@
.page {
display: flex;
flex-direction: column;
+ min-width: 0;
min-height: 100vh;
background: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-9));
}
.shell {
flex: 1;
+ width: 100%;
+ min-width: 0;
padding-top: 72px;
padding-bottom: 96px;
}
.main {
width: 100%;
+ min-width: 0;
max-width: 900px;
margin-inline: auto;
padding: clamp(30px, 5vw, 60px);
@@ -21,6 +25,7 @@
}
.intro {
+ min-width: 0;
max-width: 760px;
padding-bottom: 36px;
border-bottom: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
@@ -69,6 +74,7 @@
}
.section {
+ min-width: 0;
margin-top: 44px;
padding-top: 36px;
border-top: 1px solid light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5));
diff --git a/src/main.tsx b/src/main.tsx
index b6ebc29..327ca80 100644
--- a/src/main.tsx
+++ b/src/main.tsx
@@ -2,6 +2,42 @@ import { createRoot } from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App.tsx'
+const PRELOAD_RELOAD_KEY = 'rosetta:preload-reload-at'
+const PRELOAD_RELOAD_COOLDOWN = 10_000
+
+if (import.meta.env.PROD) {
+ window.addEventListener('vite:preloadError', (event) => {
+ let lastReload = 0
+
+ try {
+ lastReload = Number(window.sessionStorage.getItem(PRELOAD_RELOAD_KEY) || 0)
+ } catch {
+ // Storage can be unavailable in restricted browser modes.
+ }
+
+ if (Date.now() - lastReload < PRELOAD_RELOAD_COOLDOWN) {
+ return
+ }
+
+ event.preventDefault()
+
+ try {
+ window.sessionStorage.setItem(PRELOAD_RELOAD_KEY, String(Date.now()))
+ } catch {
+ // Reload recovery still works when storage is unavailable.
+ }
+
+ window.location.reload()
+ })
+
+ window.setTimeout(() => {
+ try {
+ window.sessionStorage.removeItem(PRELOAD_RELOAD_KEY)
+ } catch {
+ // Nothing to clear when storage is unavailable.
+ }
+ }, PRELOAD_RELOAD_COOLDOWN)
+}
createRoot(document.getElementById('root')!).render(
diff --git a/worker/index.js b/worker/index.js
index a163804..1992a44 100644
--- a/worker/index.js
+++ b/worker/index.js
@@ -1,3 +1,18 @@
+function withFreshHtml(response) {
+ if (!response.headers.get('content-type')?.includes('text/html')) {
+ return response;
+ }
+
+ const headers = new Headers(response.headers);
+ headers.set('Cache-Control', 'no-cache');
+
+ return new Response(response.body, {
+ status: response.status,
+ statusText: response.statusText,
+ headers,
+ });
+}
+
const worker = {
async fetch(request, env) {
const response = await env.ASSETS.fetch(request);
@@ -7,11 +22,12 @@ const worker = {
request.method !== 'GET' ||
!request.headers.get('accept')?.includes('text/html')
) {
- return response;
+ return withFreshHtml(response);
}
const indexUrl = new URL('/index.html', request.url);
- return env.ASSETS.fetch(new Request(indexUrl, request));
+ const indexResponse = await env.ASSETS.fetch(new Request(indexUrl, request));
+ return withFreshHtml(indexResponse);
},
};