Skip to content

Commit

Permalink
feat: Add quest detail routes (#85)
Browse files Browse the repository at this point in the history
  • Loading branch information
evadecker authored Sep 18, 2024
1 parent 3232441 commit 0c3a56e
Show file tree
Hide file tree
Showing 6 changed files with 369 additions and 53 deletions.
5 changes: 5 additions & 0 deletions .changeset/thirty-cups-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"namesake": minor
---

Add quest detail routes, add the ability to mark quests complete/incomplete, and support adding new quests from the global quest list
35 changes: 35 additions & 0 deletions convex/usersQuests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,38 @@ export const create = userMutation({
});
},
});

export const getUserQuestByQuestId = userQuery({
args: { questId: v.id("quests") },
handler: async (ctx, args) => {
const userQuest = await ctx.db
.query("usersQuests")
.withIndex("userId", (q) => q.eq("userId", ctx.userId))
.filter((q) => q.eq(q.field("questId"), args.questId))
.first();

return userQuest;
},
});

export const markComplete = userMutation({
args: { questId: v.id("quests") },
handler: async (ctx, args) => {
const userQuest = await getUserQuestByQuestId(ctx, {
questId: args.questId,
});
if (userQuest === null) throw new Error("Quest not found");
await ctx.db.patch(userQuest._id, { completionTime: Date.now() });
},
});

export const markIncomplete = userMutation({
args: { questId: v.id("quests") },
handler: async (ctx, args) => {
const userQuest = await getUserQuestByQuestId(ctx, {
questId: args.questId,
});
if (userQuest === null) throw new Error("Quest not found");
await ctx.db.patch(userQuest._id, { completionTime: undefined });
},
});
63 changes: 63 additions & 0 deletions src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

import { Route as rootRoute } from './routes/__root'
import { Route as SigninImport } from './routes/signin'
import { Route as QuestsRouteImport } from './routes/quests/route'
import { Route as AdminRouteImport } from './routes/admin/route'
import { Route as IndexImport } from './routes/index'
import { Route as SettingsIndexImport } from './routes/settings/index'
import { Route as AdminIndexImport } from './routes/admin/index'
import { Route as QuestsQuestIdImport } from './routes/quests/$questId'
import { Route as AdminQuestsIndexImport } from './routes/admin/quests/index'
import { Route as AdminFormsIndexImport } from './routes/admin/forms/index'
import { Route as AdminQuestsQuestIdImport } from './routes/admin/quests/$questId'
Expand All @@ -28,6 +30,11 @@ const SigninRoute = SigninImport.update({
getParentRoute: () => rootRoute,
} as any)

const QuestsRouteRoute = QuestsRouteImport.update({
path: '/quests',
getParentRoute: () => rootRoute,
} as any)

