Skip to content

Commit

Permalink
feat: Support adding questFields to a questStep; display form fie…
Browse files Browse the repository at this point in the history
…lds within a quest (#113)
  • Loading branch information
evadecker authored Sep 29, 2024
1 parent dc09f49 commit 64475c3
Show file tree
Hide file tree
Showing 8 changed files with 230 additions and 75 deletions.
5 changes: 5 additions & 0 deletions .changeset/seven-radios-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Display form fields within quest steps
13 changes: 13 additions & 0 deletions convex/questFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@ export const getAllFields = query({
},
});

export const getFields = query({
args: { fieldIds: v.array(v.id("questFields")) },
handler: async (ctx, args) => {
const fields = await Promise.all(
args.fieldIds.map(async (fieldId) => {
const field = await ctx.db.get(fieldId);
if (field) return field;
}),
).then((fields) => fields.filter((field) => field !== undefined));
return fields;
},
});

export const createField = userMutation({
args: {
type: field,
Expand Down
2 changes: 2 additions & 0 deletions convex/questSteps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ export const create = userMutation({
questId: v.id("quests"),
title: v.string(),
description: v.optional(v.string()),
fields: v.optional(v.array(v.id("questFields"))),
},
handler: async (ctx, args) => {
const questStepId = await ctx.db.insert("questSteps", {
questId: args.questId,
title: args.title,
description: args.description,
creationUser: ctx.userId,
fields: args.fields,
});

const quest = await ctx.db.get(args.questId);
Expand Down
10 changes: 2 additions & 8 deletions convex/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,7 @@ const questSteps = defineTable({
creationUser: v.id("users"),
title: v.string(),
description: v.optional(v.string()),
fields: v.optional(
v.array(
v.object({
fieldId: v.id("questFields"),
}),
),
),
fields: v.optional(v.array(v.id("questFields"))),
}).index("questId", ["questId"]);

/**
Expand Down Expand Up @@ -115,7 +109,7 @@ const userQuests = defineTable({
*/
const userData = defineTable({
userId: v.id("users"),
fieldId: v.id("fields"),
fieldId: v.id("questFields"),
value: v.string(),
}).index("userId", ["userId"]);

Expand Down
67 changes: 41 additions & 26 deletions src/components/Checkbox/Checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
Checkbox as AriaCheckbox,
CheckboxGroup as AriaCheckboxGroup,
type CheckboxGroupProps as AriaCheckboxGroupProps,
type CheckboxProps,
type CheckboxProps as AriaCheckboxProps,
type ValidationResult,
composeRenderProps,
} from "react-aria-components";
Expand Down Expand Up @@ -40,7 +40,7 @@ export function CheckboxGroup(props: CheckboxGroupProps) {
}

const checkboxStyles = tv({
base: "flex gap-2 items-center group text-sm transition",
base: "flex gap-2 items-center group transition w-max",
variants: {
isDisabled: {
false: "text-gray-normal cursor-pointer",
Expand All @@ -58,7 +58,7 @@ const boxStyles = tv({
true: "bg-purple-9 dark:bg-purpledark-9 border-transparent",
},
isInvalid: {
true: "text-red-9 dark:text-reddark-9 forced-colors:![--color:Mark] group-pressed:[--color:theme(colors.red.800)] dark:group-pressed:[--color:theme(colors.red.700)]",
true: "text-red-9 dark:text-reddark-9 forced-colors:![--color:Mark]",
},
isDisabled: {
true: "text-gray-7 dark:text-graydark-7 forced-colors:![--color:GrayText]",
Expand All @@ -69,31 +69,46 @@ const boxStyles = tv({
const iconStyles =
"w-5 h-5 text-white group-disabled:text-gray-4 dark:group-disabled:text-gray-9 forced-colors:text-[HighlightText]";

export interface CheckboxProps extends AriaCheckboxProps {
label?: string;
description?: string;
}

export function Checkbox(props: CheckboxProps) {
return (
<AriaCheckbox
{...props}
className={composeRenderProps(props.className, (className, renderProps) =>
checkboxStyles({ ...renderProps, className }),
)}
>
{({ isSelected, isIndeterminate, ...renderProps }) => (
<>
<div
className={boxStyles({
isSelected: isSelected || isIndeterminate,
...renderProps,
})}
>
{isIndeterminate ? (
<RiSubtractLine aria-hidden className={iconStyles} />
) : isSelected ? (
<RiCheckLine aria-hidden className={iconStyles} />
) : null}
</div>
{props.children}
</>
<div>
<AriaCheckbox
{...props}
className={composeRenderProps(
props.className,
(className, renderProps) =>
checkboxStyles({ ...renderProps, className }),
)}
>
{({ isSelected, isIndeterminate, ...renderProps }) => (
<>
<div
className={boxStyles({
isSelected: isSelected || isIndeterminate,
...renderProps,
})}
>
{isIndeterminate ? (
<RiSubtractLine aria-hidden className={iconStyles} />
) : isSelected ? (
<RiCheckLine aria-hidden className={iconStyles} />
) : null}
</div>
{props.label}
{props.children}
</>
)}
</AriaCheckbox>
{props.description && (
<FieldDescription className="ml-8">
{props.description}
</FieldDescription>
)}
</AriaCheckbox>
</div>
);
}
88 changes: 88 additions & 0 deletions src/components/QuestStep/QuestStep.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { api } from "@convex/_generated/api";
import type { Doc, Id } from "@convex/_generated/dataModel";
import { useQuery } from "convex/react";
import Markdown from "react-markdown";
import { Card } from "../Card";
import { Checkbox } from "../Checkbox";
import { DateField } from "../DateField";
import { NumberField } from "../NumberField";
import { Select, SelectItem } from "../Select";
import { TextArea } from "../TextArea";
import { TextField } from "../TextField";

export interface QuestStepProps {
title: string;
description?: string;
fields?: Id<"questFields">[];
}

export function QuestFields(props: { questFields: Doc<"questFields">[] }) {
const markupForField = (field: Doc<"questFields">) => {
switch (field.type) {
case "text":
return (
<TextField
label={field.label}
description={field.helpText}
type="text"
/>
);
case "email":
return (
<TextField
label={field.label}
description={field.helpText}
type="email"
/>
);
case "phone":
return (
<TextField
label={field.label}
description={field.helpText}
type="tel"
/>
);
case "textarea":
return <TextArea label={field.label} description={field.helpText} />;
case "number":
return <NumberField label={field.label} description={field.helpText} />;
case "select":
return (
<Select label={field.label} description={field.helpText}>
{/* TODO: Add select options */}
<SelectItem />
</Select>
);
case "checkbox":
return <Checkbox label={field.label} description={field.helpText} />;
case "date":
return <DateField label={field.label} description={field.helpText} />;
default:
return undefined;
}
};

// TODO: Add skeleton loaders
return props.questFields.map((field) => (
<div key={field._id}>{markupForField(field)}</div>
));
}

export function QuestStep({ title, description, fields }: QuestStepProps) {
const questFields = useQuery(api.questFields.getFields, {
fieldIds: fields ?? [],
});

return (
<Card className="flex flex-col gap-2">
<h2 className="text-xl font-semibold">{title}</h2>
{description && (
<div>
<Markdown className="prose dark:prose-invert">{description}</Markdown>
</div>
)}
{questFields && <QuestFields questFields={questFields} />}
</Card>
);
}
Loading

0 comments on commit 64475c3

Please sign in to comment.