ai/at
1
0
This commit is contained in:
2026-01-21 23:23:20 +09:00
parent 5288f33029
commit 182442e354
33 changed files with 631 additions and 634 deletions

View File

@@ -0,0 +1,3 @@
export { layout, appLayout, legalLayout } from './layout.js'
export { oauthCallback, clientMetadata } from './oauth.js'
export { baseStyles } from './styles.js'

105
web/src/templates/layout.ts Normal file
View File

@@ -0,0 +1,105 @@
import type { SiteConfig } from '../types.js'
import { baseStyles } from './styles.js'
export function layout(site: SiteConfig, title: string, content: string): string {
return `<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>${title} - ${site.name}</title>
<link rel="icon" type="image/png" href="/static/favicon.png">
<style>${baseStyles}</style>
</head>
<body>
<div class="header">
<a href="/" class="back-link">&larr; Back to ${site.backLink}</a>
</div>
${content}
<div class="footer">
<p class="copyright">&copy; syui</p>
</div>
</body>
</html>`
}
export function appLayout(site: SiteConfig, content: string): string {
return layout(site, 'App Info', `
<div class="app-header">
<img src="${site.icon}" alt="${site.name}" class="app-icon">
<div class="app-name">${site.name}</div>
</div>
<div class="section">
<p class="description">${site.description}</p>
</div>
<div class="section">
<div class="section-title">App Information</div>
<div class="info-grid">
<div class="info-item">
<div class="info-label">Category</div>
<div class="info-value">${site.category}</div>
</div>
<div class="info-item">
<div class="info-label">Supported OS</div>
<div class="info-value">${site.os}</div>
</div>
<div class="info-item">
<div class="info-label">Price</div>
<div class="info-value">${site.price}</div>
</div>
</div>
</div>
<div class="section">
<div class="section-title">Developer</div>
<div class="developer-name">syui</div>
<div class="link-row">
<span class="link-icon">Git</span>
<a href="https://git.syui.ai/syui" class="link-value" target="_blank">git.syui.ai/syui</a>
<span class="link-arrow">&rarr;</span>
</div>
<div class="link-row">
<span class="link-icon">ATProto</span>
<a href="https://syui.ai" class="link-value" target="_blank">syui.ai</a>
<span class="link-arrow">&rarr;</span>
</div>
</div>
<div class="section">
<div class="section-title">Bitcoin</div>
<div class="bitcoin-row">
<span class="bitcoin-label">&#8383;</span>
<span class="bitcoin-address" id="btc-address">3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr</span>
<span class="copy-btn" onclick="copyBTC()">copy</span>
</div>
</div>
${content}
<script>
function copyBTC() {
const addr = document.getElementById('btc-address').textContent;
navigator.clipboard.writeText(addr).then(() => {
const btn = document.querySelector('.copy-btn');
btn.textContent = 'copied!';
btn.style.color = '#4CAF50';
setTimeout(() => {
btn.textContent = 'copy';
btn.style.color = '';
}, 2000);
});
}
</script>
`)
}
export function legalLayout(site: SiteConfig, title: string, content: string): string {
return layout(site, title, `
<div class="content">
<h1>${title}</h1>
${content}
</div>
`)
}

View File

@@ -0,0 +1,46 @@
import type { SiteConfig } from '../types.js'
export function oauthCallback(site: SiteConfig): string {
return `<!DOCTYPE html>
<html>
<head>
<title>Redirecting...</title>
<script>
const params = window.location.search;
window.location.href = '${site.scheme}://oauth/callback' + params;
setTimeout(function () {
window.location.replace('/?oauth_callback=true' + params.replace('?', '&'));
}, 500);
</script>
</head>
<body>
<p>Redirecting to app...</p>
<p>If nothing happens, <a href="#"
onclick="window.location.replace('/?oauth_callback=true' + window.location.search.replace('?', '&')); return false;">click
here</a> to continue in browser.</p>
</body>
</html>`
}
export function clientMetadata(site: SiteConfig): string {
return JSON.stringify({
client_id: `https://${site.domain}/.well-known/client-metadata.json`,
client_name: site.name,
client_uri: `https://${site.domain}`,
logo_uri: `https://${site.domain}/static/icon.png`,
redirect_uris: [
`https://${site.domain}/oauth/callback`
],
scope: 'atproto transition:generic',
grant_types: [
'authorization_code',
'refresh_token'
],
response_types: [
'code'
],
token_endpoint_auth_method: 'none',
application_type: 'web',
dpop_bound_access_tokens: true
}, null, 2)
}

View File

@@ -0,0 +1,51 @@
export const baseStyles = `
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
line-height: 1.6;
color: #1a1a1a;
background: #fff;
padding: 20px;
max-width: 800px;
margin: 0 auto;
}
@media (prefers-color-scheme: dark) {
body { background: #000; color: #e0e0e0; }
a { color: #6bb3ff; }
h1, h2, h3 { color: #fff; }
.section { background: #1a1a1a; }
.info-item { background: #2a2a2a; }
}
.header { margin-bottom: 32px; }
.back-link { display: inline-block; margin-bottom: 16px; font-size: 14px; color: #0066cc; text-decoration: none; }
.back-link:hover { text-decoration: underline; }
.app-header { text-align: center; margin-bottom: 32px; }
.app-icon { width: 80px; height: 80px; border-radius: 18px; margin-bottom: 12px; }
.app-name { font-size: 24px; font-weight: bold; margin-bottom: 4px; }
.app-version { font-size: 14px; color: #666; }
.section { background: #f5f5f5; border-radius: 16px; padding: 20px; margin-bottom: 16px; }
.section-title { font-size: 13px; font-weight: 600; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
.description { font-size: 15px; line-height: 22px; }
.info-grid { display: flex; flex-wrap: wrap; gap: 8px; }
.info-item { flex: 1; min-width: 45%; text-align: center; background: #e8e8e8; border-radius: 12px; padding: 12px; }
.info-label { font-size: 11px; color: #999; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 4px; }
.info-value { font-size: 16px; font-weight: 600; }
.developer-name { font-size: 18px; font-weight: 600; margin-bottom: 12px; }
.link-row { display: flex; align-items: center; padding: 12px 0; border-top: 1px solid rgba(0,0,0,0.1); }
.link-icon { font-size: 14px; font-weight: 600; color: #666; width: 70px; }
.link-value { flex: 1; font-size: 14px; color: #0084ff; text-decoration: none; }
.link-value:hover { text-decoration: underline; }
.link-arrow { font-size: 16px; color: #ccc; }
.bitcoin-row { display: flex; align-items: center; background: rgba(247, 147, 26, 0.08); border-radius: 12px; padding: 14px; gap: 10px; }
.bitcoin-label { font-size: 18px; font-weight: 600; color: #f7931a; }
.bitcoin-address { flex: 1; font-size: 11px; font-family: monospace; color: #666; word-break: break-all; }
.copy-btn { font-size: 12px; color: #999; cursor: pointer; min-width: 50px; text-align: right; }
.copy-btn:hover { color: #0084ff; }
.footer { text-align: center; margin-top: 32px; padding-top: 20px; border-top: 1px solid #ddd; }
.copyright { font-size: 12px; color: #999; }
.content h1 { font-size: 24px; margin-bottom: 16px; }
.content h2 { font-size: 18px; margin: 24px 0 12px; }
.content p { margin-bottom: 12px; }
.content ul { margin: 12px 0; padding-left: 24px; }
.content li { margin-bottom: 8px; }
`