From f8cb05ac1b75fe7c36dc83598995c30365d5cb40 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 22 Aug 2026 13:53:12 +0000 Subject: [PATCH] iili: follow the API rename off /r3dr/v2 MoonBase renames the shortener to iili with no alias, so this page has to move with it: the API base becomes api.muchq.com/iili/v1, the app and its page files become iili, and the route is /iili. The old /r3dr redirects rather than 404s, since links to it are already out there, and saved recents are read from the pre-rename localStorage key once so nobody's list disappears. Elsewhere's entry becomes iili.uk to distinguish the standalone site from this page now that both are called iili. Requires the MoonBase deploy first: /r3dr/v2 stops answering when the host takes the rename. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01ST7XLGWstRE4CfUF84ijKD --- src/App.tsx | 5 ++-- src/__tests__/App.routes.test.tsx | 24 +++++++++++++++++ .../__tests__/CopyButton.test.tsx | 0 .../__tests__/IiliPage.test.tsx} | 18 ++++++------- .../__tests__/ShortenCard.test.tsx | 0 src/apps/{r3dr => iili}/__tests__/api.test.ts | 2 +- .../{r3dr => iili}/__tests__/expiry.test.ts | 0 .../{r3dr => iili}/__tests__/recent.test.ts | 26 ++++++++++++++++--- .../{r3dr => iili}/__tests__/urlInput.test.ts | 0 src/apps/{r3dr => iili}/api.ts | 6 ++--- .../components/CopyButton.module.css | 0 .../{r3dr => iili}/components/CopyButton.tsx | 0 .../components/RecentLinks.module.css | 0 .../{r3dr => iili}/components/RecentLinks.tsx | 0 .../components/ShortenCard.module.css | 0 .../{r3dr => iili}/components/ShortenCard.tsx | 0 src/apps/{r3dr => iili}/expiry.ts | 0 .../pages/IiliPage.module.css} | 2 +- .../R3drPage.tsx => iili/pages/IiliPage.tsx} | 8 +++--- src/apps/{r3dr => iili}/recent.ts | 8 ++++-- src/apps/{r3dr => iili}/urlInput.ts | 0 src/shared/components/Navigation.tsx | 4 +-- .../components/__tests__/Navigation.test.tsx | 10 +++---- 23 files changed, 79 insertions(+), 34 deletions(-) create mode 100644 src/__tests__/App.routes.test.tsx rename src/apps/{r3dr => iili}/__tests__/CopyButton.test.tsx (100%) rename src/apps/{r3dr/__tests__/R3drPage.test.tsx => iili/__tests__/IiliPage.test.tsx} (86%) rename src/apps/{r3dr => iili}/__tests__/ShortenCard.test.tsx (100%) rename src/apps/{r3dr => iili}/__tests__/api.test.ts (97%) rename src/apps/{r3dr => iili}/__tests__/expiry.test.ts (100%) rename src/apps/{r3dr => iili}/__tests__/recent.test.ts (74%) rename src/apps/{r3dr => iili}/__tests__/urlInput.test.ts (100%) rename src/apps/{r3dr => iili}/api.ts (90%) rename src/apps/{r3dr => iili}/components/CopyButton.module.css (100%) rename src/apps/{r3dr => iili}/components/CopyButton.tsx (100%) rename src/apps/{r3dr => iili}/components/RecentLinks.module.css (100%) rename src/apps/{r3dr => iili}/components/RecentLinks.tsx (100%) rename src/apps/{r3dr => iili}/components/ShortenCard.module.css (100%) rename src/apps/{r3dr => iili}/components/ShortenCard.tsx (100%) rename src/apps/{r3dr => iili}/expiry.ts (100%) rename src/apps/{r3dr/pages/R3drPage.module.css => iili/pages/IiliPage.module.css} (96%) rename src/apps/{r3dr/pages/R3drPage.tsx => iili/pages/IiliPage.tsx} (91%) rename src/apps/{r3dr => iili}/recent.ts (85%) rename src/apps/{r3dr => iili}/urlInput.ts (100%) diff --git a/src/App.tsx b/src/App.tsx index 237b14e..f21f523 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -13,7 +13,7 @@ import SystemsPage from './apps/metrics-systems/pages/SystemsPage' import ResilienceGamePage from './apps/metrics-systems/pages/ResilienceGamePage' import MetricsPage from './apps/metrics-systems/pages/MetricsPage' import WordchainsPage from './apps/wordchains/pages/WordchainsPage' -import R3drPage from './apps/r3dr/pages/R3drPage' +import IiliPage from './apps/iili/pages/IiliPage' import NotFoundPage from './core/pages/NotFoundPage' function App() { @@ -31,7 +31,8 @@ function App() { } /> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/src/__tests__/App.routes.test.tsx b/src/__tests__/App.routes.test.tsx new file mode 100644 index 0000000..ed265d8 --- /dev/null +++ b/src/__tests__/App.routes.test.tsx @@ -0,0 +1,24 @@ +import { render, screen, within } from '@testing-library/react' +import { MemoryRouter } from 'react-router-dom' +import { describe, expect, it } from 'vitest' +import App from '../App' + +const at = (path: string) => + render( + + + + ) + +describe('App routes', () => { + it('serves the shortener at /iili', () => { + at('/iili') + expect(within(screen.getByRole('navigation')).getByText('MuchQ : iili')).toBeDefined() + }) + + // Links to muchq.com/r3dr predate the rename and still arrive. + it('redirects the pre-rename /r3dr to /iili', () => { + at('/r3dr') + expect(within(screen.getByRole('navigation')).getByText('MuchQ : iili')).toBeDefined() + }) +}) diff --git a/src/apps/r3dr/__tests__/CopyButton.test.tsx b/src/apps/iili/__tests__/CopyButton.test.tsx similarity index 100% rename from src/apps/r3dr/__tests__/CopyButton.test.tsx rename to src/apps/iili/__tests__/CopyButton.test.tsx diff --git a/src/apps/r3dr/__tests__/R3drPage.test.tsx b/src/apps/iili/__tests__/IiliPage.test.tsx similarity index 86% rename from src/apps/r3dr/__tests__/R3drPage.test.tsx rename to src/apps/iili/__tests__/IiliPage.test.tsx index dea97d8..9cdea54 100644 --- a/src/apps/r3dr/__tests__/R3drPage.test.tsx +++ b/src/apps/iili/__tests__/IiliPage.test.tsx @@ -1,7 +1,7 @@ import { render, screen, within } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { beforeEach, describe, expect, it, vi } from 'vitest' -import R3drPage from '../pages/R3drPage' +import IiliPage from '../pages/IiliPage' import * as api from '../api' vi.mock('../api', { spy: true }) @@ -11,7 +11,7 @@ vi.mock('@/shared/components/nav/NavTagline', () => ({ default: () => null })) const NOW = 1755000000000 -describe('R3drPage', () => { +describe('IiliPage', () => { beforeEach(() => { vi.clearAllMocks() localStorage.clear() @@ -21,7 +21,7 @@ describe('R3drPage', () => { it('adds a minted link to Recent links and persists it', async () => { vi.mocked(api.shorten).mockResolvedValue({ slug: 'AQA' }) - render() + render() const user = userEvent.setup() await user.type(screen.getByLabelText('Long link'), 'https://example.com/page') @@ -29,18 +29,18 @@ describe('R3drPage', () => { const recent = await screen.findByRole('region', { name: 'Recent links' }) expect(within(recent).getByRole('link', { name: 'i.iili.uk/r/AQA' })).toBeDefined() - expect(localStorage.getItem('r3dr.recent')).toContain('"AQA"') + expect(localStorage.getItem('iili.recent')).toContain('"AQA"') }) it('boots with stored links, skipping expired ones', () => { localStorage.setItem( - 'r3dr.recent', + 'iili.recent', JSON.stringify([ { slug: 'AQA', longUrl: 'https://example.com/live', expiresAt: NOW + 1000 }, { slug: 'DAA', longUrl: 'https://example.com/dead', expiresAt: NOW - 1000 }, ]) ) - render() + render() const recent = screen.getByRole('region', { name: 'Recent links' }) expect(within(recent).getByRole('link', { name: 'i.iili.uk/r/AQA' })).toBeDefined() @@ -49,15 +49,15 @@ describe('R3drPage', () => { it('clears the list and the storage together', async () => { localStorage.setItem( - 'r3dr.recent', + 'iili.recent', JSON.stringify([{ slug: 'AQA', longUrl: 'https://example.com/x', expiresAt: NOW + 1000 }]) ) - render() + render() const user = userEvent.setup() await user.click(screen.getByRole('button', { name: 'Clear recent links' })) expect(screen.queryByRole('region', { name: 'Recent links' })).toBeNull() - expect(localStorage.getItem('r3dr.recent')).toBeNull() + expect(localStorage.getItem('iili.recent')).toBeNull() }) }) diff --git a/src/apps/r3dr/__tests__/ShortenCard.test.tsx b/src/apps/iili/__tests__/ShortenCard.test.tsx similarity index 100% rename from src/apps/r3dr/__tests__/ShortenCard.test.tsx rename to src/apps/iili/__tests__/ShortenCard.test.tsx diff --git a/src/apps/r3dr/__tests__/api.test.ts b/src/apps/iili/__tests__/api.test.ts similarity index 97% rename from src/apps/r3dr/__tests__/api.test.ts rename to src/apps/iili/__tests__/api.test.ts index dfda0dd..3c137c4 100644 --- a/src/apps/r3dr/__tests__/api.test.ts +++ b/src/apps/iili/__tests__/api.test.ts @@ -19,7 +19,7 @@ describe('shorten', () => { expect(result).toEqual({ slug: 'AQA' }) const [url, init] = fetchMock.mock.calls[0] - expect(url).toBe('https://api.muchq.com/r3dr/v2/shorten') + expect(url).toBe('https://api.muchq.com/iili/v1/shorten') expect(init.method).toBe('POST') // Without this header the browser sends text/plain and drops out of the // preflighted class the API's CORS allow-list serves. diff --git a/src/apps/r3dr/__tests__/expiry.test.ts b/src/apps/iili/__tests__/expiry.test.ts similarity index 100% rename from src/apps/r3dr/__tests__/expiry.test.ts rename to src/apps/iili/__tests__/expiry.test.ts diff --git a/src/apps/r3dr/__tests__/recent.test.ts b/src/apps/iili/__tests__/recent.test.ts similarity index 74% rename from src/apps/r3dr/__tests__/recent.test.ts rename to src/apps/iili/__tests__/recent.test.ts index cbe6174..0d0b896 100644 --- a/src/apps/r3dr/__tests__/recent.test.ts +++ b/src/apps/iili/__tests__/recent.test.ts @@ -39,19 +39,19 @@ describe('addRecent + loadRecent', () => { }) it('shrugs off garbage and missing storage', () => { - localStorage.setItem('r3dr.recent', 'not json') + localStorage.setItem('iili.recent', 'not json') expect(loadRecent(NOW)).toEqual([]) - localStorage.setItem('r3dr.recent', '{"an":"object"}') + localStorage.setItem('iili.recent', '{"an":"object"}') expect(loadRecent(NOW)).toEqual([]) - localStorage.setItem('r3dr.recent', '[{"slug":1}]') + localStorage.setItem('iili.recent', '[{"slug":1}]') expect(loadRecent(NOW)).toEqual([]) }) it('drops entries whose slug is not slug-shaped', () => { localStorage.setItem( - 'r3dr.recent', + 'iili.recent', JSON.stringify([ { slug: '', longUrl: 'https://example.com/a', expiresAt: NOW + 1000 }, { slug: 'javascript:alert(1)', longUrl: 'https://example.com/b', expiresAt: NOW + 1000 }, @@ -78,4 +78,22 @@ describe('clearRecent', () => { clearRecent() expect(loadRecent(NOW)).toEqual([]) }) + + it('reads links saved under the pre-rename key', () => { + localStorage.setItem( + 'r3dr.recent', + JSON.stringify([{ slug: 'AQA', longUrl: 'https://example.com', expiresAt: 2000 }]) + ) + expect(loadRecent(1000).map(l => l.slug)).toEqual(['AQA']) + }) + + it('moves them to the new key on the next save', () => { + localStorage.setItem( + 'r3dr.recent', + JSON.stringify([{ slug: 'AQA', longUrl: 'https://example.com', expiresAt: 2000 }]) + ) + addRecent(loadRecent(1000), { slug: 'BQA', longUrl: 'https://example.org', expiresAt: 2000 }) + expect(localStorage.getItem('r3dr.recent')).toBeNull() + expect(localStorage.getItem('iili.recent')).toContain('"AQA"') + }) }) diff --git a/src/apps/r3dr/__tests__/urlInput.test.ts b/src/apps/iili/__tests__/urlInput.test.ts similarity index 100% rename from src/apps/r3dr/__tests__/urlInput.test.ts rename to src/apps/iili/__tests__/urlInput.test.ts diff --git a/src/apps/r3dr/api.ts b/src/apps/iili/api.ts similarity index 90% rename from src/apps/r3dr/api.ts rename to src/apps/iili/api.ts index 6ae1f2a..c015619 100644 --- a/src/apps/r3dr/api.ts +++ b/src/apps/iili/api.ts @@ -1,7 +1,7 @@ -// The r3dr_v2 API (MoonBase domains/r3dr/apis/r3dr_v2) behind api.muchq.com. -// VITE_R3DR_API_URL points it at a local backend, like the other apps. +// The iili API (MoonBase domains/iili/apis/iili) behind api.muchq.com. +// VITE_IILI_API_URL points it at a local backend, like the other apps. const API_BASE: string = - (import.meta.env.VITE_R3DR_API_URL as string | undefined) || 'https://api.muchq.com/r3dr/v2' + (import.meta.env.VITE_IILI_API_URL as string | undefined) || 'https://api.muchq.com/iili/v1' // Short links resolve on i.iili.uk, where Caddy on the consolidated host // rewrites /r/{slug} onto the same API this page mints against. The diff --git a/src/apps/r3dr/components/CopyButton.module.css b/src/apps/iili/components/CopyButton.module.css similarity index 100% rename from src/apps/r3dr/components/CopyButton.module.css rename to src/apps/iili/components/CopyButton.module.css diff --git a/src/apps/r3dr/components/CopyButton.tsx b/src/apps/iili/components/CopyButton.tsx similarity index 100% rename from src/apps/r3dr/components/CopyButton.tsx rename to src/apps/iili/components/CopyButton.tsx diff --git a/src/apps/r3dr/components/RecentLinks.module.css b/src/apps/iili/components/RecentLinks.module.css similarity index 100% rename from src/apps/r3dr/components/RecentLinks.module.css rename to src/apps/iili/components/RecentLinks.module.css diff --git a/src/apps/r3dr/components/RecentLinks.tsx b/src/apps/iili/components/RecentLinks.tsx similarity index 100% rename from src/apps/r3dr/components/RecentLinks.tsx rename to src/apps/iili/components/RecentLinks.tsx diff --git a/src/apps/r3dr/components/ShortenCard.module.css b/src/apps/iili/components/ShortenCard.module.css similarity index 100% rename from src/apps/r3dr/components/ShortenCard.module.css rename to src/apps/iili/components/ShortenCard.module.css diff --git a/src/apps/r3dr/components/ShortenCard.tsx b/src/apps/iili/components/ShortenCard.tsx similarity index 100% rename from src/apps/r3dr/components/ShortenCard.tsx rename to src/apps/iili/components/ShortenCard.tsx diff --git a/src/apps/r3dr/expiry.ts b/src/apps/iili/expiry.ts similarity index 100% rename from src/apps/r3dr/expiry.ts rename to src/apps/iili/expiry.ts diff --git a/src/apps/r3dr/pages/R3drPage.module.css b/src/apps/iili/pages/IiliPage.module.css similarity index 96% rename from src/apps/r3dr/pages/R3drPage.module.css rename to src/apps/iili/pages/IiliPage.module.css index 2cffc0d..8b3afca 100644 --- a/src/apps/r3dr/pages/R3drPage.module.css +++ b/src/apps/iili/pages/IiliPage.module.css @@ -1,4 +1,4 @@ -/* Springtime meadow — the look r3dr wears within the site's per-app +/* Springtime meadow — the look iili wears within the site's per-app palette convention. */ .container { min-height: 100vh; diff --git a/src/apps/r3dr/pages/R3drPage.tsx b/src/apps/iili/pages/IiliPage.tsx similarity index 91% rename from src/apps/r3dr/pages/R3drPage.tsx rename to src/apps/iili/pages/IiliPage.tsx index 3260b8d..b4d54f1 100644 --- a/src/apps/r3dr/pages/R3drPage.tsx +++ b/src/apps/iili/pages/IiliPage.tsx @@ -4,9 +4,9 @@ import NavTagline from '@/shared/components/nav/NavTagline' import ShortenCard from '../components/ShortenCard' import RecentLinks from '../components/RecentLinks' import { addRecent, clearRecent, loadRecent, type RecentLink } from '../recent' -import styles from './R3drPage.module.css' +import styles from './IiliPage.module.css' -const R3drPage = () => { +const IiliPage = () => { const [recent, setRecent] = useState(() => loadRecent(Date.now())) // Re-render each minute so "expires in …" stays true in a tab left open, @@ -22,7 +22,7 @@ const R3drPage = () => { return (
- } /> + } />

r3dr @@ -46,4 +46,4 @@ const R3drPage = () => { ) } -export default R3drPage +export default IiliPage diff --git a/src/apps/r3dr/recent.ts b/src/apps/iili/recent.ts similarity index 85% rename from src/apps/r3dr/recent.ts rename to src/apps/iili/recent.ts index ab59693..92b427b 100644 --- a/src/apps/r3dr/recent.ts +++ b/src/apps/iili/recent.ts @@ -8,7 +8,9 @@ export interface RecentLink { expiresAt: number } -const KEY = 'r3dr.recent' +const KEY = 'iili.recent' +// Links saved before the rename; read once so nobody's list disappears. +const LEGACY_KEY = 'r3dr.recent' const MAX = 5 // Slugs are exactly 3, 6, or 11 base64url chars (the encoder's widths). A @@ -18,7 +20,7 @@ const SLUG_SHAPE = /^(?:[A-Za-z0-9_-]{3}|[A-Za-z0-9_-]{6}|[A-Za-z0-9_-]{11})$/ export function loadRecent(now: number): RecentLink[] { try { - const raw = localStorage.getItem(KEY) + const raw = localStorage.getItem(KEY) ?? localStorage.getItem(LEGACY_KEY) if (!raw) return [] const parsed: unknown = JSON.parse(raw) if (!Array.isArray(parsed)) return [] @@ -43,6 +45,7 @@ export function addRecent(links: RecentLink[], link: RecentLink): RecentLink[] { const next = [link, ...links.filter(l => l.slug !== link.slug)].slice(0, MAX) try { localStorage.setItem(KEY, JSON.stringify(next)) + localStorage.removeItem(LEGACY_KEY) } catch { // storage unavailable; the in-memory list still renders } @@ -52,6 +55,7 @@ export function addRecent(links: RecentLink[], link: RecentLink): RecentLink[] { export function clearRecent(): void { try { localStorage.removeItem(KEY) + localStorage.removeItem(LEGACY_KEY) } catch { // nothing to clear } diff --git a/src/apps/r3dr/urlInput.ts b/src/apps/iili/urlInput.ts similarity index 100% rename from src/apps/r3dr/urlInput.ts rename to src/apps/iili/urlInput.ts diff --git a/src/shared/components/Navigation.tsx b/src/shared/components/Navigation.tsx index 72b3dab..df9c7cc 100644 --- a/src/shared/components/Navigation.tsx +++ b/src/shared/components/Navigation.tsx @@ -36,7 +36,7 @@ const MENU: MenuGroup[] = [ { label: 'Posterize', to: '/posterize' }, { label: 'Metrics', to: '/metrics' }, { label: 'Wordchains', to: '/wordchains' }, - { label: 'r3dr', to: '/r3dr', description: 'URL shortener' }, + { label: 'iili', to: '/iili', description: 'URL shortener' }, ], }, { @@ -63,7 +63,7 @@ const MENU: MenuGroup[] = [ { label: 'tty1', to: 'https://tty1.uk', external: true, description: 'Web terminal' }, { label: '里に春が来ました', to: 'https://sato-ni-haru-ga-kimashita.uk', external: true, description: 'Japanese sentence breakdown' }, { label: 'p2bx', to: 'https://p2bx.uk', external: true, description: 'Stone–Čech compactification' }, - { label: 'iili', to: 'https://iili.uk', external: true, description: 'URL shortener' }, + { label: 'iili.uk', to: 'https://iili.uk', external: true, description: 'The shortener on its own domain' }, ], }, { diff --git a/src/shared/components/__tests__/Navigation.test.tsx b/src/shared/components/__tests__/Navigation.test.tsx index 961417b..388e3f6 100644 --- a/src/shared/components/__tests__/Navigation.test.tsx +++ b/src/shared/components/__tests__/Navigation.test.tsx @@ -46,7 +46,7 @@ describe('Navigation', () => { [/^tty1\s?\(external site\)/, 'https://tty1.uk', 'Web terminal'], [/^里に春が来ました\s?\(external site\)/, 'https://sato-ni-haru-ga-kimashita.uk', 'Japanese sentence breakdown'], [/^p2bx\s?\(external site\)/, 'https://p2bx.uk', 'Stone–Čech compactification'], - [/^iili\s?\(external site\)/, 'https://iili.uk', 'URL shortener'], + [/^iili\.uk\s?\(external site\)/, 'https://iili.uk', 'The shortener on its own domain'], ] for (const [name, href, description] of expected) { const link = group.getByRole('link', { name }) @@ -55,14 +55,12 @@ describe('Navigation', () => { } }) - // r3dr moved from Elsewhere to Projects when the page moved into this - // site (#1359 chunk 2): internal now, description intact. - it('links r3dr as an internal Projects page', () => { + it('links iili as an internal Projects page', () => { renderWithRouter() const groupEl = testingScreen.getByText('Projects').closest('li') if (!groupEl) throw new Error('Projects nav group not found') - const link = within(groupEl).getByRole('link', { name: /^r3dr/ }) - expect(link.getAttribute('href')).toBe('/r3dr') + const link = within(groupEl).getByRole('link', { name: /^iili/ }) + expect(link.getAttribute('href')).toBe('/iili') expect(link.textContent).not.toContain('(external site)') expect(within(link).getByText('URL shortener')).toBeDefined() })