const AdminRouteRoute = AdminRouteImport.update({
path: '/admin',
getParentRoute: () => rootRoute,
Expand All @@ -48,6 +55,11 @@ const AdminIndexRoute = AdminIndexImport.update({
getParentRoute: () => AdminRouteRoute,
} as any)

const QuestsQuestIdRoute = QuestsQuestIdImport.update({
path: '/$questId',
getParentRoute: () => QuestsRouteRoute,
} as any)

const AdminQuestsIndexRoute = AdminQuestsIndexImport.update({
path: '/quests/',
getParentRoute: () => AdminRouteRoute,
Expand Down Expand Up @@ -86,13 +98,27 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof AdminRouteImport
parentRoute: typeof rootRoute
}
'/quests': {
id: '/quests'
path: '/quests'
fullPath: '/quests'
preLoaderRoute: typeof QuestsRouteImport
parentRoute: typeof rootRoute
}
'/signin': {
id: '/signin'
path: '/signin'
fullPath: '/signin'
preLoaderRoute: typeof SigninImport
parentRoute: typeof rootRoute
}
'/quests/$questId': {
id: '/quests/$questId'
path: '/$questId'
fullPath: '/quests/$questId'
preLoaderRoute: typeof QuestsQuestIdImport
parentRoute: typeof QuestsRouteImport
}
'/admin/': {
id: '/admin/'
path: '/'
Expand Down Expand Up @@ -160,10 +186,24 @@ const AdminRouteRouteWithChildren = AdminRouteRoute._addFileChildren(
AdminRouteRouteChildren,
)

interface QuestsRouteRouteChildren {
QuestsQuestIdRoute: typeof QuestsQuestIdRoute
}

const QuestsRouteRouteChildren: QuestsRouteRouteChildren = {
QuestsQuestIdRoute: QuestsQuestIdRoute,
}

const QuestsRouteRouteWithChildren = QuestsRouteRoute._addFileChildren(
QuestsRouteRouteChildren,
)

export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren
'/quests': typeof QuestsRouteRouteWithChildren
'/signin': typeof SigninRoute
'/quests/$questId': typeof QuestsQuestIdRoute
'/admin/': typeof AdminIndexRoute
'/settings': typeof SettingsIndexRoute
'/admin/forms/$formId': typeof AdminFormsFormIdRoute
Expand All @@ -174,7 +214,9 @@ export interface FileRoutesByFullPath {

export interface FileRoutesByTo {
'/': typeof IndexRoute
'/quests': typeof QuestsRouteRouteWithChildren
'/signin': typeof SigninRoute
'/quests/$questId': typeof QuestsQuestIdRoute
'/admin': typeof AdminIndexRoute
'/settings': typeof SettingsIndexRoute
'/admin/forms/$formId': typeof AdminFormsFormIdRoute
Expand All @@ -187,7 +229,9 @@ export interface FileRoutesById {
__root__: typeof rootRoute
'/': typeof IndexRoute
'/admin': typeof AdminRouteRouteWithChildren
'/quests': typeof QuestsRouteRouteWithChildren
'/signin': typeof SigninRoute
'/quests/$questId': typeof QuestsQuestIdRoute
'/admin/': typeof AdminIndexRoute
'/settings/': typeof SettingsIndexRoute
'/admin/forms/$formId': typeof AdminFormsFormIdRoute
Expand All @@ -201,7 +245,9 @@ export interface FileRouteTypes {
fullPaths:
| '/'
| '/admin'
| '/quests'
| '/signin'
| '/quests/$questId'
| '/admin/'
| '/settings'
| '/admin/forms/$formId'
Expand All @@ -211,7 +257,9 @@ export interface FileRouteTypes {
fileRoutesByTo: FileRoutesByTo
to:
| '/'
| '/quests'
| '/signin'
| '/quests/$questId'
| '/admin'
| '/settings'
| '/admin/forms/$formId'
Expand All @@ -222,7 +270,9 @@ export interface FileRouteTypes {
| '__root__'
| '/'
| '/admin'
| '/quests'
| '/signin'
| '/quests/$questId'
| '/admin/'
| '/settings/'
| '/admin/forms/$formId'
Expand All @@ -235,13 +285,15 @@ export interface FileRouteTypes {
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
AdminRouteRoute: typeof AdminRouteRouteWithChildren
QuestsRouteRoute: typeof QuestsRouteRouteWithChildren
SigninRoute: typeof SigninRoute
SettingsIndexRoute: typeof SettingsIndexRoute
}

const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
AdminRouteRoute: AdminRouteRouteWithChildren,
QuestsRouteRoute: QuestsRouteRouteWithChildren,
SigninRoute: SigninRoute,
SettingsIndexRoute: SettingsIndexRoute,
}
Expand All @@ -260,6 +312,7 @@ export const routeTree = rootRoute
"children": [
"/",
"/admin",
"/quests",
"/signin",
"/settings/"
]
Expand All @@ -277,9 +330,19 @@ export const routeTree = rootRoute
"/admin/quests/"
]
},
"/quests": {
"filePath": "quests/route.tsx",
"children": [
"/quests/$questId"
]
},
"/signin": {
"filePath": "signin.tsx"
},
"/quests/$questId": {
"filePath": "quests/$questId.tsx",
"parent": "/quests"
},
"/admin/": {
"filePath": "admin/index.tsx",
"parent": "/admin"
Expand Down
59 changes: 6 additions & 53 deletions src/routes/index.tsx
Original file line number Diff line number Diff line change
@@ -1,56 +1,9 @@
import { RiSignpostLine } from "@remixicon/react";
import { createFileRoute } from "@tanstack/react-router";
import { Authenticated, Unauthenticated, useQuery } from "convex/react";
import { api } from "../../convex/_generated/api";
import {
Badge,
Container,
Empty,
GridList,
GridListItem,
PageHeader,
} from "../components";
import { createFileRoute, redirect } from "@tanstack/react-router";

export const Route = createFileRoute("/")({
component: IndexRoute,
beforeLoad: () => {
throw redirect({
to: "/quests",
});
},
});

function IndexRoute() {
const MyQuests = () => {
const myQuests = useQuery(api.usersQuests.getQuestsForCurrentUser);

if (myQuests === undefined) return;

if (myQuests === null || myQuests.length === 0)
return <Empty title="No quests" icon={RiSignpostLine} />;

return (
<GridList aria-label="My quests">
{myQuests.map((quest) => {
if (quest === null) return null;

return (
<GridListItem textValue={quest.title} key={quest._id}>
<div className="flex items-baseline gap-2">
<p className="font-bold text-lg">{quest.title}</p>
{quest.jurisdiction && <Badge>{quest.jurisdiction}</Badge>}
</div>
</GridListItem>
);
})}
</GridList>
);
};

return (
<Container>
<Authenticated>
<PageHeader title="My Quests" />
<MyQuests />
</Authenticated>
<Unauthenticated>
<h1>Please log in</h1>
</Unauthenticated>
</Container>
);
}
87 changes: 87 additions & 0 deletions src/routes/quests/$questId.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { RiMoreLine } from "@remixicon/react";
import { createFileRoute } from "@tanstack/react-router";
import { useMutation, useQuery } from "convex/react";
import Markdown from "react-markdown";
import { api } from "../../../convex/_generated/api";
import type { Id } from "../../../convex/_generated/dataModel";
import {
Badge,
Button,
Menu,
MenuItem,
MenuTrigger,
PageHeader,
} from "../../components";

export const Route = createFileRoute("/quests/$questId")({
component: QuestDetailRoute,
});

function QuestDetailRoute() {
const { questId } = Route.useParams();
// TODO: Opportunity to combine the `quest` and `userQuest` queries
const quest = useQuery(api.quests.getQuest, {
questId: questId as Id<"quests">,
});
const userQuest = useQuery(api.usersQuests.getUserQuestByQuestId, {
questId: questId as Id<"quests">,
});
const markComplete = useMutation(api.usersQuests.markComplete);
const markIncomplete = useMutation(api.usersQuests.markIncomplete);

const handleMarkComplete = (questId: Id<"quests">) =>
markComplete({ questId });
const handleMarkIncomplete = (questId: Id<"quests">) =>
markIncomplete({ questId });

if (quest === undefined || userQuest === undefined) return;
if (quest === null || userQuest === null) return "Quest not found";

return (
<main>
<PageHeader
title={quest.title}
badge={
<div className="flex gap-1">
<Badge>{quest.jurisdiction}</Badge>
{userQuest?.completionTime ? (
<Badge variant="success">Complete</Badge>
) : (
<Badge variant="info">In progress</Badge>
)}
</div>
}
>
<MenuTrigger>
<Button aria-label="Quest settings" variant="icon">
<RiMoreLine />
</Button>
<Menu>
{!userQuest.completionTime && (
<MenuItem onAction={() => handleMarkComplete(quest._id)}>
Mark complete
</MenuItem>
)}
{userQuest.completionTime && (
<MenuItem onAction={() => handleMarkIncomplete(quest._id)}>
Mark as in progress
</MenuItem>
)}
</Menu>
</MenuTrigger>
</PageHeader>
{quest.steps ? (
<ol className="flex flex-col gap-4">
{quest.steps.map((step, i) => (
<li key={`${quest.title}-step-${i}`}>
<h2>{step.title}</h2>
<Markdown>{step.body}</Markdown>
</li>
))}
</ol>
) : (
<p>This quest has no steps yet.</p>
)}
</main>
);
}
Loading

0 comments on commit 0c3a56e

Please sign in to comment.