Skip to content

Commit

Permalink
Locate form on its own route, improve edit experience
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker committed Dec 7, 2024
1 parent a7b282e commit 80235ba
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 62 deletions.
93 changes: 46 additions & 47 deletions src/components/quests/QuestForm/QuestForm.tsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,60 @@
import "survey-core/defaultV2.min.css";

import { Button, Modal } from "@/components/common";
import { api } from "@convex/_generated/api";
import type { Doc } from "@convex/_generated/dataModel";
import { useMutation } from "convex/react";
import { useTheme } from "next-themes";
import { useState } from "react";
import type { CompleteEvent, SurveyModel } from "survey-core";
import {
LayeredDarkPanelless,
LayeredLightPanelless,
} from "survey-core/themes";
import { Model, Survey } from "survey-react-ui";
import { Link } from "@/components/common";
import type { Doc, Id } from "@convex/_generated/dataModel";
import { Pencil, Plus } from "lucide-react";
import { Heading } from "react-aria-components";

export type QuestFormProps = {
quest: Doc<"quests">;
editable?: boolean;
};

export const QuestForm = ({ quest }: QuestFormProps) => {
const { resolvedTheme } = useTheme();
const [isSurveyOpen, setIsSurveyOpen] = useState(false);
const saveData = useMutation(api.userFormData.set);
export const EditButton = ({
isNew,
questId,
}: { isNew: boolean; questId: Id<"quests"> }) => {
const { buttonText, Icon } = isNew
? { buttonText: "Create form", Icon: Plus }
: { buttonText: "Edit form", Icon: Pencil };

if (!quest.formSchema) return null;

const survey = new Model(quest.formSchema);
survey.applyTheme(
resolvedTheme === "dark" ? LayeredDarkPanelless : LayeredLightPanelless,
return (
<Link
href={{
to: "/admin/quests/$questId/form",
params: { questId },
}}
button={{ variant: "secondary" }}
>
<Icon size={16} /> {buttonText}
</Link>
);
};

const handleSubmit = async (sender: SurveyModel, _options: CompleteEvent) => {
try {
// Validate JSON against schema and remove invalid values
sender.clearIncorrectValues(true);
for (const key in sender.data) {
const question = sender.getQuestionByName(key);
if (question) {
saveData({
field: key,
value: question.value,
});
}
}
setIsSurveyOpen(false);
} catch (err) {
console.error(err);
}
};

survey.onComplete.add(handleSubmit);
export const QuestForm = ({ quest, editable }: QuestFormProps) => {
if (!quest.formSchema && !editable) return null;

return (
<>
<Button onPress={() => setIsSurveyOpen(true)}>Get Started</Button>
<Modal isOpen={isSurveyOpen}>
<Survey model={survey} />
</Modal>
</>
<div className="rounded-lg border border-gray-dim flex items-center justify-between gap-4 p-4 mb-4">
<div className="flex flex-col">
<Heading className="text-lg">Answer questions</Heading>
<p className="text-gray-dim">
We'll walk you through the steps to fill out your forms.
</p>
</div>
{editable ? (
<EditButton isNew={!quest.formSchema} questId={quest._id} />
) : (
<Link
href={{
to: "/quests/$questId/form",
params: { questId: quest._id },
}}
button={{ variant: "secondary" }}
>
Get Started
</Link>
)}
</div>
);
};
29 changes: 29 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { Route as AuthenticatedAdminDocumentsDocumentIdImport } from './routes/_
import { Route as AuthenticatedAdminQuestsQuestIdIndexImport } from './routes/_authenticated/admin/quests.$questId.index'
import { Route as AuthenticatedHomeQuestsQuestIdIndexImport } from './routes/_authenticated/_home/quests.$questId.index'
import { Route as AuthenticatedAdminQuestsQuestIdFormImport } from './routes/_authenticated/admin_/quests.$questId.form'
import { Route as AuthenticatedHomeQuestsQuestIdFormImport } from './routes/_authenticated/_home_.quests.$questId.form'
import { Route as AuthenticatedHomeQuestsQuestIdEditImport } from './routes/_authenticated/_home/quests.$questId.edit'

// Create/Update Routes
Expand Down Expand Up @@ -157,6 +158,13 @@ const AuthenticatedAdminQuestsQuestIdFormRoute =
getParentRoute: () => AuthenticatedRoute,
} as any)

