diff --git a/.gitignore b/.gitignore index 74b1c5bb..e59e5598 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ storybook-static/ **/public/sw.js.map **/public/workbox-*.js.map **/public/worker-*.js.map +**/public/robots.txt +**/public/sitemap* **/analyze/ diff --git a/.nvmrc b/.nvmrc index 25bf17fc..0a47c855 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 \ No newline at end of file +lts/iron \ No newline at end of file diff --git a/TODO.md b/TODO.md index 5e4b59c8..31bc10ca 100644 --- a/TODO.md +++ b/TODO.md @@ -1,21 +1,48 @@ -#### MVP release TODOs - -- ~~trackpage: add half-width image~~ -- ~~trackpage: we can try add/import another track~~ (@markkos89 Added NFT track) -- ~~intro-to-ethereum page: more length in the track description (to wolovim)~~ -- ~~tracks/lessons card: add the border from figma (static and :hover border)~~ -- lesson's page: fix the mdx components (/component/mdx/Components.tsx) -- remove overflow on tracksPage -- ~~remove polygon logo from partners~~ -- ~~social buttons have incorrect vertical alignment (from wolovim - github, twitter and mirror logos)~~ -- lists (bullet points) on mdx is not being rendered correctly -- headers lacks of some padding (maybe bottom padding) -- wallet integration must be checked (markkos89) - -- fix Warm-up questions sidedrawer component (styling and placement) - -#### Links that are missing: - -- ~~feedback button ~~ (Added new links @Markkos89) -- ~~newsletter button~~ (Added new links @Markkos89) -- ~~mirror.xyz button~~ (Added new links @Markkos89) +### Warning and errors on prod http://academy.developerdao.com + +March 2024 + +- image Error while trying to use the following icon from the + Manifest: http://localhost:3000/icon-192x192.png (Download error or resource isn't a valid image) + +- fix render method of `SlotClone`. on hamburger_menu svg icon code. We have to use React.forwardRef as explained here: https://www.radix-ui.com/primitives/docs/guides/composition#your-component-must-forward-ref + +- check a future error with cookies on: https://developers.google.com/privacy-sandbox/3pcd?hl=es-419#report-issues + +- vercel analytics error present in development environment. check error msg: [Vercel Web Analytics] Failed to load script from /\_vercel/insights/script.js. Be sure to enable Web Analytics for your project and deploy again. See https://vercel.com/docs/analytics/quickstart for more information. + +- Image with src "http://localhost:3000/academy_logo.svg" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. + +- Image with src "http://localhost:3000/dd_logo.svg" has either width or height modified, but not the other. If you use CSS to change the size of your image, also include the styles 'width: "auto"' or 'height: "auto"' to maintain the aspect ratio. + +- It would be good to use https://github.com/chrishoermann/zod-prisma-types to re-use the prisma types to create zod schemas to use in the react-hook-form implementation + +--- + +## Dev Notes + +#### TOPIC: Generate Zod schemas using Prisma types dynamically (for admin app forms) + +- Read: https://github.com/prisma/prisma/discussions/10928 + The following snippet is the example code from the link above about how to type relations + +```ts +const videoWithVotes = Prisma.validator()({ + include: { votes: true }, +}); +type VideosWithVotes = Prisma.VideoGetPayload; +``` + +#### npm packages: + +- https://www.npmjs.com/package/zod-prisma-types + +- https://www.npmjs.com/package/prisma-zod-generator + +--- + +track title +track description +track image +track tags +specify lessons in the track diff --git a/apps/academy/next.config.mjs b/apps/academy/next.config.mjs index cfb7c599..6785fb6f 100644 --- a/apps/academy/next.config.mjs +++ b/apps/academy/next.config.mjs @@ -20,6 +20,14 @@ const withMDX = nextMDX({ /** @type {import('next').NextConfig} */ const config = { // basePath, + images: { + remotePatterns: [ + { + protocol: "https", + hostname: "**", + }, + ], + }, reactStrictMode: true, pageExtensions: ["ts", "tsx", "js", "jsx", "md", "mdx"], reactStrictMode: true, diff --git a/apps/academy/package.json b/apps/academy/package.json index 1ad11f20..e986585c 100644 --- a/apps/academy/package.json +++ b/apps/academy/package.json @@ -20,6 +20,7 @@ "@next/font": "^14.1.0", "@rainbow-me/rainbowkit": "^1.0.10", "@rainbow-me/rainbowkit-siwe-next-auth": "^0.3.0", + "@sendgrid/mail": "^8.1.3", "@t3-oss/env-nextjs": "^0.6.1", "@tanstack/react-query": "^4.33.0", "@trpc/client": "^10.38.1", @@ -45,6 +46,7 @@ "zod": "^3.22.2" }, "devDependencies": { + "@hookform/resolvers": "^3.3.4", "@mdx-js/loader": "^3.0.0", "@mdx-js/react": "^3.0.0", "@next/bundle-analyzer": "13.4.12", @@ -59,6 +61,7 @@ "next-config": "workspace:*", "next-pwa": "^5.6.0", "playwright-config": "workspace:*", + "react-hook-form": "^7.50.0", "react-syntax-highlighter": "^15.5.0", "remark-frontmatter": "^5.0.0", "storybook-config": "workspace:*", diff --git a/apps/academy/public/android-chrome-256x256.png b/apps/academy/public/android-chrome-256x256.png new file mode 100644 index 00000000..eab1b9a1 Binary files /dev/null and b/apps/academy/public/android-chrome-256x256.png differ diff --git a/apps/academy/public/android-chrome-384x384.png b/apps/academy/public/android-chrome-384x384.png new file mode 100644 index 00000000..c7948aee Binary files /dev/null and b/apps/academy/public/android-chrome-384x384.png differ diff --git a/apps/academy/public/assets/lessons/3/2_chainlist-amoy-dark-theme.png b/apps/academy/public/assets/lessons/3/2_chainlist-amoy-dark-theme.png new file mode 100644 index 00000000..09897761 Binary files /dev/null and b/apps/academy/public/assets/lessons/3/2_chainlist-amoy-dark-theme.png differ diff --git a/apps/academy/public/assets/lessons/3/2_faucet.png b/apps/academy/public/assets/lessons/3/2_faucet.png deleted file mode 100644 index 24ab7d41..00000000 Binary files a/apps/academy/public/assets/lessons/3/2_faucet.png and /dev/null differ diff --git a/apps/academy/public/assets/lessons/3/3_faucet.png b/apps/academy/public/assets/lessons/3/3_faucet.png deleted file mode 100644 index 573aa937..00000000 Binary files a/apps/academy/public/assets/lessons/3/3_faucet.png and /dev/null differ diff --git a/apps/academy/public/assets/lessons/3/3_polygon-faucet-amoy.png b/apps/academy/public/assets/lessons/3/3_polygon-faucet-amoy.png new file mode 100644 index 00000000..0fdec1f2 Binary files /dev/null and b/apps/academy/public/assets/lessons/3/3_polygon-faucet-amoy.png differ diff --git a/apps/academy/public/assets/lessons/3/4_deploy-tierNFT.png b/apps/academy/public/assets/lessons/3/4_deploy-tierNFT.png deleted file mode 100644 index fc7b5e5a..00000000 Binary files a/apps/academy/public/assets/lessons/3/4_deploy-tierNFT.png and /dev/null differ diff --git a/apps/academy/public/assets/lessons/3/4_tierNFT-deployment-terminal-output.png b/apps/academy/public/assets/lessons/3/4_tierNFT-deployment-terminal-output.png new file mode 100644 index 00000000..abf59823 Binary files /dev/null and b/apps/academy/public/assets/lessons/3/4_tierNFT-deployment-terminal-output.png differ diff --git a/apps/academy/public/assets/lessons/3/5_mint-tierNFT.png b/apps/academy/public/assets/lessons/3/5_mint-tierNFT.png deleted file mode 100644 index 9e538037..00000000 Binary files a/apps/academy/public/assets/lessons/3/5_mint-tierNFT.png and /dev/null differ diff --git a/apps/academy/public/assets/lessons/3/5_tierNFT-mint-script-terminal-output.png b/apps/academy/public/assets/lessons/3/5_tierNFT-mint-script-terminal-output.png new file mode 100644 index 00000000..4dd03871 Binary files /dev/null and b/apps/academy/public/assets/lessons/3/5_tierNFT-mint-script-terminal-output.png differ diff --git a/apps/academy/public/assets/lessons/3/6_tierNFTs-on-OpenSea.png b/apps/academy/public/assets/lessons/3/6_tierNFTs-on-OpenSea.png new file mode 100644 index 00000000..e645c6c3 Binary files /dev/null and b/apps/academy/public/assets/lessons/3/6_tierNFTs-on-OpenSea.png differ diff --git a/apps/academy/public/assets/lessons/5/img_1.png b/apps/academy/public/assets/lessons/5/img_1.png index 82eb2950..cd190dc1 100644 Binary files a/apps/academy/public/assets/lessons/5/img_1.png and b/apps/academy/public/assets/lessons/5/img_1.png differ diff --git a/apps/academy/public/assets/lessons/5/img_2.png b/apps/academy/public/assets/lessons/5/img_2.png index cd022b8f..adacaeba 100644 Binary files a/apps/academy/public/assets/lessons/5/img_2.png and b/apps/academy/public/assets/lessons/5/img_2.png differ diff --git a/apps/academy/public/assets/lessons/5/img_3.png b/apps/academy/public/assets/lessons/5/img_3.png index 3a1955ae..bf9933bd 100644 Binary files a/apps/academy/public/assets/lessons/5/img_3.png and b/apps/academy/public/assets/lessons/5/img_3.png differ diff --git a/apps/academy/public/manifest.json b/apps/academy/public/manifest.json index 67e5190b..9ed7daa5 100644 --- a/apps/academy/public/manifest.json +++ b/apps/academy/public/manifest.json @@ -9,22 +9,22 @@ "description": "Academy is an open-source education platform created by the Developer DAO.", "icons": [ { - "src": "/icon-192x192.png", + "src": "/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "/icon-256x256.png", + "src": "/android-chrome-256x256.png", "sizes": "256x256", "type": "image/png" }, { - "src": "/icon-384x384.png", + "src": "/android-chrome-384x384.png", "sizes": "384x384", "type": "image/png" }, { - "src": "/icon-512x512.png", + "src": "/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" }, diff --git a/apps/academy/src/components/CreatedBy.tsx b/apps/academy/src/components/CreatedBy.tsx index dff1aeca..9cbc7bce 100644 --- a/apps/academy/src/components/CreatedBy.tsx +++ b/apps/academy/src/components/CreatedBy.tsx @@ -4,12 +4,14 @@ interface CreatedByProps { author: string; authorImage: string; authorTwitter: string; + label?: string; } export default function CreatedBy({ author, authorImage = "/authors/default.png", authorTwitter, + label = "Created by:", }: CreatedByProps) { // mvp: if multiple authors, split by comma and map to multiple links const handles = authorTwitter.split(", "); @@ -28,7 +30,7 @@ export default function CreatedBy({ return (
-

