diff --git a/package-lock.json b/package-lock.json index 3fdb753..3899ec5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,11 @@ "name": "frontend_dev_assignment", "version": "0.1.0", "dependencies": { + "framer-motion": "^12.23.21", "next": "15.5.4", "react": "19.1.0", - "react-dom": "19.1.0" + "react-dom": "19.1.0", + "react-icons": "^5.5.0" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -3294,6 +3296,33 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/framer-motion": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.21.tgz", + "integrity": "sha512-UWDtzzPdRA3UpSNGril5HjUtPF1Uo/BCt5VKG/YQ8tVpSkAZ22+q8o+hYO0C1uDAZuotQjcfzsTsDtQxD46E/Q==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.21", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4543,6 +4572,21 @@ "node": ">= 18" } }, + "node_modules/motion-dom": { + "version": "12.23.21", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.21.tgz", + "integrity": "sha512-5xDXx/AbhrfgsQmSE7YESMn4Dpo6x5/DTZ4Iyy4xqDvVHWvFVoV+V2Ri2S/ksx+D40wrZ7gPYiMWshkdoqNgNQ==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5034,6 +5078,15 @@ "react": "^19.1.0" } }, + "node_modules/react-icons": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", + "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "license": "MIT", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 252da23..f7fce17 100644 --- a/package.json +++ b/package.json @@ -9,19 +9,21 @@ "lint": "eslint" }, "dependencies": { + "framer-motion": "^12.23.21", + "next": "15.5.4", "react": "19.1.0", "react-dom": "19.1.0", - "next": "15.5.4" + "react-icons": "^5.5.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" } } diff --git a/workers.json b/public/workers.json similarity index 100% rename from workers.json rename to public/workers.json diff --git a/src/app/api/services/route.ts b/src/app/api/services/route.ts index 1a660a9..6b8c517 100644 --- a/src/app/api/services/route.ts +++ b/src/app/api/services/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from 'next/server' -import workersData from '../../../../workers.json' +import workersData from "../../../../public/workers.json" // GET /api/services export async function GET(request: NextRequest) { diff --git a/src/app/api/workers/route.ts b/src/app/api/workers/route.ts index 44a245e..50da5bf 100644 --- a/src/app/api/workers/route.ts +++ b/src/app/api/workers/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from 'next/server' -import workersData from '../../../../workers.json' +import workersData from '../../../../public/workers.json' export async function GET() { try { diff --git a/src/app/layout.tsx b/src/app/layout.tsx index f7fa87e..b68c5be 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,12 @@ import type { Metadata } from "next"; import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; +import Navbar from "@/component/Navbar"; + +export const metadata: Metadata = { + title: "SolveEase", + description: "Hire skilled workers and manage services efficiently.", +}; const geistSans = Geist({ variable: "--font-geist-sans", @@ -12,11 +18,6 @@ const geistMono = Geist_Mono({ subsets: ["latin"], }); -export const metadata: Metadata = { - title: "Create Next App", - description: "Generated by create next app", -}; - export default function RootLayout({ children, }: Readonly<{ @@ -27,7 +28,8 @@ export default function RootLayout({ - {children} + +
{children}
); diff --git a/src/app/page.tsx b/src/app/page.tsx index 23eaf49..002f89e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,57 +1,12 @@ -'use client' -import { WorkerType } from '@/types/workers' -import Image from 'next/image' -import { useState, useEffect } from 'react' +// src/app/workers/page.tsx +import WorkersPageClient from "@/component/WorkerPageClient"; -export default function WorkersPage() { - const [workersData, setWorkersData] = useState([]) - - 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() - }, []) +export const metadata = { + title: "Workers | SolveEase", + description: + "Hire skilled workers for various services. Browse workers by service type and price.", +}; - 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} -
-
-

{worker.name}

-

{worker.service}

-

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

