Skip to content

Commit

Permalink
Allow user to set residence and birthplace from settings
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker committed Nov 26, 2024
1 parent bd95c5d commit f78111c
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 101 deletions.
6 changes: 4 additions & 2 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ import type * as passwordReset from "../passwordReset.js";
import type * as questFields from "../questFields.js";
import type * as quests from "../quests.js";
import type * as seed from "../seed.js";
import type * as userData from "../userData.js";
import type * as userEncryptedData from "../userEncryptedData.js";
import type * as userQuests from "../userQuests.js";
import type * as userSettings from "../userSettings.js";
import type * as users from "../users.js";
import type * as validators from "../validators.js";

Expand All @@ -45,8 +46,9 @@ declare const fullApi: ApiFromModules<{
questFields: typeof questFields;
quests: typeof quests;
seed: typeof seed;
userData: typeof userData;
userEncryptedData: typeof userEncryptedData;
userQuests: typeof userQuests;
userSettings: typeof userSettings;
users: typeof users;
validators: typeof validators;
}>;
Expand Down
21 changes: 14 additions & 7 deletions convex/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,20 @@ export const { auth, signIn, signOut, store } = convexAuth({
}

// Create a new user with defaults
return ctx.db.insert("users", {
email: args.profile.email,
emailVerified: args.profile.emailVerified ?? false,
role: "user",
theme: "system",
groupQuestsBy: "dateAdded",
});
return ctx.db
.insert("users", {
email: args.profile.email,
emailVerified: args.profile.emailVerified ?? false,
role: "user",
})
.then((userId) => {
ctx.db.insert("userSettings", {
userId,
theme: "system",
groupQuestsBy: "dateAdded",
});
return userId;
});
},

