commit 3375790f933aa4e54527cc56ed464d7b1b53e611 Author: syui Date: Fri Mar 6 16:50:21 2026 +0900 init diff --git a/.github/workflows/deploy-aiat.yml b/.github/workflows/deploy-aiat.yml new file mode 100644 index 0000000..b9ce2f3 --- /dev/null +++ b/.github/workflows/deploy-aiat.yml @@ -0,0 +1,38 @@ +name: Deploy Aiat + +on: + push: + branches: [main] + paths: + - 'web/src/sites/aiat.json' + - 'web/src/assets/aiat/**' + - 'web/src/assets/common/**' + - 'web/src/content/**' + - 'web/src/templates/**' + - 'web/src/types.ts' + - 'web/src/build.ts' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm install + working-directory: web + - run: npm run build + working-directory: web + - uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.APP_AT_NAME }} + directory: web/dist/aiat + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + wranglerVersion: '3' diff --git a/.github/workflows/deploy-aicard.yml b/.github/workflows/deploy-aicard.yml new file mode 100644 index 0000000..00a555d --- /dev/null +++ b/.github/workflows/deploy-aicard.yml @@ -0,0 +1,38 @@ +name: Deploy Aicard + +on: + push: + branches: [main] + paths: + - 'web/src/sites/aicard.json' + - 'web/src/assets/aicard/**' + - 'web/src/assets/common/**' + - 'web/src/content/**' + - 'web/src/templates/**' + - 'web/src/types.ts' + - 'web/src/build.ts' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20' + - run: npm install + working-directory: web + - run: npm run build + working-directory: web + - uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.APP_CARD_NAME }} + directory: web/dist/aicard + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + wranglerVersion: '3' diff --git a/.github/workflows/deploy-airse.yml b/.github/workflows/deploy-airse.yml new file mode 100644 index 0000000..1042dc8 --- /dev/null +++ b/.github/workflows/deploy-airse.yml @@ -0,0 +1,30 @@ +name: Deploy Airse + +on: + push: + branches: [main] + paths: + - 'rse/**' + workflow_dispatch: + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + - run: npm install + working-directory: rse + - run: npm run build + working-directory: rse + - uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.APP_RSE_NAME }} + directory: rse/dist + gitHubToken: ${{ secrets.GITHUB_TOKEN }} + wranglerVersion: '3' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..37e8dec --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.claude +.DS_Store +claude.md +CLAUDE.md +node_modules +package-lock.json +dist +/rse/video.mp4 diff --git a/README.md b/README.md new file mode 100644 index 0000000..e69de29 diff --git a/rse/index.html b/rse/index.html new file mode 100644 index 0000000..dddfc08 --- /dev/null +++ b/rse/index.html @@ -0,0 +1,238 @@ + + + + + + + Airse | Atmosphere Open World + + + + + + + + + + + + +
+ + + + + + + + +
+
+ +
+
+ +
Airse
+
+
+
Atmosphere Open World
+
+
+
+ Windows + + Airse + Airse + +
+
+ Mac + + Airse + Airse + +
+ +
+
+ +
+ + + + + +
+ +
+

+
+

A game built on social networks.

+
+ +
+ + +
+ +
+

Ai × Universe

+
+

The theme is the universe and ai.

+
+ +
+ + +
+ +
+

Airse

+
+

Atmosphere Open World.

+
+

GAME × SNS

+
+

Your SNS account becomes your game account.

+

Game data is stored on decentralized social networks like Bluesky — your world, your data.

+
+
+

Ai × Universe

+
+

The theme is the universe.

+

"Ai" refers to elemental attributes that exist in this world.

+

A combat system built around elemental reactions.

+
+
+

Elements

+
+

Elements are named after real particles — atoms, neutrons, and more.

+

Each character holds one element.

+

Land an elemental attack to apply a status. Strike again with another element for massive damage.

+
+
+

Growth

+
+

Leveling up means ascending limits.

+

Characters, items, and cards each serve different roles but share common stats.

+

Items grant EXP. Cards unlock skills.

+
+
+

Characters

+
+

Character acquisition is seasonal.

+

When a new season begins, new characters become available.

+

The season you join determines your starting character.

+

Each character has four elements: Attribute (Ai), Skill (Yui), Global Value, and Unique Value.

+

Skills are called Yui in this world — equipping a card unlocks them.

+

Global Value is called CP, which is distributed across base stats.

+
+
+

Cards

+
+

You can draw cards in the iOS app Aicard.

+

Owning a card lets you apply it in the game.

+
+
+

Story

+
+

The story is a quest for the smallest matter.

+
+
+ +
+ + + +
+
+ +
+
+ +
Airse
+
+
+
+
+
+ + + + diff --git a/rse/package.json b/rse/package.json new file mode 100644 index 0000000..66f6498 --- /dev/null +++ b/rse/package.json @@ -0,0 +1,19 @@ +{ + "name": "airse-web", + "private": true, + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview" + }, + "dependencies": { + "three": "^0.172.0" + }, + "devDependencies": { + "@types/three": "^0.172.0", + "typescript": "^5.7.0", + "vite": "^6.2.0" + } +} diff --git a/rse/public/account/index.html b/rse/public/account/index.html new file mode 100644 index 0000000..5c9fc00 --- /dev/null +++ b/rse/public/account/index.html @@ -0,0 +1,120 @@ + + + + + + + Account | Airse + + + + + + + + + + +
+
+

Account

+
+

Your SNS account is your game account.

+
+

Sign In

+
+

Sign in with your Bluesky account to access your game data.

+

Account data is managed through the AT Protocol.

+
+
+

Check Game Data

+
+

Access https://syui.ai/ and search by your username (handle).

+

For example, if your name is syui.ai, the URL will be:

+

https://syui.ai/@syui.ai/at/rse

+
+
+

Delete Game Data

+
+

Log in at https://syui.ai/, then delete the ai.syui.rse.user service.

+ Bluesky OAuth +

For example, if your name is syui.ai, you can delete it from the following URL (login required):

+

https://syui.ai/@syui.ai/at/collection/ai.syui.rse.user/self

+
+
+
+ + + + diff --git a/rse/public/icon/ai.svg b/rse/public/icon/ai.svg new file mode 100644 index 0000000..e3e634a --- /dev/null +++ b/rse/public/icon/ai.svg @@ -0,0 +1,19 @@ + + + diff --git a/rse/public/icon/language.svg b/rse/public/icon/language.svg new file mode 100644 index 0000000..89c6b9d --- /dev/null +++ b/rse/public/icon/language.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rse/public/icon/menu.svg b/rse/public/icon/menu.svg new file mode 100644 index 0000000..0a2be6b --- /dev/null +++ b/rse/public/icon/menu.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/rse/public/img/bluesky_oauth.png b/rse/public/img/bluesky_oauth.png new file mode 100644 index 0000000..69a39e0 Binary files /dev/null and b/rse/public/img/bluesky_oauth.png differ diff --git a/rse/public/img/h.png b/rse/public/img/h.png new file mode 100644 index 0000000..c127f31 Binary files /dev/null and b/rse/public/img/h.png differ diff --git a/rse/public/img/hh.png b/rse/public/img/hh.png new file mode 100644 index 0000000..53477c8 Binary files /dev/null and b/rse/public/img/hh.png differ diff --git a/rse/public/img/logo.png b/rse/public/img/logo.png new file mode 100644 index 0000000..fc978c8 Binary files /dev/null and b/rse/public/img/logo.png differ diff --git a/rse/public/img/w.png b/rse/public/img/w.png new file mode 100644 index 0000000..0c3765e Binary files /dev/null and b/rse/public/img/w.png differ diff --git a/rse/public/img/ww.png b/rse/public/img/ww.png new file mode 100644 index 0000000..4fea47d Binary files /dev/null and b/rse/public/img/ww.png differ diff --git a/rse/public/privacy/index.html b/rse/public/privacy/index.html new file mode 100644 index 0000000..c93ad7f --- /dev/null +++ b/rse/public/privacy/index.html @@ -0,0 +1,142 @@ + + + + + + + Privacy Policy | Airse + + + + + + + + + + +
+
+

Privacy Policy

+
+

Airse is an open-world action game that utilizes the AT Protocol for account authentication. This Privacy Policy explains how we handle your information when you use our game.

+
+

Information Collection

+
+

We do not collect any personal information.

+

Airse does not track, store, or analyze any user data, gameplay statistics, or personal information. Your gaming experience remains private.

+
+
+

Account Authentication

+
+

Airse uses AT Protocol for account login functionality. When you log in:

+
    +
  • We only use your AT Protocol identity for authentication purposes.
  • +
  • No additional data is collected or stored beyond what is necessary for login.
  • +
  • Your credentials are handled securely through the AT Protocol.
  • +
+
+
+

syu.is Service Users

+
+
    +
  • The service operator reserves the right to suspend or terminate (ban) accounts at their discretion.
  • +
  • This may occur in cases of terms of service violations or inappropriate behavior.
  • +
  • Account suspension decisions are final.
  • +
+
+
+

Third-Party Services

+
+

Airse relies on AT Protocol infrastructure. Please refer to the privacy policies of your AT Protocol provider (PDS) for information about how they handle your data.

+
+
+

Changes to This Policy

+
+

We may update this Privacy Policy from time to time. Any changes will be posted on this page with an updated revision date.

+
+
+

Contact

+
+

If you have any questions about this Privacy Policy, please contact the developer.

+

https://github.com/syui

+
+
+
+ + + + diff --git a/rse/public/service/ai.syui.at.png b/rse/public/service/ai.syui.at.png new file mode 100644 index 0000000..b9c9eca Binary files /dev/null and b/rse/public/service/ai.syui.at.png differ diff --git a/rse/public/service/ai.syui.card.png b/rse/public/service/ai.syui.card.png new file mode 100644 index 0000000..f5243c3 Binary files /dev/null and b/rse/public/service/ai.syui.card.png differ diff --git a/rse/public/service/ai.syui.rse.png b/rse/public/service/ai.syui.rse.png new file mode 100644 index 0000000..69c24fb Binary files /dev/null and b/rse/public/service/ai.syui.rse.png differ diff --git a/rse/public/support/index.html b/rse/public/support/index.html new file mode 100644 index 0000000..dc001ca --- /dev/null +++ b/rse/public/support/index.html @@ -0,0 +1,108 @@ + + + + + + + Support | Airse + + + + + + + + + + +
+
+

Support

+
+

If you have questions, feedback, or need help with Airse, please reach out through the following channels.

+
+

GitHub

+
+

Source code and issue tracker.

+

https://github.com/syui

+
+
+

Bluesky

+
+

@syui.ai

+
+
+
+ + + + diff --git a/rse/public/terms/index.html b/rse/public/terms/index.html new file mode 100644 index 0000000..06b3343 --- /dev/null +++ b/rse/public/terms/index.html @@ -0,0 +1,99 @@ + + + + + + + Terms | Airse + + + + + + + + + + +
+
+

Terms

+
+
    +
  • The service may be terminated at any time.
  • +
  • Game data may be lost.
  • +
  • Accounts created on syu.is may be lost.
  • +
+
+
+ + + + diff --git a/rse/public/video.mp4 b/rse/public/video.mp4 new file mode 100644 index 0000000..de848d3 Binary files /dev/null and b/rse/public/video.mp4 differ diff --git a/rse/src/main.ts b/rse/src/main.ts new file mode 100644 index 0000000..c8e3eff --- /dev/null +++ b/rse/src/main.ts @@ -0,0 +1,146 @@ +import './style.css' +import * as THREE from 'three' +import { createScene, updateScene } from './scene' + +// Scene setup +const canvas = document.getElementById('space-canvas') as HTMLCanvasElement +const sceneObjs = createScene(canvas) + +// Mouse / Touch +let targetX = 0 +let targetY = 0 +let smoothX = 0 +let smoothY = 0 + +document.addEventListener('mousemove', (e) => { + targetX = (e.clientX / window.innerWidth - 0.5) * 2 + targetY = (e.clientY / window.innerHeight - 0.5) * 2 +}) + +document.addEventListener('touchmove', (e) => { + const t = e.touches[0] + targetX = (t.clientX / window.innerWidth - 0.5) * 2 + targetY = (t.clientY / window.innerHeight - 0.5) * 2 +}, { passive: true }) + +// Animation loop +let time = 0 +const clock = new THREE.Clock() + +function animate() { + requestAnimationFrame(animate) + const dt = clock.getDelta() + time += dt + + smoothX += (targetX - smoothX) * 0.025 + smoothY += (targetY - smoothY) * 0.025 + + updateScene(sceneObjs, time, dt, smoothX, smoothY) + sceneObjs.renderer.render(sceneObjs.scene, sceneObjs.camera) +} + +animate() + +// Resize +window.addEventListener('resize', () => { + sceneObjs.camera.aspect = window.innerWidth / window.innerHeight + sceneObjs.camera.updateProjectionMatrix() + sceneObjs.renderer.setSize(window.innerWidth, window.innerHeight) +}) + +// Page navigation +const pageVideo = document.getElementById('page-video') +const pageLogo = document.getElementById('page-logo') +const pageMessage = document.getElementById('page-message') +const pageMessage2 = document.getElementById('page-message2') +const pageAbout = document.getElementById('page-about') +const pageTitle = document.getElementById('page-title') +const menuBtn = document.getElementById('menu-btn') +const menuDropdown = document.getElementById('menu-dropdown') + +let currentPage: HTMLElement | null = null + +const siteHeader = document.getElementById('site-header') +const siteFooter = document.getElementById('site-footer') +const subpages = [pageAbout] +const fullpages = [pageTitle] + +function showPage(show: HTMLElement | null, hide: HTMLElement | null) { + if (hide) { + hide.classList.remove('visible') + hide.classList.add('page-hidden') + } + if (show) { + show.classList.remove('page-hidden') + show.classList.add('visible') + } + currentPage = show + const isFull = fullpages.includes(show) + if (siteHeader) siteHeader.style.display = isFull ? 'none' : '' + if (siteFooter) { + siteFooter.style.display = isFull ? 'none' : '' + siteFooter.classList.toggle('footer-solid', subpages.includes(show)) + } +} + +// Main page navigation +document.getElementById('btn-next')?.addEventListener('click', () => showPage(pageLogo, pageVideo)) +document.getElementById('btn-logo-back')?.addEventListener('click', () => showPage(pageVideo, pageLogo)) +document.getElementById('btn-logo-next')?.addEventListener('click', () => showPage(pageMessage, pageLogo)) +document.getElementById('btn-msg-back')?.addEventListener('click', () => showPage(pageLogo, pageMessage)) +document.getElementById('btn-msg-next')?.addEventListener('click', () => showPage(pageMessage2, pageMessage)) +document.getElementById('btn-msg2-back')?.addEventListener('click', () => showPage(pageMessage, pageMessage2)) +document.getElementById('btn-msg2-next')?.addEventListener('click', () => showPage(pageAbout, pageMessage2)) +document.getElementById('btn-about-back')?.addEventListener('click', () => showPage(pageMessage2, pageAbout)) +document.getElementById('btn-about-next')?.addEventListener('click', () => showPage(pageTitle, pageAbout)) +pageTitle?.addEventListener('click', () => showPage(pageAbout, pageTitle)) + +// Menu dropdown +menuBtn?.addEventListener('click', (e) => { + e.stopPropagation() + menuDropdown?.classList.toggle('show') + langDropdown?.classList.remove('show') +}) + +// Language selector +let currentLang = localStorage.getItem('preferred-lang') || 'en' +const langBtn = document.getElementById('lang-tab') +const langDropdown = document.getElementById('lang-dropdown') + +function applyLang(lang: string) { + document.querySelectorAll('[data-lang-en]').forEach(el => { + const text = el.getAttribute(`data-lang-${lang}`) + if (text) el.innerHTML = text + }) + langDropdown?.querySelectorAll('.lang-option').forEach(opt => { + opt.classList.toggle('selected', (opt as HTMLElement).dataset.lang === lang) + }) +} + +langBtn?.addEventListener('click', (e) => { + e.stopPropagation() + langDropdown?.classList.toggle('show') + menuDropdown?.classList.remove('show') +}) + +langDropdown?.querySelectorAll('.lang-option').forEach(opt => { + opt.addEventListener('click', (e) => { + e.stopPropagation() + currentLang = (opt as HTMLElement).dataset.lang || 'en' + localStorage.setItem('preferred-lang', currentLang) + applyLang(currentLang) + langDropdown?.classList.remove('show') + }) +}) + +document.addEventListener('click', () => { + langDropdown?.classList.remove('show') + menuDropdown?.classList.remove('show') +}) + +applyLang(currentLang) + +// Show first page immediately +pageVideo?.classList.add('visible') +currentPage = pageVideo +document.body.style.opacity = '1' diff --git a/rse/src/scene.ts b/rse/src/scene.ts new file mode 100644 index 0000000..e7c77c0 --- /dev/null +++ b/rse/src/scene.ts @@ -0,0 +1,127 @@ +import * as THREE from 'three' + +export interface SceneObjects { + renderer: THREE.WebGLRenderer + scene: THREE.Scene + camera: THREE.PerspectiveCamera + starsFar: THREE.Points + starsMid: THREE.Points + starsClose: THREE.Points + dust: THREE.Points + nebulaGroup: THREE.Group + light1: THREE.PointLight + light2: THREE.PointLight +} + +function createStarLayer( + count: number, spread: number, size: number, opacity: number +): THREE.Points { + const geo = new THREE.BufferGeometry() + const pos = new Float32Array(count * 3) + const col = new Float32Array(count * 3) + + for (let i = 0; i < count; i++) { + const i3 = i * 3 + pos[i3] = (Math.random() - 0.5) * spread + pos[i3 + 1] = (Math.random() - 0.5) * spread + pos[i3 + 2] = (Math.random() - 0.5) * spread + + // Dark particles on white background + const shade = Math.random() * 0.08 + col[i3] = shade; col[i3+1] = shade; col[i3+2] = shade + 0.02 + } + + geo.setAttribute('position', new THREE.BufferAttribute(pos, 3)) + geo.setAttribute('color', new THREE.BufferAttribute(col, 3)) + + const mat = new THREE.PointsMaterial({ + size, vertexColors: true, transparent: true, opacity, + sizeAttenuation: true, depthWrite: false, + }) + + return new THREE.Points(geo, mat) +} + +export function createScene(canvas: HTMLCanvasElement): SceneObjects { + const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: true }) + renderer.setSize(window.innerWidth, window.innerHeight) + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) + renderer.setClearColor(0x000000, 0) // transparent, CSS handles bg + + const scene = new THREE.Scene() + scene.fog = new THREE.FogExp2(0xf5f5f8, 0.00025) + + const camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 5000) + camera.position.set(0, 0, 600) + + // Stars (dark particles) + const starsFar = createStarLayer(6000, 4000, 1.0, 0.3) + const starsMid = createStarLayer(3000, 2500, 1.8, 0.45) + const starsClose = createStarLayer(1000, 1500, 2.5, 0.6) + scene.add(starsFar, starsMid, starsClose) + + // Dust (dark) + const dustGeo = new THREE.BufferGeometry() + const dustCount = 2000 + const dustPos = new Float32Array(dustCount * 3) + for (let i = 0; i < dustCount; i++) { + dustPos[i*3] = (Math.random() - 0.5) * 3000 + dustPos[i*3+1] = (Math.random() - 0.5) * 3000 + dustPos[i*3+2] = (Math.random() - 0.5) * 3000 + } + dustGeo.setAttribute('position', new THREE.BufferAttribute(dustPos, 3)) + const dust = new THREE.Points(dustGeo, new THREE.PointsMaterial({ + size: 0.5, color: 0x222233, transparent: true, opacity: 0.3, + depthWrite: false, sizeAttenuation: true, + })) + scene.add(dust) + + const nebulaGroup = new THREE.Group() + + // Lights + scene.add(new THREE.AmbientLight(0xffffff, 0.6)) + + const light1 = new THREE.PointLight(0xccddee, 1.5, 1200) + light1.position.set(200, 200, 200) + scene.add(light1) + + const light2 = new THREE.PointLight(0xbbbbdd, 1.0, 1000) + light2.position.set(-300, -100, 100) + scene.add(light2) + + return { + renderer, scene, camera, + starsFar, starsMid, starsClose, dust, nebulaGroup, + light1, light2, + } +} + +export function updateScene(objs: SceneObjects, time: number, _dt: number, smoothX: number, smoothY: number) { + const { + camera, starsFar, starsMid, starsClose, dust, nebulaGroup, + light1, light2, + } = objs + + // Camera + camera.position.x = smoothX * 150 + camera.position.y = -smoothY * 100 + camera.position.z = 600 + Math.sin(time * 0.08) * 30 + camera.lookAt(smoothX * 30, -smoothY * 20, -150) + + // Star parallax + starsFar.rotation.y = time * 0.008 + smoothX * 0.015 + starsFar.rotation.x = time * 0.004 + smoothY * 0.015 + starsMid.rotation.y = time * 0.015 + smoothX * 0.03 + starsMid.rotation.x = time * 0.008 + smoothY * 0.03 + starsClose.rotation.y = time * 0.025 + smoothX * 0.05 + starsClose.rotation.x = time * 0.012 + smoothY * 0.05 + + // Dust + dust.rotation.y = time * 0.005 + smoothX * 0.01 + dust.rotation.x = time * 0.003 + smoothY * 0.01 + + + // Lights + light1.position.x = 200 + Math.sin(time * 0.25) * 120 + light2.position.y = -100 + Math.cos(time * 0.3) * 100 +} diff --git a/rse/src/style.css b/rse/src/style.css new file mode 100644 index 0000000..b6a7bf1 --- /dev/null +++ b/rse/src/style.css @@ -0,0 +1,677 @@ +@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;500;700;900&family=Space+Mono:wght@400;700&display=swap'); + +* { margin: 0; padding: 0; box-sizing: border-box; } + +:root { + --color-bg: #f5f5f8; + --color-text: #1a1a2e; + --color-accent: #3a7ca5; + --color-accent2: #6a5acd; + --color-dim: #8888a0; + --font-display: 'Orbitron', sans-serif; + --font-body: 'Space Mono', monospace; +} + +html, body { + width: 100%; height: 100%; + overflow: hidden; + background: var(--color-bg); + color: var(--color-text); + font-size: calc(1rem + 3px); +} + +#space-canvas { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 0; +} + +/* ===== HEADER ===== */ +.site-header { + position: fixed; + top: 0; left: 0; + width: 100%; + height: 5rem; + padding: 0 2rem; + display: flex; + align-items: center; + z-index: 110; + background: var(--color-bg); + opacity: 1; + transition: opacity 0.6s ease; + pointer-events: auto; +} + +.header-logo { + display: inline-flex; + align-items: center; + gap: 0.5rem; + text-decoration: none; + color: var(--color-text); + margin-right: auto; +} + +.header-logo-icon { + width: 24px; + height: 24px; +} + +.header-logo-text { + font-family: var(--font-display); + font-weight: 700; + font-size: 0.85rem; + letter-spacing: 0.1em; +} + +/* ===== FOOTER ===== */ +.site-footer { + position: fixed; + bottom: 0; left: 0; + width: 100%; + padding: 1.5rem 2rem 2.5rem; + z-index: 110; + display: flex; + flex-direction: column; + align-items: center; + gap: 1.2rem; + opacity: 1; + transition: opacity 0.6s ease; + pointer-events: auto; +} + +.site-footer.footer-solid { + background: var(--color-bg); +} + +/* ===== VIDEO APPS (position 3: top-right) ===== */ +.video-apps { + position: absolute; + top: 15%; + right: 18%; + z-index: 3; + display: flex; + flex-direction: column; + gap: 0.6rem; +} + +.video-apps-group { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 0.3rem; +} + +.video-apps-label { + font-family: var(--font-display); + font-size: 0.5rem; + font-weight: 700; + letter-spacing: 0.1em; + color: #ffffff; + background: rgba(0, 0, 0, 0.35); + padding: 0.25rem 0.7rem; + border-radius: 3px; +} + +.video-app-link { + display: flex; + align-items: center; + gap: 0.6rem; + text-decoration: none; + transition: opacity 0.3s ease; + opacity: 0.7; + padding: 0.3rem 0; +} + +.video-app-link:hover { + opacity: 1; +} + +.video-app-icon { + width: 24px; + height: 24px; + border-radius: 4px; +} + +.video-app-name { + font-family: var(--font-body); + font-size: 0.55rem; + letter-spacing: 0.05em; + color: rgba(255, 255, 255, 0.7); +} + +.footer-copy { + font-family: var(--font-body); + font-size: 0.6rem; + letter-spacing: 0.1em; + color: var(--color-dim); + line-height: 1; +} + + +/* ===== LOGO PAGE ===== */ +.logo-icon { + width: clamp(80px, 15vw, 160px); + height: auto; +} + +/* ===== PAGES ===== */ +.page { + position: fixed; + top: 5rem; left: 0; + width: 100%; height: calc(100% - 5rem); + z-index: 10; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + opacity: 0; + transition: opacity 1s cubic-bezier(0.4, 0, 0.2, 1); + pointer-events: none; +} + +.page.visible { + opacity: 1; + pointer-events: auto; +} + +.page.page-hidden { + opacity: 0; + pointer-events: none; +} + +.page.page-full { + top: 0; + height: 100%; + z-index: 120; + background: var(--color-bg); +} + +.page-full .hero-video { + width: 100vw; + height: 100vh; + border-radius: 0; +} + +.page-full .hero-video::before, +.page-full .hero-video::after { + display: none; +} + +/* ===== NAV BUTTONS ===== */ +.nav-btn { + position: fixed; + top: 50%; + transform: translateY(-50%); + width: 44px; height: 44px; + background: none; + border: none; + cursor: pointer; + opacity: 0.8; + transition: opacity 0.3s ease, transform 0.3s ease; + z-index: 20; +} + +.nav-btn:hover { + opacity: 1; + transform: translateY(-50%) scale(1.15); +} + +.nav-btn-right { + right: 2.5rem; +} + +.nav-btn-left { + left: 2.5rem; +} + +/* ===== PAGE 1: VIDEO ===== */ +.hero-video { + position: relative; + width: calc(100vw - 8rem); + height: calc(100vh - 5rem - 4rem); + border: none; + border-radius: 4px; + overflow: hidden; + background: rgba(26, 26, 46, 0.05); + opacity: 0; + transform: translateY(20px); + animation: fadeUp 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; + animation-play-state: paused; +} + +.hero-video::before { + content: ''; + position: absolute; + top: -1px; left: -1px; + right: -1px; bottom: -1px; + border-radius: 5px; + border: 1px solid transparent; + background: linear-gradient(135deg, var(--color-accent), transparent 40%, transparent 60%, var(--color-accent2)) border-box; + -webkit-mask: linear-gradient(#fff 0 0) padding-box, linear-gradient(#fff 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + z-index: 4; + pointer-events: none; +} + +.hero-video::after { + content: ''; + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + border-radius: 4px; + box-shadow: inset 0 0 30px rgba(0, 0, 0, 0.15); + z-index: 3; + pointer-events: none; +} + +.page.visible .hero-video { + animation-play-state: running; + animation-delay: 0.3s; +} + +.hero-video video { + width: 100%; + height: 100%; + object-fit: cover; + display: block; +} + +.video-text { + position: absolute; + top: 0; left: 0; + width: 100%; height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + z-index: 2; + background: radial-gradient(ellipse at center, rgba(0, 0, 0, 0.15) 0%, rgba(0, 0, 0, 0.05) 60%, rgba(0, 0, 0, 0.2) 100%); +} + +.video-title-wrap { + display: flex; + align-items: center; + gap: 0.8rem; +} + +.video-title-icon { + width: clamp(2rem, 5vw, 3.5rem); + height: auto; +} + +.video-label { + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(1.6rem, 5vw, 3.5rem); + letter-spacing: 0.15em; + color: #ffffff; + text-shadow: 0 0 30px rgba(0, 0, 0, 0.4), 0 2px 15px rgba(0, 0, 0, 0.3); +} + +.video-label-lg { + font-size: clamp(3rem, 10vw, 7rem); +} + +.video-separator { + width: 0; height: 1px; + background: linear-gradient(90deg, transparent, var(--color-accent), var(--color-accent2), transparent); + margin: 1rem 0; + animation: lineExpandWide 1.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; + animation-play-state: paused; +} + +.page.visible .video-separator { + animation-play-state: running; + animation-delay: 1s; +} + +.video-desc { + font-family: var(--font-body); + font-size: clamp(0.6rem, 1.2vw, 0.85rem); + letter-spacing: 0.25em; + color: rgba(255, 255, 255, 0.8); + text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4); +} + +/* ===== PAGE 2: MESSAGE ===== */ +.message-content { + text-align: center; + opacity: 0; + transform: translateY(20px); + animation: fadeUp 1.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; + animation-play-state: paused; +} + +.page.visible .message-content { + animation-play-state: running; + animation-delay: 0.2s; +} + +.message-title { + font-family: var(--font-display); + font-weight: 900; + font-size: clamp(2rem, 7vw, 5rem); + letter-spacing: 0.15em; + text-transform: uppercase; + color: var(--color-text); +} + +.message-separator { + width: 0; height: 1px; + margin: 1.5rem auto; + background: linear-gradient(90deg, transparent, var(--color-accent), var(--color-accent2), transparent); + animation: lineExpandWide 1.5s cubic-bezier(0.16, 1, 0.3, 1) forwards; + animation-play-state: paused; +} + +.page.visible .message-separator { + animation-play-state: running; + animation-delay: 0.8s; +} + +.message-desc { + font-family: var(--font-body); + font-size: clamp(0.7rem, 1.3vw, 0.95rem); + letter-spacing: 0.2em; + color: var(--color-dim); + margin-bottom: 2.5rem; +} + +/* ===== LANG SELECTOR ===== */ +.lang-selector { + position: relative; +} + +.lang-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + padding: 6px; + opacity: 0.4; + transition: opacity 0.3s ease, background 0.3s ease; +} + +.lang-btn:hover { + opacity: 0.9; + background: rgba(26, 26, 46, 0.06); +} + +.lang-icon { + width: 20px; + height: 20px; +} + +.lang-dropdown { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 100px; + overflow: hidden; +} + +.lang-dropdown.show { + display: block; +} + +.lang-option { + display: flex; + justify-content: space-between; + align-items: center; + padding: 10px 14px; + cursor: pointer; + font-family: var(--font-body); + font-size: 0.75rem; + letter-spacing: 0.05em; + transition: background 0.15s; +} + +.lang-option:hover { + background: #f0f0f0; +} + +.lang-option.selected { + background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%); +} + +.lang-check { + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #ccc; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + transition: all 0.2s; + color: transparent; +} + +.lang-option.selected .lang-check { + background: var(--color-accent); + border-color: var(--color-accent); + color: #fff; +} + +/* ===== MENU DROPDOWN ===== */ +.menu-selector { + position: relative; + margin-left: 8px; +} + +.menu-btn { + display: flex; + align-items: center; + justify-content: center; + background: transparent; + border: none; + border-radius: 6px; + cursor: pointer; + padding: 6px; + opacity: 0.4; + transition: opacity 0.3s ease, background 0.3s ease; +} + +.menu-btn:hover { + opacity: 0.9; + background: rgba(26, 26, 46, 0.06); +} + +.menu-icon { + width: 20px; + height: 20px; +} + +.menu-dropdown { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: #fff; + border: 1px solid #ddd; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); + min-width: 180px; + overflow: hidden; +} + +.menu-dropdown.show { + display: block; +} + +.menu-option { + display: flex; + align-items: center; + padding: 10px 14px; + cursor: pointer; + font-family: var(--font-body); + font-size: 0.75rem; + letter-spacing: 0.05em; + transition: background 0.15s; +} + +.menu-option:hover { + background: #f0f0f0; +} + +a.menu-option { + text-decoration: none; + color: inherit; +} + +.menu-option-active { + background: linear-gradient(135deg, #f0f7ff 0%, #e8f4ff 100%); +} + +/* ===== SUBPAGES: solid bg, no 3D ===== */ +#page-about, +#page-privacy, +#page-account, +#page-terms { + background: var(--color-bg); + z-index: 15; + justify-content: flex-start; + overflow-y: auto; +} + +.subpage-content { + width: 100%; + max-width: 640px; + margin: 0 auto; + padding: 4rem 2rem 6rem; +} + +.subpage-heading { + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(1rem, 2.5vw, 1.4rem); + letter-spacing: 0.1em; + color: var(--color-text); + margin-bottom: 0.8rem; +} + +.subpage-section { + margin-top: 2.5rem; +} + +.subpage-section-title { + font-family: var(--font-display); + font-weight: 700; + font-size: clamp(0.85rem, 1.8vw, 1.1rem); + letter-spacing: 0.1em; + color: var(--color-text); + margin-bottom: 0.8rem; +} + +.subpage-section-line { + width: clamp(40px, 8vw, 80px); + height: 1px; + background: linear-gradient(90deg, var(--color-accent), var(--color-accent2), transparent); + margin-bottom: 1rem; +} + +.subpage-section-text { + font-family: var(--font-body); + font-size: clamp(0.65rem, 1.1vw, 0.8rem); + line-height: 2; + letter-spacing: 0.05em; + color: rgba(26, 26, 46, 0.6); +} + +.subpage-list { + list-style: none; + margin-top: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.subpage-list-item { + font-family: var(--font-body); + font-size: clamp(0.65rem, 1.1vw, 0.8rem); + line-height: 2; + letter-spacing: 0.05em; + color: rgba(26, 26, 46, 0.6); + padding-left: 1.2em; + position: relative; +} + +.subpage-list-item::before { + content: ''; + position: absolute; + left: 0; + top: 0.85em; + width: 6px; + height: 6px; + border-radius: 50%; + background: linear-gradient(135deg, var(--color-accent), var(--color-accent2)); +} + +.subpage-section-text a { + color: var(--color-accent); + text-decoration: none; + word-break: break-all; +} + +.subpage-section-text a:hover { + text-decoration: underline; +} + +.subpage-section-text code { + background: rgba(26, 26, 46, 0.06); + padding: 0.15em 0.4em; + border-radius: 3px; + font-size: 0.9em; +} + +.subpage-img { + max-width: 100%; + border-radius: 4px; + margin: 0.5rem 0; +} + +/* Overlay */ +.vignette { + position: fixed; + top: 0; left: 0; + width: 100%; height: 100%; + z-index: 4; + pointer-events: none; + background: radial-gradient(ellipse at center, transparent 40%, rgba(245, 245, 248, 0.7) 100%); +} + +/* Animations */ +@keyframes fadeUp { + from { opacity: 0; transform: translateY(15px); } + to { opacity: 1; transform: translateY(0); } +} +@keyframes lineExpandWide { + to { width: clamp(80px, 15vw, 180px); } +} + +@media (max-width: 768px) { + .hero-video { + width: calc(100vw - 1rem); + height: calc(100vh - 1rem); + border-radius: 2px; + } + + .nav-btn-right { right: 1rem; } + .nav-btn-left { left: 1rem; } + .nav-btn { width: 36px; height: 36px; } + + .site-header { padding: 0.8rem 1rem; } + .site-footer { padding: 1rem 1rem 1.5rem; } +} diff --git a/rse/tsconfig.json b/rse/tsconfig.json new file mode 100644 index 0000000..efc620c --- /dev/null +++ b/rse/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "lib": ["ESNext", "DOM", "DOM.Iterable"] + }, + "include": ["src"] +} diff --git a/rse/vite.config.ts b/rse/vite.config.ts new file mode 100644 index 0000000..8da042d --- /dev/null +++ b/rse/vite.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from 'vite' + +export default defineConfig({ + publicDir: 'public', + build: { + assetsInlineLimit: 0, + }, + appType: 'mpa', + plugins: [{ + name: 'clean-urls', + configureServer(server) { + server.middlewares.use((req, _res, next) => { + const url = req.url?.split('?')[0] ?? '' + if (url.match(/^\/[a-z]+$/) && !url.includes('.')) { + req.url = url + '/index.html' + } + next() + }) + }, + }], +}) diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..44e4a09 --- /dev/null +++ b/web/package.json @@ -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" + } +} diff --git a/web/src/assets/aiat/app.png b/web/src/assets/aiat/app.png new file mode 100644 index 0000000..f8f45fb Binary files /dev/null and b/web/src/assets/aiat/app.png differ diff --git a/web/src/assets/aiat/icon.png b/web/src/assets/aiat/icon.png new file mode 100644 index 0000000..f8f45fb Binary files /dev/null and b/web/src/assets/aiat/icon.png differ diff --git a/web/src/assets/aicard/app.png b/web/src/assets/aicard/app.png new file mode 100644 index 0000000..69a6840 Binary files /dev/null and b/web/src/assets/aicard/app.png differ diff --git a/web/src/assets/aicard/icon.png b/web/src/assets/aicard/icon.png new file mode 100644 index 0000000..69a6840 Binary files /dev/null and b/web/src/assets/aicard/icon.png differ diff --git a/web/src/assets/common/favicon.png b/web/src/assets/common/favicon.png new file mode 100644 index 0000000..2227ba3 Binary files /dev/null and b/web/src/assets/common/favicon.png differ diff --git a/web/src/build.ts b/web/src/build.ts new file mode 100644 index 0000000..c8f8261 --- /dev/null +++ b/web/src/build.ts @@ -0,0 +1,140 @@ +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): 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`) + + // Generate apple-app-site-association for Universal Links (if appId is set) + if (site.appId) { + const aasa = { + applinks: { + apps: [], + details: [{ + appID: site.appId, + paths: ['/oauth/callback', '/oauth/callback/'] + }] + } + } + const aasaPath = join(siteDir, '.well-known/apple-app-site-association') + writeFileSync(aasaPath, JSON.stringify(aasa, null, 2)) + console.log(` -> .well-known/apple-app-site-association`) + } + + // 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() diff --git a/web/src/content/pages/app.md b/web/src/content/pages/app.md new file mode 100644 index 0000000..d27753b --- /dev/null +++ b/web/src/content/pages/app.md @@ -0,0 +1,5 @@ +--- +title: App Info +template: app +path: app/index.html +--- diff --git a/web/src/content/pages/help.md b/web/src/content/pages/help.md new file mode 100644 index 0000000..1a5c561 --- /dev/null +++ b/web/src/content/pages/help.md @@ -0,0 +1,39 @@ +--- +title: Help +template: legal +path: help/index.html +--- + +## Getting Started + +1. Download the app from the App Store +2. Sign in with your handle +3. Start connecting with your community + +## Authentication + +This app uses 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 our social account. diff --git a/web/src/content/pages/index.md b/web/src/content/pages/index.md new file mode 100644 index 0000000..75d5f34 --- /dev/null +++ b/web/src/content/pages/index.md @@ -0,0 +1,5 @@ +--- +title: App Info +template: app +path: index.html +--- diff --git a/web/src/content/pages/license.md b/web/src/content/pages/license.md new file mode 100644 index 0000000..babff61 --- /dev/null +++ b/web/src/content/pages/license.md @@ -0,0 +1,29 @@ +--- +title: License +template: legal +path: license/index.html +--- + +## Software License + +This application is open source software. + +## Third-Party Libraries + +This app uses the following open source libraries: + +- **Decentralized Social Protocol** - MIT License +- **React Native** - MIT License +- **Expo** - MIT License + +## Assets + +App icons and graphics are proprietary unless otherwise noted. + +## Attribution + +Built by syui. + +## Contact + +For licensing questions, please contact us through our Git repository. diff --git a/web/src/content/pages/privacy.md b/web/src/content/pages/privacy.md new file mode 100644 index 0000000..1edcd64 --- /dev/null +++ b/web/src/content/pages/privacy.md @@ -0,0 +1,35 @@ +--- +title: Privacy Policy +template: legal +path: privacy/index.html +--- + +## Information We Collect + +This application collects minimal information necessary for its operation: + +- **Account Information**: Your 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 decentralized social 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* diff --git a/web/src/content/pages/tos.md b/web/src/content/pages/tos.md new file mode 100644 index 0000000..bf2d27d --- /dev/null +++ b/web/src/content/pages/tos.md @@ -0,0 +1,40 @@ +--- +title: Terms of Service +template: legal +path: tos/index.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 +- 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* diff --git a/web/src/sites/aiat.json b/web/src/sites/aiat.json new file mode 100644 index 0000000..3a40517 --- /dev/null +++ b/web/src/sites/aiat.json @@ -0,0 +1,14 @@ +{ + "id": "aiat", + "name": "Aiat", + "domain": "at.syui.ai", + "scheme": "aiat", + "appUrl": "https://at.syui.ai/app", + "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" +} diff --git a/web/src/sites/aicard.json b/web/src/sites/aicard.json new file mode 100644 index 0000000..27bf3e3 --- /dev/null +++ b/web/src/sites/aicard.json @@ -0,0 +1,15 @@ +{ + "id": "aicard", + "name": "Aicard", + "domain": "card.syui.ai", + "scheme": "aicard", + "appId": "WN6KD5ZT49.ai.syui.card", + "appUrl": "https://card.syui.ai/app", + "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" +} diff --git a/web/src/templates/index.ts b/web/src/templates/index.ts new file mode 100644 index 0000000..ac716f2 --- /dev/null +++ b/web/src/templates/index.ts @@ -0,0 +1,3 @@ +export { layout, appLayout, legalLayout } from './layout.js' +export { oauthCallback, clientMetadata } from './oauth.js' +export { baseStyles } from './styles.js' diff --git a/web/src/templates/layout.ts b/web/src/templates/layout.ts new file mode 100644 index 0000000..f386faf --- /dev/null +++ b/web/src/templates/layout.ts @@ -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 ` + + + + + ${title} - ${site.name} + + + + + + ${content} + + +` +} + +export function appLayout(site: SiteConfig, content: string): string { + return layout(site, 'App Info', ` +
+ ${site.appUrl ? `` : ''}${site.name}${site.appUrl ? `` : ''} +
${site.name}
+
+ +
+

${site.description}

+
+ +
+
App Information
+
+
+
Category
+
${site.category}
+
+
+
Supported OS
+
${site.os}
+
+
+
Price
+
${site.price}
+
+
+
+ +
+
Developer
+
syui
+ + +
+ +
+
Bitcoin
+
+ + 3BqHXxraZyBapyNpJmniJDh9zqzuB8aoRr + copy +
+
+ + ${content} + + +`) +} + +export function legalLayout(site: SiteConfig, title: string, content: string): string { + return layout(site, title, ` +
+

${title}

+ ${content} +
+`) +} diff --git a/web/src/templates/oauth.ts b/web/src/templates/oauth.ts new file mode 100644 index 0000000..37c57d6 --- /dev/null +++ b/web/src/templates/oauth.ts @@ -0,0 +1,46 @@ +import type { SiteConfig } from '../types.js' + +export function oauthCallback(site: SiteConfig): string { + return ` + + + Redirecting... + + + +

Redirecting to app...

+

If nothing happens, click + here to continue in browser.

+ +` +} + +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) +} diff --git a/web/src/templates/styles.ts b/web/src/templates/styles.ts new file mode 100644 index 0000000..6fa052a --- /dev/null +++ b/web/src/templates/styles.ts @@ -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; } +` diff --git a/web/src/types.ts b/web/src/types.ts new file mode 100644 index 0000000..5d9533c --- /dev/null +++ b/web/src/types.ts @@ -0,0 +1,20 @@ +export interface SiteConfig { + id: string + name: string + domain: string + scheme: string + appId?: string + appUrl?: string + version: string + category: string + description: string + backLink: string + icon: string + os: string + price: string +} + +export interface PageMeta { + title: string + template?: 'default' | 'app' | 'legal' +} diff --git a/web/tsconfig.json b/web/tsconfig.json new file mode 100644 index 0000000..84fadc1 --- /dev/null +++ b/web/tsconfig.json @@ -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"] +}