From e78bee849685cd9dc1dd06cffab03ccd972ab7ab Mon Sep 17 00:00:00 2001 From: Sulakshani Dissanayake Date: Sat, 1 Nov 2025 23:19:09 +0530 Subject: [PATCH] customer side service browsing and catalogs --- next.config.ts | 11 +- package-lock.json | 116 +++++- package.json | 1 + src/app/(user)/services/[id]/page.tsx | 251 ++++++++++++ src/app/(user)/services/page.tsx | 526 ++++++++++++++++++++++++++ src/app/page.tsx | 266 ++++++++----- src/lib/api/config.ts | 81 ++++ src/lib/api/services.ts | 499 ++++++++++++++++++++++++ 8 files changed, 1651 insertions(+), 100 deletions(-) create mode 100644 src/app/(user)/services/[id]/page.tsx create mode 100644 src/app/(user)/services/page.tsx create mode 100644 src/lib/api/config.ts create mode 100644 src/lib/api/services.ts diff --git a/next.config.ts b/next.config.ts index e9ffa30..d94f239 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,7 +1,16 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ + images: { + remotePatterns: [ + { + protocol: 'https', + hostname: 'images.unsplash.com', + port: '', + pathname: '/**', + }, + ], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index da7dde8..f23d888 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "eda-frontend", "version": "0.1.0", "dependencies": { + "axios": "^1.13.1", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0" @@ -1888,6 +1889,12 @@ "node": ">= 0.4" } }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, "node_modules/autoprefixer": { "version": "10.4.21", "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", @@ -1952,6 +1959,17 @@ "node": ">=4" } }, + "node_modules/axios": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.1.tgz", + "integrity": "sha512-hU4EGxxt+j7TQijx1oYdAjw4xuIp1wRQSsbMFwSthCWeBQur1eF+qJ5iQ5sN3Tw8YRzQNKb8jszgBdMDVqwJcw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2073,7 +2091,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -2246,6 +2263,18 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2420,6 +2449,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/detect-libc": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", @@ -2461,7 +2499,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -2566,7 +2603,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2576,7 +2612,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -2614,7 +2649,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -2627,7 +2661,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -3244,6 +3277,26 @@ "dev": true, "license": "ISC" }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/for-each": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", @@ -3277,6 +3330,22 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fraction.js": { "version": "4.3.7", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", @@ -3310,7 +3379,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -3351,7 +3419,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -3376,7 +3443,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -3511,7 +3577,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3583,7 +3648,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -3596,7 +3660,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -3612,7 +3675,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -4345,7 +4407,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -4375,6 +4436,27 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5031,6 +5113,12 @@ "react-is": "^16.13.1" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/package.json b/package.json index e987814..946ccbf 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "lint": "eslint" }, "dependencies": { + "axios": "^1.13.1", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0" diff --git a/src/app/(user)/services/[id]/page.tsx b/src/app/(user)/services/[id]/page.tsx new file mode 100644 index 0000000..55ca286 --- /dev/null +++ b/src/app/(user)/services/[id]/page.tsx @@ -0,0 +1,251 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { useParams, useRouter } from 'next/navigation'; +import Image from 'next/image'; +import { serviceApi, ServiceResponse } from '@/lib/api/services'; + +interface ServiceDetail extends Omit { + category: string; + price: string; + duration: string; +} + +export default function ServiceDetailPage() { + const params = useParams(); + const router = useRouter(); + const [service, setService] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchServiceDetails = async () => { + setLoading(true); + setError(null); + try { + const id = params.id as string; + console.log('🔄 Fetching service details for ID:', id); + const data = await serviceApi.getServiceById(id); + + // Transform the data + const transformedService: ServiceDetail = { + ...data, + category: typeof data.category === 'object' && data.category !== null + ? (data.category as any).name + : data.category || 'Service', + price: typeof data.price === 'number' + ? `${data.currency || 'LKR'} ${data.price.toFixed(2)}` + : String(data.price), + duration: data.duration || `${data.durationInHours || 1} hours`, + }; + + setService(transformedService); + console.log('✅ Service details loaded:', transformedService); + } catch (err) { + console.error('❌ Failed to fetch service details:', err); + setError('Unable to load service details'); + } finally { + setLoading(false); + } + }; + + if (params.id) { + fetchServiceDetails(); + } + }, [params.id]); + + if (loading) { + return ( +
+
+
+

Loading service details...

+
+
+ ); + } + + if (error || !service) { + return ( +
+
+
⚠️
+

Service Not Found

+

{error || 'The requested service could not be found.'}

+ +
+
+ ); + } + + return ( +
+ {/* Header */} +
+
+
+
+ + + +
+

AutoService Hub

+
+
+
+ + {/* Main Content */} +
+ {/* Back Button */} + + + {/* Service Card */} +
+ {/* Service Title */} +
+

+ {service.name} +

+

+ {service.description} +

+
+ + {/* Service Image */} +
+
+ {service.imageUrl ? ( + {service.name} + ) : ( +
+ + + +
+ )} +
+
+ + {/* Service Overview Section */} +
+

Service Overview

+ +
+

+ Our {service.name} service utilizes advanced tools and expert technical inspection to prevent any underlying issues affecting your vehicle's performance. + This includes comprehensive diagnostics, performance monitoring, and a thorough inspection of all critical components. + We provide a detailed report of our findings and recommend necessary repairs or maintenance, ensuring your vehicle runs smoothly and efficiently. + This service is crucial for maintaining vehicle health and preventing major breakdowns. +

+ + {/* Service Details Grid */} +
+ {/* Category */} +
+
+ + + + +
+
+

Category:

+

{service.category}

+
+
+ + {/* Price */} +
+
+ + + + +
+
+

Price:

+

{service.price}

+
+
+ + {/* Duration */} +
+
+ + + +
+
+

Duration:

+

{service.duration}

+
+
+ + {/* Status */} +
+
+ + + +
+
+

Status:

+ + {service.status || 'ACTIVE'} + +
+
+
+
+ + {/* Action Button */} + +
+
+
+ + {/* Footer */} +
+
+
+
+ + + +
+

AutoService Hub

+
+

© 2025 AutoService Hub. All rights reserved.

+
+ + +
+
+
+
+ ); +} diff --git a/src/app/(user)/services/page.tsx b/src/app/(user)/services/page.tsx new file mode 100644 index 0000000..156c6cd --- /dev/null +++ b/src/app/(user)/services/page.tsx @@ -0,0 +1,526 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import Image from 'next/image'; +import { serviceApi, ServiceResponse } from '@/lib/api/services'; + +// Use the interface from API but extend it for frontend display +interface VehicleService extends Omit { + price: string | number; + category: string; // Always string after transformation +} + +const vehicleServices: VehicleService[] = [ + { + id: '1', + name: 'Oil Change & Filter Replacement', + description: 'Complete oil change service with premium synthetic or conventional oil. Includes oil filter replacement and fluid level check.', + price: '$49.99', + duration: '30 mins', + imageUrl: 'https://images.unsplash.com/photo-1632823469412-20e3dd1c7a84?w=500&h=300&fit=crop', + category: 'Maintenance', + popular: true, + features: ['Premium oil options', 'Oil filter replacement', 'Multi-point inspection', 'Fluid top-off'], + }, + { + id: '2', + name: 'Brake Inspection & Service', + description: 'Comprehensive brake system inspection including pads, rotors, calipers, and brake fluid. Replacement services available.', + price: '$89.99', + duration: '1 hour', + imageUrl: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?w=500&h=300&fit=crop', + category: 'Safety', + popular: true, + features: ['Brake pad inspection', 'Rotor measurement', 'Brake fluid check', 'Test drive included'], + }, + { + id: '3', + name: 'Tire Rotation & Balance', + description: 'Professional tire rotation and balancing to ensure even wear and optimal performance. Extends tire life significantly.', + price: '$59.99', + duration: '45 mins', + imageUrl: 'https://images.unsplash.com/photo-1619642751034-765dfdf7c58e?w=500&h=300&fit=crop', + category: 'Maintenance', + features: ['4-wheel rotation', 'Wheel balancing', 'Tire pressure check', 'Visual inspection'], + }, + { + id: '4', + name: 'Engine Diagnostics', + description: 'Advanced computer diagnostics to identify engine issues. Comprehensive scan of all vehicle systems with detailed report.', + price: '$79.99', + duration: '1 hour', + imageUrl: 'https://images.unsplash.com/photo-1615906655593-ad0386982a0f?w=500&h=300&fit=crop', + category: 'Diagnostics', + popular: true, + features: ['Full system scan', 'Error code reading', 'Detailed report', 'Expert consultation'], + }, + { + id: '5', + name: 'Air Conditioning Service', + description: 'Complete AC system inspection, refrigerant recharge, and performance check. Stay cool all summer long.', + price: '$129.99', + duration: '1.5 hours', + imageUrl: 'https://images.unsplash.com/photo-1625047509248-ec889cbff17f?w=500&h=300&fit=crop', + category: 'Climate Control', + features: ['AC system inspection', 'Refrigerant recharge', 'Leak detection', 'Performance test'], + }, + { + id: '6', + name: 'Battery Test & Replacement', + description: 'Complete battery testing and replacement service. We test charging system and install premium batteries with warranty.', + price: '$149.99', + duration: '30 mins', + imageUrl: 'https://images.unsplash.com/photo-1619642751034-765dfdf7c58e?w=500&h=300&fit=crop', + category: 'Electrical', + features: ['Battery load test', 'Charging system check', 'Terminal cleaning', 'Premium battery options'], + }, + { + id: '7', + name: 'Transmission Service', + description: 'Transmission fluid change and filter replacement. Helps prevent costly transmission repairs and ensures smooth shifting.', + price: '$179.99', + duration: '2 hours', + imageUrl: 'https://images.unsplash.com/photo-1487754180451-c456f719a1fc?w=500&h=300&fit=crop', + category: 'Maintenance', + features: ['Fluid exchange', 'Filter replacement', 'Gasket inspection', 'Test drive included'], + }, + { + id: '8', + name: 'Full Detail & Wash', + description: 'Premium interior and exterior detailing. Hand wash, wax, interior vacuum, and conditioning. Make your car look brand new.', + price: '$199.99', + duration: '3 hours', + imageUrl: 'https://images.unsplash.com/photo-1601362840469-51e4d8d58785?w=500&h=300&fit=crop', + category: 'Detailing', + popular: true, + features: ['Exterior hand wash & wax', 'Interior deep clean', 'Leather conditioning', 'Tire shine'], + }, + { + id: '9', + name: 'Wheel Alignment', + description: 'Precision wheel alignment using advanced computerized equipment. Improves handling and extends tire life.', + price: '$99.99', + duration: '1 hour', + imageUrl: 'https://images.unsplash.com/photo-1625047509248-ec889cbff17f?w=500&h=300&fit=crop', + category: 'Maintenance', + features: ['4-wheel alignment', 'Computerized precision', 'Steering check', 'Before/after report'], + }, + { + id: '10', + name: 'Exhaust System Repair', + description: 'Complete exhaust system inspection and repair. Muffler, catalytic converter, and pipe replacement available.', + price: '$299.99', + duration: '2 hours', + imageUrl: 'https://images.unsplash.com/photo-1487754180451-c456f719a1fc?w=500&h=300&fit=crop', + category: 'Repair', + features: ['Full system inspection', 'Leak detection', 'Component replacement', 'Emissions test'], + }, + { + id: '11', + name: 'Suspension Service', + description: 'Inspection and repair of shocks, struts, and suspension components. Ensures smooth ride and proper handling.', + price: '$249.99', + duration: '2.5 hours', + imageUrl: 'https://images.unsplash.com/photo-1615906655593-ad0386982a0f?w=500&h=300&fit=crop', + category: 'Repair', + features: ['Shock/strut inspection', 'Component replacement', 'Ride quality test', 'Alignment check'], + }, + { + id: '12', + name: 'Pre-Purchase Inspection', + description: 'Comprehensive 150-point inspection for used car buyers. Get complete peace of mind before your purchase.', + price: '$149.99', + duration: '2 hours', + imageUrl: 'https://images.unsplash.com/photo-1486262715619-67b85e0b08d3?w=500&h=300&fit=crop', + category: 'Diagnostics', + features: ['150-point inspection', 'Detailed written report', 'Photos included', 'Expert recommendations'], + }, +]; + +const categories = [ + 'All Services', + 'Maintenance', + 'Safety', + 'Diagnostics', + 'Repair', + 'Electrical', + 'Climate Control', + 'Detailing', +]; + +export default function VehicleServicesPage() { + const [searchQuery, setSearchQuery] = useState(''); + const [selectedCategory, setSelectedCategory] = useState('All Services'); + const [activeFilter, setActiveFilter] = useState('All'); + const [services, setServices] = useState([]); // Start with empty array - fetch from backend + const [loading, setLoading] = useState(true); // Start with loading true + const [error, setError] = useState(null); + + // Fetch services from backend + useEffect(() => { + const fetchServices = async () => { + setLoading(true); + setError(null); + try { + console.log('🔄 Fetching services from backend...'); + const data = await serviceApi.getAllServices(); + + // Transform backend data to frontend format + const transformedData = data.map(service => ({ + ...service, + // Convert price to display format (backend uses LKR, frontend displays with currency) + price: typeof service.price === 'number' + ? `${service.currency || 'LKR'} ${service.price.toFixed(2)}` + : service.price, + // Convert durationInHours to duration string if not present + duration: service.duration || `${service.durationInHours || 1} hour${(service.durationInHours || 1) !== 1 ? 's' : ''}`, + // Extract category name from category object (backend sends {id, name, description}) + category: typeof service.category === 'object' && service.category !== null + ? (service.category as any).name + : service.category || 'Service', + categoryId: typeof service.category === 'object' && service.category !== null + ? (service.category as any).id + : service.categoryId, + // Ensure features array exists (backend might not have it) + features: service.features || [], + // Set popular based on status or price (you can customize this logic) + popular: service.popular !== undefined ? service.popular : service.price > 5000, + })); + + setServices(transformedData as VehicleService[]); + console.log(`✅ Successfully loaded ${transformedData.length} services from backend`); + console.log('📦 Sample service:', transformedData[0]); + } catch (err) { + console.error('❌ Failed to fetch services from backend:', err); + setError('Unable to load services. Please make sure the backend server is running on http://localhost:8080'); + + // Option: Use fallback data in development + if (process.env.NODE_ENV === 'development') { + console.info('⚠️ Using fallback mock data for development'); + setServices(vehicleServices); + } + } finally { + setLoading(false); + } + }; + + fetchServices(); + }, []); // Run once on mount + + // Filter services based on search and category + const filteredServices = services.filter((service) => { + const matchesSearch = service.name.toLowerCase().includes(searchQuery.toLowerCase()) || + service.description.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesCategory = selectedCategory === 'All Services' || service.category === selectedCategory; + const matchesFilter = activeFilter === 'All' || + (activeFilter === 'Popular' && service.popular) || + (activeFilter === 'Maintenance' && service.category === 'Maintenance') || + (activeFilter === 'Repair' && service.category === 'Repair'); + + return matchesSearch && matchesCategory && matchesFilter; + }); + + const resetFilters = () => { + setSearchQuery(''); + setSelectedCategory('All Services'); + setActiveFilter('All'); + }; + + return ( +
+ {/* Backend Connection Error Alert */} + {error && ( +
+
+
+ + + +
+

Backend Connection Error

+

{error}

+

+ Steps to fix: +
1. Make sure Spring Boot backend is running: ./mvnw spring-boot:run +
2. Verify backend is accessible: curl http://localhost:8080/api/customer/services +
3. Check CORS configuration in your backend +

+
+
+
+
+ )} + + {/* Hero Section */} +
+
+
+

+ Vehicle Service Center +

+

+ Professional automotive care and maintenance services. Quality work, competitive prices, and exceptional customer service. +

+
+
+ + + + Certified Technicians +
+
+ + + + Warranty on All Services +
+
+ + + + Same-Day Service Available +
+
+
+
+
+ + {/* Search and Filter Section */} +
+
+ {/* Search and Filter Bar */} +
+
+ setSearchQuery(e.target.value)} + className="w-full px-4 py-2.5 pl-10 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent" + /> + + + +
+ + + + +
+ + {/* Filter Tabs */} +
+ {['All', 'Popular', 'Maintenance', 'Repair'].map((filter) => ( + + ))} +
+
+
+ + {/* Services Grid */} +
+ {/* Results count and loading indicator */} +
+

+ Showing {filteredServices.length} service{filteredServices.length !== 1 ? 's' : ''} +

+ {loading && ( +
+ + + + + Loading services... +
+ )} +
+ + {/* Loading Skeleton */} + {loading && services.length === 0 ? ( +
+ {[1, 2, 3, 4, 5, 6].map((n) => ( +
+
+
+
+
+
+
+
+
+
+
+
+ ))} +
+ ) : ( +
+ {filteredServices.map((service) => ( +
+ {/* Service Image */} +
+ {service.imageUrl ? ( + {service.name} + ) : ( +
+ + + +
+ )} + {service.popular && ( +
+ + + + Popular +
+ )} +
+ {service.category} +
+
+ + {/* Service Details */} +
+

+ {service.name} +

+ +

+ {service.description} +

+ + {/* Features */} + {service.features && Array.isArray(service.features) && service.features.length > 0 && ( +
+
    + {service.features.slice(0, 3).map((feature, index) => ( +
  • + + + + {feature} +
  • + ))} +
+
+ )} + + {/* Price and Duration */} +
+
+

+ {service.price} +

+
+
+ + + + {service.duration} +
+
+ + {/* Action Buttons */} +
+ + Details + + +
+
+
+ ))} +
+ )} + + {/* Empty State */} + {!loading && filteredServices.length === 0 && ( +
+ + + +

No services found

+

Try adjusting your search or filters

+
+ )} +
+ + {/* Footer CTA Section */} +
+
+

Need Help Choosing a Service?

+

+ Our expert technicians are here to help. Contact us for a free consultation and quote. +

+
+ + +
+
+
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index a932894..8dd37a4 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,103 +1,199 @@ +'use client'; + import Image from "next/image"; +import Link from "next/link"; export default function Home() { return ( -
-
- Next.js logo -
    -
  1. - Get started by editing{" "} - - src/app/page.tsx - - . -
  2. -
  3. - Save and see your changes instantly. -
  4. -
- -
+
+ {/* Animated background elements */} +
+
+
+
+ +
+ {/* Logo with fade-in animation */} +
+ Next.js logo +
+ + {/* Hero section */} +
+

+ Build the Future +

+

+ Create lightning-fast web experiences with Next.js +

+
+ + {/* Interactive cards */} +
+
+
+ + + +
+

Quick Start

+

+ Edit src/app/page.tsx and see changes instantly +

+
+ +
+
+ + + +
+

Hot Reload

+

+ Experience instant feedback with automatic page updates as you code +

+
+
+ + {/* CTA buttons */} +
+ + + Browse Services + + + + + - Vercel logomark - Deploy now + Read our docs +
+ + {/* Resource links */} +
- + +
); -} +} \ No newline at end of file diff --git a/src/lib/api/config.ts b/src/lib/api/config.ts new file mode 100644 index 0000000..e57559a --- /dev/null +++ b/src/lib/api/config.ts @@ -0,0 +1,81 @@ +import axios, { AxiosInstance, AxiosError, InternalAxiosRequestConfig } from 'axios'; + +// Create axios instance with default config +const apiClient: AxiosInstance = axios.create({ + baseURL: process.env.NEXT_PUBLIC_API_BASE_URL || 'http://localhost:8080/api', + timeout: parseInt(process.env.NEXT_PUBLIC_API_TIMEOUT || '10000'), + headers: { + 'Content-Type': 'application/json', + }, +}); + +// Request interceptor - Add auth token if available +apiClient.interceptors.request.use( + (config: InternalAxiosRequestConfig) => { + // Get token from localStorage or cookies + if (typeof window !== 'undefined') { + const token = localStorage.getItem('authToken'); + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}`; + } + } + return config; + }, + (error: AxiosError) => { + return Promise.reject(error); + } +); + +// Response interceptor - Handle common errors +apiClient.interceptors.response.use( + (response) => { + return response; + }, + (error: AxiosError) => { + // Handle common error scenarios + if (error.response) { + switch (error.response.status) { + case 401: + // Unauthorized - redirect to login + if (typeof window !== 'undefined') { + localStorage.removeItem('authToken'); + window.location.href = '/login'; + } + break; + case 403: + // Forbidden + console.warn('Access forbidden'); + break; + case 404: + // Not found + console.warn('Resource not found'); + break; + case 500: + // Server error + console.warn('Server error occurred'); + break; + default: + console.warn('API error:', error.response.data); + } + } else if (error.request) { + // Network error - backend not available + console.info('Backend API not available - using fallback data'); + } + return Promise.reject(error); + } +); + +export default apiClient; + +// Helper function to handle API errors +export const handleApiError = (error: unknown): string => { + if (axios.isAxiosError(error)) { + if (error.response?.data?.message) { + return error.response.data.message; + } + if (error.message) { + return error.message; + } + } + return 'An unexpected error occurred'; +}; diff --git a/src/lib/api/services.ts b/src/lib/api/services.ts new file mode 100644 index 0000000..5238b12 --- /dev/null +++ b/src/lib/api/services.ts @@ -0,0 +1,499 @@ +import apiClient, { handleApiError } from './config'; + +// ==================== ENUMS ==================== +export enum AppointmentStatus { + PENDING = 'PENDING', + CONFIRMED = 'CONFIRMED', + IN_PROGRESS = 'IN_PROGRESS', + COMPLETED = 'COMPLETED', + CANCELLED = 'CANCELLED', +} + +export enum ServiceStatus { + ACTIVE = 'ACTIVE', + INACTIVE = 'INACTIVE', +} + +// ==================== PAGINATION INTERFACES ==================== +export interface PageableResponse { + content: T[]; + pageable: { + pageNumber: number; + pageSize: number; + sort: any; + offset: number; + paged: boolean; + unpaged: boolean; + }; + last: boolean; + totalPages: number; + totalElements: number; + first: boolean; + size: number; + number: number; + sort: any; + numberOfElements: number; + empty: boolean; +} + +// Category object structure from backend +export interface CategoryResponse { + id: string | number; + name: string; + description: string; +} + +// ==================== SERVICE INTERFACES ==================== +// Matches Service.java entity +export interface VehicleService { + id: string; + name: string; + description: string; + price: number; + duration: string; + imageUrl?: string; + category: string | CategoryResponse; + categoryId?: string; + popular?: boolean; + features: string[]; + status?: ServiceStatus; + estimatedTime?: number; // in minutes + durationInHours?: number; // Backend field + currency?: string; // Backend field + createdAt?: string; // Backend field + updatedAt?: string; // Backend field +} + +// Matches ServiceResponse.java DTO +export interface ServiceResponse { + id: string; + name: string; + description: string; + price: number; + duration?: string; + imageUrl?: string; + category: string | CategoryResponse; // Can be string or object from backend + categoryId?: string; + popular?: boolean; + features?: string[]; + status?: ServiceStatus; + durationInHours?: number; // Backend field + currency?: string; // Backend field + createdAt?: string; // Backend field + updatedAt?: string; // Backend field +} + +// ==================== APPOINTMENT INTERFACES ==================== +// Matches CreateAppointmentRequest.java +export interface CreateAppointmentRequest { + serviceId: string; + vehicleId: string; + appointmentDate: string; // ISO 8601 format + timeSlot: string; + notes?: string; +} + +// Matches AppointmentResponse.java +export interface AppointmentResponse { + id: string; + serviceId: string; + serviceName: string; + vehicleId: string; + vehicleInfo: string; + customerId: string; + customerName: string; + appointmentDate: string; + timeSlot: string; + status: AppointmentStatus; + notes?: string; + createdAt: string; + updatedAt?: string; +} + +// Matches AppointmentDetailsResponse.java +export interface AppointmentDetailsResponse extends AppointmentResponse { + serviceDetails: ServiceResponse; + vehicleDetails: VehicleResponse; + progressUpdates?: ProgressResponse[]; + timeLogs?: TimeLogResponse[]; +} + +// ==================== VEHICLE INTERFACES ==================== +export interface VehicleResponse { + id: string; + make: string; + model: string; + year: number; + vin?: string; + licensePlate: string; + color?: string; + customerId: string; +} + +// ==================== PROGRESS INTERFACES ==================== +export interface ProgressResponse { + id: string; + appointmentId: string; + status: AppointmentStatus; + description: string; + percentage: number; + updatedBy: string; + updatedAt: string; +} + +// ==================== TIME LOG INTERFACES ==================== +export interface TimeLogResponse { + id: string; + appointmentId: string; + employeeId: string; + employeeName: string; + startTime: string; + endTime?: string; + duration?: number; // in minutes + description?: string; +} + +// ==================== SERVICE CATALOG API ==================== +// Maps to ServiceCatalogController.java (Member 3 - sulakshani) +export const serviceApi = { + // Get all services (handles paginated response) + getAllServices: async (params?: { + page?: number; + size?: number; + category?: string; + search?: string; + status?: ServiceStatus; + }): Promise => { + try { + const response = await apiClient.get>('/customer/services', { + params: { + page: params?.page || 0, + size: params?.size || 100, // Get all services by default + ...params + } + }); + + // Extract content array from paginated response + if (response.data && response.data.content) { + return response.data.content; + } + + // Fallback if response is already an array + return Array.isArray(response.data) ? response.data : []; + } catch (error) { + // Silently fail and let the component handle fallback + const errorMessage = handleApiError(error); + console.info('Backend not available:', errorMessage); + throw error; + } + }, + + // GET /api/customer/services/{id} + getServiceById: async (id: string): Promise => { + try { + const response = await apiClient.get(`/customer/services/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching service:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/services/category/{categoryId} + getServicesByCategory: async (categoryId: string): Promise => { + try { + const response = await apiClient.get(`/customer/services/category/${categoryId}`); + return response.data; + } catch (error) { + console.error('Error fetching services by category:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/services/popular + getPopularServices: async (): Promise => { + try { + const response = await apiClient.get('/customer/services/popular'); + return response.data; + } catch (error) { + console.error('Error fetching popular services:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/services/search?query={query} + searchServices: async (query: string): Promise => { + try { + const response = await apiClient.get('/customer/services/search', { + params: { query }, + }); + return response.data; + } catch (error) { + console.error('Error searching services:', handleApiError(error)); + throw error; + } + }, +}; + +// ==================== APPOINTMENT API ==================== +// Maps to CustomerAppointmentController.java (Member 4 - charindu, Member 5 - dilusha) +export const appointmentApi = { + // POST /api/customer/appointments - Create appointment + createAppointment: async (request: CreateAppointmentRequest): Promise => { + try { + const response = await apiClient.post('/customer/appointments', request); + return response.data; + } catch (error) { + console.error('Error creating appointment:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/appointments - Get user's appointments + getMyAppointments: async (params?: { + status?: AppointmentStatus; + page?: number; + size?: number; + }): Promise => { + try { + const response = await apiClient.get('/customer/appointments', { params }); + return response.data; + } catch (error) { + console.error('Error fetching appointments:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/appointments/{id} - Get appointment details + getAppointmentById: async (id: string): Promise => { + try { + const response = await apiClient.get(`/customer/appointments/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching appointment:', handleApiError(error)); + throw error; + } + }, + + // PUT /api/customer/appointments/{id}/cancel - Cancel appointment + cancelAppointment: async (id: string, reason?: string): Promise => { + try { + const response = await apiClient.put(`/customer/appointments/${id}/cancel`, { reason }); + return response.data; + } catch (error) { + console.error('Error canceling appointment:', handleApiError(error)); + throw error; + } + }, + + // PUT /api/customer/appointments/{id}/reschedule + rescheduleAppointment: async ( + id: string, + newDate: string, + newTimeSlot: string + ): Promise => { + try { + const response = await apiClient.put(`/customer/appointments/${id}/reschedule`, { + appointmentDate: newDate, + timeSlot: newTimeSlot, + }); + return response.data; + } catch (error) { + console.error('Error rescheduling appointment:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/appointments/available-slots + getAvailableSlots: async (date: string, serviceId: string): Promise => { + try { + const response = await apiClient.get('/customer/appointments/available-slots', { + params: { date, serviceId }, + }); + return response.data; + } catch (error) { + console.error('Error fetching available slots:', handleApiError(error)); + throw error; + } + }, +}; + +// ==================== VEHICLE API ==================== +// Maps to VehicleController.java (Member 2 - navindu) +export const vehicleApi = { + // GET /api/customer/vehicles - Get user's vehicles + getMyVehicles: async (): Promise => { + try { + const response = await apiClient.get('/customer/vehicles'); + return response.data; + } catch (error) { + console.error('Error fetching vehicles:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/vehicles/{id} + getVehicleById: async (id: string): Promise => { + try { + const response = await apiClient.get(`/customer/vehicles/${id}`); + return response.data; + } catch (error) { + console.error('Error fetching vehicle:', handleApiError(error)); + throw error; + } + }, + + // POST /api/customer/vehicles + createVehicle: async (vehicle: Omit): Promise => { + try { + const response = await apiClient.post('/customer/vehicles', vehicle); + return response.data; + } catch (error) { + console.error('Error creating vehicle:', handleApiError(error)); + throw error; + } + }, + + // PUT /api/customer/vehicles/{id} + updateVehicle: async ( + id: string, + vehicle: Partial + ): Promise => { + try { + const response = await apiClient.put(`/customer/vehicles/${id}`, vehicle); + return response.data; + } catch (error) { + console.error('Error updating vehicle:', handleApiError(error)); + throw error; + } + }, + + // DELETE /api/customer/vehicles/{id} + deleteVehicle: async (id: string): Promise => { + try { + await apiClient.delete(`/customer/vehicles/${id}`); + } catch (error) { + console.error('Error deleting vehicle:', handleApiError(error)); + throw error; + } + }, +}; + +// ==================== PROGRESS TRACKING API ==================== +// Maps to ProgressViewController.java (Member 8 - aloka) +export const progressApi = { + // GET /api/customer/appointments/{appointmentId}/progress + getAppointmentProgress: async (appointmentId: string): Promise => { + try { + const response = await apiClient.get(`/customer/appointments/${appointmentId}/progress`); + return response.data; + } catch (error) { + console.error('Error fetching progress:', handleApiError(error)); + throw error; + } + }, +}; + +// Authentication API functions +export const authApi = { + login: async (email: string, password: string) => { + try { + const response = await apiClient.post('/auth/login', { email, password }); + if (response.data.token) { + localStorage.setItem('authToken', response.data.token); + } + return response.data; + } catch (error) { + console.error('Login error:', handleApiError(error)); + throw error; + } + }, + + register: async (userData: { + name: string; + email: string; + password: string; + }) => { + try { + const response = await apiClient.post('/auth/register', userData); + return response.data; + } catch (error) { + console.error('Registration error:', handleApiError(error)); + throw error; + } + }, + + logout: async () => { + try { + await apiClient.post('/auth/logout'); + localStorage.removeItem('authToken'); + } catch (error) { + console.error('Logout error:', handleApiError(error)); + throw error; + } + }, + + getCurrentUser: async () => { + try { + const response = await apiClient.get('/auth/me'); + return response.data; + } catch (error) { + console.error('Error fetching current user:', handleApiError(error)); + throw error; + } + }, +}; + +// ==================== DASHBOARD API ==================== +// Maps to CustomerDashboardController.java (Member 5 - dilusha) +export const dashboardApi = { + // GET /api/customer/dashboard - Get customer dashboard data + getDashboardData: async () => { + try { + const response = await apiClient.get('/customer/dashboard'); + return response.data; + } catch (error) { + console.error('Error fetching dashboard data:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/dashboard/upcoming-appointments + getUpcomingAppointments: async (limit: number = 5) => { + try { + const response = await apiClient.get('/customer/dashboard/upcoming-appointments', { + params: { limit }, + }); + return response.data; + } catch (error) { + console.error('Error fetching upcoming appointments:', handleApiError(error)); + throw error; + } + }, + + // GET /api/customer/dashboard/recent-services + getRecentServices: async (limit: number = 5) => { + try { + const response = await apiClient.get('/customer/dashboard/recent-services', { + params: { limit }, + }); + return response.data; + } catch (error) { + console.error('Error fetching recent services:', handleApiError(error)); + throw error; + } + }, +}; + +// Export all APIs as default for backward compatibility +export default { + service: serviceApi, + appointment: appointmentApi, + vehicle: vehicleApi, + progress: progressApi, + auth: authApi, + dashboard: dashboardApi, +}; + +// Also export serviceApi as vehicleServiceApi for backward compatibility with existing code +export const vehicleServiceApi = serviceApi;