diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
index fe7eed2..0bad771 100644
--- a/docs/CONTRIBUTING.md
+++ b/docs/CONTRIBUTING.md
@@ -82,6 +82,7 @@ Below are Namesake's core dependencies. The links below each lead to docs.
| ----------------------------------------------------------------------------------- | -------------------------------------------------- |
| [Convex](https://docs.convex.dev/) | Type-safe database, file storage, realtime updates |
| [Convex Auth](https://labs.convex.dev/auth) | User authentication |
+| [SurveyJS](https://surveyjs.io/documentation) | Form building, validation, and display |
| [TanStack Router](https://tanstack.com/router/latest/docs/framework/react/overview) | File-based routing |
| [React](https://react.dev/reference/react) | Front-end web framework |
| [React Aria](https://react-spectrum.adobe.com/react-aria) | Accessible components and design system |
diff --git a/src/routes/_authenticated/admin/documents.$documentId.tsx b/src/routes/_authenticated/admin/documents.$documentId.tsx
index 7557797..77b60c1 100644
--- a/src/routes/_authenticated/admin/documents.$documentId.tsx
+++ b/src/routes/_authenticated/admin/documents.$documentId.tsx
@@ -1,27 +1,27 @@
-import { PageHeader } from '@/components/app'
-import { Badge, Link } from '@/components/common'
-import { api } from '@convex/_generated/api'
-import type { Id } from '@convex/_generated/dataModel'
-import { createFileRoute } from '@tanstack/react-router'
-import { useQuery } from 'convex/react'
+import { PageHeader } from "@/components/app";
+import { Badge, Link } from "@/components/common";
+import { api } from "@convex/_generated/api";
+import type { Id } from "@convex/_generated/dataModel";
+import { createFileRoute } from "@tanstack/react-router";
+import { useQuery } from "convex/react";
export const Route = createFileRoute(
- '/_authenticated/admin/documents/$documentId',
+ "/_authenticated/admin/documents/$documentId",
)({
component: AdminDocumentDetailRoute,
-})
+});
function AdminDocumentDetailRoute() {
- const { documentId } = Route.useParams()
+ const { documentId } = Route.useParams();
const document = useQuery(api.documents.getById, {
- documentId: documentId as Id<'documents'>,
- })
+ documentId: documentId as Id<"documents">,
+ });
const fileUrl = useQuery(api.documents.getURL, {
- documentId: documentId as Id<'documents'>,
- })
+ documentId: documentId as Id<"documents">,
+ });
- if (document === undefined) return
- if (document === null) return 'Document not found'
+ if (document === undefined) return;
+ if (document === null) return "Document not found";
return (
@@ -36,10 +36,10 @@ function AdminDocumentDetailRoute() {
>
Go to quest
@@ -52,5 +52,5 @@ function AdminDocumentDetailRoute() {
/>
)}
- )
+ );
}
diff --git a/src/routes/_authenticated/admin/documents.index.tsx b/src/routes/_authenticated/admin/documents.index.tsx
index 154814f..d5f3990 100644
--- a/src/routes/_authenticated/admin/documents.index.tsx
+++ b/src/routes/_authenticated/admin/documents.index.tsx
@@ -1,4 +1,4 @@
-import { PageHeader } from '@/components/app'
+import { PageHeader } from "@/components/app";
import {
Badge,
Button,
@@ -21,75 +21,75 @@ import {
TableHeader,
TableRow,
TextField,
-} from '@/components/common'
-import { api } from '@convex/_generated/api'
-import type { DataModel, Id } from '@convex/_generated/dataModel'
-import { JURISDICTIONS, type Jurisdiction } from '@convex/constants'
-import { createFileRoute } from '@tanstack/react-router'
-import { useMutation, useQuery } from 'convex/react'
-import { Ellipsis, FileText, Plus } from 'lucide-react'
-import { useState } from 'react'
+} from "@/components/common";
+import { api } from "@convex/_generated/api";
+import type { DataModel, Id } from "@convex/_generated/dataModel";
+import { JURISDICTIONS, type Jurisdiction } from "@convex/constants";
+import { createFileRoute } from "@tanstack/react-router";
+import { useMutation, useQuery } from "convex/react";
+import { Ellipsis, FileText, Plus } from "lucide-react";
+import { useState } from "react";
-export const Route = createFileRoute('/_authenticated/admin/documents/')({
+export const Route = createFileRoute("/_authenticated/admin/documents/")({
component: DocumentsRoute,
-})
+});
const NewDocumentModal = ({
isOpen,
onOpenChange,
onSubmit,
}: {
- isOpen: boolean
- onOpenChange: (isOpen: boolean) => void
- onSubmit: () => void
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ onSubmit: () => void;
}) => {
- const generateUploadUrl = useMutation(api.documents.generateUploadUrl)
- const uploadPDF = useMutation(api.documents.upload)
- const createDocument = useMutation(api.documents.create)
- const quests = useQuery(api.quests.getAllActive)
- const [isSubmitting, setIsSubmitting] = useState(false)
- const [file, setFile] = useState(null)
- const [title, setTitle] = useState('')
- const [code, setCode] = useState('')
- const [jurisdiction, setJurisdiction] = useState(null)
- const [questId, setQuestId] = useState | null>(null)
+ const generateUploadUrl = useMutation(api.documents.generateUploadUrl);
+ const uploadPDF = useMutation(api.documents.upload);
+ const createDocument = useMutation(api.documents.create);
+ const quests = useQuery(api.quests.getAllActive);
+ const [isSubmitting, setIsSubmitting] = useState(false);
+ const [file, setFile] = useState(null);
+ const [title, setTitle] = useState("");
+ const [code, setCode] = useState("");
+ const [jurisdiction, setJurisdiction] = useState(null);
+ const [questId, setQuestId] = useState | null>(null);
const clearForm = () => {
- setFile(null)
- setTitle('')
- setCode('')
- setJurisdiction(null)
- setQuestId(null)
- }
+ setFile(null);
+ setTitle("");
+ setCode("");
+ setJurisdiction(null);
+ setQuestId(null);
+ };
const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
+ e.preventDefault();
- if (jurisdiction === null) throw new Error('Jurisdiction is required')
- if (file === null) throw new Error('File is required')
- if (questId === null) throw new Error('Quest is required')
+ if (jurisdiction === null) throw new Error("Jurisdiction is required");
+ if (file === null) throw new Error("File is required");
+ if (questId === null) throw new Error("Quest is required");
- setIsSubmitting(true)
+ setIsSubmitting(true);
const documentId = await createDocument({
title,
jurisdiction,
code,
questId,
- })
+ });
- const postUrl = await generateUploadUrl()
+ const postUrl = await generateUploadUrl();
const result = await fetch(postUrl, {
- method: 'POST',
- headers: { 'Content-Type': file.type },
+ method: "POST",
+ headers: { "Content-Type": file.type },
body: file,
- })
- const { storageId } = await result.json()
+ });
+ const { storageId } = await result.json();
- await uploadPDF({ documentId, storageId })
+ await uploadPDF({ documentId, storageId });
- clearForm()
- onSubmit()
- }
+ clearForm();
+ onSubmit();
+ };
return (
setQuestId(key as Id<'quests'>)}
+ onSelectionChange={(key) => setQuestId(key as Id<"quests">)}
isRequired
className="w-full"
>
{quests?.map((quest) => {
const textValue = `${quest.title}${
- quest.jurisdiction ? ` (${quest.jurisdiction})` : ''
- }`
+ quest.jurisdiction ? ` (${quest.jurisdiction})` : ""
+ }`;
return (
- {quest.title}{' '}
+ {quest.title}{" "}
{quest.jurisdiction && {quest.jurisdiction}}
- )
+ );
})}
{
- if (e === null) return
- const files = Array.from(e)
- setFile(files[0])
+ if (e === null) return;
+ const files = Array.from(e);
+ setFile(files[0]);
}}
>
@@ -167,25 +167,25 @@ const NewDocumentModal = ({
- )
-}
+ );
+};
const DocumentTableRow = ({
document,
}: {
- document: DataModel['documents']['document']
+ document: DataModel["documents"]["document"];
}) => {
- const formUrl = useQuery(api.documents.getURL, { documentId: document._id })
- const softDelete = useMutation(api.documents.softDelete)
- const undelete = useMutation(api.documents.undoSoftDelete)
- const deleteForever = useMutation(api.documents.deleteForever)
+ const formUrl = useQuery(api.documents.getURL, { documentId: document._id });
+ const softDelete = useMutation(api.documents.softDelete);
+ const undelete = useMutation(api.documents.undoSoftDelete);
+ const deleteForever = useMutation(api.documents.deleteForever);
return (
@@ -240,12 +240,12 @@ const DocumentTableRow = ({
- )
-}
+ );
+};
function DocumentsRoute() {
- const [isNewFormModalOpen, setIsNewFormModalOpen] = useState(false)
- const documents = useQuery(api.documents.getAll)
+ const [isNewFormModalOpen, setIsNewFormModalOpen] = useState(false);
+ const documents = useQuery(api.documents.getAll);
return (
@@ -272,7 +272,7 @@ function DocumentsRoute() {
title="No documents"
icon={FileText}
button={{
- children: 'New Document',
+ children: "New Document",
onPress: () => setIsNewFormModalOpen(true),
}}
/>
@@ -289,5 +289,5 @@ function DocumentsRoute() {
onSubmit={() => setIsNewFormModalOpen(false)}
/>
- )
+ );
}
diff --git a/src/routes/_authenticated/admin/quests.index.tsx b/src/routes/_authenticated/admin/quests.index.tsx
index 4a6794b..6706acb 100644
--- a/src/routes/_authenticated/admin/quests.index.tsx
+++ b/src/routes/_authenticated/admin/quests.index.tsx
@@ -1,4 +1,4 @@
-import { PageHeader } from '@/components/app'
+import { PageHeader } from "@/components/app";
import {
Badge,
Button,
@@ -19,64 +19,64 @@ import {
TableHeader,
TableRow,
TextField,
-} from '@/components/common'
-import { api } from '@convex/_generated/api'
-import type { DataModel } from '@convex/_generated/dataModel'
+} from "@/components/common";
+import { api } from "@convex/_generated/api";
+import type { DataModel } from "@convex/_generated/dataModel";
import {
CATEGORIES,
type Category,
JURISDICTIONS,
type Jurisdiction,
-} from '@convex/constants'
-import { createFileRoute, useNavigate } from '@tanstack/react-router'
-import { useMutation, useQuery } from 'convex/react'
-import { CircleHelp, Ellipsis, Milestone, Plus } from 'lucide-react'
-import { useState } from 'react'
-import { toast } from 'sonner'
+} from "@convex/constants";
+import { createFileRoute, useNavigate } from "@tanstack/react-router";
+import { useMutation, useQuery } from "convex/react";
+import { CircleHelp, Ellipsis, Milestone, Plus } from "lucide-react";
+import { useState } from "react";
+import { toast } from "sonner";
-export const Route = createFileRoute('/_authenticated/admin/quests/')({
+export const Route = createFileRoute("/_authenticated/admin/quests/")({
component: QuestsRoute,
-})
+});
const NewQuestModal = ({
isOpen,
onOpenChange,
onSubmit,
}: {
- isOpen: boolean
- onOpenChange: (isOpen: boolean) => void
- onSubmit: () => void
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
+ onSubmit: () => void;
}) => {
- const create = useMutation(api.quests.create)
- const [title, setTitle] = useState('')
- const [category, setCategory] = useState(null)
- const [jurisdiction, setJurisdiction] = useState(null)
- const navigate = useNavigate()
+ const create = useMutation(api.quests.create);
+ const [title, setTitle] = useState("");
+ const [category, setCategory] = useState(null);
+ const [jurisdiction, setJurisdiction] = useState(null);
+ const navigate = useNavigate();
const clearForm = () => {
- setTitle('')
- setCategory(null)
- setJurisdiction(null)
- }
+ setTitle("");
+ setCategory(null);
+ setJurisdiction(null);
+ };
const handleSubmit = async (e: React.FormEvent) => {
- e.preventDefault()
+ e.preventDefault();
const questId = await create({
title,
category: category ?? undefined,
jurisdiction: jurisdiction ?? undefined,
- })
+ });
if (questId) {
- toast(`Created quest: ${title}`)
- clearForm()
- onSubmit()
+ toast(`Created quest: ${title}`);
+ clearForm();
+ onSubmit();
navigate({
- to: '/admin/quests/$questId',
+ to: "/admin/quests/$questId",
params: { questId },
- })
+ });
}
- }
+ };
return (
@@ -99,12 +99,12 @@ const NewQuestModal = ({
isRequired
>
{Object.entries(CATEGORIES).map(([key, { label, icon }]) => {
- const Icon = icon ?? CircleHelp
+ const Icon = icon ?? CircleHelp;
return (
{label}
- )
+ );
})}
- )
-}
+ );
+};
const QuestTableRow = ({
quest,
}: {
- quest: DataModel['quests']['document']
+ quest: DataModel["quests"]["document"];
}) => {
const questCount = useQuery(api.userQuests.countGlobalUsage, {
questId: quest._id,
- })
- const softDelete = useMutation(api.quests.softDelete)
- const undelete = useMutation(api.quests.undoSoftDelete)
- const deleteForever = useMutation(api.quests.deleteForever)
+ });
+ const softDelete = useMutation(api.quests.softDelete);
+ const undelete = useMutation(api.quests.undoSoftDelete);
+ const deleteForever = useMutation(api.quests.deleteForever);
const Category = () => {
- if (!quest.category) return
+ if (!quest.category) return;
- const { icon, label } = CATEGORIES[quest.category as Category]
+ const { icon, label } = CATEGORIES[quest.category as Category];
- return {label}
- }
+ return {label};
+ };
return (
@@ -210,12 +210,12 @@ const QuestTableRow = ({
- )
-}
+ );
+};
function QuestsRoute() {
- const [isNewQuestModalOpen, setIsNewQuestModalOpen] = useState(false)
- const quests = useQuery(api.quests.getAll)
+ const [isNewQuestModalOpen, setIsNewQuestModalOpen] = useState(false);
+ const quests = useQuery(api.quests.getAll);
return (
<>
@@ -244,7 +244,7 @@ function QuestsRoute() {
title="No quests"
icon={Milestone}
button={{
- children: 'New Quest',
+ children: "New Quest",
onPress: () => setIsNewQuestModalOpen(true),
}}
/>
@@ -261,5 +261,5 @@ function QuestsRoute() {
onSubmit={() => setIsNewQuestModalOpen(false)}
/>
>
- )
+ );
}
diff --git a/src/styles/index.css b/src/styles/index.css
index 558cbf9..b7587ab 100644
--- a/src/styles/index.css
+++ b/src/styles/index.css
@@ -112,7 +112,8 @@ body {
color-scheme: dark light;
}
-body, #root {
+body,
+#root {
min-height: 100dvh;
}