refact update

This commit is contained in:
2026-01-16 14:10:18 +09:00
parent 2ec33ef4ed
commit 40815a3b60
10 changed files with 60 additions and 90 deletions

View File

@@ -86,16 +86,3 @@ export function mountHeader(
})
}
}
// Keep old function for compatibility
export function mountBrowser(
container: HTMLElement,
currentHandle: string,
onSubmit: (handle: string) => void
): void {
mountHeader(container, currentHandle, false, undefined, {
onBrowse: onSubmit,
onLogin: () => {},
onLogout: () => {}
})
}

View File

@@ -1,5 +1,6 @@
import { searchPostsForUrl } from '../lib/api.js'
import { escapeHtml } from '../lib/utils.js'
import { escapeHtml, formatDate } from '../lib/utils.js'
import { MAX_SEARCH_LENGTH, DISCUSSION_POST_LIMIT } from '../lib/constants.js'
// Map network to app URL
export function getAppUrl(network: string): string {
@@ -9,15 +10,6 @@ export function getAppUrl(network: string): string {
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('/')
@@ -29,8 +21,6 @@ function getPostUrl(uri: string, appUrl: string): string {
export function renderDiscussionLink(postUrl: string, appUrl: string = 'https://bsky.app'): string {
// Convert full URL to search-friendly format (domain/post/rkey_prefix without https://)
// Keep total length around 20 chars to avoid URL truncation in posts
const MAX_SEARCH_LENGTH = 20
let searchQuery = postUrl
try {
const urlObj = new URL(postUrl)
@@ -74,7 +64,7 @@ export async function loadDiscussionPosts(container: HTMLElement, postUrl: strin
return
}
const postsHtml = posts.slice(0, 10).map(post => {
const postsHtml = posts.slice(0, DISCUSSION_POST_LIMIT).map(post => {
const author = post.author
const avatar = author.avatar || ''
const displayName = author.displayName || author.handle

View File

@@ -1,18 +1,9 @@
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 { escapeHtml, formatDate } from '../lib/utils.js'
import { renderDiscussionLink, loadDiscussionPosts, getAppUrl } from './discussion.js'
function formatDate(dateStr: string): string {
const date = new Date(dateStr)
return date.toLocaleDateString('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}
export function mountPostList(container: HTMLElement, posts: BlogPost[]): void {
if (posts.length === 0) {
container.innerHTML = '<p class="no-posts">No posts yet</p>'

View File

@@ -1,5 +1,6 @@
import { AtpAgent } from '@atproto/api'
import type { Profile, BlogPost, NetworkConfig } from '../types.js'
import { FALLBACK_PLCS, FALLBACK_BSKY_ENDPOINTS, SEARCH_TIMEOUT_MS } from './constants.js'
const agents: Map<string, AtpAgent> = new Map()
@@ -24,18 +25,6 @@ export function getAgent(service: string): AtpAgent {
return agents.get(service)!
}
// Fallback PLC directories
const FALLBACK_PLCS = [
'https://plc.directory',
'https://plc.syu.is',
]
// Fallback endpoints for handle/profile resolution
const FALLBACK_ENDPOINTS = [
'https://public.api.bsky.app',
'https://bsky.syu.is',
]
export async function resolvePds(did: string): Promise<string> {
// Try current PLC first, then fallbacks
const plcs = [getPlc(), ...FALLBACK_PLCS.filter(p => p !== getPlc())]
@@ -64,7 +53,7 @@ export async function resolveHandle(handle: string): Promise<string> {
return res.data.did
} catch {
// Try fallback endpoints
for (const endpoint of FALLBACK_ENDPOINTS) {
for (const endpoint of FALLBACK_BSKY_ENDPOINTS) {
if (endpoint === getBsky()) continue // Skip if same as current
try {
const agent = getAgent(endpoint)
@@ -80,7 +69,7 @@ export async function resolveHandle(handle: string): Promise<string> {
export async function getProfile(actor: string): Promise<Profile> {
// Try current network first
const endpoints = [getBsky(), ...FALLBACK_ENDPOINTS.filter(e => e !== getBsky())]
const endpoints = [getBsky(), ...FALLBACK_BSKY_ENDPOINTS.filter(e => e !== getBsky())]
for (const endpoint of endpoints) {
try {
@@ -273,7 +262,7 @@ export async function searchPostsForUrl(url: string): Promise<any[]> {
searchQueries.map(async query => {
try {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), 5000) // 5s timeout
const timeoutId = setTimeout(() => controller.abort(), SEARCH_TIMEOUT_MS)
const res = await fetch(
`${endpoint}/xrpc/app.bsky.feed.searchPosts?q=${encodeURIComponent(query)}&limit=20`,

19
src/lib/constants.ts Normal file
View File

@@ -0,0 +1,19 @@
// API limits
export const API_RECORD_LIMIT = 100
export const POST_LIST_LIMIT = 50
export const DISCUSSION_POST_LIMIT = 10
// Search
export const MAX_SEARCH_LENGTH = 20
export const SEARCH_TIMEOUT_MS = 5000
// Fallback endpoints
export const FALLBACK_PLCS = [
'https://plc.directory',
'https://plc.syu.is',
]
export const FALLBACK_BSKY_ENDPOINTS = [
'https://public.api.bsky.app',
'https://bsky.syu.is',
]

17
src/lib/icons.ts Normal file
View File

@@ -0,0 +1,17 @@
// Shared icon definitions
export const LANG_ICON = `<svg viewBox="0 0 640 640" width="20" height="20" fill="currentColor"><path d="M192 64C209.7 64 224 78.3 224 96L224 128L352 128C369.7 128 384 142.3 384 160C384 177.7 369.7 192 352 192L342.4 192L334 215.1C317.6 260.3 292.9 301.6 261.8 337.1C276 345.9 290.8 353.7 306.2 360.6L356.6 383L418.8 243C423.9 231.4 435.4 224 448 224C460.6 224 472.1 231.4 477.2 243L605.2 531C612.4 547.2 605.1 566.1 589 573.2C572.9 580.3 553.9 573.1 546.8 557L526.8 512L369.3 512L349.3 557C342.1 573.2 323.2 580.4 307.1 573.2C291 566 283.7 547.1 290.9 531L330.7 441.5L280.3 419.1C257.3 408.9 235.3 396.7 214.5 382.7C193.2 399.9 169.9 414.9 145 427.4L110.3 444.6C94.5 452.5 75.3 446.1 67.4 430.3C59.5 414.5 65.9 395.3 81.7 387.4L116.2 370.1C132.5 361.9 148 352.4 162.6 341.8C148.8 329.1 135.8 315.4 123.7 300.9L113.6 288.7C102.3 275.1 104.1 254.9 117.7 243.6C131.3 232.3 151.5 234.1 162.8 247.7L173 259.9C184.5 273.8 197.1 286.7 210.4 298.6C237.9 268.2 259.6 232.5 273.9 193.2L274.4 192L64.1 192C46.3 192 32 177.7 32 160C32 142.3 46.3 128 64 128L160 128L160 96C160 78.3 174.3 64 192 64zM448 334.8L397.7 448L498.3 448L448 334.8z"/></svg>`
export const DISCUSS_ICON = `<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C6.477 2 2 6.477 2 12c0 1.89.525 3.66 1.438 5.168L2.546 20.2A1.5 1.5 0 0 0 4 22h.5l2.83-.892A9.96 9.96 0 0 0 12 22c5.523 0 10-4.477 10-10S17.523 2 12 2z"/></svg>`
export const LOGIN_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M15 3h4a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2h-4"></path><polyline points="10 17 15 12 10 7"></polyline><line x1="15" y1="12" x2="3" y2="12"></line></svg>`
export const LOGOUT_ICON = `<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path><polyline points="16 17 21 12 16 7"></polyline><line x1="21" y1="12" x2="9" y2="12"></line></svg>`
// Footer link icons
export const BUILTIN_ICONS: Record<string, string> = {
bluesky: `<svg viewBox="0 0 600 530" fill="currentColor"><path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.72 40.255-67.24 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></svg>`,
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
ai: `<span class="icon-ai"></span>`,
git: `<span class="icon-git"></span>`,
}

View File

@@ -1,5 +1,6 @@
import { marked, Renderer } from 'marked'
import hljs from 'highlight.js/lib/core'
import { escapeHtml } from './utils.js'
// Import only common languages
import javascript from 'highlight.js/lib/languages/javascript'
@@ -53,13 +54,6 @@ renderer.code = function({ text, lang }: { text: string; lang?: string }) {
return `<pre><code class="hljs">${highlighted}</code></pre>`
}
function escapeHtml(str: string): string {
return str
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
}
marked.setOptions({
breaks: true,
gfm: true,

View File

@@ -5,3 +5,12 @@ export function escapeHtml(str: string): string {
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
}
export function formatDate(dateStr: string): string {
const date = new Date(dateStr)
return date.toLocaleDateString('ja-JP', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
})
}

View File

@@ -9,6 +9,7 @@ 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 { LANG_ICON, BUILTIN_ICONS } from './lib/icons.js'
import type { AppConfig, Networks } from './types.js'
let authSession: AuthSession | null = null
@@ -56,12 +57,6 @@ async function loadLinks(): Promise<FooterLink[]> {
}
}
const BUILTIN_ICONS: Record<string, string> = {
bluesky: `<svg viewBox="0 0 600 530" fill="currentColor"><path d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.72 40.255-67.24 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></svg>`,
github: `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/></svg>`,
ai: `<span class="icon-ai"></span>`,
git: `<span class="icon-git"></span>`,
}
function renderFooterLinks(links: FooterLink[]): string {
if (links.length === 0) return ''
@@ -146,12 +141,10 @@ function renderLangSelector(): string {
</div>`
}).join('')
const langIcon = `<svg viewBox="0 0 640 640" width="20" height="20" fill="currentColor"><path d="M192 64C209.7 64 224 78.3 224 96L224 128L352 128C369.7 128 384 142.3 384 160C384 177.7 369.7 192 352 192L342.4 192L334 215.1C317.6 260.3 292.9 301.6 261.8 337.1C276 345.9 290.8 353.7 306.2 360.6L356.6 383L418.8 243C423.9 231.4 435.4 224 448 224C460.6 224 472.1 231.4 477.2 243L605.2 531C612.4 547.2 605.1 566.1 589 573.2C572.9 580.3 553.9 573.1 546.8 557L526.8 512L369.3 512L349.3 557C342.1 573.2 323.2 580.4 307.1 573.2C291 566 283.7 547.1 290.9 531L330.7 441.5L280.3 419.1C257.3 408.9 235.3 396.7 214.5 382.7C193.2 399.9 169.9 414.9 145 427.4L110.3 444.6C94.5 452.5 75.3 446.1 67.4 430.3C59.5 414.5 65.9 395.3 81.7 387.4L116.2 370.1C132.5 361.9 148 352.4 162.6 341.8C148.8 329.1 135.8 315.4 123.7 300.9L113.6 288.7C102.3 275.1 104.1 254.9 117.7 243.6C131.3 232.3 151.5 234.1 162.8 247.7L173 259.9C184.5 273.8 197.1 286.7 210.4 298.6C237.9 268.2 259.6 232.5 273.9 193.2L274.4 192L64.1 192C46.3 192 32 177.7 32 160C32 142.3 46.3 128 64 128L160 128L160 96C160 78.3 174.3 64 192 64zM448 334.8L397.7 448L498.3 448L448 334.8z"/></svg>`
return `
<div class="lang-selector" id="lang-selector">
<button type="button" class="lang-btn" id="lang-btn" title="Language">
${langIcon}
${LANG_ICON}
</button>
<div class="lang-dropdown" id="lang-dropdown">
${options}