Skip to content

Commit 378c21a

Browse files
authored
Merge pull request #1108 from hackclub/onboard_gallery
OnBoard gallery
2 parents 0da52d8 + 5b2616c commit 378c21a

File tree

10 files changed

+1807
-8
lines changed

10 files changed

+1807
-8
lines changed

components/onboard/item.js

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import {Box, Divider, Flex, Heading, Image, Paragraph} from "theme-ui";
2+
import {Link} from "theme-ui";
3+
import React, {useContext} from "react";
4+
import {OBJECT} from "swr/_internal";
5+
6+
function trim(str) {
7+
return str.substring(1, str.length - 1)
8+
}
9+
10+
const onboardContext = React.createContext({})
11+
12+
const Item = ({ title, author_name, author_slack, image, project }) => {
13+
//const { projectCtx, setProjectCtx } = React.useContext(onboardContext)
14+
return (
15+
<Box
16+
sx={{
17+
bg: '#ffffff',
18+
color: 'black',
19+
borderRadius: 8,
20+
boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)',
21+
p: 4,
22+
mt: 4,
23+
position: 'relative'
24+
}}
25+
>
26+
<Flex
27+
sx={{
28+
flexDirection: 'column',
29+
alignItems: 'center'
30+
}}
31+
>
32+
<object
33+
data={image}
34+
type={'image/svg+xml'}
35+
style={{
36+
width: '100%',
37+
borderRadius: '8px'
38+
}}
39+
></object>
40+
<Link
41+
href={`/onboard/board/${project.project_name}`}
42+
sx={{
43+
textDecoration: 'none',
44+
color: 'black',
45+
':hover': {
46+
color: 'primary'
47+
}
48+
}}
49+
>
50+
<Heading
51+
as="h2"
52+
//variant="title"
53+
sx={{
54+
textAlign: 'center',
55+
mt: 3
56+
}}
57+
>
58+
{title}
59+
</Heading>
60+
</Link>
61+
<Paragraph
62+
sx={{
63+
textAlign: 'center',
64+
mt: 2,
65+
wordBreak: 'break-word'
66+
}}
67+
>
68+
{`${author_name ? `by ${trim(author_name)}` : ""} ${author_slack ? `(${trim(author_slack)})` : ""}`}
69+
</Paragraph>
70+
</Flex>
71+
</Box>
72+
)
73+
}
74+
75+
export default Item;
76+
export { onboardContext };

next.config.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,15 @@ const nextConfig = {
288288
},
289289
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' }
290290
]
291+
},
292+
{
293+
source: '/api/board/svg/(.+)',
294+
headers: [
295+
{
296+
key: 'content-type',
297+
value: 'image/svg+xml'
298+
}
299+
]
291300
}
292301
]
293302
}

package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
"@octokit/auth-app": "^6.0.1",
2626
"@octokit/core": "^5.1.0",
2727
"@octokit/rest": "^20.0.2",
28+
"@tracespace/core": "^5.0.0-alpha.0",
29+
"@tracespace/identify-layers": "^5.0.0-alpha.0",
30+
"@tracespace/parser": "^5.0.0-next.0",
31+
"@tracespace/plotter": "^5.0.0-alpha.0",
32+
"@tracespace/renderer": "^5.0.0-alpha.0",
33+
"@tracespace/xml-id": "^4.2.7",
2834
"@sendgrid/mail": "^8.1.1",
2935
"add": "^2.0.6",
3036
"airtable-plus": "^1.0.4",
@@ -43,10 +49,14 @@
4349
"globby": "^11.0.4",
4450
"graphql": "^16.8.1",
4551
"js-confetti": "^0.12.0",
52+
"jszip": "^3.10.1",
53+
"jszip-utils": "^0.1.0",
4654
"lodash": "^4.17.21",
4755
"million": "^2.6.4",
4856
"next": "^12.3.1",
4957
"next-transpile-modules": "^10.0.1",
58+
"nextjs-current-url": "^1.0.3",
59+
"pcb-stackup": "^4.2.8",
5060
"react": "^17.0.2",
5161
"react-before-after-slider-component": "^1.1.8",
5262
"react-datepicker": "^4.24.0",
@@ -58,6 +68,7 @@
5868
"react-page-visibility": "^7.0.0",
5969
"react-relative-time": "^0.0.9",
6070
"react-reveal": "^1.2.2",
71+
"react-router-dom": "^6.22.3",
6172
"react-scrolllock": "^5.0.1",
6273
"react-snowfall": "^1.2.1",
6374
"react-ticker": "^1.3.2",
@@ -66,6 +77,8 @@
6677
"react-use-websocket": "^4.7.0",
6778
"react-wrap-balancer": "^1.1.0",
6879
"recharts": "2.12.2",
80+
"remark": "^15.0.1",
81+
"remark-html": "^16.0.1",
6982
"styled-components": "^6.1.8",
7083
"swr": "^2.2.4",
7184
"theme-ui": "^0.14",

