diff --git a/src/client/src/components/layout/Body.js b/src/client/src/components/layout/Body.js index acf92f0..f771550 100644 --- a/src/client/src/components/layout/Body.js +++ b/src/client/src/components/layout/Body.js @@ -15,6 +15,7 @@ const Body = ({ currentView, onViewChange, solvedProblems, + shuffleState }) => { if (loading) { return ( @@ -42,12 +43,13 @@ const Body = ({ return (
- { const [currentPage, setCurrentPage] = useState(1); const [totalPages, setTotalPages] = useState(1); const [currentView, setCurrentView] = useState('list'); - // Use the solved problems hook - const solvedProblems = useSolvedProblems(); + const [shuffle, setShuffle] = useState(-1); // -1 means shuffle is off + const [isShuffleEnabled, setIsShuffleEnabled] = useState(false); + const solvedProblems = useSolvedProblems(); const PROBLEMS_PER_PAGE = 50; - // Check if any filters are active const hasActiveFilters = useMemo(() => { return !!(filters.company || filters.timePeriod || filters.difficulty); }, [filters]); + const generateShuffleNumber = () => { + return Math.floor(Math.random() * 10000) + 1; + }; + + // Toggle shuffle on/off + const toggleShuffle = () => { + if (isShuffleEnabled) { + // Turning shuffle off + setIsShuffleEnabled(false); + setShuffle(-1); + } else { + // Turning shuffle on - generate new number if none exists + setIsShuffleEnabled(true); + if (shuffle === -1) { + setShuffle(generateShuffleNumber()); + } + } + }; + + // Regenerate shuffle number + const regenerateShuffle = () => { + const newShuffle = generateShuffleNumber(); + setShuffle(newShuffle); + setCurrentPage(1); // Reset to first page when regenerating shuffle + }; + useEffect(() => { const loadProblems = async () => { setLoading(true); try { - // ALWAYS pass filters to API - backend will ignore empty ones - const data = await fetchProblems(currentPage, PROBLEMS_PER_PAGE, filters); + const data = await fetchProblems( + currentPage, + PROBLEMS_PER_PAGE, + filters, + isShuffleEnabled ? shuffle : -1 // Only pass shuffle if enabled + ); setProblems(data); setError(null); - // Estimate total pages based on response if (data.length < PROBLEMS_PER_PAGE) { - setTotalPages(currentPage); // This is the last page + setTotalPages(currentPage); } else { - setTotalPages(currentPage + 1); // There might be more pages + setTotalPages(currentPage + 1); } } catch (err) { setError("Failed to fetch problems. Please try again later."); @@ -51,9 +80,8 @@ const Main = () => { }; loadProblems(); - }, [currentPage, filters]); + }, [currentPage, filters, shuffle, isShuffleEnabled]); - // Extract unique company names from ALL problems (initial load for dropdown) const companies = useMemo(() => { const companySet = new Set(); problems.forEach((problem) => { @@ -68,7 +96,7 @@ const Main = () => { const handleFilterChange = (newFilters) => { setFilters(newFilters); - setCurrentPage(1); // Reset to first page when filters change + setCurrentPage(1); }; const handlePageChange = (page) => { @@ -82,6 +110,12 @@ const Main = () => { filters={filters} onFilterChange={handleFilterChange} companies={companies} + shuffleState={{ + isShuffleEnabled, + shuffle, + toggleShuffle, + regenerateShuffle + }} /> { currentView={currentView} onViewChange={setCurrentView} solvedProblems={solvedProblems} + shuffleState={{ + isShuffleEnabled, + shuffle, + toggleShuffle, + regenerateShuffle + }} />
); diff --git a/src/client/src/components/layout/ShuffleToggle.js b/src/client/src/components/layout/ShuffleToggle.js new file mode 100644 index 0000000..40684cb --- /dev/null +++ b/src/client/src/components/layout/ShuffleToggle.js @@ -0,0 +1,25 @@ +import React from "react"; +import "../../styles/layout/ShuffleToggle.css"; + +const ShuffleToggle = ({ shuffleState }) => { + const { isShuffleEnabled, toggleShuffle } = shuffleState; + + return ( +
+
+ +
+
+ ); +}; + +export default ShuffleToggle; \ No newline at end of file diff --git a/src/client/src/components/layout/ViewToggle.js b/src/client/src/components/layout/ViewToggle.js index eb5faab..ab3bfa6 100644 --- a/src/client/src/components/layout/ViewToggle.js +++ b/src/client/src/components/layout/ViewToggle.js @@ -1,7 +1,13 @@ import React from "react"; +import ShuffleToggle from "./ShuffleToggle"; import "../../styles/layout/ViewToggle.css"; -const ViewToggle = ({ currentView, onViewChange, solvedProblems }) => { +const ViewToggle = ({ + currentView, + onViewChange, + solvedProblems, + shuffleState, +}) => { const handleClearAll = () => { if ( window.confirm( @@ -16,32 +22,42 @@ const ViewToggle = ({ currentView, onViewChange, solvedProblems }) => { return (
-
- - +
+
+ + +
- {hasSolvedProblems && ( - - )} +
+ {shuffleState && ( +
+ +
+ )} + + {hasSolvedProblems && ( + + )} +
); }; diff --git a/src/client/src/components/problems/ProblemGrid.js b/src/client/src/components/problems/ProblemGrid.js index 824cbd6..d8c219d 100644 --- a/src/client/src/components/problems/ProblemGrid.js +++ b/src/client/src/components/problems/ProblemGrid.js @@ -4,19 +4,22 @@ import ProblemList from "./ProblemList"; import ViewToggle from "../layout/ViewToggle"; import "../../styles/components/ProblemGrid.css"; -const ProblemGrid = ({ - problems, - hasActiveFilters, - currentView, +const ProblemGrid = ({ + problems, + hasActiveFilters, + currentView, onViewChange, - solvedProblems + solvedProblems, + shuffleState, }) => { if (problems.length === 0) { return (

No problems found matching your criteria.

{hasActiveFilters && ( -

Try adjusting your filters or clearing them to see all problems.

+

+ Try adjusting your filters or clearing them to see all problems. +

)}
); @@ -24,32 +27,30 @@ const ProblemGrid = ({ return (
- - - {currentView === 'grid' ? ( + + {currentView === "grid" ? (
{problems.map((problem) => ( - ))}
) : (
- +
)}
); }; -export default ProblemGrid; \ No newline at end of file +export default ProblemGrid; diff --git a/src/client/src/services/api.js b/src/client/src/services/api.js index 01b341b..ca72016 100644 --- a/src/client/src/services/api.js +++ b/src/client/src/services/api.js @@ -1,22 +1,32 @@ // services/api.js -const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "http://default-fallback.com/api"; +const API_BASE_URL = + process.env.REACT_APP_API_BASE_URL || "http://default-fallback.com/api"; -export const fetchProblems = async (page = 1, limit = 50, filters = {}) => { +export const fetchProblems = async ( + page = 1, + limit = 50, + filters = {}, + shuffle = -1 +) => { const skip = (page - 1) * limit; const params = new URLSearchParams({ skip: skip.toString(), - limit: limit.toString() + limit: limit.toString(), }); // Add filter parameters if they exist if (filters.company) { - params.append('company', filters.company); + params.append("company", filters.company); } if (filters.difficulty) { - params.append('difficulty', filters.difficulty); + params.append("difficulty", filters.difficulty); } if (filters.timePeriod) { - params.append('tag', filters.timePeriod); + params.append("tag", filters.timePeriod); + } + + if (shuffle !== -1) { + params.append("shuffle", shuffle.toString()); } const response = await fetch(`${API_BASE_URL}/problems?${params}`); @@ -32,4 +42,4 @@ export const getProblemById = async (id) => { throw new Error(`Failed to fetch problem #${id}`); } return await response.json(); -}; \ No newline at end of file +}; diff --git a/src/client/src/styles/components/ProblemGrid.css b/src/client/src/styles/components/ProblemGrid.css index 4680458..7eeadfb 100644 --- a/src/client/src/styles/components/ProblemGrid.css +++ b/src/client/src/styles/components/ProblemGrid.css @@ -1,28 +1,26 @@ .problem-container { - margin-bottom: 0.5rem; + margin-bottom: 1rem; padding-bottom: 0; } .problem-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: 0.75rem; - padding: 0.25rem 0; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 1rem; + padding: 0.5rem 0; } .no-problems { text-align: center; - padding: 1rem; + padding: 1.5rem; background: white; - border-radius: 6px; + border-radius: 8px; color: #666; - margin: 0.75rem 0; - font-size: 0.9rem; + margin: 1rem 0; } -/* Remove space between toggle and list */ +/* Ensure list view takes only needed space */ .problem-list-container { - margin-top: 0; /* Ensure no top margin */ margin-bottom: 0; padding-bottom: 0; } @@ -31,6 +29,6 @@ @media (max-width: 640px) { .problem-grid { grid-template-columns: 1fr; - gap: 0.5rem; + gap: 0.75rem; } } \ No newline at end of file diff --git a/src/client/src/styles/layout/Nav.css b/src/client/src/styles/layout/Nav.css index 9817eb4..26d5a20 100644 --- a/src/client/src/styles/layout/Nav.css +++ b/src/client/src/styles/layout/Nav.css @@ -1,12 +1,10 @@ -/* styles/layout/Nav.css */ .nav { background: linear-gradient(135deg, #ffffff 0%, #f8fafc 100%); border-bottom: 1px solid #e2e8f0; padding: 0; - box-shadow: 0 1px 6px rgba(0, 0, 0, 0.04); + box-shadow: 0 1px 8px rgba(0, 0, 0, 0.04); position: relative; z-index: 100; - min-height: auto; } .nav-content { @@ -15,9 +13,31 @@ padding: 0 1.5rem; position: relative; z-index: 101; + display: flex; + flex-direction: column; + gap: 1rem; + padding-top: 1rem; + padding-bottom: 1rem; } -/* Remove any extra padding that might be adding space */ -.nav * { - box-sizing: border-box; +/* Ensure filter options and shuffle toggle are properly aligned */ +.filter-options { + display: flex; + gap: 2rem; + align-items: flex-end; + flex-wrap: wrap; + padding: 0; /* Remove padding since nav-content handles it */ +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .nav-content { + padding: 0 1rem; + gap: 0.75rem; + } + + .filter-options { + flex-direction: column; + gap: 1rem; + } } \ No newline at end of file diff --git a/src/client/src/styles/layout/ShuffleToggle.css b/src/client/src/styles/layout/ShuffleToggle.css new file mode 100644 index 0000000..df82379 --- /dev/null +++ b/src/client/src/styles/layout/ShuffleToggle.css @@ -0,0 +1,80 @@ +.shuffle-toggle-container { + display: flex; + align-items: center; +} + +.shuffle-toggle { + display: flex; + align-items: center; + padding: 0.5rem; + background: white; + border-radius: 6px; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); + border: 1px solid #e2e8f0; +} + +.shuffle-label { + display: flex; + align-items: center; + gap: 0.5rem; + cursor: pointer; + font-weight: 500; + color: #4a5568; + margin: 0; +} + +.shuffle-checkbox { + display: none; +} + +.shuffle-slider { + width: 40px; + height: 20px; + background: #cbd5e0; + border-radius: 20px; + position: relative; + transition: all 0.3s ease; + flex-shrink: 0; +} + +.shuffle-slider::before { + content: ''; + position: absolute; + width: 16px; + height: 16px; + border-radius: 50%; + background: white; + top: 2px; + left: 2px; + transition: all 0.3s ease; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.shuffle-checkbox:checked + .shuffle-slider { + background: #0066cc; +} + +.shuffle-checkbox:checked + .shuffle-slider::before { + transform: translateX(20px); +} + +.shuffle-text { + font-size: 0.8rem; + font-weight: 600; + white-space: nowrap; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .shuffle-toggle { + padding: 0.4rem; + } + + .shuffle-label { + gap: 0.4rem; + } + + .shuffle-text { + font-size: 0.75rem; + } +} \ No newline at end of file diff --git a/src/client/src/styles/layout/ViewToggle.css b/src/client/src/styles/layout/ViewToggle.css index 2fb0006..475d59c 100644 --- a/src/client/src/styles/layout/ViewToggle.css +++ b/src/client/src/styles/layout/ViewToggle.css @@ -6,11 +6,28 @@ gap: 1rem; } +.left-controls { + display: flex; + align-items: center; + gap: 1rem; +} + +.right-controls { + display: flex; + align-items: center; + gap: 1rem; +} + .view-toggle { display: flex; gap: 0.5rem; } +.shuffle-control { + display: flex; + align-items: center; +} + .toggle-btn { padding: 0.4rem 0.8rem; border: 1px solid #e2e8f0; @@ -63,12 +80,19 @@ gap: 0.75rem; } - .view-toggle { + .left-controls { justify-content: center; + width: 100%; } - .clear-all-btn { - order: -1; /* Move clear button to top on mobile */ + .right-controls { + justify-content: space-between; + width: 100%; + order: -1; + } + + .view-toggle { + justify-content: center; } .toggle-btn {