feat: added language selection to courses page

This commit is contained in:
2023-12-11 01:13:08 -06:00
parent 09c1a48d18
commit ca0efb56cb
11 changed files with 151 additions and 49 deletions

17
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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<HTMLSelectElement>,
) => {
setSelectedLanguage(event.target.value);
};
interface Props {
courses: InferSelectModel<typeof language>[];
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<FormInputs>({ defaultValues: { languageId: activeLanguage } });
const onSubmit: SubmitHandler<FormInputs> = async (data) => {
if (data.languageId) {
mutate({ languageId: BigInt(data.languageId) });
} else {
setError("languageId", { message: "You must select a valid language." });
}
};
return (
<form className="flex flex-col gap-y-2" onSubmit={handleSubmit}>
<form className="flex flex-col gap-y-2" onSubmit={handleSubmit(onSubmit)}>
<label htmlFor="course-select">Select a language:</label>
<select
id="course-select"
name="course"
value={selectedLanguage}
onChange={handleLanguageChange}
className="border"
>
<select id="course-select" className="border" {...register("languageId")}>
<option value="">Select...</option>
<option value="Japanese">Japanese 🇯🇵</option>
<option value="French">French 🇫🇷</option>
{courses.map(({ name, emoji, languageId }) => (
<option value={languageId}>
{name} {emoji}
</option>
))}
</select>
{errors.languageId && (
<p className="text-sm text-red-500">{errors.languageId.message}</p>
)}
<button type="submit" className="border">
Submit
</button>
{errors.root && (
<p className="text-sm text-red-500">{errors.root.message}</p>
)}
{error && <p className="text-sm text-red-500">{error.message}</p>}
</form>
);
}

View File

@@ -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 (
<main className="">
<main className="mx-auto max-w-2xl">
<h1 className="text-2xl font-bold">Course</h1>
<h2 className="text-lg font-medium">Available languages</h2>
<CourseSelection />
<CourseSelection
courses={languages}
activeLanguage={activeLanguage?.activeLanguageId ?? undefined}
/>
</main>
);
}

View File

@@ -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 (
<main className="">
<main className="mx-auto max-w-2xl">
<h1 className="text-2xl font-bold">Home</h1>
<p>Active language:</p>
<pre>{JSON.stringify(activeLanguage, undefined, 2)}</pre>
</main>
);
}

View File

@@ -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

View File

@@ -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));
}),
});

View File

@@ -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}`,
};
}),
});

View File

@@ -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,
};
}),
});

View File

@@ -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<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,
},
});
});

View File

@@ -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 }) => ({