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>
146 lines
3.6 KiB
TypeScript
146 lines
3.6 KiB
TypeScript
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;
|
|
},
|
|
});
|