Skip to content
Merged
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
5 changes: 3 additions & 2 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -31,7 +31,8 @@ function App() {
<Route path="/tracy" element={<TracyPage />} />
<Route path="/posterize" element={<PosterizePage />} />
<Route path="/wordchains" element={<WordchainsPage />} />
<Route path="/r3dr" element={<R3drPage />} />
<Route path="/iili" element={<IiliPage />} />
<Route path="/r3dr" element={<Navigate to="/iili" replace />} />
<Route path="/metrics" element={<Navigate to="/metrics/host" replace />} />
<Route path="/metrics/:tab" element={<MetricsPage />} />
<Route path="/resilience" element={<SystemsPage />} />
Expand Down
24 changes: 24 additions & 0 deletions src/__tests__/App.routes.test.tsx
Original file line number Diff line number Diff line change
@@ -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(
<MemoryRouter initialEntries={[path]}>
<App />
</MemoryRouter>
)

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()
})
})
Original file line number Diff line number Diff line change
@@ -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 })
Expand All @@ -11,7 +11,7 @@ vi.mock('@/shared/components/nav/NavTagline', () => ({ default: () => null }))

const NOW = 1755000000000

describe('R3drPage', () => {
describe('IiliPage', () => {
beforeEach(() => {
vi.clearAllMocks()
localStorage.clear()
Expand All @@ -21,26 +21,26 @@ describe('R3drPage', () => {

it('adds a minted link to Recent links and persists it', async () => {
vi.mocked(api.shorten).mockResolvedValue({ slug: 'AQA' })
render(<R3drPage />)
render(<IiliPage />)

const user = userEvent.setup()
await user.type(screen.getByLabelText('Long link'), 'https://example.com/page')
await user.click(screen.getByRole('button', { name: 'Shorten' }))

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(<R3drPage />)
render(<IiliPage />)

const recent = screen.getByRole('region', { name: 'Recent links' })
expect(within(recent).getByRole('link', { name: 'i.iili.uk/r/AQA' })).toBeDefined()
Expand All @@ -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(<R3drPage />)
render(<IiliPage />)

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()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: '<img src=x>', longUrl: 'https://example.com/a', expiresAt: NOW + 1000 },
{ slug: 'javascript:alert(1)', longUrl: 'https://example.com/b', expiresAt: NOW + 1000 },
Expand All @@ -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"')
})
})
6 changes: 3 additions & 3 deletions src/apps/r3dr/api.ts → src/apps/iili/api.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
File renamed without changes.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RecentLink[]>(() => loadRecent(Date.now()))

// Re-render each minute so "expires in …" stays true in a tab left open,
Expand All @@ -22,7 +22,7 @@ const R3drPage = () => {

return (
<div className={styles.container}>
<Navigation appName="r3dr" context={<NavTagline text="URL Shortener" />} />
<Navigation appName="iili" context={<NavTagline text="URL Shortener" />} />
<header className={styles.header}>
<h1 className={styles.wordmark}>
r<span className={styles.blossom}>3</span>dr
Expand All @@ -46,4 +46,4 @@ const R3drPage = () => {
)
}

export default R3drPage
export default IiliPage
8 changes: 6 additions & 2 deletions src/apps/r3dr/recent.ts → src/apps/iili/recent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 []
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
File renamed without changes.
4 changes: 2 additions & 2 deletions src/shared/components/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
{
Expand All @@ -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' },
],
},
{
Expand Down
10 changes: 4 additions & 6 deletions src/shared/components/__tests__/Navigation.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand All @@ -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(<Navigation />)
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()
})
Expand Down
Loading