Skip to content
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
74 changes: 74 additions & 0 deletions src/client/src/components/hooks/useSolvedProblems.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { useState, useEffect } from 'react';

const useSolvedProblems = () => {
// Initialize state directly from localStorage using lazy initial state
const [solvedProblems, setSolvedProblems] = useState(() => {
try {
const stored = localStorage.getItem('leetcode-solved-problems');
if (stored) {
const parsed = JSON.parse(stored);
return new Set(parsed);
}
} catch (error) {
console.error('Error loading solved problems from localStorage:', error);
}
return new Set();
});

// Single useEffect for saving to localStorage
useEffect(() => {
try {
const array = Array.from(solvedProblems);
localStorage.setItem('leetcode-solved-problems', JSON.stringify(array));
} catch (error) {
console.error('Error saving solved problems to localStorage:', error);
}
}, [solvedProblems]);

const toggleSolved = (problemId) => {
setSolvedProblems(prev => {
const newSet = new Set(prev);
if (newSet.has(problemId)) {
newSet.delete(problemId);
} else {
newSet.add(problemId);
}
return newSet;
});
};

const isSolved = (problemId) => {
return solvedProblems.has(problemId);
};

const markSolved = (problemId) => {
setSolvedProblems(prev => {
const newSet = new Set(prev);
newSet.add(problemId);
return newSet;
});
};

const markUnsolved = (problemId) => {
setSolvedProblems(prev => {
const newSet = new Set(prev);
newSet.delete(problemId);
return newSet;
});
};

const clearAllSolved = () => {
setSolvedProblems(new Set());
};

return {
solvedProblems,
toggleSolved,
isSolved,
markSolved,
markUnsolved,
clearAllSolved
};
};

