Merge pull request #1108 from hackclub/onboard_gallery

OnBoard gallery
This commit is contained in:
Max Wofford 2024-04-02 16:33:04 +00:00 committed by GitHub
commit 378c21aed4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 1807 additions and 8 deletions

View file

@ -0,0 +1,76 @@
import {Box, Divider, Flex, Heading, Image, Paragraph} from "theme-ui";
import {Link} from "theme-ui";
import React, {useContext} from "react";
import {OBJECT} from "swr/_internal";
function trim(str) {
return str.substring(1, str.length - 1)
}
const onboardContext = React.createContext({})
const Item = ({ title, author_name, author_slack, image, project }) => {
//const { projectCtx, setProjectCtx } = React.useContext(onboardContext)
return (
<Box
sx={{
bg: '#ffffff',
color: 'black',
borderRadius: 8,
boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)',
p: 4,
mt: 4,
position: 'relative'
}}
>
<Flex
sx={{
flexDirection: 'column',
alignItems: 'center'
}}
>
<object
data={image}
type={'image/svg+xml'}
style={{
width: '100%',
borderRadius: '8px'
}}
></object>
<Link
href={`/onboard/board/${project.project_name}`}
sx={{
textDecoration: 'none',
color: 'black',
':hover': {
color: 'primary'
}
}}
>
<Heading
as="h2"
//variant="title"
sx={{
textAlign: 'center',
mt: 3
}}
>
{title}
</Heading>
</Link>
<Paragraph
sx={{
textAlign: 'center',
mt: 2,
wordBreak: 'break-word'
}}
>
{`${author_name ? `by ${trim(author_name)}` : ""} ${author_slack ? `(${trim(author_slack)})` : ""}`}
</Paragraph>
</Flex>
</Box>
)
}
export default Item;
export { onboardContext };

View file

@ -288,6 +288,15 @@ const nextConfig = {
},
{ key: 'Access-Control-Allow-Headers', value: 'Content-Type' }
]
},
{
source: '/api/board/svg/(.+)',
headers: [
{
key: 'content-type',
value: 'image/svg+xml'
}
]
}
]
}

View file

@ -25,6 +25,12 @@
"@octokit/auth-app": "^6.0.1",
"@octokit/core": "^5.1.0",
"@octokit/rest": "^20.0.2",
"@tracespace/core": "^5.0.0-alpha.0",
"@tracespace/identify-layers": "^5.0.0-alpha.0",
"@tracespace/parser": "^5.0.0-next.0",
"@tracespace/plotter": "^5.0.0-alpha.0",
"@tracespace/renderer": "^5.0.0-alpha.0",
"@tracespace/xml-id": "^4.2.7",
"@sendgrid/mail": "^8.1.1",
"add": "^2.0.6",
"airtable-plus": "^1.0.4",
@ -43,10 +49,14 @@
"globby": "^11.0.4",
"graphql": "^16.8.1",
"js-confetti": "^0.12.0",
"jszip": "^3.10.1",
"jszip-utils": "^0.1.0",
"lodash": "^4.17.21",
"million": "^2.6.4",
"next": "^12.3.1",
"next-transpile-modules": "^10.0.1",
"nextjs-current-url": "^1.0.3",
"pcb-stackup": "^4.2.8",
"react": "^17.0.2",
"react-before-after-slider-component": "^1.1.8",
"react-datepicker": "^4.24.0",
@ -58,6 +68,7 @@
"react-page-visibility": "^7.0.0",
"react-relative-time": "^0.0.9",
"react-reveal": "^1.2.2",
"react-router-dom": "^6.22.3",
"react-scrolllock": "^5.0.1",
"react-snowfall": "^1.2.1",
"react-ticker": "^1.3.2",
@ -66,6 +77,8 @@
"react-use-websocket": "^4.7.0",
"react-wrap-balancer": "^1.1.0",
"recharts": "2.12.2",
"remark": "^15.0.1",
"remark-html": "^16.0.1",
"styled-components": "^6.1.8",
"swr": "^2.2.4",
"theme-ui": "^0.14",

51
pages/api/board/[name].js Normal file
View file

