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",
|
"next": "^14.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.49.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"svix": "^1.15.0",
|
"svix": "^1.15.0",
|
||||||
@@ -4185,6 +4186,22 @@
|
|||||||
"react": "^18.2.0"
|
"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": {
|
"node_modules/react-ssr-prepass": {
|
||||||
"version": "1.5.0",
|
"version": "1.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz",
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"next": "^14.0.3",
|
"next": "^14.0.3",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
|
"react-hook-form": "^7.49.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"svix": "^1.15.0",
|
"svix": "^1.15.0",
|
||||||
|
|||||||
@@ -1,39 +1,63 @@
|
|||||||
"use client";
|
"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() {
|
interface FormInputs {
|
||||||
const [selectedLanguage, setSelectedLanguage] = useState("");
|
languageId: bigint;
|
||||||
|
}
|
||||||
|
|
||||||
const handleLanguageChange = (
|
interface Props {
|
||||||
event: React.ChangeEvent<HTMLSelectElement>,
|
courses: InferSelectModel<typeof language>[];
|
||||||
) => {
|
activeLanguage?: bigint;
|
||||||
setSelectedLanguage(event.target.value);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = (event: FormEvent) => {
|
export default function CourseSelection({ courses, activeLanguage }: Props) {
|
||||||
event.preventDefault();
|
const { mutate, error } = api.language.setActiveLanguage.useMutation({
|
||||||
// Handle form submission logic here, e.g., send selectedLanguage to the server
|
onSuccess: () => {
|
||||||
console.log("Selected language:", selectedLanguage);
|
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 (
|
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>
|
<label htmlFor="course-select">Select a language:</label>
|
||||||
<select
|
<select id="course-select" className="border" {...register("languageId")}>
|
||||||
id="course-select"
|
|
||||||
name="course"
|
|
||||||
value={selectedLanguage}
|
|
||||||
onChange={handleLanguageChange}
|
|
||||||
className="border"
|
|
||||||
>
|
|
||||||
<option value="">Select...</option>
|
<option value="">Select...</option>
|
||||||
<option value="Japanese">Japanese 🇯🇵</option>
|
{courses.map(({ name, emoji, languageId }) => (
|
||||||
<option value="French">French 🇫🇷</option>
|
<option value={languageId}>
|
||||||
|
{name} {emoji}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
</select>
|
</select>
|
||||||
|
{errors.languageId && (
|
||||||
|
<p className="text-sm text-red-500">{errors.languageId.message}</p>
|
||||||
|
)}
|
||||||
<button type="submit" className="border">
|
<button type="submit" className="border">
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</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>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import CourseSelection from "~/app/courses/course-selection";
|
import CourseSelection from "~/app/courses/course-selection";
|
||||||
|
import { api } from "~/trpc/server";
|
||||||
|
|
||||||
export default async function Course() {
|
export default async function Course() {
|
||||||
|
const { languages } = await api.language.getAllLanguages();
|
||||||
|
const activeLanguage = await api.language.getActiveLanguage();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="">
|
<main className="mx-auto max-w-2xl">
|
||||||
<h1 className="text-2xl font-bold">Course</h1>
|
<h1 className="text-2xl font-bold">Course</h1>
|
||||||
<h2 className="text-lg font-medium">Available languages</h2>
|
<h2 className="text-lg font-medium">Available languages</h2>
|
||||||
<CourseSelection />
|
<CourseSelection
|
||||||
|
courses={languages}
|
||||||
|
activeLanguage={activeLanguage?.activeLanguageId ?? undefined}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { api } from "~/trpc/server";
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
|
const activeLanguage = await api.user.activeLanguage();
|
||||||
|
if (!activeLanguage.language) redirect("/courses");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="">
|
<main className="mx-auto max-w-2xl">
|
||||||
<h1 className="text-2xl font-bold">Home</h1>
|
<h1 className="text-2xl font-bold">Home</h1>
|
||||||
|
<p>Active language:</p>
|
||||||
|
<pre>{JSON.stringify(activeLanguage, undefined, 2)}</pre>
|
||||||
</main>
|
</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";
|
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.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
post: postRouter,
|
user: userRouter,
|
||||||
|
language: languageRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// 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 superjson from "superjson";
|
||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import { type Context } from "~/server/api/context";
|
import { type Context } from "~/server/api/context";
|
||||||
|
import { auth } from "@clerk/nextjs/server";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
@@ -49,12 +50,16 @@ const t = initTRPC.context<Context>().create({
|
|||||||
|
|
||||||
// check if the user is signed in, otherwise throw a UNAUTHORIZED CODE
|
// check if the user is signed in, otherwise throw a UNAUTHORIZED CODE
|
||||||
const enforceUserIsAuthed = t.middleware(({ next, ctx }) => {
|
const enforceUserIsAuthed = t.middleware(({ next, ctx }) => {
|
||||||
if (!ctx.auth?.userId) {
|
const userAuth = auth();
|
||||||
|
|
||||||
|
if (!userAuth.userId) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx: {
|
||||||
auth: ctx.auth,
|
...ctx,
|
||||||
|
auth: userAuth,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const language = mysqlTable("language", {
|
|||||||
languageId: serial("languageId").primaryKey(),
|
languageId: serial("languageId").primaryKey(),
|
||||||
name: varchar("name", { length: 255 }),
|
name: varchar("name", { length: 255 }),
|
||||||
abbreviation: varchar("abbreviation", { length: 2 }),
|
abbreviation: varchar("abbreviation", { length: 2 }),
|
||||||
|
emoji: varchar("emoji", { length: 2 }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const course = mysqlTable("course", {
|
export const course = mysqlTable("course", {
|
||||||
@@ -67,9 +68,15 @@ export const courseRelations = relations(course, ({ one }) => ({
|
|||||||
}),
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const progressRelations = relations(progress, ({ many }) => ({
|
export const progressRelations = relations(progress, ({ one }) => ({
|
||||||
language: many(language),
|
language: one(language, {
|
||||||
user: many(user),
|
fields: [progress.languageId],
|
||||||
|
references: [language.languageId],
|
||||||
|
}),
|
||||||
|
user: one(user, {
|
||||||
|
fields: [progress.userId],
|
||||||
|
references: [user.userId],
|
||||||
|
}),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const contentRelations = relations(content, ({ one }) => ({
|
export const contentRelations = relations(content, ({ one }) => ({
|
||||||
|
|||||||
Reference in New Issue
Block a user