Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/email verification flow completed #233

Merged
merged 12 commits into from
May 29, 2024
17 changes: 16 additions & 1 deletion apps/academy/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import localFont from "next/font/local";
import { useRouter } from "next/router";
import type { FunctionComponent, PropsWithChildren } from "react";
import { type FunctionComponent, type PropsWithChildren,useEffect, useState } from "react";
import { Footer } from "ui";

import { Header } from "@/components/Header";
import { RequestEmailDialog } from "@/components/RequestEmailDialog";
import { api } from "@/utils/api";

const bttf = localFont({
src: "../../public/fonts/BTTF.ttf",
Expand All @@ -23,8 +25,21 @@ const fontVars = `${bttf.variable} ${deathstar.variable} ${andale.variable}`;
export const Layout: FunctionComponent<PropsWithChildren> = ({ children }) => {
const router = useRouter();
const { pathname } = router;
const [requestEmail, setRequestEmail] = useState(false);

const { data: userEmailData } = api.user.getUserEmail.useQuery();

useEffect(() => {
if (userEmailData) {
if (userEmailData.email === null) {
setRequestEmail(true);
}
}
}, [userEmailData]);

return (
<>
<RequestEmailDialog open={requestEmail} setIsOpen={() => { setRequestEmail(false); }} />
<Header />
<main className={fontVars}>{children}</main>
{pathname !== "/tracks" && pathname !== "/fundamentals" ? <Footer /> : null}
Expand Down
93 changes: 93 additions & 0 deletions apps/academy/src/components/RequestEmailDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { type Dispatch, type SetStateAction, useState } from "react";
import { Button } from "ui";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "ui";
import { Input } from "ui";
import { Label } from "ui";
import { useToast } from "ui";

import { api } from "@/utils/api";

interface Props {
open: boolean;
setIsOpen: Dispatch<SetStateAction<boolean>>;
}

export function RequestEmailDialog({ open, setIsOpen }: Props) {
const { toast } = useToast();
const [userEmail, setUserEmail] = useState("");
const [saveBtnClicked, setSaveBtnClicked] = useState(false);

const { mutate: saveUserEmail } = api.user.addEmail.useMutation({
onSuccess: () => {
toast({
title: "Amazing!",
description: "Now Check your inbox to verify your email address",
});
setSaveBtnClicked(true);
const timer = setTimeout(() => { setIsOpen(false); }, 350);
return () => { clearTimeout(timer); };
},
});

const handleSaveBtnClick = (e: any) => {
e.preventDefault();
if (userEmail !== "") {
saveUserEmail(userEmail);
}
};

return (
<Dialog open={open}>
<DialogContent
className=" h-fit w-fit gap-10 border-[#44AF96] bg-black"
onEscapeKeyDown={(e) => {
e.preventDefault();
}}
onPointerDown={(e) => {
e.preventDefault();
}}
onInteractOutside={(e) => {
e.preventDefault();
}}
>
<DialogHeader className="gap-2">
<DialogTitle className="text-3xl text-[#44AF96]">Configure your email</DialogTitle>
</DialogHeader>
<div className="flex h-fit flex-col gap-10">
<div className="flex flex-col gap-10">
<Label htmlFor="collaborators" className="text-left text-sm text-[#999999]">
Your email address will be used to receive notifications about updates, join the
frens!
</Label>
<Input
id="email"
type="email" //TODO: VALIDATE EMAIL FORMAT VALID EMAIL FORMAT https://ui.shadcn.com/docs/components/input#form
placeholder="Keep up to date with Academy's updates!"
className="col-span-3 border-0 bg-[#333333] text-[#999999]"
value={userEmail}
onChange={(e) => {
setUserEmail(e.target.value);
}}
/>
</div>
</div>
<DialogFooter className="w-full justify-end">
{!saveBtnClicked ? (
<Button
variant="primary"
disabled={!userEmail}
onClick={handleSaveBtnClick}
className="disabled:bg-gray-600 disabled:hover:bg-gray-500"
>
Save
</Button>
) : (
<Label htmlFor="collaborators" className="w-full text-center text-sm text-[#999999]">
You will receive a verification email, check your inbox!
</Label>
)}
</DialogFooter>
</DialogContent>
</Dialog>
);
}
88 changes: 0 additions & 88 deletions apps/academy/src/contexts/AppContextProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ interface PropsInterface {
export function AppContextProvider({ children }: PropsInterface) {
const [completedQuizzesIds, setCompletedQuizzesIds] = useState<string[]>([]);
const [sessionDataUser, setSessionDataUser] = useState<any>(null);
// const [formattedAllTracksData, setFormattedAllTracksData] = useState<TrackWithTags>([]);

const { data: sessionData, status: sessionStatus } = useSession();
const { address, status: walletStatus } = useAccount();
Expand Down Expand Up @@ -57,27 +56,6 @@ export function AppContextProvider({ children }: PropsInterface) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [completedQuizzesAllData]);

// const fetchFromDirs = async () => {
// const lessonsData = await fetch("/api/readfiles").then(async (res) => res.json());

// const lessonsFormatResult: FormatedLessonInterface = lessonsData.reduce(
// (acc: any, curr: any) => {
// if (acc[curr.path] === undefined) acc[curr.path] = [];

// acc[curr.path].push(curr);
// return acc as Project | Fundamental;
// },
// {},
// );

// setFundamentals(lessonsFormatResult.fundamentals);
// setProjects(lessonsFormatResult.projects);
// };

// useEffect(() => {
// void fetchFromDirs();
// }, []);

// - Get All Tracks data
const { data: allTracksData, isLoading: allTracksDataIsLoading } = api.tracks.getAll.useQuery(
undefined,
Expand All @@ -94,72 +72,6 @@ export function AppContextProvider({ children }: PropsInterface) {
},
);

// useEffect(() => {
// if (
// allLessonsData !== undefined &&
// projects !== undefined &&
// completedQuizzesIds.length !== 0
// ) {
// const projectsWithCompleteStatus = projects.map((lesson) => {
// const currentLessonId = allLessonsData.find(
// (lessonData: any) =>
// lessonData.projectLessonNumber?.toString() === lesson.slug.toString(), // DEV_NOTE: forcing .toString() to avoid type errors
// )?.id;

// const completed =
// currentLessonId !== undefined && completedQuizzesIds.includes(currentLessonId)
// ? true
// : false; // DEV_NOTE: if the lesson is not found, it is not completed
// return { ...lesson, completed };
// });

// setProjects(projectsWithCompleteStatus);
// } else if (
// allLessonsData !== undefined &&
// projects !== undefined &&
// completedQuizzesIds.length === 0
// ) {
// const projectsWithCompleteStatus = projects.map((lesson) => {
// return { ...lesson, completed: false };
// });
// setProjects(projectsWithCompleteStatus);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [completedQuizzesIds]);

// useEffect(() => {
// if (
// allLessonsData !== undefined &&
// fundamentals !== undefined &&
// completedQuizzesIds.length !== 0
// ) {
// const fundamentalsWithCompleteStatus = fundamentals.map((lesson) => {
// const currentLessonId = allLessonsData.find(
// (lessonData: any) =>
// lessonData.fundamentalLessonName?.toString() === lesson.slug.toString(), // DEV_NOTE: forcing .toString() to avoid type errors
// )?.id;

// const completed =
// currentLessonId !== undefined && completedQuizzesIds.includes(currentLessonId)
// ? true
// : false; // DEV_NOTE: if the lesson is not found, it is not completed
// return { ...lesson, completed };
// });

// setFundamentals(fundamentalsWithCompleteStatus);
// } else if (
// allLessonsData !== undefined &&
// fundamentals !== undefined &&
// completedQuizzesIds.length === 0
// ) {
// const fundamentalsWithCompleteStatus = fundamentals.map((lesson) => {
// return { ...lesson, completed: false };
// });
// setFundamentals(fundamentalsWithCompleteStatus);
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [completedQuizzesIds]);

return (
<AppContext.Provider
value={{
Expand Down
2 changes: 2 additions & 0 deletions apps/academy/src/server/api/root.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { completedQuizzesRouter } from "@/server/api/routers/completedquizzes";
import { lessonsRouter } from "@/server/api/routers/lessons";
import { TracksRouter } from "@/server/api/routers/tracks";
import { userRouter } from "@/server/api/routers/user";
import { createTRPCRouter } from "@/server/api/trpc";
// export * from "database";

Expand All @@ -10,6 +11,7 @@ import { createTRPCRouter } from "@/server/api/trpc";
* All routers added in /api/routers should be manually added here.
*/
export const appRouter = createTRPCRouter({
user: userRouter,
tracks: TracksRouter,
lessons: lessonsRouter,
completedQuizzes: completedQuizzesRouter,
Expand Down
37 changes: 37 additions & 0 deletions apps/academy/src/server/api/routers/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Imports
// ========================================================

import { z } from "zod";

import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";

// Router
// ========================================================
export const userRouter = createTRPCRouter({
getUserEmail: protectedProcedure.query(async ({ ctx }) => {
const userEmail = await ctx.prisma.user.findUnique({
where: {
id: ctx.session.user.id,
},
select: {
email: true,
emailVerified: true,
},
});

return {
...userEmail,
};
}),
addEmail: protectedProcedure.input(z.string().email()).mutation(async ({ ctx, input }) => {
const emailSaved = await ctx.prisma.user.update({
where: {
id: ctx.session.user.id,
},
data: {
email: input,
},
});
return emailSaved;
}),
});
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"turbo": "1.10.11",
"typescript": "5.1.6"
},
"packageManager": "pnpm@8.6.10",
"packageManager": "pnpm@9.0.6",
"prisma": {
"schema": "packages/database/prisma/schema.prisma",
"seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} packages/database/prisma/seed.ts"
Expand Down