@ -0,0 +1,51 @@
import {gerberToSvg} from "./svg/[board_url]";
export const FetchProject = async (name) => {
const readme = await fetch(`https://raw.githubusercontent.com/hackclub/OnBoard/main/projects/${name}/README.md`)
const text = await readme.text()
// parse YAML frontmatter
const lines = text.split('\n')
const frontmatter = {}
let i = 0
for (; i < lines.length; i++) {
if (lines[i].startsWith('---')) {
break
}
}
for (i++; i < lines.length; i++) {
if (lines[i].startsWith('---')) {
break
}
const [key, value] = lines[i].split(': ')
frontmatter[key] = value
}
// check for a "thumbnail.png" file in the project directory
//console.log(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`)
/*const thumbnail = await fetch(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`, {mode: 'no-cors'})*/
/*console.log(thumbnail)*/
const image = /*thumbnail.ok ? `https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/thumbnail.png`
:*/ /*`data:image/svg+xml;base64,${btoa((await gerberToSvg(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/gerber.zip`)).top)}`*/
`/api/board/svg/${encodeURIComponent(`https://github.com/snoglobe/OnBoard/raw/main/projects/${name}/gerber.zip`)}/top`
console.log("done")
return({
project_name: name ?? null,
maker_name: frontmatter.name ?? null,
slack_handle: frontmatter.slack_handle ?? null,
github_handle: frontmatter.github_handle ?? null,
tutorial: frontmatter.tutorial ?? null,
description: lines.slice(i + 1).join('\n') ?? null,
image: image ?? null
})
}
export default async function handler(req, res) {
const { name } = req.query
if (!name) {
return res.status(400).json({ status: 400, error: 'Must provide name' })
}
const project = await FetchProject(name)
if (!project) {
return res.status(404).json({ status: 404, error: 'Project not found' })
}
return res.status(200).json(project)
}

View file

@ -0,0 +1,17 @@
// 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
import { gerberToSvg } from '.'
export default async function handler(req, res) {
const { board_url } = req.query
if (!board_url) {
return res.status(404).json({ status: 404, error: 'Must provide file' })
}
// ensure valid file url is included
const parsed_url = new URL(decodeURI(board_url))
if (!parsed_url) {
return res.status(404).json({ status: 404, error: 'Invalid file' })
}
const svg = await gerberToSvg(parsed_url)
return res.status(200).send(svg.bottom)
}

View file

@ -0,0 +1,93 @@
import JSZip from 'jszip'
import {
read,
plot,
renderLayers,
renderBoard,
stringifySvg
} from '@tracespace/core'
import fs from 'fs'
export const gerberToSvg = async gerberURL => {
const data = await fetch(gerberURL).then(res => res.arrayBuffer())
const files = []
const zip = new JSZip()
const zippedData = await new Promise((resolve, _reject) => {
zip.loadAsync(data).then(resolve, e => {
console.error(e)
resolve({
files: {} // TODO: actually handle this error (bad or nonexistent gerber.zip)
})
})
})
const allowedExtensions = [
'gbr', // gerber
'drl', // drillfile
'gko', // gerber board outline
'gbl', // gerber bottom layer
'gbp', // gerber bottom paste
'gbs', // gerber bottom solder mask
'gbo', // gerber bottom silk
'gtl', // gerber top layer
'gto', // gerber top silk
'gts' // gerber top soldermask
]
const unzipJobs = Object.entries(zippedData.files).map(
async ([filename, file]) => {
const extension = filename.split('.').pop().toLowerCase()
if (allowedExtensions.includes(extension)) {
const filePath = `/tmp/${filename}`
await new Promise((resolve, _reject) => {
file.async('uint8array').then(function (fileData) {
fs.writeFileSync(filePath, fileData)
files.push(filePath)
resolve()
})
})
}
}
)
await Promise.all(unzipJobs)
let readResult
try {
readResult = await read(files)
} catch (e) {
console.error(e)
return {}
}
const plotResult = plot(readResult)
const renderLayersResult = renderLayers(plotResult)
const renderBoardResult = renderBoard(renderLayersResult)
for (const file of files) {
if (fs.existsSync(file)) {
fs.unlinkSync(file)
}
}
return {
top: stringifySvg(renderBoardResult.top),
bottom: stringifySvg(renderBoardResult.bottom)
// all: stringifySvg(renderLayersResult)
}
}
export default async function handler(req, res) {
const { file, format } = req.query
if (!file) {
return res.status(400).json({ status: 400, error: 'Must provide file' })
}
// ensure valid file url is included
const url = new URL(decodeURI(file))
const svg = await gerberToSvg(url)
if (format === 'top') {
res.contentType('image/svg')
return res.status(200).send(svg.top)
}
if (format === 'json') return res.status(200).json(svg)
return res.status(200).json(svg)
}
// 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

View file

