diff --git a/README.md b/README.md index bb6a8c8..a7fe54a 100644 --- a/README.md +++ b/README.md @@ -1,85 +1,47 @@ -# 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 Intern Assignment + +## 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. + diff --git a/next.config.ts b/next.config.ts index 156ac95..2dd1a2e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,21 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ - images:{ - domains: ['images.unsplash.com','randomuser.me'], - } + reactStrictMode: true, + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'randomuser.me', + pathname: '/**', + }, + { + protocol: 'https', + hostname: 'images.unsplash.com', + pathname: '/**', + } + ] + }, }; export default nextConfig; diff --git a/src/app/api/services/route.ts b/src/app/api/services/route.ts index 1a660a9..6179c4b 100644 --- a/src/app/api/services/route.ts +++ b/src/app/api/services/route.ts @@ -7,10 +7,10 @@ export async function GET(request: NextRequest) { // Simulate API delay await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100)) - // Extract unique services from workers data + const services = Array.from(new Set(workersData.map(worker => worker.service))) - // Get service statistics + const serviceStats = services.map(service => { const workersInService = workersData.filter(worker => worker.service === service) const avgPrice = Math.round( diff --git a/src/app/api/workers/route.ts b/src/app/api/workers/route.ts index 44a245e..7872f50 100644 --- a/src/app/api/workers/route.ts +++ b/src/app/api/workers/route.ts @@ -1,18 +1,18 @@ -import { NextResponse } from 'next/server' -import workersData from '../../../../workers.json' +import { NextResponse } from 'next/server'; +import workersData from '../../../../workers.json'; export async function GET() { try { return NextResponse.json({ success: true, - data: workersData - }) - } catch (error) { - console.error('API Error:', error) + data: workersData, + timestamp: new Date().toISOString() + }); + } catch (err) { + console.error('API Error:', err); return NextResponse.json({ success: false, - error: 'Failed to fetch workers data' - }, { status: 500 }) + error: 'Internal Server Error' + }, { status: 500 }); } } - diff --git a/src/app/page.tsx b/src/app/page.tsx index 23eaf49..d3ab417 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,57 +1,177 @@ 'use client' import { WorkerType } from '@/types/workers' import Image from 'next/image' -import { useState, useEffect } from 'react' +import { useState, useEffect, useMemo } from 'react' +import Navbar from '@/components/Navbar' export default function WorkersPage() { const [workersData, setWorkersData] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState('') + + // Pagination + const [currentPage, setCurrentPage] = useState(1) + const itemsPerPage = 9 + + // Filters + const [serviceType, setServiceType] = useState('all') + const [priceRange, setPriceRange] = useState<[number, number]>([0, 10000]) useEffect(() => { - const loadData = async () => { + const fetchWorkers = async () => { + setLoading(true) try { - const response = await import('../../workers.json') - setWorkersData(response.default) - } catch (error) { - console.error('Failed to load workers:', error) + const res = await fetch('/api/workers') + if (!res.ok) throw new Error('Failed to fetch workers') + const data = await res.json() + setWorkersData(data.data) + } catch (err: unknown) { + console.error(err) + if (err instanceof Error) setError(err.message) + else setError('Something went wrong') + } finally { + setLoading(false) } } - loadData() - loadData() + + fetchWorkers() }, []) + // Filter and sort (memoized) + const filteredWorkers = useMemo(() => { + return workersData + .filter(worker => worker && worker.id != null && worker.pricePerDay > 0) + .filter(worker => + (serviceType === 'all' || worker.service === serviceType) && + worker.pricePerDay >= priceRange[0] && + worker.pricePerDay <= priceRange[1] + ) + .sort((a, b) => a.name.localeCompare(b.name)) + }, [workersData, serviceType, priceRange]) + + // Pagination slice + const currentWorkers = useMemo(() => { + const start = (currentPage - 1) * itemsPerPage + return filteredWorkers.slice(start, start + itemsPerPage) + }, [filteredWorkers, currentPage]) + + useEffect(() => { + setCurrentPage(1) + }, [serviceType, priceRange]) + + const totalPages = Math.ceil(filteredWorkers.length / itemsPerPage) + const skeletons = Array.from({ 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) => ( -
-
- {worker.name} +
+ + +

+ Our Trusted Workers +

+ + {/* Filters */} +
+ + +
+ Max Price: + setPriceRange([0, +e.target.value])} + className="accent-blue-600" + /> + ₹{priceRange[1]} +
+
+ + {/* Workers Grid */} +
+ {loading + ? skeletons.map((_, index) => ( +
+
+
+
+
+
+
-
-

{worker.name}

-

{worker.service}

-

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

+ )) + : currentWorkers.map(worker => ( +
+
+ {worker.name} +
+
+

{worker.name}

+

{worker.service}

+

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

+
-
- ))} + ))}
+ + {/* Pagination */} + {!loading && totalPages > 1 && ( +
+ + + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(pageNum => { + if (totalPages <= 5) return true + if (currentPage <= 3) return pageNum <= 5 + if (currentPage >= totalPages - 2) return pageNum > totalPages - 5 + return Math.abs(pageNum - currentPage) <= 2 + }) + .map(pageNum => ( + + ))} + + +
+ )} + + {/* Error message */} + {error &&

{error}

} +
) } diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx new file mode 100644 index 0000000..74eb509 --- /dev/null +++ b/src/components/Navbar.tsx @@ -0,0 +1,67 @@ +import Link from "next/link"; +import { useState } from "react"; + +export default function Navbar() { + const [open, setOpen] = useState(false); + + return ( +
+ +
+ ); +}