├── web ├── src │ ├── corsBridge.js │ ├── tailwind.css │ ├── index.js │ ├── units.js │ ├── jlc.js │ ├── serviceWorker.js │ ├── history.js │ ├── sortableTable.js │ ├── app.js │ ├── db.js │ └── componentTable.js ├── public │ ├── favicon.ico │ ├── robots.txt │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── mstile-70x70.png │ ├── apple-touch-icon.png │ ├── mstile-144x144.png │ ├── mstile-150x150.png │ ├── mstile-310x150.png │ ├── mstile-310x310.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── browserconfig.xml │ ├── site.webmanifest │ ├── safari-pinned-tab.svg │ ├── brokenimage.svg │ ├── index.html │ └── favicon.svg ├── .modernizrrc ├── postcss.config.js ├── .gitignore ├── .vscode │ └── launch.json ├── tailwind.js ├── package.json └── README.md ├── jlcparts ├── common.py ├── migrate.py ├── descriptionAttributes.py ├── lcsc.py ├── jlcpcb.py ├── ui.py ├── datatables.py ├── partLib.py └── attributes.py ├── LCSC-API.md ├── .github ├── FUNDING.yml └── workflows │ └── update_components.yaml ├── setup.py ├── LICENSE ├── .gitignore ├── README.md └── test └── testParts.csv /web/src/corsBridge.js: -------------------------------------------------------------------------------- 1 | 2 | export let CORS_KEY = "94b8549f-2d17-40b3-b5e1-72f90bc99206"; -------------------------------------------------------------------------------- /web/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/favicon.ico -------------------------------------------------------------------------------- /web/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /web/src/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /web/public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/favicon-16x16.png -------------------------------------------------------------------------------- /web/public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/favicon-32x32.png -------------------------------------------------------------------------------- /web/public/mstile-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/mstile-70x70.png -------------------------------------------------------------------------------- /web/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/apple-touch-icon.png -------------------------------------------------------------------------------- /web/public/mstile-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/mstile-144x144.png -------------------------------------------------------------------------------- /web/public/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/mstile-150x150.png -------------------------------------------------------------------------------- /web/public/mstile-310x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/mstile-310x150.png -------------------------------------------------------------------------------- /web/public/mstile-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/mstile-310x310.png -------------------------------------------------------------------------------- /web/public/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/android-chrome-192x192.png -------------------------------------------------------------------------------- /web/public/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yaqwsx/jlcparts/HEAD/web/public/android-chrome-512x512.png -------------------------------------------------------------------------------- /web/.modernizrrc: -------------------------------------------------------------------------------- 1 | { 2 | "minify": true, 3 | "options": [ 4 | "setClasses" 5 | ], 6 | "feature-detects": [ 7 | "test/indexeddb" 8 | ] 9 | } -------------------------------------------------------------------------------- /web/postcss.config.js: -------------------------------------------------------------------------------- 1 | const tailwindcss = require('tailwindcss'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | tailwindcss('./tailwind.js'), 6 | require('autoprefixer') 7 | ], 8 | }; -------------------------------------------------------------------------------- /jlcparts/common.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | 3 | def sha256file(filename): 4 | sha256_hash = hashlib.sha256() 5 | with open(filename, "rb") as f: 6 | for byte_block in iter(lambda: f.read(4096), b""): 7 | sha256_hash.update(byte_block) 8 | return sha256_hash.hexdigest() 9 | -------------------------------------------------------------------------------- /web/public/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #da532c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | # Generated css 26 | src/main.css 27 | 28 | # Data 29 | public/data 30 | -------------------------------------------------------------------------------- /web/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "", 3 | "short_name": "", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /web/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:3000", 12 | "webRoot": "${workspaceFolder}" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /web/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './app'; 4 | import * as serviceWorker from './serviceWorker'; 5 | 6 | ReactDOM.render( 7 | 8 | 9 | , 10 | document.getElementById('root') 11 | ); 12 | 13 | // If you want your app to work offline and load faster, you can change 14 | // unregister() to register() below. Note this comes with some pitfalls. 15 | // Learn more about service workers: https://bit.ly/CRA-PWA 16 | serviceWorker.unregister(); 17 | -------------------------------------------------------------------------------- /LCSC-API.md: -------------------------------------------------------------------------------- 1 | # LCSC API 2 | 3 | - to get a product page based on LCSC number use GET request on 4 | `https://lcsc.com/api/global/additional/search?q=`. You get a 5 | JSON with URL of the product. 6 | - to get a product options based on LSCS number use POST request to 7 | `https://lcsc.com/api/products/search` with data 8 | `current_page=1&in_stock=false&is_RoHS=false&show_icon=false&search_content=` 9 | You have to include CSRF token and cookies. Both you can get from the category 10 | page (e.g., `https://lcsc.com/products/Pre-ordered-Products_11171.html`) -------------------------------------------------------------------------------- /jlcparts/migrate.py: -------------------------------------------------------------------------------- 1 | import click 2 | from .partLib import PartLibrary, PartLibraryDb 3 | 4 | 5 | @click.command() 6 | @click.argument("input") 7 | @click.argument("output") 8 | def migrate_to_db(input, output): 9 | pLib = PartLibrary(input) 10 | dbLib = PartLibraryDb(output) 11 | 12 | with dbLib.startTransaction(): 13 | l = len(pLib.index) 14 | for i, id in enumerate(pLib.index.keys()): 15 | c = pLib.getComponent(id) 16 | if i % 1000 == 0: 17 | print(f"{((i+1) / l * 100):.2f} %") 18 | dbLib.addComponent(c) 19 | 20 | 21 | if __name__ == "__main__": 22 | migrate_to_db() 23 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: yaqwsx 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: yaqwsx 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] -------------------------------------------------------------------------------- /jlcparts/descriptionAttributes.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | def chipResistor(description): 4 | attrs = {} 5 | 6 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?Ohms", description) 7 | if matches is not None: 8 | attrs["Resistance"] = matches.group(0) 9 | 10 | matches = re.search(r"±\d+(\.\d+)?%", description) 11 | if matches is not None: 12 | attrs["Tolerance"] = matches.group(0) 13 | 14 | matches = re.search(r"((\d+/\d+)|(\d+(.\d+)?[a-zA-Z]?))W", description) 15 | if matches is not None: 16 | attrs["Power"] = matches.group(0) 17 | 18 | return attrs 19 | 20 | def capacitor(description): 21 | attrs = {} 22 | 23 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?F", description) 24 | if matches is not None: 25 | attrs["Capacitance"] = matches.group(0) 26 | 27 | matches = re.search(r"\d+(\.\d+)?[a-zA-Z]?V", description) 28 | if matches is not None: 29 | attrs["Voltage - Rated"] = matches.group(0) 30 | 31 | return attrs 32 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import setuptools 4 | 5 | 6 | with open("README.md", "r") as fh: 7 | long_description = fh.read() 8 | 9 | setuptools.setup( 10 | name="jlcparts", 11 | version="0.1.0", 12 | author="Jan Mrázek", 13 | author_email="email@honzamrazek.cz", 14 | description="Better view of JLC PCB parts", 15 | long_description=long_description, 16 | long_description_content_type="text/markdown", 17 | url="https://github.com/RoboticsBrno/JLCPCB-Parts", 18 | packages=setuptools.find_packages(), 19 | classifiers=[ 20 | "Programming Language :: Python :: 3", 21 | "License :: OSI Approved :: MIT License", 22 | "Operating System :: OS Independent", 23 | ], 24 | install_requires=[ 25 | "requests", 26 | "click", 27 | "lxml" 28 | ], 29 | setup_requires=[ 30 | 31 | ], 32 | zip_safe=False, 33 | include_package_data=True, 34 | entry_points = { 35 | "console_scripts": [ 36 | "jlcparts=jlcparts.ui:cli" 37 | ], 38 | } 39 | ) -------------------------------------------------------------------------------- /web/tailwind.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: [ 3 | "./src/**/*.js", 4 | "./public/**.html" 5 | ], 6 | target: 'relaxed', 7 | prefix: '', 8 | important: false, 9 | separator: ':', 10 | theme: { 11 | fontFamily: { 12 | 'sans': ['Lato', 'Arial', 'sans-serif'], 13 | }, 14 | container: { 15 | screens: { 16 | sm: "100%", 17 | md: "100%", 18 | lg: "100%", 19 | xl: "1600px" 20 | } 21 | }, 22 | minWidth: { 23 | '0': '0', 24 | '200': '200pt', 25 | 'full': '100%', 26 | } 27 | }, 28 | variants: { 29 | backgroundColor: ['responsive', 'odd', 'even', 'hover', 'focus'], 30 | overflow: ['responsive', 'hover', 'focus'], 31 | zIndex: ['responsive', 'hover', 'focus'], 32 | position: ['responsive', 'hover', 'focus'], 33 | wordBreak: ['responsive', 'hover', 'focus'], 34 | whitespace: ['responsive', 'hover', 'focus'], 35 | }, 36 | future: { 37 | removeDeprecatedGapUtilities: true, 38 | purgeLayersByDefault: true 39 | }, 40 | corePlugins: {}, 41 | plugins: [], 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024 Jan Mrázek 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /web/public/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jlcparts", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@discoveryjs/natural-compare": "^1.0.0", 7 | "@fortawesome/fontawesome-svg-core": "^1.2.30", 8 | "@fortawesome/free-brands-svg-icons": "^5.14.0", 9 | "@fortawesome/free-regular-svg-icons": "^5.14.0", 10 | "@fortawesome/free-solid-svg-icons": "^5.14.0", 11 | "@fortawesome/react-fontawesome": "^0.1.11", 12 | "@testing-library/jest-dom": "^4.2.4", 13 | "@testing-library/react": "^9.5.0", 14 | "@testing-library/user-event": "^7.2.1", 15 | "dexie": "^3.0.2", 16 | "immer": "^7.0.8", 17 | "pako": "^2.0.4", 18 | "react": "^16.13.1", 19 | "react-copy-to-clipboard": "^5.0.2", 20 | "react-dom": "^16.13.1", 21 | "react-lazy-load-image-component": "^1.5.0", 22 | "react-router-dom": "^5.2.0", 23 | "react-scripts": "^5.0.1", 24 | "react-scroll": "^1.8.1", 25 | "react-waypoint": "^9.0.3" 26 | }, 27 | "scripts": { 28 | "start": "npm run watch:css && react-scripts start", 29 | "build": "npm run build:css && react-scripts build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject", 32 | "build:css": "postcss src/tailwind.css -o src/main.css", 33 | "watch:css": "postcss src/tailwind.css -o src/main.css" 34 | }, 35 | "eslintConfig": { 36 | "extends": "react-app" 37 | }, 38 | "browserslist": { 39 | "production": [ 40 | ">0.2%", 41 | "not dead", 42 | "not op_mini all" 43 | ], 44 | "development": [ 45 | "last 1 chrome version", 46 | "last 1 firefox version", 47 | "last 1 safari version" 48 | ] 49 | }, 50 | "homepage": ".", 51 | "devDependencies": { 52 | "autoprefixer": "^9.8.6", 53 | "postcss-cli": "^9.1.0", 54 | "postcss-flexbugs-fixes": "^5.0.2", 55 | "postcss-normalize": "^10.0.1", 56 | "postcss-preset-env": "^7.7.1", 57 | "tailwindcss": "^1.8.4" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /jlcparts/lcsc.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | import os 4 | import time 5 | import random 6 | import string 7 | import urllib 8 | import hashlib 9 | from requests.exceptions import ConnectionError 10 | 11 | LCSC_KEY = os.environ.get("LCSC_KEY") 12 | LCSC_SECRET = os.environ.get("LCSC_SECRET") 13 | 14 | def makeLcscRequest(url, payload=None): 15 | if payload is None: 16 | payload = {} 17 | payload = [(key, value) for key, value in payload.items()] 18 | payload.sort(key=lambda x: x[0]) 19 | newPayload = { 20 | "key": LCSC_KEY, 21 | "nonce": "".join(random.choices(string.ascii_lowercase, k=16)), 22 | "secret": LCSC_SECRET, 23 | "timestamp": str(int(time.time())), 24 | } 25 | for k, v in payload: 26 | newPayload[k] = v 27 | payloadStr = urllib.parse.urlencode(newPayload).encode("utf-8") 28 | newPayload["signature"] = hashlib.sha1(payloadStr).hexdigest() 29 | 30 | return requests.get(url, params=newPayload) 31 | 32 | def pullPreferredComponents(): 33 | resp = requests.get("https://jlcpcb.com/api/overseas-pcb-order/v1/getAll") 34 | token = resp.cookies.get_dict()["XSRF-TOKEN"] 35 | 36 | headers = { 37 | "Content-Type": "application/json", 38 | "X-XSRF-TOKEN": token, 39 | } 40 | PAGE_SIZE = 1000 41 | 42 | currentPage = 1 43 | components = set() 44 | while True: 45 | body = { 46 | "currentPage": currentPage, 47 | "pageSize": PAGE_SIZE, 48 | "preferredComponentFlag": True 49 | } 50 | 51 | resp = requests.post( 52 | "https://jlcpcb.com/api/overseas-pcb-order/v1/shoppingCart/smtGood/selectSmtComponentList", 53 | headers=headers, 54 | json=body 55 | ) 56 | 57 | body = resp.json() 58 | for c in [x["componentCode"] for x in body["data"]["componentPageInfo"]["list"]]: 59 | components.add(c) 60 | 61 | if not body["data"]["componentPageInfo"]["hasNextPage"]: 62 | break 63 | currentPage += 1 64 | 65 | return components 66 | 67 | if __name__ == "__main__": 68 | r = makeLcscRequest("https://ips.lcsc.com/rest/wmsc2agent/product/info/C7063") 69 | print(r.json()) 70 | 71 | -------------------------------------------------------------------------------- /web/public/brokenimage.svg: -------------------------------------------------------------------------------- 1 | Created by Ryan Beckfrom the Noun Project -------------------------------------------------------------------------------- /web/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 38 | JLC Component Catalogue 39 | 40 | 41 | 42 |
43 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.sqlite* 2 | .idea 3 | *.zip 4 | *.z* 5 | 6 | # Byte-compiled / optimized / DLL files 7 | __pycache__/ 8 | *.py[cod] 9 | *$py.class 10 | 11 | # C extensions 12 | *.so 13 | 14 | # Distribution / packaging 15 | .Python 16 | build/ 17 | develop-eggs/ 18 | dist/ 19 | downloads/ 20 | eggs/ 21 | .eggs/ 22 | lib/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | share/python-wheels/ 29 | *.egg-info/ 30 | .installed.cfg 31 | *.egg 32 | MANIFEST 33 | 34 | # PyInstaller 35 | # Usually these files are written by a python script from a template 36 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 37 | *.manifest 38 | *.spec 39 | 40 | # Installer logs 41 | pip-log.txt 42 | pip-delete-this-directory.txt 43 | 44 | # Unit test / coverage reports 45 | htmlcov/ 46 | .tox/ 47 | .nox/ 48 | .coverage 49 | .coverage.* 50 | .cache 51 | nosetests.xml 52 | coverage.xml 53 | *.cover 54 | *.py,cover 55 | .hypothesis/ 56 | .pytest_cache/ 57 | cover/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | .pybuilder/ 81 | target/ 82 | 83 | # Jupyter Notebook 84 | .ipynb_checkpoints 85 | 86 | # IPython 87 | profile_default/ 88 | ipython_config.py 89 | 90 | # pyenv 91 | # For a library or package, you might want to ignore these files since the code is 92 | # intended to run in multiple environments; otherwise, check them in: 93 | # .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # Cython debug symbols 143 | cython_debug/ 144 | 145 | # Ignore sandbox 146 | sandbox 147 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | ## Available Scripts 4 | 5 | In the project directory, you can run: 6 | 7 | ### `npm start` 8 | 9 | Runs the app in the development mode.
10 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 11 | 12 | The page will reload if you make edits.
13 | You will also see any lint errors in the console. 14 | 15 | ### `npm test` 16 | 17 | Launches the test runner in the interactive watch mode.
18 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 19 | 20 | ### `npm run build` 21 | 22 | Builds the app for production to the `build` folder.
23 | It correctly bundles React in production mode and optimizes the build for the best performance. 24 | 25 | The build is minified and the filenames include the hashes.
26 | Your app is ready to be deployed! 27 | 28 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 29 | 30 | ### `npm run eject` 31 | 32 | **Note: this is a one-way operation. Once you `eject`, you can’t go back!** 33 | 34 | If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 35 | 36 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own. 37 | 38 | You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it. 39 | 40 | ## Learn More 41 | 42 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 43 | 44 | To learn React, check out the [React documentation](https://reactjs.org/). 45 | 46 | ### Code Splitting 47 | 48 | This section has moved here: https://facebook.github.io/create-react-app/docs/code-splitting 49 | 50 | ### Analyzing the Bundle Size 51 | 52 | This section has moved here: https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size 53 | 54 | ### Making a Progressive Web App 55 | 56 | This section has moved here: https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app 57 | 58 | ### Advanced Configuration 59 | 60 | This section has moved here: https://facebook.github.io/create-react-app/docs/advanced-configuration 61 | 62 | ### Deployment 63 | 64 | This section has moved here: https://facebook.github.io/create-react-app/docs/deployment 65 | 66 | ### `npm run build` fails to minify 67 | 68 | This section has moved here: https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify 69 | -------------------------------------------------------------------------------- /jlcparts/jlcpcb.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import csv 4 | import time 5 | from typing import Optional, List, Any, Callable 6 | 7 | JLCPCB_KEY = os.environ.get("JLCPCB_KEY") 8 | JLCPCB_SECRET = os.environ.get("JLCPCB_SECRET") 9 | 10 | class JlcPcbInterface: 11 | def __init__(self, key: str, secret: str) -> None: 12 | self.key = key 13 | self.secret = secret 14 | self.token = None 15 | self.lastPage = None 16 | 17 | def _obtainToken(self) -> None: 18 | body = { 19 | "appKey": self.key, 20 | "appSecret": self.secret 21 | } 22 | headers = { 23 | "Content-Type": "application/json", 24 | } 25 | resp = requests.post("https://jlcpcb.com/external/genToken", 26 | json=body, headers=headers) 27 | if resp.status_code != 200: 28 | raise RuntimeError(f"Cannot obtain token {resp.json()}") 29 | data = resp.json() 30 | if data["code"] != 200: 31 | raise RuntimeError(f"Cannot obtain toke {data}") 32 | self.token = data["data"] 33 | 34 | def getPage(self) -> Optional[List[Any]]: 35 | if self.token is None: 36 | self._obtainToken() 37 | headers = { 38 | "externalApiToken": self.token, 39 | } 40 | if self.lastPage is None: 41 | body = {} 42 | else: 43 | body = { 44 | "lastKey": self.lastPage 45 | } 46 | resp = requests.post("https://jlcpcb.com/external/component/getComponentInfos", 47 | data=body, headers=headers) 48 | try: 49 | data = resp.json()["data"] 50 | except: 51 | raise RuntimeError(f"Cannot fetch page: {resp.text}") 52 | self.lastPage = data["lastKey"] 53 | return data["componentInfos"] 54 | 55 | def dummyReporter(progress) -> None: 56 | return 57 | 58 | def pullComponentTable(filename: str, reporter: Callable[[int], None] = dummyReporter, 59 | retries: int = 10, retryDelay: int = 5) -> None: 60 | interf = JlcPcbInterface(JLCPCB_KEY, JLCPCB_SECRET) 61 | with open(filename, "w", encoding="utf-8") as f: 62 | writer = csv.writer(f) 63 | writer.writerow([ 64 | "LCSC Part", 65 | "First Category", 66 | "Second Category", 67 | "MFR.Part", 68 | "Package", 69 | "Solder Joint", 70 | "Manufacturer", 71 | "Library Type", 72 | "Description", 73 | "Datasheet", 74 | "Stock", 75 | "Price" 76 | ]) 77 | count = 0 78 | while True: 79 | for i in range(retries): 80 | try: 81 | page = interf.getPage() 82 | break 83 | except Exception as e: 84 | if i == retries - 1: 85 | raise e from None 86 | time.sleep(retryDelay) 87 | if page is None: 88 | break 89 | for c in page: 90 | writer.writerow([ 91 | c["lcscPart"], 92 | c["firstCategory"], 93 | c["secondCategory"], 94 | c["mfrPart"], 95 | c["package"], 96 | c["solderJoint"], 97 | c["manufacturer"], 98 | c["libraryType"], 99 | c["description"], 100 | c["datasheet"], 101 | c["stock"], 102 | c["price"] 103 | ]) 104 | count += len(page) 105 | reporter(count) 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Logo](web/public/favicon.svg) 2 | 3 | # JLC PCB SMD Assembly Component Catalogue 4 | 5 | A better tool to browse the components offered by the [JLC PCB SMT Assembly 6 | Service](https://jlcpcb.com/smt-assembly). 7 | 8 | ## How To Use It? 9 | 10 | Just visit: [https://yaqwsx.github.io/jlcparts/](https://yaqwsx.github.io/jlcparts/) 11 | 12 | ## Why? 13 | 14 | Probably all of us love JLC PCB SMT assembly service. It is easy to use, cheap 15 | and fast. However, you can use only components from [their 16 | catalogue](https://jlcpcb.com/parts). This is not as bad, since the library is 17 | quite broad. However, the library UI sucks. You can only browse the categories, 18 | do full-text search. You cannot do parametric search nor sort by property. 19 | That's why I created a simple page which presents the catalogue in much nicer 20 | form. You can: 21 | - do full-text search 22 | - browse categories 23 | - parametric search 24 | - sort by any component attribute 25 | - sort by price based on quantity 26 | - easily access datasheet and LCSC product page. 27 | 28 | ## Do You Enjoy It? Does It Make Your Life Easier? 29 | 30 | [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/E1E2181LU) 31 | 32 | Support on Ko-Fi allows me to develop such tools as this one and perform 33 | hardware-related experiments. 34 | 35 | ## How Does It Look Like? 36 | 37 | Title page 38 | 39 | ![Preview 1](https://user-images.githubusercontent.com/1590880/93708766-32ab0d80-fb39-11ea-8365-da2ca1b13d8b.jpg) 40 | 41 | Property filter 42 | 43 | ![Preview 2](https://user-images.githubusercontent.com/1590880/93708599-e01d2180-fb37-11ea-96b6-5d5eb4e0f285.jpg) 44 | 45 | Component detail 46 | 47 | ![Preview 3](https://user-images.githubusercontent.com/1590880/93708601-e0b5b800-fb37-11ea-84ed-6ba73f07911d.jpg) 48 | 49 | 50 | ## How Does It Work? 51 | 52 | The page has no backend so it can be easily hosted on GitHub Pages. Therefore, 53 | Travis CI download XLS spreadsheet from the JLC PCB page, a Python script 54 | process it and it generates per-category JSON file with components. 55 | 56 | The frontend uses IndexedDB in the browser to store the component library and 57 | perform queries on it. Therefore, before the first use, you have to download the 58 | component library and it can take a while. Then, all the queries are performed 59 | locally. 60 | 61 | ## Development 62 | 63 | To get started with developing the frontend, you will need NodeJS & Python 3. 64 | 65 | Set up the Python portion of the program by running: 66 | 67 | ``` 68 | $ virtualenv venv 69 | $ source venv/bin/activate 70 | $ pip install -e . 71 | ``` 72 | 73 | Then to download the cached parts list and process it, run: 74 | 75 | ``` 76 | $ wget https://yaqwsx.github.io/jlcparts/data/cache.zip https://yaqwsx.github.io/jlcparts/data/cache.z0{1..8} 77 | $ 7z x cache.zip 78 | $ mkdir -p web/public/data/ 79 | $ jlcparts buildtables --jobs 0 --ignoreoldstock 30 cache.sqlite3 web/public/data 80 | ``` 81 | 82 | To launch the frontend web server, run: 83 | 84 | ``` 85 | $ cd web 86 | $ npm install 87 | $ npm start 88 | ``` 89 | 90 | ## The Page Is Broken! 91 | 92 | Feel free to open an issue on GitHub. 93 | 94 | ## You Might Also Be Interested 95 | 96 | - [KiKit](https://github.com/yaqwsx/KiKit): a tool for automatic panelization of 97 | KiCAD PCBs. It can also perform fully automatic export of manufacturing data 98 | for JLC PCB assembly - read [the 99 | documentation](https://github.com/yaqwsx/KiKit/blob/master/doc/fabrication/jlcpcb.md) 100 | or produce a solder-paste stencil for populating components missing at JLC PCB - read [the 101 | documentation](https://github.com/yaqwsx/KiKit/blob/master/doc/stencil.md). 102 | - [PcbDraw](https://github.com/yaqwsx/PcbDraw): a tool for making nice schematic 103 | drawings of your boards and population manuals. 104 | -------------------------------------------------------------------------------- /.github/workflows/update_components.yaml: -------------------------------------------------------------------------------- 1 | name: "Update component database" 2 | on: 3 | push: 4 | pull_request: 5 | schedule: 6 | # - cron: '0 3 * * *' 7 | jobs: 8 | build_and_update: 9 | name: "Update component database and frontend" 10 | runs-on: ubuntu-22.04 11 | environment: github-pages 12 | steps: 13 | - name: Maximize build space 14 | uses: easimon/maximize-build-space@master 15 | with: 16 | root-reserve-mb: 512 17 | swap-size-mb: 1024 18 | remove-dotnet: 'true' 19 | - name: Checkout 20 | uses: actions/checkout@v3 21 | - name: Install dependencies 22 | run: | 23 | sudo apt-get update 24 | sudo apt-get install -y --no-install-recommends \ 25 | python3 python3-pip nodejs npm wget zip unzip p7zip-full 26 | sudo pip3 install requests click 27 | - name: Build frontend 28 | run: | 29 | cd web 30 | if [ "$GITHUB_REPOSITORY" = 'yaqwsx/jlcparts-dev' ]; then 31 | export BASEURL=https://jlcparts-dev.honzamrazek.cz 32 | else 33 | export BASEURL=https://yaqwsx.github.io/jlcparts 34 | fi 35 | npm install --silent 36 | NODE_ENV=production PUBLIC_URL=${BASEURL} npm run build 37 | if [ $GITHUB_REPOSITORY = 'yaqwsx/jlcparts-dev' ]; then 38 | echo 'jlcparts-dev.honzamrazek.cz' > build/CNAME 39 | fi 40 | touch .nojekyll 41 | - name: Update database 42 | env: # Or as an environment variable 43 | LCSC_KEY: ${{ secrets.LCSC_KEY }} 44 | LCSC_SECRET: ${{ secrets.LCSC_SECRET }} 45 | JLCPCB_KEY: ${{ secrets.JLCPCB_KEY }} 46 | JLCPCB_SECRET: ${{ secrets.JLCPCB_SECRET }} 47 | run: | 48 | set -x 49 | sudo pip3 install -e . 50 | 51 | wget -q https://yaqwsx.github.io/jlcparts/data/cache.zip 52 | for seq in $(seq -w 01 30); do 53 | wget -q https://yaqwsx.github.io/jlcparts/data/cache.z$seq || true 54 | done 55 | 56 | 7z x cache.zip 57 | for seq in $(seq -w 01 30); do 58 | rm cache.z$seq || true 59 | done 60 | 61 | jlcparts fetchtable parts.csv 62 | 63 | jlcparts getlibrary --age 10000 \ 64 | --limit 1000 \ 65 | parts.csv cache.sqlite3 66 | jlcparts updatepreferred cache.sqlite3 67 | jlcparts buildtables --jobs 0 \ 68 | --ignoreoldstock 120 \ 69 | cache.sqlite3 web/build/data 70 | 71 | rm -f web/build/data/cache.z* 72 | zip -s 50m web/build/data/cache.zip cache.sqlite3 73 | - name: Tar artifact # Artifact are case insensitive, this is workaround 74 | run: tar -czf web_build.tar.gz web/build/ 75 | - name: Upload artifact 76 | uses: actions/upload-artifact@v4 77 | with: 78 | name: web_build 79 | path: web_build.tar.gz 80 | retention-days: 14 81 | - name: Upload table 82 | uses: actions/upload-artifact@v4 83 | with: 84 | name: component_table 85 | path: parts.csv 86 | retention-days: 14 87 | deploy: 88 | name: "Deploy" 89 | runs-on: ubuntu-22.04 90 | needs: build_and_update 91 | if: github.ref == 'refs/heads/master' 92 | steps: 93 | - name: Checkout # Required for GH-pages deployment 94 | uses: actions/checkout@v3 95 | - name: "Download web" 96 | uses: actions/download-artifact@v4 97 | with: 98 | name: web_build 99 | - name: Untar artifact 100 | run: tar -xzf web_build.tar.gz 101 | - name: Deploy to GH Pages 102 | uses: JamesIves/github-pages-deploy-action@v4.4.3 103 | with: 104 | branch: gh-pages 105 | folder: web/build 106 | single-commit: true 107 | -------------------------------------------------------------------------------- /web/src/units.js: -------------------------------------------------------------------------------- 1 | import { naturalCompare } from '@discoveryjs/natural-compare'; 2 | 3 | // Return comparator for given quantity 4 | export function quantityComparator(quantityName) { 5 | const numericQuantities = [ 6 | "resistance", "voltage", "current", "power", "count", "capacitance", 7 | "length", "inductance", "temperature", "charge" 8 | ]; 9 | if (numericQuantities.includes(quantityName)) 10 | return numericComparator; 11 | return naturalCompare; 12 | } 13 | 14 | // Return formatter for given quantity 15 | export function quantityFormatter(quantityName) { 16 | const formatters = { 17 | resistance: resistanceFormatter, 18 | voltage: siFormatter("V"), 19 | current: siFormatter("A"), 20 | power: siFormatter("W"), 21 | capacitance: siFormatter("F"), 22 | frequency: siFormatter("Hz"), 23 | length: siFormatter("m"), 24 | inductance: siFormatter("H"), 25 | charge: siFormatter("C"), 26 | count: x => String(x), 27 | temperature: x => `${x} °C` 28 | }; 29 | 30 | let formatter = formatters[quantityName]; 31 | if (formatter) 32 | return formatter 33 | return x => String(x); 34 | } 35 | 36 | function numericComparator(a, b) { 37 | if (a === "NaN") 38 | a = undefined; 39 | if (b === "NaN") 40 | b = undefined; 41 | if (a === undefined && b === undefined) 42 | return 0; 43 | if (a === undefined) 44 | return 1; 45 | if (b === undefined) 46 | return -1; 47 | 48 | return a - b; 49 | } 50 | 51 | function removeTrailingChar(str, charToRemove) { 52 | while(str.endsWith(charToRemove)) { 53 | str = str.slice(0, -1); 54 | } 55 | return str; 56 | } 57 | 58 | 59 | // Format values like 1u6, 1k6, 1M9 60 | function infixMagnitudeFormatter(value, letter, order) { 61 | value = value / order; 62 | let integralPart = Math.floor(value); 63 | let fractionalPart = (value - integralPart) * 1000; // Number of significant digits 64 | let fractionalPartStr = removeTrailingChar(fractionalPart.toFixed(0).padStart(3, '0'), '0') 65 | 66 | return String(integralPart) + letter + fractionalPartStr; 67 | } 68 | 69 | function siFormatterImpl(value, unit) { 70 | if (value === "NaN") 71 | return "-"; 72 | if (value === 0) 73 | return "0 " + unit; 74 | let prefixes = [ 75 | { magnitude: 1e-12, prefix: "p" }, 76 | { magnitude: 1e-9, prefix: "n" }, 77 | { magnitude: 1e-6, prefix: "μ" }, 78 | { magnitude: 1e-3, prefix: "m" }, 79 | { magnitude: 1, prefix: "" }, 80 | { magnitude: 1e3, prefix: "k" }, 81 | { magnitude: 1e6, prefix: "M" }, 82 | { magnitude: 1e9, prefix: "G" } 83 | ]; 84 | // Choose prefix to use 85 | let prefix; 86 | for (var idx = 0; idx < prefixes.length; idx++) { 87 | if (idx === prefixes.length - 1 || Math.abs(value) < prefixes[idx + 1].magnitude) { 88 | prefix = prefixes[idx]; 89 | break; 90 | } 91 | } 92 | 93 | return (value / prefix.magnitude) 94 | .toFixed(6) 95 | .replace(/0*$/,'') 96 | .replace(/[.,]$/,'') + " " + prefix.prefix + unit; 97 | } 98 | 99 | function siFormatter(unit) { 100 | return value => siFormatterImpl(value, unit); 101 | } 102 | 103 | function resistanceFormatter(resistance) { 104 | if (resistance === "NaN") 105 | return "-" 106 | if (resistance === 0) 107 | return "0R"; 108 | if (resistance < 1) { 109 | return (resistance * 1000).toFixed(6).replace(/0*$/,'').replace(/[.,]$/,'') + "mR"; 110 | } 111 | if (resistance < 1e3) { 112 | // Format with R, e.g., 1R 5R6 113 | return infixMagnitudeFormatter(resistance, "R", 1); 114 | } 115 | if (resistance < 1e6) { 116 | // Format with k, e.g, 3k3 56k 117 | return infixMagnitudeFormatter(resistance, "k", 1e3); 118 | } 119 | if (resistance < 1e9) { 120 | // Format with M, e.g., 1M, 5M6 121 | return infixMagnitudeFormatter(resistance, "M", 1e6); 122 | } 123 | // Format with G 124 | return infixMagnitudeFormatter(resistance, "G", 1e9); 125 | } 126 | -------------------------------------------------------------------------------- /web/src/jlc.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { InlineSpinbox } from "./componentTable.js" 3 | import { CORS_KEY } from "./corsBridge.js"; 4 | 5 | export function getQuantityPrice(quantity, pricelist) { 6 | return pricelist.find(pricepoint => 7 | quantity >= pricepoint.qFrom && (quantity <= pricepoint.qTo || !pricepoint.qTo) 8 | )?.price ?? pricelist[0]?.price; 9 | } 10 | 11 | export class AttritionInfo extends React.Component { 12 | constructor(props) { 13 | super(props); 14 | this.props = props; 15 | this.state = {} 16 | } 17 | 18 | componentDidMount() { 19 | fetch("https://cors.bridged.cc/https://jlcpcb.com/shoppingCart/smtGood/selectSmtComponentList", { 20 | method: 'POST', 21 | headers: { 22 | "Accept": 'application/json, text/plain, */*', 23 | "Content-Type": 'application/json;charset=UTF-8', 24 | "x-cors-grida-api-key": CORS_KEY 25 | }, 26 | body: JSON.stringify({ 27 | currentPage: 1, 28 | pageSize: 25, 29 | keyword: this.props.component.lcsc, 30 | firstSortName: "", 31 | secondeSortName: "", 32 | searchSource: "search", 33 | componentAttributes: [] 34 | }) 35 | }) 36 | .then(response => { 37 | if (!response.ok || response.status !== 200) { 38 | throw new Error(`Cannot fetch ${this.props.component.lcsc}: ${response.statusText}`); 39 | } 40 | return response.json(); 41 | }) 42 | .then(({data}) => { 43 | const lcscId = data.componentPageInfo.list.find(({componentCode}) => componentCode === this.props.component.lcsc)?.componentId; 44 | if (lcscId === undefined) { 45 | throw new Error(`No search results for ${this.props.component.lcsc}`); 46 | } 47 | return fetch("https://cors.bridged.cc/https://jlcpcb.com/shoppingCart/smtGood/getComponentDetail?componentLcscId=" + lcscId, { 48 | headers: { 49 | "x-cors-grida-api-key": CORS_KEY 50 | }, 51 | }); 52 | }) 53 | .then(response => { 54 | if (!response.ok || response.status !== 200) { 55 | throw new Error(`Cannot fetch ${this.props.lcsc}: ${response.statusText}`); 56 | } 57 | return response.json(); 58 | }) 59 | .then(({data}) => { 60 | this.setState({data}); 61 | }) 62 | .catch(error => { 63 | this.setState({error: true, errorMessage: error.toString()}); 64 | console.log(error); 65 | }); 66 | } 67 | 68 | price() { 69 | let q = Math.max(parseInt(this.props.quantity) + parseInt(this.state.data.lossNumber), 70 | this.state.data.leastNumber); 71 | return q * getQuantityPrice(q, this.props.component.price); 72 | } 73 | 74 | render() { 75 | let data = this.state.data; 76 | if (this.state.error) { 77 | return
78 | Cannot fetch attrition data from JLC website: {this.state.errorMessage}. 79 |
80 | } 81 | if (data) 82 | return 83 | 84 | { data.lossNumber 85 | ? 86 | 87 | 88 | 89 | : "" 90 | } 91 | { data.leastNumber 92 | ? 93 | 94 | 95 | 96 | : "" 97 | } 98 | 99 | 100 | 101 | 102 | 103 |
Attrition:{data.lossNumber} pcs
Minimal order quantity:{data.leastNumber} pcs
Price for {this.props.quantity} pcs:{Math.round((this.price() + Number.EPSILON) * 1000) / 1000} USD
104 | return
105 | 106 |
; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /web/public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | image/svg+xml -------------------------------------------------------------------------------- /web/src/serviceWorker.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener('load', () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | 'This web app is being served cache-first by a service ' + 46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then(registration => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === 'installed') { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | 'New content is available and will be used when all ' + 74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log('Content is cached for offline use.'); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch(error => { 97 | console.error('Error during service worker registration:', error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { 'Service-Worker': 'script' }, 105 | }) 106 | .then(response => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get('content-type'); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf('javascript') === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then(registration => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | 'No internet connection found. App is running in offline mode.' 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ('serviceWorker' in navigator) { 133 | navigator.serviceWorker.ready 134 | .then(registration => { 135 | registration.unregister(); 136 | }) 137 | .catch(error => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /test/testParts.csv: -------------------------------------------------------------------------------- 1 | LCSC Part;First Category;Second Category;MFR.Part;Package;Solder Joint;Manufacturer;Library Type;Description;Datasheet;Price;Stock 2 | C25725;Resistors;Resistor Networks & Arrays;4D02WGJ0103TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 10KOhms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0103TCE_C25725.pdf;1-199:0.005210145,200-:0.001866667;67586 3 | C25726;Resistors;Resistor Networks & Arrays;4D02WGJ0102TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 1KOhms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0102TCE_C25726.pdf;1-199:0.005210145,200-:0.001866667;78875 4 | C25501;Resistors;Resistor Networks & Arrays;4D02WGJ0330TCE;0402_x4;8;Uniroyal Elec;Basic;Resistor Networks & Arrays 33Ohms ±5% 1/16W 0402_x4 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-4D02WGJ0330TCE_C25501.pdf;1-199:0.004749275,200-:0.001701449;76307 5 | C32375;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012T220KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012T220KTF_C32375.pdf;1-199:0.026200000,200-:0.012500000;43999 6 | C51725;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012Q3R3KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012Q3R3KTF_C51725.pdf;1-199:0.016815942,200-:0.007427536;27602 7 | C14304;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012T470KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012T470KTF_C14304.pdf;1-99:0.043747826,100-:0.023369565;26497 8 | C1042;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012Q1R0KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012Q1R0KTF_C1042.pdf;1-199:0.017931884,200-:0.008240580;37413 9 | C1043;Inductors & Chokes & Transformers;Inductors (SMD);CMI201209U2R2KT;0805;2;Guangdong Fenghua Advanced Tech;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-CMI201209U2R2KT_C1043.pdf;1-199:0.010449275,200-:0.004427536;34565 10 | C1046;Inductors & Chokes & Transformers;Inductors (SMD);SDFL2012S100KTF;0805;2;Sunlord;Basic;Inductors (SMD) 0805 RoHS;https://datasheet.lcsc.com/szlcsc/Sunlord-SDFL2012S100KTF_C1046.pdf;1-199:0.017739130,200-:0.008152174;96901 11 | C17902;Resistors;Chip Resistor - Surface Mount;1206W4F1002T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 10KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F1002T5E_C17902.pdf;1-199:0.005760870,200-:0.002156522;600403 12 | C17903;Resistors;Chip Resistor - Surface Mount;1206W4F100JT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 10Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F100JT5E_C17903.pdf;1-199:0.004056522,200-:0.001453623;180708 13 | C17924;Resistors;Chip Resistor - Surface Mount;1206W4F1800T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 180Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F1800T5E_C17924.pdf;1-199:0.007044928,200-:0.002752174;43294 14 | C17928;Resistors;Chip Resistor - Surface Mount;1206W4F100KT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 1Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F100KT5E_C17928.pdf;1-199:0.007086957,200-:0.002768116;50315 15 | C17936;Resistors;Chip Resistor - Surface Mount;1206W4F4701T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 4.7KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F4701T5E_C17936.pdf;1-199:0.005760870,200-:0.002156522;555574 16 | C17944;Resistors;Chip Resistor - Surface Mount;1206W4F2001T5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 2KOhms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F2001T5E_C17944.pdf;1-199:0.004091304,200-:0.001465217;141238 17 | C17955;Resistors;Chip Resistor - Surface Mount;1206W4F200JT5E;1206;2;Uniroyal Elec;Basic;Chip Resistor - Surface Mount 20Ohms ±1% 1/4W 1206 RoHS;https://datasheet.lcsc.com/szlcsc/Uniroyal-Elec-1206W4F200JT5E_C17955.pdf;1-199:0.005837681,200-:0.002185507;277566 18 | C1558;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG2R0C500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 2pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG2R0C500NT_C1558.pdf;1-999:0.001739130,1000-:0.000543478;63891 19 | C1561;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG2R7C500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 2.7pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG2R7C500NT_C1561.pdf;1-999:0.001739130,1000-:0.000543478;59863 20 | C1554;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG200J500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 20pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG200J500NT_C1554.pdf;1-199:0.005649275,200-:0.002114493;91205 21 | C1530;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402B221K500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 220pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402B221K500NT_C1530.pdf;1-199:0.010688406,200-:0.004528986;79360 22 | C1532;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402B223K500NT;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 22nF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402B223K500NT_C1532.pdf;1-199:0.010688406,200-:0.004528986;110653 23 | C1557;Capacitors;Multilayer Ceramic Capacitors MLCC - SMD/SMT;0402CG270J500NTN;0402;2;Guangdong Fenghua Advanced Tech;Basic;Multilayer Ceramic Capacitors MLCC - SMD/SMT 27pF 50V 0402 RoHS;https://datasheet.lcsc.com/szlcsc/Guangdong-Fenghua-Advanced-Tech-0402CG270J500NTN_C1557.pdf;1-199:0.012878261,200-:0.005688406;101996 24 | -------------------------------------------------------------------------------- /jlcparts/ui.py: -------------------------------------------------------------------------------- 1 | from multiprocessing import Pool 2 | import json 3 | 4 | import click 5 | 6 | from jlcparts.datatables import buildtables, normalizeAttribute 7 | from jlcparts.lcsc import pullPreferredComponents 8 | from jlcparts.partLib import (PartLibrary, PartLibraryDb, getLcscExtraNew, 9 | loadJlcTable, loadJlcTableLazy) 10 | 11 | 12 | def fetchLcscData(lcsc): 13 | extra = getLcscExtraNew(lcsc) 14 | return (lcsc, extra) 15 | 16 | @click.command() 17 | @click.argument("source", type=click.Path(dir_okay=False, exists=True)) 18 | @click.argument("db", type=click.Path(dir_okay=False, writable=True)) 19 | @click.option("--age", type=int, default=0, 20 | help="Automatically discard n oldest components and fetch them again") 21 | @click.option("--limit", type=int, default=10000, 22 | help="Limit number of newly added components") 23 | def getLibrary(source, db, age, limit): 24 | """ 25 | Download library inside OUTPUT (JSON format) based on SOURCE (csv table 26 | provided by JLC PCB). 27 | 28 | You can specify previously downloaded library as a cache to save requests to 29 | fetch LCSC extra data. 30 | """ 31 | OLD = 0 32 | REFRESHED = 1 33 | 34 | db = PartLibraryDb(db) 35 | missing = set() 36 | total = 0 37 | with db.startTransaction(): 38 | db.resetFlag(value=OLD) 39 | with open(source, newline="") as f: 40 | jlcTable = loadJlcTableLazy(f) 41 | for component in jlcTable: 42 | total += 1 43 | if db.exists(component["lcsc"]): 44 | db.updateJlcPart(component, flag=REFRESHED) 45 | else: 46 | component["extra"] = {} 47 | db.addComponent(component, flag=REFRESHED) 48 | missing.add(component["lcsc"]) 49 | print(f"New {len(missing)} components out of {total} total") 50 | ageCount = min(age, max(0, limit - len(missing))) 51 | print(f"{ageCount} components will be aged and thus refreshed") 52 | missing = missing.union(db.getNOldest(ageCount)) 53 | 54 | # Truncate the missing components to respect the limit: 55 | missing = list(missing)[:limit] 56 | 57 | with Pool(processes=10) as pool: 58 | for i, (lcsc, extra) in enumerate(pool.imap_unordered(fetchLcscData, missing)): 59 | print(f" {lcsc} fetched. {((i+1) / len(missing) * 100):.2f} %") 60 | db.updateExtra(lcsc, extra) 61 | db.removeWithFlag(value=OLD) 62 | # Temporary work-around for space-related issues in CI - simply don't rebuild the DB 63 | # db.vacuum() 64 | 65 | 66 | 67 | @click.command() 68 | @click.argument("db", type=click.Path(dir_okay=False, writable=True)) 69 | def updatePreferred(db): 70 | """ 71 | Download list of preferred components from JLC PCB and mark them into the DB. 72 | """ 73 | preferred = pullPreferredComponents() 74 | lib = PartLibraryDb(db) 75 | lib.setPreferred(preferred) 76 | 77 | 78 | @click.command() 79 | @click.argument("libraryFilename") 80 | def listcategories(libraryfilename): 81 | """ 82 | Print all categories from library specified by LIBRARYFILENAMEto standard 83 | output 84 | """ 85 | lib = PartLibrary(libraryfilename) 86 | for c, subcats in lib.categories().items(): 87 | print(f"{c}:") 88 | for s in subcats: 89 | print(f" {s}") 90 | 91 | @click.command() 92 | @click.argument("libraryFilename") 93 | def listattributes(libraryfilename): 94 | """ 95 | Print all keys in the extra["attributes"] arguments from library specified by 96 | LIBRARYFILENAME to standard output 97 | """ 98 | keys = set() 99 | lib = PartLibrary(libraryfilename) 100 | for subcats in lib.lib.values(): 101 | for parts in subcats.values(): 102 | for data in parts.values(): 103 | if "extra" not in data: 104 | continue 105 | extra = data["extra"] 106 | attr = extra.get("attributes", {}) 107 | if not isinstance(attr, list): 108 | for k in extra.get("attributes", {}).keys(): 109 | keys.add(k) 110 | for k in keys: 111 | print(k) 112 | 113 | @click.command() 114 | @click.argument("lcsc_code") 115 | def fetchDetails(lcsc_code): 116 | """ 117 | Fetch LCSC extra information for a given LCSC code 118 | """ 119 | print(getLcscExtraNew(lcsc_code)) 120 | 121 | @click.command() 122 | @click.argument("filename", type=click.Path(writable=True)) 123 | @click.option("--verbose", is_flag=True, 124 | help="Be verbose") 125 | def fetchTable(filename, verbose): 126 | """ 127 | Fetch JLC PCB component table 128 | """ 129 | from .jlcpcb import pullComponentTable 130 | 131 | def report(count: int) -> None: 132 | if (verbose): 133 | print(f"Fetched {count}") 134 | 135 | pullComponentTable(filename, report) 136 | 137 | @click.command() 138 | @click.argument("lcsc") 139 | def testComponent(lcsc): 140 | """ 141 | Tests parsing attributes of given component 142 | """ 143 | extra = getLcscExtraNew(lcsc)["attributes"] 144 | 145 | extra.pop("url", None) 146 | extra.pop("images", None) 147 | extra.pop("prices", None) 148 | extra.pop("datasheet", None) 149 | extra.pop("id", None) 150 | extra.pop("manufacturer", None) 151 | extra.pop("number", None) 152 | extra.pop("title", None) 153 | extra.pop("quantity", None) 154 | for i in range(10): 155 | extra.pop(f"quantity{i}", None) 156 | normalized = dict(normalizeAttribute(key, val) for key, val in extra.items()) 157 | print(json.dumps(normalized, indent=4)) 158 | 159 | 160 | @click.group() 161 | def cli(): 162 | pass 163 | 164 | cli.add_command(getLibrary) 165 | cli.add_command(listcategories) 166 | cli.add_command(listattributes) 167 | cli.add_command(buildtables) 168 | cli.add_command(updatePreferred) 169 | cli.add_command(fetchDetails) 170 | cli.add_command(fetchTable) 171 | cli.add_command(testComponent) 172 | 173 | if __name__ == "__main__": 174 | cli() 175 | -------------------------------------------------------------------------------- /web/src/history.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { fetchJson, db } from './db' 3 | import { Spinbox, InlineSpinbox, ZoomableLazyImage, 4 | formatAttribute, findCategoryById, getImageUrl, 5 | restoreLcscUrl } from './componentTable' 6 | import { getQuantityPrice } from './jlc' 7 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 8 | 9 | export function History(props) { 10 | return
11 | 12 |
13 | } 14 | 15 | class HistoryItem extends React.Component { 16 | constructor(props) { 17 | super(props); 18 | this.state = {}; 19 | } 20 | 21 | componentDidMount() { 22 | db.components.get({lcsc: this.props.lcsc}).then( component => { 23 | this.setState({info: component}); 24 | }); 25 | } 26 | 27 | renderImage() { 28 | let x = this.state.info; 29 | const imgSrc = getImageUrl(x.img, "small") ?? "./brokenimage.svg"; 30 | return 37 | } 38 | 39 | renderLoaded() { 40 | let x = this.state.info; 41 | let price = getQuantityPrice(1, x.price) 42 | let unitPrice = Math.round((price + Number.EPSILON) * 1000) / 1000; 43 | let category = findCategoryById(this.props.categories, x.category); 44 | return 45 | 46 | e.stopPropagation()} 49 | target="_blank" 50 | rel="noopener noreferrer"> 51 | {x.lcsc} 52 | 53 | 54 | 55 | e.stopPropagation()} 58 | target="_blank" 59 | rel="noopener noreferrer"> 60 | {x.mfr} 61 | 62 | 63 | 64 | {formatAttribute(x.attributes["Basic/Extended"])[0]} 65 | 66 | 67 | {this.renderImage()} 68 | 69 | 70 | {x.description} 71 | 72 | 73 | {category.category}: {category.subcategory} 74 | 75 | 76 | {`${unitPrice}$/unit`} 77 | 78 | 79 | {x.stock} 80 | 81 | 82 | } 83 | 84 | renderUnknown() { 85 | return 86 | 87 | {this.props.lcsc} 88 | 89 | 90 | Component is missing in database. Do you use the latest database? 91 | 92 | 93 | } 94 | 95 | render() { 96 | if (this.state?.info !== undefined) 97 | return this.renderLoaded(); 98 | if ("info" in this.state) 99 | return this.renderUnknown(); 100 | return 101 | 102 | {this.props.lcsc} 103 | 104 | 105 | 106 | 107 | 108 | } 109 | } 110 | 111 | function DayTable(props) { 112 | return 113 | 114 | { 115 | ["LCSC", "MFR", "Basic/Extended", "Image", "Description", 116 | "Category", "Price", "Stock"].map( label => { 117 | return 120 | }) 121 | } 122 | 123 | 124 | { 125 | props.components.map( 126 | lcsc => 127 | ) 131 | } 132 | 133 |
118 | {label} 119 |
134 | } 135 | 136 | class HistoryTable extends React.Component { 137 | constructor(props) { 138 | super(props); 139 | this.state = {}; 140 | } 141 | 142 | componentDidMount() { 143 | fetchJson(process.env.PUBLIC_URL + "/data/changelog.json") 144 | .then(response => { 145 | let log = []; 146 | for (const day in response) { 147 | log.push({ 148 | day: new Date(day), 149 | components: response[day] 150 | }); 151 | } 152 | log.sort((a, b) => b.day - a.day); 153 | this.setState({table: log}); 154 | }); 155 | db.categories.toArray().then( categories => this.setState({categories}) ); 156 | } 157 | 158 | render() { 159 | if (this.state.table === undefined) { 160 | return 161 | } 162 | return this.state.table.map(item => { 163 | if (item.components.length === 0) 164 | return null; 165 | let day = item.day; 166 | return
167 |

168 | Newly added components on {day.getDate()}. {day.getMonth() + 1}. {day.getFullYear()}: 169 |

170 | 174 |
175 | }); 176 | } 177 | } -------------------------------------------------------------------------------- /web/src/sortableTable.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' 3 | import produce from 'immer'; 4 | import { Waypoint } from 'react-waypoint'; 5 | 6 | 7 | function SortableHeaderField(props) { 8 | var sortIcons; 9 | var className = "bg-blue-500 mx-1 p-2 border-r-2 rounded" 10 | if (props.sortable) { 11 | className += " cursor-pointer" 12 | let icon = "sort"; 13 | if (props.sortDirection === "asc") 14 | icon = "sort-amount-up"; 15 | if (props.sortDirection === "desc") 16 | icon = "sort-amount-down"; 17 | sortIcons = 18 | } else { 19 | sortIcons = null; 20 | } 21 | 22 | return <> 23 | props.onClick()} className={className}> 24 |
25 |
26 | {props.header} 27 | {sortIcons} 28 |
29 | { 30 | props.onDelete && ( 31 |
{ 32 | e.stopPropagation(); 33 | props.onDelete(); 34 | }}> 35 | 36 |
37 | ) 38 | } 39 |
40 | 41 | 42 | } 43 | 44 | export class SortableTable extends React.Component { 45 | constructor(props) { 46 | super(props) 47 | this.state = { 48 | sortBy: null, 49 | sortDirection: "asc", 50 | visibleItems: 100 51 | }; 52 | } 53 | 54 | componentDidUpdate(prevProps) { 55 | if (prevProps.data !== this.props.data) { 56 | this.setState({visibleItems: 100}); 57 | } 58 | } 59 | 60 | handleHeaderClick = name => { 61 | if (this.state.sortBy === name) { 62 | this.setState(produce(this.state, draft => { 63 | if (draft.sortDirection === "asc") 64 | draft.sortDirection = "desc"; 65 | else 66 | draft.sortDirection = "asc"; 67 | })); 68 | } 69 | else { 70 | this.setState(produce(this.state, draft => { 71 | draft.sortBy = name; 72 | draft.sortDirection = "asc"; 73 | })); 74 | } 75 | } 76 | 77 | getComparator = columnName => { 78 | return this.props.header.find(obj => obj.name === columnName)?.comparator; 79 | } 80 | 81 | getPropAsString = propName => { 82 | return this.props?.[propName] ?? ""; 83 | } 84 | 85 | rowClassName = () => { return this.getPropAsString("rowClassName"); } 86 | 87 | evenRowClassName = () => { return this.getPropAsString("evenRowClassName"); } 88 | 89 | oddRowClassName = () => { return this.getPropAsString("oddRowClassName"); } 90 | 91 | showMore = () => { 92 | this.setState(produce(this.state, draft => { 93 | if (this.state.visibleItems < this.props.data.length) 94 | draft.visibleItems += 50; 95 | })); 96 | } 97 | 98 | render() { 99 | var t0 = performance.now() 100 | var sortedData = [...this.props.data]; 101 | if (this.state.sortBy) { 102 | let pureComparator = this.getComparator(this.state.sortBy); 103 | let comparator; 104 | if (this.state.sortDirection === "desc") 105 | comparator = (a, b) => - pureComparator(a, b); 106 | else 107 | comparator = pureComparator; 108 | 109 | sortedData.sort(comparator); 110 | } 111 | var t1 = performance.now() 112 | console.log("Sorting took " + (t1 - t0) + " milliseconds.") 113 | sortedData = sortedData.slice(0, this.state.visibleItems); 114 | return <> 115 | 116 | 117 | { 118 | this.props.header.map( x => { 119 | let sortDirection = null; 120 | if (this.state.sortBy === x.name) 121 | sortDirection = this.state.sortDirection; 122 | return this.handleHeaderClick(x.name)} 127 | sortDirection={sortDirection} 128 | onDelete={x.onDelete}/>; 129 | }) 130 | } 131 | 132 | { 133 | sortedData.map((row, index) => { 134 | let className = this.rowClassName(); 135 | if ( index % 2 === 0 ) 136 | className += " " + this.evenRowClassName(); 137 | else 138 | className += " " + this.oddRowClassName(); 139 | return 142 | { 143 | this.props.header.map(cell => { 144 | return 147 | }) 148 | } 149 | 150 | }) 151 | } 152 |
145 | { cell.displayGetter(row) } 146 |
153 | { 154 | this.state.visibleItems < this.props.data.length && ( 155 |

Loading more components...

156 | ) 157 | } 158 | 159 | 160 | } 161 | } 162 | 163 | class ExpandableTableRow extends React.Component { 164 | constructor(props) { 165 | super(props); 166 | this.state = { 167 | expanded: false 168 | } 169 | } 170 | 171 | handleClick = (e) => { 172 | e.preventDefault(); 173 | this.setState(produce(this.state, draft => { 174 | draft.expanded = !draft.expanded; 175 | })); 176 | } 177 | 178 | render() { 179 | let expandableContent = null; 180 | let className = this.props.className ?? ""; 181 | if (this.state.expanded && this.props.expandableContent) { 182 | expandableContent = 183 | 184 | {this.props.expandableContent} 185 | 186 | ; 187 | } 188 | if (this.props.expandableContent) 189 | className += " cursor-pointer"; 190 | return <> 191 | 192 | {this.props.children} 193 | 194 | {expandableContent} 195 | 196 | } 197 | } -------------------------------------------------------------------------------- /web/src/app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | HashRouter as Router, 4 | Switch, 5 | Route, 6 | NavLink 7 | } from "react-router-dom"; 8 | 9 | import { library } from '@fortawesome/fontawesome-svg-core' 10 | import { fas } from '@fortawesome/free-solid-svg-icons' 11 | import { far } from '@fortawesome/free-regular-svg-icons' 12 | import { fab } from '@fortawesome/free-brands-svg-icons' 13 | 14 | import './main.css'; 15 | import { updateComponentLibrary, checkForComponentLibraryUpdate, db } from './db' 16 | import { ComponentOverview } from './componentTable' 17 | import { History } from './history' 18 | 19 | 20 | library.add(fas, far, fab); 21 | 22 | function Header(props) { 23 | return <> 24 |
25 | 26 |
27 |

28 | JLC PCB SMD Assembly Component Catalogue 29 |

30 |

31 | Parametric search for components offered by JLC PCB SMD assembly service. 32 |

33 |

34 | Read more at project's GitHub page. 35 |

36 |
37 |
38 |
39 | Do you enjoy this site? Consider supporting me so I can actively maintain projects like this one! 40 | Read more about my story. 41 | 42 | 43 | 44 | 47 | 50 | 51 | 52 | 55 | 60 | 61 | 62 |
45 | GitHub Sponsors: 46 | 48 | 49 |
53 | Ko-Fi: 54 | 56 | 57 | Ko-Fi button 58 | 59 |
63 |
64 | 65 | } 66 | 67 | function Footer(props) { 68 | return
69 | 70 |
71 | } 72 | 73 | class FirstTimeNote extends React.Component { 74 | constructor(props) { 75 | super(props); 76 | this.state = { 77 | componentCount: undefined 78 | }; 79 | } 80 | 81 | componentDidMount() { 82 | db.components.count().then(x => { 83 | this.setState({componentCount: x}); 84 | }) 85 | } 86 | 87 | render() { 88 | if (this.state.componentCount === undefined || this.state.componentCount !== 0) 89 | return null; 90 | return
91 |

92 | Hey, it seems that you run the application for the first time, hence, 93 | there's no component library in your device. Just press the "Update 94 | the component library button" in the upper right corner to download it 95 | and use the app. 96 |

97 |

98 | Note that the initial download of the component library might take a while. 99 |

100 |
101 | } 102 | } 103 | 104 | class NewComponentFormatWarning extends React.Component { 105 | constructor(props) { 106 | super(props); 107 | this.state = { 108 | newComponentFormat: true 109 | }; 110 | } 111 | 112 | componentDidMount() { 113 | db.components.toCollection().first().then(x => { 114 | if (x !== undefined && typeof x.attributes[Object.keys(x.attributes)[0]] !== 'object') 115 | this.setState({newComponentFormat: false}); 116 | }); 117 | } 118 | 119 | render() { 120 | if (this.state.newComponentFormat) 121 | return null; 122 | return
123 |

124 | Hey, there have been some breaking changes to the library format. 125 | Please, update the library before continuing to use the tool. 126 |

127 |
128 | } 129 | } 130 | 131 | class UpdateBar extends React.Component { 132 | constructor(props) { 133 | super(props); 134 | this.state = { 135 | updateAvailable: this.props.updateAvailable 136 | }; 137 | } 138 | 139 | componentDidMount() { 140 | let checkStatus = () => { 141 | checkForComponentLibraryUpdate().then( updateAvailable => { 142 | this.setState({updateAvailable}); 143 | }); 144 | db.settings.get("lastUpdate").then(lastUpdate => { 145 | this.setState({lastUpdate}); 146 | }) 147 | }; 148 | 149 | checkStatus(); 150 | this.timerID = setInterval(checkStatus, 60000); 151 | } 152 | 153 | componentWillUnmount() { 154 | clearInterval(this.timerID); 155 | } 156 | 157 | handleUpdateClick = e => { 158 | e.preventDefault(); 159 | this.props.onTriggerUpdate(); 160 | } 161 | 162 | render() { 163 | if (this.state.updateAvailable) { 164 | return
165 |

There is an update of the component library available.

166 | 170 |
171 | } 172 | else { 173 | return
174 |

The component database is up to-date {this.state.lastUpdate ? `(${this.state.lastUpdate})` : ""}.

175 |
176 | } 177 | } 178 | } 179 | 180 | class Updater extends React.Component { 181 | constructor(props) { 182 | super(props); 183 | this.state = { 184 | progress: {} 185 | }; 186 | } 187 | 188 | componentDidMount() { 189 | let t0 = performance.now(); 190 | updateComponentLibrary( 191 | progress => { this.setState({progress}); } 192 | ).then(() => { 193 | let t1 = performance.now(); 194 | console.log("Library update took ", t1 - t0, "ms"); 195 | this.props.onFinish(); 196 | }); 197 | } 198 | 199 | listItems() { 200 | let items = [] 201 | for (const [task, status] of Object.entries(this.state.progress)) { 202 | let color = status[1] ? "bg-green-500" : "bg-yellow-400"; 203 | items.push( 204 | {task} 205 | {status[0]} 206 | ) 207 | } 208 | return items; 209 | } 210 | 211 | render() { 212 | return
213 |

Update progress:

214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | {this.listItems()} 223 | 224 |
Operation/categoryProgress
225 |
226 | } 227 | } 228 | 229 | function Container(props) { 230 | return
{props.children}
231 | } 232 | 233 | function Navbar() { 234 | return
235 | 238 | Component search 239 | 240 | 243 | Catalog history 244 | 245 |
246 | } 247 | 248 | export function NoMatch() { 249 | return

404 not found

; 250 | } 251 | 252 | class App extends React.Component { 253 | constructor(props) { 254 | super(props); 255 | this.state = { 256 | updating: false 257 | }; 258 | } 259 | 260 | onUpdateFinish = () => { 261 | this.setState({updating: false}); 262 | } 263 | 264 | triggerUpdate = () => { 265 | this.setState({updating: true}); 266 | } 267 | 268 | render() { 269 | if (this.state.updating) { 270 | return 271 | 272 | 273 | } 274 | return ( 275 | 276 | 277 | 278 |
279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 |