add card.admin, rse.admin
This commit is contained in:
@@ -14,6 +14,33 @@ export interface CardCollection {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
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 current language
|
||||
function getLang(): string {
|
||||
return localStorage.getItem('preferredLang') || 'ja'
|
||||
}
|
||||
|
||||
// Get localized text
|
||||
function getLocalizedText(obj: { ja: string; en: string } | undefined): string {
|
||||
if (!obj) return ''
|
||||
const lang = getLang()
|
||||
return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || ''
|
||||
}
|
||||
|
||||
// Get rarity class name
|
||||
function getRarityClass(card: UserCard): string {
|
||||
if (card.unique) return 'unique'
|
||||
@@ -21,9 +48,13 @@ function getRarityClass(card: UserCard): string {
|
||||
return ''
|
||||
}
|
||||
|
||||
|
||||
// Render single card with optional count badge
|
||||
export function renderCard(card: UserCard, baseUrl: string = '/card', count?: number): string {
|
||||
// Render single card with optional count badge and admin info
|
||||
export function renderCard(
|
||||
card: UserCard,
|
||||
baseUrl: string = '/card',
|
||||
count?: number,
|
||||
adminEntry?: CardAdminEntry
|
||||
): string {
|
||||
const rarityClass = getRarityClass(card)
|
||||
const imageUrl = `${baseUrl}/${card.id}.webp`
|
||||
|
||||
@@ -34,6 +65,21 @@ export function renderCard(card: UserCard, baseUrl: string = '/card', count?: nu
|
||||
|
||||
const countBadge = count && count > 1 ? `<span class="card-count">x${count}</span>` : ''
|
||||
|
||||
// Admin info (name, key, text)
|
||||
const name = adminEntry ? getLocalizedText(adminEntry.name) : ''
|
||||
const text = adminEntry ? getLocalizedText(adminEntry.text) : ''
|
||||
const key = adminEntry?.key || ''
|
||||
|
||||
const infoHtml = (name || text || key) ? `
|
||||
<div class="card-info">
|
||||
<div class="card-info-header">
|
||||
<span class="card-info-name">${name}</span>
|
||||
${key ? `<button class="card-key-btn">${key}</button>` : ''}
|
||||
</div>
|
||||
${text ? `<div class="card-info-text">${text}</div>` : ''}
|
||||
</div>
|
||||
` : ''
|
||||
|
||||
return `
|
||||
<div class="card-item">
|
||||
<div class="card-wrapper" data-card-id="${card.id}" data-cid="${card.cid}">
|
||||
@@ -46,6 +92,7 @@ export function renderCard(card: UserCard, baseUrl: string = '/card', count?: nu
|
||||
<div class="card-detail">
|
||||
<span class="card-cp">${card.cp}</span>
|
||||
</div>
|
||||
${infoHtml}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -109,13 +156,9 @@ export function renderCardPage(
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by unique first, then rarity (desc), then by id
|
||||
// Sort by id
|
||||
const sortedGroups = Array.from(cardGroups.values())
|
||||
.sort((a, b) => {
|
||||
if (a.card.unique !== b.card.unique) return a.card.unique ? -1 : 1
|
||||
if (b.card.rare !== a.card.rare) return b.card.rare - a.card.rare
|
||||
return a.card.id - b.card.id
|
||||
})
|
||||
.sort((a, b) => a.card.id - b.card.id)
|
||||
|
||||
const cardsHtml = sortedGroups.map(({ card, count }) => {
|
||||
return renderCard(card, '/card', count)
|
||||
|
||||
@@ -1,4 +1,15 @@
|
||||
// RSE display component for ai.syui.rse.user collection
|
||||
import { renderCard, type UserCard, type CardAdminEntry, type CardAdminData } from './card'
|
||||
|
||||
export interface RseAdminItem {
|
||||
id: number
|
||||
name: string
|
||||
text: { ja: string; en: string }
|
||||
}
|
||||
|
||||
export interface RseAdminData {
|
||||
item: RseAdminItem[]
|
||||
}
|
||||
|
||||
export interface RseItem {
|
||||
id: number
|
||||
@@ -14,31 +25,135 @@ export interface RseCollection {
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Get current language
|
||||
function getLang(): string {
|
||||
return localStorage.getItem('preferredLang') || 'ja'
|
||||
}
|
||||
|
||||
// Get localized text
|
||||
function getLocalizedText(obj: { ja: string; en: string } | undefined): string {
|
||||
if (!obj) return ''
|
||||
const lang = getLang()
|
||||
return obj[lang as 'ja' | 'en'] || obj.ja || obj.en || ''
|
||||
}
|
||||
|
||||
// Get rarity class from unique flag
|
||||
function getRarityClass(item: RseItem): string {
|
||||
if (item.unique) return 'unique'
|
||||
return ''
|
||||
}
|
||||
|
||||
// Render single item/character
|
||||
function renderRseItem(item: RseItem, type: 'item' | 'character'): string {
|
||||
// Get cards for a character (character 0 = cards 0-99, character 1 = cards 100-199, etc.)
|
||||
function getCardsForCharacter(
|
||||
characterId: number,
|
||||
userCards: UserCard[],
|
||||
adminData: CardAdminData | null
|
||||
): { card: UserCard; adminEntry?: CardAdminEntry }[] {
|
||||
const minId = characterId * 100
|
||||
const maxId = minId + 99
|
||||
|
||||
// Build admin lookup map
|
||||
const adminMap = new Map<number, CardAdminEntry>()
|
||||
if (adminData?.card) {
|
||||
for (const entry of adminData.card) {
|
||||
adminMap.set(entry.id, entry)
|
||||
}
|
||||
}
|
||||
|
||||
// Filter and dedupe cards for this character
|
||||
const cardGroups = new Map<number, UserCard>()
|
||||
for (const card of userCards) {
|
||||
if (card.id >= minId && card.id <= maxId) {
|
||||
const existing = cardGroups.get(card.id)
|
||||
if (!existing || card.cp > existing.cp || card.unique) {
|
||||
cardGroups.set(card.id, card)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by ID and add admin entries
|
||||
return Array.from(cardGroups.values())
|
||||
.sort((a, b) => a.id - b.id)
|
||||
.map(card => ({
|
||||
card,
|
||||
adminEntry: adminMap.get(card.id)
|
||||
}))
|
||||
}
|
||||
|
||||
// Render character section with its cards below
|
||||
function renderCharacterSection(
|
||||
item: RseItem,
|
||||
userCards: UserCard[],
|
||||
adminData: CardAdminData | null
|
||||
): string {
|
||||
const rarityClass = getRarityClass(item)
|
||||
const effectsHtml = rarityClass ? `
|
||||
<div class="card-status pattern-${rarityClass}"></div>
|
||||
<div class="card-status color-${rarityClass}"></div>
|
||||
` : ''
|
||||
|
||||
// Get cards for this character
|
||||
const characterCards = getCardsForCharacter(item.id, userCards, adminData)
|
||||
const cardsHtml = characterCards.map(({ card, adminEntry }) =>
|
||||
renderCard(card, '/card', undefined, adminEntry)
|
||||
).join('')
|
||||
|
||||
return `
|
||||
<div class="rse-character-section">
|
||||
<div class="rse-character-main">
|
||||
<div class="card-item">
|
||||
<div class="card-wrapper">
|
||||
<div class="card-reflection">
|
||||
<img src="/rse/character/${item.id}.webp" alt="character ${item.id}" loading="lazy" />
|
||||
</div>
|
||||
${effectsHtml}
|
||||
</div>
|
||||
<div class="card-detail">
|
||||
<span class="card-cp">${item.cp}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${characterCards.length > 0 ? `
|
||||
<div class="rse-card-grid">${cardsHtml}</div>
|
||||
` : ''}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
|
||||
// Render single item
|
||||
function renderRseItem(item: RseItem, rseAdminData: RseAdminData | null): string {
|
||||
const rarityClass = getRarityClass(item)
|
||||
const effectsHtml = rarityClass ? `
|
||||
<div class="card-status pattern-${rarityClass}"></div>
|
||||
<div class="card-status color-${rarityClass}"></div>
|
||||
` : ''
|
||||
|
||||
// Get admin entry for this item
|
||||
const adminEntry = rseAdminData?.item?.find(i => i.id === item.id)
|
||||
const name = adminEntry?.name || ''
|
||||
const text = adminEntry ? getLocalizedText(adminEntry.text) : ''
|
||||
|
||||
const infoHtml = (name || text) ? `
|
||||
<div class="card-info">
|
||||
<div class="card-info-header">
|
||||
<span class="card-info-name">${name}</span>
|
||||
</div>
|
||||
${text ? `<div class="card-info-text">${text}</div>` : ''}
|
||||
</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" />
|
||||
<img src="/rse/item/${item.id}.webp" alt="item ${item.id}" loading="lazy" />
|
||||
</div>
|
||||
${effectsHtml}
|
||||
</div>
|
||||
<div class="card-detail">
|
||||
<span class="card-cp">${item.cp}</span>
|
||||
</div>
|
||||
${infoHtml}
|
||||
</div>
|
||||
`
|
||||
}
|
||||
@@ -46,7 +161,10 @@ 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,
|
||||
rseAdminData: RseAdminData | null = null
|
||||
): string {
|
||||
const jsonUrl = `/@${handle}/at/collection/ai.syui.rse.user/self`
|
||||
|
||||
@@ -70,19 +188,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, rseAdminData)).join('')
|
||||
|
||||
return `
|
||||
<div class="card-page">
|
||||
@@ -107,7 +220,7 @@ export function renderRsePage(
|
||||
</div>
|
||||
${charsHtml ? `
|
||||
<h3 class="rse-section-title">Characters</h3>
|
||||
<div class="card-grid">${charsHtml}</div>
|
||||
<div class="rse-characters">${charsHtml}</div>
|
||||
` : ''}
|
||||
${itemsHtml ? `
|
||||
<h3 class="rse-section-title">Items</h3>
|
||||
|
||||
@@ -679,6 +679,105 @@ 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<CardAdminData | null> {
|
||||
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
|
||||
}
|
||||
|
||||
// RSE admin data types
|
||||
export interface RseAdminItem {
|
||||
id: number
|
||||
name: string
|
||||
text: { ja: string; en: string }
|
||||
}
|
||||
|
||||
export interface RseAdminData {
|
||||
item: RseAdminItem[]
|
||||
ability: unknown[]
|
||||
character: unknown[]
|
||||
system: unknown[]
|
||||
collection: unknown[]
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
// Get RSE admin data (ai.syui.rse.admin)
|
||||
export async function getRseAdmin(did: string): Promise<RseAdminData | null> {
|
||||
const collection = 'ai.syui.rse.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 RseAdminData
|
||||
}
|
||||
} 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 RseAdminData
|
||||
}
|
||||
} catch {
|
||||
// Failed
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// Get user's links (ai.syui.at.link)
|
||||
export async function getLinks(did: string): Promise<LinkCollection | null> {
|
||||
const collection = 'ai.syui.at.link'
|
||||
|
||||
@@ -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, getRseAdmin, 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,17 @@ async function render(route: Route): Promise<void> {
|
||||
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>`
|
||||
// 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, rseAdminData] = await Promise.all([
|
||||
getRse(did),
|
||||
getCards(did, cardCollection),
|
||||
getCardAdmin(adminDid),
|
||||
getRseAdmin(adminDid)
|
||||
])
|
||||
const userCards = cards?.card || []
|
||||
html += `<div id="content">${renderRsePage(rseData, handle, userCards, adminData, rseAdminData)}</div>`
|
||||
html += `<nav class="back-nav"><a href="/@${handle}">${handle}</a></nav>`
|
||||
|
||||
} else if (route.type === 'link') {
|
||||
|
||||
@@ -522,3 +522,133 @@
|
||||
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;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rse-character-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.rse-character-main {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.rse-character-main .card-wrapper {
|
||||
width: 250px;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
/* RSE card grid (cards below character) */
|
||||
.rse-card-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
padding: 20px;
|
||||
background: rgba(128, 128, 128, 0.08);
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.rse-card-grid .card-wrapper {
|
||||
max-width: 180px;
|
||||
}
|
||||
|
||||
.rse-card-grid .card-info-name {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.rse-card-grid .card-info-text {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rse-card-grid .card-key-btn {
|
||||
font-size: 9px;
|
||||
padding: 2px 8px;
|
||||
}
|
||||
|
||||
/* 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.5;
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user