-
-
- ))} -
-
- ) +export default function WorkersPage() { + return ; } diff --git a/src/component/Navbar.tsx b/src/component/Navbar.tsx new file mode 100644 index 0000000..6b875ec --- /dev/null +++ b/src/component/Navbar.tsx @@ -0,0 +1,72 @@ +"use client"; +import { useState } from "react"; +import Link from "next/link"; +import { HiMenu, HiX } from "react-icons/hi"; + + + +export default function Navbar() { + const [isOpen, setIsOpen] = useState(false); + + const toggleMenu = () => setIsOpen(!isOpen); + + const navLinks = [ + { name: "Home", href: "/" }, + { name: "Workers", href: "/workers" }, + { name: "Contact", href: "/contact" }, + ]; + + return ( + + ); +} diff --git a/src/component/WorkerPageClient.tsx b/src/component/WorkerPageClient.tsx new file mode 100644 index 0000000..dba1ce6 --- /dev/null +++ b/src/component/WorkerPageClient.tsx @@ -0,0 +1,274 @@ +// src/app/workers/WorkersPageClient.tsx +"use client"; + +import { WorkerType } from "@/types/workers"; +import Image from "next/image"; +import { useState, useEffect, useMemo } from "react"; +import { motion, AnimatePresence } from "framer-motion"; + +const ITEMS_PER_PAGE = 9; +const PAGE_WINDOW = 3; + +export default function WorkersPageClient() { + const [workersData, setWorkersData] = useState([]); + const [selectedWorker, setSelectedWorker] = useState(null); + const [loading, setLoading] = useState(true); + const [currentPage, setCurrentPage] = useState(1); + + // Filters + const [serviceFilter, setServiceFilter] = useState("All"); + const [priceFilter, setPriceFilter] = useState(0); // max price selected + + const maxWorkerPrice = useMemo( + () => Math.max(...workersData.map((w) => w.pricePerDay * 1.18)), + [workersData] + ); + + // Load workers data + useEffect(() => { + const loadData = async () => { + try { + const res = await fetch("/workers.json"); // fetch JSON from public folder + const data = await res.json(); + setWorkersData(data); + setPriceFilter( + Math.max( + ...data.map((w: WorkerType) => Math.round(w.pricePerDay * 1.18)) + ) + ); + } catch (error) { + console.error("Failed to load workers:", error); + } finally { + setLoading(false); + } + }; + loadData(); + }, []); + + // Filtered workers + const filteredWorkers = useMemo(() => { + return workersData + .filter((w) => w.pricePerDay > 0 && w.id !== null) + .filter((w) => serviceFilter === "All" || w.service === serviceFilter) + .filter((w) => Math.round(w.pricePerDay * 1.18) <= priceFilter) + .sort((a, b) => a.name.localeCompare(b.name)); + }, [workersData, serviceFilter, priceFilter]); + + const totalPages = Math.ceil(filteredWorkers.length / ITEMS_PER_PAGE); + + const paginatedWorkers = useMemo(() => { + const startIndex = (currentPage - 1) * ITEMS_PER_PAGE; + return filteredWorkers.slice(startIndex, startIndex + ITEMS_PER_PAGE); + }, [filteredWorkers, currentPage]); + + const getPageNumbers = () => { + const start = Math.max(1, currentPage - 1); + const end = Math.min(totalPages, start + PAGE_WINDOW - 1); // ✅ use const + const adjustedStart = + end - start < PAGE_WINDOW - 1 + ? Math.max(1, end - PAGE_WINDOW + 1) + : start; + + const pages = []; + for (let i = adjustedStart; i <= end; i++) pages.push(i); + return pages; + }; + + const workerCards = useMemo(() => { + return paginatedWorkers.map((worker: WorkerType, index: number) => ( + +
+ {worker.name} +
+ +
+
+

+ {worker.name} +

+

{worker.service}

+
+
+

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

+ + setSelectedWorker(worker)} + className="mt-5 w-full py-2.5 rounded-lg bg-indigo-600 text-white font-medium shadow hover:bg-indigo-700 hover:shadow-lg transition-all duration-300" + > + Hire Now + +
+
+
+ )); + }, [paginatedWorkers]); + + const serviceOptions = useMemo(() => { + const services = Array.from(new Set(workersData.map((w) => w.service))); + return ["All", ...services]; + }, [workersData]); + + return ( +
+

+ Meet Our Workers +

+ + {/* Filters */} +
+ {/* Service Filter */} + + + {/* Price Filter */} +
+ + { + setPriceFilter(Number(e.target.value)); + setCurrentPage(1); + }} + className="w-40" + /> +
+
+ + {/* Workers Grid */} +
+ {loading + ? Array.from({ length: ITEMS_PER_PAGE }).map((_, idx) => ( +
+ )) + : workerCards} +
+ + {/* Pagination Controls */} + {!loading && totalPages > 1 && ( +
+ + + {getPageNumbers().map((page) => ( + + ))} + + +
+ )} + + {/* Worker Modal */} + + {selectedWorker && ( + + + + +
+ {selectedWorker.name} + +

+ {selectedWorker.name} +

+

{selectedWorker.service}

+

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

+
+ + + Confirm Booking + +
+
+ )} +
+
+ ); +}