Skip to content

Commit

Permalink
feat: Add fields to admin, data to settings (#112)
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker authored Sep 27, 2024
1 parent b57e265 commit dc09f49
Show file tree
Hide file tree
Showing 14 changed files with 658 additions and 268 deletions.
5 changes: 5 additions & 0 deletions .changeset/rare-donkeys-beam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Support defining quest fields and displaying user data
4 changes: 4 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ import type * as constants from "../constants.js";
import type * as forms from "../forms.js";
import type * as helpers from "../helpers.js";
import type * as http from "../http.js";
import type * as questFields from "../questFields.js";
import type * as questSteps from "../questSteps.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 userQuests from "../userQuests.js";
import type * as users from "../users.js";
import type * as validators from "../validators.js";
Expand All @@ -41,9 +43,11 @@ declare const fullApi: ApiFromModules<{
forms: typeof forms;
helpers: typeof helpers;
http: typeof http;
questFields: typeof questFields;
questSteps: typeof questSteps;
quests: typeof quests;
seed: typeof seed;
userData: typeof userData;
userQuests: typeof userQuests;
users: typeof users;
validators: typeof validators;
Expand Down
28 changes: 28 additions & 0 deletions convex/questFields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { v } from "convex/values";
import { query } from "./_generated/server";
import { userMutation } from "./helpers";
import { field } from "./validators";

export const getAllFields = query({
args: {},
handler: async (ctx) => {
return await ctx.db.query("questFields").collect();
},
});

export const createField = userMutation({
args: {
type: field,
label: v.string(),
slug: v.string(),
helpText: v.optional(v.string()),
},
handler: async (ctx, args) => {
return await ctx.db.insert("questFields", {
type: args.type,
label: args.label,
slug: args.slug,
helpText: args.helpText,
});
},
});
73 changes: 53 additions & 20 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,6 @@ import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";
import { field, icon, jurisdiction, role, theme } from "./validators";

/**
* Represents a PDF form that can be filled out by users.
* @param title - The title of the form. (e.g. "Petition to Change Name of Adult")
* @param formCode - The legal code for the form. (e.g. "CJP 27")
* @param creationUser - The user who created the form.
* @param file - The storageId for the PDF file.
* @param state - The US State the form applies to. (e.g. "MA")
* @param deletionTime - Time in ms since epoch when the form was deleted.
*/
const forms = defineTable({
title: v.string(),
formCode: v.optional(v.string()),
creationUser: v.id("users"),
file: v.optional(v.id("_storage")),
jurisdiction: jurisdiction,
deletionTime: v.optional(v.number()),
});

/**
* Represents a collection of steps and forms for a user to complete.
* @param title - The title of the quest. (e.g. "Court Order")
Expand Down Expand Up @@ -49,9 +31,48 @@ const questSteps = defineTable({
creationUser: v.id("users"),
title: v.string(),
description: v.optional(v.string()),
fields: v.optional(v.array(field)),
fields: v.optional(
v.array(
v.object({
fieldId: v.id("questFields"),
}),
),
),
}).index("questId", ["questId"]);

/**
* Represents a single input field which may be shared across multiple quests
* or steps. Data entered into these fields are end-to-end encrypted and used
* to pre-fill fields that point to the same data in future quests.
* @param type - The type of field. (e.g. "text", "select")
* @param label - The label for the field. (e.g. "First Name")
* @param helpText - Additional help text for the field.
*/
const questFields = defineTable({
type: field,
label: v.string(),
slug: v.string(),
helpText: v.optional(v.string()),
});

/**
* Represents a PDF form that can be filled out by users.
* @param title - The title of the form. (e.g. "Petition to Change Name of Adult")
* @param formCode - The legal code for the form. (e.g. "CJP 27")
* @param creationUser - The user who created the form.
* @param file - The storageId for the PDF file.
* @param state - The US State the form applies to. (e.g. "MA")
* @param deletionTime - Time in ms since epoch when the form was deleted.
*/
const forms = defineTable({
title: v.string(),
formCode: v.optional(v.string()),
creationUser: v.id("users"),
file: v.optional(v.id("_storage")),
jurisdiction: jurisdiction,
deletionTime: v.optional(v.number()),
});

/**
* Represents a user of Namesake.
* @param name - The user's preferred first name.
Expand All @@ -69,7 +90,7 @@ const users = defineTable({
image: v.optional(v.string()),
email: v.optional(v.string()),
emailVerified: v.boolean(),
isAnonymous: v.optional(v.boolean()),
jurisdiction: v.optional(jurisdiction),
isMinor: v.optional(v.boolean()),
theme: theme,
}).index("email", ["email"]);
Expand All @@ -88,11 +109,23 @@ 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("fields"),
value: v.string(),
}).index("userId", ["userId"]);

export default defineSchema({
...authTables,
forms,
quests,
questSteps,
questFields,
users,
userQuests,
userData,
});
13 changes: 13 additions & 0 deletions convex/userData.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { userQuery } from "./helpers";

export const getUserData = userQuery({
args: {},
handler: async (ctx, _args) => {
const userData = await ctx.db
.query("userData")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.first();

return userData;
},
});
45 changes: 2 additions & 43 deletions convex/validators.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import { v } from "convex/values";
import {
FIELDS,
type Field,
ICONS,
JURISDICTIONS,
ROLES,
THEMES,
} from "./constants";
import { FIELDS, ICONS, JURISDICTIONS, ROLES, THEMES } from "./constants";

export const jurisdiction = v.union(
...Object.keys(JURISDICTIONS).map((jurisdiction) => v.literal(jurisdiction)),
Expand All @@ -24,40 +17,6 @@ export const icon = v.union(
...Object.keys(ICONS).map((icon) => v.literal(icon)),
);

const sharedFieldProps = {
label: v.string(),
helpText: v.optional(v.string()),
isRequired: v.boolean(),
};

const optionsForField: any = (field: Field) => {
switch (field) {
case "select":
return {
options: v.array(v.string()),
};
case "number":
return {
min: v.optional(v.number()),
max: v.optional(v.number()),
};
case "text":
case "textarea":
return {
minLength: v.optional(v.number()),
maxLength: v.optional(v.number()),
};
default:
return {};
}
};

export const field = v.union(
...Object.keys(FIELDS).map((field) =>
v.object({
type: v.literal(field),
...sharedFieldProps,
...optionsForField(field as keyof typeof FIELDS),
}),
),
...Object.keys(FIELDS).map((field) => v.literal(field)),
);
11 changes: 4 additions & 7 deletions src/components/Nav/Nav.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,15 @@ import { focusRing } from "../utils";

interface NavProps {
routes: {
icon: {
default: RemixiconComponentType;
active: RemixiconComponentType;
};
icon: RemixiconComponentType;
href: LinkProps;
label: string;
}[];
}

const styles = tv({
extend: focusRing,
base: "rounded-lg no-underline flex items-center gap-2 text-gray-dim hover:text-gray-normal py-1 aria-current:font-semibold aria-current:text-gray-normal",
base: "rounded-lg no-underline flex items-center gap-2 text-gray-dim hover:text-gray-normal py-1.5 aria-current:font-semibold aria-current:text-gray-normal",
});

export const Nav = ({ routes }: NavProps) => {
Expand All @@ -27,7 +24,7 @@ export const Nav = ({ routes }: NavProps) => {
<nav className="flex flex-col w-[160px] shrink-0 pt-5 pl-6">
{routes.map(({ href, label, icon }) => {
const current = matchRoute({ ...href, fuzzy: true });
const { default: DefaultIcon, active: ActiveIcon } = icon;
const Icon = icon;

return (
<Link
Expand All @@ -36,7 +33,7 @@ export const Nav = ({ routes }: NavProps) => {
className={styles()}
aria-current={current ? "true" : null}
>
{current ? <ActiveIcon /> : <DefaultIcon />}
<Icon />
{label}
</Link>
);
Expand Down
Loading

0 comments on commit dc09f49

Please sign in to comment.