Skip to content
Open
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
7 changes: 5 additions & 2 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export default defineSchema({
})
.index("by_visitor", ["visitorId"])
.index("by_user", ["userId"])
.index("by_visitor_song", ["visitorId", "songId"]),
.index("by_visitor_song", ["visitorId", "songId"])
.index("by_user_song", ["userId", "songId"]),

// Track which lines users have explicitly marked as "learned"
// Separate from linesCompleted which tracks practice - this is for mastery
Expand All @@ -93,7 +94,9 @@ export default defineSchema({
.index("by_visitor", ["visitorId"])
.index("by_user", ["userId"])
.index("by_visitor_song", ["visitorId", "songId"])
.index("by_visitor_song_line", ["visitorId", "songId", "lineNumber"]),
.index("by_visitor_song_line", ["visitorId", "songId", "lineNumber"])
.index("by_user_song", ["userId", "songId"])
.index("by_user_song_line", ["userId", "songId", "lineNumber"]),

// User's learning queue / wishlist of songs to learn later
userWishlist: defineTable({
Expand Down
84 changes: 46 additions & 38 deletions convex/songProgress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const getByUser = query({
handler: async (ctx) => {
const userId = await getAuthUserId(ctx);
if (!userId) return [];

return await ctx.db
.query("userSongProgress")
.withIndex("by_user", (q) => q.eq("userId", userId))
Expand Down Expand Up @@ -44,16 +44,20 @@ export const getWithSongDetails = query({
...p,
song,
totalLines: lyrics.length,
progressPercent: lyrics.length > 0
? Math.round((p.linesCompleted.length / lyrics.length) * 100)
: 0,
progressPercent:
lyrics.length > 0
? Math.round((p.linesCompleted.length / lyrics.length) * 100)
: 0,
};
})
}),
);

// Filter out nulls (deleted songs), songs with no progress, and sort by lastPracticed descending
return withDetails
.filter((p): p is NonNullable<typeof p> => p !== null && p.linesCompleted.length > 0)
.filter(
(p): p is NonNullable<typeof p> =>
p !== null && p.linesCompleted.length > 0,
)
.sort((a, b) => b.lastPracticed - a.lastPracticed);
},
});
Expand Down Expand Up @@ -89,9 +93,11 @@ export const getRecentForContinue = query({

// Find the last practiced line (or first line if none recorded)
const lastLineIndex = p.lastLineIndex ?? 0;
const lastLine = lyrics.find((l) => l.lineNumber === lastLineIndex) ?? lyrics[0];
const lastLine =
lyrics.find((l) => l.lineNumber === lastLineIndex) ?? lyrics[0];
const lastLinePreview = lastLine
? lastLine.original.substring(0, 50) + (lastLine.original.length > 50 ? "..." : "")
? lastLine.original.substring(0, 50) +
(lastLine.original.length > 50 ? "..." : "")
: "";

return {
Expand All @@ -102,11 +108,12 @@ export const getRecentForContinue = query({
lastLinePreview,
song,
totalLines: lyrics.length,
progressPercent: lyrics.length > 0
? Math.round((p.linesCompleted.length / lyrics.length) * 100)
: 0,
progressPercent:
lyrics.length > 0
? Math.round((p.linesCompleted.length / lyrics.length) * 100)
: 0,
};
})
}),
);

return withDetails.filter((p): p is NonNullable<typeof p> => p !== null);
Expand All @@ -122,10 +129,9 @@ export const getByUserSong = query({

return await ctx.db
.query("userSongProgress")
.withIndex("by_user", (q) =>
q.eq("userId", userId)
.withIndex("by_user_song", (q) =>
q.eq("userId", userId).eq("songId", args.songId),
)
.filter((q) => q.eq(q.field("songId"), args.songId))
.first();
},
});
Expand All @@ -134,17 +140,16 @@ export const getByUserSong = query({
export const recordLineCompletion = mutation({
args: {
songId: v.id("songs"),
lineNumber: v.number()
lineNumber: v.number(),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);

const existing = await ctx.db
.query("userSongProgress")
.withIndex("by_user", (q) =>
q.eq("userId", userId)
.withIndex("by_user_song", (q) =>
q.eq("userId", userId).eq("songId", args.songId),
)
.filter((q) => q.eq(q.field("songId"), args.songId))
.first();

if (existing) {
Expand Down Expand Up @@ -177,26 +182,28 @@ export const recordLineCompletion = mutation({
export const recordLinesCompletion = mutation({
args: {
songId: v.id("songs"),
lineNumbers: v.array(v.number())
lineNumbers: v.array(v.number()),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);
if (args.lineNumbers.length === 0) return null;

const existing = await ctx.db
.query("userSongProgress")
.withIndex("by_user", (q) =>
q.eq("userId", userId)
.withIndex("by_user_song", (q) =>
q.eq("userId", userId).eq("songId", args.songId),
)
.filter((q) => q.eq(q.field("songId"), args.songId))
.first();

// Track the highest line number as the "last" position
const lastLineIndex = Math.max(...args.lineNumbers);

if (existing) {
// Merge new lines with existing, avoiding duplicates
const lineSet = new Set([...existing.linesCompleted, ...args.lineNumbers]);
const lineSet = new Set([
...existing.linesCompleted,
...args.lineNumbers,
]);
const linesCompleted = [...lineSet].sort((a, b) => a - b);

await ctx.db.patch(existing._id, {
Expand All @@ -207,7 +214,9 @@ export const recordLinesCompletion = mutation({
return existing._id;
} else {
// Create new progress record
const linesCompleted = [...new Set(args.lineNumbers)].sort((a, b) => a - b);
const linesCompleted = [...new Set(args.lineNumbers)].sort(
(a, b) => a - b,
);
return await ctx.db.insert("userSongProgress", {
userId,
visitorId: "authenticated",
Expand All @@ -225,19 +234,18 @@ export const recordLinesCompletion = mutation({
export const toggleLineLearned = mutation({
args: {
songId: v.id("songs"),
lineNumber: v.number()
lineNumber: v.number(),
},
handler: async (ctx, args) => {
const userId = await requireAuth(ctx);

const existing = await ctx.db
.query("lineProgress")
.withIndex("by_user", (q) =>
q.eq("userId", userId)
)
.filter((q) =>
q.eq(q.field("songId"), args.songId) &&
q.eq(q.field("lineNumber"), args.lineNumber)
.withIndex("by_user_song_line", (q) =>
q
.eq("userId", userId)
.eq("songId", args.songId)
.eq("lineNumber", args.lineNumber),
)
.first();

Expand Down Expand Up @@ -266,8 +274,9 @@ export const toggleLineLearned = mutation({
// Also update userSongProgress so song appears in "My Songs" dashboard
const songProgress = await ctx.db
.query("userSongProgress")
.withIndex("by_user", (q) => q.eq("userId", userId))
.filter((q) => q.eq(q.field("songId"), args.songId))
.withIndex("by_user_song", (q) =>
q.eq("userId", userId).eq("songId", args.songId),
)
.first();

if (songProgress) {
Expand Down Expand Up @@ -310,10 +319,9 @@ export const getLineProgressByUserSong = query({

return await ctx.db
.query("lineProgress")
.withIndex("by_user", (q) =>
q.eq("userId", userId)
.withIndex("by_user_song", (q) =>
q.eq("userId", userId).eq("songId", args.songId),
)
.filter((q) => q.eq(q.field("songId"), args.songId))
.collect();
},
});
});