diff --git a/components/posts/emoji.js b/components/posts/emoji.js
new file mode 100644
index 00000000..29c9ec30
--- /dev/null
+++ b/components/posts/emoji.js
@@ -0,0 +1,55 @@
+import { memo, useState, useEffect } from 'react'
+import Image from 'next/image'
+
+const stripColons = str => {
+ const colonIndex = str.indexOf(':')
+ if (colonIndex > -1) {
+ // :emoji:
+ if (colonIndex === str.length - 1) {
+ str = str.substring(0, colonIndex)
+ return stripColons(str)
+ } else {
+ str = str.substr(colonIndex + 1)
+ return stripColons(str)
+ }
+ }
+ return str
+}
+
+export const EmojiImg = ({ name, ...props }) => (
+
+)
+
+const CustomEmoji = memo(({ name }) => {
+ const emoji = stripColons(name)
+ let [image, setImage] = useState()
+ useEffect(() => {
+ try {
+ fetch('https://scrapbook.hackclub.com/api/emoji')
+ .then(r => r.json())
+ .then(emojis => {
+ if (emojis[emoji]) {
+ setImage(emojis[emoji])
+ return
+ }
+ setImage(
+ 'https://emoji.slack-edge.com/T0266FRGM/parrot/c9f4fddc5e03d762.gif'
+ )
+ })
+ } catch (e) {}
+ }, [])
+ return image ? (
+
+ ) : (
+ :{emoji}:
+ )
+})
+
+export default CustomEmoji
diff --git a/components/posts/index.js b/components/posts/index.js
new file mode 100644
index 00000000..35d17ce9
--- /dev/null
+++ b/components/posts/index.js
@@ -0,0 +1,219 @@
+import { Button, Box, Card, Text, Grid, Avatar, Flex } from 'theme-ui'
+import { formatDate } from '../../lib/dates'
+import { Fragment, memo } from 'react'
+import { last, filter } from 'lodash'
+import Masonry from 'react-masonry-css'
+import Image from 'next/image'
+import Mention from './mention'
+import Emoji from './emoji'
+
+const dataDetector = /(<.+?\|?\S+>)|(@\S+)|(`{3}[\S\s]+`{3})|(`[^`]+`)|(_[^_]+_)|(\*[^\*]+\*)|(:[^ .,;`\u2013~!@#$%^&*(){}=\\:"<>?|A-Z]+:)/
+
+export const formatText = text =>
+ text.split(dataDetector).map((chunk, i) => {
+ if (chunk?.startsWith(':') && chunk?.endsWith(':')) {
+ return
+ }
+ if (chunk?.startsWith('@') || chunk?.startsWith('<@')) {
+ const punct = /([,!:.'"’”]|’s|'s|\))+$/g
+ const username = chunk.replace(/[@<>]/g, '').replace(punct, '')
+ return (
+
+
+ {chunk.match(punct)}
+
+ )
+ }
+ if (chunk?.startsWith('<')) {
+ const parts = chunk.match(/<(([^\|]+)\|)?([^>]+?)>/)
+ const url = parts?.[2] || last(parts)
+ const children = last(parts)
+ ?.replace(/https?:\/\//, '')
+ .replace(/\/$/, '')
+ return (
+
+ {children}
+
+ )
+ }
+ if (chunk?.startsWith('```')) {
+ return
{chunk.replace(/```/g, '')}
+ }
+ if (chunk?.startsWith('`')) {
+ return {chunk.replace(/`/g, '')}
+ }
+ if (chunk?.startsWith('*')) {
+ return {chunk.replace(/\*/g, '')}
+ }
+ if (chunk?.startsWith('_')) {
+ return {chunk.replace(/_/g, '')}
+ }
+ return {chunk?.replace(/&/g, '&')}
+ })
+
+const Post = ({
+ id = new Date().toISOString(),
+ profile = false,
+ user = {
+ username: 'abc',
+ avatar: '',
+ streakDisplay: false,
+ streakCount: 0
+ },
+ text,
+ attachments = [],
+ postedAt
+}) => (
+
+
+
+
+
+ @{user.username}
+
+
+ {formatDate(postedAt)}
+
+
+
+ div': { width: 18, height: 18 }
+ }}
+ >
+ {formatText(text)}
+
+ {attachments.length > 0 && (
+ <>
+ div': {
+ maxWidth: '100%',
+ maxHeight: 256,
+ bg: 'sunken',
+ gridColumn: attachments.length === 1 ? 'span 2' : null
+ },
+ img: { objectFit: 'contain' }
+ }}
+ >
+ {filter(attachments, a => a.type.startsWith('image')).map(img => (
+
+ ))}
+
+ >
+ )}
+
+)
+
+const Posts = ({ data = [] }) => (
+
+
+ {data.map(post => (
+
+ ))}
+
+
+
+ These are just a few posts…
+
+
+
+
+
+)
+
+export default Posts
diff --git a/components/posts/mention.js b/components/posts/mention.js
new file mode 100644
index 00000000..8bbc3491
--- /dev/null
+++ b/components/posts/mention.js
@@ -0,0 +1,37 @@
+import { Link, Avatar } from 'theme-ui'
+import { memo, useState, useEffect } from 'react'
+import { trim } from 'lodash'
+
+const Mention = memo(({ username }) => {
+ const [img, setImg] = useState(null)
+ useEffect(() => {
+ try {
+ fetch(`https://scrapbook.hackclub.com/api/profiles/${trim(username)}`)
+ .then(r => r.json())
+ .then(profile => setImg(profile.avatar))
+ } catch (e) {}
+ }, [])
+ return (
+
+ {img && (
+
+ )}
+ @{username}
+
+ )
+})
+
+export default Mention
diff --git a/next.config.js b/next.config.js
index e1fccb7d..68b3b27a 100755
--- a/next.config.js
+++ b/next.config.js
@@ -3,7 +3,12 @@ module.exports = withMDX({
trailingSlash: true,
pageExtensions: ['js', 'jsx', 'mdx'],
images: {
- domains: ['hackclub.com', 'dl.airtable.com', 'cdn.glitch.com']
+ domains: [
+ 'hackclub.com',
+ 'dl.airtable.com',
+ 'emoji.slack-edge.com',
+ 'cdn.glitch.com'
+ ]
},
webpack: (config, { isServer }) => {
if (isServer) require('./lib/sitemap')
diff --git a/package.json b/package.json
index 0d6ae10f..7b78eca9 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
"next": "^10.0.2-canary.2",
"react": "^17.0.1",
"react-dom": "^17.0.1",
+ "react-masonry-css": "^1.0.14",
"react-reveal": "^1.2.2",
"react-scrolllock": "^5.0.1",
"react-use-websocket": "2.2.0",
diff --git a/pages/ship.js b/pages/ship.js
index c09d9940..1035c081 100644
--- a/pages/ship.js
+++ b/pages/ship.js
@@ -18,7 +18,7 @@ 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 Posts from '../components/posts'
import Footer from '../components/footer'
import { timeSince } from '../lib/dates'
import { orderBy, filter, take, map, uniq, reverse } from 'lodash'
@@ -41,107 +41,17 @@ 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 ({ stats = {} }) => (
+const ShipPage = ({ posts = [] }) => (
<>
@@ -191,14 +101,13 @@ export default ({ stats = {} }) => (
- {/*
(
}
}}
>
-
-
- In the last month on Hack Club…
-
-
-
-
- div': { width: '100%' }
- }}
- >
- {ships.map(s => (
-
- ))}
-
+ Recently shipped…
+
+
- */}
(
>
)
-/*
+export default ShipPage
+
export const getStaticProps = async () => {
- const ships = await fetch('https://airbridge.hackclub.com/v0.1/Ships/Ships')
+ const posts = await fetch('https://scrapbook.hackclub.com/api/r/ship')
.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(posts =>
+ filter(posts, p =>
+ ['image/jpg', 'image/jpeg', 'image/png'].includes(
+ p.attachments?.[0]?.type
+ )
+ )
)
- .then(data => orderBy(data, { timestamp: 'desc' }))
- const stats = {
- projects: ships.length,
- makers: uniq(map(ships, 'username')).length
- }
- return { props: { stats }, revalidate: 1 }
+ .then(posts => orderBy(posts, 'postedAt', 'desc'))
+ .then(posts => take(posts, 24))
+ return { props: { posts }, revalidate: 2 }
}
-*/
diff --git a/yarn.lock b/yarn.lock
index bd60d7ba..fb56528e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -4292,6 +4292,11 @@ react-is@16.13.1, react-is@^16.8.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+react-masonry-css@^1.0.14:
+ version "1.0.14"
+ resolved "https://registry.yarnpkg.com/react-masonry-css/-/react-masonry-css-1.0.14.tgz#2ac1ca7bb2c7e96826f7da3accc9e95ae12b2f65"
+ integrity sha512-oAPVOCMApTT0HkxZJy84yU1EWaaQNZnJE0DjDMy/L+LxZoJEph4RRXsT9ppPKbFSo/tCzj+cCLwiBHjZmZ2eXA==
+
react-refresh@0.8.3:
version "0.8.3"
resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.8.3.tgz#721d4657672d400c5e3c75d063c4a85fb2d5d68f"