add app
3
.gitignore
vendored
@@ -2,9 +2,11 @@ public
|
|||||||
build
|
build
|
||||||
resources
|
resources
|
||||||
*.lock
|
*.lock
|
||||||
|
.claude
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
|
package-lock.json
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
@@ -37,3 +39,4 @@ content/*/*_private.pem
|
|||||||
content/*/*.json
|
content/*/*.json
|
||||||
|
|
||||||
static/*lock*
|
static/*lock*
|
||||||
|
app/dist
|
||||||
|
|||||||
28
app/package.json
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "app-pages",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsx src/build.ts",
|
||||||
|
"watch": "tsx watch src/build.ts",
|
||||||
|
"serve": "npx serve dist",
|
||||||
|
"serve:aiat": "npx serve dist/aiat -p 3000",
|
||||||
|
"serve:aicard": "npx serve dist/aicard -p 3001",
|
||||||
|
"serve:airse": "npx serve dist/airse -p 3002",
|
||||||
|
"dev": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aiat\"",
|
||||||
|
"dev:aiat": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aiat\"",
|
||||||
|
"dev:aicard": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:aicard\"",
|
||||||
|
"dev:airse": "npm run build && npx concurrently \"npm run watch\" \"npm run serve:airse\""
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.0.0",
|
||||||
|
"concurrently": "^9.0.0",
|
||||||
|
"serve": "^14.0.0",
|
||||||
|
"tsx": "^4.0.0",
|
||||||
|
"typescript": "^5.0.0"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"marked": "^15.0.0",
|
||||||
|
"gray-matter": "^4.0.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
app/src/assets/aiat/app.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/assets/aiat/icon.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
app/src/assets/aicard/app.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
app/src/assets/aicard/icon.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
app/src/assets/airse/app.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
app/src/assets/airse/icon.png
Normal file
|
After Width: | Height: | Size: 1.6 MiB |
BIN
app/src/assets/common/favicon.png
Normal file
|
After Width: | Height: | Size: 1005 B |
124
app/src/build.ts
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
import { readFileSync, writeFileSync, mkdirSync, readdirSync, copyFileSync, existsSync } from 'fs'
|
||||||
|
import { join, dirname } from 'path'
|
||||||
|
import { fileURLToPath } from 'url'
|
||||||
|
import { marked } from 'marked'
|
||||||
|
import matter from 'gray-matter'
|
||||||
|
import type { SiteConfig, PageMeta } from './types.js'
|
||||||
|
import { appLayout, legalLayout, oauthCallback, clientMetadata } from './templates/index.js'
|
||||||
|
|
||||||
|
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||||
|
const sitesDir = join(__dirname, 'sites')
|
||||||
|
const contentDir = join(__dirname, 'content/pages')
|
||||||
|
const assetsDir = join(__dirname, 'assets')
|
||||||
|
const distDir = join(__dirname, '..', 'dist')
|
||||||
|
|
||||||
|
// Load all site configs
|
||||||
|
function loadSites(): SiteConfig[] {
|
||||||
|
const files = readdirSync(sitesDir).filter(f => f.endsWith('.json'))
|
||||||
|
return files.map(f => {
|
||||||
|
const content = readFileSync(join(sitesDir, f), 'utf-8')
|
||||||
|
return JSON.parse(content) as SiteConfig
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load all markdown pages
|
||||||
|
function loadPages(): Array<{ meta: PageMeta & { path: string }, content: string }> {
|
||||||
|
const files = readdirSync(contentDir).filter(f => f.endsWith('.md'))
|
||||||
|
return files.map(f => {
|
||||||
|
const raw = readFileSync(join(contentDir, f), 'utf-8')
|
||||||
|
const { data, content } = matter(raw)
|
||||||
|
return {
|
||||||
|
meta: data as PageMeta & { path: string },
|
||||||
|
content: marked(content) as string
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure directory exists
|
||||||
|
function ensureDir(filePath: string): void {
|
||||||
|
mkdirSync(dirname(filePath), { recursive: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy assets for a site
|
||||||
|
function copyAssets(site: SiteConfig): void {
|
||||||
|
const siteAssetsDir = join(assetsDir, site.id)
|
||||||
|
const commonAssetsDir = join(assetsDir, 'common')
|
||||||
|
const outDir = join(distDir, site.id, 'static')
|
||||||
|
|
||||||
|
ensureDir(join(outDir, '_'))
|
||||||
|
|
||||||
|
// Copy common assets first
|
||||||
|
if (existsSync(commonAssetsDir)) {
|
||||||
|
const files = readdirSync(commonAssetsDir)
|
||||||
|
for (const file of files) {
|
||||||
|
copyFileSync(join(commonAssetsDir, file), join(outDir, file))
|
||||||
|
console.log(` -> static/${file} (common)`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy site-specific assets (overrides common)
|
||||||
|
if (existsSync(siteAssetsDir)) {
|
||||||
|
const files = readdirSync(siteAssetsDir)
|
||||||
|
for (const file of files) {
|
||||||
|
copyFileSync(join(siteAssetsDir, file), join(outDir, file))
|
||||||
|
console.log(` -> static/${file}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build site
|
||||||
|
function buildSite(site: SiteConfig, pages: ReturnType<typeof loadPages>): void {
|
||||||
|
const siteDir = join(distDir, site.id)
|
||||||
|
|
||||||
|
console.log(`Building ${site.name} (${site.domain})...`)
|
||||||
|
|
||||||
|
// Build pages from markdown
|
||||||
|
for (const page of pages) {
|
||||||
|
const outPath = join(siteDir, page.meta.path)
|
||||||
|
ensureDir(outPath)
|
||||||
|
|
||||||
|
let html: string
|
||||||
|
if (page.meta.template === 'app') {
|
||||||
|
html = appLayout(site, page.content)
|
||||||
|
} else {
|
||||||
|
html = legalLayout(site, page.meta.title, page.content)
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(outPath, html)
|
||||||
|
console.log(` -> ${page.meta.path}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build OAuth callback
|
||||||
|
const callbackPath = join(siteDir, 'oauth/callback/index.html')
|
||||||
|
ensureDir(callbackPath)
|
||||||
|
writeFileSync(callbackPath, oauthCallback(site))
|
||||||
|
console.log(` -> oauth/callback/index.html`)
|
||||||
|
|
||||||
|
// Build client metadata
|
||||||
|
const metadataPath = join(siteDir, '.well-known/client-metadata.json')
|
||||||
|
ensureDir(metadataPath)
|
||||||
|
writeFileSync(metadataPath, clientMetadata(site))
|
||||||
|
console.log(` -> .well-known/client-metadata.json`)
|
||||||
|
|
||||||
|
// Copy assets
|
||||||
|
copyAssets(site)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Main
|
||||||
|
function main(): void {
|
||||||
|
console.log('App Pages Builder\n')
|
||||||
|
|
||||||
|
const sites = loadSites()
|
||||||
|
const pages = loadPages()
|
||||||
|
|
||||||
|
console.log(`Found ${sites.length} sites, ${pages.length} pages\n`)
|
||||||
|
|
||||||
|
for (const site of sites) {
|
||||||
|
buildSite(site, pages)
|
||||||
|
console.log('')
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Done!')
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
5
app/src/content/pages/app.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: App Info
|
||||||
|
template: app
|
||||||
|
path: about/support/app.html
|
||||||
|
---
|
||||||
39
app/src/content/pages/help.md
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
title: Help
|
||||||
|
template: legal
|
||||||
|
path: about/support/help.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
1. Download the app from the App Store
|
||||||
|
2. Sign in with your AT Protocol handle
|
||||||
|
3. Start connecting with your community
|
||||||
|
|
||||||
|
## Authentication
|
||||||
|
|
||||||
|
This app uses AT Protocol OAuth for secure authentication. Your credentials are never stored on our servers.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Feed**: View posts from people you follow
|
||||||
|
- **Profile**: Customize your profile and view your posts
|
||||||
|
- **Search**: Find users and content
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Login Issues
|
||||||
|
|
||||||
|
- Ensure your handle is correct
|
||||||
|
- Check your internet connection
|
||||||
|
- Try logging out and back in
|
||||||
|
|
||||||
|
### App Crashes
|
||||||
|
|
||||||
|
- Update to the latest version
|
||||||
|
- Restart the app
|
||||||
|
- If issues persist, contact support
|
||||||
|
|
||||||
|
## Contact Support
|
||||||
|
|
||||||
|
For additional help, please visit our Git repository or contact us through AT Protocol.
|
||||||
5
app/src/content/pages/index.md
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
title: App Info
|
||||||
|
template: app
|
||||||
|
path: index.html
|
||||||
|
---
|
||||||
29
app/src/content/pages/license.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
---
|
||||||
|
title: License
|
||||||
|
template: legal
|
||||||
|
path: about/support/license.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Software License
|
||||||
|
|
||||||
|
This application is open source software.
|
||||||
|
|
||||||
|
## Third-Party Libraries
|
||||||
|
|
||||||
|
This app uses the following open source libraries:
|
||||||
|
|
||||||
|
- **AT Protocol** - MIT License
|
||||||
|
- **React Native** - MIT License
|
||||||
|
- **Expo** - MIT License
|
||||||
|
|
||||||
|
## Assets
|
||||||
|
|
||||||
|
App icons and graphics are proprietary unless otherwise noted.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
Built with AT Protocol by syui.
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
For licensing questions, please contact us through our Git repository.
|
||||||
35
app/src/content/pages/privacy.md
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
title: Privacy Policy
|
||||||
|
template: legal
|
||||||
|
path: about/support/privacy.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Information We Collect
|
||||||
|
|
||||||
|
This application collects minimal information necessary for its operation:
|
||||||
|
|
||||||
|
- **Account Information**: Your AT Protocol handle and DID for authentication
|
||||||
|
- **Usage Data**: Basic app usage statistics to improve the service
|
||||||
|
|
||||||
|
## How We Use Your Information
|
||||||
|
|
||||||
|
- To provide and maintain the service
|
||||||
|
- To authenticate your identity
|
||||||
|
- To improve user experience
|
||||||
|
|
||||||
|
## Data Storage
|
||||||
|
|
||||||
|
Your data is stored securely on AT Protocol compatible servers. We do not sell or share your personal information with third parties.
|
||||||
|
|
||||||
|
## Your Rights
|
||||||
|
|
||||||
|
You have the right to:
|
||||||
|
- Access your personal data
|
||||||
|
- Delete your account and associated data
|
||||||
|
- Export your data
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you have questions about this privacy policy, please contact us through our Git repository.
|
||||||
|
|
||||||
|
*Last updated: 2025-01-15*
|
||||||
40
app/src/content/pages/tos.md
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
title: Terms of Service
|
||||||
|
template: legal
|
||||||
|
path: about/support/tos.html
|
||||||
|
---
|
||||||
|
|
||||||
|
## Acceptance of Terms
|
||||||
|
|
||||||
|
By using this application, you agree to these Terms of Service.
|
||||||
|
|
||||||
|
## Use of Service
|
||||||
|
|
||||||
|
You agree to use this service in accordance with:
|
||||||
|
- Applicable laws and regulations
|
||||||
|
- AT Protocol community guidelines
|
||||||
|
- Respectful behavior towards other users
|
||||||
|
|
||||||
|
## User Content
|
||||||
|
|
||||||
|
You retain ownership of content you create. By posting content, you grant us a license to display it within the service.
|
||||||
|
|
||||||
|
## Prohibited Activities
|
||||||
|
|
||||||
|
- Harassment or abuse of other users
|
||||||
|
- Spam or automated abuse
|
||||||
|
- Attempting to compromise the service
|
||||||
|
|
||||||
|
## Termination
|
||||||
|
|
||||||
|
We reserve the right to terminate accounts that violate these terms.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
This service is provided "as is" without warranties of any kind.
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
We may update these terms from time to time. Continued use constitutes acceptance of changes.
|
||||||
|
|
||||||
|
*Last updated: 2025-01-15*
|
||||||
13
app/src/sites/aiat.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "aiat",
|
||||||
|
"name": "Aiat",
|
||||||
|
"domain": "at.syui.ai",
|
||||||
|
"scheme": "aiat",
|
||||||
|
"version": "1.111.0",
|
||||||
|
"category": "Social",
|
||||||
|
"description": "Aiat is a social networking application based on AT Protocol. Connect with your community on syu.is.",
|
||||||
|
"backLink": "syu.is",
|
||||||
|
"icon": "/static/app.png",
|
||||||
|
"os": "iOS 26.0+",
|
||||||
|
"price": "Free"
|
||||||
|
}
|
||||||
13
app/src/sites/aicard.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "aicard",
|
||||||
|
"name": "Aicard",
|
||||||
|
"domain": "card.syui.ai",
|
||||||
|
"scheme": "aicard",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "Game",
|
||||||
|
"description": "Aicard is a card collection game integrated with AT Protocol.",
|
||||||
|
"backLink": "syui.ai",
|
||||||
|
"icon": "/static/app.png",
|
||||||
|
"os": "iOS 26.0+",
|
||||||
|
"price": "Free"
|
||||||
|
}
|
||||||
13
app/src/sites/airse.json
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"id": "airse",
|
||||||
|
"name": "Airse",
|
||||||
|
"domain": "rse.syui.ai",
|
||||||
|
"scheme": "airse",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"category": "Game",
|
||||||
|
"description": "Airse is an adventure game built with Unreal Engine, featuring AT Protocol integration.",
|
||||||
|
"backLink": "syui.ai",
|
||||||
|
"icon": "/static/app.png",
|
||||||
|
"os": "Windows / macOS",
|
||||||
|
"price": "Free"
|
||||||
|
}
|
||||||
3
app/src/templates/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
export { layout, appLayout, legalLayout } from './layout.js'
|
||||||
|
export { oauthCallback, clientMetadata } from './oauth.js'
|
||||||
|
export { baseStyles } from './styles.js'
|
||||||
110
app/src/templates/layout.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
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">← Back to ${site.backLink}</a>
|
||||||
|
</div>
|
||||||
|
${content}
|
||||||
|
<div class="footer">
|
||||||
|
<p class="copyright">© 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 class="app-version">v${site.version}</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">Version</div>
|
||||||
|
<div class="info-value">${site.version}</div>
|
||||||
|
</div>
|
||||||
|
<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">→</span>
|
||||||
|
</div>
|
||||||
|
<div class="link-row">
|
||||||
|
<span class="link-icon">ATProto</span>
|
||||||
|
<a href="https://syu.is/syui" class="link-value" target="_blank">syu.is/syui</a>
|
||||||
|
<span class="link-arrow">→</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<div class="section-title">Bitcoin</div>
|
||||||
|
<div class="bitcoin-row">
|
||||||
|
<span class="bitcoin-label">₿</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>
|
||||||
|
`)
|
||||||
|
}
|
||||||
46
app/src/templates/oauth.ts
Normal 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)
|
||||||
|
}
|
||||||
51
app/src/templates/styles.ts
Normal 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; }
|
||||||
|
`
|
||||||
18
app/src/types.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
export interface SiteConfig {
|
||||||
|
id: string
|
||||||
|
name: string
|
||||||
|
domain: string
|
||||||
|
scheme: string
|
||||||
|
version: string
|
||||||
|
category: string
|
||||||
|
description: string
|
||||||
|
backLink: string
|
||||||
|
icon: string
|
||||||
|
os: string
|
||||||
|
price: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageMeta {
|
||||||
|
title: string
|
||||||
|
template?: 'default' | 'app' | 'legal'
|
||||||
|
}
|
||||||
16
app/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"strict": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"declaration": true,
|
||||||
|
"skipLibCheck": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||