From 33e74338c060b60a3b183468e7622489fdd0207d Mon Sep 17 00:00:00 2001 From: Aman Kumar Bairagi Date: Sat, 11 May 2024 15:26:17 +0530 Subject: [PATCH 1/2] feat:Added Leetcode like heatmap --- apps/web/app/profile/heatmap/page.tsx | 63 +++++++++++ apps/web/components/Calendar.tsx | 108 ++++++++++++++++++ apps/web/components/CalendarRenderer.tsx | 122 +++++++++++++++++++++ packages/ui/package.json | 3 +- packages/ui/src/profile/ProfileOptions.tsx | 7 +- 5 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 apps/web/app/profile/heatmap/page.tsx create mode 100644 apps/web/components/Calendar.tsx create mode 100644 apps/web/components/CalendarRenderer.tsx diff --git a/apps/web/app/profile/heatmap/page.tsx b/apps/web/app/profile/heatmap/page.tsx new file mode 100644 index 00000000..dd5aecbd --- /dev/null +++ b/apps/web/app/profile/heatmap/page.tsx @@ -0,0 +1,63 @@ +import React from 'react' +import { AreaChart } from "lucide-react" +import { getServerSession } from 'next-auth'; +import { authOptions } from '../../../lib/auth'; +import { redirect } from 'next/navigation'; +import db from "@repo/db/client"; +import CalendarRenderer from '../../../components/CalendarRenderer'; + +export default async function Heatmap() { + + const session = await getServerSession(authOptions); + const monthData = ['January', 'February', 'March', 'April', 'May', 'June', 'July', "August", "September", "October", "November", "December"]; + + if (!session || !session?.user) { + redirect("/"); + } + + const getAllSubmissions = async () => { + if (!session || !session.user) { + return null; + } + + const userId = session.user.id; + const submissions = await db.submission.findMany({ + where: { + userId, + }, + include: { + language: true, + }, + orderBy: { + createdAt: "desc", + }, + }); + return submissions; + }; + + const submissions = await getAllSubmissions(); + + const acceptedSubmissions = submissions?.filter((submission: any) => submission.statusId <= 3); + + return ( + <> +
+
+

+ + Heatmap +

+ + This page shows you your submissions visually in the form of heatmap + +
+
+ + + + + ) +} diff --git a/apps/web/components/Calendar.tsx b/apps/web/components/Calendar.tsx new file mode 100644 index 00000000..9c6224d4 --- /dev/null +++ b/apps/web/components/Calendar.tsx @@ -0,0 +1,108 @@ +import React, { useMemo } from 'react' +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@repo/ui/tooltip" + +interface CalendarProps { + childKey?: string; + type: string; + submissionDates: string[], + selectedMonth: number; + selectedYear: number; + isCurrentMonth: boolean; + monthData: string[], +} + +function Calendar({ childKey, type, submissionDates, isCurrentMonth, monthData, selectedMonth, selectedYear }: CalendarProps) { + + const date = new Date(); + const currMonth = selectedMonth; + const currYear = selectedYear; + const currDate = date.getDate(); + // const currDay = date.getDay(); + + let firstDayOfMonth = new Date(currYear, currMonth, 1).getDay(); + let LastDateOfMonth = new Date(currYear, currMonth + 1, 0).getDate() + let LastDateOfLastMonth = new Date(currYear, currMonth, 0).getDate() + + const getDatesOfLastMonth = (firstDayOfMonth: number, LastDateOfLastMonth: number) => { + return Array.from( + { length: firstDayOfMonth }, + (_, index) => LastDateOfLastMonth - firstDayOfMonth + index + 1 + ); + }; + + //getting the dates of current month + const getCurrMonthDates = (LastDateOfMonth: number): number[] => { + const dates: number[] = []; + for (let date = 1; date <= LastDateOfMonth; date++) { + dates.push(date); + } + return dates; + } + + // getting the dates of last month + const datesOfLastMonth = useMemo(() => { + return getDatesOfLastMonth(firstDayOfMonth, LastDateOfLastMonth) + }, [currMonth, currYear]); + + const currMonthDateArray = useMemo(() => { + return getCurrMonthDates(LastDateOfMonth); + }, [currMonth, currYear]); + + const datesNumber: number[] = submissionDates.map((item: string) => Number(item?.split("/")[0])); + const dateFrequency = useMemo(() => { + return datesNumber.reduce((acc: any, date: any) => { + acc[date] = (acc[date] || 0) + 1; + return acc; + }, {}); + }, [submissionDates]); + + const renderColorIntensity = (frequency: number) => { + if (frequency === 0) { + return 'bg-[#161b22]'; + } else if (frequency > 0 && frequency <= 5) { + return 'bg-[#006d32]'; + } else if (frequency > 5 && frequency <= 10) { + return 'bg-[#26a641]'; + } else if (frequency > 10) { + return 'bg-[#39d353]'; + } + } + return ( +
+ {/* to add gaps before the new month starts */} + {datesOfLastMonth.map((date, index) => ( +
+ ))} + + + {currMonthDateArray.map((x, dayIndex) => { + const isMatching: boolean = datesNumber.some((item: number) => Number(item) == x); + const frequency = dateFrequency[x] || 0; + return ( + + + +
+
+
+ +

{frequency} submissions on {monthData[selectedMonth]} {x} , {selectedYear}

+
+
+
+ ); + })} +
+
+ ) +} + +export default React.memo(Calendar); \ No newline at end of file diff --git a/apps/web/components/CalendarRenderer.tsx b/apps/web/components/CalendarRenderer.tsx new file mode 100644 index 00000000..f0dca06b --- /dev/null +++ b/apps/web/components/CalendarRenderer.tsx @@ -0,0 +1,122 @@ +'use client' +import React, { useState } from 'react' +import Calendar from './Calendar' +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from '@repo/ui/select'; + +interface CalendarRendererProps { + acceptedSubmissions: any; + monthData: string[]; +} + +export default function CalendarRenderer({ acceptedSubmissions, monthData }: CalendarRendererProps) { + const [selectedMonth, setSelectedMonth] = useState(String(new Date().getMonth())); + const [selectedYear, setSelectedYear] = useState(String(new Date().getFullYear())); + const selectedMonthSubmissionsDates: string[] = acceptedSubmissions?.map((x: any) => x.createdAt.toLocaleString("en-GB")).filter((date: any) => Number(date.split(',')[0].split("/")[1]) - 1 === Number(selectedMonth) && Number(date.split(',')[0].split('/')[2]) === new Date().getFullYear()); + const yearWiseSubmissionDates: string[] = acceptedSubmissions?.map((x: any) => x.createdAt.toLocaleString("en-GB")).filter((date: any) => Number(date.split(',')[0].split('/')[2]) === Number(selectedYear)); + + const generateYearsData = (currentYear: number) => { + let finalYearsArray = []; + let it = 0; + while (currentYear >= 2023) { + finalYearsArray[it] = currentYear; + currentYear--; + it++; + } + return finalYearsArray; + } + + return ( +
+
+
{selectedMonthSubmissionsDates && selectedMonthSubmissionsDates.length} Submission(s) in {monthData[Number(selectedMonth)]} {new Date().getFullYear()}
+
+ +
+
+ + {/* Rendering Single month Calendar */} +
+ +
+ +
+
+ {yearWiseSubmissionDates && yearWiseSubmissionDates.length} Submissions in {selectedYear} +
+
+ +
+
+ + {/* Rendering each month Calendar */} + +
+ { + monthData.map((item: string, index: number) => { + const eachMonthSubmissionDates = acceptedSubmissions?.map((x: any) => x.createdAt.toLocaleString("en-GB")).filter((date: any) => Number(date.split("/")[1]) - 1 === Number(index) && Number(date.split(',')[0].split('/')[2]) === Number(selectedYear)); + const isCurrentMonth = new Date().getMonth() === Number(index) && new Date().getFullYear() === Number(selectedYear); + return ( +
+ +
+ {item.slice(0, 3)} +
+
+ ); + }) + } +
+
+
+ Less + {['#161b22', '#006d32', '#26a641', '#39d353'].map((colorHash, index) => ( + + ))} + More +
+
+
+ ) +} diff --git a/packages/ui/package.json b/packages/ui/package.json index 32bcdec7..1a6d635b 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -23,7 +23,8 @@ "./Blog": "./src/Blog.tsx", "./CodeProblemRenderer": "./src/code/CodeProblemRenderer.tsx", "./MCQQuestionRenderer": "./src/MCQQuestionRenderer.tsx", - "./UserImage": "./src/UserImage.tsx" + "./UserImage": "./src/UserImage.tsx", + "./tooltip" : "./src/shad/ui/tooltip.tsx" }, "scripts": { "lint": "eslint . --max-warnings 0", diff --git a/packages/ui/src/profile/ProfileOptions.tsx b/packages/ui/src/profile/ProfileOptions.tsx index a1f2f702..d859a2cc 100644 --- a/packages/ui/src/profile/ProfileOptions.tsx +++ b/packages/ui/src/profile/ProfileOptions.tsx @@ -1,5 +1,5 @@ "use client"; -import { Braces, UserRound } from "lucide-react"; +import { AreaChart, Braces, UserRound } from "lucide-react"; import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; @@ -15,6 +15,11 @@ const ProfileOptions = () => { icon: , href: "/profile/submissions", }, + { + name: "Heatmap", + icon: , + href: "/profile/heatmap", + }, ]; const router = useRouter(); const pathname = usePathname(); From e7d03ab3e3d15c3c25ff7804f7e01cadb0e959bc Mon Sep 17 00:00:00 2001 From: Aman Kumar Bairagi Date: Thu, 13 Jun 2024 16:03:37 +0530 Subject: [PATCH 2/2] fix: Merge Conflicts resolved --- packages/ui/package.json | 29 +---------- packages/ui/src/profile/ProfileOptions.tsx | 58 ---------------------- 2 files changed, 2 insertions(+), 85 deletions(-) delete mode 100644 packages/ui/src/profile/ProfileOptions.tsx diff --git a/packages/ui/package.json b/packages/ui/package.json index 51a19741..94dfe807 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -3,33 +3,8 @@ "version": "0.0.0", "private": true, "exports": { - "./components": "./src/index.tsx", - "./button": "./src/shad/ui/button.tsx", - "./card": "./src/shad/ui/card.tsx", - "./code": "./src/code.tsx", - "./globals.css": "./app/globals.css", - "./utils": "./lib/utils.ts", - "./pages": "./src/pages/index.ts", - "./toast": "./src/shad/ui/toast.tsx", - "./toaster": "./src/shad/ui/toaster.tsx", - "./use-toast": "./src/shad/ui/use-toast.ts", - "./table": "./src/shad/ui/table.tsx", - "./badge": "./src/shad/ui/badge.tsx", - "./select": "./src/shad/ui/select.tsx", - "./input": "./src/shad/ui/input.tsx", - "./label": "./src/shad/ui/label.tsx", - "./checkbox": "./src/shad/ui/checkbox.tsx", - "./profileOptions": "./src/profile/ProfileOptions.tsx", - "./Blog": "./src/Blog.tsx", - "./CodeProblemRenderer": "./src/code/CodeProblemRenderer.tsx", - "./MCQQuestionRenderer": "./src/MCQQuestionRenderer.tsx", - - "./UserImage": "./src/UserImage.tsx", - "./tooltip" : "./src/shad/ui/tooltip.tsx" - - "./MCQRenderer" : "./src/mcq/MCQRenderer.tsx", - "./RedirectToLoginCard": "./src/RedirectToLoginCard.tsx", - "./UserImage": "./src/UserImage.tsx" + ".": "./src/index.ts", + "./utils": "./src/lib/utils.ts" }, "scripts": { "lint": "eslint . --max-warnings 0", diff --git a/packages/ui/src/profile/ProfileOptions.tsx b/packages/ui/src/profile/ProfileOptions.tsx deleted file mode 100644 index d859a2cc..00000000 --- a/packages/ui/src/profile/ProfileOptions.tsx +++ /dev/null @@ -1,58 +0,0 @@ -"use client"; -import { AreaChart, Braces, UserRound } from "lucide-react"; -import { usePathname, useRouter } from "next/navigation"; -import { useEffect, useState } from "react"; - -const ProfileOptions = () => { - const optionsData = [ - { - name: "Profile", - icon: , - href: "/profile", - }, - { - name: "Submissions", - icon: , - href: "/profile/submissions", - }, - { - name: "Heatmap", - icon: , - href: "/profile/heatmap", - }, - ]; - const router = useRouter(); - const pathname = usePathname(); - const [activeOption, setActiveOption] = useState(() => { - return pathname === "/profile" ? 0 : null; - }); - - useEffect(() => { - const currentPath = pathname; - const activeIndex = optionsData.findIndex((option) => option.href === currentPath); - setActiveOption(activeIndex); - }, [pathname, optionsData]); - - const handleOptionClick = (index: number, href: string) => { - setActiveOption(index); - router.push(href); - }; - return ( -
- {optionsData.map((x, i) => { - return ( -
handleOptionClick(i, x.href)} - className={` ${activeOption === i ? "border-r-8 border-blue-500 dark:bg-[#ffffff1d] bg-[#2020204d]" : ""} cursor-pointer hover:bg-[#ffffff1f] flex items-center gap-2 rounded-md p-2 `} - key={i} - > - {x.icon} - {x.name} -
- ); - })} -
- ); -}; - -export default ProfileOptions;