680 lines
22 KiB
TypeScript
680 lines
22 KiB
TypeScript
import { getProfile, listRecords, getRecord, setNetworkConfig } from './lib/api.js'
|
|
import { renderServices } from './components/services.js'
|
|
import { login, logout, restoreSession, handleOAuthCallback, setAuthNetworkConfig, type AuthSession } from './lib/auth.js'
|
|
import { mountProfile } from './components/profile.js'
|
|
import { mountPostList, mountPostDetail } from './components/posts.js'
|
|
import { mountHeader } from './components/browser.js'
|
|
import { mountAtBrowser } from './components/atbrowser.js'
|
|
import { mountPostForm } from './components/postform.js'
|
|
import { loadDiscussionPosts } from './components/discussion.js'
|
|
import { parseRoute, type Route } from './lib/router.js'
|
|
import { escapeHtml } from './lib/utils.js'
|
|
import type { AppConfig, Networks } from './types.js'
|
|
|
|
let authSession: AuthSession | null = null
|
|
let config: AppConfig
|
|
let networks: Networks = {}
|
|
let browserNetwork: string = '' // Network for AT Browser
|
|
|
|
// Browser state
|
|
let browserMode = false
|
|
let browserState = {
|
|
handle: '',
|
|
collection: null as string | null,
|
|
rkey: null as string | null,
|
|
service: null as string | null
|
|
}
|
|
let savedContent: { profile: string; content: string } | null = null
|
|
|
|
async function loadConfig(): Promise<AppConfig> {
|
|
const res = await fetch('/config.json')
|
|
return res.json()
|
|
}
|
|
|
|
async function loadNetworks(): Promise<Networks> {
|
|
const res = await fetch('/networks.json')
|
|
return res.json()
|
|
}
|
|
|
|
function renderFooter(handle: string): string {
|
|
const parts = handle.split('.')
|
|
const username = parts[0] || handle
|
|
return `
|
|
<footer class="site-footer">
|
|
<p>© ${username}</p>
|
|
</footer>
|
|
`
|
|
}
|
|
|
|
function renderPdsSelector(): string {
|
|
const networkKeys = Object.keys(networks)
|
|
const options = networkKeys.map(key => {
|
|
const isSelected = key === browserNetwork
|
|
return `<div class="pds-option ${isSelected ? 'selected' : ''}" data-network="${escapeHtml(key)}">
|
|
<span class="pds-name">${escapeHtml(key)}</span>
|
|
<span class="pds-check">✓</span>
|
|
</div>`
|
|
}).join('')
|
|
|
|
return `
|
|
<div class="pds-selector" id="pds-selector">
|
|
<button type="button" class="tab" id="pds-tab">PDS</button>
|
|
<div class="pds-dropdown" id="pds-dropdown">
|
|
${options}
|
|
</div>
|
|
</div>
|
|
`
|
|
}
|
|
|
|
function updatePdsSelector(): void {
|
|
const dropdown = document.getElementById('pds-dropdown')
|
|
if (!dropdown) return
|
|
|
|
const options = dropdown.querySelectorAll('.pds-option')
|
|
options.forEach(opt => {
|
|
const el = opt as HTMLElement
|
|
const network = el.dataset.network
|
|
const isSelected = network === browserNetwork
|
|
el.classList.toggle('selected', isSelected)
|
|
})
|
|
}
|
|
|
|
function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean): string {
|
|
let tabs = `
|
|
<a href="/" class="tab ${activeTab === 'blog' ? 'active' : ''}" id="blog-tab">Blog</a>
|
|
<button type="button" class="tab ${activeTab === 'browser' ? 'active' : ''}" id="browser-tab" data-handle="${config.handle}">Browser</button>
|
|
`
|
|
|
|
if (isLoggedIn) {
|
|
tabs += `<a href="/post" class="tab ${activeTab === 'new' ? 'active' : ''}">Post</a>`
|
|
}
|
|
|
|
tabs += renderPdsSelector()
|
|
|
|
return `<div class="mode-tabs">${tabs}</div>`
|
|
}
|
|
|
|
// Browser functions (page-based, not modal)
|
|
function openBrowser(handle: string, service: string | null = null, collection: string | null = null, rkey: string | null = null): void {
|
|
const contentEl = document.getElementById('content')
|
|
const tabsEl = document.querySelector('.mode-tabs')
|
|
|
|
if (!contentEl || !tabsEl) return
|
|
|
|
// Save current content if not already in browser mode
|
|
if (!browserMode) {
|
|
savedContent = {
|
|
profile: '', // Not used anymore
|
|
content: contentEl.innerHTML
|
|
}
|
|
}
|
|
|
|
browserMode = true
|
|
browserState = { handle, service, collection, rkey }
|
|
|
|
// Update tabs to show browser as active
|
|
const blogTab = tabsEl.querySelector('#blog-tab, a[href="/"]')
|
|
const browserTab = tabsEl.querySelector('#browser-tab')
|
|
if (blogTab) blogTab.classList.remove('active')
|
|
if (browserTab) browserTab.classList.add('active')
|
|
|
|
// Show skeleton UI immediately
|
|
contentEl.innerHTML = `
|
|
<div class="browser-skeleton">
|
|
<div class="skeleton-header">
|
|
<div class="skeleton-title"></div>
|
|
</div>
|
|
<ul class="skeleton-list">
|
|
<li class="skeleton-item"><div class="skeleton-icon"></div><div class="skeleton-text"></div></li>
|
|
<li class="skeleton-item"><div class="skeleton-icon"></div><div class="skeleton-text"></div></li>
|
|
<li class="skeleton-item"><div class="skeleton-icon"></div><div class="skeleton-text"></div></li>
|
|
</ul>
|
|
</div>
|
|
`
|
|
|
|
// Load browser content
|
|
loadBrowserContent()
|
|
}
|
|
|
|
function closeBrowser(): void {
|
|
if (!browserMode || !savedContent) return
|
|
|
|
const contentEl = document.getElementById('content')
|
|
const tabsEl = document.querySelector('.mode-tabs')
|
|
|
|
if (!contentEl || !tabsEl) return
|
|
|
|
// Restore saved content
|
|
contentEl.innerHTML = savedContent.content
|
|
|
|
// Update tabs to show blog as active
|
|
const blogTab = tabsEl.querySelector('#blog-tab, a[href="/"]')
|
|
const browserTab = tabsEl.querySelector('#browser-tab')
|
|
if (blogTab) blogTab.classList.add('active')
|
|
if (browserTab) browserTab.classList.remove('active')
|
|
|
|
browserMode = false
|
|
browserState = { handle: '', service: null, collection: null, rkey: null }
|
|
savedContent = null
|
|
}
|
|
|
|
async function loadBrowserContent(): Promise<void> {
|
|
const contentEl = document.getElementById('content')
|
|
if (!contentEl) return
|
|
|
|
// Set network config for browser
|
|
const browserNetworkConfig = networks[browserNetwork]
|
|
if (browserNetworkConfig) {
|
|
setNetworkConfig(browserNetworkConfig)
|
|
}
|
|
|
|
const loginDid = authSession?.did || null
|
|
await mountAtBrowser(
|
|
contentEl,
|
|
browserState.handle,
|
|
browserState.collection,
|
|
browserState.rkey,
|
|
browserState.service,
|
|
loginDid
|
|
)
|
|
}
|
|
|
|
// Add edit button to static post page
|
|
async function addEditButtonToStaticPost(collection: string, rkey: string, session: AuthSession): Promise<void> {
|
|
const postMeta = document.querySelector('.post-meta')
|
|
if (!postMeta) return
|
|
|
|
// Check if user owns this post
|
|
const profile = await getProfile(config.handle)
|
|
if (session.did !== profile.did) return
|
|
|
|
// Add edit button
|
|
const editBtn = document.createElement('button')
|
|
editBtn.className = 'edit-btn'
|
|
editBtn.id = 'edit-btn'
|
|
editBtn.textContent = 'edit'
|
|
postMeta.appendChild(editBtn)
|
|
|
|
// Get current post content
|
|
const titleEl = document.querySelector('.post-title') as HTMLElement
|
|
const contentEl = document.querySelector('.post-content') as HTMLElement
|
|
const postArticle = document.querySelector('.post-detail') as HTMLElement
|
|
|
|
if (!titleEl || !contentEl || !postArticle) return
|
|
|
|
const originalTitle = titleEl.textContent || ''
|
|
// Get original markdown from the record
|
|
const record = await getRecord(profile.did, collection, rkey)
|
|
if (!record) return
|
|
|
|
// Create edit form
|
|
const editFormContainer = document.createElement('div')
|
|
editFormContainer.className = 'edit-form-container'
|
|
editFormContainer.id = 'edit-form-container'
|
|
editFormContainer.style.display = 'none'
|
|
editFormContainer.innerHTML = `
|
|
<h3>Edit Post</h3>
|
|
<form class="edit-form" id="edit-form">
|
|
<input type="text" id="edit-title" class="edit-form-title" value="${escapeHtml(originalTitle)}" placeholder="Title" required>
|
|
<textarea id="edit-content" class="edit-form-body" placeholder="Content" required>${escapeHtml(record.content)}</textarea>
|
|
<div class="edit-form-footer">
|
|
<button type="button" id="edit-cancel" class="edit-cancel-btn">Cancel</button>
|
|
<button type="submit" id="edit-submit" class="edit-submit-btn">Save</button>
|
|
</div>
|
|
</form>
|
|
`
|
|
postArticle.parentNode?.insertBefore(editFormContainer, postArticle.nextSibling)
|
|
|
|
// Event listeners
|
|
editBtn.addEventListener('click', () => {
|
|
postArticle.style.display = 'none'
|
|
editFormContainer.style.display = 'block'
|
|
})
|
|
|
|
document.getElementById('edit-cancel')?.addEventListener('click', () => {
|
|
postArticle.style.display = 'block'
|
|
editFormContainer.style.display = 'none'
|
|
})
|
|
|
|
document.getElementById('edit-form')?.addEventListener('submit', async (e) => {
|
|
e.preventDefault()
|
|
const title = (document.getElementById('edit-title') as HTMLInputElement).value
|
|
const content = (document.getElementById('edit-content') as HTMLTextAreaElement).value
|
|
const submitBtn = document.getElementById('edit-submit') as HTMLButtonElement
|
|
|
|
try {
|
|
submitBtn.disabled = true
|
|
submitBtn.textContent = 'Saving...'
|
|
|
|
const { putRecord } = await import('./lib/auth.js')
|
|
await putRecord(collection, rkey, {
|
|
title,
|
|
content,
|
|
createdAt: record.createdAt,
|
|
})
|
|
|
|
window.location.reload()
|
|
} catch (err) {
|
|
alert('Save failed: ' + err)
|
|
submitBtn.disabled = false
|
|
submitBtn.textContent = 'Save'
|
|
}
|
|
})
|
|
}
|
|
|
|
// Refresh post list from API (for static pages)
|
|
async function refreshPostListFromAPI(): Promise<void> {
|
|
const contentEl = document.getElementById('content')
|
|
if (!contentEl) return
|
|
|
|
try {
|
|
const profile = await getProfile(config.handle)
|
|
const apiPosts = await listRecords(profile.did, config.collection)
|
|
|
|
// Get current static post rkeys
|
|
const staticPostLinks = contentEl.querySelectorAll('.post-item a.post-link')
|
|
const staticRkeys = new Set<string>()
|
|
staticPostLinks.forEach(link => {
|
|
const href = link.getAttribute('href')
|
|
if (href) {
|
|
// Handle both /post/rkey and /post/rkey/ (trailing slash)
|
|
const parts = href.split('/').filter(Boolean)
|
|
const rkey = parts[parts.length - 1]
|
|
if (rkey) staticRkeys.add(rkey)
|
|
}
|
|
})
|
|
|
|
// Find new posts not in static content
|
|
const newPosts = apiPosts.filter(post => {
|
|
const rkey = post.uri.split('/').pop()
|
|
return rkey && !staticRkeys.has(rkey)
|
|
})
|
|
|
|
// Find deleted posts (in static but not in API)
|
|
const apiRkeys = new Set(apiPosts.map(p => p.uri.split('/').pop()))
|
|
const deletedRkeys = new Set<string>()
|
|
staticRkeys.forEach(rkey => {
|
|
if (!apiRkeys.has(rkey)) {
|
|
deletedRkeys.add(rkey)
|
|
}
|
|
})
|
|
|
|
// Remove deleted posts from DOM
|
|
if (deletedRkeys.size > 0) {
|
|
staticPostLinks.forEach(link => {
|
|
const href = link.getAttribute('href')
|
|
if (href) {
|
|
const parts = href.split('/').filter(Boolean)
|
|
const rkey = parts[parts.length - 1]
|
|
if (rkey && deletedRkeys.has(rkey)) {
|
|
const listItem = link.closest('.post-item')
|
|
listItem?.remove()
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// Add new posts at the top
|
|
if (newPosts.length > 0) {
|
|
const postList = contentEl.querySelector('.post-list')
|
|
if (postList) {
|
|
const newPostsHtml = newPosts.map(post => {
|
|
const rkey = post.uri.split('/').pop()
|
|
const date = new Date(post.createdAt).toLocaleDateString('ja-JP', {
|
|
year: 'numeric',
|
|
month: '2-digit',
|
|
day: '2-digit',
|
|
})
|
|
return `
|
|
<li class="post-item post-item-new">
|
|
<a href="/post/${rkey}" class="post-link">
|
|
<span class="post-title">${escapeHtml(post.title)}</span>
|
|
<span class="post-date">${date}</span>
|
|
</a>
|
|
</li>
|
|
`
|
|
}).join('')
|
|
postList.insertAdjacentHTML('afterbegin', newPostsHtml)
|
|
}
|
|
}
|
|
} catch (err) {
|
|
console.error('Failed to refresh posts from API:', err)
|
|
}
|
|
}
|
|
|
|
function setupEventHandlers(): void {
|
|
document.addEventListener('click', (e) => {
|
|
const target = e.target as HTMLElement
|
|
|
|
// Blog tab click - close browser and go back
|
|
if (target.id === 'blog-tab' || target.closest('#blog-tab')) {
|
|
if (browserMode) {
|
|
e.preventDefault()
|
|
closeBrowser()
|
|
return
|
|
}
|
|
}
|
|
|
|
// Browser tab button
|
|
if (target.id === 'browser-tab' || target.closest('#browser-tab')) {
|
|
e.preventDefault()
|
|
const btn = (target.closest('#browser-tab') || target) as HTMLElement
|
|
const handle = btn.dataset.handle || config.handle
|
|
openBrowser(handle)
|
|
return
|
|
}
|
|
|
|
// PDS tab button - toggle dropdown
|
|
if (target.id === 'pds-tab' || target.closest('#pds-tab')) {
|
|
e.preventDefault()
|
|
e.stopPropagation()
|
|
const dropdown = document.getElementById('pds-dropdown')
|
|
if (dropdown) {
|
|
dropdown.classList.toggle('show')
|
|
}
|
|
return
|
|
}
|
|
|
|
// PDS option selection
|
|
const pdsOption = target.closest('.pds-option') as HTMLElement
|
|
if (pdsOption) {
|
|
e.preventDefault()
|
|
const selectedNetwork = pdsOption.dataset.network
|
|
if (selectedNetwork && selectedNetwork !== browserNetwork) {
|
|
browserNetwork = selectedNetwork
|
|
localStorage.setItem('browserNetwork', selectedNetwork)
|
|
|
|
// Update network config for API
|
|
const networkConfig = networks[selectedNetwork]
|
|
if (networkConfig) {
|
|
setNetworkConfig(networkConfig)
|
|
}
|
|
|
|
// Update UI
|
|
updatePdsSelector()
|
|
|
|
// Reload browser if in browser mode
|
|
if (browserMode) {
|
|
loadBrowserContent()
|
|
}
|
|
}
|
|
// Close dropdown
|
|
const dropdown = document.getElementById('pds-dropdown')
|
|
if (dropdown) {
|
|
dropdown.classList.remove('show')
|
|
}
|
|
return
|
|
}
|
|
|
|
// Close PDS dropdown when clicking outside
|
|
if (!target.closest('#pds-selector')) {
|
|
const dropdown = document.getElementById('pds-dropdown')
|
|
if (dropdown) {
|
|
dropdown.classList.remove('show')
|
|
}
|
|
}
|
|
|
|
// JSON button click (on post detail page)
|
|
const jsonBtn = target.closest('.json-btn') as HTMLAnchorElement
|
|
if (jsonBtn) {
|
|
const href = jsonBtn.getAttribute('href')
|
|
if (href?.startsWith('/at/')) {
|
|
e.preventDefault()
|
|
const parts = href.split('/').filter(Boolean)
|
|
// /at/handle/collection/rkey
|
|
if (parts.length >= 4) {
|
|
openBrowser(parts[1], null, parts[2], parts[3])
|
|
}
|
|
return
|
|
}
|
|
}
|
|
|
|
// Service item click (on static page or in browser view)
|
|
if (target.closest('.service-item')) {
|
|
e.preventDefault()
|
|
const link = target.closest('.service-item') as HTMLAnchorElement
|
|
const href = link.getAttribute('href')
|
|
if (href) {
|
|
// Parse /at/handle/service from href
|
|
const parts = href.split('/').filter(Boolean)
|
|
if (parts[0] === 'at' && parts[1] && parts[2]) {
|
|
openBrowser(parts[1], parts[2])
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Links inside browser content
|
|
const contentEl = document.getElementById('content')
|
|
if (browserMode && contentEl?.contains(target)) {
|
|
const link = target.closest('a')
|
|
if (link) {
|
|
const href = link.getAttribute('href')
|
|
if (href?.startsWith('/at/')) {
|
|
e.preventDefault()
|
|
const parts = href.split('/').filter(Boolean)
|
|
// /at/handle, /at/handle/service, /at/handle/collection, /at/handle/collection/rkey
|
|
if (parts.length >= 2) {
|
|
const handle = parts[1]
|
|
let service = null
|
|
let collection = null
|
|
let rkey = null
|
|
|
|
if (parts.length === 3) {
|
|
// Could be service or collection
|
|
const segment = parts[2]
|
|
if (segment.split('.').length <= 2) {
|
|
service = segment
|
|
} else {
|
|
collection = segment
|
|
}
|
|
} else if (parts.length >= 4) {
|
|
collection = parts[2]
|
|
rkey = parts[3]
|
|
}
|
|
|
|
openBrowser(handle, service, collection, rkey)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
async function render(): Promise<void> {
|
|
const route = parseRoute(window.location.pathname)
|
|
const appEl = document.getElementById('app')
|
|
const isStatic = appEl?.dataset.static === 'true'
|
|
|
|
const profileEl = document.getElementById('profile')
|
|
const contentEl = document.getElementById('content')
|
|
const headerEl = document.getElementById('header')
|
|
const footerEl = document.getElementById('footer')
|
|
|
|
if (!profileEl || !contentEl || !headerEl) return
|
|
|
|
const isLoggedIn = !!authSession
|
|
const handle = route.handle || config.handle
|
|
|
|
// Skip re-rendering for static blog/post pages (but still mount header for login)
|
|
const isStaticRoute = route.type === 'blog' || route.type === 'post'
|
|
if (isStatic && isStaticRoute) {
|
|
// Only mount header for login functionality (pass isStatic=true to skip unnecessary re-render)
|
|
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
|
onBrowse: (newHandle) => {
|
|
openBrowser(newHandle)
|
|
},
|
|
onLogin: async () => {
|
|
const inputHandle = (document.getElementById('header-input') as HTMLInputElement)?.value || handle
|
|
try {
|
|
await login(inputHandle)
|
|
} catch (err) {
|
|
console.error('Login error:', err)
|
|
alert('Login failed: ' + err)
|
|
}
|
|
},
|
|
onLogout: async () => {
|
|
await logout()
|
|
window.location.reload()
|
|
}
|
|
}, true)
|
|
|
|
// Update tabs to show Post tab if logged in
|
|
if (isLoggedIn) {
|
|
const tabsEl = document.querySelector('.mode-tabs')
|
|
if (tabsEl && !tabsEl.querySelector('a[href="/post"]')) {
|
|
tabsEl.insertAdjacentHTML('beforeend', '<a href="/post" class="tab">Post</a>')
|
|
}
|
|
}
|
|
|
|
// For post pages, add edit button if logged in and can edit
|
|
if (route.type === 'post' && isLoggedIn && route.rkey) {
|
|
addEditButtonToStaticPost(config.collection, route.rkey, authSession!)
|
|
}
|
|
|
|
// For post pages, load discussion posts
|
|
if (route.type === 'post') {
|
|
const discussionContainer = document.getElementById('discussion-posts')
|
|
if (discussionContainer) {
|
|
const postUrl = discussionContainer.dataset.postUrl
|
|
if (postUrl) {
|
|
loadDiscussionPosts(discussionContainer.parentElement!, postUrl)
|
|
}
|
|
}
|
|
}
|
|
|
|
// For blog top page, check for new posts from API and merge
|
|
if (route.type === 'blog') {
|
|
refreshPostListFromAPI()
|
|
}
|
|
|
|
return // Skip content re-rendering
|
|
}
|
|
|
|
// Footer
|
|
if (footerEl) {
|
|
footerEl.innerHTML = renderFooter(config.handle)
|
|
}
|
|
|
|
// Header with login
|
|
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
|
onBrowse: (newHandle) => {
|
|
openBrowser(newHandle)
|
|
},
|
|
onLogin: async () => {
|
|
const inputHandle = (document.getElementById('header-input') as HTMLInputElement)?.value || handle
|
|
try {
|
|
await login(inputHandle)
|
|
} catch (err) {
|
|
console.error('Login error:', err)
|
|
alert('Login failed: ' + err)
|
|
}
|
|
},
|
|
onLogout: async () => {
|
|
await logout()
|
|
window.location.reload()
|
|
}
|
|
})
|
|
|
|
// Route handling
|
|
switch (route.type) {
|
|
case 'new':
|
|
if (isLoggedIn) {
|
|
profileEl.innerHTML = renderTabs('new', isLoggedIn)
|
|
mountPostForm(contentEl, config.collection, () => {
|
|
window.location.href = '/'
|
|
})
|
|
} else {
|
|
window.location.href = '/'
|
|
}
|
|
break
|
|
|
|
case 'post':
|
|
try {
|
|
const profile = await getProfile(config.handle)
|
|
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
|
|
const profileContentEl = document.createElement('div')
|
|
profileEl.appendChild(profileContentEl)
|
|
mountProfile(profileContentEl, profile)
|
|
|
|
const servicesHtml = await renderServices(config.handle)
|
|
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
|
|
|
const post = await getRecord(profile.did, config.collection, route.rkey!)
|
|
if (post) {
|
|
const canEdit = isLoggedIn && authSession?.did === profile.did
|
|
mountPostDetail(contentEl, post, config.handle, config.collection, canEdit, config.siteUrl, config.network)
|
|
} else {
|
|
contentEl.innerHTML = '<p>Post not found</p>'
|
|
}
|
|
} catch (err) {
|
|
console.error(err)
|
|
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
|
}
|
|
break
|
|
|
|
case 'blog':
|
|
default:
|
|
try {
|
|
const profile = await getProfile(config.handle)
|
|
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
|
|
const profileContentEl = document.createElement('div')
|
|
profileEl.appendChild(profileContentEl)
|
|
mountProfile(profileContentEl, profile)
|
|
|
|
const servicesHtml = await renderServices(config.handle)
|
|
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
|
|
|
const posts = await listRecords(profile.did, config.collection)
|
|
mountPostList(contentEl, posts)
|
|
} catch (err) {
|
|
console.error(err)
|
|
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
async function init(): Promise<void> {
|
|
const [configData, networksData] = await Promise.all([loadConfig(), loadNetworks()])
|
|
config = configData
|
|
networks = networksData
|
|
|
|
// Set page title
|
|
document.title = config.title || 'ailog'
|
|
|
|
// Set theme color
|
|
if (config.color) {
|
|
document.documentElement.style.setProperty('--btn-color', config.color)
|
|
}
|
|
|
|
// Initialize browser network from localStorage or default to config.network
|
|
browserNetwork = localStorage.getItem('browserNetwork') || config.network
|
|
|
|
// Set network config for blog (uses config.network)
|
|
const blogNetworkConfig = networks[config.network]
|
|
if (blogNetworkConfig) {
|
|
setNetworkConfig(blogNetworkConfig)
|
|
setAuthNetworkConfig(blogNetworkConfig)
|
|
}
|
|
|
|
// Handle OAuth callback
|
|
const callbackSession = await handleOAuthCallback()
|
|
if (callbackSession) {
|
|
authSession = callbackSession
|
|
} else {
|
|
authSession = await restoreSession()
|
|
}
|
|
|
|
// Setup event handlers
|
|
setupEventHandlers()
|
|
|
|
// Initial render
|
|
await render()
|
|
|
|
// Handle browser navigation
|
|
window.addEventListener('popstate', () => render())
|
|
}
|
|
|
|
init()
|