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
29 changes: 28 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,31 @@ This assignment is designed to assess your practical skills in **React, Next.js,
- Add comment for any **bug fix or optimization.**
- Document any **extra improvements** you make in your submission.

Good luck 🚀
## How to run locally

```bash
npm install
npm run dev
```
Node version: 18.x+

## What I implemented

- Responsive card grid and improved card UI
- Sticky, responsive navbar
- Pagination and filters for workers
- API integration with loading/error states
- Performance optimizations (lazy loading, memoization)
- Unit/component tests

## Trade-offs / Known Issues

- [List any limitations or decisions you made]

## How to run tests

```bash
npm test
```

Good luck 🚀
26 changes: 26 additions & 0 deletions components/Filters.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// components/Filters.tsx
import React from 'react';

export default function Filters({ onTypeChange, onPriceChange }:{ onTypeChange:(v:string|null)=>void, onPriceChange:(r:[number,number]|null)=>void }) {
return (
<div className="flex gap-4 items-center">
<select onChange={e => onTypeChange(e.target.value || null)} className="border rounded p-2">
<option value="">All services</option>
<option value="photography">Photography</option>
<option value="video">Video</option>
<option value="design">Design</option>
</select>

<select onChange={e => {
const val = e.target.value;
if (val === '0-100') onPriceChange([0,100]);
else if (val === '101-300') onPriceChange([101,300]);
else onPriceChange(null);
}} className="border rounded p-2">
<option value="">Any price</option>
<option value="0-100">0 - 100 / day</option>
<option value="101-300">101 - 300 / day</option>
</select>
</div>
);
}
17 changes: 17 additions & 0 deletions components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// components/Navbar.tsx
import Link from 'next/link';

export default function Navbar() {
return (
<header className="sticky top-0 z-40 bg-white/90 backdrop-blur shadow-sm">
<div className="container mx-auto px-4 py-3 flex items-center justify-between">
<Link href="/" className="text-xl font-bold">SolveEase</Link>
<nav className="space-x-4 hidden md:flex">
<a href="#workers" className="hover:text-sky-600">Workers</a>
<a href="#pricing" className="hover:text-sky-600">Pricing</a>
<a href="#faq" className="hover:text-sky-600">FAQ</a>
</nav>
</div>
</header>
);
}
22 changes: 22 additions & 0 deletions components/Pagination.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// components/Pagination.tsx
import React from 'react';

export default function Pagination({ current, total, onChange } : { current:number, total:number, onChange:(n:number)=>void }) {
const pages = Array.from({length: total}, (_,i)=>i+1);
return (
<nav className="flex justify-center mt-6">
<ul className="flex gap-2">
{pages.map(p => (
<li key={p}>
<button
onClick={() => onChange(p)}
className={`px-3 py-1 rounded ${p===current ? 'bg-sky-600 text-white' : 'bg-gray-100'}`}
>
{p}
</button>
</li>
))}
</ul>
</nav>
);
}
17 changes: 17 additions & 0 deletions components/SkeltonGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// components/SkeletonGrid.tsx
import React from 'react';

export default function SkeletonGrid({ cols=3, rows=3 }:{cols?:number, rows?:number}) {
const items = Array.from({length: cols*rows});
return (
<div className="grid gap-6 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{items.map((_,i) => (
<div key={i} className="animate-pulse bg-white rounded-lg p-4 shadow">
<div className="bg-gray-200 h-44 w-full mb-4" />
<div className="h-4 bg-gray-200 mb-2 w-3/4" />
<div className="h-3 bg-gray-200 w-1/2" />
</div>
))}
</div>
);
}
27 changes: 27 additions & 0 deletions components/WorkerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// components/WorkerCard.tsx
import React from 'react';
import Image from 'next/image';

export default function WorkerCard({ worker }:{ worker:any }) {
return (
<article className="bg-white rounded-lg shadow-sm overflow-hidden">
<div className="relative h-44 w-full">
<Image
src={worker.image || '/placeholder.jpg'}
alt={worker.name}
fill
className="object-cover"
sizes="(max-width: 640px) 100vw, 33vw"
/>
</div>
<div className="p-4">
<h3 className="text-lg font-semibold">{worker.name}</h3>
<p className="text-sm text-gray-600 mt-1 line-clamp-2">{worker.description}</p>
<div className="mt-3 flex items-center justify-between">
<span className="text-sky-600 font-medium">₹{worker.price}/day</span>
<button className="px-3 py-1 text-sm rounded border">View</button>
</div>
</div>
</article>
);
}
7 changes: 7 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
module.exports = {
testEnvironment: 'jsdom',
transform: { '^.+\\.[tj]sx?$': 'ts-jest' },
moduleNameMapper: {
'\\.(css|scss)$': 'identity-obj-proxy',
},
};
20 changes: 20 additions & 0 deletions src/_tests_/WorkerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { render, screen } from "@testing-library/react";
import WorkerCard from "../app/page"; // adjust import if WorkerCard is in a separate file

const worker = {
id: 1,
name: "John Doe",
service: "Plumber",
pricePerDay: 500,
image: "/john.jpg",
};

describe("WorkerCard", () => {
it("renders worker details", () => {
render(<WorkerCard worker={worker} />);
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("Plumber")).toBeInTheDocument();
expect(screen.getByText("₹500/day")).toBeInTheDocument();
expect(screen.getByAltText("John Doe")).toBeInTheDocument();
});
});
16 changes: 16 additions & 0 deletions src/app/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";
import Link from "next/link";

export default function Navbar() {
return (
<header className="sticky top-0 z-50 bg-white/90 backdrop-blur shadow">
<nav className="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
<Link href="/" className="font-bold text-lg text-gray-800">SolveEase Workers</Link>
<div className="space-x-4">
<Link href="/" className="text-gray-600 hover:text-blue-600">Home</Link>
<a href="https://github.com/solve-ease/frontend_dev_assignment" target="_blank" rel="noopener noreferrer" className="text-gray-600 hover:text-blue-600">Repo</a>
</div>
</nav>
</header>
);
}
24 changes: 12 additions & 12 deletions src/app/api/workers/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { NextResponse } from 'next/server'
import workersData from '../../../../workers.json'
import { NextResponse } from 'next/server';
import fs from 'fs';
import path from 'path';

export async function GET() {
try {
return NextResponse.json({
success: true,
data: workersData
})
} catch (error) {
console.error('API Error:', error)
return NextResponse.json({
success: false,
error: 'Failed to fetch workers data'
}, { status: 500 })
// adjust path if your workers.json lives somewhere else
const filePath = path.join(process.cwd(), 'data', 'workers.json');
const raw = fs.readFileSync(filePath, 'utf8');
const data = JSON.parse(raw);
// optionally: support query params for paging/filters here
return NextResponse.json({ ok: true, data });
} catch (err) {
console.error('API /api/wprkers error:', err);
return NextResponse.json({ ok: false, error: 'Failed to load workers' }, { status: 500 });
}
}

2 changes: 2 additions & 0 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 Navbar from "./Navbar";

const geistSans = Geist({
variable: "--font-geist-sans",
Expand All @@ -27,6 +28,7 @@ export default function RootLayout({
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
<Navbar />
{children}
</body>
</html>
Expand Down
Loading