const AuthenticatedHomeQuestsQuestIdFormRoute =
AuthenticatedHomeQuestsQuestIdFormImport.update({
id: '/_home_/quests/$questId/form',
path: '/quests/$questId/form',
getParentRoute: () => AuthenticatedRoute,
} as any)

const AuthenticatedHomeQuestsQuestIdEditRoute =
AuthenticatedHomeQuestsQuestIdEditImport.update({
id: '/quests/$questId/edit',
Expand Down Expand Up @@ -287,6 +295,13 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AuthenticatedHomeQuestsQuestIdEditImport
parentRoute: typeof AuthenticatedHomeImport
}
'/_authenticated/_home_/quests/$questId/form': {
id: '/_authenticated/_home_/quests/$questId/form'
path: '/quests/$questId/form'
fullPath: '/quests/$questId/form'
preLoaderRoute: typeof AuthenticatedHomeQuestsQuestIdFormImport
parentRoute: typeof AuthenticatedImport
}
'/_authenticated/admin_/quests/$questId/form': {
id: '/_authenticated/admin_/quests/$questId/form'
path: '/admin/quests/$questId/form'
Expand Down Expand Up @@ -380,6 +395,7 @@ interface AuthenticatedRouteChildren {
AuthenticatedSettingsRouteRoute: typeof AuthenticatedSettingsRouteRouteWithChildren
AuthenticatedHomeRoute: typeof AuthenticatedHomeRouteWithChildren
AuthenticatedBrowseIndexRoute: typeof AuthenticatedBrowseIndexRoute
AuthenticatedHomeQuestsQuestIdFormRoute: typeof AuthenticatedHomeQuestsQuestIdFormRoute
AuthenticatedAdminQuestsQuestIdFormRoute: typeof AuthenticatedAdminQuestsQuestIdFormRoute
}

Expand All @@ -388,6 +404,8 @@ const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
AuthenticatedSettingsRouteRoute: AuthenticatedSettingsRouteRouteWithChildren,
AuthenticatedHomeRoute: AuthenticatedHomeRouteWithChildren,
AuthenticatedBrowseIndexRoute: AuthenticatedBrowseIndexRoute,
AuthenticatedHomeQuestsQuestIdFormRoute:
AuthenticatedHomeQuestsQuestIdFormRoute,
AuthenticatedAdminQuestsQuestIdFormRoute:
AuthenticatedAdminQuestsQuestIdFormRoute,
}
Expand Down Expand Up @@ -424,6 +442,7 @@ export interface FileRoutesByFullPath {
'/admin/documents': typeof AuthenticatedAdminDocumentsIndexRoute
'/admin/quests': typeof AuthenticatedAdminQuestsIndexRoute
'/quests/$questId/edit': typeof AuthenticatedHomeQuestsQuestIdEditRoute
'/quests/$questId/form': typeof AuthenticatedHomeQuestsQuestIdFormRoute
'/admin/quests/$questId/form': typeof AuthenticatedAdminQuestsQuestIdFormRoute
'/quests/$questId': typeof AuthenticatedHomeQuestsQuestIdIndexRoute
'/admin/quests/$questId': typeof AuthenticatedAdminQuestsQuestIdIndexRoute
Expand All @@ -443,6 +462,7 @@ export interface FileRoutesByTo {
'/admin/documents': typeof AuthenticatedAdminDocumentsIndexRoute
'/admin/quests': typeof AuthenticatedAdminQuestsIndexRoute
'/quests/$questId/edit': typeof AuthenticatedHomeQuestsQuestIdEditRoute
'/quests/$questId/form': typeof AuthenticatedHomeQuestsQuestIdFormRoute
'/admin/quests/$questId/form': typeof AuthenticatedAdminQuestsQuestIdFormRoute
'/quests/$questId': typeof AuthenticatedHomeQuestsQuestIdIndexRoute
'/admin/quests/$questId': typeof AuthenticatedAdminQuestsQuestIdIndexRoute
Expand All @@ -467,6 +487,7 @@ export interface FileRoutesById {
'/_authenticated/admin/documents/': typeof AuthenticatedAdminDocumentsIndexRoute
'/_authenticated/admin/quests/': typeof AuthenticatedAdminQuestsIndexRoute
'/_authenticated/_home/quests/$questId/edit': typeof AuthenticatedHomeQuestsQuestIdEditRoute
'/_authenticated/_home_/quests/$questId/form': typeof AuthenticatedHomeQuestsQuestIdFormRoute
'/_authenticated/admin_/quests/$questId/form': typeof AuthenticatedAdminQuestsQuestIdFormRoute
'/_authenticated/_home/quests/$questId/': typeof AuthenticatedHomeQuestsQuestIdIndexRoute
'/_authenticated/admin/quests/$questId/': typeof AuthenticatedAdminQuestsQuestIdIndexRoute
Expand All @@ -490,6 +511,7 @@ export interface FileRouteTypes {
| '/admin/documents'
| '/admin/quests'
| '/quests/$questId/edit'
| '/quests/$questId/form'
| '/admin/quests/$questId/form'
| '/quests/$questId'
| '/admin/quests/$questId'
Expand All @@ -508,6 +530,7 @@ export interface FileRouteTypes {
| '/admin/documents'
| '/admin/quests'
| '/quests/$questId/edit'
| '/quests/$questId/form'
| '/admin/quests/$questId/form'
| '/quests/$questId'
| '/admin/quests/$questId'
Expand All @@ -530,6 +553,7 @@ export interface FileRouteTypes {
| '/_authenticated/admin/documents/'
| '/_authenticated/admin/quests/'
| '/_authenticated/_home/quests/$questId/edit'
| '/_authenticated/_home_/quests/$questId/form'
| '/_authenticated/admin_/quests/$questId/form'
| '/_authenticated/_home/quests/$questId/'
| '/_authenticated/admin/quests/$questId/'
Expand Down Expand Up @@ -567,6 +591,7 @@ export const routeTree = rootRoute
"/_authenticated/settings",
"/_authenticated/_home",
"/_authenticated/browse/",
"/_authenticated/_home_/quests/$questId/form",
"/_authenticated/admin_/quests/$questId/form"
]
},
Expand Down Expand Up @@ -654,6 +679,10 @@ export const routeTree = rootRoute
"filePath": "_authenticated/_home/quests.$questId.edit.tsx",
"parent": "/_authenticated/_home"
},
"/_authenticated/_home_/quests/$questId/form": {
"filePath": "_authenticated/_home_.quests.$questId.form.tsx",
"parent": "/_authenticated"
},
"/_authenticated/admin_/quests/$questId/form": {
"filePath": "_authenticated/admin_/quests.$questId.form.tsx",
"parent": "/_authenticated"
Expand Down
10 changes: 8 additions & 2 deletions src/routes/_authenticated/_home/quests.$questId.edit.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { AppContent, PageHeader } from "@/components/app";
import { Badge, Empty, Link, RichText } from "@/components/common";
import { QuestCosts, QuestTimeRequired, QuestUrls } from "@/components/quests";
import {
QuestCosts,
QuestForm,
QuestTimeRequired,
QuestUrls,
} from "@/components/quests";
import { api } from "@convex/_generated/api";
import type { Id } from "@convex/_generated/dataModel";
import { createFileRoute, redirect, useNavigate } from "@tanstack/react-router";
Expand Down Expand Up @@ -82,10 +87,11 @@ function QuestEditRoute() {
Save
</Link>
</PageHeader>
<div className="flex gap-4 mb-4 lg:mb-6 xl:mb-8">
<div className="flex gap-4 mb-4">
<QuestCosts quest={quest} editable />
<QuestTimeRequired quest={quest} editable />
</div>
<QuestForm quest={quest} editable />
<QuestUrls urls={quest.urls} />
<RichText initialContent={quest.content} onChange={setContent} />
</AppContent>
Expand Down
2 changes: 1 addition & 1 deletion src/routes/_authenticated/_home/quests.$questId.index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ function QuestDetailRoute() {
</Menu>
</MenuTrigger>
</PageHeader>
<div className="flex gap-4 mb-4 lg:mb-6 xl:mb-8">
<div className="flex gap-4 mb-4">
<QuestCosts quest={quest} />
<QuestTimeRequired quest={quest} />
</div>
Expand Down
57 changes: 57 additions & 0 deletions src/routes/_authenticated/_home_.quests.$questId.form.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { api } from "@convex/_generated/api";
import type { Id } from "@convex/_generated/dataModel";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { useMutation, useQuery } from "convex/react";
import { useTheme } from "next-themes";
import type { CompleteEvent, SurveyModel } from "survey-core";
import {
LayeredDarkPanelless,
LayeredLightPanelless,
} from "survey-core/themes";
import { Model, Survey } from "survey-react-ui";

export const Route = createFileRoute(
"/_authenticated/_home_/quests/$questId/form",
)({
component: QuestFormRoute,
});

function QuestFormRoute() {
const navigate = useNavigate();
const { questId } = Route.useParams();
const { resolvedTheme } = useTheme();
const quest = useQuery(api.quests.getById, {
questId: questId as Id<"quests">,
});

const saveData = useMutation(api.userFormData.set);

const survey = new Model(quest?.formSchema);
survey.applyTheme(
resolvedTheme === "dark" ? LayeredDarkPanelless : LayeredLightPanelless,
);

const handleSubmit = async (sender: SurveyModel, _options: CompleteEvent) => {
try {
// Validate JSON against schema and remove invalid values
sender.clearIncorrectValues(true);
for (const key in sender.data) {
const question = sender.getQuestionByName(key);
if (question) {
saveData({
field: key,
value: question.value,
});
}
}

navigate({ to: "/quests/$questId", params: { questId } });
} catch (err) {
console.error(err);
}
};

survey.onComplete.add(handleSubmit);

return <Survey model={survey} />;
}
23 changes: 11 additions & 12 deletions src/routes/_authenticated/admin_/quests.$questId.form.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import "survey-core/defaultV2.min.css";
import "survey-creator-core/survey-creator-core.min.css";

import { Badge, Button, Link } from "@/components/common";
import { Badge, Button } from "@/components/common";
import { api } from "@convex/_generated/api";
import type { Id } from "@convex/_generated/dataModel";
import { createFileRoute, useNavigate } from "@tanstack/react-router";
import { createFileRoute, useRouter } from "@tanstack/react-router";
import { useMutation, useQuery } from "convex/react";
import { CircleCheck, Pencil } from "lucide-react";
import { useEffect, useState } from "react";
Expand All @@ -19,11 +19,12 @@ export const Route = createFileRoute(

function AdminQuestSurveyRoute() {
const { questId } = Route.useParams();
const navigate = useNavigate();
const router = useRouter();
const [isPending, setIsPending] = useState(false);
const quest = useQuery(api.quests.getById, {
questId: questId as Id<"quests">,
});

const setFormSchema = useMutation(api.quests.setFormSchema);

const creator = new SurveyCreator({
Expand Down Expand Up @@ -62,7 +63,7 @@ function AdminQuestSurveyRoute() {
const handleSaveAndExit = () => {
try {
creator.saveSurvey();
navigate({ to: "/admin/quests/$questId", params: { questId } });
router.history.back();
toast.success("Saved form changes");
} catch (err) {
console.error(err);
Expand All @@ -79,16 +80,14 @@ function AdminQuestSurveyRoute() {
{quest?.jurisdiction && <Badge>{quest.jurisdiction}</Badge>}
</h1>
<div className="flex gap-2 items-center">
<Link
href={{ to: "/admin/quests/$questId", params: { questId } }}
button={{
variant: "ghost",
size: "small",
isDisabled: isPending,
}}
<Button
variant="ghost"
onPress={() => router.history.back()}
size="small"
isDisabled={isPending}
>
Discard changes
</Link>
</Button>
<Button
variant="primary"
onPress={handleSaveAndExit}
Expand Down

0 comments on commit 80235ba

Please sign in to comment.