Skip to content
Open
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
200 changes: 116 additions & 84 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,119 @@
# 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").
# SolveEase Frontend Assignment – Workers Directory

Enterprise-quality Next.js app that lists workers with responsive cards, sticky navbar, API-driven data, pagination, filters, and performance/a11y optimizations.

## Stack
- Next.js 15 (App Router)
- React 19
- TypeScript
- Tailwind CSS v4
- ESLint 9

## Getting started
1. Install dependencies:
```bash
npm install
```
2. Run the app:
```bash
npm run dev
```
3. Open `http://localhost:3000`

## Features
- Sticky, responsive navbar and clean layout shell
- Workers listing with responsive grid (1/2/3/4 columns)
- API integration with loading skeletons and error handling
- Client-side pagination (12 items/page)
- Filters: by service and by max price/day (integrated with pagination)
- Performance improvements (memoization, lazy images, correct `sizes`, preconnect hints)
- Accessibility improvements (focus-visible styles, aria-live status, semantic controls)

## Project structure (high level)
- `package.json`
- `tsconfig.json`
- `next.config.ts`
- `postcss.config.mjs`
- `workers.json` – mock data source
- `public/` – static assets
- `src/`
- `types/`
- `workers.ts` – `WorkerType`
- `app/` – Next.js App Router
- `globals.css` – Tailwind v4 import
- `layout.tsx` – layout shell, navbar, footer
- `page.tsx` – workers listing UI
- `api/`
- `workers/route.ts` – GET `/api/workers`
- `services/route.ts` – GET `/api/services` (unique services + stats)

## API
### GET `/api/workers`
Returns `{ success: boolean, data: WorkerType[] }`.

### GET `/api/services?stats=true|false`
- `?stats=true` returns service list with count/price stats
- `?stats=false` (default) returns unique service names

## How it works
- The UI fetches from `/api/workers` on mount with an abort-safe `fetch`.
- While loading, skeleton cards are shown; on error, a friendly message is displayed.
- Users can filter by service or max price/day; results integrate with pagination.
- Cards are responsive and performance friendly (lazy images, proper `sizes`).

## Changelog (what changed, why, impact)

### src/app/page.tsx
- Switched from dynamic `import('../../workers.json')` to `fetch('/api/workers')` with AbortController.
- Why: Align with assignment; proper data flow and UX.
- Impact: Real API integration, skeletons for loading, clear error state, no duplicate requests.

- Removed duplicate data loading and guarded with `hasFetchedRef`.
- Why: Bug fix; previous code called loader twice.
- Impact: Fewer renders, no redundant network calls.

- Added filters (service dropdown and max price input) integrated with pagination.
- Why: Assignment requirement; better data exploration.
- Impact: Users refine results without losing pagination state.

- Implemented pagination (12 per page) with accessible controls.
- Why: Assignment requirement; improve UX and perceived performance.
- Impact: Faster initial render, simple navigation between pages.

- Extracted `WorkerCard` component and memoized with `React.memo`.
- Why: Cleaner structure; reduce unnecessary re-renders.
- Impact: More maintainable and performant list rendering.

- Redesigned grid and cards; fixed `md:grid-cols-1` issue.
- Why: The grid collapsed at medium breakpoints; enterprise look & responsiveness.
- Impact: Consistent 1/2/3/4 column layout; polished UI.

- Performance and a11y
- Added skeleton loaders, `useMemo` for transforms, lazy images with `sizes`.
- Added aria-live for result counts; added focus-visible rings; linked pagination buttons via `aria-controls`.
- Impact: Better LCP/CLS, keyboard accessibility, and assistive tech support.

### src/app/layout.tsx
- Added sticky navbar and footer with a clean shell.
- Why: Enterprise-style frame and quick navigation; assignment requirement.
- Impact: Consistent navigation experience across the app.

- Added preconnect hints to image domains.
- Why: Reduce latency for third-party images.
- Impact: Faster image loads; improved performance.

## Development scripts
- `npm run dev` – Start dev server (Turbopack)
- `npm run build` – Production build
- `npm run start` – Start production server
- `npm run lint` – Lint codebase

## Future enhancements
- Unit tests for filtering/pagination logic
- SWR/React Query for caching and revalidation
- Server-side pagination for very large datasets
- Lighthouse/perf budget and accessibility audits

---

