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>
58 lines
1.4 KiB
TypeScript
58 lines
1.4 KiB
TypeScript
import { v } from "convex/values";
|
|
import { query, mutation } from "./_generated/server";
|
|
|
|
// Get recent scan history
|
|
export const getRecent = query({
|
|
args: { userId: v.id("users"), limit: v.optional(v.number()) },
|
|
handler: async (ctx, { userId, limit = 50 }) => {
|
|
const history = await ctx.db
|
|
.query("scanHistory")
|
|
.withIndex("by_user", (q) => q.eq("userId", userId))
|
|
.order("desc")
|
|
.take(limit);
|
|
|
|
// Enrich with card data
|
|
const enriched = await Promise.all(
|
|
history.map(async (entry) => {
|
|
const card = entry.cardId ? await ctx.db.get(entry.cardId) : null;
|
|
return {
|
|
...entry,
|
|
card,
|
|
};
|
|
})
|
|
);
|
|
|
|
return enriched;
|
|
},
|
|
});
|
|
|
|
// Record a scan
|
|
export const record = mutation({
|
|
args: {
|
|
userId: v.id("users"),
|
|
cardId: v.optional(v.id("cards")),
|
|
confidence: v.number(),
|
|
addedToCollection: v.boolean(),
|
|
},
|
|
handler: async (ctx, args) => {
|
|
return await ctx.db.insert("scanHistory", {
|
|
...args,
|
|
scannedAt: Date.now(),
|
|
});
|
|
},
|
|
});
|
|
|
|
// Clear scan history
|
|
export const clear = mutation({
|
|
args: { userId: v.id("users") },
|
|
handler: async (ctx, { userId }) => {
|
|
const entries = await ctx.db
|
|
.query("scanHistory")
|
|
.withIndex("by_user", (q) => q.eq("userId", userId))
|
|
.collect();
|
|
|
|
for (const entry of entries) {
|
|
await ctx.db.delete(entry._id);
|
|
}
|
|
},
|
|
});
|