From ee012986f0ed8e89d254412ee4ab6c5a07625ceb Mon Sep 17 00:00:00 2001 From: Sandip Bhesaniya <67558102+sandip444@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:34:41 +0530 Subject: [PATCH] Add sensor shop UI with admin inventory and API --- README.md | 18 + server/index.js | 172 ++++++++++ server/package.json | 13 + src/App.css | 498 ++++++++++++++++++++++++++++ src/App.js | 34 +- src/Containers/AdminPanel.jsx | 278 ++++++++++++++++ src/Containers/Header.jsx | 43 ++- src/Containers/prodectDetails.jsx | 114 +++++-- src/Containers/productComponent.jsx | 64 ++-- src/Containers/productListing.jsx | 89 ++++- src/api.js | 8 + src/data/sampleProducts.js | 80 +++++ 12 files changed, 1312 insertions(+), 99 deletions(-) create mode 100644 server/index.js create mode 100644 server/package.json create mode 100644 src/Containers/AdminPanel.jsx create mode 100644 src/api.js create mode 100644 src/data/sampleProducts.js diff --git a/README.md b/README.md index 58beeac..8e4a001 100644 --- a/README.md +++ b/README.md @@ -68,3 +68,21 @@ This section has moved here: [https://facebook.github.io/create-react-app/docs/d ### `npm run build` fails to minify This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) + +## Backend API (Node.js) + +The sensor catalog uses a lightweight Express API located in `server/`. + +```bash +cd server +npm install +npm start +``` + +The API runs on `http://localhost:4000` and exposes: + +- `GET /api/products` +- `GET /api/products/:id` +- `POST /api/products` +- `PUT /api/products/:id` +- `DELETE /api/products/:id` diff --git a/server/index.js b/server/index.js new file mode 100644 index 0000000..3ef2df5 --- /dev/null +++ b/server/index.js @@ -0,0 +1,172 @@ +const express = require("express"); +const cors = require("cors"); + +const app = express(); +const PORT = process.env.PORT || 4000; + +app.use(cors()); +app.use(express.json()); + +let products = [ + { + id: "SEN-1001", + title: "ThermoSense Pro Temperature Sensor", + category: "Temperature", + price: 129.0, + stock: 42, + status: "In Stock", + description: + "Industrial-grade temperature sensor with rapid response time and high accuracy for HVAC and manufacturing environments.", + image: + "https://images.unsplash.com/photo-1518770660439-4636190af475?auto=format&fit=crop&w=800&q=80", + specs: ["-40°C to 125°C", "±0.2°C accuracy", "IP67 sealed housing"], + }, + { + id: "SEN-1002", + title: "PulseGuard Vibration Sensor", + category: "Vibration", + price: 189.0, + stock: 31, + status: "In Stock", + description: + "Multi-axis vibration sensor for predictive maintenance with real-time alerts and analytics-ready output.", + image: + "https://images.unsplash.com/photo-1489515217757-5fd1be406fef?auto=format&fit=crop&w=800&q=80", + specs: ["3-axis monitoring", "4 kHz sampling", "Modbus-ready"], + }, + { + id: "SEN-1003", + title: "AeroFlow Pressure Sensor", + category: "Pressure", + price: 149.0, + stock: 18, + status: "Low Stock", + description: + "Compact pressure sensor designed for airflow systems with long-life calibration and rugged casing.", + image: + "https://images.unsplash.com/photo-1581090700227-1e37b190418e?auto=format&fit=crop&w=800&q=80", + specs: ["0-10 bar", "0.5% FS accuracy", "Stainless steel body"], + }, + { + id: "SEN-1004", + title: "LumaTrace Optical Sensor", + category: "Optical", + price: 99.0, + stock: 0, + status: "Out of Stock", + description: + "Optical sensor for line detection and quality control with adjustable sensitivity and smart filtering.", + image: + "https://images.unsplash.com/photo-1518779578993-ec3579fee39f?auto=format&fit=crop&w=800&q=80", + specs: ["0.1 mm resolution", "Auto-gain", "Class 2 laser"], + }, + { + id: "SEN-1005", + title: "HydroSense Humidity Sensor", + category: "Humidity", + price: 139.0, + stock: 27, + status: "In Stock", + description: + "Precision humidity sensor for cleanrooms and storage facilities with quick drift compensation.", + image: + "https://images.unsplash.com/photo-1519389950473-47ba0277781c?auto=format&fit=crop&w=800&q=80", + specs: ["0-100% RH", "±1.5% RH", "Digital I2C output"], + }, + { + id: "SEN-1006", + title: "GeoTrack Proximity Sensor", + category: "Proximity", + price: 159.0, + stock: 12, + status: "Low Stock", + description: + "Short-range proximity sensor ideal for automated sorting lines with configurable detection zones.", + image: + "https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?auto=format&fit=crop&w=800&q=80", + specs: ["0-50 mm range", "PNP/NPN output", "IP68 rated"], + }, +]; + +const findProduct = (id) => products.find((product) => product.id === id); + +app.get("/api/health", (req, res) => { + res.json({ status: "ok" }); +}); + +app.get("/api/products", (req, res) => { + res.json(products); +}); + +app.get("/api/products/:id", (req, res) => { + const product = findProduct(req.params.id); + if (!product) { + return res.status(404).json({ message: "Product not found" }); + } + return res.json(product); +}); + +app.post("/api/products", (req, res) => { + const { title, category, price, stock, description, image, specs, status } = + req.body; + + if (!title || !category) { + return res.status(400).json({ message: "Title and category are required." }); + } + + const newProduct = { + id: `SEN-${Date.now()}`, + title, + category, + price: Number(price) || 0, + stock: Number(stock) || 0, + status: status || (Number(stock) > 0 ? "In Stock" : "Out of Stock"), + description: description || "Custom sensor configuration.", + image: + image || + "https://images.unsplash.com/photo-1555661530-68c8e98db4e0?auto=format&fit=crop&w=800&q=80", + specs: Array.isArray(specs) ? specs : [], + }; + + products = [newProduct, ...products]; + return res.status(201).json(newProduct); +}); + +app.put("/api/products/:id", (req, res) => { + const { id } = req.params; + const product = findProduct(id); + if (!product) { + return res.status(404).json({ message: "Product not found" }); + } + + const updates = req.body; + const updatedProduct = { + ...product, + ...updates, + price: + updates.price === undefined ? product.price : Number(updates.price) || 0, + stock: + updates.stock === undefined ? product.stock : Number(updates.stock) || 0, + }; + if (!updates.status) { + updatedProduct.status = + updatedProduct.stock > 0 ? "In Stock" : "Out of Stock"; + } + + products = products.map((item) => (item.id === id ? updatedProduct : item)); + return res.json(updatedProduct); +}); + +app.delete("/api/products/:id", (req, res) => { + const { id } = req.params; + const product = findProduct(id); + if (!product) { + return res.status(404).json({ message: "Product not found" }); + } + products = products.filter((item) => item.id !== id); + return res.status(204).send(); +}); + +app.listen(PORT, () => { + console.log(`Sensor shop API running on port ${PORT}`); +}); diff --git a/server/package.json b/server/package.json new file mode 100644 index 0000000..2dd8d4c --- /dev/null +++ b/server/package.json @@ -0,0 +1,13 @@ +{ + "name": "sensor-shop-backend", + "version": "1.0.0", + "description": "Node.js API for the sensor ecommerce demo.", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "dependencies": { + "cors": "^2.8.5", + "express": "^4.18.2" + } +} diff --git a/src/App.css b/src/App.css index e69de29..cb0fc93 100644 --- a/src/App.css +++ b/src/App.css @@ -0,0 +1,498 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: "Inter", "Segoe UI", sans-serif; + background: #f4f6fb; + color: #14223b; +} + +a { + text-decoration: none; + color: inherit; +} + +.App { + min-height: 100vh; +} + +.site-header { + background: #0b1324; + color: #fff; + padding: 24px 32px; + position: sticky; + top: 0; + z-index: 10; +} + +.site-header__inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + max-width: 1200px; + margin: 0 auto; +} + +.brand { + display: flex; + align-items: center; + gap: 16px; +} + +.brand__mark { + width: 44px; + height: 44px; + border-radius: 14px; + background: #4ade80; + color: #0b1324; + display: grid; + place-items: center; + font-weight: 700; + font-size: 20px; +} + +.brand__title { + margin: 0; + font-weight: 700; + font-size: 20px; +} + +.brand__subtitle { + margin: 4px 0 0; + color: #c7d2fe; + font-size: 13px; +} + +.nav-links { + display: flex; + gap: 16px; +} + +.nav-link { + padding: 10px 16px; + border-radius: 20px; + color: #e0e7ff; + border: 1px solid transparent; + transition: all 0.2s ease; + font-size: 14px; +} + +.nav-link.active, +.nav-link:hover { + border-color: rgba(255, 255, 255, 0.35); + background: rgba(255, 255, 255, 0.08); +} + +.page { + max-width: 1200px; + margin: 0 auto; + padding: 32px; +} + +.hero { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 32px; + margin-bottom: 40px; +} + +.hero__content h1 { + font-size: 36px; + margin: 16px 0; +} + +.hero__subtitle { + color: #4b5563; + line-height: 1.6; +} + +.pill { + display: inline-flex; + padding: 6px 14px; + border-radius: 999px; + background: #e0f2fe; + color: #0369a1; + font-weight: 600; + font-size: 12px; +} + +.hero__actions { + display: flex; + gap: 12px; + margin-top: 20px; + flex-wrap: wrap; +} + +.hero__stats { + display: flex; + gap: 24px; + margin-top: 28px; + flex-wrap: wrap; +} + +.hero__stats h3 { + margin: 0; + font-size: 20px; +} + +.hero__stats p { + margin: 4px 0 0; + color: #6b7280; + font-size: 13px; +} + +.hero__card { + background: #fff; + border-radius: 24px; + padding: 28px; + box-shadow: 0 18px 45px rgba(15, 23, 42, 0.08); +} + +.hero__card ul { + padding-left: 18px; + color: #475569; +} + +.primary-button, +.secondary-button, +.ghost-button { + border: none; + border-radius: 999px; + padding: 12px 22px; + font-weight: 600; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.primary-button { + background: #2563eb; + color: #fff; + box-shadow: 0 12px 24px rgba(37, 99, 235, 0.25); +} + +.secondary-button { + background: #0ea5e9; + color: #fff; +} + +.ghost-button { + background: transparent; + border: 1px solid #cbd5f5; + color: #1e3a8a; +} + +.primary-button:hover, +.secondary-button:hover, +.ghost-button:hover { + transform: translateY(-1px); +} + +.inventory__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + flex-wrap: wrap; +} + +.section-label { + text-transform: uppercase; + font-size: 12px; + letter-spacing: 0.12em; + color: #94a3b8; + margin: 0 0 8px; +} + +.inventory__note { + background: #fff; + padding: 12px 18px; + border-radius: 16px; + font-size: 13px; + color: #475569; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08); +} + +.inventory__note span { + color: #1d4ed8; + font-weight: 600; +} + +.inventory__grid { + margin-top: 24px; + display: grid; + grid-template-columns: repeat(auto-fit, minmax(240px, 1fr)); + gap: 24px; +} + +.product-card { + background: #fff; + border-radius: 20px; + overflow: hidden; + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); + display: flex; + flex-direction: column; +} + +.product-card__image img { + width: 100%; + height: 200px; + object-fit: cover; +} + +.product-card__body { + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; +} + +.product-card__category { + color: #64748b; + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.12em; + margin: 0; +} + +.product-card__price { + font-size: 18px; + font-weight: 700; +} + +.product-card__meta { + display: flex; + flex-wrap: wrap; + gap: 10px; + color: #475569; + font-size: 12px; +} + +.product-card__cta { + margin-top: auto; + text-align: center; +} + +.tag { + background: #e2e8f0; + padding: 4px 10px; + border-radius: 999px; + font-size: 12px; +} + +.muted { + color: #64748b; +} + +.empty-state, +.loading { + background: #fff; + padding: 24px; + border-radius: 16px; + text-align: center; + color: #475569; + box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08); +} + +.details { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); + gap: 32px; + background: #fff; + border-radius: 24px; + padding: 32px; + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); +} + +.details__image img { + width: 100%; + border-radius: 16px; + object-fit: cover; +} + +.details__content h1 { + margin: 12px 0; +} + +.details__meta { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.details__price { + font-size: 24px; + font-weight: 700; +} + +.details__description { + color: #475569; + line-height: 1.6; +} + +.details__specs ul { + padding-left: 18px; + color: #475569; +} + +.details__actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 12px; +} + +.details__message { + margin-top: 16px; + color: #1d4ed8; + font-weight: 600; +} + +.breadcrumb { + color: #2563eb; + font-size: 13px; +} + +.admin { + display: flex; + flex-direction: column; + gap: 32px; +} + +.admin__hero { + display: flex; + justify-content: space-between; + gap: 24px; + flex-wrap: wrap; +} + +.admin__status { + display: flex; + gap: 18px; +} + +.admin__status div { + background: #fff; + padding: 16px 20px; + border-radius: 16px; + box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08); + text-align: center; +} + +.admin__grid { + display: grid; + grid-template-columns: minmax(280px, 1fr) 2fr; + gap: 24px; +} + +.admin__form, +.admin__table { + background: #fff; + padding: 24px; + border-radius: 20px; + box-shadow: 0 16px 40px rgba(15, 23, 42, 0.08); +} + +.admin__form label { + display: flex; + flex-direction: column; + gap: 6px; + margin-bottom: 14px; + font-size: 13px; + color: #475569; +} + +.admin__form input, +.admin__form textarea { + padding: 10px 12px; + border-radius: 12px; + border: 1px solid #e2e8f0; + font-size: 14px; +} + +.admin__split { + display: grid; + grid-template-columns: repeat(2, minmax(120px, 1fr)); + gap: 12px; +} + +.admin__actions { + display: flex; + gap: 12px; + flex-wrap: wrap; + margin-top: 8px; +} + +.form-message { + color: #1d4ed8; + font-weight: 600; +} + +.table { + display: grid; + gap: 10px; +} + +.table__head, +.table__row { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr 1fr; + gap: 12px; + align-items: center; +} + +.table__head { + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.1em; + color: #94a3b8; +} + +.table__row { + background: #f8fafc; + padding: 12px; + border-radius: 12px; + font-size: 13px; +} + +.table__row small { + display: block; + color: #94a3b8; + font-size: 11px; + margin-top: 4px; +} + +.table__actions { + display: flex; + gap: 8px; +} + +.link-button { + background: transparent; + border: none; + color: #2563eb; + cursor: pointer; + font-weight: 600; +} + +.link-button.danger { + color: #dc2626; +} + +@media (max-width: 980px) { + .admin__grid { + grid-template-columns: 1fr; + } + + .table__head { + display: none; + } + + .table__row { + grid-template-columns: 1fr; + gap: 6px; + } +} diff --git a/src/App.js b/src/App.js index 37846af..ef40d7e 100644 --- a/src/App.js +++ b/src/App.js @@ -1,24 +1,26 @@ -import React from 'react'; -import { BrowserRouter as Router, Route,Routes } from "react-router-dom"; -import Header from './Containers/Header'; -import ProductListing from './Containers/productListing'; -import ProdectDetails from './Containers/prodectDetails'; +import React from "react"; +import { BrowserRouter as Router, Route, Routes } from "react-router-dom"; +import Header from "./Containers/Header"; +import ProductListing from "./Containers/productListing"; +import ProdectDetails from "./Containers/prodectDetails"; import "./App.css"; -import PageNotFount from './Containers/PageNotFount'; +import PageNotFount from "./Containers/PageNotFount"; +import AdminPanel from "./Containers/AdminPanel"; -const App =()=> { +const App = () => { return (
- -
- - } /> - } /> - } /> - - + +
+ + } /> + } /> + } /> + } /> + +
); -} +}; export default App; diff --git a/src/Containers/AdminPanel.jsx b/src/Containers/AdminPanel.jsx new file mode 100644 index 0000000..5ca1337 --- /dev/null +++ b/src/Containers/AdminPanel.jsx @@ -0,0 +1,278 @@ +import React, { useEffect, useState } from "react"; +import { api } from "../api"; +import { sampleProducts } from "../data/sampleProducts"; + +const defaultFormState = { + title: "", + category: "", + price: "", + stock: "", + status: "", + description: "", + image: "", + specs: "", +}; + +const AdminPanel = () => { + const [products, setProducts] = useState([]); + const [formState, setFormState] = useState(defaultFormState); + const [editingId, setEditingId] = useState(""); + const [message, setMessage] = useState(""); + const [loading, setLoading] = useState(false); + + const fetchProducts = async () => { + try { + const response = await api.get("/products"); + setProducts(response.data); + } catch (err) { + setProducts(sampleProducts); + setMessage( + "Unable to reach the API. Showing sample inventory data instead." + ); + } + }; + + useEffect(() => { + fetchProducts(); + }, []); + + const handleChange = (event) => { + const { name, value } = event.target; + setFormState((prev) => ({ ...prev, [name]: value })); + }; + + const resetForm = () => { + setFormState(defaultFormState); + setEditingId(""); + }; + + const handleEdit = (product) => { + setEditingId(product.id); + setFormState({ + title: product.title, + category: product.category, + price: product.price, + stock: product.stock, + status: product.status, + description: product.description, + image: product.image, + specs: (product.specs || []).join(", "), + }); + setMessage(""); + }; + + const handleDelete = async (id) => { + setLoading(true); + try { + await api.delete(`/products/${id}`); + setMessage("Sensor removed from inventory."); + resetForm(); + fetchProducts(); + } catch (err) { + setMessage("Delete failed. Please try again."); + } finally { + setLoading(false); + } + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + setLoading(true); + setMessage(""); + + const payload = { + ...formState, + price: Number(formState.price), + stock: Number(formState.stock), + specs: formState.specs + ? formState.specs.split(",").map((item) => item.trim()) + : [], + }; + + try { + if (editingId) { + await api.put(`/products/${editingId}`, payload); + setMessage("Inventory updated."); + } else { + await api.post("/products", payload); + setMessage("New sensor added."); + } + resetForm(); + fetchProducts(); + } catch (err) { + setMessage("Save failed. Confirm required fields are filled."); + } finally { + setLoading(false); + } + }; + + return ( +
+
+
+

Admin Panel

+

Inventory management

+

+ Add new sensors, adjust stock levels, and track ready-to-ship + inventory in one place. +

+
+
+
+

{products.length}

+

Active SKUs

+
+
+

+ {products.reduce((total, product) => total + product.stock, 0)} +

+

Total units

+
+
+
+ +
+
+

{editingId ? "Edit Sensor" : "Add New Sensor"}

+ + +
+ + +
+ + + +