From 6056db12e4f766c52f2deb85f26c033222863215 Mon Sep 17 00:00:00 2001 From: syui Date: Thu, 15 Jan 2026 23:26:34 +0900 Subject: [PATCH] add comment --- .gitignore | 1 - .../ai.syui.log.post/3mchqlshygs2s.json | 7 + .../collections.json | 11 + .../favicons/bsky.app.png | Bin 0 -> 2240 bytes .../favicons/syui.ai.png | Bin 0 -> 23243 bytes .../profile.json | 21 ++ public/config.json | 7 +- scripts/generate.ts | 91 +++----- src/components/atbrowser.ts | 46 ++-- src/components/discussion.ts | 87 ++++++++ src/components/posts.ts | 19 +- src/lib/api.ts | 16 +- src/lib/utils.ts | 7 + src/main.ts | 145 ++++++++++-- src/styles/main.css | 208 ++++++++++++++++++ src/types.ts | 1 + 16 files changed, 557 insertions(+), 110 deletions(-) create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/collections.json create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/bsky.app.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syui.ai.png create mode 100644 content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json create mode 100644 src/components/discussion.ts create mode 100644 src/lib/utils.ts diff --git a/.gitignore b/.gitignore index e87a534..0062c02 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,3 @@ dist node_modules package-lock.json repos -content/ diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json new file mode 100644 index 0000000..2454a1b --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s.json @@ -0,0 +1,7 @@ +{ + "uri": "at://did:plc:vzsvtbtbnwn22xjqhcu3vd6y/ai.syui.log.post/3mchqlshygs2s", + "cid": "bafyreiaww3o6uoayzosgkymc7cazaja6ebyw4ahtk3dpvqq5xd3m6juyz4", + "title": "ailogを作り直した", + "content": "## ailogとは\n\natprotoと連携するサイトジェネレータ。\n\n## ailogの使い方\n\n```sh\n$ git clone https://git.syui.ai/ai/log\n$ cd log\n$ cat public/config.json\n{\n \"title\": \"syui.syui.ai\",\n \"handle\": \"syui.syui.ai\",\n \"collection\": \"ai.syui.log.post\",\n \"network\": \"syu.is\",\n \"color\": \"#0066cc\"\n}\n---\n$ npm run dev\n```\n\n## ailogのコンセプト\n\n1. at-browserを基本にする\n2. atproto oauthでログインする\n3. 記事をポストする\n\n## ailogの追加機能\n\n1. atproto recordからjsonをdownloadすると表示速度が上がる\n2. コメントはurlの言及を検索して表示", + "createdAt": "2026-01-15T13:59:52.367Z" +} \ No newline at end of file diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/collections.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/collections.json new file mode 100644 index 0000000..ce1adc2 --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/collections.json @@ -0,0 +1,11 @@ +[ + "ai.syui.log", + "ai.syui.log.chat", + "ai.syui.log.post", + "ai.syui.rse.user", + "app.bsky.actor.profile", + "app.bsky.feed.post", + "app.bsky.feed.repost", + "app.bsky.graph.follow", + "chat.bsky.actor.declaration" +] \ No newline at end of file diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/bsky.app.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/bsky.app.png new file mode 100644 index 0000000000000000000000000000000000000000..a5ca7eed1e24b9554e417be0b81b5d0cf34bcdd5 GIT binary patch literal 2240 zcmaJ?3piA17(N*HW;MDfl4&Xlb2p7SjZutSx#iZ7Y(obVW|)~WDbY%)R4UX;#t^Bn zNEdA>o7*GFZq(C7Ek%SbWTLk0IYUvaJ@fqk|DFGP-}n3e|9sywd)-|f&|3Oh005w! z9N8Z5ibaNo8a#_)V&1`vlE}ls1}JWt*8{)QLL8@H7Z<=3wlx4XC0#%TfxtgNNgq&E z*Z|<8WboAvP%;_ED8m>s0MEcIcm(g^0jP2Oc6db|M|i+WW2-w)0)N3ID}6tgdz7Xk4_$oZ&noGBes5;;F2PI62l!gmk4 zz?Ha>E9SDfVu*#5Kb}m%Q^;O$`P1ki*#b|p1d*%#5B579TOkB1OrfG^qRA?gkW*<0 zgkAzcIBTk|9?L?(A4iP~Fo{S_Ppa{;22$+89mLXtnP0k*r3IZn;sf{YeJA!xFOFvC z+I68`A=9%)=msl~8rRWGw_I zSRB))-mBdB(76BNDx(#B8}hY-3`QEf%$@n17I(G-+8OFF_YJ#WbeA|xQ0kx01*VV{M z>=?BT4CbXJ$<;KyQkM$n+^B+LXyszFknZG`^{#hRIC*xJ36Iyk*Tm#*wa@Kp4;HAJ zV$7Y^GX^iVrJ=w}{rtAFMa;nH)msU318Zw?KVnF0X0@Z6Qnt@od_#`WjGKSKE$69c zW4E5X_h8cj9Y%Ah-IdtN0LUxlQ{|KT{VM->rp#D@$+bkcbBAb_VTUSq z-jwHtHD)<{V*1=6rmrZ*Uh~pg8MUK!m9d)ZlZ7A5B-iPG z3^CGs41d1INR6K}D4Onk)OT3j{Dq@FahJgdYQdf+z%aFtY8sai-*p6heD;>ekJ{o* z6D~|ELdn8RR=hUOQhFJr_X#pA=&`o(t&BVVqNCgRUd9e@sg&aX?z3A$RhrR5Wm>A* z2#y+;H?T;hwIzHg>dpHR;a}#Bu|Wyj8T;yV-EB&x(K)TNbDn1q0`S*g_14TYOvo%p zyG31R5rzgaQlrdXVnl literal 0 HcmV?d00001 diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syui.ai.png b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/favicons/syui.ai.png new file mode 100644 index 0000000000000000000000000000000000000000..fae8d5aaa6eda78f356e804c90db97bef6eb5bb6 GIT binary patch literal 23243 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 literal 0 HcmV?d00001 diff --git a/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json new file mode 100644 index 0000000..c7441a2 --- /dev/null +++ b/content/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/profile.json @@ -0,0 +1,21 @@ +{ + "did": "did:plc:vzsvtbtbnwn22xjqhcu3vd6y", + "handle": "syui.syui.ai", + "displayName": "syui", + "avatar": "https://bsky.syu.is/img/avatar/plain/did:plc:vzsvtbtbnwn22xjqhcu3vd6y/bafkreigta4pf5h7uvx6jpfcm3d6aeq4g3qpsiqjdoeytnutwp6vwc2yo7u@jpeg", + "associated": { + "lists": 0, + "feedgens": 0, + "starterPacks": 0, + "labeler": false, + "activitySubscription": { + "allowSubscriptions": "followers" + } + }, + "labels": [], + "createdAt": "2025-09-19T06:17:42.000Z", + "indexedAt": "2025-09-19T06:17:42.000Z", + "followersCount": 1, + "followsCount": 1, + "postsCount": 74 +} \ No newline at end of file diff --git a/public/config.json b/public/config.json index 353dab9..946bd70 100644 --- a/public/config.json +++ b/public/config.json @@ -1,7 +1,8 @@ { "title": "syui.ai", - "handle": "syui.ai", + "handle": "syui.syui.ai", "collection": "ai.syui.log.post", - "network": "bsky.social", - "color": "#0066cc" + "network": "syu.is", + "color": "#0066cc", + "siteUrl": "https://syui.ai" } diff --git a/scripts/generate.ts b/scripts/generate.ts index 05330c4..c84918c 100644 --- a/scripts/generate.ts +++ b/scripts/generate.ts @@ -1,38 +1,8 @@ import * as fs from 'fs' import * as path from 'path' import { marked, Renderer } from 'marked' - -// Types -interface AppConfig { - title: string - handle: string - collection: string - network: string - color?: string -} - -interface Networks { - [key: string]: { - plc: string - bsky: string - } -} - -interface Profile { - did: string - handle: string - displayName?: string - description?: string - avatar?: string -} - -interface BlogPost { - uri: string - cid: string - title: string - content: string - createdAt: string -} +import type { AppConfig, Profile, BlogPost, Networks } from '../src/types.ts' +import { escapeHtml } from '../src/lib/utils.ts' // Highlight.js for syntax highlighting (core + common languages only) let hljs: typeof import('highlight.js/lib/core').default @@ -100,14 +70,6 @@ function setupMarked() { }) } -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - function formatDate(dateStr: string): string { const date = new Date(dateStr) return date.toLocaleDateString('ja-JP', { @@ -445,10 +407,23 @@ function generatePostListHtml(posts: BlogPost[]): string { return `
    ${items}
` } -function generatePostDetailHtml(post: BlogPost, handle: string, collection: string): string { +// Map network to app URL for discussion links +function getAppUrl(network: string): string { + if (network === 'syu.is') { + return 'https://syu.is' + } + return 'https://bsky.app' +} + +function generatePostDetailHtml(post: BlogPost, handle: string, collection: string, network: string, siteUrl?: string): string { const rkey = post.uri.split('/').pop() || '' const jsonUrl = `/at/${handle}/${collection}/${rkey}/` const content = marked.parse(post.content) as string + // Use siteUrl from config, or construct from handle + const baseSiteUrl = siteUrl || `https://${handle}` + const postUrl = `${baseSiteUrl}/post/${rkey}/` + const appUrl = getAppUrl(network) + const searchUrl = `${appUrl}/search?q=${encodeURIComponent(postUrl)}` return `
@@ -461,6 +436,15 @@ function generatePostDetailHtml(post: BlogPost, handle: string, collection: stri
${content}
+ ` } @@ -500,7 +484,7 @@ function generatePostPageContent(profile: Profile, post: BlogPost, config: AppCo ${generateServicesHtml(profile.did, config.handle, collections)}
- ${generatePostDetailHtml(post, config.handle, config.collection)} + ${generatePostDetailHtml(post, config.handle, config.collection, config.network, config.siteUrl)}
${generateFooterHtml(config.handle)} @@ -595,27 +579,20 @@ async function generate() { const localPosts = localDid ? loadPostsFromFiles(localDid, config.collection) : [] console.log(`Found ${localPosts.length} posts from local`) - // Merge: API is the source of truth for what exists - // - If post exists in API and local: use local (may have edits) - // - If post exists in API only: use API - // - If post exists in local only: skip (was deleted from API) + // Merge: API is the source of truth + // - If post exists in API: always use API (has latest edits) + // - If post exists in local only: keep if not deleted (for posts beyond API limit) const apiRkeys = new Set(apiPosts.map(p => p.uri.split('/').pop())) - const localRkeys = new Set(localPosts.map(p => p.uri.split('/').pop())) - // Local posts that still exist in API - const validLocalPosts = localPosts.filter(p => apiRkeys.has(p.uri.split('/').pop())) - // API posts that don't exist locally - const newApiPosts = apiPosts.filter(p => !localRkeys.has(p.uri.split('/').pop())) + // Local posts that don't exist in API (older posts beyond 100 limit) + // Note: these might be deleted posts, so we keep them cautiously + const oldLocalPosts = localPosts.filter(p => !apiRkeys.has(p.uri.split('/').pop())) - posts = [...validLocalPosts, ...newApiPosts].sort((a, b) => + posts = [...apiPosts, ...oldLocalPosts].sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() ) - const deletedCount = localPosts.length - validLocalPosts.length - if (deletedCount > 0) { - console.log(`Skipped ${deletedCount} deleted posts (exist locally but not in API)`) - } - console.log(`Total ${posts.length} posts (${validLocalPosts.length} local + ${newApiPosts.length} new from API)`) + console.log(`Total ${posts.length} posts (${apiPosts.length} from API + ${oldLocalPosts.length} old local)`) // Create output directory const distDir = path.join(process.cwd(), 'dist') diff --git a/src/components/atbrowser.ts b/src/components/atbrowser.ts index 4176093..7d4fa8d 100644 --- a/src/components/atbrowser.ts +++ b/src/components/atbrowser.ts @@ -1,33 +1,38 @@ -import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo } from '../lib/api.js' +import { describeRepo, listRecordsRaw, getRecordRaw, fetchLexicon, resolveHandle, getServiceInfo, resolvePds, getPlc } from '../lib/api.js' import { deleteRecord } from '../lib/auth.js' +import { escapeHtml } from '../lib/utils.js' function extractRkey(uri: string): string { const parts = uri.split('/') return parts[parts.length - 1] } -function formatDate(dateStr: string): string { - const date = new Date(dateStr) - return date.toLocaleDateString('ja-JP', { - year: 'numeric', - month: '2-digit', - day: '2-digit', - }) -} - -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - async function renderServices(did: string, handle: string): Promise { - const collections = await describeRepo(did) + const [collections, pds] = await Promise.all([ + describeRepo(did), + resolvePds(did) + ]) + + // Server info section + const plcUrl = `${getPlc()}/${did}/log` + const serverHtml = ` +
+

Server

+
+
+
DID
+
${escapeHtml(did)}
+
+
+
PDS
+
${escapeHtml(pds)}
+
+
+
+ ` if (collections.length === 0) { - return '

No collections found

' + return serverHtml + '

No collections found

' } // Group by service domain @@ -57,6 +62,7 @@ async function renderServices(did: string, handle: string): Promise { }).join('') return ` + ${serverHtml}

Services

    ${items}
diff --git a/src/components/discussion.ts b/src/components/discussion.ts new file mode 100644 index 0000000..a94179e --- /dev/null +++ b/src/components/discussion.ts @@ -0,0 +1,87 @@ +import { searchPostsForUrl } from '../lib/api.js' +import { escapeHtml } from '../lib/utils.js' + +// Map network to app URL +export function getAppUrl(network: string): string { + if (network === 'syu.is') { + return 'https://syu.is' + } + return 'https://bsky.app' +} + +function formatDate(dateStr: string): string { + const date = new Date(dateStr) + return date.toLocaleDateString('ja-JP', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + }) +} + +function getPostUrl(uri: string, appUrl: string): string { + // at://did:plc:xxx/app.bsky.feed.post/rkey -> {appUrl}/profile/did:plc:xxx/post/rkey + const parts = uri.replace('at://', '').split('/') + if (parts.length >= 3) { + return `${appUrl}/profile/${parts[0]}/post/${parts[2]}` + } + return '#' +} + +export function renderDiscussionLink(postUrl: string, appUrl: string = 'https://bsky.app'): string { + const searchUrl = `${appUrl}/search?q=${encodeURIComponent(postUrl)}` + return ` + + ` +} + +export async function loadDiscussionPosts(container: HTMLElement, postUrl: string, appUrl: string = 'https://bsky.app'): Promise { + const postsContainer = container.querySelector('#discussion-posts') as HTMLElement + if (!postsContainer) return + + // Get appUrl from data attribute if available + const dataAppUrl = postsContainer.dataset.appUrl + const effectiveAppUrl = dataAppUrl || appUrl + + postsContainer.innerHTML = '
Loading...
' + + const posts = await searchPostsForUrl(postUrl) + + if (posts.length === 0) { + postsContainer.innerHTML = '' + return + } + + const postsHtml = posts.slice(0, 10).map(post => { + const author = post.author + const avatar = author.avatar || '' + const displayName = author.displayName || author.handle + const handle = author.handle + const text = post.record?.text || '' + const createdAt = post.record?.createdAt || '' + const postLink = getPostUrl(post.uri, effectiveAppUrl) + + return ` + +
+ ${avatar ? `` : ''} +
+ ${escapeHtml(displayName)} + @${escapeHtml(handle)} +
+ ${formatDate(createdAt)} +
+
${escapeHtml(text)}
+
+ ` + }).join('') + + postsContainer.innerHTML = postsHtml +} diff --git a/src/components/posts.ts b/src/components/posts.ts index 0ec8aa2..360103e 100644 --- a/src/components/posts.ts +++ b/src/components/posts.ts @@ -1,6 +1,8 @@ import type { BlogPost } from '../types.js' import { putRecord } from '../lib/auth.js' import { renderMarkdown } from '../lib/markdown.js' +import { escapeHtml } from '../lib/utils.js' +import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js' function formatDate(dateStr: string): string { const date = new Date(dateStr) @@ -11,14 +13,6 @@ function formatDate(dateStr: string): string { }) } -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - export function mountPostList(container: HTMLElement, posts: BlogPost[]): void { if (posts.length === 0) { container.innerHTML = '

No posts yet

' @@ -40,9 +34,11 @@ export function mountPostList(container: HTMLElement, posts: BlogPost[]): void { container.innerHTML = `
    ${html}
` } -export function mountPostDetail(container: HTMLElement, post: BlogPost, handle: string, collection: string, canEdit: boolean = false): void { +export function mountPostDetail(container: HTMLElement, post: BlogPost, handle: string, collection: string, canEdit: boolean = false, siteUrl?: string, network: string = 'bsky.social'): void { const rkey = post.uri.split('/').pop() || '' const jsonUrl = `/at/${handle}/${collection}/${rkey}` + const postUrl = siteUrl ? `${siteUrl}/post/${rkey}` : `${window.location.origin}/post/${rkey}` + const appUrl = getAppUrl(network) const editBtn = canEdit ? `` : '' @@ -59,6 +55,8 @@ export function mountPostDetail(container: HTMLElement, post: BlogPost, handle:
${renderMarkdown(post.content)}
+ ${renderDiscussionLink(postUrl, appUrl)} + ` + // Load discussion posts + loadDiscussionPosts(container, postUrl) + if (canEdit) { const editBtnEl = document.getElementById('edit-btn') const editFormContainer = document.getElementById('edit-form-container') diff --git a/src/lib/api.ts b/src/lib/api.ts index c59f2a3..d3b14b0 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -9,7 +9,7 @@ export function setNetworkConfig(config: NetworkConfig): void { networkConfig = config } -function getPlc(): string { +export function getPlc(): string { return networkConfig?.plc || 'https://plc.directory' } @@ -190,6 +190,20 @@ const SERVICE_MAP: Record = { 'pub.leaflet': { domain: 'leaflet.pub' }, } +// Search Bluesky posts mentioning a URL +export async function searchPostsForUrl(url: string): Promise { + try { + const res = await fetch( + `https://public.api.bsky.app/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(url)}&limit=20` + ) + if (!res.ok) return [] + const data = await res.json() + return data.posts || [] + } catch { + return [] + } +} + export function getServiceInfo(collection: string): { name: string; domain: string; favicon: string } | null { // Try to find matching service prefix for (const [prefix, info] of Object.entries(SERVICE_MAP)) { diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..cf2a61a --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,7 @@ +export function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') +} diff --git a/src/main.ts b/src/main.ts index 605dede..0beb68a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,11 +6,15 @@ 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 @@ -42,6 +46,39 @@ function renderFooter(handle: string): string { ` } +function renderPdsSelector(): string { + const networkKeys = Object.keys(networks) + const options = networkKeys.map(key => { + const isSelected = key === browserNetwork + return `
+ ${escapeHtml(key)} + +
` + }).join('') + + return ` +
+ +
+ ${options} +
+
+ ` +} + +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 = ` Blog @@ -52,6 +89,8 @@ function renderTabs(activeTab: 'blog' | 'browser' | 'new', isLoggedIn: boolean): tabs += `Post` } + tabs += renderPdsSelector() + return `
${tabs}
` } @@ -123,6 +162,12 @@ async function loadBrowserContent(): Promise { 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, @@ -217,14 +262,6 @@ async function addEditButtonToStaticPost(collection: string, rkey: string, sessi }) } -function escapeHtml(str: string): string { - return str - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') -} - // Refresh post list from API (for static pages) async function refreshPostListFromAPI(): Promise { const contentEl = document.getElementById('content') @@ -327,6 +364,56 @@ function setupEventHandlers(): void { 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) { @@ -432,12 +519,17 @@ async function render(): Promise { } }, 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"]')) { + // Update tabs + const tabsEl = document.querySelector('.mode-tabs') + if (tabsEl) { + // Add Post tab if logged in + if (isLoggedIn && !tabsEl.querySelector('a[href="/post"]')) { tabsEl.insertAdjacentHTML('beforeend', 'Post') } + // Add PDS selector if not present + if (!tabsEl.querySelector('#pds-selector')) { + tabsEl.insertAdjacentHTML('beforeend', renderPdsSelector()) + } } // For post pages, add edit button if logged in and can edit @@ -445,6 +537,17 @@ async function render(): Promise { 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() @@ -505,7 +608,7 @@ async function render(): Promise { 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) + mountPostDetail(contentEl, post, config.handle, config.collection, canEdit, config.siteUrl, config.network) } else { contentEl.innerHTML = '

Post not found

' } @@ -538,8 +641,9 @@ async function render(): Promise { } async function init(): Promise { - const [configData, networks] = await Promise.all([loadConfig(), loadNetworks()]) + const [configData, networksData] = await Promise.all([loadConfig(), loadNetworks()]) config = configData + networks = networksData // Set page title document.title = config.title || 'ailog' @@ -549,11 +653,14 @@ async function init(): Promise { document.documentElement.style.setProperty('--btn-color', config.color) } - // Set network config - const networkConfig = networks[config.network] - if (networkConfig) { - setNetworkConfig(networkConfig) - setAuthNetworkConfig(networkConfig) + // 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 diff --git a/src/styles/main.css b/src/styles/main.css index 822fc1a..ff11764 100644 --- a/src/styles/main.css +++ b/src/styles/main.css @@ -436,6 +436,110 @@ body { cursor: not-allowed; } +/* Discussion Section */ +.discussion-section { + margin-top: 48px; + padding-top: 24px; + border-top: 1px solid #eee; +} + +.discussion-section h3 { + font-size: 18px; + margin-bottom: 16px; +} + +.discuss-link { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + background: var(--btn-color); + color: #fff; + border-radius: 20px; + text-decoration: none; + font-size: 14px; + font-weight: 500; +} + +.discuss-link:hover { + background: var(--btn-color); + filter: brightness(0.85); +} + +.discuss-link svg { + width: 18px; + height: 18px; +} + +.discussion-posts { + margin-top: 20px; +} + +.loading-small { + color: #888; + font-size: 14px; +} + +.no-discussion { + color: #888; + font-size: 14px; +} + +.discussion-post { + display: block; + padding: 16px; + margin-bottom: 12px; + background: #f9f9f9; + border-radius: 8px; + text-decoration: none; + color: inherit; +} + +.discussion-post:hover { + background: #f0f0f0; +} + +.discussion-author { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.discussion-avatar { + width: 32px; + height: 32px; + border-radius: 50%; +} + +.discussion-author-info { + flex: 1; + display: flex; + flex-direction: column; +} + +.discussion-name { + font-weight: 600; + font-size: 14px; +} + +.discussion-handle { + font-size: 12px; + color: #888; +} + +.discussion-date { + font-size: 12px; + color: #888; +} + +.discussion-text { + font-size: 14px; + line-height: 1.5; + white-space: pre-wrap; + word-break: break-word; +} + .post-content { font-size: 16px; line-height: 1.8; @@ -729,7 +833,111 @@ body { color: #fff; } +/* PDS Selector */ +.pds-selector { + position: relative; + margin-left: auto; +} + +.pds-dropdown { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + min-width: 180px; + z-index: 100; + overflow: hidden; +} + +.pds-dropdown.show { + display: block; +} + +.pds-option { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + cursor: pointer; + font-size: 14px; + transition: background 0.15s; +} + +.pds-option:hover { + background: #f5f5f5; +} + +.pds-option.selected { + background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%); +} + +.pds-name { + color: #333; + font-weight: 500; +} + +.pds-check { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #ccc; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + transition: all 0.2s; +} + +.pds-option.selected .pds-check { + background: var(--btn-color); + border-color: var(--btn-color); + color: #fff; +} + +.pds-option:not(.selected) .pds-check { + color: transparent; +} + /* AT Browser */ +.server-info { + padding: 16px 0; + border-bottom: 1px solid #eee; + margin-bottom: 8px; +} + +.server-info h3 { + font-size: 18px; + margin-bottom: 12px; +} + +.server-details { + font-size: 13px; +} + +.server-row { + display: flex; + gap: 12px; + padding: 6px 0; +} + +.server-row dt { + font-weight: 600; + min-width: 40px; + color: #666; +} + +.server-row dd { + font-family: 'SF Mono', Monaco, monospace; + font-size: 12px; + word-break: break-all; + color: #333; +} + .services-list, .collections, .records, diff --git a/src/types.ts b/src/types.ts index 578ad7a..00d85fc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -26,6 +26,7 @@ export interface AppConfig { collection: string network: string color?: string + siteUrl?: string } export type Networks = Record