diff --git a/.env b/.env
index 0ea2b3b..197c09c 100644
--- a/.env
+++ b/.env
@@ -14,3 +14,6 @@ DATABASE_URL="postgresql://partpilot:partpilotPass@localhost:5432/partpilotdb?sc
# openssl rand -base64 32
NEXTAUTH_SECRET="K3l7o+g1mCZUQjszYk6SH0k66mmYeX1gh1ANqJA6/9o=" # for local
NEXTAUTH_URL="http://localhost:3000" # for local
+
+# Autocomplete
+# MOUSER_API_KEY="your_mouser_api_key_here"
\ No newline at end of file
diff --git a/README.md b/README.md
index 6de8956..774e285 100644
--- a/README.md
+++ b/README.md
@@ -49,7 +49,7 @@ Welcome to PartPilot, the ultimate open-source solution designed to streamline a
- 🏬 **Inventory Management**: Effortlessly catalog your electronic parts with detailed information, including datasheets, supplier data, stock levels, and more.
-- 🖥️ **Direct LCSC Integration**: Seamlessly connect with LCSC for direct access to a vast inventory of parts, enabling easy addition and management of components within PartPilot.
+- 🖥️ **Direct LCSC & Mouser Integration**: Seamlessly connect with LCSC or Mouser for direct access to a vast inventory of parts, enabling easy addition and management of components within PartPilot.
- 👁️ **Barcode Scanner Functionality**: Add parts to your inventory swiftly using the barcode scanner feature, enhancing efficiency and accuracy in part management.
@@ -83,6 +83,11 @@ To host PartPilot on your homeserver using docker-compose:
copy the contents of the `docker-compose-release.yml` into a `docker-compose.yml` file on your server
start the service using `docker-compose up -d` or `docker compose up -d`.
+### ⚙️ Configuration
+
+To enable the Mouser search functionality, you need to provide a Mouser Search API key.
+Add the `MOUSER_API_KEY` environment variable to your deployment (e.g., in `docker-compose.yml` or `.env` file).
+
## 👨💻 Development
diff --git a/app/add/page.tsx b/app/add/page.tsx
index bb318d8..aa29f4e 100644
--- a/app/add/page.tsx
+++ b/app/add/page.tsx
@@ -18,6 +18,7 @@ import {
Image,
ThemeIcon,
LoadingOverlay,
+ Select,
} from "@mantine/core";
import { useEffect, useRef, useState } from "react";
import { scannerInputToType } from "../dashboardPage";
@@ -67,6 +68,17 @@ export default function Add() {
const [scannerInput, setScannerInput] = useState("");
const [productCode, setProductCode] = useState("");
+ const [provider, setProvider] = useState("LCSC");
+ const [availableProviders, setAvailableProviders] = useState(["LCSC"]);
+
+ useEffect(() => {
+ fetch("/api/providers")
+ .then((res) => res.json())
+ .then((data) => {
+ setAvailableProviders(data);
+ })
+ .catch((e) => console.error(e));
+ }, []);
//When the user presses the Autocomplete Button
async function handleAutocomplete() {
@@ -88,6 +100,7 @@ export default function Add() {
method: "POST",
body: JSON.stringify({
productCode: productCodeInternal,
+ provider: provider,
}),
}).then((response) =>
response
@@ -290,13 +303,22 @@ export default function Add() {
Autocomplete:
-
+
+
+
+
{
+ const apiKey = process.env.MOUSER_API_KEY;
+
+ if (!apiKey) {
+ throw new Error("MOUSER_API_KEY environment variable is not set");
+ }
+
+ const response = await fetch(
+ `https://api.mouser.com/api/v1/search/partnumber?apiKey=${apiKey}`,
+ {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ SearchByPartRequest: {
+ mouserPartNumber: partNumber,
+ partSearchOptions: "Exact",
+ },
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ throw new Error(`Mouser API error: ${response.statusText}`);
+ }
+
+ const data = await response.json();
+ const parts = data.SearchResults?.Parts;
+
+ if (!parts || parts.length === 0) {
+ return null;
+ }
+
+ // Take the first result
+ const result = parts[0];
+ return mapMouserResponseToPartState(result);
+}
+
+// Helper to convert to specific units if possible, otherwise base
+function parseValue(value: string, targetUnit?: string): number | undefined {
+ if (!value) return undefined;
+ try {
+ const u = unit(value);
+ if (targetUnit) {
+ return u.toNumber(targetUnit);
+ }
+ return u.toNumber(); // Base unit
+ } catch (e) {
+ const num = parseFloat(value);
+ return isNaN(num) ? undefined : num;
+ }
+}
+
+function mapMouserResponseToPartState(result: any): PartState {
+ const attributes = result.ProductAttributes || [];
+ const getAttr = (name: string) =>
+ attributes.find((a: any) => a.AttributeName === name)?.AttributeValue;
+
+ return {
+ id: 0, // Placeholder, generated by DB
+ title: result.Description,
+ quantity: result.Availability ? parseInt(result.Availability.replace(/[^0-9]/g, "")) : 0,
+ productId: 0, // Mouser does not provide a numeric ProductId that maps to our schema.
+ productCode: result.MouserPartNumber,
+ productModel: result.ManufacturerPartNumber,
+ productDescription: result.Description,
+ catalogName: "Electronic Components",
+ brandName: result.Manufacturer,
+ productImages: result.ImagePath ? [result.ImagePath] : [],
+ pdfLink: result.DataSheetUrl,
+ productLink: result.ProductDetailUrl,
+ createdAt: new Date(),
+ updatedAt: new Date(),
+
+ // Parse technical parameters
+ voltage: parseValue(getAttr("Voltage Rating"), "V"),
+ resistance: parseValue(getAttr("Resistance"), "ohm"),
+ power: parseValue(getAttr("Power Rating"), "W"),
+ current: parseValue(getAttr("Current Rating"), "A"),
+ tolerance: getAttr("Tolerance"),
+ frequency: parseValue(getAttr("Frequency"), "Hz"),
+ capacitance: parseValue(getAttr("Capacitance"), "nF"),
+ inductance: parseValue(getAttr("Inductance"), "uH"),
+
+ prices: result.PriceBreaks?.map((pb: any) => ({
+ ladder: pb.Quantity.toString(),
+ price: parseFloat(pb.Price.replace(',', '.').replace(/[^0-9.]/g, "")),
+ })),
+ };
+}