async redirect({ redirectTo }) {
Expand Down
48 changes: 30 additions & 18 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,28 +84,49 @@ const forms = defineTable({
});

/**
* Represents a user of Namesake.
* Represents a user of Namesake's identity.
* @param name - The user's preferred first name.
* @param role - The user's role: "admin", "editor", or "user".
* @param image - A URL to the user's profile picture.
* @param email - The user's email address.
* @param emailVerificationTime - Time in ms since epoch when the user verified their email.
* @param isAnonymous - Denotes anonymous/unauthenticated users.
* @param emailVerified - Denotes whether the user has verified their email.
* @param residence - The US State the user lives in.
* @param birthplace - The US State the user was born in.
* @param isMinor - Denotes users under 18.
* @param preferredTheme - The user's preferred color scheme.
*/
const users = defineTable({
name: v.optional(v.string()),
role: role,
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerified: v.boolean(),
jurisdiction: v.optional(jurisdiction),
residence: v.optional(jurisdiction),
birthplace: v.optional(jurisdiction),
isMinor: v.optional(v.boolean()),
theme: theme,
groupQuestsBy: v.optional(groupQuestsBy),
}).index("email", ["email"]);

/**
* Pre-fillable user data entered throughout quests.
* All data in this table is end-to-end encrypted.
*/
const userEncryptedData = defineTable({
userId: v.id("users"),
fieldId: v.id("questFields"),
value: v.string(),
}).index("userId", ["userId"]);

/**
* Store user preferences.
* @param userId
* @param theme - The user's preferred color scheme.
* @param groupQuestsBy - The user's preferred way to group quests.
*/
const userSettings = defineTable({
userId: v.id("users"),
theme: v.optional(theme),
groupQuestsBy: v.optional(groupQuestsBy),
}).index("userId", ["userId"]);

/**
* Represents a user's unique progress in completing a quest.
* @param userId
Expand All @@ -122,22 +143,13 @@ const userQuests = defineTable({
.index("userId", ["userId"])
.index("questId", ["questId"]);

/**
* Pre-fillable user data entered throughout quests.
* All data in this table is end-to-end encrypted.
*/
const userData = defineTable({
userId: v.id("users"),
fieldId: v.id("questFields"),
value: v.string(),
}).index("userId", ["userId"]);

export default defineSchema({
...authTables,
forms,
quests,
questFields,
users,
userEncryptedData,
userSettings,
userQuests,
userData,
});
2 changes: 0 additions & 2 deletions convex/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,6 @@ const seed = internalMutation(async (ctx) => {
image: faker.image.avatar(),
role: "admin",
emailVerified: faker.datatype.boolean(),
theme: faker.helpers.arrayElement(["system", "light", "dark"]),
groupQuestsBy: "dateAdded",
});
console.log(`Created user ${firstName} ${lastName}`);

Expand Down
2 changes: 1 addition & 1 deletion convex/userData.ts → convex/userEncryptedData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export const getUserData = userQuery({
args: {},
handler: async (ctx, _args) => {
const userData = await ctx.db
.query("userData")
.query("userEncryptedData")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.first();

Expand Down
30 changes: 30 additions & 0 deletions convex/userSettings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { userMutation } from "./helpers";
import { groupQuestsBy, theme } from "./validators";

export const setTheme = userMutation({
args: { theme: theme },
handler: async (ctx, args) => {
const userSettings = await ctx.db
.query("userSettings")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.first();

if (!userSettings) throw new Error("User settings not found");

await ctx.db.patch(userSettings._id, { theme: args.theme });
},
});

export const setGroupQuestsBy = userMutation({
args: { groupQuestsBy: groupQuestsBy },
handler: async (ctx, args) => {
const userSettings = await ctx.db
.query("userSettings")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.first();

if (!userSettings) throw new Error("User settings not found");

await ctx.db.patch(userSettings._id, { groupQuestsBy: args.groupQuestsBy });
},
});
29 changes: 18 additions & 11 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 { groupQuestsBy, theme } from "./validators";
import { jurisdiction } from "./validators";

// TODO: Add `returns` value validation
// https://docs.convex.dev/functions/validation
Expand Down Expand Up @@ -43,31 +43,31 @@ export const getUserByEmail = query({
},
});

export const setCurrentUserName = userMutation({
export const setName = userMutation({
args: { name: v.optional(v.string()) },
handler: async (ctx, args) => {
await ctx.db.patch(ctx.userId, { name: args.name });
},
});

export const setCurrentUserIsMinor = userMutation({
args: { isMinor: v.boolean() },
export const setResidence = userMutation({
args: { residence: jurisdiction },
handler: async (ctx, args) => {
await ctx.db.patch(ctx.userId, { isMinor: args.isMinor });
await ctx.db.patch(ctx.userId, { residence: args.residence });
},
});

export const setUserTheme = userMutation({
args: { theme: theme },
export const setBirthplace = userMutation({
args: { birthplace: jurisdiction },
handler: async (ctx, args) => {
await ctx.db.patch(ctx.userId, { theme: args.theme });
await ctx.db.patch(ctx.userId, { birthplace: args.birthplace });
},
});

export const setGroupQuestsBy = userMutation({
args: { groupQuestsBy: groupQuestsBy },
export const setCurrentUserIsMinor = userMutation({
args: { isMinor: v.boolean() },
handler: async (ctx, args) => {
await ctx.db.patch(ctx.userId, { groupQuestsBy: args.groupQuestsBy });
await ctx.db.patch(ctx.userId, { isMinor: args.isMinor });
},
});

Expand All @@ -84,6 +84,13 @@ export const deleteCurrentUser = userMutation({
.collect();
for (const userQuest of userQuests) await ctx.db.delete(userQuest._id);

// Delete userSettings
const userSettings = await ctx.db
.query("userSettings")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.first();
if (userSettings) await ctx.db.delete(userSettings._id);

// Delete authAccounts
const authAccounts = await ctx.db
.query("authAccounts")
Expand Down
110 changes: 56 additions & 54 deletions src/routes/_authenticated/_home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -94,21 +94,6 @@ function IndexRoute() {

if (groupedQuests === undefined) return;

if (groupedQuests === null || Object.keys(groupedQuests).length === 0)
return (
<Empty
title="No quests"
icon={Milestone}
link={{
children: "Add quest",
button: {
variant: "primary",
},
href: { to: "/browse" },
}}
/>
);

return (
<AppSidebar>
<div className="flex items-center mb-4 bg-gray-app z-10">
Expand Down Expand Up @@ -150,46 +135,63 @@ function IndexRoute() {
</TooltipTrigger>
</div>
<Nav>
{Object.entries(groupedQuests)
.sort(([groupA], [groupB]) =>
sortGroupedQuests(groupA, groupB, groupByValue),
)
.map(([group, quests]) => {
if (quests.length === 0) return null;
let groupDetails: GroupDetails;
switch (groupByValue) {
case "category":
groupDetails = CATEGORIES[group as keyof typeof CATEGORIES];
break;
case "status":
groupDetails = STATUS[group as keyof typeof STATUS];
break;
case "dateAdded":
groupDetails = DATE_ADDED[group as keyof typeof DATE_ADDED];
break;
}
const { label } = groupDetails;
{Object.keys(groupedQuests).length === 0 ? (
<Empty
title="No quests"
icon={Milestone}
link={{
children: "Add quest",
button: {
variant: "primary",
},
href: { to: "/browse" },
}}
/>
) : (
Object.entries(groupedQuests)
.sort(([groupA], [groupB]) =>
sortGroupedQuests(groupA, groupB, groupByValue),
)
.map(([group, quests]) => {
if (quests.length === 0) return null;
let groupDetails: GroupDetails;
switch (groupByValue) {
case "category":
groupDetails = CATEGORIES[group as keyof typeof CATEGORIES];
break;
case "status":
groupDetails = STATUS[group as keyof typeof STATUS];
break;
case "dateAdded":
groupDetails = DATE_ADDED[group as keyof typeof DATE_ADDED];
break;
}
const { label } = groupDetails;

return (
<NavGroup key={label} label={label} count={quests.length}>
{quests.map((quest) => (
<NavItem
key={quest._id}
href={{
to: "/quests/$questId",
params: { questId: quest.questId },
}}
>
<StatusBadge status={quest.status as Status} condensed />
{quest.title}
{quest.jurisdiction && (
<Badge size="xs">{quest.jurisdiction}</Badge>
)}
</NavItem>
))}
</NavGroup>
);
})}
return (
<NavGroup key={label} label={label} count={quests.length}>
{quests.map((quest) => (
<NavItem
key={quest._id}
href={{
to: "/quests/$questId",
params: { questId: quest.questId },
}}
>
<StatusBadge
status={quest.status as Status}
condensed
/>
{quest.title}
{quest.jurisdiction && (
<Badge size="xs">{quest.jurisdiction}</Badge>
)}
</NavItem>
))}
</NavGroup>
);
})
)}
</Nav>
</AppSidebar>
);
Expand Down
Loading

0 comments on commit f78111c

Please sign in to comment.