## Deliverables
1. Fork the repo and work from a branch named: assignment/<your-full-name> (for example: assignment/adarsh-maurya).
2. Implement improvements and features that demonstrate your mastery of the job requirements (UI polish, responsiveness, Tailwind usage, tests, accessibility, performance).
3. Push your branch to GitHub, add a clear README, and (strongly recommended) deploy the app (Vercel/Netlify/GH Pages)
3. Fill in the Google 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, optimisations).
- 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 🚀
© SolveEase – Built with Next.js, React, and Tailwind CSS.
11 changes: 9 additions & 2 deletions next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,15 @@ import type { NextConfig } from "next";

const nextConfig: NextConfig = {
/* config options here */
images:{
domains: ['images.unsplash.com','randomuser.me'],
images: {
domains: ['images.unsplash.com', 'randomuser.me'],
remotePatterns: [
{ protocol: 'https', hostname: 'images.unsplash.com' },
{ protocol: 'https', hostname: 'randomuser.me' },
],
// Some remote hosts intermittently block the Image Optimizer on Vercel.
// Disable optimization to avoid 502 from /_next/image for those hosts.
unoptimized: true,
}
};

Expand Down
61 changes: 35 additions & 26 deletions src/app/api/services/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import { NextRequest, NextResponse } from 'next/server'
import workersData from '../../../../workers.json'

export const runtime = 'nodejs'

// GET /api/services
export async function GET(request: NextRequest) {
try {
const mod = await import('../../../../workers.json')
const workersData = mod.default
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 50 + Math.random() * 100))

Expand Down Expand Up @@ -37,36 +40,42 @@ export async function GET(request: NextRequest) {
const includeStats = searchParams.get('stats') === 'true'

if (includeStats) {
return NextResponse.json({
success: true,
data: serviceStats,
metadata: {
totalServices: services.length,
totalWorkers: workersData.length
return NextResponse.json(
{
success: true,
data: serviceStats,
metadata: {
totalServices: services.length,
totalWorkers: workersData.length
},
timestamp: new Date().toISOString()
},
timestamp: new Date().toISOString()
}, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300',
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300'
}
}
})
)
} else {
return NextResponse.json({
success: true,
data: services,
metadata: {
count: services.length
return NextResponse.json(
{
success: true,
data: services,
metadata: {
count: services.length
},
timestamp: new Date().toISOString()
},
timestamp: new Date().toISOString()
}, {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300',
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300'
}
}
})
)
}

} catch (error) {
Expand Down
22 changes: 17 additions & 5 deletions src/app/api/workers/route.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,24 @@
import { NextResponse } from 'next/server'
import workersData from '../../../../workers.json'

export const runtime = 'nodejs'

export async function GET() {
try {
return NextResponse.json({
success: true,
data: workersData
})
const mod = await import('../../../../workers.json')
const workersData = mod.default
return NextResponse.json(
{
success: true,
data: workersData
},
{
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300'
}
}
)
} catch (error) {
console.error('API Error:', error)
return NextResponse.json({
Expand Down
27 changes: 25 additions & 2 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import "./globals.css";
import Link from "next/link";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -24,10 +25,32 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<head>
<link rel="preconnect" href="https://images.unsplash.com" />
<link rel="preconnect" href="https://randomuser.me" />
</head>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} antialiased bg-gray-50 text-gray-900`}
>
{children}
<header className="sticky top-0 z-50 bg-white/80 backdrop-blur border-b">
<nav className="container mx-auto px-4 h-14 flex items-center justify-between">
<Link href="/" className="font-semibold">SolveEase</Link>
<div className="flex items-center gap-6 text-sm">
<Link href="/" className="hover:text-gray-700">Workers</Link>
<Link href="/api/workers" className="hover:text-gray-700">API</Link>
<a href="https://github.com/solve-ease" target="_blank" rel="noreferrer" className="hover:text-gray-700">GitHub</a>
</div>
</nav>
</header>
<div className="min-h-[calc(100dvh-3.5rem)]">
{children}
</div>
<footer className="border-t bg-white">
<div className="container mx-auto px-4 py-6 text-sm text-gray-500 flex items-center justify-between">
<span>© {new Date().getFullYear()} SolveEase</span>
<span>Next.js • React • Tailwind CSS</span>
</div>
</footer>
</body>
</html>
);
Expand Down
Loading