feat: added language selection to courses page
This commit is contained in:
17
package-lock.json
generated
17
package-lock.json
generated
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
31
src/server/api/routers/language.ts
Normal file
31
src/server/api/routers/language.ts
Normal 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));
|
||||
}),
|
||||
});
|
||||
@@ -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}`,
|
||||
};
|
||||
}),
|
||||
});
|
||||
17
src/server/api/routers/user.ts
Normal file
17
src/server/api/routers/user.ts
Normal 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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
@@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 }) => ({
|
||||
|
||||
Reference in New Issue
Block a user