Skip to content

Commit

Permalink
feat: Support "group by" for quests (#170)
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker authored Nov 6, 2024
1 parent a9a4c74 commit 9a7282a
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 148 deletions.
5 changes: 5 additions & 0 deletions .changeset/green-lions-reply.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Support grouping quests by category, status, and date added
2 changes: 1 addition & 1 deletion convex/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export const { auth, signIn, signOut, store } = convexAuth({
emailVerified: args.profile.emailVerified ?? false,
role: "user",
theme: "system",
sortQuestsBy: "newest",
groupQuestsBy: "dateAdded",
});
},

Expand Down
51 changes: 44 additions & 7 deletions convex/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
RiBankLine,
RiCalendarLine,
RiChat3Line,
RiCheckLine,
RiCheckboxLine,
RiComputerLine,
RiDropdownList,
Expand All @@ -19,6 +20,7 @@ import {
RiMovie2Line,
RiParagraph,
RiPhoneLine,
RiProgress4Line,
RiQuestionLine,
RiScales3Line,
RiShoppingBag4Line,
Expand Down Expand Up @@ -135,17 +137,19 @@ export const ROLES = {
} as const;
export type Role = keyof typeof ROLES;

export const SORT_QUESTS_BY = {
newest: "Newest",
oldest: "Oldest",
export const GROUP_QUESTS_BY = {
dateAdded: "Date added",
category: "Category",
status: "Status",
} as const;
export type SortQuestsBy = keyof typeof SORT_QUESTS_BY;
export type GroupQuestsBy = keyof typeof GROUP_QUESTS_BY;

interface CategoryDetails {
interface GroupDetails {
label: string;
icon: RemixiconComponentType;
icon: RemixiconComponentType | null;
}
export const CATEGORIES: Record<string, CategoryDetails> = {

export const CATEGORIES: Record<string, GroupDetails> = {
core: {
label: "Core",
icon: RiSignpostLine,
Expand Down Expand Up @@ -207,4 +211,37 @@ export const CATEGORIES: Record<string, CategoryDetails> = {
icon: RiQuestionLine,
},
};
export const CATEGORY_ORDER: Category[] = Object.keys(CATEGORIES) as Category[];
export type Category = keyof typeof CATEGORIES;

export const DATE_ADDED: Record<string, GroupDetails> = {
lastWeek: {
label: "Last 7 days",
icon: null,
},
lastMonth: {
label: "Last 30 days",
icon: null,
},
earlier: {
label: "Earlier",
icon: null,
},
};
export const DATE_ADDED_ORDER: DateAdded[] = Object.keys(
DATE_ADDED,
) as DateAdded[];
export type DateAdded = keyof typeof DATE_ADDED;

export const STATUS: Record<string, GroupDetails> = {
incomplete: {
label: "In Progress",
icon: RiProgress4Line,
},
complete: {
label: "Completed",
icon: RiCheckLine,
},
} as const;
export const STATUS_ORDER: Status[] = Object.keys(STATUS) as Status[];
export type Status = keyof typeof STATUS;
4 changes: 2 additions & 2 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { v } from "convex/values";
import {
category,
field,
groupQuestsBy,
jurisdiction,
role,
sortQuestsBy,
theme,
} from "./validators";

Expand Down Expand Up @@ -86,7 +86,7 @@ const users = defineTable({
jurisdiction: v.optional(jurisdiction),
isMinor: v.optional(v.boolean()),
theme: theme,
sortQuestsBy: v.optional(sortQuestsBy),
groupQuestsBy: v.optional(groupQuestsBy),
}).index("email", ["email"]);

/**
Expand Down
2 changes: 1 addition & 1 deletion convex/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const seed = internalMutation(async (ctx) => {
role: "admin",
emailVerified: faker.datatype.boolean(),
theme: faker.helpers.arrayElement(["system", "light", "dark"]),
sortQuestsBy: "newest",
groupQuestsBy: "dateAdded",
});
console.log(`Created user ${firstName} ${lastName}`);

Expand Down
125 changes: 125 additions & 0 deletions convex/userQuests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { v } from "convex/values";
import { query } from "./_generated/server";
import type { Category } from "./constants";
import { userMutation, userQuery } from "./helpers";

// TODO: Add `returns` value validation
Expand All @@ -26,6 +27,18 @@ export const getQuestsForCurrentUser = userQuery({
},
});

export const getUserQuestCount = userQuery({
args: {},
returns: v.number(),
handler: async (ctx) => {
const userQuests = await ctx.db
.query("userQuests")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.collect();
return userQuests.length;
},
});

export const getAvailableQuestsForUser = userQuery({
args: {},
handler: async (ctx, _args) => {
Expand Down Expand Up @@ -172,3 +185,115 @@ export const getQuestCounts = query({
return counts;
},
});

export const getUserQuestsByCategory = userQuery({
args: {},
handler: async (ctx) => {
const userQuests = await ctx.db
.query("userQuests")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.collect();

const questsWithDetails = await Promise.all(
userQuests.map(async (userQuest) => {
const quest = await ctx.db.get(userQuest.questId);
return quest && quest.deletionTime === undefined
? { ...quest, ...userQuest }
: null;
}),
);

const validQuests = questsWithDetails.filter(
(q): q is NonNullable<typeof q> => q !== null,
);

return validQuests.reduce(
(acc, quest) => {
const category = (quest.category ?? "other") as Category;
if (!acc[category]) acc[category] = [];
acc[category].push(quest);
return acc;
},
{} as Record<string, typeof validQuests>,
);
},
});

export const getUserQuestsByDate = userQuery({
args: {},
handler: async (ctx) => {
const userQuests = await ctx.db
.query("userQuests")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.collect();

const questsWithDetails = await Promise.all(
userQuests.map(async (userQuest) => {
const quest = await ctx.db.get(userQuest.questId);
return quest && quest.deletionTime === undefined
? { ...quest, ...userQuest }
: null;
}),
);

const validQuests = questsWithDetails.filter(
(q): q is NonNullable<typeof q> => q !== null,
);

const now = Date.now();
const oneWeekAgo = now - 7 * 24 * 60 * 60 * 1000;
const oneMonthAgo = now - 30 * 24 * 60 * 60 * 1000;

return validQuests.reduce(
(acc, quest) => {
if (quest._creationTime > oneWeekAgo) {
acc.lastWeek.push(quest);
} else if (quest._creationTime > oneMonthAgo) {
acc.lastMonth.push(quest);
} else {
acc.earlier.push(quest);
}
return acc;
},
{ lastWeek: [], lastMonth: [], earlier: [] } as Record<
string,
typeof validQuests
>,
);
},
});

export const getUserQuestsByStatus = userQuery({
args: {},
handler: async (ctx) => {
const userQuests = await ctx.db
.query("userQuests")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.collect();

const questsWithDetails = await Promise.all(
userQuests.map(async (userQuest) => {
const quest = await ctx.db.get(userQuest.questId);
return quest && quest.deletionTime === undefined
? { ...quest, ...userQuest }
: null;
}),
);

const validQuests = questsWithDetails.filter(
(q): q is NonNullable<typeof q> => q !== null,
);

return validQuests.reduce(
(acc, quest) => {
if (quest.completionTime) {
acc.complete.push(quest);
} else {
acc.incomplete.push(quest);
}
return acc;
},
{ incomplete: [], complete: [] } as Record<string, typeof validQuests>,
);
},
});
8 changes: 4 additions & 4 deletions convex/users.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { v } from "convex/values";
import { query } from "./_generated/server";
import type { Role } from "./constants";
import { userMutation, userQuery } from "./helpers";
import { sortQuestsBy, theme } from "./validators";
import { groupQuestsBy, theme } from "./validators";

// TODO: Add `returns` value validation
// https://docs.convex.dev/functions/validation
Expand Down Expand Up @@ -64,10 +64,10 @@ export const setUserTheme = userMutation({
},
});

export const setSortQuestsBy = userMutation({
args: { sortQuestsBy: sortQuestsBy },
export const setGroupQuestsBy = userMutation({
args: { groupQuestsBy: groupQuestsBy },
handler: async (ctx, args) => {
await ctx.db.patch(ctx.userId, { sortQuestsBy: args.sortQuestsBy });
await ctx.db.patch(ctx.userId, { groupQuestsBy: args.groupQuestsBy });
},
});

Expand Down
8 changes: 5 additions & 3 deletions convex/validators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import { v } from "convex/values";
import {
CATEGORIES,
FIELDS,
GROUP_QUESTS_BY,
JURISDICTIONS,
ROLES,
SORT_QUESTS_BY,
THEMES,
} from "./constants";

Expand All @@ -24,8 +24,10 @@ export const field = v.union(
...Object.keys(FIELDS).map((field) => v.literal(field)),
);

export const sortQuestsBy = v.union(
...Object.keys(SORT_QUESTS_BY).map((sortQuestsBy) => v.literal(sortQuestsBy)),
export const groupQuestsBy = v.union(
...Object.keys(GROUP_QUESTS_BY).map((groupQuestsBy) =>
v.literal(groupQuestsBy),
),
);

export const category = v.union(
Expand Down
2 changes: 1 addition & 1 deletion src/components/GridList/GridList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function GridList<T extends object>({

const itemStyles = tv({
extend: focusRing,
base: "relative text-gray-normal flex items-center gap-3 cursor-pointer select-none py-2 px-3 text-sm first:rounded-t-md last:rounded-b-md -mb-px last:mb-0 -outline-offset-2",
base: "relative text-gray-normal flex items-center gap-3 cursor-pointer select-none py-2 px-4 text-sm first:rounded-t-md last:rounded-b-md -mb-px last:mb-0 -outline-offset-2",
variants: {
isSelected: {
false: "",
Expand Down
Empty file added src/convex/constants.ts
Empty file.
16 changes: 13 additions & 3 deletions src/routes/_authenticated/_home.quests.$questId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {
} from "@/components";
import { api } from "@convex/_generated/api";
import type { Id } from "@convex/_generated/dataModel";
import { RiLink, RiMoreFill, RiSignpostLine } from "@remixicon/react";
import {
RiCheckLine,
RiLink,
RiMoreFill,
RiProgress4Line,
RiSignpostLine,
} from "@remixicon/react";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useMutation, useQuery } from "convex/react";
import Markdown from "react-markdown";
Expand Down Expand Up @@ -66,9 +72,13 @@ function QuestDetailRoute() {
<div className="flex gap-1">
{quest.jurisdiction && <Badge>{quest.jurisdiction}</Badge>}
{userQuest?.completionTime ? (
<Badge variant="success">Complete</Badge>
<Badge variant="success" icon={RiCheckLine}>
Complete
</Badge>
) : (
<Badge variant="info">In progress</Badge>
<Badge variant="warning" icon={RiProgress4Line}>
In progress
</Badge>
)}
</div>
}
Expand Down
Loading

0 comments on commit 9a7282a

Please sign in to comment.