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 }) => ( + {name +) + +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 => ( + {img.filename} + ))} + + + )} + +) + +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 = [] }) => ( <>