diff --git a/client/package.json b/client/package.json index fbc2d72..0bca9cf 100644 --- a/client/package.json +++ b/client/package.json @@ -9,14 +9,18 @@ "lint": "next lint" }, "dependencies": { + "@types/react-calendar-heatmap": "^1.6.7", "@vercel/analytics": "^1.3.1", "@vercel/speed-insights": "^1.0.12", "axios": "^1.7.3", + "html2canvas": "^1.4.1", "mongoose": "^8.5.2", "next": "14.2.5", "react": "^18", + "react-calendar-heatmap": "^1.9.0", "react-dom": "^18", - "react-toastify": "^10.0.5" + "react-toastify": "^10.0.5", + "react-tooltip": "^5.28.0" }, "devDependencies": { "@types/node": "^20", diff --git a/client/public/assets/icons/loading.svg b/client/public/assets/icons/loading.svg new file mode 100644 index 0000000..02e78c4 --- /dev/null +++ b/client/public/assets/icons/loading.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/client/public/assets/icons/money.svg b/client/public/assets/icons/money.svg new file mode 100644 index 0000000..1046586 --- /dev/null +++ b/client/public/assets/icons/money.svg @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/client/public/assets/icons/money2.svg b/client/public/assets/icons/money2.svg new file mode 100644 index 0000000..8d454b3 --- /dev/null +++ b/client/public/assets/icons/money2.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/client/src/app/components/Card.tsx b/client/src/app/components/Card.tsx index 335ca72..073112c 100644 --- a/client/src/app/components/Card.tsx +++ b/client/src/app/components/Card.tsx @@ -36,6 +36,13 @@ export const data = { easySolved: 175, mediumSolved: 110, hardSolved: 21, + activeYears: [2021, 2024], + calendarData: { + userCalendar: { + streak: 39, + totalActiveDays: 89, + }, + }, }; export default function Card({ userData = data, index }: any) { diff --git a/client/src/app/components/Circle.tsx b/client/src/app/components/Circle.tsx index edc8eda..0700c54 100644 --- a/client/src/app/components/Circle.tsx +++ b/client/src/app/components/Circle.tsx @@ -29,7 +29,7 @@ const Circle = ({ total }: any) => { strokeWidth="5" strokeLinecap="round" stroke="#FFC11F" - className="cursor-pointer " + className="" strokeDasharray={`${dashLength} ${circumference}`} strokeDashoffset="0" data-difficulty="ALL" diff --git a/client/src/app/components/Heatmap.tsx b/client/src/app/components/Heatmap.tsx new file mode 100644 index 0000000..dddc084 --- /dev/null +++ b/client/src/app/components/Heatmap.tsx @@ -0,0 +1,480 @@ +"use client"; + +import React, { useState, useEffect, useRef } from "react"; +import dynamic from "next/dynamic"; +import Image from "next/image"; +import { toast, ToastContainer } from "react-toastify"; +import "react-toastify/dist/ReactToastify.css"; +import { data as TempData } from "./Card"; +import Link from "next/link"; +import Circle from "./Circle"; +import Questions from "./Questions"; +import html2canvas from "html2canvas"; + +const CalendarHeatmap = dynamic(() => import("react-calendar-heatmap"), { + ssr: false, +}); + +function convertSubmissionCalendar(submissionCalendar: any) { + // Parse the JSON string to convert it into a JavaScript object + const submissionCalendarData = JSON.parse(submissionCalendar); + + return submissionCalendarData; +} + +const submissionCalendarData = { + "1704067200": 2, + "1704153600": 4, + "1704240000": 3, + "1704326400": 1, + "1704412800": 2, + "1704499200": 1, + "1704585600": 3, + "1704672000": 1, + "1704758400": 2, + "1704844800": 1, + "1704931200": 1, + "1705017600": 1, + "1705104000": 1, + "1705190400": 1, + "1705276800": 1, + "1705363200": 1, + "1705449600": 1, + "1705520000": 1, + "1705536000": 1, + "1705622400": 1, + "1705708800": 1, + "1705795200": 1, + "1705881600": 1, + "1705968000": 1, + "1706054400": 1, + "1706140800": 1, + "1706227200": 1, + + "1702080000": 8, + "1702166400": 4, + "1702252800": 2, + "1702339200": 1, + "1702425600": 1, + "1702512000": 1, + "1702598400": 1, + "1702684800": 1, + "1702771200": 1, + "1702857600": 1, + "1702944000": 1, + "1703030400": 1, + "1703116800": 1, + "1703203200": 1, + "1703289600": 1, + "1703376000": 1, + "1703402400": 1, + + // Random data for each month of 2024 + "1703443200": 3, + "1703529600": 5, + "1703616000": 2, + "1703702400": 4, + "1703788800": 1, + "1703875200": 2, + "1703961600": 6, + "1704048000": 3, + "1704134400": 5, + "1704220800": 2, + "1704307200": 4, + "1704393600": 1, + "1704480000": 3, + "1704566400": 5, + "1704652800": 2, + "1704739200": 4, + + "1704825600": 6, + "1704912000": 1, + "1704998400": 2, + "1705084800": 3, + "1705171200": 5, + "1705257600": 4, + "1705344000": 1, + "1705430400": 3, + "1705516800": 5, + "1705603200": 2, + "1705689600": 4, + "1705776000": 6, + "1705862400": 1, + "1705948800": 2, + "1706035200": 3, + + // More random values for the remaining months + "1706121600": 1, + "1706208000": 4, + "1706294400": 2, + "1706380800": 5, + "1706467200": 1, + "1706553600": 3, + "1706640000": 6, + "1706726400": 2, + "1706812800": 4, + "1706899200": 1, + "1706985600": 5, + "1707072000": 3, + "1707158400": 6, + "1707244800": 2, + "1707331200": 4, + "1707417600": 1, +}; + +function worthCalculator( + streak: number, + easySolved: number, + mediumSolved: number, + hardSolved: number, + activeYears: number, + totalActiveDays: number +) { + const easyPoints = 1; + const mediumPoints = 2; + const hardPoints = 5; + const streakPoints = streak >= 30 ? 10 : 5; + const activeYearPoints = 2; + const totalActiveDaysPoints = 10; + + return ( + easySolved * easyPoints + + mediumSolved * mediumPoints + + hardSolved * hardPoints + + streak * streakPoints + + activeYears * activeYearPoints + + totalActiveDays * totalActiveDaysPoints + ); +} + +export default function Heatmap() { + const [isMounted, setIsMounted] = useState(false); + const [username, setUsername] = useState(""); + const [submissionCalendar, setSubmissionCalendar] = useState( + submissionCalendarData + ); + const [loading, setLoading] = useState(false); + const [data, setData] = useState(TempData); + const [tooltip, setTooltip] = useState({ + show: false, + content: "", + x: 0, + y: 0, + }); + + useEffect(() => { + setIsMounted(true); + }, []); + + const generateStats = async (e: React.FormEvent) => { + setLoading(true); + e.preventDefault(); + + if (!username) { + return; + } + + try { + const res = await fetch("/api/" + username); + if (!res.ok) { + toast("👻 User not found"); + setLoading(false); + return; + } + + const data = await res.json(); + setData(data); + setSubmissionCalendar(convertSubmissionCalendar(data.submissionCalendar)); + toast("🫡 Stats generated successfully"); + } catch (error) { + console.error("An error occurred. Please try again later"); + toast("😞 An error occurred. Please try again later"); + } finally { + setLoading(false); + } + }; + + const today = new Date(); + const endDate = today; + const startDate = new Date( + today.getFullYear() - 1, + today.getMonth(), + today.getDate() + 1 + ); + + const values = Object.entries(submissionCalendar) + .map(([timestamp, count]) => ({ + date: new Date(parseInt(timestamp) * 1000), + count: count, + })) + .filter((value) => value.date >= startDate && value.date <= endDate); + + const formatDate = (date: Date) => { + const months = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + return `${ + months[date.getMonth()] + } ${date.getDate()}, ${date.getFullYear()}`; + }; + + const contentRef = useRef(null); + + const downloadAsImage = async () => { + if (contentRef.current) { + try { + // Apply a background color to the element before capturing + contentRef.current.style.backgroundColor = "#1c1c1c"; + contentRef.current.style.padding = "20px"; + contentRef.current.style.borderRadius = "10px"; + + const canvas = await html2canvas(contentRef.current, { + scale: 2, // Increase scale for better quality + useCORS: true, // This can help with loading cross-origin images + logging: true, // This can help debug issues + backgroundColor: "#1c1c1c", // Set the background color + }); + + // Reset the inline styles after capturing + contentRef.current.style.backgroundColor = ""; + contentRef.current.style.padding = ""; + contentRef.current.style.borderRadius = ""; + + const image = canvas.toDataURL("image/png"); + const link = document.createElement("a"); + link.href = image; + link.download = "leetcode-stats.png"; + link.click(); + toast("🖼️ Image downloaded successfully"); + } catch (error) { + console.error("Error generating image:", error); + toast("😞 Error generating image. Please try again."); + } + } + }; + + const handleMouseOver = (event: any, value: any) => { + if (value && value.date) { + const rect = event.target.getBoundingClientRect(); + setTooltip({ + show: true, + content: `${value.count} submission${ + value.count !== 1 ? "s" : "" + } on ${formatDate(value.date)}`, + x: rect.left + window.scrollX, + y: rect.top + window.scrollY - 40, + }); + } + }; + + const handleMouseLeave = () => { + setTooltip({ ...tooltip, show: false }); + }; + + if (!isMounted) return null; + + return ( + <> +
+
+

+ Estimate LeetCode Worth Generator +

+ +
+
+ setUsername(e.target.value)} + type="text" + placeholder="Enter the leetcode username" + className="bg-[#1f1f1f] pl-10 max-w-[600px] w-[300px] md:w-[450px] lg:w-[500px] h-[40px] text-white rounded px-4 py-6 md:text-lg text-base focus:outline-none" + /> + MOney Icon +
+ +
+ + +
+
+
+ +
+
+
+ {data.profileData.fullName} +
+

+ {data.profileData.fullName} +

+

@{data.profileData.username}

+
+
+
+
+ {/* Circle */} + + + +
+ {/* Questions */} + + + + + + +
+
+
+ +
+ { + if (!value || value.count === 0) { + return "color-empty"; + } + return `color-github-${Math.min(value.count, 4)}`; + }} + onMouseOver={handleMouseOver} + onMouseLeave={handleMouseLeave} + gutterSize={2} + horizontal={true} + /> + +

+ + {worthCalculator( + data.easySolved, + data.mediumSolved, + data.hardSolved, + data?.activeYears.length, + data?.calendarData.userCalendar.streak, + data?.calendarData.userCalendar.totalActiveDays + )} + $ + +
+ Estimated Worth +

+ +

+ Get Yours at:  + + leetcode-profiles-delta.vercel.app + +

+
+ {tooltip.show && ( +
+ {tooltip.content} +
+ )} +
+
+ + + ); +} diff --git a/client/src/app/components/Navbar.tsx b/client/src/app/components/Navbar.tsx index 672831f..17c0eef 100644 --- a/client/src/app/components/Navbar.tsx +++ b/client/src/app/components/Navbar.tsx @@ -4,16 +4,25 @@ import Image from "next/image"; import Link from "next/link"; import React, { useState } from "react"; -type NavbarProps = { - search: string; - setSearch: React.Dispatch>; -}; +interface NavbarProps { + search?: string; + setSearch?: (search: string) => void; + searchBarPresent?: boolean; +} -const Navbar: React.FC = ({ search, setSearch }) => { +const Navbar: React.FC = ({ + search, + setSearch, + searchBarPresent, +}) => { const [hoveredItem, setHoveredItem] = useState(null); return ( -