Files
log/src/web/components/posts.ts
2026-02-19 08:18:02 +09:00

158 lines
5.5 KiB
TypeScript

import type { Post } from '../types'
import { renderMarkdown } from '../lib/markdown'
import { renderDiscussion, loadDiscussionPosts } from './discussion'
import { getCurrentLang } from './mode-tabs'
// Extract text content from post content (handles both string and object formats)
function getContentText(content: unknown): string {
if (!content) return ''
if (typeof content === 'string') return content
if (typeof content === 'object' && content !== null) {
const obj = content as Record<string, unknown>
if (typeof obj.text === 'string') return obj.text
}
return ''
}
// Format date as yyyy/mm/dd
function formatDate(dateStr: string): string {
const d = new Date(dateStr)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${year}/${month}/${day}`
}
// Render post list
export function renderPostList(posts: Post[], handle: string): string {
if (posts.length === 0) {
return '<p class="no-posts">No posts yet.</p>'
}
const currentLang = getCurrentLang()
const items = posts.map(post => {
const rkey = post.uri.split('/').pop() || ''
const date = formatDate(post.value.publishedAt)
const originalLang = post.value.langs?.[0] || 'ja'
const translations = post.value.translations
// Use translation if available
let displayTitle = post.value.title
if (translations && currentLang !== originalLang && translations[currentLang]) {
displayTitle = translations[currentLang].title || post.value.title
}
return `
<article class="post-item">
<a href="/@${handle}/${rkey}" class="post-link">
<h2 class="post-title">${escapeHtml(displayTitle)}</h2>
<time class="post-date">${date}</time>
</a>
</article>
`
}).join('')
return `<div class="post-list">${items}</div>`
}
// Render single post detail
export function renderPostDetail(
post: Post,
handle: string,
collection: string,
isOwner: boolean = false,
siteUrl?: string,
appUrl: string = 'https://bsky.app',
canMerge: boolean = false
): string {
const rkey = post.uri.split('/').pop() || ''
const date = formatDate(post.value.publishedAt)
const jsonUrl = `/@${handle}/at/collection/${collection}/${rkey}`
// Build post URL for discussion search
const postUrl = siteUrl ? `${siteUrl}/@${handle}/${rkey}` : `${window.location.origin}/@${handle}/${rkey}`
const rawContent = getContentText(post.value.content)
const editBtn = isOwner ? `<button type="button" class="post-edit-btn" id="post-edit-btn">Edit</button>` : ''
const mergeBtn = canMerge ? `<button type="button" class="post-merge-btn" id="post-merge-btn" data-collection="${collection}" data-rkey="${rkey}" data-handle="${escapeHtml(handle)}" data-title="${escapeHtml(post.value.title || '')}" data-content="${escapeHtml(rawContent)}">Merge</button>` : ''
// Get current language and show appropriate content
const currentLang = getCurrentLang()
const translations = post.value.translations
const originalLang = post.value.langs?.[0] || 'ja'
let displayTitle = post.value.title || ''
let displayContent = rawContent
// Use translation if available and not original language
if (translations && currentLang !== originalLang && translations[currentLang]) {
const trans = translations[currentLang]
displayTitle = trans.title || post.value.title || ''
displayContent = trans.content || rawContent
}
const content = displayContent ? renderMarkdown(displayContent) : ''
const editForm = isOwner ? `
<div class="post-edit-form" id="post-edit-form" style="display: none;">
<input type="text" class="post-edit-title" id="post-edit-title" value="${escapeHtml(post.value.title || '')}" placeholder="Title">
<textarea class="post-edit-content" id="post-edit-content" rows="15">${escapeHtml(rawContent)}</textarea>
<div class="post-edit-actions">
<button type="button" class="post-edit-cancel" id="post-edit-cancel">Cancel</button>
<button type="button" class="post-edit-save" id="post-edit-save" data-collection="${collection}" data-rkey="${rkey}">Save</button>
</div>
</div>
` : ''
return `
<article class="post-detail" data-post-url="${escapeHtml(postUrl)}" data-app-url="${escapeHtml(appUrl)}">
<header class="post-header">
<div class="post-meta">
<time class="post-date">${date}</time>
<a href="${jsonUrl}" class="json-btn">json</a>
${editBtn}
${mergeBtn}
</div>
</header>
${editForm}
<div id="post-display">
<h1 class="post-title">${escapeHtml(displayTitle)}</h1>
<div class="post-content">${content}</div>
</div>
</article>
${renderDiscussion(postUrl, appUrl)}
`
}
// Setup post detail interactions (discussion loading)
export function setupPostDetail(container: HTMLElement): void {
const article = container.querySelector('.post-detail') as HTMLElement
if (!article) return
// Load discussion posts
const postUrl = article.dataset.postUrl
const appUrl = article.dataset.appUrl || 'https://bsky.app'
if (postUrl) {
loadDiscussionPosts(container, postUrl, appUrl)
}
}
export function mountPostList(container: HTMLElement, html: string): void {
container.innerHTML = html
}
export function mountPostDetail(container: HTMLElement, html: string): void {
container.innerHTML = html
}
function escapeHtml(text: string): string {
return text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;')
}