@ -0,0 +1,17 @@
// 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
import { gerberToSvg } from '.'
export default async function handler(req, res) {
const { board_url } = req.query
if (!board_url) {
return res.status(404).json({ status: 404, error: 'Must provide file' })
}
// ensure valid file url is included
const parsed_url = new URL(decodeURI(board_url))
if (!parsed_url) {
return res.status(404).json({ status: 404, error: 'Invalid file' })
}
const svg = await gerberToSvg(parsed_url)
return res.status(200).send(svg.top)
}

View file

@ -0,0 +1,260 @@
import { Box, Button, Flex, Grid, Heading, Image, Link, Text } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from '../../../components/nav'
import usePrefersReducedMotion from '../../../lib/use-prefers-reduced-motion'
import { useEffect, useRef, useState } from 'react'
import { remark } from 'remark'
import html from 'remark-html'
import { useRouter } from 'next/router'
import { FetchProject } from '../../api/board/[name]'
// example projects
const curated = [
'Touch Capacitive Piano',
'Small Stepper Motor Breakout',
'ShawnsMultipurposeMacropad',
'Hall-Effect Sensor Plate',
'Gas_Smoke_Detector',
'GPS Tracker for GOKART',
"Ewoud's Desktop Clock PCB",
'Connor Ender 3 Bed Leveler'
]
const BoardPage = ({ projectObj }) => {
const prefersReducedMotion = usePrefersReducedMotion()
// const router = useRouter()
// get slug
// const name = router.query.slug
const spotlightRef = useRef()
useEffect(() => {
const handler = event => {
spotlightRef.current.style.background = `radial-gradient(
circle at ${event.pageX}px ${event.pageY}px,
rgba(0, 0, 0, 0) 10px,
rgba(0, 0, 0, 0.8) 80px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
console.log(projectObj)
const [project, setProject] = useState({})
useEffect(() => {
/*(async () => {
const project = await (await fetch(`/api/board/${name}`)).json()
console.log(project)
setProject(project)
})()*/
setProject(projectObj)
}, [projectObj])
return (
<>
<Meta
as={Head}
title="Gallery"
description="Check out the latest and greatest from the Onboard project."
></Meta>
<style>{`
@font-face {
font-family: 'Phantom Sans';
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff')
format('woff'),
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
html {
scroll-behavior: smooth;
}
`}</style>
<Head></Head>
<Nav />
<Box
as="header"
sx={{
bg: '#000000',
backgroundImage: `
linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
url('https://cloud-dst3a9oz5-hack-club-bot.vercel.app/0image.png')
`,
color: '#ffffff',
position: 'relative'
}}
>
<Box
ref={spotlightRef}
sx={{
position: 'absolute',
zIndex: 2,
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: '#000000',
pointerEvents: 'none'
}}
/>
<Flex
sx={{
p: 4,
flexDirection: 'column',
alignItems: 'center',
zIndex: 5,
position: 'relative'
}}
>
<Flex
sx={{
p: 4,
flexDirection: 'column',
alignItems: 'center',
zIndex: 5,
margin: '3vh auto',
position: 'relative'
}}
>
<Heading as="h1" variant="title" sx={{ textAlign: 'center' }}>
Gallery
</Heading>
<Text as="p" variant="subtitle" sx={{ textAlign: 'center' }}>
Check out the latest and greatest from the OnBoard project.
</Text>
<Flex sx={{ mt: 16, gap: 10, flexDirection: ['column', 'row'] }}>
<Button
variant="ctaLg"
as="a"
href="https://hackclub.com/onboard"
target="_blank"
sx={{
background: t => t.util.gx('#60cc38', '#113b11')
}}
>
Make your own!
</Button>
</Flex>
</Flex>
</Flex>
</Box>
<Box
sx={{
bg: 'white',
py: [4, 5],
textAlign: 'center'
}}
>
<Box
sx={{
maxWidth: 'copyUltra',
mx: 'auto',
px: 3
}}
>
{
// two-column layout - image on left, title + desc on right
}
<Box
sx={{
display: 'grid',
gap: 4,
gridTemplateColumns: ['1fr', 'repeat(2, 1fr)'],
color: 'black'
}}
>
<Image
src={project.image}
alt={project.project_name}
sx={{
borderRadius: 8,
boxShadow: '0px 4px 4px rgba(0, 0, 0, 0.25)'
}}
/>
<Box
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center'
}}
>
<Heading as="h2" variant="title" sx={{ textAlign: 'left' }}>
{project.project_name}
</Heading>
<Text as="p" variant="subtitle" sx={{ textAlign: 'left' }}>
{project.maker_name ? `by ${project.maker_name}` : ''}{' '}
{project.slack_handle ? `(${project.slack_handle})` : ''}
</Text>
<Link
href={`https://github.com/hackclub/OnBoard/blob/main/projects/${project.project_name}/`}
sx={{
textDecoration: 'none',
color: 'black',
':hover': {
color: 'primary'
},
textAlign: 'left'
}}
>
View on GitHub
</Link>
{/* custom innerHTML */}
<Box
sx={{
textAlign: 'left'
}}
dangerouslySetInnerHTML={{
__html:
// render with remark to parse markdown
remark()
.use(html)
.processSync(project.description)
.toString()
.replaceAll('h4', 'p')
}}
/>
</Box>
</Box>
</Box>
</Box>
</>
)
}
export async function getStaticPaths(context) {
const res = await fetch(
'https://api.github.com/repos/hackclub/OnBoard/contents/projects'
)
const data =
await res.json() /*.filter(project => curated.includes(project.name))*/
//console.log(data)
const projects = data.map(project => project.name)
const paths = projects.map(name => ({
params: {
slug: name
}
}))
return {
paths,
fallback: 'blocking'
}
}
export async function getStaticProps(context) {
let name = context.params.slug
let project = await FetchProject(name)
return {
props: {
projectObj: project
}
}
}
export default BoardPage

View file

@ -0,0 +1,318 @@
import { Box, Button, Flex, Grid, Heading, Text } from 'theme-ui'
import Head from 'next/head'
import Meta from '@hackclub/meta'
import Nav from '../../../components/nav'
import usePrefersReducedMotion from '../../../lib/use-prefers-reduced-motion'
import { useEffect, useRef, useState } from 'react'
import Item from '../../../components/onboard/item'
import { getUrl } from 'nextjs-current-url/server'
import { getURL } from 'next/dist/shared/lib/utils'
import { FetchProject } from '../../api/board/[name]'
import { useRouter } from 'next/router'
/*import pcbStackup from "pcb-stackup";
import JSZip from "jszip";
import JSZipUtils from "jszip-utils";*/
async function get_fallback_image(project) {
/*const fileNamesBlobs = {}
// load the zip file
const zip = new JSZip();
await JSZipUtils.getBinaryContent(project, async (err, data) => {
if (err) {
console.error(err)
}
try {
const zipData = await zip.loadAsync(data)
// get the file names and blobs
for (const [fileName, file] of Object.entries(zipData.files)) {
fileNamesBlobs[fileName] = await file.async('blob')
}
} catch (e) {
console.error(e)
}
})
const layers = []
for (const [fileName, blob] of Object.entries(fileNamesBlobs)) {
if (!fileName.includes('.txt')) { // filter out the text files
layers.push({
fileName,
gerber: blob.stream()
})
}
}
return (await pcbStackup(layers)).top.svg*/
return 'https://cloud-2jz3jz3jz-hack-club-bot.vercel.app/0image.png'
}
// example projects
const curated = [
'Touch Capacitive Piano',
'Small Stepper Motor Breakout',
'ShawnsMultipurposeMacropad',
'Hall-Effect Sensor Plate',
'Gas_Smoke_Detector',
'GPS Tracker for GOKART',
"Ewoud's Desktop Clock PCB",
'Connor Ender 3 Bed Leveler'
]
const GalleryPage = ({ projects }) => {
const prefersReducedMotion = usePrefersReducedMotion()
const router = useRouter()
const page = router.query.page
const spotlightRef = useRef()
useEffect(() => {
const handler = event => {
spotlightRef.current.style.background = `radial-gradient(
circle at ${event.pageX}px ${event.pageY}px,
rgba(0, 0, 0, 0) 10px,
rgba(0, 0, 0, 0.8) 80px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
// fetch all folders in the https://github.com/hackclub/OnBoard/tree/main/projects directory
/*const [projects, setProjects] = useState([])
useEffect(() => {
const fetchProjects = async () => {
const res = await fetch(
'https://api.github.com/repos/hackclub/OnBoard/contents/projects'
)
const data = (await res.json()).filter(project => curated.includes(project.name))
console.log(data)
const projectData = data.map(async project => {
return await (await fetch(`/api/board/${project.name}`)).json()
})
let projects = await Promise.all(projectData)
//console.log(projects)
setProjects(projects)
}
fetchProjects()
}, [])*/
return (
<>
<Meta
as={Head}
title="Gallery"
description="Check out the latest and greatest from the Onboard project."
></Meta>
<style>{`
@font-face {
font-family: 'Phantom Sans';
src: url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff')
format('woff'),
url('https://assets.hackclub.com/fonts/Phantom_Sans_0.7/Med.woff2')
format('woff2');
font-weight: 500;
font-style: normal;
font-display: swap;
}
html {
scroll-behavior: smooth;
}
`}</style>
<Head></Head>
<Nav />
<Box
as="header"
sx={{
bg: '#000000',
backgroundImage: `
linear-gradient(rgba(0, 0, 0, 0.7), rgba(0, 0, 0, 0.7)),
url('https://cloud-dst3a9oz5-hack-club-bot.vercel.app/0image.png')
`,
color: '#ffffff',
position: 'relative'
}}
>
<Box
ref={spotlightRef}
sx={{
position: 'absolute',
zIndex: 2,
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: '#000000',
pointerEvents: 'none'
}}
/>
<Flex
sx={{
p: 4,
flexDirection: 'column',
alignItems: 'center',
zIndex: 5,
position: 'relative'
}}
>
<Flex
sx={{
p: 4,
flexDirection: 'column',
alignItems: 'center',
zIndex: 5,
margin: '3vh auto',
position: 'relative'
}}
>
<Heading as="h1" variant="title" sx={{ textAlign: 'center' }}>
Gallery
</Heading>
<Text as="p" variant="subtitle" sx={{ textAlign: 'center' }}>
Check out the latest and greatest from the OnBoard project.
</Text>
<Flex sx={{ mt: 16, gap: 10, flexDirection: ['column', 'row'] }}>
<Button
variant="ctaLg"
as="a"
href="https://hackclub.com/onboard"
target="_blank"
sx={{
background: t => t.util.gx('#60cc38', '#113b11')
}}
>
Make your own!
</Button>
</Flex>
</Flex>
</Flex>
</Box>
<Box
sx={{
bg: 'white',
py: [4, 5],
textAlign: 'center'
}}
>
<Grid
gap={4}
columns={[null, 2]}
sx={{
p: 4,
maxWidth: 'copyPlus',
mx: 'auto',
mt: 4,
mb: 5,
textAlign: 'center'
}}
>
{projects.map(project => (
<Item
key={project.project_name}
title={project.project_name}
author_name={project.maker_name}
author_slack={project.slack_handle}
image={project.image}
project={project}
/>
))}
</Grid>
<Box
sx={{
mt: 5,
textAlign: 'center'
}}
>
<Button
as="a"
href={`/onboard/gallery/${parseInt(page) - 1}`}
sx={{
bg: 'black',
color: 'white',
':hover': {
bg: 'white',
color: 'black'
}
}}
>
{'<'}
</Button>
<Text
as="span"
sx={{
mx: 3,
color: 'black'
}}
>
{page}
</Text>
<Button
as="a"
href={`/onboard/gallery/${parseInt(page) + 1}`}
sx={{
bg: 'black',
color: 'white',
':hover': {
bg: 'white',
color: 'black'
}
}}
>
{'>'}
</Button>
</Box>
</Box>
</>
)
}
/*export async function getServerSideProps(context) {
const res = await fetch(
'https://api.github.com/repos/hackclub/OnBoard/contents/projects'
)
const data = (await res.json())/*.filter(project => curated.includes(project.name))
console.log(data)
const projectData = data.map(async project => {
const url = getUrl({ req: context.req })
console.log(url)
return await (await fetch(encodeURI(`${url.origin}/api/board/${project.name}`))).json()
})
let projects = await Promise.all(projectData)
return {
props: {
projects
}
}
}*/
export async function getStaticProps(context) {
const res = await fetch(
'https://api.github.com/repos/hackclub/OnBoard/contents/projects'
)
const data = (await res.json()).slice(
(parseInt(context.params.page) - 1) * 10,
parseInt(context.params.page) * 10
)
//console.log(data)
const projects = []
for (const project of data) {
projects.push(await FetchProject(project.name))
}
return {
props: {
projects
}
}
}
export async function getStaticPaths(context) {
// divide the projects into chunks of 10
const res = await fetch(
'https://api.github.com/repos/hackclub/OnBoard/contents/projects'
)
const data = await res.json()
const pages = Math.ceil(data.length / 10)
const paths = Array(pages)
.fill()
.map((_, i) => ({ params: { page: (i + 1).toString() } }))
return { paths, fallback: false }
}
export default GalleryPage

961
yarn.lock

File diff suppressed because it is too large Load diff