add ai.syui.rse
This commit is contained in:
@@ -38,6 +38,16 @@ export function getServiceLinks(handle: string, collections: string[], migration
|
||||
})
|
||||
}
|
||||
|
||||
// RSE
|
||||
if (collections.includes('ai.syui.rse.user')) {
|
||||
services.push({
|
||||
name: 'RSE',
|
||||
icon: '/service/ai.syui.rse.png',
|
||||
url: `/@${handle}/at/rse`,
|
||||
collection: 'ai.syui.rse.user'
|
||||
})
|
||||
}
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
|
||||
127
src/web/components/rse.ts
Normal file
127
src/web/components/rse.ts
Normal file
@@ -0,0 +1,127 @@
|
||||
// RSE display component for ai.syui.rse.user collection
|
||||
|
||||
export interface RseItem {
|
||||
id: number
|
||||
cp: number
|
||||
mode: number
|
||||
shiny: boolean
|
||||
unique: boolean
|
||||
}
|
||||
|
||||
export interface RseCollection {
|
||||
item: RseItem[]
|
||||
character: RseItem[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Get rarity class from shiny/unique flags
|
||||
function getRarityClass(item: RseItem): string {
|
||||
if (item.unique) return 'unique'
|
||||
if (item.shiny) return 'shiny'
|
||||
return ''
|
||||
}
|
||||
|
||||
// Render single item/character
|
||||
function renderRseItem(item: RseItem, type: 'item' | 'character'): string {
|
||||
const rarityClass = getRarityClass(item)
|
||||
const effectsHtml = rarityClass ? `
|
||||
<div class="card-status pattern-${rarityClass}"></div>
|
||||
<div class="card-status color-${rarityClass}"></div>
|
||||
` : ''
|
||||
|
||||
return `
|
||||
<div class="card-item">
|
||||
<div class="card-wrapper">
|
||||
<div class="card-reflection">
|
||||
<img src="/rse/${type}/${item.id}.webp" alt="${type} ${item.id}" loading="lazy" />
|
||||
</div>
|
||||
${effectsHtml}
|
||||
</div>
|
||||
<div class="card-detail">
|
||||
<span class="card-cp">${item.cp}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// Render RSE page
|
||||
export function renderRsePage(
|
||||
collection: RseCollection | null,
|
||||
handle: string
|
||||
): string {
|
||||
const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self`
|
||||
|
||||
if (!collection) {
|
||||
return `
|
||||
<div class="card-page">
|
||||
<div class="card-header">
|
||||
<h2>RSE</h2>
|
||||
<a href="${jsonUrl}" class="json-btn">json</a>
|
||||
</div>
|
||||
<p class="no-cards">No RSE data found for @${handle}</p>
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
const characters = collection.character || []
|
||||
const items = collection.item || []
|
||||
|
||||
// Stats
|
||||
const totalChars = characters.length
|
||||
const totalItems = items.length
|
||||
const uniqueChars = characters.filter(c => c.unique).length
|
||||
const shinyChars = characters.filter(c => c.shiny).length
|
||||
|
||||
// Sort by unique > shiny > id
|
||||
const sortedChars = [...characters].sort((a, b) => {
|
||||
if (a.unique !== b.unique) return a.unique ? -1 : 1
|
||||
if (a.shiny !== b.shiny) return a.shiny ? -1 : 1
|
||||
return a.id - b.id
|
||||
})
|
||||
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
if (a.unique !== b.unique) return a.unique ? -1 : 1
|
||||
if (a.shiny !== b.shiny) return a.shiny ? -1 : 1
|
||||
return a.id - b.id
|
||||
})
|
||||
|
||||
const charsHtml = sortedChars.map(c => renderRseItem(c, 'character')).join('')
|
||||
const itemsHtml = sortedItems.map(i => renderRseItem(i, 'item')).join('')
|
||||
|
||||
return `
|
||||
<div class="card-page">
|
||||
<div class="card-header">
|
||||
<div class="card-stats">
|
||||
<div class="stat">
|
||||
<span class="stat-value">${totalChars}</span>
|
||||
<span class="stat-label">Characters</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="stat-value">${totalItems}</span>
|
||||
<span class="stat-label">Items</span>
|
||||
</div>
|
||||
<div class="stat rare-unique">
|
||||
<span class="stat-value">${uniqueChars}</span>
|
||||
<span class="stat-label">Unique</span>
|
||||
</div>
|
||||
<div class="stat rare-shiny">
|
||||
<span class="stat-value">${shinyChars}</span>
|
||||
<span class="stat-label">Shiny</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<a href="${jsonUrl}" class="json-btn">json</a>
|
||||
</div>
|
||||
${charsHtml ? `
|
||||
<h3 class="rse-section-title">Characters</h3>
|
||||
<div class="card-grid">${charsHtml}</div>
|
||||
` : ''}
|
||||
${itemsHtml ? `
|
||||
<h3 class="rse-section-title">Items</h3>
|
||||
<div class="card-grid">${itemsHtml}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -603,3 +603,52 @@ export async function getCards(
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// RSE collection type
|
||||
export interface RseItem {
|
||||
id: number
|
||||
cp: number
|
||||
mode: number
|
||||
shiny: boolean
|
||||
unique: boolean
|
||||
}
|
||||
|
||||
export interface RseCollection {
|
||||
item: RseItem[]
|
||||
character: RseItem[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Get user's RSE collection (ai.syui.rse.user)
|
||||
export async function getRse(did: string): Promise<RseCollection | null> {
|
||||
const collection = 'ai.syui.rse.user'
|
||||
|
||||
// Try local first
|
||||
try {
|
||||
const res = await fetch(`/content/${did}/${collection}/self.json`)
|
||||
if (res.ok && isJsonResponse(res)) {
|
||||
const record = await res.json()
|
||||
return record.value as RseCollection
|
||||
}
|
||||
} catch {
|
||||
// Try remote
|
||||
}
|
||||
|
||||
// Remote fallback
|
||||
const pds = await getPds(did)
|
||||
if (!pds) return null
|
||||
|
||||
try {
|
||||
const host = pds.replace('https://', '')
|
||||
const url = `${xrpcUrl(host, comAtprotoRepo.getRecord)}?repo=${did}&collection=${collection}&rkey=self`
|
||||
const res = await fetch(url)
|
||||
if (res.ok) {
|
||||
const record = await res.json()
|
||||
return record.value as RseCollection
|
||||
}
|
||||
} catch {
|
||||
// Failed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export interface Route {
|
||||
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old'
|
||||
type: 'home' | 'user' | 'post' | 'postpage' | 'atbrowser' | 'service' | 'collection' | 'record' | 'chat' | 'chat-thread' | 'card' | 'card-old' | 'rse'
|
||||
handle?: string
|
||||
rkey?: string
|
||||
service?: string
|
||||
@@ -63,6 +63,12 @@ export function parseRoute(): Route {
|
||||
return { type: 'card-old', handle: cardOldMatch[1] }
|
||||
}
|
||||
|
||||
// RSE page: /@handle/at/rse
|
||||
const rseMatch = path.match(/^\/@([^/]+)\/at\/rse\/?$/)
|
||||
if (rseMatch) {
|
||||
return { type: 'rse', handle: rseMatch[1] }
|
||||
}
|
||||
|
||||
// Chat thread: /@handle/at/chat/{rkey}
|
||||
const chatThreadMatch = path.match(/^\/@([^/]+)\/at\/chat\/([^/]+)$/)
|
||||
if (chatThreadMatch) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import './styles/main.css'
|
||||
import './styles/card.css'
|
||||
import './styles/card-migrate.css'
|
||||
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getOldApiUserByDid, hasCardOldRecord } from './lib/api'
|
||||
import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getRse, getOldApiUserByDid, hasCardOldRecord } from './lib/api'
|
||||
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
|
||||
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost } from './lib/auth'
|
||||
import { validateRecord } from './lib/lexicon'
|
||||
@@ -14,6 +14,7 @@ import { renderModeTabs, renderLangSelector, setupModeTabs } from './components/
|
||||
import { renderFooter } from './components/footer'
|
||||
import { renderChatListPage, renderChatThreadPage } from './components/chat'
|
||||
import { renderCardPage } from './components/card'
|
||||
import { renderRsePage } from './components/rse'
|
||||
import { checkMigrationStatus, renderMigrationPage, setupMigrationButton } from './components/card-migrate'
|
||||
import { showLoading, hideLoading } from './components/loading'
|
||||
|
||||
@@ -254,6 +255,12 @@ async function render(route: Route): Promise<void> {
|
||||
html += `<div id="content">${renderMigrationPage(cardMigrationState, handle, isOwner)}</div>`
|
||||
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
||||
|
||||
} else if (route.type === 'rse') {
|
||||
// RSE page
|
||||
const rseData = await getRse(did)
|
||||
html += `<div id="content">${renderRsePage(rseData, handle)}</div>`
|
||||
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
||||
|
||||
} else if (route.type === 'chat') {
|
||||
// Chat list page - show threads started by this user
|
||||
if (!config.bot) {
|
||||
|
||||
Reference in New Issue
Block a user