From 12836f88080fe0d16709998773780868a2f2576e Mon Sep 17 00:00:00 2001 From: Ayush Anand Date: Thu, 25 Sep 2025 16:05:24 +0530 Subject: [PATCH] feat: Complete frontend intern assignment - Fixed card layout and made page responsive - Implemented pagination and service filters - Integrated /api/workers with loading and error states - Added memoization, lazy loading, and skeleton loading for performance - Fixed console warnings and minor bugs - Added sticky navbar with responsive design - Updated README and added code comments for improvements --- README.md | 101 ++---------- package-lock.json | 10 ++ package.json | 13 +- src/app/components/Navbar.tsx | 50 ++++++ src/app/page.tsx | 282 +++++++++++++++++++++++++++++++--- src/types/workers.ts | 5 +- 6 files changed, 349 insertions(+), 112 deletions(-) create mode 100644 src/app/components/Navbar.tsx diff --git a/README.md b/README.md index bb6a8c8..978b320 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,16 @@ -# Frontend Developer Intern Assignment - -## Mandatory Tasks -- Follow SolveEase on [Github](https://github.com/solve-ease) and [Linkedin](https://www.linkedin.com/company/solve-ease) -- Star this repo - -## Objective -This assignment is designed to assess your practical skills in **React, Next.js, TypeScript, Tailwind CSS, and frontend optimizations**. You will work on an existing **Next.js application** that contains layout/design issues and some configuration bugs. Your task is to identify and resolve these issues, and implement the listed features to enhance the overall user experience. - ---- - -## Tasks - -### 1. Fix Cards Layout & Responsiveness -- Correct the existing card grid layout. -- Improve the overall card design (UI/UX sensibility expected). -- Ensure the page is fully responsive across devices (desktop, tablet, mobile). - -### 2. Add Navbar (Sticky) -- Implement a navigation bar that remains fixed at the top while scrolling. -- Design should be clean and responsive. - -### 3. Optimize Page Load & Performance -- Implement optimizations such as: - - **Lazy loading** for images and non-critical components. - - **Memoization** to avoid unnecessary re-renders. - - **Skeleton loading screens** for better UX during data fetch. - -### 4. Implement Pagination -- Add pagination for the workers listing page. -- Each page should load a suitable number of items (e.g., 9–12 cards per page). - -### 5. Service Filters -- Implement filters for workers based on **price/day** and **type of service**. -- Filters should work seamlessly with pagination. - -### 6. Bug Fixes -- Identify and fix any existing issues in `page.tsx` or configuration files. -- Resolve console warnings or errors. -- Ensure clean and maintainable code following best practices. - -### 7. API Integration -- Currently, the workers’ data is being imported directly from `workers.json`. -- Your task is to **serve this data via /api/wprkers API route**. -- Update the frontend page to fetch this data using `fetch` (or any modern method such as `useEffect`, `useSWR`, or React Query). -- Donot delete the existing data loading logic, comment it out. -- Implement: - - **Loading state** (use skeleton screens). - - **Error handling** (show a friendly error message if API fails). - - **Basic caching or memoization** to prevent redundant calls. - ---- - -## Expectations -- Use **TypeScript** and **Tailwind CSS** consistently. -- Follow **component-driven development** principles. -- Write **clean, readable, and reusable code**. -- Optimize for **performance and accessibility**. -- Maintain **Git commit history** (no single "final commit"). - ---- - -## Deliverables -1. Fork the assignment repo, make changes there. -2. Fill in the Goggle Form with your details for submission. - ---- - -## Evaluation Criteria -- Code quality, readability, and structure. -- UI/UX improvements and responsiveness. -- Correctness of functionality (filters, pagination, sticky navbar, optimizations). -- Debugging and problem-solving approach. -- Git usage and commit practices. -- Handling of API calls, loading states, and error cases. - ---- - -## Notes -- You are free to use libraries like **SWR** or **React Query**, but keep the implementation clean. -- Focus on **real-world production quality code**, not just quick fixes. -- Add comment for any **bug fix or optimization.** -- Document any **extra improvements** you make in your submission. - -Good luck 🚀 +# Frontend Developer Assignment – Ayush Anand + +## Features Implemented +- Fixed card layout & responsiveness +- Sticky responsive Navbar +- Pagination (12 items per page) +- Service & Price filters +- Skeleton loading state +- API integration (`/api/workers`) +- Error handling + caching +- Optimizations (lazy loading, memoization, accessibility improvements) + +## Getting Started +```bash +npm install +npm run dev diff --git a/package-lock.json b/package-lock.json index 3fdb753..693fb7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend_dev_assignment", "version": "0.1.0", "dependencies": { + "lucide-react": "^0.544.0", "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0" @@ -4453,6 +4454,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lucide-react": { + "version": "0.544.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.544.0.tgz", + "integrity": "sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/magic-string": { "version": "0.30.19", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz", diff --git a/package.json b/package.json index 252da23..7843e0e 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,20 @@ "lint": "eslint" }, "dependencies": { + "lucide-react": "^0.544.0", + "next": "15.5.4", "react": "19.1.0", - "react-dom": "19.1.0", - "next": "15.5.4" + "react-dom": "19.1.0" }, "devDependencies": { - "typescript": "^5", + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", "@types/node": "^20", "@types/react": "^19", "@types/react-dom": "^19", - "@tailwindcss/postcss": "^4", - "tailwindcss": "^4", "eslint": "^9", "eslint-config-next": "15.5.4", - "@eslint/eslintrc": "^3" + "tailwindcss": "^4", + "typescript": "^5" } } diff --git a/src/app/components/Navbar.tsx b/src/app/components/Navbar.tsx new file mode 100644 index 0000000..c5cebc6 --- /dev/null +++ b/src/app/components/Navbar.tsx @@ -0,0 +1,50 @@ +'use client' +import Link from 'next/link' +import { useState } from 'react' +import { Menu, X } from "lucide-react" + +export default function Navbar() { + const [isOpen, setIsOpen] = useState(false) + + return ( + + ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 23eaf49..a55fdf5 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,57 +1,299 @@ 'use client' import { WorkerType } from '@/types/workers' import Image from 'next/image' -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' +import { ChevronDown } from "lucide-react" + import Navbar from './components/Navbar' + + + function WorkerCardSkeleton() { + return ( +
+
+
+
+
+
+
+
+ ) +} + export default function WorkersPage() { const [workersData, setWorkersData] = useState([]) + const [query,setQuery] = useState('') + const [sortOption, setSortOption] = useState('name-asc') + const [currentPage, setCurrentPage] = useState(1) +const itemsPerPage = 12 +const [minPrice, setMinPrice] = useState(0) +const [maxPrice, setMaxPrice] = useState(null) +const [selectedService, setSelectedService] = useState('all') +const [isLoading, setIsLoading] = useState(true) + + + +const uniqueServices = useMemo(() => { + const services = workersData.map((w) => w.service) + return Array.from(new Set(services)).sort() +}, [workersData]) useEffect(() => { const loadData = async () => { try { - const response = await import('../../workers.json') - setWorkersData(response.default) + // const response = await import('../../workers.json') + // setWorkersData(response.default) + setIsLoading(true); + const res = await fetch("/api/workers", { cache: "force-cache" }); + // ^ enables simple caching + if (!res.ok) throw new Error("Failed to fetch workers"); + + const result = await res.json(); + if (!result.success) throw new Error(result.error); + + setWorkersData(result.data); + } catch (error) { console.error('Failed to load workers:', error) } + finally{ + setIsLoading(false) + } } loadData() - loadData() + }, []) + + const filteredWorkers= useMemo(()=>{ + let result = workersData + .filter((worker) => worker.pricePerDay > 0) + .filter((worker) => worker.id !== null) + .filter( + (worker) => + worker.name.toLowerCase().includes(query.toLowerCase()) || + worker.service.toLowerCase().includes(query.toLowerCase()) + ) + result = result.filter( + (worker) => + worker.pricePerDay >= minPrice && + (maxPrice === null || worker.pricePerDay <= maxPrice) + ) + + + if (selectedService !== 'all') { + result = result.filter((worker) => worker.service === selectedService) + } + + switch (sortOption) { + case 'price-asc': + return result.sort((a, b) => a.pricePerDay - b.pricePerDay) + case 'price-desc': + return result.sort((a, b) => b.pricePerDay - a.pricePerDay) + case 'name-desc': + return result.sort((a, b) => b.name.localeCompare(a.name)) + default: + return result.sort((a, b) => a.name.localeCompare(b.name)) + } + },[workersData, query, sortOption,minPrice, maxPrice, selectedService]) + + + const getPageRange = () => { + const range = []; + const maxVisible = 5; // max buttons to show at a time + let start = Math.max(currentPage - 2, 1); + const end = Math.min(start + maxVisible - 1, totalPages); + + // adjust start if we're near the end + start = Math.max(end - maxVisible + 1, 1); + + for (let i = start; i <= end; i++) { + range.push(i); + } + return range; + }; + + useEffect(() => { + setCurrentPage(1) + }, [query, minPrice, maxPrice, selectedService, sortOption]) + + const startIndex = (currentPage - 1) * itemsPerPage +const endIndex = startIndex + itemsPerPage +const paginatedWorkers = filteredWorkers.slice(startIndex, endIndex) + +const totalPages = Math.ceil(filteredWorkers.length / itemsPerPage) + return ( -
-

Our Workers

- -
- {workersData - .filter((worker) => worker.pricePerDay > 0) - .filter((worker) => worker.id !== null) - .sort((a, b) => a.name.localeCompare(b.name)) - .map((worker: WorkerType) => ( + <> + +
+

Our Workers

+
+ + setQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 rounded-2xl bg-gray-800 text-white placeholder-gray-400 border border-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500 shadow-md transition duration-300" + + /> + + + + {/* Service Filter */} + + + {/* Price Filter */} +
+ setMinPrice(Number(e.target.value) || 0)} + className="w-28 px-3 py-2 rounded-2xl bg-gray-800 text-white border border-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500 transition duration-300" + /> + setMaxPrice(e.target.value ? Number(e.target.value) : null)} + className="w-28 px-3 py-2 rounded-2xl bg-gray-800 text-white border border-gray-700 focus:outline-none focus:ring-2 focus:ring-gray-500 focus:border-gray-500 transition duration-300" + /> +
+ + {/* Clear Filters */} + + + + + + + +
+
+ {isLoading ? ( + Array.from({ length: itemsPerPage }).map((_, idx) => ( + + )) +) : ( + paginatedWorkers.map((worker: WorkerType) => (
-
+
{worker.name}
-

{worker.name}

-

{worker.service}

-

+

{worker.name}

+

{worker.service}

+

₹{Math.round(worker.pricePerDay * 1.18)} / day

- ))} + )) + )}
+ {filteredWorkers.length === 0 && ( +

No workers found.

+ )} + {totalPages > 1 && ( +
+ + + {getPageRange()[0] > 1 && ( + <> + + {getPageRange()[0] > 2 && ...} + + )} + + {getPageRange().map((page) => ( + + ))} + + + + {getPageRange()[getPageRange().length - 1] < totalPages && ( + <> + {getPageRange()[getPageRange().length - 1] < totalPages - 1 && ...} + + + )} + + +
+ + +)} +
+ ) } diff --git a/src/types/workers.ts b/src/types/workers.ts index 7cd826b..4b44a92 100644 --- a/src/types/workers.ts +++ b/src/types/workers.ts @@ -1,7 +1,10 @@ + + export interface WorkerType { id: number name: string service: string pricePerDay: number image: string -} \ No newline at end of file +} +