diff --git a/.changeset/olive-lemons-turn.md b/.changeset/olive-lemons-turn.md
new file mode 100644
index 0000000..d500ad1
--- /dev/null
+++ b/.changeset/olive-lemons-turn.md
@@ -0,0 +1,5 @@
+---
+"namesake": minor
+---
+
+Link uploaded forms to quests
diff --git a/convex/forms.ts b/convex/forms.ts
index c0e12cd..e172cb1 100644
--- a/convex/forms.ts
+++ b/convex/forms.ts
@@ -60,17 +60,39 @@ export const uploadPDF = userMutation({
},
});
+export const getFormsForQuest = query({
+ args: { questId: v.id("quests") },
+ handler: async (ctx, args) => {
+ const forms = await ctx.db
+ .query("forms")
+ .withIndex("quest", (q) => q.eq("questId", args.questId))
+ .filter((q) => q.eq(q.field("deletionTime"), undefined))
+ .collect();
+
+ return await Promise.all(
+ forms.map(async (form) => ({
+ ...form,
+ url: form.file ? await ctx.storage.getUrl(form.file) : null,
+ })),
+ );
+ },
+});
+
export const createForm = userMutation({
args: {
title: v.string(),
- jurisdiction: jurisdiction,
formCode: v.optional(v.string()),
+ jurisdiction: jurisdiction,
+ file: v.optional(v.id("_storage")),
+ questId: v.id("quests"),
},
handler: async (ctx, args) => {
return await ctx.db.insert("forms", {
title: args.title,
- jurisdiction: args.jurisdiction,
formCode: args.formCode,
+ jurisdiction: args.jurisdiction,
+ file: args.file,
+ questId: args.questId,
creationUser: ctx.userId,
});
},
diff --git a/convex/schema.ts b/convex/schema.ts
index 989b3e6..401ae8d 100644
--- a/convex/schema.ts
+++ b/convex/schema.ts
@@ -68,21 +68,23 @@ const questFields = defineTable({
/**
* Represents a PDF form that can be filled out by users.
+ * @param questId - The ID of the quest this form belongs to.
* @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 jurisdiction - 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({
+ questId: v.id("quests"),
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()),
-});
+}).index("quest", ["questId"]);
/**
* Represents a user of Namesake's identity.
diff --git a/src/components/Badge/Badge.tsx b/src/components/Badge/Badge.tsx
index 1fa1dfa..1ec1fbe 100644
--- a/src/components/Badge/Badge.tsx
+++ b/src/components/Badge/Badge.tsx
@@ -47,7 +47,11 @@ export function Badge({ icon: Icon, className, ...props }: BadgeProps) {
return (
{Icon &&
}
{props.children}
diff --git a/src/components/Button/Button.tsx b/src/components/Button/Button.tsx
index e8e1c72..ca71977 100644
--- a/src/components/Button/Button.tsx
+++ b/src/components/Button/Button.tsx
@@ -22,9 +22,9 @@ export const buttonStyles = tv({
primary: "bg-purple-solid text-white",
secondary: "bg-gray-ghost text-gray-normal",
destructive: "bg-red-solid",
- icon: "bg-transparent hover:bg-gray-3 dark:hover:bg-graydark-3 text-gray-dim hover:text-gray-normal border-0 flex items-center justify-center rounded-full",
+ icon: "bg-transparent hover:bg-graya-3 dark:hover:bg-graydarka-3 text-gray-dim hover:text-gray-normal border-0 flex items-center justify-center rounded-full",
ghost:
- "bg-transparent hover:bg-gray-3 dark:hover:bg-graydark-3 text-gray-dim hover:text-gray-normal border-0",
+ "bg-transparent hover:bg-graya-3 dark:hover:bg-graydarka-3 text-gray-dim hover:text-gray-normal border-0",
},
size: {
small: "h-8 px-2",
diff --git a/src/components/DocumentCard/DocumentCard.tsx b/src/components/DocumentCard/DocumentCard.tsx
new file mode 100644
index 0000000..328b69b
--- /dev/null
+++ b/src/components/DocumentCard/DocumentCard.tsx
@@ -0,0 +1,40 @@
+import { CircleArrowDown } from "lucide-react";
+import { Link } from "../Link";
+import { Tooltip, TooltipTrigger } from "../Tooltip";
+
+export type DocumentCardProps = {
+ title: string;
+ formCode?: string;
+ downloadUrl?: string;
+};
+
+export const DocumentCard = ({
+ title,
+ formCode,
+ downloadUrl,
+}: DocumentCardProps) => {
+ const fileTitle = formCode ? `${formCode} ${title}` : title;
+
+ return (
+
+ {formCode &&
{formCode}
}
+
+
+ {downloadUrl && (
+
+
+
+
+ Download
+
+ )}
+
+
+ );
+};
diff --git a/src/components/DocumentCard/index.ts b/src/components/DocumentCard/index.ts
new file mode 100644
index 0000000..759ffc9
--- /dev/null
+++ b/src/components/DocumentCard/index.ts
@@ -0,0 +1 @@
+export * from "./DocumentCard";
diff --git a/src/components/index.ts b/src/components/index.ts
index 9089fc6..5cc0643 100644
--- a/src/components/index.ts
+++ b/src/components/index.ts
@@ -16,6 +16,7 @@ export * from "./DatePicker";
export * from "./DateRangePicker";
export * from "./Dialog";
export * from "./Disclosure";
+export * from "./DocumentCard";
export * from "./Empty";
export * from "./Field";
export * from "./FileTrigger";
diff --git a/src/routes/_authenticated/_home/quests.$questId.tsx b/src/routes/_authenticated/_home/quests.$questId.tsx
index 7d5d3d9..fd3b4ed 100644
--- a/src/routes/_authenticated/_home/quests.$questId.tsx
+++ b/src/routes/_authenticated/_home/quests.$questId.tsx
@@ -3,6 +3,7 @@ import {
Badge,
Button,
DialogTrigger,
+ DocumentCard,
Empty,
Link,
Menu,
@@ -145,6 +146,35 @@ const QuestUrls = ({ urls }: { urls?: string[] }) => {
);
};
+const QuestForms = ({ questId }: { questId: Id<"quests"> }) => {
+ const forms = useQuery(api.forms.getFormsForQuest, {
+ questId,
+ });
+
+ if (!forms || forms.length === 0) return null;
+
+ return (
+
+
+ Forms
+
+ {forms.length}
+
+
+
+ {forms.map((form) => (
+
+ ))}
+
+
+ );
+};
+
function QuestDetailRoute() {
const { questId } = Route.useParams();
const navigate = useNavigate();
@@ -207,6 +237,7 @@ function QuestDetailRoute() {
+
{quest.content ? (
{quest.content}
diff --git a/src/routes/_authenticated/admin/forms/$formId.tsx b/src/routes/_authenticated/admin/forms/$formId.tsx
index 5830afc..695364d 100644
--- a/src/routes/_authenticated/admin/forms/$formId.tsx
+++ b/src/routes/_authenticated/admin/forms/$formId.tsx
@@ -1,4 +1,4 @@
-import { Badge, PageHeader } from "@/components";
+import { Badge, Link, PageHeader } from "@/components";
import { api } from "@convex/_generated/api";
import type { Id } from "@convex/_generated/dataModel";
import { createFileRoute } from "@tanstack/react-router";
@@ -30,7 +30,17 @@ function AdminFormDetailRoute() {
{form.formCode}
>
}
- />
+ >
+
+ Go to quest
+
+
{fileUrl && (