export default useSolvedProblems;
10 changes: 6 additions & 4 deletions src/client/src/components/layout/Body.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ const Body = ({
onPageChange,
hasActiveFilters,
currentView,
onViewChange
onViewChange,
solvedProblems,
}) => {
if (loading) {
return (
Expand Down Expand Up @@ -41,11 +42,12 @@ const Body = ({
return (
<main className="body">
<div className="body-content">
<ProblemGrid
problems={problems}
<ProblemGrid
problems={problems}
hasActiveFilters={hasActiveFilters}
currentView={currentView}
onViewChange={onViewChange}
solvedProblems={solvedProblems}
/>
<Pagination
currentPage={currentPage}
Expand All @@ -57,4 +59,4 @@ const Body = ({
);
};

export default Body;
export default Body;
8 changes: 6 additions & 2 deletions src/client/src/components/layout/Main.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from "react";
import { fetchProblems } from "../../services/api";
import Nav from "./Nav";
import Body from "./Body";
import useSolvedProblems from "../hooks/useSolvedProblems";
import "../../styles/layout/Main.css";

const Main = () => {
Expand All @@ -15,7 +16,9 @@ const Main = () => {
});
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [currentView, setCurrentView] = useState('list'); // s states 'list' or 'grid'
const [currentView, setCurrentView] = useState('list');
// Use the solved problems hook
const solvedProblems = useSolvedProblems();

const PROBLEMS_PER_PAGE = 50;

Expand Down Expand Up @@ -48,7 +51,7 @@ const Main = () => {
};

loadProblems();
}, [currentPage, filters]); // Re-fetch when page OR filters change
}, [currentPage, filters]);

// Extract unique company names from ALL problems (initial load for dropdown)
const companies = useMemo(() => {
Expand Down Expand Up @@ -91,6 +94,7 @@ const Main = () => {
hasActiveFilters={hasActiveFilters}
currentView={currentView}
onViewChange={setCurrentView}
solvedProblems={solvedProblems}
/>
</div>
);
Expand Down
58 changes: 41 additions & 17 deletions src/client/src/components/layout/ViewToggle.js
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
import React from "react";
import "../../styles/layout/ViewToggle.css";

const ViewToggle = ({ currentView, onViewChange }) => {
const ViewToggle = ({ currentView, onViewChange, solvedProblems }) => {
const handleClearAll = () => {
if (
window.confirm(
"Are you sure you want to clear all solved problems? This action cannot be undone."
)
) {
solvedProblems.clearAllSolved();
}
};

const hasSolvedProblems = solvedProblems.solvedProblems.size > 0;

return (
<div className="view-toggle">
<button
className={`toggle-btn ${currentView === 'grid' ? 'active' : ''}`}
onClick={() => onViewChange('grid')}
title="Grid View"
>
⏹️ Grid
</button>
<button
className={`toggle-btn ${currentView === 'list' ? 'active' : ''}`}
onClick={() => onViewChange('list')}
title="List View"
>
📋 List
</button>
<div className="view-toggle-container">
<div className="view-toggle">
<button
className={`toggle-btn ${currentView === "grid" ? "active" : ""}`}
onClick={() => onViewChange("grid")}
title="Grid View"
>
⏹️ Grid
</button>
<button
className={`toggle-btn ${currentView === "list" ? "active" : ""}`}
onClick={() => onViewChange("list")}
title="List View"
>
📋 List
</button>
</div>

{hasSolvedProblems && (
<button
className="clear-all-btn"
onClick={handleClearAll}
title="Clear all solved problems"
>
🗑️ Clear Solved
</button>
)}
</div>
);
};

export default ViewToggle;
export default ViewToggle;
21 changes: 18 additions & 3 deletions src/client/src/components/problems/ProblemCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,14 @@ const TIME_PERIOD_LABELS = {
all: "All Time",
};

const ProblemCard = ({ problem }) => {
const ProblemCard = ({ problem, solvedProblems }) => {
const difficultyText = getDifficultyText(problem.difficulty);
const difficultyClass = getDifficultyClass(problem.difficulty);
const isSolved = solvedProblems.isSolved(problem.id);

const handleCheckboxChange = () => {
solvedProblems.toggleSolved(problem.id);
};

const getTopCompanies = (companies) => {
if (!companies) return [];
Expand All @@ -41,7 +46,17 @@ const ProblemCard = ({ problem }) => {
const topCompanies = getTopCompanies(problem.companies);

return (
<div className="problem-card">
<div className={`problem-card ${isSolved ? 'solved' : ''}`}>
{/* Checkbox in top-right corner */}
<div className="problem-card-checkbox">
<input
type="checkbox"
checked={isSolved}
onChange={handleCheckboxChange}
className="solved-checkbox"
/>
</div>

<div className="problem-header">
<h3>
<Link to={`/problems/${problem.id}`}>{problem.title}</Link>
Expand Down Expand Up @@ -91,4 +106,4 @@ const ProblemCard = ({ problem }) => {
);
};

export default ProblemCard;
export default ProblemCard;
25 changes: 21 additions & 4 deletions src/client/src/components/problems/ProblemGrid.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import ProblemList from "./ProblemList";
import ViewToggle from "../layout/ViewToggle";
import "../../styles/components/ProblemGrid.css";

const ProblemGrid = ({ problems, hasActiveFilters, currentView, onViewChange }) => {
const ProblemGrid = ({
problems,
hasActiveFilters,
currentView,
onViewChange,
solvedProblems
}) => {
if (problems.length === 0) {
return (
<div className="no-problems">
Expand All @@ -18,17 +24,28 @@ const ProblemGrid = ({ problems, hasActiveFilters, currentView, onViewChange })

return (
<div className="problem-container">
<ViewToggle currentView={currentView} onViewChange={onViewChange} />
<ViewToggle
currentView={currentView}
onViewChange={onViewChange}
solvedProblems={solvedProblems}
/>

{currentView === 'grid' ? (
<div className="problem-grid">
{problems.map((problem) => (
<ProblemCard key={problem.id} problem={problem} />
<ProblemCard
key={problem.id}
problem={problem}
solvedProblems={solvedProblems}
/>
))}
</div>
) : (
<div className="problem-list-container">
<ProblemList problems={problems} />
<ProblemList
problems={problems}
solvedProblems={solvedProblems}
/>
</div>
)}
</div>
Expand Down
27 changes: 23 additions & 4 deletions src/client/src/components/problems/ProblemList.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
} from "../../constants/difficulty";
import "../../styles/components/ProblemList.css";

const ProblemList = ({ problems }) => {
const ProblemList = ({ problems, solvedProblems }) => {
if (problems.length === 0) {
return (
<div className="no-problems">
Expand All @@ -18,26 +18,45 @@ const ProblemList = ({ problems }) => {
return (
<div className="problem-list">
<div className="list-header">
<span className="header-solved">Solved</span>
<span className="header-id">ID</span>
<span className="header-title">Problem Name</span>
<span className="header-difficulty">Difficulty</span>
<span className="header-acceptance">Acceptance</span>
</div>
<div className="list-content">
{problems.map((problem, index) => (
<ProblemListItem key={problem.id} problem={problem} index={index} />
<ProblemListItem
key={problem.id}
problem={problem}
index={index}
solvedProblems={solvedProblems}
/>
))}
</div>
</div>
);
};

const ProblemListItem = ({ problem, index }) => {
const ProblemListItem = ({ problem, index, solvedProblems }) => {
const difficultyText = getDifficultyText(problem.difficulty);
const difficultyClass = getDifficultyClass(problem.difficulty);
const isSolved = solvedProblems.isSolved(problem.id);

const handleCheckboxChange = () => {
solvedProblems.toggleSolved(problem.id);
};

return (
<div className="problem-list-item" data-index={index}>
<div className={`problem-list-item ${isSolved ? 'solved' : ''}`} data-index={index}>
<div className="problem-solved">
<input
type="checkbox"
checked={isSolved}
onChange={handleCheckboxChange}
className="solved-checkbox"
/>
</div>
<div className="problem-id">{problem.id}</div>
<div className="problem-title">
<Link to={`/problems/${problem.id}`}>{problem.title}</Link>
Expand Down
Loading