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
10 changes: 10 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
}
2 changes: 1 addition & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function RootLayout({
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`font-sans ${geistSans.variable} ${geistMono.variable} antialiased bg-neutral-50`}
>
{children}
</body>
Expand Down
110 changes: 61 additions & 49 deletions src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,69 @@
'use client'
import { ImageContainer } from '@/components/images-container'
import {Navbar} from '@/components/navbar'
import { WorkerType } from '@/types/workers'
import Image from 'next/image'
import { useState, useEffect } from 'react'

export default function WorkersPage() {
const [workersData, setWorkersData] = useState<WorkerType[]>([])
// export default function WorkersPage() {
// const [workersData, setWorkersData] = useState<WorkerType[] | []>([])

useEffect(() => {
const loadData = async () => {
try {
const response = await import('../../workers.json')
setWorkersData(response.default)
} catch (error) {
console.error('Failed to load workers:', error)
}
}
loadData()
loadData()
}, [])
// const loadData = async () => {
// try {
// const response = await fetch('/api/workers')
// const data = await response.json()
// setWorkersData(data.data)
// } catch (error) {
// console.error('Failed to load workers:', error)
// }
// }

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>
// useEffect(() => {
// loadData()
// }, [])

<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'
>
<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>
))}
</div>
</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-3 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'
// >
// <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>
// ))}
// </div>
// </main>
// )
// }

export default function WorkersPage(){
return (
<>
<Navbar/>
<ImageContainer/>
</>
)
}
32 changes: 32 additions & 0 deletions src/components/image-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Image from "next/image"
import { WorkerType } from "@/types/workers"

export function ImageCard({ id, name, service, image, pricePerDay }: WorkerType) {
return (
<div className="max-w-xs rounded-lg overflow-hidden shadow-md hover:shadow-lg transition-shadow duration-300 p-2 bg-white">
<div className="relative w-full h-48">
<Image
src={image}
alt={name}
fill
className="object-cover rounded-lg"
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
priority={false}
/>
</div>

<div className="p-4">
<h3 className="text-lg font-medium text-gray-800">{name}</h3>
<p className="text-sm text-gray-500">{service}</p>
<div className="flex justify-between items-center mt-3">
<span className="text-teal-600 font-medium">₹{pricePerDay}/day</span>
<button className="px-6 py-1 text-[15px] rounded-lg bg-teal-600 text-white hover:bg-teal-700 transition-colors cursor-pointer">
Hire
</button>
</div>
</div>
</div>
)
}
129 changes: 129 additions & 0 deletions src/components/images-container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
"use client"

import { useEffect, useState, useMemo } from "react"
import { ImageCard } from "./image-card"
import { useWorkers } from "@/hooks/useWorkers"
import { WorkerType } from "@/types/workers"
import { ChevronLeft, ChevronRight } from "lucide-react"

function SkeletonCard() {
return (
<div className="max-w-xs rounded-lg p-2 bg-gray-200 animate-pulse">
<div className="w-full h-48 bg-gray-300 rounded-lg" />
<div className="mt-4 space-y-2">
<div className="h-4 bg-gray-300 rounded w-3/4" />
<div className="h-3 bg-gray-300 rounded w-1/2" />
<div className="h-5 bg-gray-300 rounded w-1/4 mt-4" />
</div>
</div>
)
}

export function ImageContainer() {
const { workers, loading } = useWorkers()
const [counter, setCounter] = useState(0)
const [priceRange, setPriceRange] = useState<[number, number]>([0, 10000])
const [serviceType, setServiceType] = useState("all")

const services = useMemo(() => {
const unique = Array.from(new Set(workers.map((w) => w.service)))
return ["all", ...unique]
}, [workers])

const filteredWorkers = useMemo(() => {
return workers.filter((w) => {
const inPriceRange = w.pricePerDay >= priceRange[0] && w.pricePerDay <= priceRange[1]
const matchesService = serviceType === "all" || w.service.toLowerCase() === serviceType.toLowerCase()
return inPriceRange && matchesService
})
}, [workers, priceRange, serviceType])

const sliceWorkers = useMemo(() => {
return filteredWorkers.slice(counter, counter + 12)
}, [filteredWorkers, counter])

useEffect(() => {
setCounter(0)
}, [priceRange, serviceType])

const handleNext = () => {
setCounter((prev) => (prev + 12 >= filteredWorkers.length ? 0 : prev + 12))
}

const handlePrev = () => {
setCounter((prev) => (prev - 12 < 0 ? Math.max(filteredWorkers.length - 12, 0) : prev - 12))
}

if (loading) {
return (
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6 p-8 mt-20">
{Array.from({ length: 12 }).map((_, i) => (
<SkeletonCard key={i} />
))}
</div>
)
}

return (
<div className="relative p-8 mt-20">
<div className="flex items-center justify-end mb-6 gap-4">
<select
value={serviceType}
onChange={(e) => setServiceType(e.target.value)}
className="px-3 py-2 border rounded-lg text-gray-700 focus:ring-2 focus:ring-teal-500 focus:outline-none"
>
{services.map((service) => (
<option key={service} value={service}>
{service.charAt(0).toUpperCase() + service.slice(1)}
</option>
))}
</select>

<div className="flex items-center gap-2">
<input
type="number"
placeholder="Min"
className="w-20 px-2 py-2 border rounded-lg focus:ring-2 focus:ring-teal-500 focus:outline-none"
value={priceRange[0]}
onChange={(e) => setPriceRange([+e.target.value || 0, priceRange[1]])}
/>
<span className="text-gray-500">-</span>
<input
type="number"
placeholder="Max"
className="w-20 px-2 py-2 border rounded-lg focus:ring-2 focus:ring-teal-500 focus:outline-none"
value={priceRange[1]}
onChange={(e) => setPriceRange([priceRange[0], +e.target.value || 10000])}
/>
</div>
</div>

{sliceWorkers.length > 0 ? (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-6">
{sliceWorkers.map((worker) => (
<ImageCard key={worker.id} {...worker} />
))}
</div>

<div className="flex items-center justify-center gap-4 mt-10">
<button
className="w-10 h-10 flex items-center justify-center rounded-full border border-gray-400 hover:border-teal-600 hover:text-teal-600 transition-colors bg-white shadow-md"
onClick={handlePrev}
>
<ChevronLeft size={20} />
</button>
<button
className="w-10 h-10 flex items-center justify-center rounded-full border border-gray-400 hover:border-teal-600 hover:text-teal-600 transition-colors bg-white shadow-md"
onClick={handleNext}
>
<ChevronRight size={20} />
</button>
</div>
</>
) : (
<p className="text-center text-gray-600 mt-10">No workers found.</p>
)}
</div>
)
}
Loading