diff --git a/frontend/next.config.js b/frontend/next.config.js index 301857f516..efc28620ce 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -65,7 +65,12 @@ module.exports = async phase => { source: '/:user/:repo/_', destination: '/:user/:repo', permanent: true, - } + }, + { + source: '/:user/:repo/1-click-BOM.tsv', + destination: '/:user/:repo/_/1-click-BOM.tsv', + permanent: true, + }, ] }, async rewrites() { diff --git a/frontend/src/components/Board/BuyParts/index.jsx b/frontend/src/components/Board/BuyParts/index.jsx index 4f2adeae60..6e6d84be83 100644 --- a/frontend/src/components/Board/BuyParts/index.jsx +++ b/frontend/src/components/Board/BuyParts/index.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useRef, 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' @@ -9,17 +9,71 @@ import DirectStores from './DirectStores' import styles from './index.module.scss' const BuyParts = ({ projectFullName, lines, parts }) => { - const [extensionPresence, setExtensionPresence] = useState('unknown') - // it's needed to fix the extension integration. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const [buyParts, setBuyParts] = useState(null) + const extensionPresence = useRef('unknown') + const [buyParts, setBuyParts] = useState(() => install1ClickBOM) const [buyMultiplier, setBuyMultiplier] = useState(1) const [mult, setMult] = useState(1) const [buyAddPercent, setBuyAddPercent] = useState(0) const [adding, setAdding] = useState({}) - const retailerList = OneClickBom.getRetailers() - const retailerButtons = retailerList + 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]) + + useEffect(() => { + const handleExtensionMessages = event => { + if (event.source !== window) { + return + } + + if (event.data.from === 'extension') { + extensionPresence.current = 'present' + switch (event.data.message) { + case 'register': + setBuyParts(() => retailer => { + window.plausible('Buy Parts', { + props: { + project: projectFullName, + vendor: retailer, + multiplier: mult, + }, + }) + window.postMessage( + { + from: 'page', + message: 'quickAddToCart', + value: { + retailer, + multiplier: mult, + }, + }, + '*', + ) + }) + break + case 'updateAddingState': + setAdding(event.data.value) + break + default: + break + } + } + } + + window.addEventListener('message', handleExtensionMessages) + + return () => window.removeEventListener('message', handleExtensionMessages) + }, [mult, projectFullName]) + + const retailerButtons = OneClickBom.getRetailers() .map(name => { const [numberOfLines, numberOfParts] = lines.reduce( ([numOfLines, numOfParts], line) => { @@ -35,8 +89,10 @@ const BuyParts = ({ projectFullName, lines, parts }) => { install1ClickBOM()} - extensionPresence={name === 'Digikey' ? 'absent' : extensionPresence} + buyParts={() => buyParts(name)} + extensionPresence={ + name === 'Digikey' ? 'absent' : extensionPresence.current + } name={name} numberOfLines={numberOfLines} numberOfParts={numberOfParts} @@ -48,63 +104,6 @@ const BuyParts = ({ projectFullName, lines, parts }) => { }) .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 'register': - setBuyParts(retailer => { - window.plausible('Buy Parts', { - props: { - project: projectFullName, - vendor: retailer, - multiplier: mult, - }, - }) - window.postMessage( - { - from: 'page', - message: 'quickAddToCart', - value: { - retailer, - multiplier: mult, - }, - }, - '*', - ) - }) - break - case 'updateAddingState': - setAdding(event.data.value) - break - default: - break - } - } - }, - false, - ) - }, [mult, projectFullName]) - - 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, @@ -121,7 +120,7 @@ const BuyParts = ({ projectFullName, lines, parts }) => { {hasPurchasableParts ? ( <> - + { + url = new URL(url) + url.protocol = 'http:' + return url.toString().slice(0, -1) // remove the trailing slash +} + +const No_SSL_KITSPACE_PROCESSOR_URL = sslWorkaround( + process.env.KITSPACE_PROCESSOR_URL, +) + +/* + * Make the 1-click-bom.tsv file accessible to the 1-click-bom extension. + * We can't use the following snippet `next.config.js` redirects: it generates the redirect URLs during the build time of the container. + * But the KITSPACE_PROCESSOR_URL is only available at runtime. + { + source: '/:user/:repo/:project/1-click-BOM.tsv', + destination: `${process.env.KITSPACE_PROCESSOR_URL}/files/:user/:repo/HEAD/:project/1-click-BOM.tsv`, + permanent: true, + } +*/ +export function middleware(request) { + // We are using the pattern because simply using ':user/:repo/:project/1-click-BOM.tsv' matches `/static/` files as well. + const matches = request.nextUrl.pathname.match( + /^\/(?.+)\/(?.+)\/(?.+)\/(?:1-click-BOM.tsv)$/, + ) + if (matches) { + const { user, repo, project } = matches.groups + return NextResponse.redirect( + `${No_SSL_KITSPACE_PROCESSOR_URL}/files/${user}/${repo}/HEAD/${project}/1-click-BOM.tsv`, + ) + } +} diff --git a/frontend/src/server.js b/frontend/src/server.js index c6e7652bf8..bc2d36901b 100644 --- a/frontend/src/server.js +++ b/frontend/src/server.js @@ -5,8 +5,9 @@ const next = require('next') const fetch = require('node-fetch') const port = parseInt(process.env.PORT, 10) || 3000 const dev = process.env.NODE_ENV !== 'production' +const hostname = process.env.KITSPACE_DOMAIN -const app = next({ dev, port }) +const app = next({ dev, port, hostname }) const nextHandler = app.getRequestHandler() main().catch(e => {