pages/api/board/[name].js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import {gerberToSvg} from "./svg/[board_url]";
2+
3+
export const FetchProject = async (name) => {
4+
const readme = await fetch(`https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${name}/README.md`)
5+
const text = await readme.text()
6+
// parse YAML frontmatter
7+
const lines = text.split('\n')
8+
const frontmatter = {}
9+
let i = 0
10+
for (; i < lines.length; i++) {
11+
if (lines[i].startsWith('---')) {
12+
break
13+
}
14+
}
15+
for (i++; i < lines.length; i++) {
16+
if (lines[i].startsWith('---')) {
17+
break
18+
}
19+
const [key, value] = lines[i].split(': ')
20+
frontmatter[key] = value
21+
}
22+
// check for a "thumbnail.png" file in the project directory
23+
//console.log(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`)
24+
/*const thumbnail = await fetch(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`, {mode: 'no-cors'})*/
25+
/*console.log(thumbnail)*/
26+
const image = /*thumbnail.ok ? `https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`
27+
:*/ /*`data:image/svg+xml;base64,${btoa((await gerberToSvg(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/gerber.zip`)).top)}`*/
28+
`/api/board/svg/${encodeURIComponent(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/gerber.zip`)}/top`
29+
console.log("done")
30+
return({
31+
project_name: name ?? null,
32+
maker_name: frontmatter.name ?? null,
33+
slack_handle: frontmatter.slack_handle ?? null,
34+
github_handle: frontmatter.github_handle ?? null,
35+
tutorial: frontmatter.tutorial ?? null,
36+
description: lines.slice(i + 1).join('\n') ?? null,
37+
image: image ?? null
38+
})
39+
}
40+
41+
export default async function handler(req, res) {
42+
const { name } = req.query
43+
if (!name) {
44+
return res.status(400).json({ status: 400, error: 'Must provide name' })
45+
}
46+
const project = await FetchProject(name)
47+
if (!project) {
48+
return res.status(404).json({ status: 404, error: 'Project not found' })
49+
}
50+
return res.status(200).json(project)
51+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip/bottom
2+
3+
import { gerberToSvg } from '.'
4+
5+
export default async function handler(req, res) {
6+
const { board_url } = req.query
7+
if (!board_url) {
8+
return res.status(404).json({ status: 404, error: 'Must provide file' })
9+
}
10+
// ensure valid file url is included
11+
const parsed_url = new URL(decodeURI(board_url))
12+
if (!parsed_url) {
13+
return res.status(404).json({ status: 404, error: 'Invalid file' })
14+
}
15+
const svg = await gerberToSvg(parsed_url)
16+
return res.status(200).send(svg.bottom)
17+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import JSZip from 'jszip'
2+
import {
3+
read,
4+
plot,
5+
renderLayers,
6+
renderBoard,
7+
stringifySvg
8+
} from '@tracespace/core'
9+
import fs from 'fs'
10+
11+
export const gerberToSvg = async gerberURL => {
12+
const data = await fetch(gerberURL).then(res => res.arrayBuffer())
13+
const files = []
14+
const zip = new JSZip()
15+
16+
const zippedData = await new Promise((resolve, _reject) => {
17+
zip.loadAsync(data).then(resolve, e => {
18+
console.error(e)
19+
resolve({
20+
files: {} // TODO: actually handle this error (bad or nonexistent gerber.zip)
21+
})
22+
})
23+
})
24+
25+
const allowedExtensions = [
26+
'gbr', // gerber
27+
'drl', // drillfile
28+
'gko', // gerber board outline
29+
'gbl', // gerber bottom layer
30+
'gbp', // gerber bottom paste
31+
'gbs', // gerber bottom solder mask
32+
'gbo', // gerber bottom silk
33+
'gtl', // gerber top layer
34+
'gto', // gerber top silk
35+
'gts' // gerber top soldermask
36+
]
37+
const unzipJobs = Object.entries(zippedData.files).map(
38+
async ([filename, file]) => {
39+
const extension = filename.split('.').pop().toLowerCase()
40+
if (allowedExtensions.includes(extension)) {
41+
const filePath = `/tmp/${filename}`
42+
await new Promise((resolve, _reject) => {
43+
file.async('uint8array').then(function (fileData) {
44+
fs.writeFileSync(filePath, fileData)
45+
files.push(filePath)
46+
resolve()
47+
})
48+
})
49+
}
50+
}
51+
)
52+
53+
await Promise.all(unzipJobs)
54+
55+
let readResult
56+
try {
57+
readResult = await read(files)
58+
} catch (e) {
59+
console.error(e)
60+
return {}
61+
}
62+
const plotResult = plot(readResult)
63+
const renderLayersResult = renderLayers(plotResult)
64+
const renderBoardResult = renderBoard(renderLayersResult)
65+
for (const file of files) {
66+
if (fs.existsSync(file)) {
67+
fs.unlinkSync(file)
68+
}
69+
}
70+
return {
71+
top: stringifySvg(renderBoardResult.top),
72+
bottom: stringifySvg(renderBoardResult.bottom)
73+
// all: stringifySvg(renderLayersResult)
74+
}
75+
}
76+
77+
export default async function handler(req, res) {
78+
const { file, format } = req.query
79+
if (!file) {
80+
return res.status(400).json({ status: 400, error: 'Must provide file' })
81+
}
82+
// ensure valid file url is included
83+
const url = new URL(decodeURI(file))
84+
const svg = await gerberToSvg(url)
85+
if (format === 'top') {
86+
res.contentType('image/svg')
87+
return res.status(200).send(svg.top)
88+
}
89+
if (format === 'json') return res.status(200).json(svg)
90+
91+
return res.status(200).json(svg)
92+
}
93+
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
// test me with: curl http://localhost:3000/api/board/svg/https%3A%2F%2Fgithub.com%2Fhackclub%2FOnBoard%2Fraw%2Fmain%2Fprojects%2F2_Switch_Keyboard%2Fgerber.zip/top
2+
3+
import { gerberToSvg } from '.'
4+
5+
export default async function handler(req, res) {
6+
const { board_url } = req.query
7+
if (!board_url) {
8+
return res.status(404).json({ status: 404, error: 'Must provide file' })
9+
}
10+
// ensure valid file url is included
11+
const parsed_url = new URL(decodeURI(board_url))
12+
if (!parsed_url) {
13+
return res.status(404).json({ status: 404, error: 'Invalid file' })
14+
}
15+
const svg = await gerberToSvg(parsed_url)
16+
return res.status(200).send(svg.top)
17+
}

0 commit comments

Comments
 (0)