diff --git a/app/dashboard/layout.tsx b/app/dashboard/layout.tsx index c5aba37..adb8895 100644 --- a/app/dashboard/layout.tsx +++ b/app/dashboard/layout.tsx @@ -17,7 +17,7 @@ export default function RootLayout({
-
{children}
+
{children}
); } diff --git a/app/dashboard/problems/page.tsx b/app/dashboard/problems/page.tsx new file mode 100644 index 0000000..9bd8671 --- /dev/null +++ b/app/dashboard/problems/page.tsx @@ -0,0 +1,234 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { Problem } from "@/utils/problem"; +import { HintCard } from "@/components/HintCard"; +import { DetailedProblem } from "@/utils/detailedProblem"; // Import the new type +import Link from "next/link"; +import { ThumbsUp, ThumbsDown } from 'lucide-react'; +import { Loader } from "@/components/ui/percept-ui/loader"; +import { getTagSlug } from "@/lib/tags"; + +export default function Home() { + const [loading, setLoading] = useState(true); + const [problems, setProblems] = useState([]); + const [detailedProblems, setDetailedProblems] = useState([]); + const [error, setError] = useState(null); + + // Filters and Pagination State + const [difficulty, setDifficulty] = useState("ALL"); + const [tagSearch, setTagSearch] = useState(""); + const [page, setPage] = useState(1); + const [limit, setLimit] = useState(10); + + function parseTags(tagSearch: string): string { + if (!tagSearch) return ''; + return tagSearch + .split(',') + .map(tag => getTagSlug(tag.trim())) + .filter(Boolean) + .join(','); + } + + async function fetchProblems() { + try { + const apiUrl = process.env.NEXT_PUBLIC_LEETCODE_API_URL; + if (!apiUrl) { + setError("API URL is not defined in environment variables") + throw new Error("API URL is not defined in environment variables"); + } + + const difficultyParam = difficulty !== "ALL" ? `difficulty=${difficulty}` : ""; + const parsedTags = parseTags(tagSearch); // Call parseTags function + const tagParam = parsedTags ? `tags=${parsedTags}` : ""; + const skip = (page - 1) * limit; + const queryParams = [difficultyParam, tagParam, `limit=${limit}`, `skip=${skip}`] + .filter(Boolean) + .join("&"); + + // console.log(queryParams); // Debug statement:- Log the query params to the console + const res = await fetch(`${apiUrl}/problems?${queryParams}`); + const data = await res.json(); + if (data && Array.isArray(data.problemsetQuestionList)) { + setProblems(data.problemsetQuestionList); + fetchProblemsData(data.problemsetQuestionList); + } else { + setError("Failed to fetch problems"); + setLoading(false); + } + } catch (err) { + console.error(err); + setError("Error fetching problems"); + setLoading(false); + } + } + + async function fetchProblemsData(problems: Problem[]) { + try { + const apiUrl = process.env.NEXT_PUBLIC_LEETCODE_API_URL; + if (!apiUrl) { + throw new Error("API URL is not defined in environment variables"); + } + + const detailedProblemsData = await Promise.all( + problems.map(async (problem) => { + const res = await fetch(`${apiUrl}/select?titleSlug=${problem.titleSlug}`); + return res.json(); + }) + ); + + setDetailedProblems(detailedProblemsData); + setLoading(false); + } catch (err) { + console.error(err); + setError("Error fetching detailed problems"); + setLoading(false); + } + } + + useEffect(() => { + if (typeof window !== "undefined") { + fetchProblems(); + } + }, [difficulty, tagSearch, page, limit]); + + return ( +
+
+
+
+ {/* Div icons to show the color pattern for Free & Paid problems */} + {/*
+
+

+ Free Problems +

+
+
+

+ Paid Problems +

+
+
*/} +
+ {loading ? ( +
+ +
+ ) : error ? ( +
+ +

{error}

+
+
+ ) : ( + <> +
+
+ + + Page {page} + +
+
+ + + setTagSearch(e.target.value)} + className="mr-2 outline-none min-w-96 h-9 bg-black text-white border border-white px-4 py-2" + /> +
+
+ + +
+
+
+ + + + + + + + + + + + + + + + {Array.isArray(problems) && problems.length > 0 ? ( + problems.map((problem, index) => ( + + + + + + + + + + + + ))) : ( + + + + )} + +
IDTitleDifficultyAccuracyVideoTagsHints
{detailedProblems[index]?.questionId} + + {`${problem.title}`} + + {problem.difficulty}{Math.round((problem.acRate))}%{problem.hasVideoSolution ? "Yes" : "No"}{problem.topicTags.map((tag) => tag.name).join(", ")}{detailedProblems[index]?.likes > 1000 ? (`${Math.round(detailedProblems[index]?.likes / 1000)}K` + ) : (detailedProblems[index]?.likes)}{detailedProblems[index]?.dislikes > 1000 ? (`${Math.round(detailedProblems[index]?.dislikes / 1000)}K` + ) : (detailedProblems[index]?.dislikes)} + {detailedProblems[index]?.hints?.length > 0 ? ( + + ) : ( + No hints found + )} +
No problems found
+
+ + )} +
+ ); +} diff --git a/app/globals.css b/app/globals.css index 55f9d8e..9025aad 100644 --- a/app/globals.css +++ b/app/globals.css @@ -154,4 +154,15 @@ html { body { @apply bg-background text-foreground; } +} + + + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } } \ No newline at end of file diff --git a/components/HintCard.tsx b/components/HintCard.tsx new file mode 100644 index 0000000..f73acc8 --- /dev/null +++ b/components/HintCard.tsx @@ -0,0 +1,40 @@ +"use client"; + +import { + AlertDialog, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, + } from "@/components/ui/alert-dialog"; +import { Button } from "@/components/ui/button"; + +export function HintCard({ hints }: { hints: string[] }) { + return ( + + + + + + + Hints + + +
+
    + {hints.slice(0, 3).map((hint, index) => ( +
  • {String(hint)}
  • + ))} +
+
+
+ + Close + +
+
+ ); +} \ No newline at end of file diff --git a/components/ui/checkbox.tsx b/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/components/ui/percept-ui/loader.tsx b/components/ui/percept-ui/loader.tsx new file mode 100644 index 0000000..b43d769 --- /dev/null +++ b/components/ui/percept-ui/loader.tsx @@ -0,0 +1,101 @@ +"use client"; +import { cn } from "@/lib/utils"; +import { forwardRef } from "react"; + +import { cva, VariantProps } from "class-variance-authority"; +import { LoaderIcon } from "lucide-react"; + +const loaderColors = { + black: "black", + dark: "slate", + light: "white", + blue: "blue", + red: "red", + green: "green", + yellow: "yellow", + cyan: "cyan", + gray: "gray", + emerald: "emerald", + rose: "rose", + amber: "amber", + orange: "orange", + pink: "pink", + purple: "purple", + indigo: "indigo", + teal: "teal", + lime: "lime", + sky: "sky", +}; + +type LoaderColors = keyof typeof loaderColors; + +const colorClasses = { + blue: " text-blue-600", + red: " text-red-600", + green: " text-green-600", + yellow: " text-yellow-600", + cyan: " text-cyan-600", + gray: " text-gray-600", + emerald: " text-emerald-600", + rose: " text-rose-600", + amber: " text-amber-600", + orange: " text-orange-600", + pink: " text-pink-600", + purple: " text-purple-600", + indigo: " text-indigo-600", + teal: " text-teal-600", + lime: " text-lime-600", + sky: " text-sky-600", + black: " text-black dark:text-slate-400", + dark: " dark:text-slate-300 text-slate-700", + light: " text-slate-900 dark:text-white", +}; + +const loaderVariants = cva( + ["inline-block", "rounded-[100%]", "animate-spin"], + { + variants: { + size: { + xs: "h-3 w-3", + sm: "h-5 w-5", + md: "h-7 w-7", + lg: "h-9 w-9", + xl: "h-11 w-11", + }, + color: Object.keys(loaderColors).reduce( + (acc, key) => ({ + ...acc, + [key]: colorClasses[key as LoaderColors], + }), + {} as Record + ), + }, + compoundVariants: (Object.keys(loaderColors) as LoaderColors[]).flatMap( + (scheme) => [ + { + color: scheme, + }, + ] + ), + defaultVariants: { + size: "xl", + color: "blue", + }, + } +); + +type LoaderProps = VariantProps & { + color?: LoaderColors; + size?: string; + className?: string; +}; + +export const Loader = forwardRef( + ({ className, color, size }) => ( + + ) +); + +Loader.displayName = "Loader"; diff --git a/components/ui/table.tsx b/components/ui/table.tsx new file mode 100644 index 0000000..7f3502f --- /dev/null +++ b/components/ui/table.tsx @@ -0,0 +1,117 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Table = React.forwardRef< + HTMLTableElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+ + +)) +Table.displayName = "Table" + +const TableHeader = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableHeader.displayName = "TableHeader" + +const TableBody = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableBody.displayName = "TableBody" + +const TableFooter = React.forwardRef< + HTMLTableSectionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + tr]:last:border-b-0", + className + )} + {...props} + /> +)) +TableFooter.displayName = "TableFooter" + +const TableRow = React.forwardRef< + HTMLTableRowElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableRow.displayName = "TableRow" + +const TableHead = React.forwardRef< + HTMLTableCellElement, + React.ThHTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableHead.displayName = "TableHead" + +const TableCell = React.forwardRef< + HTMLTableCellElement, + React.TdHTMLAttributes +>(({ className, ...props }, ref) => ( + +)) +TableCell.displayName = "TableCell" + +const TableCaption = React.forwardRef< + HTMLTableCaptionElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +TableCaption.displayName = "TableCaption" + +export { + Table, + TableHeader, + TableBody, + TableFooter, + TableHead, + TableRow, + TableCell, + TableCaption, +} diff --git a/data/SidebarData.ts b/data/SidebarData.ts index 1e3ee4d..a172ad1 100644 --- a/data/SidebarData.ts +++ b/data/SidebarData.ts @@ -1,6 +1,11 @@ import { BookA, FileQuestion, Settings } from "lucide-react"; export const SidebarData = [ + { + title: "Dashboard", + icon: Settings, + href: "/dashboard", + }, { title: "Problems", icon: FileQuestion, diff --git a/lib/getBlogs.ts b/lib/getBlogs.ts deleted file mode 100644 index 74b540a..0000000 --- a/lib/getBlogs.ts +++ /dev/null @@ -1,32 +0,0 @@ -import fs from 'fs'; -import path from 'path'; -import matter from 'gray-matter'; - -const BLOG_DIR = path.join(process.cwd(), 'content/blog'); - -export function getBlogsByYear() { - const years = fs.readdirSync(BLOG_DIR); - const blogs: Record = {}; - - years.forEach((year) => { - const yearDir = path.join(BLOG_DIR, year); - - if (fs.statSync(yearDir).isDirectory()) { - const files = fs.readdirSync(yearDir); - - blogs[year] = files.map((file) => { - const filePath = path.join(yearDir, file); - const fileContent = fs.readFileSync(filePath, 'utf8'); - const { data, content } = matter(fileContent); // Parse front matter and content - const slug = `${year}/${file.replace(/\.md$/, '')}`; - return { - title: data.title || 'Untitled', - slug, - content, - }; - }); - } - }); - - return blogs; -} diff --git a/lib/tags.ts b/lib/tags.ts new file mode 100644 index 0000000..4341f94 --- /dev/null +++ b/lib/tags.ts @@ -0,0 +1,77 @@ +export const tags: Record = { + "Array" : "array", + "String" : "string", + "Hash Table" : "hash-table", + "Dynamic Programming" : "dynamic-programming", + "Math" : "math", + "Sorting" : "sorting", + "Greedy" : "greedy", + "Depth First Search" : "depth-first-search", + "Binary Search" : "binary-search", + "Database" : "database", + "Matrix" : "matrix", + "Tree" : "tree", + "Breadth First Search" : "breadth-first-search", + "Bit Manipulation" : "bit-manipulation", + "Two Pointers" : "two-pointers", + "Prefix Sum" : "prefix-sum", + "Heap (Priority Queue)" : "heap-priority-queue", + "Binary Tree" : "binary-tree", + "Simulation" : "simulation", + "Stack" : "stack", + "Graph" : "graph", + "Counting" : "counting", + "Sliding Window" : "sliding-window", + "Design" : "design", + "Enumeration" : "enumeration", + "Backtracking" : "backtracking", + "Union Find" : "union-find", + "Linked List" : "linked-list", + "Number Theory" : "number-theory", + "Ordered Set" : "ordered-set", + "Monotonic Stack" : "monotonic-stack", + "Segment Tree" : "segment-tree", + "Trie" : "trie", + "Combinatorics" : "combinatorics", + "Bitmask" : "bitmask", + "Queue" : "queue", + "Divide and Conquer" : "divide-and-conquer", + "Recursion" : "recursion", + "Memoization" : "memoization", + "Binary Indexed Tree" : "binary-indexed-tree", + "Geometry" : "geometry", + "Binary Search Tree" : "binary-search-tree", + "Hash Function" : "hash-function", + "String Matching" : "string-matching", + "Topological Sort" : "topological-sort", + "Shortest Path" : "shortest-path", + "Rolling Hash" : "rolling-hash", + "Game Theory" : "game-theory", + "Interactive" : "interactive", + "Data Stream" : "data-stream", + "Monotonic Queue" : "monotonic-queue", + "Brainteaser" : "brainteaser", + "Randomized" : "randomized", + "Merge Sort" : "merge-sort", + "Doubly-Linked List" : "doubly-linked-list", + "Counting Sort" : "counting-sort", + "Iterator" : "iterator", + "Concurrency" : "concurrency", + "Probability and Statistics" : "probability-and-statistics", + "Quickselect" : "quickselect", + "Suffix Array" : "suffix-array", + "Bucket Sort" : "bucket-sort", + "Line Sweep" : "line-sweep", + "Minimum Spanning Tree" : "minimum-spanning-tree", + "Shell" : "shell", + "Reservoir Sampling" : "reservoir-sampling", + "Strongly Connected Component" : "strongly-connected-component", + "Eulerian Circuit" : "eulerian-circuit", + "Radix Sort" : "radix-sort", + "Rejection Sampling" : "rejection-sampling", + "Biconnected Component" : "biconnected-component", +}; + +export function getTagSlug(tag: string): string { + return tags[tag] || tag; +} diff --git a/package-lock.json b/package-lock.json index 385368d..d3f08f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", @@ -35,6 +36,7 @@ "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "csv-parse": "^5.6.0", "embla-carousel-react": "^8.5.1", "express": "^4.21.2", "framer-motion": "^11.18.2", @@ -1222,6 +1224,77 @@ } } }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "license": "MIT", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox/node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "license": "MIT", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collapsible": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.2.tgz", @@ -3634,6 +3707,12 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/csv-parse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==", + "license": "MIT" + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", diff --git a/package.json b/package.json index 0c26e44..0859fe9 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-avatar": "^1.1.3", + "@radix-ui/react-checkbox": "^1.1.4", "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-dropdown-menu": "^2.1.4", "@radix-ui/react-label": "^2.1.1", @@ -36,6 +37,7 @@ "chroma-js": "^3.1.2", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "csv-parse": "^5.6.0", "embla-carousel-react": "^8.5.1", "express": "^4.21.2", "framer-motion": "^11.18.2", diff --git a/utils/detailedProblem.ts b/utils/detailedProblem.ts new file mode 100644 index 0000000..14d1266 --- /dev/null +++ b/utils/detailedProblem.ts @@ -0,0 +1,19 @@ +type Difficulty = 'All' | 'Easy' | 'Medium' | 'Hard'; + +export interface DetailedProblem { + content: string; + companyTagStats: string[]; + difficulty: Difficulty; + dislikes: number; + exampleTestcases: object[]; + hints: []; + isPaidOnly: boolean; + likes: number; + questionId: number; + questionFrontendId: number; + solution: string; + similarQuestions: object[]; + title: string; + titleSlug: string; + topicTags: string[]; +} diff --git a/utils/problem.ts b/utils/problem.ts new file mode 100644 index 0000000..8d05dab --- /dev/null +++ b/utils/problem.ts @@ -0,0 +1,20 @@ +export type TopicTag = { + name: string; + id: string; + slug: string; +}; + +export type Problem = { + acRate: number; + difficulty: 'Easy' | 'Medium' | 'Hard'; + freqBar: number | null; + questionFrontendId: string; + isFavor: boolean; + isPaidOnly: boolean; + status: string | null; + title: string; + titleSlug: string; + topicTags: TopicTag[]; + hasSolution: boolean; + hasVideoSolution: boolean; +}; \ No newline at end of file