Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions convex/credits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,4 +127,35 @@ export const markSent = mutation({
sentAt: Date.now(),
});
},
});

// List all sent credits with person info (for redemption checking)
export const listSentWithPerson = query({
handler: async (ctx) => {
const credits = await ctx.db
.query("credits")
.withIndex("by_status", (q) => q.eq("status", "sent"))
.collect();

// Join with people to get names
return Promise.all(
credits.map(async (credit) => {
const person = credit.assignedTo
? await ctx.db.get(credit.assignedTo)
: null;
return { ...credit, person };
})
);
},
});

// Mark credit as redeemed
export const markRedeemed = mutation({
args: { creditId: v.id("credits") },
handler: async (ctx, args) => {
await ctx.db.patch(args.creditId, {
status: "redeemed",
checkedAt: Date.now(),
});
},
});
3 changes: 2 additions & 1 deletion convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,6 @@ export default defineSchema({
sentAt: v.optional(v.number()),
})
.index("by_status", ["status"])
.index("by_code", ["code"]),
.index("by_code", ["code"])
.index("by_assignedTo", ["assignedTo"]),
});
13 changes: 12 additions & 1 deletion src/cli.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { ConvexProvider, ConvexReactClient } from "convex/react";
import SendCredits from "./screens/SendCredits.js";
import UploadCredits from "./screens/UploadCredits.js";
import UploadAttendees from "./screens/UploadAttendees.js";
import CheckRedemptions from "./screens/CheckRedemptions.js";
import ModeSelector from "./components/ModeSelector.js";
import { StorageProvider, useStorage, type StorageMode } from "./context/StorageContext.js";

Expand All @@ -20,7 +21,7 @@ const hasCloudConfig = Boolean(
// Only create Convex client if we have the URL
const convex = new ConvexReactClient(process.env.CONVEX_URL!);

type Screen = "menu" | "send" | "upload" | "attendees";
type Screen = "menu" | "send" | "upload" | "attendees" | "check";

const clearScreen = () => {
process.stdout.write("\x1B[2J\x1B[0f");
Expand Down Expand Up @@ -66,13 +67,17 @@ const MainMenu = () => {
} else if (input === "3") {
clearScreen();
setScreen("attendees");
} else if (input === "4") {
clearScreen();
setScreen("check");
}
});

const menuItems = [
{ label: "Send Cursor Credits", value: "send" },
{ label: "Upload Cursor Credits", value: "upload" },
{ label: "Upload Attendees", value: "attendees" },
{ label: "Check Credit Redemptions", value: "check" },
];

const handleSelect = (item: { label: string; value: string }) => {
Expand Down Expand Up @@ -100,6 +105,11 @@ const MainMenu = () => {
return <UploadAttendees onBack={handleBack} />;
}

// Render Check Redemptions screen
if (screen === "check") {
return <CheckRedemptions onBack={handleBack} />;
}

// Render main menu
return (
<Box flexDirection="column" padding={1}>
Expand Down Expand Up @@ -136,6 +146,7 @@ const MainMenu = () => {
<Text color={highlighted === "send" ? "white" : "gray"}><Text inverse> 1 </Text> Send</Text>
<Text color={highlighted === "upload" ? "white" : "gray"}><Text inverse> 2 </Text> Credits</Text>
<Text color={highlighted === "attendees" ? "white" : "gray"}><Text inverse> 3 </Text> Attendees</Text>
<Text color={highlighted === "check" ? "white" : "gray"}><Text inverse> 4 </Text> Check</Text>
<Text color="gray"><Text inverse> Q </Text> Quit</Text>
</Box>
</Box>
Expand Down
82 changes: 82 additions & 0 deletions src/hooks/useStorageHooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,85 @@ export function useSendCredits() {
return cloudAction({ personId: personId as Id<"people"> });
}, [isLocal, dataPath, cloudAction]);
}

// Type for sent credit with person info
export interface SentCreditWithPerson {
id: string;
url: string;
code: string;
amount: number;
person: {
firstName: string;
lastName: string;
email: string;
} | null;
}

// Hook for listing sent credits with person info
export function useSentCredits(): { data: SentCreditWithPerson[] | undefined; refresh: () => void } {
const { isLocal, dataPath } = useStorage();
const [localData, setLocalData] = useState<SentCreditWithPerson[] | undefined>(undefined);
const [refreshKey, setRefreshKey] = useState(0);

// Cloud query - use "skip" when in local mode
const cloudData = useConvexQuery(api.credits.listSentWithPerson, isLocal ? "skip" : {});

// Load local data
useEffect(() => {
if (isLocal) {
const sentCredits = localStorage.loadSentCreditsWithPerson(dataPath);
setLocalData(sentCredits.map(({ credit, person }) => ({
id: credit.id,
url: credit.url,
code: credit.code,
amount: credit.amount,
person: person ? {
firstName: person.firstName,
lastName: person.lastName,
email: person.email,
} : null,
})));
}
}, [isLocal, dataPath, refreshKey]);

const refresh = useCallback(() => {
setRefreshKey(k => k + 1);
}, []);

if (isLocal) {
return { data: localData, refresh };
}

const data = cloudData?.map(item => ({
id: item._id,
url: item.url,
code: item.code,
amount: item.amount,
person: item.person ? {
firstName: item.person.firstName,
lastName: item.person.lastName,
email: item.person.email,
} : null,
}));

return { data, refresh };
}

// Hook for marking credit as redeemed
export function useMarkRedeemed() {
const { isLocal, dataPath } = useStorage();
const cloudMutation = useConvexMutation(api.credits.markRedeemed);

return useCallback(async (creditId: string): Promise<boolean> => {
if (isLocal) {
return localStorage.markCreditRedeemed(creditId, dataPath);
}

try {
await cloudMutation({ creditId: creditId as Id<"credits"> });
return true;
} catch {
return false;
}
}, [isLocal, dataPath, cloudMutation]);
}
Loading