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() })