👁️
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

add temp migration tool

+424 -18
+69 -17
src/routeTree.gen.ts
··· 10 10 11 11 import { Route as rootRouteImport } from './routes/__root' 12 12 import { Route as SigninRouteImport } from './routes/signin' 13 - import { Route as PmDemoRouteImport } from './routes/pm-demo' 13 + import { Route as DevRouteRouteImport } from './routes/dev/route' 14 14 import { Route as IndexRouteImport } from './routes/index' 15 15 import { Route as CardsIndexRouteImport } from './routes/cards/index' 16 16 import { Route as UHandleRouteImport } from './routes/u/$handle' 17 17 import { Route as OauthCallbackRouteImport } from './routes/oauth/callback' 18 + import { Route as DevPmDemoRouteImport } from './routes/dev/pm-demo' 19 + import { Route as DevMigrateRouteImport } from './routes/dev/migrate' 18 20 import { Route as DeckNewRouteImport } from './routes/deck/new' 19 21 import { Route as CardIdRouteImport } from './routes/card/$id' 20 22 import { Route as ProfileDidIndexRouteImport } from './routes/profile/$did/index' ··· 30 32 path: '/signin', 31 33 getParentRoute: () => rootRouteImport, 32 34 } as any) 33 - const PmDemoRoute = PmDemoRouteImport.update({ 34 - id: '/pm-demo', 35 - path: '/pm-demo', 35 + const DevRouteRoute = DevRouteRouteImport.update({ 36 + id: '/dev', 37 + path: '/dev', 36 38 getParentRoute: () => rootRouteImport, 37 39 } as any) 38 40 const IndexRoute = IndexRouteImport.update({ ··· 55 57 path: '/oauth/callback', 56 58 getParentRoute: () => rootRouteImport, 57 59 } as any) 60 + const DevPmDemoRoute = DevPmDemoRouteImport.update({ 61 + id: '/pm-demo', 62 + path: '/pm-demo', 63 + getParentRoute: () => DevRouteRoute, 64 + } as any) 65 + const DevMigrateRoute = DevMigrateRouteImport.update({ 66 + id: '/migrate', 67 + path: '/migrate', 68 + getParentRoute: () => DevRouteRoute, 69 + } as any) 58 70 const DeckNewRoute = DeckNewRouteImport.update({ 59 71 id: '/deck/new', 60 72 path: '/deck/new', ··· 104 116 105 117 export interface FileRoutesByFullPath { 106 118 '/': typeof IndexRoute 107 - '/pm-demo': typeof PmDemoRoute 119 + '/dev': typeof DevRouteRouteWithChildren 108 120 '/signin': typeof SigninRoute 109 121 '/card/$id': typeof CardIdRoute 110 122 '/deck/new': typeof DeckNewRoute 123 + '/dev/migrate': typeof DevMigrateRoute 124 + '/dev/pm-demo': typeof DevPmDemoRoute 111 125 '/oauth/callback': typeof OauthCallbackRoute 112 126 '/u/$handle': typeof UHandleRoute 113 127 '/cards': typeof CardsIndexRoute ··· 121 135 } 122 136 export interface FileRoutesByTo { 123 137 '/': typeof IndexRoute 124 - '/pm-demo': typeof PmDemoRoute 138 + '/dev': typeof DevRouteRouteWithChildren 125 139 '/signin': typeof SigninRoute 126 140 '/card/$id': typeof CardIdRoute 127 141 '/deck/new': typeof DeckNewRoute 142 + '/dev/migrate': typeof DevMigrateRoute 143 + '/dev/pm-demo': typeof DevPmDemoRoute 128 144 '/oauth/callback': typeof OauthCallbackRoute 129 145 '/u/$handle': typeof UHandleRoute 130 146 '/cards': typeof CardsIndexRoute ··· 137 153 export interface FileRoutesById { 138 154 __root__: typeof rootRouteImport 139 155 '/': typeof IndexRoute 140 - '/pm-demo': typeof PmDemoRoute 156 + '/dev': typeof DevRouteRouteWithChildren 141 157 '/signin': typeof SigninRoute 142 158 '/card/$id': typeof CardIdRoute 143 159 '/deck/new': typeof DeckNewRoute 160 + '/dev/migrate': typeof DevMigrateRoute 161 + '/dev/pm-demo': typeof DevPmDemoRoute 144 162 '/oauth/callback': typeof OauthCallbackRoute 145 163 '/u/$handle': typeof UHandleRoute 146 164 '/cards/': typeof CardsIndexRoute ··· 156 174 fileRoutesByFullPath: FileRoutesByFullPath 157 175 fullPaths: 158 176 | '/' 159 - | '/pm-demo' 177 + | '/dev' 160 178 | '/signin' 161 179 | '/card/$id' 162 180 | '/deck/new' 181 + | '/dev/migrate' 182 + | '/dev/pm-demo' 163 183 | '/oauth/callback' 164 184 | '/u/$handle' 165 185 | '/cards' ··· 173 193 fileRoutesByTo: FileRoutesByTo 174 194 to: 175 195 | '/' 176 - | '/pm-demo' 196 + | '/dev' 177 197 | '/signin' 178 198 | '/card/$id' 179 199 | '/deck/new' 200 + | '/dev/migrate' 201 + | '/dev/pm-demo' 180 202 | '/oauth/callback' 181 203 | '/u/$handle' 182 204 | '/cards' ··· 188 210 id: 189 211 | '__root__' 190 212 | '/' 191 - | '/pm-demo' 213 + | '/dev' 192 214 | '/signin' 193 215 | '/card/$id' 194 216 | '/deck/new' 217 + | '/dev/migrate' 218 + | '/dev/pm-demo' 195 219 | '/oauth/callback' 196 220 | '/u/$handle' 197 221 | '/cards/' ··· 206 230 } 207 231 export interface RootRouteChildren { 208 232 IndexRoute: typeof IndexRoute 209 - PmDemoRoute: typeof PmDemoRoute 233 + DevRouteRoute: typeof DevRouteRouteWithChildren 210 234 SigninRoute: typeof SigninRoute 211 235 CardIdRoute: typeof CardIdRoute 212 236 DeckNewRoute: typeof DeckNewRoute ··· 227 251 preLoaderRoute: typeof SigninRouteImport 228 252 parentRoute: typeof rootRouteImport 229 253 } 230 - '/pm-demo': { 231 - id: '/pm-demo' 232 - path: '/pm-demo' 233 - fullPath: '/pm-demo' 234 - preLoaderRoute: typeof PmDemoRouteImport 254 + '/dev': { 255 + id: '/dev' 256 + path: '/dev' 257 + fullPath: '/dev' 258 + preLoaderRoute: typeof DevRouteRouteImport 235 259 parentRoute: typeof rootRouteImport 236 260 } 237 261 '/': { ··· 262 286 preLoaderRoute: typeof OauthCallbackRouteImport 263 287 parentRoute: typeof rootRouteImport 264 288 } 289 + '/dev/pm-demo': { 290 + id: '/dev/pm-demo' 291 + path: '/pm-demo' 292 + fullPath: '/dev/pm-demo' 293 + preLoaderRoute: typeof DevPmDemoRouteImport 294 + parentRoute: typeof DevRouteRoute 295 + } 296 + '/dev/migrate': { 297 + id: '/dev/migrate' 298 + path: '/migrate' 299 + fullPath: '/dev/migrate' 300 + preLoaderRoute: typeof DevMigrateRouteImport 301 + parentRoute: typeof DevRouteRoute 302 + } 265 303 '/deck/new': { 266 304 id: '/deck/new' 267 305 path: '/deck/new' ··· 328 366 } 329 367 } 330 368 369 + interface DevRouteRouteChildren { 370 + DevMigrateRoute: typeof DevMigrateRoute 371 + DevPmDemoRoute: typeof DevPmDemoRoute 372 + } 373 + 374 + const DevRouteRouteChildren: DevRouteRouteChildren = { 375 + DevMigrateRoute: DevMigrateRoute, 376 + DevPmDemoRoute: DevPmDemoRoute, 377 + } 378 + 379 + const DevRouteRouteWithChildren = DevRouteRoute._addFileChildren( 380 + DevRouteRouteChildren, 381 + ) 382 + 331 383 interface ProfileDidDeckRkeyRouteChildren { 332 384 ProfileDidDeckRkeyBulkEditRoute: typeof ProfileDidDeckRkeyBulkEditRoute 333 385 ProfileDidDeckRkeyPlayRoute: typeof ProfileDidDeckRkeyPlayRoute ··· 356 408 357 409 const rootRouteChildren: RootRouteChildren = { 358 410 IndexRoute: IndexRoute, 359 - PmDemoRoute: PmDemoRoute, 411 + DevRouteRoute: DevRouteRouteWithChildren, 360 412 SigninRoute: SigninRoute, 361 413 CardIdRoute: CardIdRoute, 362 414 DeckNewRoute: DeckNewRoute,
+349
src/routes/dev/migrate.tsx
··· 1 + import { createFileRoute } from "@tanstack/react-router"; 2 + import { useId, useState } from "react"; 3 + import { getCardDataProvider } from "@/lib/card-data-provider"; 4 + import type { ScryfallId } from "@/lib/scryfall-types"; 5 + 6 + export const Route = createFileRoute("/dev/migrate")({ 7 + component: MigratePage, 8 + head: () => ({ 9 + meta: [{ title: "Migrate Records | DeckBelcher" }], 10 + }), 11 + }); 12 + 13 + interface OldDeckCard { 14 + scryfallId: string; 15 + quantity: number; 16 + section: string; 17 + tags?: string[]; 18 + } 19 + 20 + interface OldCollectionCardItem { 21 + $type: "com.deckbelcher.collection.list#cardItem"; 22 + scryfallId: string; 23 + addedAt: string; 24 + } 25 + 26 + interface OldCollectionDeckItem { 27 + $type: "com.deckbelcher.collection.list#deckItem"; 28 + deckUri: string; 29 + addedAt: string; 30 + } 31 + 32 + type OldCollectionItem = OldCollectionCardItem | OldCollectionDeckItem; 33 + 34 + interface OldDeckList { 35 + $type: "com.deckbelcher.deck.list"; 36 + name: string; 37 + format?: string; 38 + primer?: unknown; 39 + cards: OldDeckCard[]; 40 + createdAt: string; 41 + updatedAt?: string; 42 + } 43 + 44 + interface OldCollectionList { 45 + $type: "com.deckbelcher.collection.list"; 46 + name: string; 47 + description?: unknown; 48 + items: OldCollectionItem[]; 49 + createdAt: string; 50 + updatedAt?: string; 51 + } 52 + 53 + type OldRecord = OldDeckList | OldCollectionList; 54 + 55 + interface CardRef { 56 + scryfallUri: string; 57 + oracleUri: string; 58 + } 59 + 60 + interface OldCardRefFeature { 61 + $type: "com.deckbelcher.richtext.facet#cardRef"; 62 + scryfallId: string; 63 + } 64 + 65 + interface OldFacet { 66 + index: { byteStart: number; byteEnd: number }; 67 + features: Array<OldCardRefFeature | { $type: string }>; 68 + } 69 + 70 + interface OldBlock { 71 + $type: string; 72 + text?: string; 73 + facets?: OldFacet[]; 74 + } 75 + 76 + interface OldDocument { 77 + content: OldBlock[]; 78 + } 79 + 80 + interface MigrationResult { 81 + success: boolean; 82 + output?: unknown; 83 + errors: string[]; 84 + } 85 + 86 + async function migrateRecord(record: OldRecord): Promise<MigrationResult> { 87 + const errors: string[] = []; 88 + const provider = await getCardDataProvider(); 89 + 90 + async function buildCardRef(scryfallId: string): Promise<CardRef | null> { 91 + const card = await provider.getCardById(scryfallId as ScryfallId); 92 + if (!card) { 93 + errors.push(`Card not found: ${scryfallId}`); 94 + return null; 95 + } 96 + return { 97 + scryfallUri: `scry:${scryfallId}`, 98 + oracleUri: `oracle:${card.oracle_id}`, 99 + }; 100 + } 101 + 102 + async function migrateDocument( 103 + doc: OldDocument | undefined, 104 + ): Promise<OldDocument | undefined> { 105 + if (!doc?.content) return doc; 106 + 107 + const newContent = await Promise.all( 108 + doc.content.map(async (block) => { 109 + if (!block.facets) return block; 110 + 111 + const newFacets = await Promise.all( 112 + block.facets.map(async (facet) => { 113 + const newFeatures = await Promise.all( 114 + facet.features.map(async (feature) => { 115 + if ( 116 + feature.$type === "com.deckbelcher.richtext.facet#cardRef" && 117 + "scryfallId" in feature 118 + ) { 119 + const ref = await buildCardRef( 120 + (feature as OldCardRefFeature).scryfallId, 121 + ); 122 + if (!ref) return feature; 123 + return { $type: feature.$type, ref }; 124 + } 125 + return feature; 126 + }), 127 + ); 128 + return { ...facet, features: newFeatures }; 129 + }), 130 + ); 131 + return { ...block, facets: newFacets }; 132 + }), 133 + ); 134 + 135 + return { content: newContent }; 136 + } 137 + 138 + if (record.$type === "com.deckbelcher.deck.list") { 139 + const newCards = await Promise.all( 140 + record.cards.map(async (card) => { 141 + const ref = await buildCardRef(card.scryfallId); 142 + if (!ref) return null; 143 + return { 144 + ref, 145 + quantity: card.quantity, 146 + section: card.section, 147 + ...(card.tags ? { tags: card.tags } : {}), 148 + }; 149 + }), 150 + ); 151 + 152 + if (newCards.some((c) => c === null)) { 153 + return { success: false, errors }; 154 + } 155 + 156 + const newPrimer = await migrateDocument(record.primer as OldDocument); 157 + 158 + return { 159 + success: true, 160 + output: { 161 + $type: record.$type, 162 + name: record.name, 163 + ...(record.format ? { format: record.format } : {}), 164 + ...(newPrimer ? { primer: newPrimer } : {}), 165 + cards: newCards, 166 + createdAt: record.createdAt, 167 + ...(record.updatedAt ? { updatedAt: record.updatedAt } : {}), 168 + }, 169 + errors, 170 + }; 171 + } 172 + 173 + if (record.$type === "com.deckbelcher.collection.list") { 174 + const newItems = await Promise.all( 175 + record.items.map(async (item) => { 176 + if (item.$type === "com.deckbelcher.collection.list#deckItem") { 177 + return item; 178 + } 179 + const ref = await buildCardRef(item.scryfallId); 180 + if (!ref) return null; 181 + return { 182 + $type: item.$type, 183 + ref, 184 + addedAt: item.addedAt, 185 + }; 186 + }), 187 + ); 188 + 189 + if (newItems.some((i) => i === null)) { 190 + return { success: false, errors }; 191 + } 192 + 193 + const newDescription = await migrateDocument( 194 + record.description as OldDocument, 195 + ); 196 + 197 + return { 198 + success: true, 199 + output: { 200 + $type: record.$type, 201 + name: record.name, 202 + ...(newDescription ? { description: newDescription } : {}), 203 + items: newItems, 204 + createdAt: record.createdAt, 205 + ...(record.updatedAt ? { updatedAt: record.updatedAt } : {}), 206 + }, 207 + errors, 208 + }; 209 + } 210 + 211 + return { success: false, errors: ["Unknown record type"] }; 212 + } 213 + 214 + function MigratePage() { 215 + const inputId = useId(); 216 + const outputId = useId(); 217 + const [input, setInput] = useState(""); 218 + const [output, setOutput] = useState(""); 219 + const [status, setStatus] = useState< 220 + "idle" | "loading" | "success" | "error" 221 + >("idle"); 222 + const [errors, setErrors] = useState<string[]>([]); 223 + 224 + const handleMigrate = async () => { 225 + setStatus("loading"); 226 + setErrors([]); 227 + setOutput(""); 228 + 229 + try { 230 + const record = JSON.parse(input) as OldRecord; 231 + const result = await migrateRecord(record); 232 + 233 + if (result.success) { 234 + setOutput(JSON.stringify(result.output, null, 2)); 235 + setStatus("success"); 236 + } else { 237 + setErrors(result.errors); 238 + setStatus("error"); 239 + } 240 + } catch (e) { 241 + setErrors([e instanceof Error ? e.message : "Unknown error"]); 242 + setStatus("error"); 243 + } 244 + }; 245 + 246 + return ( 247 + <div className="min-h-screen bg-white dark:bg-slate-900 p-8"> 248 + <div className="max-w-4xl mx-auto space-y-6"> 249 + <div> 250 + <h1 className="text-2xl font-bold text-gray-900 dark:text-white mb-2"> 251 + Record Migration 252 + </h1> 253 + <p className="text-gray-600 dark:text-gray-400"> 254 + Migrate old format records (scryfallId) to new format (cardRef with 255 + scryfallUri + oracleUri). 256 + </p> 257 + </div> 258 + 259 + <div className="grid grid-cols-1 md:grid-cols-2 gap-4"> 260 + <div className="space-y-2"> 261 + <label 262 + htmlFor={inputId} 263 + className="block text-sm font-medium text-gray-700 dark:text-gray-300" 264 + > 265 + Old Format (paste JSON) 266 + </label> 267 + <textarea 268 + id={inputId} 269 + value={input} 270 + onChange={(e) => setInput(e.target.value)} 271 + className="w-full h-96 p-3 font-mono text-xs bg-gray-50 dark:bg-slate-800 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 focus:border-transparent text-gray-900 dark:text-gray-100" 272 + placeholder='{"$type": "com.deckbelcher.deck.list", ...}' 273 + /> 274 + </div> 275 + 276 + <div className="space-y-2"> 277 + <label 278 + htmlFor={outputId} 279 + className="block text-sm font-medium text-gray-700 dark:text-gray-300" 280 + > 281 + New Format (output) 282 + </label> 283 + <textarea 284 + id={outputId} 285 + value={output} 286 + readOnly 287 + className="w-full h-96 p-3 font-mono text-xs bg-gray-100 dark:bg-slate-800/50 border border-gray-300 dark:border-slate-600 rounded-lg text-gray-900 dark:text-gray-100" 288 + placeholder="Migrated output will appear here..." 289 + /> 290 + </div> 291 + </div> 292 + 293 + <div className="flex items-center gap-4"> 294 + <button 295 + type="button" 296 + onClick={handleMigrate} 297 + disabled={status === "loading" || !input.trim()} 298 + className="px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 dark:disabled:bg-gray-600 text-white font-medium rounded-lg transition-colors" 299 + > 300 + {status === "loading" ? "Migrating..." : "Migrate"} 301 + </button> 302 + 303 + {status === "success" && ( 304 + <span className="text-green-600 dark:text-green-400 text-sm"> 305 + Migration successful 306 + </span> 307 + )} 308 + </div> 309 + 310 + {errors.length > 0 && ( 311 + <div className="p-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg"> 312 + <h3 className="text-sm font-medium text-red-800 dark:text-red-300 mb-2"> 313 + Errors 314 + </h3> 315 + <ul className="list-disc list-inside text-sm text-red-700 dark:text-red-400 space-y-1"> 316 + {errors.map((err) => ( 317 + <li key={err}>{err}</li> 318 + ))} 319 + </ul> 320 + </div> 321 + )} 322 + 323 + <details className="text-sm"> 324 + <summary className="cursor-pointer text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 font-medium"> 325 + Migration Details 326 + </summary> 327 + <div className="mt-2 p-4 bg-gray-50 dark:bg-slate-800 rounded-lg text-gray-700 dark:text-gray-300 space-y-2"> 328 + <p> 329 + <strong>Old format:</strong>{" "} 330 + <code className="text-xs bg-gray-200 dark:bg-slate-700 px-1 rounded"> 331 + scryfallId: "uuid" 332 + </code> 333 + </p> 334 + <p> 335 + <strong>New format:</strong>{" "} 336 + <code className="text-xs bg-gray-200 dark:bg-slate-700 px-1 rounded"> 337 + {`ref: { scryfallUri: "scry:uuid", oracleUri: "oracle:uuid" }`} 338 + </code> 339 + </p> 340 + <p className="text-xs text-gray-500 dark:text-gray-400"> 341 + Supports both com.deckbelcher.deck.list and 342 + com.deckbelcher.collection.list records. 343 + </p> 344 + </div> 345 + </details> 346 + </div> 347 + </div> 348 + ); 349 + }
+5
src/routes/dev/route.tsx
··· 1 + import { createFileRoute, Outlet } from "@tanstack/react-router"; 2 + 3 + export const Route = createFileRoute("/dev")({ 4 + component: () => <Outlet />, 5 + });
+1 -1
src/routes/pm-demo.tsx src/routes/dev/pm-demo.tsx
··· 5 5 import { lexiconToTree, treeToLexicon } from "@/lib/richtext-convert"; 6 6 import type { PMDocJSON } from "@/lib/useProseMirror"; 7 7 8 - export const Route = createFileRoute("/pm-demo")({ 8 + export const Route = createFileRoute("/dev/pm-demo")({ 9 9 component: ProseMirrorDemo, 10 10 head: () => ({ 11 11 meta: [{ title: "Editor Demo | DeckBelcher" }],