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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,12 @@ This assignment is designed to assess your practical skills in **React, Next.js,
- Document any **extra improvements** you make in your submission.

Good luck 🚀





During the assignment, I worked on each task by thoroughly testing the implemented features after completion. For each task, I committed the changes to separate branches and pushed them to the remote repository to maintain an independent snapshot of the code at every stage. This organized version control approach ensured both the quality of the project and the traceability of changes throughout development.

## Branches
- assignment/krishna-sharma: Complete assignment with all implemented features.
1 change: 1 addition & 0 deletions frontend_dev_assignment
Submodule frontend_dev_assignment added at e8d2c0
203 changes: 157 additions & 46 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,168 @@
'use client'
import { WorkerType } from '@/types/workers'
import Image from 'next/image'
import { useState, useEffect } from 'react'
'use client';
import Navbar from '../components/Navbar';
import WorkerCard from '../components/WorkerCard';
import SkeletonCard from '../components/SkeletonCard';
import { WorkerType } from '@/types/workers';
import { useState, useEffect } from 'react';

export default function WorkersPage() {
const [workersData, setWorkersData] = useState<WorkerType[]>([])
const [workersData, setWorkersData] = useState<WorkerType[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState('');
const [currentPage, setCurrentPage] = useState(1);
const [minPrice, setMinPrice] = useState<number | ''>('');
const [maxPrice, setMaxPrice] = useState<number | ''>('');
const [selectedService, setSelectedService] = useState<string>('');
const itemsPerPage = 9;

useEffect(() => {
const loadData = async () => {
const fetchWorkers = async () => {
setLoading(true);
setError('');
try {
const response = await import('../../workers.json')
setWorkersData(response.default)
} catch (error) {
console.error('Failed to load workers:', error)
const response = await fetch('/api/workers');
if (!response.ok) throw new Error('Failed to fetch workers');
const json = await response.json();
setWorkersData(json.data || []); // Use the 'data' property from API response
} catch (err: any) {
setError(err.message || 'Unknown error');
}
}
loadData()
loadData()
}, [])
setLoading(false);
};
fetchWorkers();
}, []);

useEffect(() => {
setCurrentPage(1);
}, [minPrice, maxPrice, selectedService]);

const filteredWorkers = Array.isArray(workersData)
? workersData
.filter((worker) => worker.pricePerDay > 0)
.filter((worker) => worker.id !== null)
.filter((worker) => (minPrice === '' ? true : worker.pricePerDay >= minPrice))
.filter((worker) => (maxPrice === '' ? true : worker.pricePerDay <= maxPrice))
.filter((worker) => (selectedService === '' ? true : worker.service === selectedService))
.sort((a, b) => a.name.localeCompare(b.name))
: [];

const totalPages = Math.ceil(filteredWorkers.length / itemsPerPage);
const indexOfLastItem = currentPage * itemsPerPage;
const indexOfFirstItem = indexOfLastItem - itemsPerPage;
const currentWorkers = filteredWorkers.slice(indexOfFirstItem, indexOfLastItem);

const goToPage = (pageNumber: number) => {
if (pageNumber < 1 || pageNumber > totalPages) return;
setCurrentPage(pageNumber);
};

const goNext = () => {
if (currentPage < totalPages) setCurrentPage(currentPage + 1);
};

const goPrev = () => {
if (currentPage > 1) setCurrentPage(currentPage - 1);
};

if (loading) {
return (
<>
<Navbar />
<main className="container mx-auto px-4 py-8 bg-[#000000]">
<h1 className="text-3xl font-bold mb-8 text-center text-white">Loading workers...</h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{Array.from({ length: itemsPerPage }, (_, idx) => (
<SkeletonCard key={idx} />
))}
</div>
</main>
</>
);
}

if (error) {
return (
<>
<Navbar />
<main className="container mx-auto px-4 py-8 bg-[#000000]">
<h1 className="text-3xl font-bold mb-8 text-center text-red-600">Error: {error}</h1>
</main>
</>
);
}

