From 5af816821fbe11873a97441f2c84ae279d7d20e7 Mon Sep 17 00:00:00 2001
From: akintewe <85641756+akintewe@users.noreply.github.com>
Date: Wed, 25 Feb 2026 08:00:10 +0100
Subject: [PATCH 1/3] feat: implement puzzle list page with grid display and
filtering
- Create PuzzleCard component with type, difficulty badges, description, category, and time limit
- Rewrite puzzles page with responsive 1/2/3 column grid layout
- Wire FilterBar with category and difficulty filters into usePuzzles query params
- Add skeleton loading grid, error state with retry, and empty state UI
---
frontend/app/puzzles/page.tsx | 117 +++++++++++++++++++--
frontend/components/puzzles/PuzzleCard.tsx | 97 +++++++++++++++++
2 files changed, 208 insertions(+), 6 deletions(-)
create mode 100644 frontend/components/puzzles/PuzzleCard.tsx
diff --git a/frontend/app/puzzles/page.tsx b/frontend/app/puzzles/page.tsx
index 983ebb8..b76a983 100644
--- a/frontend/app/puzzles/page.tsx
+++ b/frontend/app/puzzles/page.tsx
@@ -1,9 +1,114 @@
-import React from 'react'
+'use client';
+
+import React, { useState } from 'react';
+import { usePuzzles } from '@/hooks/usePuzzles';
+import PuzzleCard from '@/components/puzzles/PuzzleCard';
+import FilterBar from '@/components/puzzles/FilterBar';
+import type { PuzzleFilters } from '@/lib/types/puzzles';
+
+const DEFAULT_FILTERS: PuzzleFilters = {
+ categoryId: '',
+ difficulty: 'ALL',
+};
+
+// Skeleton card to match PuzzleCard proportions
+const PuzzleCardSkeleton: React.FC = () => (
+
+ {/* Badge row */}
+
+ {/* Title */}
+
+ {/* Description lines */}
+
+ {/* Footer */}
+
+
+);
+
+const PuzzleListPage: React.FC = () => {
+ const [filters, setFilters] = useState(DEFAULT_FILTERS);
+
+ const queryParams = {
+ ...(filters.categoryId ? { categoryId: filters.categoryId } : {}),
+ ...(filters.difficulty !== 'ALL' ? { difficulty: filters.difficulty } : {}),
+ };
+
+ const { data: puzzles, isLoading, isError, refetch } = usePuzzles(queryParams);
-const page = () => {
return (
- page
- )
-}
+
+
+
+ {/* Page header */}
+
+
Puzzles
+
+ Challenge yourself with logic, coding, and blockchain puzzles.
+
+
+
+ {/* Filter bar */}
+
+
+
+
+ {/* Error state */}
+ {isError && (
+
+
Failed to load puzzles.
+
+
+ )}
+
+ {/* Skeleton grid while loading */}
+ {isLoading && (
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+ )}
+
+ {/* Puzzle grid */}
+ {!isLoading && !isError && (
+ <>
+ {puzzles && puzzles.length > 0 ? (
+ <>
+
+ {puzzles.length} puzzle{puzzles.length !== 1 ? 's' : ''} found
+
+
+ {puzzles.map((puzzle) => (
+
+ ))}
+
+ >
+ ) : (
+
+
🔍
+
No puzzles found
+
Try adjusting your filters.
+
+ )}
+ >
+ )}
+
+
+ );
+};
-export default page
\ No newline at end of file
+export default PuzzleListPage;
diff --git a/frontend/components/puzzles/PuzzleCard.tsx b/frontend/components/puzzles/PuzzleCard.tsx
new file mode 100644
index 0000000..cad653e
--- /dev/null
+++ b/frontend/components/puzzles/PuzzleCard.tsx
@@ -0,0 +1,97 @@
+'use client';
+
+import React from 'react';
+import { useRouter } from 'next/navigation';
+import type { Puzzle } from '@/lib/types/puzzles';
+
+interface PuzzleCardProps {
+ puzzle: Puzzle;
+}
+
+const TYPE_CONFIG = {
+ logic: {
+ label: 'Logic',
+ icon: '🧠',
+ className: 'bg-purple-500/15 text-purple-300 border-purple-500/25',
+ },
+ coding: {
+ label: 'Coding',
+ icon: '💻',
+ className: 'bg-blue-500/15 text-blue-300 border-blue-500/25',
+ },
+ blockchain: {
+ label: 'Blockchain',
+ icon: '⛓️',
+ className: 'bg-emerald-500/15 text-emerald-300 border-emerald-500/25',
+ },
+} as const;
+
+const DIFFICULTY_CONFIG = {
+ ALL: { label: 'All', className: 'bg-slate-500/15 text-slate-300 border-slate-500/25', dot: 'bg-slate-400' },
+ BEGINNER: { label: 'Beginner', className: 'bg-green-500/15 text-green-300 border-green-500/25', dot: 'bg-green-400' },
+ INTERMEDIATE: { label: 'Intermediate', className: 'bg-yellow-500/15 text-yellow-300 border-yellow-500/25', dot: 'bg-yellow-400' },
+ ADVANCED: { label: 'Advanced', className: 'bg-orange-500/15 text-orange-300 border-orange-500/25', dot: 'bg-orange-400' },
+ EXPERT: { label: 'Expert', className: 'bg-red-500/15 text-red-300 border-red-500/25', dot: 'bg-red-500' },
+} as const;
+
+const PuzzleCard: React.FC = ({ puzzle }) => {
+ const router = useRouter();
+
+ const typeConfig = TYPE_CONFIG[puzzle.type] ?? TYPE_CONFIG.logic;
+ const diffConfig = DIFFICULTY_CONFIG[puzzle.difficulty] ?? DIFFICULTY_CONFIG.BEGINNER;
+
+ return (
+
+ );
+};
+
+export default PuzzleCard;
From 833833f82eb79e53635d8c2cc8db5f3b42b2fe81 Mon Sep 17 00:00:00 2001
From: akintewe <85641756+akintewe@users.noreply.github.com>
Date: Wed, 25 Feb 2026 10:39:30 +0100
Subject: [PATCH 2/3] fix: resolve TypeScript errors in animation hooks
- Fix useReducedMotionCheck to import usePrefersReducedMotion from local hooks, not framer-motion
- Fix usePrefersReducedMotion return type by coalescing null to false so boolean is satisfied
---
frontend/hooks/useReducedMotionCheck.ts | 2 +-
frontend/lib/animations/hooks.ts | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/frontend/hooks/useReducedMotionCheck.ts b/frontend/hooks/useReducedMotionCheck.ts
index 1e49bce..95f27e3 100644
--- a/frontend/hooks/useReducedMotionCheck.ts
+++ b/frontend/hooks/useReducedMotionCheck.ts
@@ -1,4 +1,4 @@
-import { usePrefersReducedMotion } from 'framer-motion';
+import { usePrefersReducedMotion } from '@/lib/animations/hooks';
const useReducedMotionCheck = () => {
const prefersReducedMotion = usePrefersReducedMotion();
diff --git a/frontend/lib/animations/hooks.ts b/frontend/lib/animations/hooks.ts
index 4284fc7..4006f32 100644
--- a/frontend/lib/animations/hooks.ts
+++ b/frontend/lib/animations/hooks.ts
@@ -5,7 +5,7 @@ import { useReducedMotion } from 'framer-motion';
* Hook to determine if animations should be disabled based on user preferences.
*/
export const usePrefersReducedMotion = (): boolean => {
- return useReducedMotion();
+ return useReducedMotion() ?? false;
};
/**
From d030efba84a56469245db70e513e79e00483ba59 Mon Sep 17 00:00:00 2001
From: akintewe <85641756+akintewe@users.noreply.github.com>
Date: Wed, 25 Feb 2026 10:51:07 +0100
Subject: [PATCH 3/3] fix: add QueryClientProvider to root layout
React Query hooks (useQuery) require a QueryClientProvider in the tree.
Create QueryProvider wrapper and add it to the root layout so all pages
including /puzzles can use usePuzzles and useCategories without crashing
during prerender.
---
frontend/app/layout.tsx | 3 +++
frontend/providers/QueryProvider.tsx | 19 +++++++++++++++++++
2 files changed, 22 insertions(+)
create mode 100644 frontend/providers/QueryProvider.tsx
diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx
index 4edaf55..5f11f56 100644
--- a/frontend/app/layout.tsx
+++ b/frontend/app/layout.tsx
@@ -7,6 +7,7 @@ import CompletionFeatureProvider from "@/providers/CompletionFeatureProvider";
import DashboardFeatureProvider from "@/providers/DashboardFeatureProvider";
import ErrorBoundary from "@/components/error/ErrorBoundary";
import { NetworkStatusProvider } from "@/providers/NetworkStatusProvider";
+import QueryProvider from "@/providers/QueryProvider";
const geistSans = Geist({
variable: "--font-geist-sans",
@@ -31,6 +32,7 @@ export default function RootLayout({
+
@@ -40,6 +42,7 @@ export default function RootLayout({
+
diff --git a/frontend/providers/QueryProvider.tsx b/frontend/providers/QueryProvider.tsx
new file mode 100644
index 0000000..e6f7ee0
--- /dev/null
+++ b/frontend/providers/QueryProvider.tsx
@@ -0,0 +1,19 @@
+'use client';
+
+import { useState } from 'react';
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
+
+export default function QueryProvider({ children }: { children: React.ReactNode }) {
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: 60 * 1000,
+ },
+ },
+ })
+ );
+
+ return {children};
+}