mirror of
https://github.com/System-End/site.git
synced 2026-04-19 20:55:09 +00:00
Add Ship page (#18)
* Create Ship page * Add wave SVG * Variety of frontend improvements
This commit is contained in:
parent
096e79e4a9
commit
c08a421a11
9 changed files with 489 additions and 4 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -2,4 +2,4 @@
|
||||||
.next
|
.next
|
||||||
node_modules
|
node_modules
|
||||||
.DS_Store
|
.DS_Store
|
||||||
public
|
public/home
|
||||||
|
|
|
||||||
26
components/fade-in.js
Normal file
26
components/fade-in.js
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { keyframes } from '@emotion/core'
|
||||||
|
|
||||||
|
const fadeIn = keyframes({ from: { opacity: 0 }, to: { opacity: 1 } })
|
||||||
|
|
||||||
|
const Wrapper = styled(Box)`
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation-name: ${fadeIn};
|
||||||
|
animation-fill-mode: backwards;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const FadeIn = ({ duration = 300, delay = 0, ...props }) => (
|
||||||
|
<Wrapper
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
...(props.style || {}),
|
||||||
|
animationDuration: duration + 'ms',
|
||||||
|
animationDelay: delay + 'ms'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default FadeIn
|
||||||
17
components/ship/why.mdx
Normal file
17
components/ship/why.mdx
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { Card } from 'theme-ui'
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
|
||||||
|
## Your first ship your first day.
|
||||||
|
|
||||||
|
Students in many traditional computer science classes are lucky to make a single project. At Hack Clubs, every member makes & ships their first website their very first meeting.
|
||||||
|
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
|
||||||
|
## Keeping your eyes on the prize.
|
||||||
|
|
||||||
|
Instead of learning programming concepts in isolation, learning by shipping means you focus on what you need to build real projects. It’s more fun & leads to better learning.
|
||||||
|
|
||||||
|
</Card>
|
||||||
29
components/slide-up.js
Normal file
29
components/slide-up.js
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Box } from 'theme-ui'
|
||||||
|
import styled from '@emotion/styled'
|
||||||
|
import { keyframes } from '@emotion/core'
|
||||||
|
|
||||||
|
const slideUp = keyframes({
|
||||||
|
from: { transform: 'translateY(100%)', opacity: 0 },
|
||||||
|
to: { transform: 'translateY(0)', opacity: 1 }
|
||||||
|
})
|
||||||
|
|
||||||
|
const Wrapper = styled(Box)`
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
animation-name: ${slideUp};
|
||||||
|
animation-fill-mode: backwards;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const SlideUp = ({ duration = 300, delay = 0, ...props }) => (
|
||||||
|
<Wrapper
|
||||||
|
{...props}
|
||||||
|
style={{
|
||||||
|
...(props.style || {}),
|
||||||
|
animationDuration: duration + 'ms',
|
||||||
|
animationDelay: delay + 'ms'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default SlideUp
|
||||||
92
components/stat.js
Normal file
92
components/stat.js
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
import { Flex, Text } from 'theme-ui'
|
||||||
|
import { isEmpty } from 'lodash'
|
||||||
|
|
||||||
|
const Stat = ({
|
||||||
|
value,
|
||||||
|
label,
|
||||||
|
unit = '',
|
||||||
|
color = 'text',
|
||||||
|
of,
|
||||||
|
center = false,
|
||||||
|
reversed = false,
|
||||||
|
half = false,
|
||||||
|
lg = false,
|
||||||
|
...props
|
||||||
|
}) => (
|
||||||
|
<Flex
|
||||||
|
{...props}
|
||||||
|
sx={{
|
||||||
|
fontFamily: 'heading',
|
||||||
|
flexDirection: reversed ? 'column-reverse' : 'column',
|
||||||
|
gridColumn: lg
|
||||||
|
? ['initial', 'span 1']
|
||||||
|
: half
|
||||||
|
? 'span 1 !important'
|
||||||
|
: 'initial',
|
||||||
|
lineHeight: 1,
|
||||||
|
...props.sx
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: center ? 'center' : 'start',
|
||||||
|
position: 'relative'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
sx={{
|
||||||
|
color,
|
||||||
|
fontSize: lg ? [5, 6, 7] : [4, 5, 6],
|
||||||
|
fontWeight: 'heading',
|
||||||
|
letterSpacing: 'title',
|
||||||
|
my: 0
|
||||||
|
}}
|
||||||
|
children={value || '—'}
|
||||||
|
/>
|
||||||
|
{!isEmpty(unit) && (
|
||||||
|
<Text
|
||||||
|
as="sup"
|
||||||
|
sx={{
|
||||||
|
fontSize: lg ? [2, 3] : [1, 2],
|
||||||
|
color: color === 'text' ? 'secondary' : color,
|
||||||
|
ml: [null, unit === '%' ? 1 : null],
|
||||||
|
mr: [null, 1],
|
||||||
|
pt: [null, 1]
|
||||||
|
}}
|
||||||
|
children={unit}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!isEmpty(of) && (
|
||||||
|
<Text
|
||||||
|
as="sup"
|
||||||
|
sx={{
|
||||||
|
fontSize: lg ? [2, 3] : [1, 2],
|
||||||
|
color: 'muted',
|
||||||
|
ml: [null, 1],
|
||||||
|
pt: [null, 1],
|
||||||
|
'::before': {
|
||||||
|
content: '"/"'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
children={of}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
{!isEmpty(label) && (
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
variant="caption"
|
||||||
|
sx={{
|
||||||
|
fontSize: lg ? [1, 2, 3] : [0, 1],
|
||||||
|
letterSpacing: 'headline',
|
||||||
|
textTransform: 'uppercase'
|
||||||
|
}}
|
||||||
|
children={label}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default Stat
|
||||||
|
|
@ -10,8 +10,8 @@ export const tinyDt = d =>
|
||||||
export const timeSince = (
|
export const timeSince = (
|
||||||
previous,
|
previous,
|
||||||
absoluteDuration = false,
|
absoluteDuration = false,
|
||||||
current = new Date().toISOString(),
|
longForm = false,
|
||||||
longForm = false
|
current = new Date().toISOString()
|
||||||
) => {
|
) => {
|
||||||
const msPerMinute = 60 * 1000
|
const msPerMinute = 60 * 1000
|
||||||
const msPerHour = msPerMinute * 60
|
const msPerHour = msPerMinute * 60
|
||||||
|
|
@ -22,7 +22,7 @@ export const timeSince = (
|
||||||
|
|
||||||
const future = new Date(previous) - new Date(current)
|
const future = new Date(previous) - new Date(current)
|
||||||
const past = new Date(current) - new Date(previous)
|
const past = new Date(current) - new Date(previous)
|
||||||
const elapsed = [future, past].sort()[0]
|
const elapsed = [future, past].sort()[1]
|
||||||
|
|
||||||
let humanizedTime
|
let humanizedTime
|
||||||
if (elapsed < msPerMinute) {
|
if (elapsed < msPerMinute) {
|
||||||
|
|
|
||||||
18
lib/theme.js
18
lib/theme.js
|
|
@ -104,5 +104,23 @@ theme.forms.labelCheckbox = {
|
||||||
theme.layout.copy.maxWidth = [null, null, 'copyPlus']
|
theme.layout.copy.maxWidth = [null, null, 'copyPlus']
|
||||||
|
|
||||||
theme.text.lead = {}
|
theme.text.lead = {}
|
||||||
|
theme.text.eyebrow = {
|
||||||
|
color: 'muted',
|
||||||
|
fontSize: [3, 4],
|
||||||
|
fontWeight: 'heading',
|
||||||
|
letterSpacing: 'headline',
|
||||||
|
lineHeight: 'subheading',
|
||||||
|
textTransform: 'uppercase',
|
||||||
|
mt: 0,
|
||||||
|
mb: 2
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.text.titleUltra = {
|
||||||
|
...theme.text.title,
|
||||||
|
fontSize: [5, 6, 7],
|
||||||
|
lineHeight: 0.875
|
||||||
|
}
|
||||||
|
|
||||||
|
theme.text.subtitle.mt = 3
|
||||||
|
|
||||||
export default theme
|
export default theme
|
||||||
|
|
|
||||||
296
pages/ship.js
Normal file
296
pages/ship.js
Normal file
|
|
@ -0,0 +1,296 @@
|
||||||
|
import {
|
||||||
|
Avatar,
|
||||||
|
Badge,
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Card,
|
||||||
|
Grid,
|
||||||
|
Heading,
|
||||||
|
Image,
|
||||||
|
Text,
|
||||||
|
Flex
|
||||||
|
} from 'theme-ui'
|
||||||
|
import NextLink from 'next/link'
|
||||||
|
import Head from 'next/head'
|
||||||
|
import Meta from '@hackclub/meta'
|
||||||
|
import Nav from '../components/nav'
|
||||||
|
import SlideUp from '../components/slide-up'
|
||||||
|
import Why from '../components/ship/why.mdx'
|
||||||
|
import Icon from '../components/icon'
|
||||||
|
import Stat from '../components/stat'
|
||||||
|
import Footer from '../components/footer'
|
||||||
|
import { timeSince } from '../lib/dates'
|
||||||
|
import { orderBy, filter, take, map, uniq, reverse } from 'lodash'
|
||||||
|
import { keyframes } from '@emotion/core'
|
||||||
|
|
||||||
|
const ShipBadge = props => (
|
||||||
|
<Badge
|
||||||
|
as="mark"
|
||||||
|
sx={{
|
||||||
|
bg: 'orange',
|
||||||
|
color: 'inherit',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
display: 'inline-block',
|
||||||
|
borderRadius: 'default',
|
||||||
|
px: [2, 3],
|
||||||
|
py: 1,
|
||||||
|
...props.sx
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
|
||||||
|
const Ship = ({ timestamp, message, url, img, username, avatar }) => (
|
||||||
|
<Card as="section" p={[0, 0]} sx={{ width: '100%' }}>
|
||||||
|
{img && (
|
||||||
|
<Image
|
||||||
|
src={img}
|
||||||
|
sx={{
|
||||||
|
width: '100%',
|
||||||
|
maxHeight: 16 * 16,
|
||||||
|
bg: 'snow',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Box p={3}>
|
||||||
|
<Text
|
||||||
|
as="p"
|
||||||
|
title={message}
|
||||||
|
sx={{
|
||||||
|
display: message ? null : 'none',
|
||||||
|
fontSize: 2,
|
||||||
|
lineHeight: 'caption',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
overflowY: 'hidden',
|
||||||
|
maxWidth: '100%',
|
||||||
|
'@supports (-webkit-line-clamp: 4)': {
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitLineClamp: ['6', null, '8'],
|
||||||
|
WebkitBoxOrient: 'vertical'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message}
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
as="footer"
|
||||||
|
sx={{
|
||||||
|
mt: 2,
|
||||||
|
display: 'grid',
|
||||||
|
gridGap: [2, null, 3],
|
||||||
|
gridTemplateColumns: [null, null, '1fr auto'],
|
||||||
|
alignItems: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex sx={{ alignItems: 'center' }}>
|
||||||
|
{avatar && (
|
||||||
|
<Avatar size={48} src={avatar} alt={`${username} avatar`} mr={2} />
|
||||||
|
)}
|
||||||
|
<Box sx={{ flex: '1 1 auto' }}>
|
||||||
|
<Text
|
||||||
|
as="strong"
|
||||||
|
variant="caption"
|
||||||
|
sx={{ color: 'secondary', display: 'block' }}
|
||||||
|
>
|
||||||
|
{username}
|
||||||
|
</Text>
|
||||||
|
<Text as="time" variant="caption" sx={{ lineHeight: 'title' }}>
|
||||||
|
{timeSince(new Date(timestamp), false, true)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</Flex>
|
||||||
|
{url && !url?.includes('hackclub.slack.com') && (
|
||||||
|
<Button as="a" href={url} sx={{ bg: 'cyan', svg: { ml: -1 } }}>
|
||||||
|
{url.includes('slack-files') ? (
|
||||||
|
<>
|
||||||
|
<Icon glyph="attachment" size={24} />
|
||||||
|
View file
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon glyph="link" size={24} />
|
||||||
|
<Text as="span" sx={{ textTransform: 'lowercase' }}>
|
||||||
|
{
|
||||||
|
url
|
||||||
|
.replace(/https?:\/\//, '')
|
||||||
|
.replace('www.', '')
|
||||||
|
.split(/[/?#]/)[0]
|
||||||
|
}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
|
||||||
|
const waves = keyframes({
|
||||||
|
'0%': { backgroundPositionX: '0' },
|
||||||
|
'100%': { backgroundPositionX: '-100%' }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default ({ ships, stats }) => (
|
||||||
|
<>
|
||||||
|
<Meta
|
||||||
|
as={Head}
|
||||||
|
name="Ship"
|
||||||
|
description={`Hack Clubbers ship projects: a real-time list of the ${stats.projects} projects created by the Hack Club high school community in the last month.`}
|
||||||
|
image="https://assets.hackclub.com/log/2020-05-22-ship.png"
|
||||||
|
/>
|
||||||
|
<Nav />
|
||||||
|
<Box
|
||||||
|
as="header"
|
||||||
|
sx={{
|
||||||
|
bg: 'blue',
|
||||||
|
backgroundImage: t =>
|
||||||
|
`linear-gradient(to bottom, ${t.colors.cyan}, ${t.colors.blue})`,
|
||||||
|
color: 'white',
|
||||||
|
textAlign: 'center',
|
||||||
|
pt: [5, 6],
|
||||||
|
pb: [3, 4]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container
|
||||||
|
sx={{
|
||||||
|
maxWidth: [null, null, 'copyPlus', 'copyUltra'],
|
||||||
|
p: { fontSize: [2, 3, 4], maxWidth: 'copy', mx: 'auto' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="eyebrow" sx={{ color: 'white', opacity: 0.875 }}>
|
||||||
|
All aboard!
|
||||||
|
</Text>
|
||||||
|
<Heading as="h1" variant="titleUltra" sx={{ mb: [3, 4] }}>
|
||||||
|
Hack Clubbers focus on one thing: <ShipBadge>shipping.</ShipBadge>
|
||||||
|
</Heading>
|
||||||
|
<Text as="p" variant="subtitle">
|
||||||
|
After building a project, like an app or website, “shipping” is
|
||||||
|
publishing & sharing it online.
|
||||||
|
</Text>
|
||||||
|
</Container>
|
||||||
|
<SlideUp duration={750}>
|
||||||
|
<Grid
|
||||||
|
as="section"
|
||||||
|
columns={[null, null, 2]}
|
||||||
|
gap={[3, 4]}
|
||||||
|
variant="layout.container"
|
||||||
|
sx={{
|
||||||
|
mt: [3, 4, 5],
|
||||||
|
textAlign: 'left',
|
||||||
|
div: { p: [3, 4] },
|
||||||
|
h2: { variant: 'text.headline', color: 'blue', mt: 0, mb: 2 },
|
||||||
|
p: { fontSize: 2, my: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Why />
|
||||||
|
</Grid>
|
||||||
|
</SlideUp>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
as="section"
|
||||||
|
id="projects"
|
||||||
|
sx={{
|
||||||
|
bg: 'blue',
|
||||||
|
color: 'white',
|
||||||
|
py: [4, 5],
|
||||||
|
backgroundImage: 'url(/ship/wave.svg)',
|
||||||
|
backgroundSize: '200% auto',
|
||||||
|
'@media (prefers-reduced-motion: no-preference)': {
|
||||||
|
animation: `${waves} 15s linear forwards infinite`
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Grid
|
||||||
|
as="header"
|
||||||
|
columns={[2, null, 4]}
|
||||||
|
gap={[3, 4]}
|
||||||
|
variant="layout.container"
|
||||||
|
sx={{
|
||||||
|
mt: [2, 4],
|
||||||
|
textAlign: 'left',
|
||||||
|
span: { color: 'white' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Heading as="h2" variant="title" sx={{ my: 0, gridColumn: 'span 2' }}>
|
||||||
|
In the last month on Hack Club…
|
||||||
|
</Heading>
|
||||||
|
<Stat value={stats.projects} label="Projects shipped" lg />
|
||||||
|
<Stat value={stats.makers} label="Makers" lg />
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
as="article"
|
||||||
|
gap={[3, null, null, 4]}
|
||||||
|
p={[3, null, null, 4]}
|
||||||
|
variant="layout.wide"
|
||||||
|
sx={{
|
||||||
|
alignItems: 'start',
|
||||||
|
gridTemplateColumns: [
|
||||||
|
null,
|
||||||
|
'repeat(2,minmax(0, 1fr))',
|
||||||
|
'repeat(3,minmax(0, 1fr))'
|
||||||
|
],
|
||||||
|
'> div': { width: '100%' }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{ships.map(s => (
|
||||||
|
<Ship key={s.timestamp} {...s} />
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
as="section"
|
||||||
|
sx={{
|
||||||
|
color: 'black',
|
||||||
|
bg: 'white',
|
||||||
|
py: [4, 5]
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Container variant="copy" sx={{ textAlign: 'center' }}>
|
||||||
|
<Icon glyph="message-new" size={72} sx={{ color: 'blue' }} />
|
||||||
|
<Heading as="h2" variant="headline" mt={0}>
|
||||||
|
Want to ship your own projects?
|
||||||
|
</Heading>
|
||||||
|
<Text variant="subtitle" sx={{ lineHeight: 'caption', mb: 3 }}>
|
||||||
|
These projects are streamed live from the #ship channel on the
|
||||||
|
Hack Club Slack, where 9k teenagers from around the world share
|
||||||
|
what they’re working on & help each other.
|
||||||
|
</Text>
|
||||||
|
<NextLink href="/" passHref>
|
||||||
|
<Button bg="red" as="a">
|
||||||
|
Learn more
|
||||||
|
</Button>
|
||||||
|
</NextLink>
|
||||||
|
</Container>
|
||||||
|
</Box>
|
||||||
|
<Footer />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export const getStaticProps = async () => {
|
||||||
|
const ships = await fetch('http://api2.hackclub.com/v0.1/Ships/Ships')
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
const monthAgo = new Date().getTime() - 30 * 24 * 60 * 60 * 1000
|
||||||
|
return filter(data, s => new Date(s.fields.Timestamp) > monthAgo)
|
||||||
|
})
|
||||||
|
.then(data =>
|
||||||
|
data.map(({ fields }) => ({
|
||||||
|
timestamp: fields['Timestamp'] || new Date().toISOString(),
|
||||||
|
avatar: fields['User Avatar'] || null,
|
||||||
|
username: fields['User Name'] || '@unknown',
|
||||||
|
message: fields['Message'] || '',
|
||||||
|
url: fields['Project URL'] || null,
|
||||||
|
img: fields['Image URL'] || null
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
.then(data => orderBy(data, { timestamp: 'desc' }))
|
||||||
|
const stats = {
|
||||||
|
projects: ships.length,
|
||||||
|
makers: uniq(map(ships, 'username')).length
|
||||||
|
}
|
||||||
|
return { props: { ships, stats }, unstable_revalidate: 1 }
|
||||||
|
}
|
||||||
7
public/ship/wave.svg
Normal file
7
public/ship/wave.svg
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1500" height="1200">
|
||||||
|
<g id="y"><g id="x">
|
||||||
|
<path id="w" fill="none" stroke="#42a1ec" stroke-width="66" d="m-115,50q38-30 75,0t75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0 75,0"/>
|
||||||
|
<use xlink:href="#w" transform="translate(0,146)"/></g>
|
||||||
|
<use xlink:href="#x" transform="translate(0,292)"/></g>
|
||||||
|
<use xlink:href="#y" transform="translate(0,584)"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 493 B |
Loading…
Add table
Reference in a new issue