import './styles/main.css' import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks } from './lib/api' import { parseRoute, onRouteChange, navigate, type Route } from './lib/router' import { login, logout, handleCallback, restoreSession } from './lib/auth' import { renderHeader } from './components/header' import { renderProfile } from './components/profile' import { renderPostList, renderPostDetail } from './components/posts' import { renderCollectionButtons, renderServerInfo, renderServiceList, renderCollectionList, renderRecordList, renderRecordDetail } from './components/browser' import { renderFooter } from './components/footer' const app = document.getElementById('app')! let currentHandle = '' // Filter collections by service domain function filterCollectionsByService(collections: string[], service: string): string[] { return collections.filter(col => { const parts = col.split('.') if (parts.length >= 2) { const colService = `${parts[1]}.${parts[0]}` return colService === service } return false }) } // Get web URL for handle from networks async function getWebUrl(handle: string): Promise { const networks = await getNetworks() // Check each network for matching handle domain for (const [domain, network] of Object.entries(networks)) { // Direct domain match (e.g., handle.syu.is -> syu.is) if (handle.endsWith(`.${domain}`)) { return network.web } // Check if handle domain matches network's web domain (e.g., syui.syui.ai -> syu.is via web: syu.is) const webDomain = network.web?.replace(/^https?:\/\//, '') if (webDomain && handle.endsWith(`.${webDomain}`)) { return network.web } } // Check for syui.ai handles -> syu.is network if (handle.endsWith('.syui.ai')) { return networks['syu.is']?.web } // Default to first network's web const firstNetwork = Object.values(networks)[0] return firstNetwork?.web } async function render(route: Route): Promise { try { const config = await getConfig() // Apply theme color from config if (config.color) { document.documentElement.style.setProperty('--btn-color', config.color) } // Handle OAuth callback if present (check both ? and #) const searchParams = new URLSearchParams(window.location.search) const hashParams = window.location.hash ? new URLSearchParams(window.location.hash.slice(1)) : null if (searchParams.has('code') || searchParams.has('state') || hashParams?.has('code') || hashParams?.has('state')) { await handleCallback() } else { // Try to restore existing session await restoreSession() } // Determine handle and whether to use local data let handle: string let localFirst: boolean if (route.type === 'home') { handle = config.handle localFirst = true } else if (route.handle) { handle = route.handle localFirst = handle === config.handle } else { handle = config.handle localFirst = true } currentHandle = handle // Resolve handle to DID const did = await resolveHandle(handle) if (!did) { app.innerHTML = ` ${renderHeader(handle)}
Could not resolve handle: ${handle}
${renderFooter(handle)} ` setupEventHandlers() return } // Load profile const profile = await getProfile(did, localFirst) const webUrl = await getWebUrl(handle) // Build page let html = renderHeader(handle) // Profile section if (profile) { html += await renderProfile(did, profile, handle, webUrl) } // Content section based on route type if (route.type === 'record' && route.collection && route.rkey) { // AT-Browser: Single record view const record = await getRecord(did, route.collection, route.rkey) if (record) { html += `
${renderRecordDetail(record, route.collection)}
` } else { html += `
Record not found
` } html += `` } else if (route.type === 'collection' && route.collection) { // AT-Browser: Collection records list const records = await listRecords(did, route.collection) html += `
${renderRecordList(records, handle, route.collection)}
` const parts = route.collection.split('.') const service = parts.length >= 2 ? `${parts[1]}.${parts[0]}` : '' html += `` } else if (route.type === 'service' && route.service) { // AT-Browser: Service collections list const collections = await describeRepo(did) const filtered = filterCollectionsByService(collections, route.service) html += `
${renderCollectionList(filtered, handle, route.service)}
` html += `` } else if (route.type === 'atbrowser') { // AT-Browser: Main view with server info + service list const pds = await getPds(did) const collections = await describeRepo(did) html += `
` html += renderServerInfo(did, pds) html += renderServiceList(collections, handle) html += `
` html += `` } else if (route.type === 'post' && route.rkey) { // Post detail (config.collection with markdown) const post = await getPost(did, config.collection, route.rkey, localFirst) if (post) { html += `
${renderPostDetail(post, handle, config.collection)}
` } else { html += `
Post not found
` } html += `` } else { // User page: compact collection buttons + posts const collections = await describeRepo(did) html += `
${renderCollectionButtons(collections, handle)}
` const posts = await getPosts(did, config.collection, localFirst) html += `
${renderPostList(posts, handle)}
` } html += renderFooter(handle) app.innerHTML = html setupEventHandlers() } catch (error) { console.error('Render error:', error) app.innerHTML = ` ${renderHeader(currentHandle)}
Error: ${error}
${renderFooter(currentHandle)} ` setupEventHandlers() } } function setupEventHandlers(): void { // Header form const form = document.getElementById('header-form') as HTMLFormElement const input = document.getElementById('header-input') as HTMLInputElement form?.addEventListener('submit', (e) => { e.preventDefault() const handle = input.value.trim() if (handle) { navigate({ type: 'user', handle }) } }) // Login button const loginBtn = document.getElementById('login-btn') loginBtn?.addEventListener('click', async () => { const handle = input.value.trim() || currentHandle if (handle) { try { await login(handle) } catch (e) { console.error('Login failed:', e) alert('Login failed. Please check your handle.') } } else { alert('Please enter a handle first.') } }) // Logout button const logoutBtn = document.getElementById('logout-btn') logoutBtn?.addEventListener('click', async () => { await logout() }) } // Initial render render(parseRoute()) // Handle route changes onRouteChange(render)