From ca0efb56cb9ce63ff616d7bc252aaa1687a4094d Mon Sep 17 00:00:00 2001 From: Zeke Abshire Date: Mon, 11 Dec 2023 01:13:08 -0600 Subject: [PATCH] feat: added language selection to courses page --- package-lock.json | 17 +++++++ package.json | 1 + src/app/courses/course-selection.tsx | 68 +++++++++++++++++++--------- src/app/courses/page.tsx | 11 ++++- src/app/home/page.tsx | 10 +++- src/server/api/root.ts | 6 ++- src/server/api/routers/language.ts | 31 +++++++++++++ src/server/api/routers/post.ts | 17 ------- src/server/api/routers/user.ts | 17 +++++++ src/server/api/trpc.ts | 9 +++- src/server/db/schema.ts | 13 ++++-- 11 files changed, 151 insertions(+), 49 deletions(-) create mode 100644 src/server/api/routers/language.ts delete mode 100644 src/server/api/routers/post.ts create mode 100644 src/server/api/routers/user.ts diff --git a/package-lock.json b/package-lock.json index b9b67db..ccdfc4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "next": "^14.0.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.49.0", "server-only": "^0.0.1", "superjson": "^2.2.1", "svix": "^1.15.0", @@ -4185,6 +4186,22 @@ "react": "^18.2.0" } }, + "node_modules/react-hook-form": { + "version": "7.49.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.0.tgz", + "integrity": "sha512-gf4qyY4WiqK2hP/E45UUT6wt3Khl49pleEVcIzxhLBrD6m+GMWtLRk0vMrRv45D1ZH8PnpXFwRPv0Pewske2jw==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-ssr-prepass": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz", diff --git a/package.json b/package.json index 21eac4c..8185801 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "next": "^14.0.3", "react": "18.2.0", "react-dom": "18.2.0", + "react-hook-form": "^7.49.0", "server-only": "^0.0.1", "superjson": "^2.2.1", "svix": "^1.15.0", diff --git a/src/app/courses/course-selection.tsx b/src/app/courses/course-selection.tsx index e6e95b3..aed4909 100644 --- a/src/app/courses/course-selection.tsx +++ b/src/app/courses/course-selection.tsx @@ -1,39 +1,63 @@ "use client"; -import { type FormEvent, useState } from "react"; +import { type InferSelectModel } from "drizzle-orm"; +import { redirect } from "next/navigation"; +import { useForm, type SubmitHandler } from "react-hook-form"; +import { type language } from "~/server/db/schema"; +import { api } from "~/trpc/react"; -export default function CourseSelection() { - const [selectedLanguage, setSelectedLanguage] = useState(""); +interface FormInputs { + languageId: bigint; +} - const handleLanguageChange = ( - event: React.ChangeEvent, - ) => { - setSelectedLanguage(event.target.value); - }; +interface Props { + courses: InferSelectModel[]; + activeLanguage?: bigint; +} - const handleSubmit = (event: FormEvent) => { - event.preventDefault(); - // Handle form submission logic here, e.g., send selectedLanguage to the server - console.log("Selected language:", selectedLanguage); +export default function CourseSelection({ courses, activeLanguage }: Props) { + const { mutate, error } = api.language.setActiveLanguage.useMutation({ + onSuccess: () => { + redirect("/home"); + }, + }); + + const { + register, + handleSubmit, + formState: { errors }, + setError, + } = useForm({ defaultValues: { languageId: activeLanguage } }); + + const onSubmit: SubmitHandler = async (data) => { + if (data.languageId) { + mutate({ languageId: BigInt(data.languageId) }); + } else { + setError("languageId", { message: "You must select a valid language." }); + } }; return ( -
+ - - - + {courses.map(({ name, emoji, languageId }) => ( + + ))} + {errors.languageId && ( +

{errors.languageId.message}

+ )} + {errors.root && ( +

{errors.root.message}

+ )} + {error &&

{error.message}

}
); } diff --git a/src/app/courses/page.tsx b/src/app/courses/page.tsx index d4eaa9e..d49b75a 100644 --- a/src/app/courses/page.tsx +++ b/src/app/courses/page.tsx @@ -1,11 +1,18 @@ import CourseSelection from "~/app/courses/course-selection"; +import { api } from "~/trpc/server"; export default async function Course() { + const { languages } = await api.language.getAllLanguages(); + const activeLanguage = await api.language.getActiveLanguage(); + return ( -
+

Course

Available languages

- +
); } diff --git a/src/app/home/page.tsx b/src/app/home/page.tsx index 60b81b1..c031c09 100644 --- a/src/app/home/page.tsx +++ b/src/app/home/page.tsx @@ -1,7 +1,15 @@ +import { redirect } from "next/navigation"; +import { api } from "~/trpc/server"; + export default async function Home() { + const activeLanguage = await api.user.activeLanguage(); + if (!activeLanguage.language) redirect("/courses"); + return ( -
+

Home

+

Active language:

+
{JSON.stringify(activeLanguage, undefined, 2)}
); } diff --git a/src/server/api/root.ts b/src/server/api/root.ts index 3d629a7..1a168a6 100644 --- a/src/server/api/root.ts +++ b/src/server/api/root.ts @@ -1,4 +1,5 @@ -import { postRouter } from "~/server/api/routers/post"; +import { languageRouter } from "~/server/api/routers/language"; +import { userRouter } from "~/server/api/routers/user"; import { createTRPCRouter } from "~/server/api/trpc"; /** @@ -7,7 +8,8 @@ import { createTRPCRouter } from "~/server/api/trpc"; * All routers added in /api/routers should be manually added here. */ export const appRouter = createTRPCRouter({ - post: postRouter, + user: userRouter, + language: languageRouter, }); // export type definition of API diff --git a/src/server/api/routers/language.ts b/src/server/api/routers/language.ts new file mode 100644 index 0000000..14065bd --- /dev/null +++ b/src/server/api/routers/language.ts @@ -0,0 +1,31 @@ +import { eq } from "drizzle-orm"; +import { z } from "zod"; +import { + createTRPCRouter, + protectedProcedure, + publicProcedure, +} from "~/server/api/trpc"; +import { user } from "~/server/db/schema"; + +export const languageRouter = createTRPCRouter({ + getAllLanguages: publicProcedure.query(async ({ ctx }) => { + const languages = await ctx.db.query.language.findMany(); + return { languages }; + }), + getActiveLanguage: protectedProcedure.query(async ({ ctx }) => { + const activeLanguage = await ctx.db.query.user.findFirst({ + columns: { activeLanguageId: true }, + where: eq(user.userId, ctx.auth.userId), + }); + + return activeLanguage; + }), + setActiveLanguage: protectedProcedure + .input(z.object({ languageId: z.bigint() })) + .mutation(async ({ ctx, input }) => { + await ctx.db + .update(user) + .set({ activeLanguageId: input.languageId }) + .where(eq(user.userId, ctx.auth.userId)); + }), +}); diff --git a/src/server/api/routers/post.ts b/src/server/api/routers/post.ts deleted file mode 100644 index 8695195..0000000 --- a/src/server/api/routers/post.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; - -import { - createTRPCRouter, - protectedProcedure, - publicProcedure, -} from "~/server/api/trpc"; - -export const postRouter = createTRPCRouter({ - hello: publicProcedure - .input(z.object({ text: z.string() })) - .query(({ input }) => { - return { - greeting: `Hello ${input.text}`, - }; - }), -}); diff --git a/src/server/api/routers/user.ts b/src/server/api/routers/user.ts new file mode 100644 index 0000000..b9f3987 --- /dev/null +++ b/src/server/api/routers/user.ts @@ -0,0 +1,17 @@ +import { eq } from "drizzle-orm"; +import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc"; +import { user } from "~/server/db/schema"; + +export const userRouter = createTRPCRouter({ + activeLanguage: protectedProcedure.query(async ({ ctx }) => { + const currentUser = await ctx.db.query.user.findFirst({ + columns: {}, + where: eq(user.userId, ctx.auth.userId), + with: { language: true }, + }); + + return { + language: currentUser?.language, + }; + }), +}); diff --git a/src/server/api/trpc.ts b/src/server/api/trpc.ts index 6ef9077..ba5c8e6 100644 --- a/src/server/api/trpc.ts +++ b/src/server/api/trpc.ts @@ -10,6 +10,7 @@ import { ZodError } from "zod"; import superjson from "superjson"; import { initTRPC, TRPCError } from "@trpc/server"; import { type Context } from "~/server/api/context"; +import { auth } from "@clerk/nextjs/server"; /** * 1. CONTEXT @@ -49,12 +50,16 @@ const t = initTRPC.context().create({ // check if the user is signed in, otherwise throw a UNAUTHORIZED CODE const enforceUserIsAuthed = t.middleware(({ next, ctx }) => { - if (!ctx.auth?.userId) { + const userAuth = auth(); + + if (!userAuth.userId) { throw new TRPCError({ code: "UNAUTHORIZED" }); } + return next({ ctx: { - auth: ctx.auth, + ...ctx, + auth: userAuth, }, }); }); diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index 207f240..c69b1f7 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -24,6 +24,7 @@ export const language = mysqlTable("language", { languageId: serial("languageId").primaryKey(), name: varchar("name", { length: 255 }), abbreviation: varchar("abbreviation", { length: 2 }), + emoji: varchar("emoji", { length: 2 }), }); export const course = mysqlTable("course", { @@ -67,9 +68,15 @@ export const courseRelations = relations(course, ({ one }) => ({ }), })); -export const progressRelations = relations(progress, ({ many }) => ({ - language: many(language), - user: many(user), +export const progressRelations = relations(progress, ({ one }) => ({ + language: one(language, { + fields: [progress.languageId], + references: [language.languageId], + }), + user: one(user, { + fields: [progress.userId], + references: [user.userId], + }), })); export const contentRelations = relations(content, ({ one }) => ({