Migrate from .NET MAUI to Expo + Convex
Complete rewrite of Scry using TypeScript stack:
- Expo/React Native with Expo Router (file-based routing)
- Convex backend (serverless functions + real-time database)
- Adaptive camera system (expo-camera in Expo Go, Vision Camera in production)
- React Native Skia + fast-opencv for image processing
- GDPR-compliant auth setup with Zitadel OIDC (pending configuration)
Key features:
- Card recognition pipeline ported to TypeScript
- Perceptual hashing (192-bit color pHash)
- CLAHE preprocessing for lighting normalization
- Local SQLite cache with Convex sync
- Collection management with offline support
Removes all .NET/MAUI code (src/, test/, tools/).
💘 Generated with Crush
Assisted-by: Claude Opus 4.5 via Crush <crush@charm.land>
This commit is contained in:
parent
56499d5af9
commit
83ab4df537
138 changed files with 19136 additions and 7681 deletions
146
convex/cards.ts
Normal file
146
convex/cards.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { v } from "convex/values";
|
||||
import { query, mutation } from "./_generated/server";
|
||||
|
||||
// Get all cards (for local caching)
|
||||
export const list = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
return await ctx.db.query("cards").collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Get cards updated after a timestamp (for incremental sync)
|
||||
export const listUpdatedAfter = query({
|
||||
args: { since: v.number() },
|
||||
handler: async (ctx, { since }) => {
|
||||
return await ctx.db
|
||||
.query("cards")
|
||||
.withIndex("by_updated", (q) => q.gt("updatedAt", since))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Get a single card by Scryfall ID
|
||||
export const getByScryfallId = query({
|
||||
args: { scryfallId: v.string() },
|
||||
handler: async (ctx, { scryfallId }) => {
|
||||
return await ctx.db
|
||||
.query("cards")
|
||||
.withIndex("by_scryfall", (q) => q.eq("scryfallId", scryfallId))
|
||||
.first();
|
||||
},
|
||||
});
|
||||
|
||||
// Get cards by oracle ID (all printings of a card)
|
||||
export const getByOracleId = query({
|
||||
args: { oracleId: v.string() },
|
||||
handler: async (ctx, { oracleId }) => {
|
||||
return await ctx.db
|
||||
.query("cards")
|
||||
.withIndex("by_oracle", (q) => q.eq("oracleId", oracleId))
|
||||
.collect();
|
||||
},
|
||||
});
|
||||
|
||||
// Search cards by name
|
||||
export const searchByName = query({
|
||||
args: { name: v.string() },
|
||||
handler: async (ctx, { name }) => {
|
||||
const cards = await ctx.db.query("cards").collect();
|
||||
const lowerName = name.toLowerCase();
|
||||
return cards.filter((card) =>
|
||||
card.name.toLowerCase().includes(lowerName)
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
// Get total card count
|
||||
export const count = query({
|
||||
args: {},
|
||||
handler: async (ctx) => {
|
||||
const cards = await ctx.db.query("cards").collect();
|
||||
return cards.length;
|
||||
},
|
||||
});
|
||||
|
||||
// Insert a card (used by migration script)
|
||||
export const insert = mutation({
|
||||
args: {
|
||||
scryfallId: v.string(),
|
||||
oracleId: v.string(),
|
||||
name: v.string(),
|
||||
setCode: v.string(),
|
||||
collectorNumber: v.string(),
|
||||
rarity: v.string(),
|
||||
artist: v.optional(v.string()),
|
||||
imageUri: v.optional(v.string()),
|
||||
hash: v.bytes(),
|
||||
hashVersion: v.number(),
|
||||
},
|
||||
handler: async (ctx, args) => {
|
||||
// Check if card already exists
|
||||
const existing = await ctx.db
|
||||
.query("cards")
|
||||
.withIndex("by_scryfall", (q) => q.eq("scryfallId", args.scryfallId))
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
// Update existing card
|
||||
await ctx.db.patch(existing._id, {
|
||||
...args,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
return existing._id;
|
||||
}
|
||||
|
||||
// Insert new card
|
||||
return await ctx.db.insert("cards", {
|
||||
...args,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// Batch insert cards
|
||||
export const insertBatch = mutation({
|
||||
args: {
|
||||
cards: v.array(
|
||||
v.object({
|
||||
scryfallId: v.string(),
|
||||
oracleId: v.string(),
|
||||
name: v.string(),
|
||||
setCode: v.string(),
|
||||
collectorNumber: v.string(),
|
||||
rarity: v.string(),
|
||||
artist: v.optional(v.string()),
|
||||
imageUri: v.optional(v.string()),
|
||||
hash: v.bytes(),
|
||||
hashVersion: v.number(),
|
||||
})
|
||||
),
|
||||
},
|
||||
handler: async (ctx, { cards }) => {
|
||||
const results = [];
|
||||
for (const card of cards) {
|
||||
const existing = await ctx.db
|
||||
.query("cards")
|
||||
.withIndex("by_scryfall", (q) => q.eq("scryfallId", card.scryfallId))
|
||||
.first();
|
||||
|
||||
if (existing) {
|
||||
await ctx.db.patch(existing._id, {
|
||||
...card,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
results.push(existing._id);
|
||||
} else {
|
||||
const id = await ctx.db.insert("cards", {
|
||||
...card,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
results.push(id);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue