Skip to content

Feat: Problem dashboard #126

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

Merged
merged 13 commits into from
Mar 1, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/dashboard/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export default function RootLayout({
<div className="md:hidden block fixed top-1 left-1 w-full z-50">
<MobileSidear/>
</div>
<div className="w-full dark:bg-neutral-900 bg-neutral-200 md:rounded-xl rounded-sm">{children}</div>
<div className="w-full dark:bg-neutral-900 dark: text-white bg-neutral-200 md:rounded-xl rounded-sm">{children}</div>
</main>
);
}
234 changes: 234 additions & 0 deletions app/dashboard/problems/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Problem[]>([]);
const [detailedProblems, setDetailedProblems] = useState<DetailedProblem[]>([]);
const [error, setError] = useState<string | null>(null);

// Filters and Pagination State
const [difficulty, setDifficulty] = useState<string>("ALL");
const [tagSearch, setTagSearch] = useState<string>("");
const [page, setPage] = useState<number>(1);
const [limit, setLimit] = useState<number>(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 className="px-4 py-2 shadow-lg rounded-lg bg-neutral-900 border">
<div className="flex justify-stretch">
<div className="gap-2">
</div>
{/* Div icons to show the color pattern for Free & Paid problems */}
{/* <div className="font-semibold content-center text-black grid justify-center items-center gap-2 ml-4">
<div className="flex bg-cyan-500">
<p className="mr-2 ml-2">
Free Problems
</p>
</div>
<div className="flex bg-amber-500">
<p className="mr-2 ml-2">
Paid Problems
</p>
</div>
</div> */}
</div>
{loading ? (
<div className="flex mb-1 mt-1 justify-center">
<Loader color="purple" size="xl"/>
</div>
) : error ? (
<div className="h-6 w-[200px] border-20 animate-pulse bg-red-600 duration-500">
<span className="flex justify-center items-center h-full w-full">
<p className="font-semibold text-white">{error}</p>
</span>
</div>
) : (
<>
<div className="flex justify-between items-center mb-3">
<div className="flex justify-end items-center my-6 gap-4">
<button className="px-4 py-2 border border-white text-white font-semibold hover:bg-neutral-600/50 duration-200 transition-colors disabled:opacity-50"
onClick={() => setPage(1)}
disabled={page === 1}>
Home
</button>
<button
className="px-4 py-2 border border-white text-white font-semibold hover:bg-neutral-600/50 duration-200 transition-colors disabled:opacity-50"
onClick={() => setPage((prev) => Math.max(prev - 1, 1))}
disabled={page === 1}
>
Previous
</button>
<span className="text-lg">Page {page}</span>
<button
className="px-4 py-2 border border-white font-semibold hover:bg-neutral-600/50 duration-200 transition-colors text-white disabled:opacity-50 disabled:cursor-not-allowed"
onClick={() => setPage((prev) => prev + 1)}
disabled={problems.length === 0}
>
Next
</button>
</div>
<div className="my-2 justify-end items-center flex gap-2">
<select value={difficulty} onChange={(e) => setDifficulty(e.target.value)} className="mr-2 outline-none bg-black text-white border border-white px-4 py-2">
{
["ALL", "EASY", "MEDIUM", "HARD"].map((e) => <option key={e} className=" border border-white px-4 py-2" value={e}>{e}</option>)
}
</select>

<input
type="text"
placeholder="Search by tags"
value={tagSearch}
onChange={(e) => 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"
/>
</div>
<div className="my-2 flex items-center gap-2">
<label className="text-white">Problems per page:</label>
<select
value={limit}
onChange={(e) => setLimit(Number(e.target.value))}
className="outline-none bg-black text-white border border-white px-4 py-2"
>
{[10, 20, 30].map((e) => (
<option key={e} value={e}>
{e}
</option>
))}
</select>
</div>
</div>
<div className="overflow-x-auto">
<table className="min-w-full border border-white">
<thead>
<tr className="bg-gray-800 text-white">
<th className="border border-white px-4 py-2">ID</th>
<th className="border border-white px-4 py-2">Title</th>
<th className="border border-white px-4 py-2">Difficulty</th>
<th className="border border-white px-4 py-2">Accuracy</th>
<th className="border border-white px-4 py-2">Video</th>
<th className="border border-white px-4 py-2">Tags</th>
<th className="border border-white px-4 py-2"><ThumbsUp className="text-green-600"/></th>
<th className="border border-white px-4 py-2"><ThumbsDown className="text-red-600"/></th>
<th className="border border-white px-4 py-2">Hints</th>
</tr>
</thead>
<tbody className="border border-white">
{Array.isArray(problems) && problems.length > 0 ? (
problems.map((problem, index) => (
<tr key={problem.titleSlug} className="border-2 border-white hover:bg-neutral-800">
<td className="border border-white text-center border px-4 py-2">{detailedProblems[index]?.questionId}</td>
<td className={`border border-white text-sm font-bold px-4 py-2 ${detailedProblems[index]?.isPaidOnly ? 'hover:bg-amber-500' : 'hover:bg-cyan-500'} hover:text-black`}>
<a href={`https://leetcode.com/problems/${problem.titleSlug}`}
target="_blank">
{`${problem.title}`}
</a>
</td>
<td className={`border border-white text-center font-semibold px-4 py-2 ${problem.difficulty === 'Easy' ? 'text-green-600' :
problem.difficulty === 'Medium' ? 'text-yellow-600' : 'text-red-600'}`}>{problem.difficulty}</td>
<td className="border border-white text-center border px-4 py-2">{Math.round((problem.acRate))}%</td>
<td className="border border-white text-center border px-4 py-2">{problem.hasVideoSolution ? "Yes" : "No"}</td>
<td className="border border-white text-wrap text-xs px-4 py-2">{problem.topicTags.map((tag) => tag.name).join(", ")}</td>
<td className="border border-white px-4 py-2">{detailedProblems[index]?.likes > 1000 ? (`${Math.round(detailedProblems[index]?.likes / 1000)}K`
) : (detailedProblems[index]?.likes)}</td>
<td className="border border-white px-4 py-2">{detailedProblems[index]?.dislikes > 1000 ? (`${Math.round(detailedProblems[index]?.dislikes / 1000)}K`
) : (detailedProblems[index]?.dislikes)}</td>
<td className="border border-white text-center px-4 py-2">
{detailedProblems[index]?.hints?.length > 0 ? (
<HintCard hints={detailedProblems[index].hints} />
) : (
<span className="text-red-500">No hints found</span>
)}
</td>
</tr>
))) : (
<tr>
<td className="border px-4 py-2 text-center" rowSpan={10} colSpan={10}>No problems found</td>
</tr>
)}
</tbody>
</table>
</div>
</>
)}
</div>
);
}
11 changes: 11 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
40 changes: 40 additions & 0 deletions components/HintCard.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<AlertDialog>
<AlertDialogTrigger asChild className="w-36rem rounded-none">
<Button className="py-2 text-black hover:bg-purple-700 bg-white hover:text-white" variant="outline">Hints</Button>
</AlertDialogTrigger>
<AlertDialogContent className="max-w-36rem backdrop-blur bg-opacity-100 border border-gray-100 text-white">
<AlertDialogHeader>
<AlertDialogTitle className="w-16 px-2 bg-purple-500">Hints</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription className="rounded-none">
<div>
<ul className="list-outside list-disc">
{hints.slice(0, 3).map((hint, index) => (
<li key={index} className="mb-2 text-white text-wrap text-sm">{String(hint)}</li>
))}
</ul>
</div>
</AlertDialogDescription>
<AlertDialogFooter className="rounded-none">
<AlertDialogCancel className="rounded-none">Close</AlertDialogCancel>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
}
30 changes: 30 additions & 0 deletions components/ui/checkbox.tsx
Original file line number Diff line number Diff line change
@@ -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<typeof CheckboxPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
>(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center text-current")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName

export { Checkbox }
Loading