add @user
This commit is contained in:
@@ -803,6 +803,7 @@ async function generate() {
|
|||||||
// Generate _redirects for Cloudflare Pages (SPA routes)
|
// Generate _redirects for Cloudflare Pages (SPA routes)
|
||||||
const redirects = `/app / 301
|
const redirects = `/app / 301
|
||||||
/oauth/* /app.html 200
|
/oauth/* /app.html 200
|
||||||
|
/@* /app.html 200
|
||||||
`
|
`
|
||||||
fs.writeFileSync(path.join(distDir, '_redirects'), redirects)
|
fs.writeFileSync(path.join(distDir, '_redirects'), redirects)
|
||||||
console.log('Generated: /_redirects')
|
console.log('Generated: /_redirects')
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export function renderHeader(currentHandle: string, isLoggedIn: boolean, userHan
|
|||||||
</svg>
|
</svg>
|
||||||
</button>`
|
</button>`
|
||||||
|
|
||||||
|
// Use logged-in user's handle for input if available
|
||||||
|
const inputHandle = isLoggedIn && userHandle ? userHandle : currentHandle
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<form class="header-form" id="header-form">
|
<form class="header-form" id="header-form">
|
||||||
@@ -21,7 +24,7 @@ export function renderHeader(currentHandle: string, isLoggedIn: boolean, userHan
|
|||||||
class="header-input"
|
class="header-input"
|
||||||
id="header-input"
|
id="header-input"
|
||||||
placeholder="handle (e.g., syui.ai)"
|
placeholder="handle (e.g., syui.ai)"
|
||||||
value="${currentHandle}"
|
value="${inputHandle}"
|
||||||
>
|
>
|
||||||
<button type="submit" class="header-btn at-btn" title="Browse">@</button>
|
<button type="submit" class="header-btn at-btn" title="Browse">@</button>
|
||||||
${loginBtn}
|
${loginBtn}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { renderMarkdown } from '../lib/markdown.js'
|
|||||||
import { escapeHtml, formatDate } from '../lib/utils.js'
|
import { escapeHtml, formatDate } from '../lib/utils.js'
|
||||||
import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js'
|
import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js'
|
||||||
|
|
||||||
export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
export function mountPostList(container: HTMLElement, posts: BlogPost[], userHandle?: string): void {
|
||||||
if (posts.length === 0) {
|
if (posts.length === 0) {
|
||||||
container.innerHTML = '<p class="no-posts">No posts yet</p>'
|
container.innerHTML = '<p class="no-posts">No posts yet</p>'
|
||||||
return
|
return
|
||||||
@@ -12,9 +12,11 @@ export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
|
|||||||
|
|
||||||
const html = posts.map(post => {
|
const html = posts.map(post => {
|
||||||
const rkey = post.uri.split('/').pop()
|
const rkey = post.uri.split('/').pop()
|
||||||
|
// Use /@handle/post/rkey for user pages, /post/rkey for own blog
|
||||||
|
const postUrl = userHandle ? `/@${userHandle}/post/${rkey}` : `/post/${rkey}`
|
||||||
return `
|
return `
|
||||||
<li class="post-item">
|
<li class="post-item">
|
||||||
<a href="/post/${rkey}" class="post-link">
|
<a href="${postUrl}" class="post-link">
|
||||||
<span class="post-title">${escapeHtml(post.title)}</span>
|
<span class="post-title">${escapeHtml(post.title)}</span>
|
||||||
<span class="post-date">${formatDate(post.createdAt)}</span>
|
<span class="post-date">${formatDate(post.createdAt)}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
export interface Route {
|
export interface Route {
|
||||||
type: 'blog' | 'post' | 'browser-services' | 'browser-collections' | 'browser-record' | 'new'
|
type: 'blog' | 'post' | 'browser-services' | 'browser-collections' | 'browser-record' | 'new' | 'user-blog' | 'user-post'
|
||||||
handle?: string
|
handle?: string
|
||||||
collection?: string
|
collection?: string
|
||||||
rkey?: string
|
rkey?: string
|
||||||
@@ -33,6 +33,16 @@ export function parseRoute(pathname: string): Route {
|
|||||||
return { type: 'new' }
|
return { type: 'new' }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// /@${handle} - User blog (any user's posts)
|
||||||
|
// /@${handle}/post/${rkey} - User post detail
|
||||||
|
if (parts[0].startsWith('@')) {
|
||||||
|
const handle = parts[0].slice(1) // Remove @ prefix
|
||||||
|
if (parts[1] === 'post' && parts[2]) {
|
||||||
|
return { type: 'user-post', handle, rkey: parts[2] }
|
||||||
|
}
|
||||||
|
return { type: 'user-blog', handle }
|
||||||
|
}
|
||||||
|
|
||||||
// /at/${handle} - Browser services
|
// /at/${handle} - Browser services
|
||||||
// /at/${handle}/${service-or-collection} - Browser collections or records
|
// /at/${handle}/${service-or-collection} - Browser collections or records
|
||||||
// /at/${handle}/${collection}/${rkey} - Browser record detail
|
// /at/${handle}/${collection}/${rkey} - Browser record detail
|
||||||
@@ -75,6 +85,10 @@ export function buildPath(route: Route): string {
|
|||||||
return '/new'
|
return '/new'
|
||||||
case 'post':
|
case 'post':
|
||||||
return `/post/${route.rkey}`
|
return `/post/${route.rkey}`
|
||||||
|
case 'user-blog':
|
||||||
|
return `/@${route.handle}`
|
||||||
|
case 'user-post':
|
||||||
|
return `/@${route.handle}/post/${route.rkey}`
|
||||||
case 'browser-services':
|
case 'browser-services':
|
||||||
return `/at/${route.handle}`
|
return `/at/${route.handle}`
|
||||||
case 'browser-collections':
|
case 'browser-collections':
|
||||||
|
|||||||
123
src/main.ts
123
src/main.ts
@@ -94,6 +94,40 @@ function renderFooter(handle: string): string {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect network from handle domain
|
||||||
|
// e.g., syui.ai → bsky.social, syui.syui.ai → syu.is
|
||||||
|
function detectNetworkFromHandle(handle: string): string {
|
||||||
|
const parts = handle.split('.')
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
// Get domain part (last 2 parts for most cases)
|
||||||
|
const domain = parts.slice(-2).join('.')
|
||||||
|
// Check if domain matches any network key
|
||||||
|
if (networks[domain]) {
|
||||||
|
return domain
|
||||||
|
}
|
||||||
|
// Check if it's a subdomain of a known network
|
||||||
|
for (const networkKey of Object.keys(networks)) {
|
||||||
|
if (handle.endsWith(`.${networkKey}`) || handle.endsWith(networkKey)) {
|
||||||
|
return networkKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Default to bsky.social
|
||||||
|
return 'bsky.social'
|
||||||
|
}
|
||||||
|
|
||||||
|
function switchNetwork(newNetwork: string): void {
|
||||||
|
if (newNetwork === browserNetwork) return
|
||||||
|
browserNetwork = newNetwork
|
||||||
|
localStorage.setItem('browserNetwork', newNetwork)
|
||||||
|
const networkConfig = networks[newNetwork]
|
||||||
|
if (networkConfig) {
|
||||||
|
setNetworkConfig(networkConfig)
|
||||||
|
setAuthNetworkConfig(networkConfig)
|
||||||
|
}
|
||||||
|
updatePdsSelector()
|
||||||
|
}
|
||||||
|
|
||||||
function renderPdsSelector(): string {
|
function renderPdsSelector(): string {
|
||||||
const networkKeys = Object.keys(networks)
|
const networkKeys = Object.keys(networks)
|
||||||
const options = networkKeys.map(key => {
|
const options = networkKeys.map(key => {
|
||||||
@@ -220,10 +254,11 @@ function applyTitleTranslations(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean): string {
|
function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean, handle?: string): string {
|
||||||
|
const browserHandle = handle || config.handle
|
||||||
let tabs = `
|
let tabs = `
|
||||||
<a href="/" class="tab ${activeTab === 'blog' ? 'active' : ''}" id="blog-tab">Blog</a>
|
<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>
|
<button type="button" class="tab ${activeTab === 'browser' ? 'active' : ''}" id="browser-tab" data-handle="${browserHandle}">Browser</button>
|
||||||
`
|
`
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
@@ -242,6 +277,10 @@ function openBrowser(handle: string, service: string | null = null, collection:
|
|||||||
|
|
||||||
if (!contentEl || !tabsEl) return
|
if (!contentEl || !tabsEl) return
|
||||||
|
|
||||||
|
// Auto-detect and switch network based on handle
|
||||||
|
const detectedNetwork = detectNetworkFromHandle(handle)
|
||||||
|
switchNetwork(detectedNetwork)
|
||||||
|
|
||||||
// Save current content if not already in browser mode
|
// Save current content if not already in browser mode
|
||||||
if (!browserMode) {
|
if (!browserMode) {
|
||||||
savedContent = {
|
savedContent = {
|
||||||
@@ -678,8 +717,10 @@ async function render(): Promise<void> {
|
|||||||
const handle = route.handle || config.handle
|
const handle = route.handle || config.handle
|
||||||
|
|
||||||
// Skip re-rendering for static blog/post pages (but still mount header for login)
|
// Skip re-rendering for static blog/post pages (but still mount header for login)
|
||||||
|
// Exception: if logged in on blog page, re-render to show user's blog
|
||||||
const isStaticRoute = route.type === 'blog' || route.type === 'post'
|
const isStaticRoute = route.type === 'blog' || route.type === 'post'
|
||||||
if (isStatic && isStaticRoute) {
|
const shouldUseStatic = isStatic && isStaticRoute && !(isLoggedIn && route.type === 'blog')
|
||||||
|
if (shouldUseStatic) {
|
||||||
// Only mount header for login functionality (pass isStatic=true to skip unnecessary re-render)
|
// Only mount header for login functionality (pass isStatic=true to skip unnecessary re-render)
|
||||||
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
mountHeader(headerEl, handle, isLoggedIn, authSession?.handle, {
|
||||||
onBrowse: (newHandle) => {
|
onBrowse: (newHandle) => {
|
||||||
@@ -826,21 +867,83 @@ async function render(): Promise<void> {
|
|||||||
}
|
}
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'blog':
|
case 'user-blog':
|
||||||
default:
|
// /@{handle} - Any user's blog
|
||||||
try {
|
try {
|
||||||
const profile = await getProfile(config.handle)
|
const userHandle = route.handle!
|
||||||
const webUrl = networks[config.network]?.web
|
// Auto-detect and switch network based on handle
|
||||||
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
|
const detectedNetwork = detectNetworkFromHandle(userHandle)
|
||||||
|
switchNetwork(detectedNetwork)
|
||||||
|
const profile = await getProfile(userHandle)
|
||||||
|
const webUrl = networks[browserNetwork]?.web
|
||||||
|
profileEl.innerHTML = renderTabs('blog', isLoggedIn, userHandle)
|
||||||
const profileContentEl = document.createElement('div')
|
const profileContentEl = document.createElement('div')
|
||||||
profileEl.appendChild(profileContentEl)
|
profileEl.appendChild(profileContentEl)
|
||||||
mountProfile(profileContentEl, profile, webUrl)
|
mountProfile(profileContentEl, profile, webUrl)
|
||||||
|
|
||||||
const servicesHtml = await renderServices(config.handle)
|
const servicesHtml = await renderServices(userHandle)
|
||||||
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||||
|
|
||||||
const posts = await listRecords(profile.did, config.collection)
|
const posts = await listRecords(profile.did, config.collection)
|
||||||
mountPostList(contentEl, posts)
|
mountPostList(contentEl, posts, userHandle)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||||
|
}
|
||||||
|
break
|
||||||
|
|
||||||
|
case 'user-post':
|
||||||
|
// /@{handle}/post/{rkey} - Any user's post detail
|
||||||
|
try {
|
||||||
|
const userHandle = route.handle!
|
||||||
|
// Auto-detect and switch network based on handle
|
||||||
|
const detectedNetwork = detectNetworkFromHandle(userHandle)
|
||||||
|
switchNetwork(detectedNetwork)
|
||||||
|
const profile = await getProfile(userHandle)
|
||||||
|
const webUrl = networks[browserNetwork]?.web
|
||||||
|
profileEl.innerHTML = renderTabs('blog', isLoggedIn, userHandle)
|
||||||
|
const profileContentEl = document.createElement('div')
|
||||||
|
profileEl.appendChild(profileContentEl)
|
||||||
|
mountProfile(profileContentEl, profile, webUrl)
|
||||||
|
|
||||||
|
const servicesHtml = await renderServices(userHandle)
|
||||||
|
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, userHandle, config.collection, canEdit, undefined, browserNetwork)
|
||||||
|
} 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 {
|
||||||
|
// If logged in, show logged-in user's blog instead of site owner's
|
||||||
|
const blogHandle = isLoggedIn ? authSession!.handle : config.handle
|
||||||
|
const detectedNetwork = isLoggedIn ? detectNetworkFromHandle(blogHandle) : config.network
|
||||||
|
if (isLoggedIn) {
|
||||||
|
switchNetwork(detectedNetwork)
|
||||||
|
}
|
||||||
|
const profile = await getProfile(blogHandle)
|
||||||
|
const webUrl = networks[detectedNetwork]?.web
|
||||||
|
profileEl.innerHTML = renderTabs('blog', isLoggedIn, blogHandle)
|
||||||
|
const profileContentEl = document.createElement('div')
|
||||||
|
profileEl.appendChild(profileContentEl)
|
||||||
|
mountProfile(profileContentEl, profile, webUrl)
|
||||||
|
|
||||||
|
const servicesHtml = await renderServices(blogHandle)
|
||||||
|
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
|
||||||
|
|
||||||
|
const posts = await listRecords(profile.did, config.collection)
|
||||||
|
// Use handle for post links if logged in user
|
||||||
|
mountPostList(contentEl, posts, isLoggedIn ? blogHandle : undefined)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err)
|
console.error(err)
|
||||||
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
contentEl.innerHTML = `<p class="error">Failed to load: ${err}</p>`
|
||||||
|
|||||||
Reference in New Issue
Block a user