This commit is contained in:
2026-01-16 01:42:31 +09:00
parent 4731d64b4d
commit 1728f8dd35
13 changed files with 358 additions and 25 deletions

View File

@@ -1,18 +1,23 @@
import type { Profile } from '../types.js'
import { escapeHtml } from '../lib/utils.js'
export function renderProfile(profile: Profile): string {
export function renderProfile(profile: Profile, webUrl?: string): string {
const profileLink = webUrl ? `${webUrl}/profile/${profile.did}` : null
const handleHtml = profileLink
? `<a href="${escapeHtml(profileLink)}" class="profile-handle-link" target="_blank" rel="noopener">@${escapeHtml(profile.handle)}</a>`
: `@${escapeHtml(profile.handle)}`
return `
<div class="profile">
${profile.avatar ? `<img src="${profile.avatar}" alt="avatar" class="profile-avatar">` : ''}
${profile.avatar ? `<img src="${escapeHtml(profile.avatar)}" alt="avatar" class="profile-avatar">` : ''}
<div class="profile-info">
<h1 class="profile-name">${profile.displayName || profile.handle}</h1>
<p class="profile-handle">@${profile.handle}</p>
${profile.description ? `<p class="profile-desc">${profile.description}</p>` : ''}
<h1 class="profile-name">${escapeHtml(profile.displayName || profile.handle)}</h1>
<p class="profile-handle">${handleHtml}</p>
${profile.description ? `<p class="profile-desc">${escapeHtml(profile.description)}</p>` : ''}
</div>
</div>
`
}
export function mountProfile(container: HTMLElement, profile: Profile): void {
container.innerHTML = renderProfile(profile)
export function mountProfile(container: HTMLElement, profile: Profile, webUrl?: string): void {
container.innerHTML = renderProfile(profile, webUrl)
}

View File

@@ -36,10 +36,62 @@ async function loadNetworks(): Promise<Networks> {
return res.json()
}
interface FooterLink {
name: string
url: string
icon?: string
}
let footerLinks: FooterLink[] = []
async function loadLinks(): Promise<FooterLink[]> {
try {
const res = await fetch('/links.json')
if (!res.ok) return []
const data = await res.json()
return data.links || []
} catch {
return []
}
}
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 ''
const items = links.map(link => {
let iconHtml = ''
if (link.icon && BUILTIN_ICONS[link.icon]) {
iconHtml = BUILTIN_ICONS[link.icon]
} else {
try {
const domain = new URL(link.url).hostname
iconHtml = `<img src="https://www.google.com/s2/favicons?domain=${domain}&sz=32" alt="" class="footer-link-favicon">`
} catch {
iconHtml = ''
}
}
return `
<a href="${escapeHtml(link.url)}" class="footer-link-item" title="${escapeHtml(link.name)}" target="_blank" rel="noopener">
${iconHtml}
</a>
`
}).join('')
return `<div class="footer-links">${items}</div>`
}
function renderFooter(handle: string): string {
const parts = handle.split('.')
const username = parts[0] || handle
return `
${renderFooterLinks(footerLinks)}
<footer class="site-footer">
<p>&copy; ${username}</p>
</footer>
@@ -598,10 +650,11 @@ async function render(): Promise<void> {
case 'post':
try {
const profile = await getProfile(config.handle)
const webUrl = networks[config.network]?.web
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
const profileContentEl = document.createElement('div')
profileEl.appendChild(profileContentEl)
mountProfile(profileContentEl, profile)
mountProfile(profileContentEl, profile, webUrl)
const servicesHtml = await renderServices(config.handle)
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
@@ -623,10 +676,11 @@ async function render(): Promise<void> {
default:
try {
const profile = await getProfile(config.handle)
const webUrl = networks[config.network]?.web
profileEl.innerHTML = renderTabs('blog', isLoggedIn)
const profileContentEl = document.createElement('div')
profileEl.appendChild(profileContentEl)
mountProfile(profileContentEl, profile)
mountProfile(profileContentEl, profile, webUrl)
const servicesHtml = await renderServices(config.handle)
profileContentEl.insertAdjacentHTML('beforeend', servicesHtml)
@@ -642,9 +696,10 @@ async function render(): Promise<void> {
}
async function init(): Promise<void> {
const [configData, networksData] = await Promise.all([loadConfig(), loadNetworks()])
const [configData, networksData, linksData] = await Promise.all([loadConfig(), loadNetworks(), loadLinks()])
config = configData
networks = networksData
footerLinks = linksData
// Set page title
document.title = config.title || 'ailog'

View File

@@ -217,6 +217,16 @@ body {
margin-bottom: 8px;
}
.profile-handle-link {
color: #666;
text-decoration: none;
}
.profile-handle-link:hover {
color: var(--btn-color);
text-decoration: underline;
}
.profile-desc {
font-size: 14px;
color: #444;
@@ -796,9 +806,56 @@ body {
}
}
/* Footer Links */
.footer-links {
display: flex;
justify-content: center;
gap: 20px;
margin-top: 40px;
padding: 20px 0;
}
.footer-link-item {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
color: #666;
text-decoration: none;
transition: color 0.2s;
}
.footer-link-item:hover {
color: var(--btn-color);
}
.footer-link-item svg {
width: 24px;
height: 24px;
}
.footer-link-item [class^="icon-"] {
font-size: 24px;
}
.footer-link-favicon {
width: 24px;
height: 24px;
}
@media (prefers-color-scheme: dark) {
.footer-link-item {
color: #888;
}
.footer-link-item:hover {
color: var(--btn-color);
}
}
/* Footer */
.site-footer {
margin-top: 60px;
margin-top: 20px;
padding: 20px 0;
text-align: center;
font-size: 13px;

View File

@@ -18,6 +18,7 @@ export interface BlogPost {
export interface NetworkConfig {
plc: string
bsky: string
web?: string
}
export interface AppConfig {