-

+
${effectsHtml}
@@ -46,7 +124,9 @@ function renderRseItem(item: RseItem, type: 'item' | 'character'): string {
// Render RSE page
export function renderRsePage(
collection: RseCollection | null,
- handle: string
+ handle: string,
+ userCards: UserCard[] = [],
+ adminData: CardAdminData | null = null
): string {
const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self`
@@ -70,19 +150,14 @@ export function renderRsePage(
const totalItems = items.length
const uniqueChars = characters.filter(c => c.unique).length
- // Sort by unique > id
- const sortedChars = [...characters].sort((a, b) => {
- if (a.unique !== b.unique) return a.unique ? -1 : 1
- return a.id - b.id
- })
+ // Sort by id
+ const sortedChars = [...characters].sort((a, b) => a.id - b.id)
+ const sortedItems = [...items].sort((a, b) => a.id - b.id)
- const sortedItems = [...items].sort((a, b) => {
- if (a.unique !== b.unique) return a.unique ? -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('')
+ const charsHtml = sortedChars.map(c =>
+ renderCharacterSection(c, userCards, adminData)
+ ).join('')
+ const itemsHtml = sortedItems.map(i => renderRseItem(i)).join('')
return `
@@ -107,7 +182,7 @@ export function renderRsePage(
${charsHtml ? `
Characters
-
${charsHtml}
+
${charsHtml}
` : ''}
${itemsHtml ? `
Items
diff --git a/src/web/lib/api.ts b/src/web/lib/api.ts
index b6e3752..755efa4 100644
--- a/src/web/lib/api.ts
+++ b/src/web/lib/api.ts
@@ -679,6 +679,55 @@ export interface LinkCollection {
updatedAt?: string
}
+// Card admin data types
+export interface CardAdminEntry {
+ id: number
+ character: number
+ name: { ja: string; en: string }
+ text: { ja: string; en: string }
+ cp: string
+ effect: string
+ key?: string | null
+}
+
+export interface CardAdminData {
+ gacha: { pickup: number; rate: { rare: number; pickup: number } }
+ card: CardAdminEntry[]
+}
+
+// Get card admin data (ai.syui.card.admin)
+export async function getCardAdmin(did: string): Promise
{
+ const collection = 'ai.syui.card.admin'
+
+ // 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 CardAdminData
+ }
+ } 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 CardAdminData
+ }
+ } catch {
+ // Failed
+ }
+ return null
+}
+
// Get user's links (ai.syui.at.link)
export async function getLinks(did: string): Promise {
const collection = 'ai.syui.at.link'
diff --git a/src/web/main.ts b/src/web/main.ts
index 459034f..0ee84a6 100644
--- a/src/web/main.ts
+++ b/src/web/main.ts
@@ -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, getRse, getLinks } from './lib/api'
+import { getConfig, resolveHandle, getProfile, getPosts, getPost, describeRepo, listRecords, getRecord, getPds, getNetworks, getChatMessages, getCards, getCardAdmin, getRse, getLinks } from './lib/api'
import { parseRoute, onRouteChange, navigate, type Route } from './lib/router'
import { login, logout, handleCallback, restoreSession, isLoggedIn, getLoggedInHandle, getLoggedInDid, deleteRecord, updatePost, updateChat, updateLinks } from './lib/auth'
import { validateRecord } from './lib/lexicon'
@@ -253,9 +253,16 @@ async function render(route: Route): Promise {
html += ``
} else if (route.type === 'rse') {
- // RSE page
- const rseData = await getRse(did)
- html += `${renderRsePage(rseData, handle)}
`
+ // RSE page with character cards
+ const cardCollection = config.cardCollection || 'ai.syui.card.user'
+ const adminDid = config.bot?.did || config.did || did
+ const [rseData, cards, adminData] = await Promise.all([
+ getRse(did),
+ getCards(did, cardCollection),
+ getCardAdmin(adminDid)
+ ])
+ const userCards = cards?.card || []
+ html += `${renderRsePage(rseData, handle, userCards, adminData)}
`
html += ``
} else if (route.type === 'link') {
diff --git a/src/web/styles/card.css b/src/web/styles/card.css
index 0cd61ed..67eec6b 100644
--- a/src/web/styles/card.css
+++ b/src/web/styles/card.css
@@ -522,3 +522,127 @@
color: var(--text-secondary, #aaa);
}
}
+
+/* RSE section title */
+.rse-section-title {
+ margin: 24px 0 12px;
+ font-size: 1.1em;
+ color: var(--text-primary, #333);
+}
+
+/* RSE character sections */
+.rse-characters {
+ display: flex;
+ flex-direction: column;
+ gap: 32px;
+}
+
+.rse-character-section {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 16px;
+}
+
+.rse-character-main {
+ display: flex;
+ justify-content: center;
+}
+
+.rse-character-main .card-wrapper {
+ width: 250px;
+ max-width: 250px;
+}
+
+/* RSE card grid (smaller cards below character) */
+.rse-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
+ gap: 16px;
+ width: 100%;
+ max-width: 500px;
+ padding: 16px;
+ background: rgba(128, 128, 128, 0.08);
+ border-radius: 12px;
+}
+
+.rse-card-grid .card-item {
+ max-width: 140px;
+}
+
+.rse-card-grid .card-wrapper {
+ max-width: 140px;
+}
+
+/* Card info (below card) */
+.card-info {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 4px;
+ margin-top: 8px;
+ text-align: center;
+}
+
+.card-info-header {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ width: 100%;
+ position: relative;
+}
+
+.card-info-name {
+ font-size: 13px;
+ font-weight: 600;
+ color: var(--text-primary, #333);
+}
+
+.card-info-header .card-key-btn {
+ position: absolute;
+ right: 0;
+}
+
+.card-info-text {
+ font-size: 11px;
+ color: var(--text-secondary, #666);
+ line-height: 1.4;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.card-key-btn {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ border: none;
+ color: white;
+ padding: 3px 10px;
+ border-radius: 10px;
+ font-size: 10px;
+ font-weight: 600;
+ cursor: default;
+ box-shadow: 0 2px 6px rgba(102, 126, 234, 0.3);
+ text-transform: uppercase;
+ letter-spacing: 0.5px;
+}
+
+/* Dark mode for RSE */
+@media (prefers-color-scheme: dark) {
+ .rse-section-title {
+ color: var(--text-primary, #eee);
+ }
+
+ .rse-card-grid {
+ background: rgba(255, 255, 255, 0.05);
+ }
+
+ .card-info-name {
+ color: var(--text-primary, #eee);
+ }
+
+ .card-info-text {
+ color: var(--text-secondary, #aaa);
+ }
+}