// Card migration component - migrate from api.syui.ai to ATProto import { getOldApiUserByDid, getOldApiCards, getCardOldRecordKey, generateChecksum, type OldApiUser, type OldApiCard } from '../lib/api' import { saveMigratedCardData, isLoggedIn, getLoggedInDid } from '../lib/auth' export interface MigrationState { loading: boolean oldApiUser: OldApiUser | null oldApiCards: OldApiCard[] hasMigrated: boolean migratedRkey: string | null error: string | null } // Check migration status for a user export async function checkMigrationStatus(did: string): Promise { const state: MigrationState = { loading: true, oldApiUser: null, oldApiCards: [], hasMigrated: false, migratedRkey: null, error: null } try { // Check if already migrated state.migratedRkey = await getCardOldRecordKey(did) state.hasMigrated = state.migratedRkey !== null // Check if user exists in api.syui.ai state.oldApiUser = await getOldApiUserByDid(did) if (state.oldApiUser) { // Load cards state.oldApiCards = await getOldApiCards(state.oldApiUser.id) } } catch (err) { state.error = String(err) } state.loading = false return state } // Convert datetime to ISO UTC format function toUtcDatetime(dateStr: string): string { try { return new Date(dateStr).toISOString() } catch { return new Date().toISOString() } } // Perform migration export async function performMigration(user: OldApiUser, cards: OldApiCard[]): Promise { const checksum = generateChecksum(user, cards) // Convert user data (only required + used fields, matching lexicon types) // Note: ATProto doesn't support float, so planet is converted to integer const userData = { username: user.username, did: user.did, aiten: Math.floor(user.aiten), fav: Math.floor(user.fav), coin: Math.floor(user.coin), planet: Math.floor(user.planet), createdAt: toUtcDatetime(user.created_at), updatedAt: toUtcDatetime(user.updated_at), } // Convert card data (only required + used fields) const cardData = cards.map(c => ({ id: c.id, card: c.card, cp: c.cp, status: c.status || 'normal', skill: c.skill || 'normal', createdAt: toUtcDatetime(c.created_at), })) const result = await saveMigratedCardData(userData, cardData, checksum) return result !== null } // Render migration icon for profile (shown when user has api.syui.ai account) export function renderMigrationIcon(handle: string, hasOldApi: boolean, hasMigrated: boolean): string { if (!hasOldApi) return '' const icon = hasMigrated ? '/service/ai.syui.card.png' : '/service/ai.syui.card.old.png' const title = hasMigrated ? 'Card (Migrated)' : 'Card Migration Available' return ` Card Migration ` } // Convert status to rarity function statusToRare(status: string): number { switch (status) { case 'super': return 3 // unique case 'shiny': return 2 // shiny (assumed from skill or special status) case 'first': return 1 // rare default: return 0 // normal } } // Render migration page (simplified) export function renderMigrationPage( state: MigrationState, handle: string, isOwner: boolean ): string { const { oldApiUser, oldApiCards, hasMigrated, migratedRkey, error } = state const jsonUrl = migratedRkey ? `/@${handle}/at/collection/ai.syui.card.old/${migratedRkey}` : `/@${handle}/at/collection/ai.syui.card.old` if (error) { return `
Error: ${error}
` } if (!oldApiUser) { return `

No api.syui.ai account found

` } // Button or migrated status let buttonHtml = '' if (hasMigrated) { buttonHtml = `✓ migrated` } else if (isOwner && isLoggedIn()) { buttonHtml = `` } // Card grid (same style as /card page) const cardGroups = new Map() for (const card of oldApiCards) { const existing = cardGroups.get(card.card) const rare = statusToRare(card.status) if (existing) { existing.count++ if (card.cp > existing.maxCp) existing.maxCp = card.cp if (rare > existing.rare) existing.rare = rare } else { cardGroups.set(card.card, { card, count: 1, maxCp: card.cp, rare }) } } const sortedGroups = Array.from(cardGroups.values()) .sort((a, b) => a.card.card - b.card.card) const cardsHtml = sortedGroups.map(({ card, count, maxCp, rare }) => { const rarityClass = rare === 3 ? 'unique' : rare === 2 ? 'shiny' : rare === 1 ? 'rare' : '' const effectsHtml = rarityClass ? `
` : '' const countBadge = count > 1 ? `x${count}` : '' return `
Card ${card.card}
${effectsHtml} ${countBadge}
${maxCp}
` }).join('') return `
${oldApiUser.username} User
${oldApiUser.aiten.toLocaleString()} Aiten
${Math.floor(oldApiUser.planet).toLocaleString()} Planet
json ${buttonHtml}
${cardsHtml}
` } // Setup migration button handler export function setupMigrationButton( oldApiUser: OldApiUser, oldApiCards: OldApiCard[], onSuccess: () => void ): void { const btn = document.getElementById('migrate-btn') if (!btn) return btn.addEventListener('click', async (e) => { e.preventDefault() e.stopPropagation() const loggedInDid = getLoggedInDid() if (!loggedInDid || loggedInDid !== oldApiUser.did) { alert('DID mismatch. Please login with the correct account.') return } if (!confirm(`Migrate ${oldApiCards.length} cards to ATProto?`)) { return } btn.textContent = 'Migrating...' ;(btn as HTMLButtonElement).disabled = true try { const success = await performMigration(oldApiUser, oldApiCards) if (success) { alert('Migration successful!') onSuccess() } else { alert('Migration failed.') } } catch (err) { alert('Migration error: ' + err) } btn.textContent = 'Migrate to ATProto' ;(btn as HTMLButtonElement).disabled = false }) }