Created by:

+

{label}

diff --git a/apps/academy/src/components/EmailRequestDialog.tsx b/apps/academy/src/components/EmailRequestDialog.tsx new file mode 100644 index 00000000..fe5a3529 --- /dev/null +++ b/apps/academy/src/components/EmailRequestDialog.tsx @@ -0,0 +1,96 @@ +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>; + setRequestVerification: Dispatch>; +} + +export function EmailRequestDialog({ open, setIsOpen, setRequestVerification }: Props) { + const { toast } = useToast(); + const [userEmail, setUserEmail] = useState(""); + + const { mutate: saveUserEmail } = api.user.addEmail.useMutation({ + onSuccess: () => { + toast({ + title: "Amazing!", + description: "Now Check your inbox to verify your email address.", + }); + }, + onError: ({ message }) => { + toast({ + variant: "destructive", + title: "Error!", + description: `Unexpected error: ${message}`, + }); + }, + onSettled: () => { + setIsOpen(false); + setRequestVerification(true); + }, + }); + + const handleSaveBtnClick = (e: any) => { + e.preventDefault(); + if (userEmail !== "") { + saveUserEmail(userEmail); + } + }; + + return ( + + { + e.preventDefault(); + }} + onPointerDown={(e) => { + e.preventDefault(); + }} + onInteractOutside={(e) => { + e.preventDefault(); + }} + > + + Configure your email + +
+
+ + { + setUserEmail(e.target.value); + }} + /> +
+
+ + + +
+
+ ); +} diff --git a/apps/academy/src/components/EmailVerificationDialog.tsx b/apps/academy/src/components/EmailVerificationDialog.tsx new file mode 100644 index 00000000..6dbda534 --- /dev/null +++ b/apps/academy/src/components/EmailVerificationDialog.tsx @@ -0,0 +1,185 @@ +import { type Dispatch, type SetStateAction, useEffect, useState } from "react"; +import { Button, InputOTP, InputOTPGroup, InputOTPSeparator, InputOTPSlot } from "ui"; +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "ui"; +import { Label } from "ui"; +import { useToast } from "ui"; + +import { api } from "@/utils/api"; + +interface Props { + open: boolean; + setIsOpen: Dispatch>; + emailAlreadySent: boolean; + verificationCodeNumber: string; + emailAddress: string; +} + +export function EmailVerificationDialog({ + open, + setIsOpen, + emailAlreadySent, + verificationCodeNumber, + emailAddress, +}: Props) { + const { toast } = useToast(); + const [numberToVerify, setNumberToVerify] = useState(""); + const [timer, setTimer] = useState("2:00"); + const [allowNewEmail, setAllowNewEmail] = useState(false); + + const { mutate: saveEmailVerificatedSuccess } = api.user.emailVerificatedSuccess.useMutation({ + onSuccess: () => { + toast({ + title: "Email verified!", + description: "Thank you so much for verifying you email address, keep learning now. Enjoy!", + }); + setIsOpen(false); + }, + }); + + const { mutate: resendCodeVerificationEmail } = api.user.resendCodeVerificationEmail.useMutation({ + onSuccess: () => { + toast({ + title: "Email Resent!", + description: "Check your email inbox for the new email with the verificacion code.", + }); + // setIsOpen(false); + }, + }); + + useEffect(() => { + if (!emailAlreadySent) { + countdown(); + } + }, []); + + const handleRequestResendEmail = () => { + resendCodeVerificationEmail(emailAddress); + countdown(); + }; + + const handleVerifyVerificationNumber = () => { + const verificationCorrect = Number(numberToVerify) === Number(verificationCodeNumber); + + if (verificationCorrect) { + saveEmailVerificatedSuccess(); + } else { + console.log("notttt correct"); + //TODO: resend another email with another number + setAllowNewEmail(true); + } + }; + + function countdown() { + // clearInterval(interval); + const interval = setInterval(function () { + const time = timer.split(":"); + let minutes = Number(time[0]); + let seconds = Number(time[1]); + let secondsSTR = seconds.toString(); + seconds -= 1; + secondsSTR = seconds.toString(); + if (minutes < 0) return; + else if (seconds < 0 && minutes != 0) { + minutes -= 1; + seconds = 59; + secondsSTR = seconds.toString(); + } else if (seconds < 10 && seconds.toString().length != 2) { + secondsSTR = "0" + seconds.toString(); + } + + setTimer(minutes.toString() + ":" + secondsSTR); + + if (minutes == 0 && seconds == 0) { + setAllowNewEmail(true); + clearInterval(interval); + } + }, 1000); + } + + return ( + + { + e.preventDefault(); + }} + onPointerDown={(e) => { + e.preventDefault(); + }} + onInteractOutside={(e) => { + e.preventDefault(); + }} + > + + + Insert the verification code sent + + +
+
+ +
+ { + setNumberToVerify(val); + }} + > + + + + + + + + + + + + +
+
+
+ +
+
+ Haven't received the email yet? + {allowNewEmail ? ( + + {`You can request new email in a moment. `} + + ) : ( + + )} +
+
+ +
+
+
+
+
+ ); +} diff --git a/apps/academy/src/components/Layout.tsx b/apps/academy/src/components/Layout.tsx index cf0eef7d..5cc9b729 100644 --- a/apps/academy/src/components/Layout.tsx +++ b/apps/academy/src/components/Layout.tsx @@ -1,9 +1,13 @@ import localFont from "next/font/local"; import { useRouter } from "next/router"; -import type { FunctionComponent, PropsWithChildren } from "react"; +import { useSession } from "next-auth/react"; +import { type FunctionComponent, type PropsWithChildren, useEffect, useState } from "react"; import { Footer } from "ui"; +import { EmailRequestDialog } from "@/components/EmailRequestDialog"; +import { EmailVerificationDialog } from "@/components/EmailVerificationDialog"; import { Header } from "@/components/Header"; +import { api } from "@/utils/api"; const bttf = localFont({ src: "../../public/fonts/BTTF.ttf", @@ -23,8 +27,71 @@ const fontVars = `${bttf.variable} ${deathstar.variable} ${andale.variable}`; export const Layout: FunctionComponent = ({ children }) => { const router = useRouter(); const { pathname } = router; + const [requestEmail, setRequestEmail] = useState(false); + const [emailAlreadySent, setEmailAlreadySent] = useState(false); + + const [requestVerification, setRequestVerification] = useState(false); + + const { status, data: sessionData } = useSession(); + + const { data: userEmailData, refetch: refetchGetUSerEMailData } = api.user.getUserEmail.useQuery( + sessionData?.user.id!, + { + enabled: sessionData?.user.id !== null && sessionData?.user.id !== undefined, + }, + ); + + useEffect(() => { + if (status === "authenticated") { + const fetchGetUserEmailData = async () => { + await refetchGetUSerEMailData(); + }; + void fetchGetUserEmailData(); + } + }, [status]); + + useEffect(() => { + if ( + status === "authenticated" && + typeof userEmailData?.email === "string" && + (userEmailData.emailVerified === null || userEmailData.emailVerified === undefined) + ) { + setRequestVerification(true); + setEmailAlreadySent(userEmailData.emailSent === true ? true : false); + } else if ( + status === "authenticated" && + userEmailData?.email === null && + userEmailData.emailVerified === null && + userEmailData.emailSent !== undefined + ) { + setRequestEmail(true); + setEmailAlreadySent(userEmailData.emailSent || false); + } + }, [userEmailData, status]); + return ( <> + { + setRequestEmail(false); + }} + setRequestVerification={setRequestVerification} + /> + {userEmailData?.verificationNumber !== null && + userEmailData?.verificationNumber !== undefined && + userEmailData.email !== null && + userEmailData.email !== undefined ? ( + { + setRequestVerification(false); + }} + verificationCodeNumber={userEmailData.verificationNumber.toString()} + emailAlreadySent={emailAlreadySent} + emailAddress={userEmailData.email} + /> + ) : null}
{children}
{pathname !== "/tracks" && pathname !== "/fundamentals" ?