Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1clickBOM extension integration #224

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/next.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
133 changes: 66 additions & 67 deletions frontend/src/components/Board/BuyParts/index.jsx
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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) => {
Expand All @@ -35,8 +89,10 @@ const BuyParts = ({ projectFullName, lines, parts }) => {
<RetailerButton
key={name}
adding={adding[name]}
buyParts={() => install1ClickBOM()}
extensionPresence={name === 'Digikey' ? 'absent' : extensionPresence}
buyParts={() => buyParts(name)}
extensionPresence={
name === 'Digikey' ? 'absent' : extensionPresence.current
}
name={name}
numberOfLines={numberOfLines}
numberOfParts={numberOfParts}
Expand All @@ -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,
Expand All @@ -121,7 +120,7 @@ const BuyParts = ({ projectFullName, lines, parts }) => {
</Header>
{hasPurchasableParts ? (
<>
<InstallPrompt extensionPresence={extensionPresence} />
<InstallPrompt extensionPresence={extensionPresence.current} />
<AdjustQuantity
buyAddPercent={buyAddPercent}
buyMultiplier={buyMultiplier}
Expand Down
37 changes: 37 additions & 0 deletions frontend/src/pages/_middleware.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// middleware.ts
import { NextResponse } from 'next/server'

// The redirection between the different proxied services corrupts SSL somehow.
// As a workaround we redirect using http:// and nginx will automatically upgrade it to https://.
const sslWorkaround = url => {
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(
/^\/(?<user>.+)\/(?<repo>.+)\/(?<project>.+)\/(?: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`,
)
}
}
3 changes: 2 additions & 1 deletion frontend/src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down