diff --git a/frontend/src/components/Board/BuyParts/Bom.module.scss b/frontend/src/components/Board/BuyParts/Bom.module.scss
index f6e9123d02..fa8f5f81d5 100644
--- a/frontend/src/components/Board/BuyParts/Bom.module.scss
+++ b/frontend/src/components/Board/BuyParts/Bom.module.scss
@@ -14,4 +14,18 @@
font-size: 14px !important;
background-color: white !important;
}
+
+ .expandWrapper {
+ padding-left: 1px;
+ padding-right: 1px;
+ }
+
+ .expandTable {
+ border-top: 0px;
+ cursor: pointer;
+ }
+
+ .expandTableRow {
+ border-top: 0px;
+ }
}
diff --git a/frontend/src/components/Board/BuyParts/Bom.jsx b/frontend/src/components/Board/BuyParts/Bom.tsx
similarity index 76%
rename from frontend/src/components/Board/BuyParts/Bom.jsx
rename to frontend/src/components/Board/BuyParts/Bom.tsx
index 955942b56f..faa56b71c0 100644
--- a/frontend/src/components/Board/BuyParts/Bom.jsx
+++ b/frontend/src/components/Board/BuyParts/Bom.tsx
@@ -2,11 +2,10 @@ import React, { useEffect, useState } from 'react'
import { Icon, Table } from 'semantic-ui-react'
import DoubleScrollBar from 'react-double-scrollbar'
-import { array, bool, func, number, string } from 'prop-types'
import styles from './Bom.module.scss'
import TsvTable from './TsvTable'
-const Bom = ({ length, parts, tsv }) => {
+const Bom = ({ length, parts, tsv }: BomProps) => {
const [diff, setDiff] = useState(0)
const [collapsed, setCollapsed] = useState(true)
@@ -32,7 +31,7 @@ const Bom = ({ length, parts, tsv }) => {
)
}
-const ExpandBom = ({ diff, collapsed, setCollapsed }) => {
+const ExpandBom = ({ diff, collapsed, setCollapsed }: ExpandBomProps) => {
const summary =
diff > 0 && collapsed ? (
@@ -44,13 +43,13 @@ const ExpandBom = ({ diff, collapsed, setCollapsed }) => {
) : null
return (
-
+
{
>
{summary}
-
+
{collapsed ? 'View all' : 'Hide'}
@@ -71,16 +70,16 @@ const ExpandBom = ({ diff, collapsed, setCollapsed }) => {
)
}
-Bom.propTypes = {
- length: number.isRequired,
- parts: array.isRequired,
- tsv: string.isRequired,
+interface BomProps {
+ length: number
+ parts: Array
+ tsv: string
}
-ExpandBom.propTypes = {
- diff: number.isRequired,
- collapsed: bool.isRequired,
- setCollapsed: func.isRequired,
+interface ExpandBomProps {
+ diff: number
+ collapsed: boolean
+ setCollapsed: (collapsed: boolean) => void
}
export default Bom
diff --git a/frontend/src/components/Board/BuyParts/DirectStores.jsx b/frontend/src/components/Board/BuyParts/DirectStores.jsx
deleted file mode 100644
index bc3fde2579..0000000000
--- a/frontend/src/components/Board/BuyParts/DirectStores.jsx
+++ /dev/null
@@ -1,139 +0,0 @@
-import React, { useEffect, useState, useCallback } from 'react'
-
-import { array, number } from 'prop-types'
-import DigikeyData from '1-click-bom-minimal/lib/data/digikey.json'
-import FarnellData from '1-click-bom-minimal/lib/data/farnell.json'
-import countriesData from '1-click-bom-minimal/lib/data/countries.json'
-
-const DirectStores = ({ items, multiplier }) => {
- const [countryCode, setCountryCode] = useState('Other')
- const [digikeyParts, setDigikeyParts] = useState([])
- const [farnellParts, setFarnellParts] = useState([])
- const [newarkParts, setNewarkParts] = useState([])
-
- const getParts = useCallback(
- retailer =>
- items
- .filter(
- part => retailer in part.retailers && part.retailers[retailer] !== '',
- )
- .map(part => ({
- sku: part.retailers[retailer],
- reference: part.reference,
- quantity: Math.ceil(multiplier * part.quantity),
- })),
- [items, multiplier],
- )
-
- const getLocation = async () => {
- const usedCountryCodes = Object.keys(countriesData).map(
- key => countriesData[key],
- )
- const freegeoipEndpoint = 'https://freegeoip.kitspace.org'
-
- try {
- const res = await fetch(freegeoipEndpoint)
- const body = await res.json()
- const { country_code: code } = body
- if (code === 'GB') {
- return 'UK'
- }
- if (usedCountryCodes.indexOf(code) < 0) {
- return 'Other'
- }
- return code
- } catch (err) {
- console.error(err)
- return 'Other'
- }
- }
-
- useEffect(() => {
- if (typeof window !== 'undefined') {
- getLocation().then(code => setCountryCode(code))
- }
-
- setDigikeyParts(getParts('Digikey'))
- setFarnellParts(getParts('Farnell'))
- setNewarkParts(getParts('Newark'))
- }, [getParts])
-
- const tildeDelimiter = part => `${part.sku}~${part.quantity}`
-
- const digikeyPartRenderer = (part, index) => {
- index += 1
- return (
-
-
-
-
-
- )
- }
-
- const digikey = (code, parts) => {
- const site = DigikeyData.sites[DigikeyData.lookup[code]]
- return (
-
- )
- }
-
- const farnell = (code, parts) => {
- const site = FarnellData.sites[FarnellData.lookup[code]]
- const queryString = parts.map(tildeDelimiter).join('~')
- return (
-
- )
- }
-
- const newark = parts => {
- const queryString = parts.map(tildeDelimiter).join('~')
- return (
-
- )
- }
-
- return (
-
- {[
- digikey(countryCode, digikeyParts),
- farnell(countryCode, farnellParts),
- newark(newarkParts),
- ]}
-
- )
-}
-
-DirectStores.propTypes = {
- items: array.isRequired,
- multiplier: number.isRequired,
-}
-export default DirectStores
diff --git a/frontend/src/components/Board/BuyParts/DirectStores.tsx b/frontend/src/components/Board/BuyParts/DirectStores.tsx
new file mode 100644
index 0000000000..e885c20a53
--- /dev/null
+++ b/frontend/src/components/Board/BuyParts/DirectStores.tsx
@@ -0,0 +1,96 @@
+import React, { useEffect, useState, useCallback } from 'react'
+
+import DigikeyData from '1-click-bom-minimal/lib/data/digikey.json'
+import countriesData from '1-click-bom-minimal/lib/data/countries.json'
+
+const DirectStores = ({ items, multiplier }: DirectStoresProps) => {
+ const [countryCode, setCountryCode] = useState('Other')
+ const [digikeyParts, setDigikeyParts] = useState([])
+
+ const getParts = useCallback(
+ retailer =>
+ items
+ .filter(
+ part => retailer in part.retailers && part.retailers[retailer] !== '',
+ )
+ .map(part => ({
+ sku: part.retailers[retailer],
+ reference: part.reference,
+ quantity: Math.ceil(multiplier * part.quantity),
+ })),
+ [items, multiplier],
+ )
+
+ useEffect(() => {
+ const abortController = new AbortController()
+ const signal = abortController.signal
+ if (typeof window !== 'undefined') {
+ getLocation(signal).then(code => {
+ if (code && !signal.aborted) {
+ setCountryCode(code)
+ }
+ })
+ }
+ setDigikeyParts(getParts('Digikey'))
+ return () => {
+ abortController.abort()
+ }
+ }, [getParts])
+
+ const digikeyPartRenderer = (part, index) => {
+ index += 1
+ return (
+
+
+
+
+
+ )
+ }
+
+ const digikey = (code, parts) => {
+ const site = DigikeyData.sites[DigikeyData.lookup[code]]
+ return (
+
+ )
+ }
+
+ return {[digikey(countryCode, digikeyParts)]}
+}
+
+const getLocation = async (signal: AbortSignal) => {
+ const usedCountryCodes = Object.keys(countriesData).map(key => countriesData[key])
+ const freegeoipEndpoint = 'https://freegeoip.kitspace.org'
+
+ try {
+ const res = await fetch(freegeoipEndpoint, { signal })
+ const body = await res.json()
+ const { country_code: code } = body
+ if (code === 'GB') {
+ return 'UK'
+ }
+ if (usedCountryCodes.indexOf(code) < 0) {
+ return 'Other'
+ }
+ return code
+ } catch (err) {
+ if (!signal.aborted) {
+ console.error(err)
+ }
+ return 'Other'
+ }
+}
+
+interface DirectStoresProps {
+ items: Array
+ multiplier: number
+}
+export default DirectStores
diff --git a/frontend/src/components/Board/BuyParts/InstallPrompt.jsx b/frontend/src/components/Board/BuyParts/InstallPrompt.jsx
deleted file mode 100644
index 13546be102..0000000000
--- a/frontend/src/components/Board/BuyParts/InstallPrompt.jsx
+++ /dev/null
@@ -1,105 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import Link from 'next/link'
-
-import { Icon, Message } from 'semantic-ui-react'
-import { func, string } from 'prop-types'
-
-import BrowserVersion from '@utils/getBrowserVersion'
-import styles from './InstallPrompt.module.scss'
-
-export const install1ClickBOM = () => {
- const version = BrowserVersion()
- let onClick
- if (/Chrome/.test(version)) {
- onClick = () => {
- window.plausible('Install Extension')
- window.open(
- 'https://chrome.google.com/webstore/detail/kitspace-1-click-bom/mflpmlediakefinapghmabapjeippfdi',
- )
- }
- } else if (/Firefox/.test(version)) {
- onClick = () => {
- window.plausible('Install Extension')
- window.open('https://addons.mozilla.org/en-US/firefox/addon/1clickbom')
- }
- } else {
- onClick = () => {
- window.plausible('Install Extension')
- window.open('/1-click-bom', '_self')
- }
- }
- return onClick()
-}
-
-const InstallPrompt = ({ extensionPresence }) => {
- const [isCompatible, setIsCompatible] = useState(true)
- const [timedOut, setTimedOut] = useState(false)
-
- const getCompatibility = () => {
- if (typeof navigator === 'undefined') {
- return true
- }
- if (/Mobile/i.test(navigator.userAgent)) {
- return false
- }
- const version = BrowserVersion()
- return /Chrome/.test(version) || /Firefox/.test(version)
- }
-
- useEffect(() => {
- setTimedOut(() => {
- setTimedOut(true)
- setIsCompatible(getCompatibility())
- }, 5000)
- }, [])
-
- if (extensionPresence === 'present') {
- return
- }
- if (timedOut) {
- return isCompatible ? (
-
- ) : (
-
- )
- }
- return null
-}
-
-const PleaseInstall = ({ install1ClickBOMCallback }) => (
-
-
- Please{' '}
- {' '}
- make full use of this feature.
-
-)
-
-const NotCompatible = () => (
-
-
- Sorry, the{' '}
-
- 1-click BOM extension
- {' '}
- is not yet available for your browser. Only the Digikey add-to-cart links work
- fully, Farnell and Newark should work but the references will not be added as
- line-notes.
-
-)
-
-InstallPrompt.propTypes = {
- extensionPresence: string.isRequired,
-}
-
-PleaseInstall.propTypes = {
- install1ClickBOMCallback: func.isRequired,
-}
-
-export default InstallPrompt
diff --git a/frontend/src/components/Board/BuyParts/InstallPrompt.module.scss b/frontend/src/components/Board/BuyParts/InstallPrompt.module.scss
deleted file mode 100644
index 7345ea0159..0000000000
--- a/frontend/src/components/Board/BuyParts/InstallPrompt.module.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import '../../colors.scss';
-
-.extensionLinkButton {
- background: none!important;
- border: none;
- padding: 0!important;
- color:$mine-shaft;
- font-weight: bold;
- cursor: pointer;
-}
-
-.extensionLinkButton:hover {
- text-decoration: underline;
-}
\ No newline at end of file
diff --git a/frontend/src/components/Board/BuyParts/index.jsx b/frontend/src/components/Board/BuyParts/index.jsx
deleted file mode 100644
index 1ddcbab8cf..0000000000
--- a/frontend/src/components/Board/BuyParts/index.jsx
+++ /dev/null
@@ -1,317 +0,0 @@
-import React, { useEffect, useState } from 'react'
-import { array, bool, func, number, string } from 'prop-types'
-import OneClickBom from '1-click-bom-minimal'
-import { Header, Icon, Segment, Input, Button } from 'semantic-ui-react'
-
-import Bom from './Bom'
-import InstallPrompt, { install1ClickBOM } from './InstallPrompt'
-import DirectStores from './DirectStores'
-import styles from './index.module.scss'
-
-const BuyParts = ({ projectFullName, lines, parts }) => {
- const [extensionPresence, setExtensionPresence] = useState('unknown')
- const [buyMultiplier, setBuyMultiplier] = useState(1)
- const [mult, setMult] = useState(1)
- const [buyAddPercent, setBuyAddPercent] = useState(0)
- const [adding, setAdding] = useState({})
-
- const buyParts = distributor => {
- window.plausible('Buy Parts', {
- props: {
- project: projectFullName,
- vendor: distributor,
- multiplier: mult,
- },
- })
- window.postMessage(
- {
- from: 'page',
- message: 'quickAddToCart',
- value: {
- retailer: distributor,
- multiplier: mult,
- },
- },
- '*',
- )
- }
-
- const retailerList = OneClickBom.getRetailers()
- const retailerButtons = retailerList
- .map(name => {
- const [numberOfLines, numberOfParts] = lines.reduce(
- ([numOfLines, numOfParts], line) => {
- if (line.retailers[name]) {
- return [numOfLines + 1, numOfParts + Math.ceil(mult * line.quantity)]
- }
- return [numOfLines, numOfParts]
- },
- [0, 0],
- )
- if (numberOfLines > 0) {
- return (
- buyParts(name)}
- extensionPresence={name === 'Digikey' ? 'absent' : extensionPresence}
- install1ClickBOM={install1ClickBOM}
- name={name}
- numberOfLines={numberOfLines}
- numberOfParts={numberOfParts}
- totalLines={lines.length}
- />
- )
- }
- return null
- })
- .filter(l => l != null)
-
- useEffect(() => {
- // extension communication
- window.addEventListener(
- 'message',
- event => {
- if (event.source !== window) {
- return
- }
- if (event.data.from === 'extension') {
- setExtensionPresence('present')
- switch (event.data.message) {
- case 'updateAddingState':
- setAdding(event.data.value)
- break
- default:
- break
- }
- }
- },
- false,
- )
- }, [])
-
- useEffect(() => {
- const multi = buyMultiplier
- if (Number.isNaN(multi) || multi < 1) {
- setMult(1)
- }
- const percent = buyAddPercent
- if (Number.isNaN(percent) || percent < 1) {
- setMult(0)
- }
- setMult(multi + multi * (percent / 100))
- }, [buyMultiplier, buyAddPercent])
-
- const linesToTsv = () => {
- const linesMult = lines.map(line => ({
- ...line,
- quantity: Math.ceil(line.quantity * mult),
- }))
- return OneClickBom.writeTSV(linesMult)
- }
- const hasPurchasableParts = retailerButtons.length !== 0
- return (
-
-
- {hasPurchasableParts ? (
- <>
-
-
setBuyAddPercent(v)}
- setBuyMultiplier={v => setBuyMultiplier(v)}
- />
-
- {retailerButtons}
-
-
- >
- ) : (
-
- )}
-
-
- )
-}
-
-const NoPurchasableParts = () => (
-
-
- No parts to buy have been specified in this project's BOM yet.
-
-
-)
-
-const AdjustQuantity = ({
- buyMultiplier,
- setBuyMultiplier,
- buyAddPercent,
- setBuyAddPercent,
-}) => (
-
- Adjust quantity:
-
- {
- const v = buyMultiplier
- if (Number.isNaN(v) || v < 1) {
- setBuyMultiplier(1)
- }
- }}
- onChange={e => {
- const v = parseFloat(e.target.value)
- setBuyMultiplier(v)
- }}
- />
-
- {
- const v = buyAddPercent
- if (Number.isNaN(v) || v < 0) {
- setBuyAddPercent(0)
- }
- }}
- onChange={e => {
- const v = parseFloat(e.target.value)
- setBuyAddPercent(v)
- }}
- />
-
- %
-
-
-)
-
-const RetailerButton = ({
- name,
- buyParts,
- install1ClickBOM,
- extensionPresence,
- numberOfLines,
- totalLines,
- numberOfParts,
- adding,
-}) => {
- let onClick = buyParts
- // if the extension is not here fallback to direct submissions
- if (extensionPresence !== 'present' && typeof document !== 'undefined') {
- onClick = () => {
- const form = document.getElementById(`${name}Form`)
- if (form) {
- form.submit()
- } else {
- install1ClickBOM()
- }
- }
- }
- const color = numberOfLines === totalLines ? 'green' : 'pink'
- return (
-