return (
<main className='container mx-auto px-4 py-8 bg-[#000000]'>
<h1 className='text-3xl font-bold mb-8 text-center'>Our Workers</h1>

<div className='grid grid-cols-1 sm:grid-cols-2 md:grid-cols-1 gap-6'>
{workersData
.filter((worker) => worker.pricePerDay > 0)
.filter((worker) => worker.id !== null)
.sort((a, b) => a.name.localeCompare(b.name))
.map((worker: WorkerType) => (
<div
key={worker.id}
className='border rounded-lg overflow-hidden shadow hover:shadow-lg transition-shadow duration-300'
<>
<Navbar />
<main className="container mx-auto px-4 py-8 bg-[#000000]">
<h1 className="text-3xl font-bold mb-8 text-center text-white">Our Workers</h1>

{/* Filters Section */}
<div className="mb-6 flex flex-wrap justify-center space-x-4 space-y-2">
<input
type="number"
placeholder="Min Price"
value={minPrice}
onChange={(e) => setMinPrice(e.target.value === '' ? '' : Number(e.target.value))}
className="border p-2 rounded w-40"
/>
<input
type="number"
placeholder="Max Price"
value={maxPrice}
onChange={(e) => setMaxPrice(e.target.value === '' ? '' : Number(e.target.value))}
className="border p-2 rounded w-40"
/>
<select
value={selectedService}
onChange={(e) => setSelectedService(e.target.value)}
className="border p-2 rounded w-40"
>
<option value="">All Services</option>
{Array.from(new Set(workersData.map((w) => w.service))).map((service) => (
<option key={service} value={service}>
{service}
</option>
))}
</select>
</div>

{/* Workers Grid */}
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{currentWorkers.map((worker: WorkerType) => (
<WorkerCard key={worker.id} worker={worker} />
))}
</div>

{/* Pagination Controls */}
<div className="flex justify-center space-x-2 mt-6 overflow-auto">
<button
onClick={goPrev}
disabled={currentPage === 1}
className="px-3 py-1 border rounded disabled:opacity-50 text-white bg-gray-700"
>
Prev
</button>
{Array.from({ length: totalPages }, (_, i) => (
<button
key={i}
onClick={() => goToPage(i + 1)}
className={`px-3 py-1 border rounded ${
currentPage === i + 1 ? 'bg-blue-600 text-white' : 'text-white bg-gray-700'
}`}
>
<div className='w-full h-48 relative'>
<Image
src={worker.image}
alt={worker.name}
fill
className='object-cover'
priority={worker.id <= 10}
/>
</div>
<div className='p-4'>
<h2 className='text-xl font-semibold'>{worker.name}</h2>
<p className='text-gray-600'>{worker.service}</p>
<p className='mt-2 font-medium'>
₹{Math.round(worker.pricePerDay * 1.18)} / day
</p>
</div>
</div>
{i + 1}
</button>
))}
</div>
</main>
)
<button
onClick={goNext}
disabled={currentPage === totalPages}
className="px-3 py-1 border rounded disabled:opacity-50 text-white bg-gray-700"
>
Next
</button>
</div>
</main>
</>
);
}
44 changes: 44 additions & 0 deletions src/components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { useState } from 'react';

const Navbar = () => {
const [isOpen, setIsOpen] = useState(false);

return (
<nav className="sticky top-0 bg-white shadow-md p-4 z-50">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div className="font-bold text-xl">MyBrand</div>

{/* Desktop links */}
<div className="space-x-4 hidden md:flex">
<a href="#home" className="hover:text-blue-600">Home</a>
<a href="#services" className="hover:text-blue-600">Services</a>
<a href="#about" className="hover:text-blue-600">About</a>
</div>

{/* Mobile hamburger button */}
<div className="md:hidden">
<button
onClick={() => setIsOpen(!isOpen)}
aria-label="Toggle menu"
type="button"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" strokeWidth="2" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d={isOpen ? "M6 18L18 6M6 6l12 12" : "M4 6h16M4 12h16M4 18h16"} />
</svg>
</button>
</div>
</div>

{/* Mobile menu items */}
{isOpen && (
<div className="md:hidden px-4 py-2 space-y-2">
<a href="#home" className="block py-1 hover:text-blue-600">Home</a>
<a href="#services" className="block py-1 hover:text-blue-600">Services</a>
<a href="#about" className="block py-1 hover:text-blue-600">About</a>
</div>
)}
</nav>
);
};

export default Navbar;
14 changes: 14 additions & 0 deletions src/components/SkeletonCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const SkeletonCard = () => {
return (
<div className="border rounded-lg shadow animate-pulse bg-gray-300 flex flex-col h-72">
<div className="w-full h-48 bg-gray-400 rounded-t-lg"></div>
<div className="p-4 flex flex-col flex-grow space-y-2">
<div className="h-6 bg-gray-400 rounded w-3/4"></div>
<div className="h-4 bg-gray-400 rounded w-1/2"></div>
<div className="h-5 bg-gray-400 rounded mt-auto w-1/4"></div>
</div>
</div>
);
};

export default SkeletonCard;
24 changes: 24 additions & 0 deletions src/components/WorkerCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
'use client';
import Image from 'next/image';
import { WorkerType } from '@/types/workers';

export default function WorkerCard({ worker }: { worker: WorkerType }) {
return (
<div className="bg-white p-4 rounded shadow relative">
<div className="relative w-full h-48 rounded overflow-hidden mb-4">
<Image
src={worker.image}
alt={worker.name}
fill
sizes="(max-width: 768px) 100vw, 33vw"
priority
style={{ objectFit: 'cover' }}
// Placeholder blur removed due to missing image file
/>
</div>
<h2 className="font-semibold text-lg mb-1">{worker.name}</h2>
<p className="text-gray-600 mb-1">{worker.service}</p>
<p className="font-bold">₹{worker.pricePerDay} / day</p>
</div>
);
}