Files
log/src/web/lib/router.ts
2026-01-21 02:30:44 +09:00

136 lines
4.7 KiB
TypeScript

export interface Route {
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old'
handle?: string
rkey?: string
service?: string
collection?: string
}
// Parse current URL to route
export function parseRoute(): Route {
const path = window.location.pathname
// Home: / or /app
if (path === '/' || path === '' || path === '/app' || path === '/app/') {
return { type: 'home' }
}
// AT-Browser main: /@handle/at or /@handle/at/
const atBrowserMatch = path.match(/^\/@([^/]+)\/at\/?$/)
if (atBrowserMatch) {
return { type: 'atbrowser', handle: atBrowserMatch[1] }
}
// AT-Browser service: /@handle/at/service/domain.tld
const serviceMatch = path.match(/^\/@([^/]+)\/at\/service\/([^/]+)$/)
if (serviceMatch) {
return { type: 'service', handle: serviceMatch[1], service: decodeURIComponent(serviceMatch[2]) }
}
// AT-Browser collection: /@handle/at/collection/namespace.name
const collectionMatch = path.match(/^\/@([^/]+)\/at\/collection\/([^/]+)$/)
if (collectionMatch) {
return { type: 'collection', handle: collectionMatch[1], collection: collectionMatch[2] }
}
// AT-Browser record: /@handle/at/collection/namespace.name/rkey
const recordMatch = path.match(/^\/@([^/]+)\/at\/collection\/([^/]+)\/([^/]+)$/)
if (recordMatch) {
return { type: 'record', handle: recordMatch[1], collection: recordMatch[2], rkey: recordMatch[3] }
}
// User page: /@handle or /@handle/
const userMatch = path.match(/^\/@([^/]+)\/?$/)
if (userMatch) {
return { type: 'user', handle: userMatch[1] }
}
// Post form page: /@handle/at/post
const postPageMatch = path.match(/^\/@([^/]+)\/at\/post\/?$/)
if (postPageMatch) {
return { type: 'postpage', handle: postPageMatch[1] }
}
// Card page: /@handle/at/card
const cardMatch = path.match(/^\/@([^/]+)\/at\/card\/?$/)
if (cardMatch) {
return { type: 'card', handle: cardMatch[1] }
}
// Card migration page: /@handle/at/card-old
const cardOldMatch = path.match(/^\/@([^/]+)\/at\/card-old\/?$/)
if (cardOldMatch) {
return { type: 'card-old', handle: cardOldMatch[1] }
}
// Chat thread: /@handle/at/chat/{rkey}
const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/)
if (chatThreadMatch) {
return { type: 'chat-thread', handle: chatThreadMatch[1], rkey: chatThreadMatch[2] }
}
// Chat list: /@handle/at/chat
const chatMatch = path.match(/^\/@([^/]+)\/at\/chat\/?$/)
if (chatMatch) {
return { type: 'chat', handle: chatMatch[1] }
}
// Post detail page: /@handle/rkey (for config.collection)
const postMatch = path.match(/^\/@([^/]+)\/([^/]+)$/)
if (postMatch) {
return { type: 'post', handle: postMatch[1], rkey: postMatch[2] }
}
// Default to home
return { type: 'home' }
}
// Navigate to a route
export function navigate(route: Route): void {
let path = '/'
if (route.type === 'user' && route.handle) {
path = `/@${route.handle}`
} else if (route.type === 'postpage' && route.handle) {
path = `/@${route.handle}/at/post`
} else if (route.type === 'post' && route.handle && route.rkey) {
path = `/@${route.handle}/${route.rkey}`
} else if (route.type === 'atbrowser' && route.handle) {
path = `/@${route.handle}/at`
} else if (route.type === 'service' && route.handle && route.service) {
path = `/@${route.handle}/at/service/${encodeURIComponent(route.service)}`
} else if (route.type === 'collection' && route.handle && route.collection) {
path = `/@${route.handle}/at/collection/${route.collection}`
} else if (route.type === 'record' && route.handle && route.collection && route.rkey) {
path = `/@${route.handle}/at/collection/${route.collection}/${route.rkey}`
} else if (route.type === 'card' && route.handle) {
path = `/@${route.handle}/at/card`
} else if (route.type === 'card-old' && route.handle) {
path = `/@${route.handle}/at/card-old`
} else if (route.type === 'chat' && route.handle) {
path = `/@${route.handle}/at/chat`
} else if (route.type === 'chat-thread' && route.handle && route.rkey) {
path = `/@${route.handle}/at/chat/${route.rkey}`
}
window.history.pushState({}, '', path)
window.dispatchEvent(new PopStateEvent('popstate'))
}
// Subscribe to route changes
export function onRouteChange(callback: (route: Route) => void): void {
const handler = () => callback(parseRoute())
window.addEventListener('popstate', handler)
// Handle link clicks
document.addEventListener('click', (e) => {
const target = e.target as HTMLElement
const anchor = target.closest('a')
if (anchor && anchor.href.startsWith(window.location.origin)) {
e.preventDefault()
window.history.pushState({}, '', anchor.href)
handler()
}
})
}