diff --git a/.gitignore b/.gitignore
index 8c3f20f9..c79f3933 100755
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,4 @@
.next
node_modules
.DS_Store
-public
+public/home
diff --git a/components/fade-in.js b/components/fade-in.js
new file mode 100644
index 00000000..b44aa8c5
--- /dev/null
+++ b/components/fade-in.js
@@ -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 }) => (
+
+)
+
+export default FadeIn
diff --git a/components/ship/why.mdx b/components/ship/why.mdx
new file mode 100644
index 00000000..ff924ecb
--- /dev/null
+++ b/components/ship/why.mdx
@@ -0,0 +1,17 @@
+import { Card } from 'theme-ui'
+
+
+
+## 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.
+
+
+
+
+
+## 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.
+
+
diff --git a/components/slide-up.js b/components/slide-up.js
new file mode 100644
index 00000000..7cd51c0d
--- /dev/null
+++ b/components/slide-up.js
@@ -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 }) => (
+
+)
+
+export default SlideUp
diff --git a/components/stat.js b/components/stat.js
new file mode 100644
index 00000000..60f59289
--- /dev/null
+++ b/components/stat.js
@@ -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
+}) => (
+
+
+
+ {!isEmpty(unit) && (
+
+ )}
+ {!isEmpty(of) && (
+
+ )}
+
+ {!isEmpty(label) && (
+
+ )}
+
+)
+
+export default Stat
diff --git a/lib/dates.js b/lib/dates.js
index a7efd66c..0aed21e1 100644
--- a/lib/dates.js
+++ b/lib/dates.js
@@ -10,8 +10,8 @@ export const tinyDt = d =>
export const timeSince = (
previous,
absoluteDuration = false,
- current = new Date().toISOString(),
- longForm = false
+ longForm = false,
+ current = new Date().toISOString()
) => {
const msPerMinute = 60 * 1000
const msPerHour = msPerMinute * 60
@@ -22,7 +22,7 @@ export const timeSince = (
const future = new Date(previous) - new Date(current)
const past = new Date(current) - new Date(previous)
- const elapsed = [future, past].sort()[0]
+ const elapsed = [future, past].sort()[1]
let humanizedTime
if (elapsed < msPerMinute) {
diff --git a/lib/theme.js b/lib/theme.js
index d894174f..8f1658b2 100644
--- a/lib/theme.js
+++ b/lib/theme.js
@@ -104,5 +104,23 @@ theme.forms.labelCheckbox = {
theme.layout.copy.maxWidth = [null, null, 'copyPlus']
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
diff --git a/pages/ship.js b/pages/ship.js
new file mode 100644
index 00000000..fccd5a81
--- /dev/null
+++ b/pages/ship.js
@@ -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 => (
+
+)
+
+const Ship = ({ timestamp, message, url, img, username, avatar }) => (
+
+ {img && (
+
+ )}
+
+
+ {message}
+
+
+
+ {avatar && (
+
+ )}
+
+
+ {username}
+
+
+ {timeSince(new Date(timestamp), false, true)}
+
+
+
+ {url && !url?.includes('hackclub.slack.com') && (
+
+ )}
+
+
+
+)
+
+const waves = keyframes({
+ '0%': { backgroundPositionX: '0' },
+ '100%': { backgroundPositionX: '-100%' }
+})
+
+export default ({ ships, stats }) => (
+ <>
+
+
+
+ `linear-gradient(to bottom, ${t.colors.cyan}, ${t.colors.blue})`,
+ color: 'white',
+ textAlign: 'center',
+ pt: [5, 6],
+ pb: [3, 4]
+ }}
+ >
+
+
+ All aboard!
+
+
+ Hack Clubbers focus on one thing: shipping.
+
+
+ After building a project, like an app or website, “shipping” is
+ publishing & sharing it online.
+
+
+
+
+
+
+
+
+
+
+
+ In the last month on Hack Club…
+
+
+
+
+ div': { width: '100%' }
+ }}
+ >
+ {ships.map(s => (
+
+ ))}
+
+
+
+
+
+
+ Want to ship your own projects?
+
+
+ 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.
+
+
+
+
+
+
+
+ >
+)
+
+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 }
+}
diff --git a/public/ship/wave.svg b/public/ship/wave.svg
new file mode 100644
index 00000000..a0ad4a2d
--- /dev/null
+++ b/public/ship/wave.svg
@@ -0,0 +1,7 @@
+
\ No newline at end of file