Merge branch 'arcade-gallery'

This commit is contained in:
Clay Nicholson 2024-08-19 15:56:08 -04:00
commit 0cceaaab0b
46 changed files with 5895 additions and 3135 deletions

View file

@ -0,0 +1,201 @@
import React from 'react'
import { Text, Close } from 'theme-ui'
import styles from './cohort-card.module.css'
import { useState } from 'react'
import { Button } from 'theme-ui'
import Icon from '@hackclub/icons'
import randomNotFoundImg from './random-not-found-img'
/** @jsxImportSource theme-ui */
const CohortCard = ({
id,
title = 'Title Not Found',
desc = 'Description Not Found',
imageLink = '',
personal = false,
reload,
color = '#09AFB4',
textColor="#000000"
}) => {
const [isHovered, setIsHovered] = useState(false)
const [isVisible, setIsVisible] = useState(true);
async function handleDelete() {
try {
const authToken = window.localStorage.getItem('arcade.authToken')
if (!authToken) {
throw new Error('Authorization token is missing.')
}
const response = await fetch(
`/api/arcade/showcase/projects/${id}/delete`,
{
method: 'PATCH',
headers: {
Authorization: `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({}) // Empty body since your API expects some body content
}
)
if (!response.ok) {
throw new Error(
`Failed to delete project with ID ${id}: ${response.statusText}`
)
}
console.log(`Project with ID ${id} marked as deleted.`)
} catch (error) {
console.error('Error deleting project:', error)
}
}
const firstImage = imageLink || randomNotFoundImg(id)
console.log({imageLink})
function red() {
window.location.href = '/arcade/showcase/project/' + id + '/edit'
}
return (
<>
{isVisible? (<div
sx={{
backgroundColor: color,
}}
className={styles.card}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{/* attempt to make blob... not working rn */}
{/* <div sx={{
background: 'black',
clipPath: `
path('M36.8,-68.8C45.8,-58.6,49.9,-44.9,51.7,-32.8C53.5,-20.8,53.1,-10.4,56.4,1.9C59.6,14.2,66.6,28.3,63.8,38.6C60.9,48.9,48.3,55.3,36,62.2C23.8,69.1,11.9,76.4,-1.7,79.4C-15.3,82.3,-30.7,81,-38.4,71.6C-46.2,62.1,-46.5,44.5,-54,31.3C-61.4,18,-76.2,9,-78.4,-1.3C-80.6,-11.5,-70.2,-23.1,-63.1,-36.9C-55.9,-50.7,-51.9,-66.8,-41.9,-76.4C-31.9,-86.1,-15.9,-89.3,-1,-87.6C13.9,-85.8,27.8,-79.1,36.8,-68.8Z')
`,
width: '130%',
height: '400px',
transform: "translate(-50%, -50%) scale(1.5)",
}}>
</div> */}
{personal && isHovered && (
<div
sx={{
position: 'absolute',
top: '10px',
left: '10px',
display: 'flex',
gap: '5px'
}}
>
<div
as="a"
onClick={() => red()}
sx={{
color: 'white',
bg: '#09AFB4',
borderRadius: '10px',
height: '32px',
cursor: 'pointer',
transitionDuration: '0.4s',
'&:hover': {
transform: 'scale(1.15)'
}
}}
>
<Icon glyph="edit" />{' '}
</div>
<div
onClick={e => {
document.getElementById('delete-project').showModal()
}}
sx={{
color: 'white',
bg: '#09AFB4',
borderRadius: '10px',
height: '32px',
cursor: 'pointer',
transitionDuration: '0.4s',
'&:hover': {
transform: 'scale(1.15)'
}
}}
>
<Icon glyph="minus" />{' '}
</div>
</div>
)}
<a
href={`/arcade/showcase/project/${id}`}
className={styles.linkWrapper}
target="_blank"
rel="noopener noreferrer"
>
<img
src={firstImage}
alt="Project Image"
className={styles.card_img}
/>
<h1
sx={{color: textColor}}
className={styles.card_title}>
{title}
</h1>
<p sx={{color: textColor}} className={styles.card_description}>{desc}</p>
</a>
<dialog
id="delete-project"
sx={{ borderRadius: '10px', border: '3px dashed #09AFB4' }}
className="gaegu"
>
<Text>Are you sure you want to delete this project?</Text>
<br />
<Button
sx={{
backgroundColor: '#FF5C00',
color: '#FAEFD6',
borderRadius: '5px',
border: 'none',
px: '20px',
transitionDuration: '0.3s',
'&:hover': {
transform: 'scale(1.05)'
},
width: 'fit-content'
}}
onClick={e => {
setIsVisible(false)
document.getElementById('add-project').close()
handleDelete()
}}
>
Yes
</Button>
<Close
sx={{
'&:hover': { cursor: 'pointer' },
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 2,
color: '#09AFB4'
}}
onClick={e => {
document.getElementById('add-project').close()
}}
/>
</dialog>
</div>) : null
}
</>
)
}
export default CohortCard

View file

@ -0,0 +1,53 @@
.card {
flex: 1;
break-inside: avoid;
position: relative;
background-size: cover;
background-position: center;
overflow: hidden;
min-width: 100px;
display: flex;
flex-direction: column;
align-items: left;
justify-content: flex-start;
position: relative;
padding: 10px;
}
.card_img {
width: 100%;
max-width: 200px;
height: auto;
max-height: 100%;
object-fit: cover;
aspect-ratio: 1 / 1;
}
.card_title {
color: #09AFB4;
font-size: 1.5rem;
font-weight: bold;
margin-bottom: 0;
}
.card_description {
font-size: 1rem;
color: #0BB6BB;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-height: 3em;
margin-bottom: 10px;
}
.linkWrapper {
display: block;
text-decoration: none;
color: inherit;
}

View file

@ -0,0 +1,18 @@
import React from 'react'
import styles from './create-card.module.css'
import img from '../../../public/arcade/plus.png'
const CreateCard = ({ createCardLink }) => {
return (
<>
<a href={createCardLink} className={styles.linkWrapper} rel="noopener noreferrer">
<div className={styles.card}>
<img src={img}/>
Create a card
</div>
</a>
</>
)
}
export default CreateCard

View file

@ -0,0 +1,14 @@
.card{
flex: 1;
break-inside: avoid;
position: relative;
background-size: cover;
background-position: center;
overflow: hidden;
min-width: 100px;
display: flex;
flex-direction: column;
align-items: left;
justify-content: flex-start;
}

View file

@ -0,0 +1,66 @@
.feed {
width: 90vw;
max-width: 1200px;
overflow-y: auto;
overflow-x: hidden;
align-self: center;
display: grid;
grid-gap: 20px;
padding: 10px;
margin: auto;
/* Small screens */
@media (min-width: 640px) {
grid-template-columns: repeat(3, 1fr); /* 4 equal-width columns */
}
/* Medium screens */
@media (min-width: 768px) {
grid-template-columns: repeat(4, 1fr); /* 5 equal-width columns */
}
/* Large screens */
@media (min-width: 1024px) {
grid-template-columns: repeat(5, 1fr); /* 6 equal-width columns */
}
}
.title{
margin-top: 200px;
}
.container {
display: flex;
align-items: center;
justify-content: center; /* Center the title horizontally */
}
.title-text {
font-size: 24px;
font-weight: bold;
color: #000;
}
.timer_box {
display: flex;
flex-direction: column;
align-items: center;
background-color: #f0f0f0;
border: 1px solid #ccc;
padding: 8px 16px;
border-radius: 8px;
margin-left: 16px; /* Space between the title and countdown box */
}
.timer_text {
margin-bottom: 8px;
font-size: 16px;
color: #333;
}
.countdown {
font-size: 24px;
font-weight: bold;
color: #000;
}

View file

@ -0,0 +1,61 @@
import React from 'react'
import styles from './post.module.css'
import { useRef } from 'react';
const Post = ({ id, title, desc, slack, scrapbook, playable, images, githubProf}) => {
const cardRef = useRef(null);
var backgroundImage = `url(https://via.placeholder.com/300x300)`;
const handleMouseMove = (e) => {
const card = cardRef.current;
const rect = card.getBoundingClientRect();
const x = e.clientX - rect.left; // x position within the element
const y = e.clientY - rect.top; // y position within the element
const centerX = rect.width / 3;
const centerY = rect.height / 3;
const percentX = (x - centerX) / centerX;
const percentY = (y - centerY) / centerY;
const rotateX = percentY * -2 // Rotate between -15deg to 15deg
const rotateY = percentX * 2; // Rotate between -15deg to 15deg
card.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg)`;
};
const handleMouseLeave = () => {
const card = cardRef.current;
card.style.transform = 'perspective(1000px) rotateX(0) rotateY(0)';
};
if (images){
if (images.length !== 0) {
backgroundImage = `url(${images[0].url})`;
}
}
return (
<div
alt={id}
className={styles.gallery_card}
ref={cardRef}
onMouseMove={handleMouseMove}
onMouseLeave={handleMouseLeave}
style={{ backgroundImage }}
>
<h1 className={styles.card_title}>
{title}<br/>
</h1>
<div className={styles.overlay}>
<p className={styles.description}>{desc}</p>
</div>
</div>
)
}
export default Post

View file

@ -0,0 +1,88 @@
.gallery_card{
flex: 1;
break-inside: avoid;
border-radius: 0.5rem; /* Equivalent to rounded-lg */
background-color: rgba(158, 158, 158, 1); /* Equivalent to bg-white/20 */
background-clip: padding-box; /* Equivalent to bg-clip-padding */
padding: 1.5rem 1.5rem 1rem 1.5rem; /* Equivalent to p-6 pb-4 */
cursor: pointer;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); /* Equivalent to shadow-lg */
height: fit-content; /* Equivalent to h-fit */
width: 100%; /* Make the card width responsive within the column */
margin-bottom: 24px; /* Add space between cards vertically */
min-height: 300px;
background-image: url("https://img.buzzfeed.com/buzzfeed-static/static/2020-05/21/17/asset/19f3032de0de/sub-buzz-1010-1590082675-7.png");
position: relative;
background-size: cover;
background-position: center;
overflow: hidden;
}
.feed {
min-height: 1000px;
padding-top: 32px;
padding-bottom: 32px;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
align-self: center;
column-gap: 24px;
padding: 24px;
@media (min-width: 640px) {
column-count: 1;
}
/* Medium screens */
@media (min-width: 768px) {
column-count: 2;
}
/* Large screens */
@media (min-width: 1024px) {
column-count: 3;
}
}
.card_title{
font-family: 'Trebuchet MS';
font-size: 1.5rem;
font-weight: 700;
text-align: left;
color: #e1e1e1;
margin-top: 0;
margin-bottom: 1rem;
}
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0); /* Transparent initially */
display: flex;
justify-content: center;
align-items: center;
transition: background-color 0.3s ease;
border-radius: 0.5rem;
padding: 15px;
}
.description {
color: white;
font-size: 18px;
opacity: 0;
transition: opacity 0.3s ease;
}
.gallery_card:hover .overlay {
background-color: rgba(0, 0, 0, 0.6); /* Black overlay with 60% opacity */
}
.gallery_card:hover .description {
opacity: 1;
}

View file

@ -0,0 +1,86 @@
import { Input, Label, Text } from 'theme-ui'
import useForm from '../../../lib/use-form'
import { useEffect, useState } from 'react'
import Submit from '../../submit'
const Loading = () => <div>Loading...</div>
const ErrorMsg = () => <div>There was an error loading your projects.</div>
async function projectAdded(response) {
const projectID = response.project
window.location.href = '/arcade/showcase/project/' + projectID + '/edit'
}
const NewProjectForm = ({ authToken }) => {
const { status, formProps, useField } = useForm(
'/api/arcade/showcase/projects/add',
projectAdded,
{ initData: { authToken } }
)
return (
<div>
<form {...formProps}>
<Label>
<Text className="slackey">GitHub Repo link</Text>
<Text color="muted">
We'll pull in your project details from this repo
</Text>
<Input
{...useField('codeLink')}
placeholder="https://github.com/hackclub/arcade"
required
sx={{ border: '1px solid', borderColor: 'muted', mb: 2 }}
/>
</Label>
{/* <Label>
<Text className="slackey">GitHub README link</Text>
<Text color="muted">We'll pull in your project description</Text>
<Input
{...useField('readMeLink')}
placeholder="https://github.com/hackclub/arcade/README.md"
required
sx={{ border: '1px solid', borderColor: 'muted', mb: 2 }}
/>
</Label> */}
<Input {...useField('authToken')} type="hidden" />
<Submit
status={status}
labels={{
default: 'Submit repo',
error: 'Something went wrong!',
success: 'Pulling repo data'
}}
sx={{
background: status == 'error' ? '#DE4E2B' : '#09AFB4',
borderRadius: '10px'
}}
/>
</form>
</div>
)
}
const ProjectAddView = () => {
const [authToken, setAuthToken] = useState('')
const [status, setStatus] = useState('loading')
useEffect(() => {
const token = window.localStorage.getItem('arcade.authToken')
if (!token) {
setStatus('error')
}
setAuthToken(token)
setStatus('success')
}, [])
return (
<>
{status === 'loading' && <Loading />}
{status === 'error' && <ErrorMsg />}
{status === 'success' && <NewProjectForm authToken={authToken} />}
</>
)
}
export default ProjectAddView

View file

@ -0,0 +1,255 @@
import { Input, Label, Text, Flex, Box, Grid } from 'theme-ui'
import ProjectView from './project-view'
import useForm from '../../../lib/use-form'
import Submit from '../../submit'
import { useState } from 'react'
import Icon from '@hackclub/icons'
// import FileInput from '../../../pages/api/arcade/showcase/projects/[projectID]/file-input'
/** @jsxImportSource theme-ui */
const ProjectEditForm = ({ project }) => {
// const [previewProject, setPreviewProject] = useState(project)
const [screenshot, setScreenshot] = useState(project.screenshot)
const [newScreenshot, setNewScreenshot] = useState('')
const [video, setVideo] = useState(project.video)
const [newVideo, setNewVideo] = useState('')
function publishedChanges(e) {
console.log('published changes', e)
}
const { status, formProps, useField, data } = useForm(
`/api/arcade/showcase/projects/${project.id}/edit/`,
publishedChanges,
{
method: 'PATCH',
initData: { ...project, recordId: project.id },
bearer: window.localStorage.getItem('arcade.authToken'),
clearOnSubmit: null
}
)
const updateScreenshot = newMedia => {
if (screenshot.some(item => item === newMedia)) {
alert('This media already exists and cannot be added.')
return
}
setScreenshot(screenshot => [...screenshot, newMedia])
}
const deleteScreenshot = deletedMedia => {
setScreenshot(screenshot.filter(item => !item.includes(deletedMedia)))
}
const updateNewScreenshot = e => {
setNewScreenshot(e.target.value)
}
const updateVideo = newMedia => {
if (video.some(item => item === newMedia)) {
alert('This media already exists and cannot be added.')
return
}
setVideo(video => [...video, newMedia])
}
const deleteVideo = deletedMedia => {
setVideo(video.filter(item => !item.includes(deletedMedia)))
}
const updateNewVideo = e => {
setNewVideo(e.target.value)
}
const previewProject = {
...data
}
return (
<Box
sx={{
width: '90vw',
maxWidth: '1200px',
margin: 'auto',
position: 'relative',
my: 5
}}
>
<Text
variant="subtitle"
className="slackey"
as="h3"
sx={{
textAlign: 'center',
display: 'flex',
width: '100%',
mb: 2,
color: '#333'
}}
>
<Icon glyph="edit" />
Editing {project.title} details
</Text>
<Text
as="a"
href="/arcade/showcase/my"
sx={{
border: '2px dashed #333',
borderRadius: '5px',
position: ['relative', 'relative', 'absolute'],
display: 'flex',
right: 0,
top: 0,
justifyContent: 'center',
alignItems: 'center',
px: 2,
py: 1,
transitionDuration: '0.4s',
cursor: 'pointer',
textDecoration: 'none',
mb: 3,
'&:hover': {
background: '#333',
color: '#f8e4c4'
}
}}
>
<Icon glyph="home" /> View all my ships
</Text>
<Grid
className="gaegu"
sx={{
backgroundColor: '#F4E7C7',
p: 4,
borderRadius: '10px',
gridTemplateColumns: ['1fr', '1fr 2fr']
}}
>
<form {...formProps}>
<Label>
<Text>Project name</Text>
<Input
{...useField('title')}
placeholder="Arcade"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>ReadMe Link</Text>
<Input
{...useField('readMeLink')}
placeholder="https://github.com/hackclub/arcade/README.md"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>Repo Link</Text>
<Input
{...useField('codeLink')}
placeholder="https://github.com/hackclub/arcade"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>Play Link</Text>
<Input
{...useField('playLink')}
placeholder="https://hackclub.com/arcade"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>Screenshot link</Text>
<Text variant="caption">
Demo your work! No hosted link? Try{' '}
<a href="https://hackclub.slack.com/archives/C016DEDUL87">#cdn</a>{' '}
or <a href="https://tmpfiles.org/?upload">tmpfiles</a>
</Text>
<Input
{...useField('screenshot')}
type="url"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>Video link</Text>
<Text variant="caption">
Add a link to your demo video! Need a host? Try{' '}
<a href="https://hackclub.slack.com/archives/C016DEDUL87">#cdn</a>{' '}
or <a href="https://tmpfiles.org/?upload">tmpfiles</a>
</Text>
<Input
{...useField('video')}
type="url"
sx={{ border: '1px dashed', borderColor: '#09AFB4', mb: 2 }}
/>
</Label>
<Label>
<Text>Background Color</Text>
<Input
{...useField('color')}
type="color"
// value={color}
// onChange={handleColorChange}
sx={{
width: '150px',
height: '50px',
padding: '0',
backgroundColor: 'transparent',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
zIndex: 1,
position: 'relative'
}}
/>
</Label>
<Label>
<Text>Text Color</Text>
<Input
{...useField('textColor')}
type="color"
sx={{
width: '150px',
height: '50px',
padding: '0',
backgroundColor: 'transparent',
border: 'none',
borderRadius: '8px',
cursor: 'pointer',
zIndex: 1,
position: 'relative'
}}
/>
</Label>
<Input {...useField('authToken')} type="hidden" />
<Submit
status={status}
labels={{
default: 'Publish changes',
error: 'Something went wrong',
success: 'Updated!'
}}
sx={{
borderRadius: '10px'
}}
/>
</form>
<Box
sx={{
// backgroundColor: color,
border: '2px dashed #09AFB4',
borderRadius: '5px'
}}
>
<ProjectView {...previewProject} />
</Box>
</Grid>
</Box>
)
}
export default ProjectEditForm

View file

@ -0,0 +1,242 @@
import styles from './project-view.module.css'
import { useState, useEffect } from 'react'
import randomNotFoundImg from './random-not-found-img'
import { Button, Text } from 'theme-ui'
import Icon from '@hackclub/icons'
import ReadmeRenderer from './readme-renderer'
/** @jsxImportSource theme-ui */
function darkenColor(hex, factor) {
let r = parseInt(hex.substring(1, 3), 16)
let g = parseInt(hex.substring(3, 5), 16)
let b = parseInt(hex.substring(5, 7), 16)
r = Math.floor(r * factor)
g = Math.floor(g * factor)
b = Math.floor(b * factor)
return (
'#' +
('0' + r.toString(16)).slice(-2) +
('0' + g.toString(16)).slice(-2) +
('0' + b.toString(16)).slice(-2)
)
}
function invertColor(hex) {
hex = hex.replace(/^#/, '')
let r = parseInt(hex.substring(0, 2), 16)
let g = parseInt(hex.substring(2, 4), 16)
let b = parseInt(hex.substring(4, 6), 16)
r = (255 - r).toString(16).padStart(2, '0')
g = (255 - g).toString(16).padStart(2, '0')
b = (255 - b).toString(16).padStart(2, '0')
return `#${r}${g}${b}`
}
const ProjectView = ({
id,
title = 'Title Not Found',
desc = 'Description Not Found',
slack = 'Slack Not Found',
scrapbook = '',
playLink,
images = [],
githubProf,
user = 'User Not Found',
codeLink = '',
color = '',
textColor = '',
screenshot = '',
video = '',
readMeLink = '',
...props
}) => {
const [darkColor, setDarkColor] = useState('#000000')
const [invertedColor, setInvertedColor] = useState('#000000')
const codeHost = codeLink.includes('github')
? 'View on GitHub'
: 'View project source'
const image = screenshot.length > 1 ? screenshot : [randomNotFoundImg(id)]
useEffect(() => {
setDarkColor(darkenColor(color, 0.8))
setInvertedColor(invertColor(textColor))
}, [color])
function convertToRawUrl(githubUrl) {
if (!githubUrl.includes('github.com')) {
// throw new Error('Invalid GitHub URL')
return ''
}
return githubUrl
.replace('github.com', 'raw.githubusercontent.com')
.replace('/blob/', '/')
}
const [markdown, setMarkdown] = useState('')
useEffect(() => {
const fetchMarkdown = async () => {
const rawReadMeLink = convertToRawUrl(readMeLink)
if (rawReadMeLink) {
try {
const res = await fetch(rawReadMeLink)
const text = await res.text()
setMarkdown(text)
} catch (error) {
console.error('Error fetching markdown:', error)
setMarkdown('Failed to load markdown content')
}
}
}
fetchMarkdown()
}, [readMeLink])
return (
<div
{...props}
className="gaegu"
sx={{ position: 'relative', backgroundColor: color, color: textColor }}
>
<div
sx={{
py: 4,
backgroundColor: darkColor,
textAlign: 'center',
color: textColor
}}
>
<h1 className="slackey">{title}</h1>
<h3>By {user}</h3>
<Text
as="a"
href="/arcade/showcase/my"
sx={{
border: `2px dashed ${textColor}`,
borderRadius: '5px',
position: ['relative', 'relative', 'absolute'],
display: 'flex',
left: '10px',
top: '10px',
justifyContent: 'center',
alignItems: 'center',
px: 2,
py: 1,
transitionDuration: '0.4s',
cursor: 'pointer',
textDecoration: 'none',
mb: 3,
'&:hover': {
background: textColor || '#333',
color: invertedColor || '#F4E7C7'
}
}}
>
<Icon glyph="home" /> View all my ships
</Text>
</div>
<div
sx={{
width: '90%',
margin: 'auto',
my: 3,
maxWidth: '800px',
}}
>
<div
sx={{
display: 'grid',
flexWrap: 'wrap',
gridTemplateColumns:
screenshot != '' && video != ''
? ['1fr', '1fr 1fr', '1fr 1fr']
: '1fr',
gap: '10px'
}}
>
{ image != '' && (
<div
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<img
src={image}
alt="Project Image"
className={styles.image}
/>
</div>
)}
{ video != '' && (
<div
sx={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<video sx={{ width: '100%', height: 'auto' }} controls>
<source src={video} type="video/mp4" />
Your browser does not support the video tag.
</video>
</div>
)}
</div>
<p
className={styles.description}
sx={{ textAlign: screenshot.length != 1 ? 'center' : 'left' }}
>
<ReadmeRenderer markdown={markdown} />
</p>
</div>
<div
className={styles.buttonGroup}
sx={{ width: '90%', margin: 'auto', pt: 1, pb: 5 }}
>
{playLink && (
<Button
as="a"
sx={{
backgroundColor: '#FF5C00',
color: '#ebebeb',
textSizeAdjust: '16px',
borderRadius: '10px'
}}
href={playLink}
target="_blank"
rel="noopener"
>
Play Now
</Button>
)}
<Button
as="a"
sx={{
backgroundColor: '#09AFB4',
color: '#ebebeb',
textSizeAdjust: '16px',
borderRadius: '10px'
}}
href={codeLink}
target="_blank"
rel="noopener"
>
{codeHost}
</Button>
</div>
</div>
)
}
export default ProjectView

View file

@ -0,0 +1,68 @@
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.title {
font-size: 2.5rem;
margin-bottom: 20px;
text-align: center;
color: #333;
}
.image {
max-width: 24em;
max-height: 100%;
/* width: auto; */
height: auto;
border-radius: 8px;
margin: 0 auto;
}
.description {
font-size: 1.2rem;
/* color: #363636; */
margin-bottom: 30px;
line-height: 1.6;
font-weight: 500;
}
.buttonGroup {
display: flex;
justify-content: center;
gap: 20px;
}
.button {
padding: 10px 20px;
background-color: #0070f3;
color: #fff;
text-decoration: none;
border-radius: 5px;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: #005bb5;
}
.min{
min-height: 800px;
text-align: center;
}
.loading{
font-size: 2.5rem;
font-weight: 800px;
margin-bottom: 20px;
padding-top: 100px;
}
@media screen and (max-width: 500px) {
.buttonGroup {
flex-direction: column;
}
}

View file

@ -0,0 +1,19 @@
const notFoundImgs = [
'https://cloud-6laa73jem-hack-club-bot.vercel.app/0not_found5.png',
'https://cloud-6laa73jem-hack-club-bot.vercel.app/1not_found4.png',
'https://cloud-6laa73jem-hack-club-bot.vercel.app/2not_found3.png',
'https://cloud-6laa73jem-hack-club-bot.vercel.app/3not_found2.png',
'https://cloud-6laa73jem-hack-club-bot.vercel.app/4not_found1.png'
]
const hashCode = (s='key') =>
s.split('').reduce((a, b) => {
a = (a << 5) - a + b.charCodeAt(0)
return a & a
}, 0)
const randomNotFoundImg = key => {
return notFoundImgs[hashCode(key) % notFoundImgs.length]
}
export default randomNotFoundImg

View file

@ -0,0 +1,14 @@
import ReactMarkdown from "react-markdown"
import remarkGfm from "remark-gfm"
import style from "./readme-renderer.module.css"
const ReadmeRenderer = ({ markdown }) => {
return (
<ReactMarkdown
className={style.reactMarkDown}
remarkPlugins={[remarkGfm]}
children={markdown}
/>
)
}
export default ReadmeRenderer

View file

@ -0,0 +1,3 @@
.reactMarkDown img {
max-width: 100%;
}

View file

@ -66,7 +66,7 @@ const Footer = () => {
clipRule="evenodd"></path>
</svg>
<div className={styles.footer_icons_container}>
<a target="_self" rel="noopener me" href="/slack" title="Hack Club on Slack">
<a target="_self" rel="noopener me" href="https://hackclub.com/slack" title="Hack Club on Slack">
<svg fillRule="evenodd" clipRule="evenodd" strokeLinejoin="round"
strokeMiterlimit="1.414" xmlns="http://www.w3.org/2000/svg" aria-label="slack-fill"
viewBox="0 0 32 32" preserveAspectRatio="xMidYMid meet" fill="currentColor" width="32"

View file

@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'
const useForm = (
submitURL = '/',
callback,
options = { clearOnSubmit: 5000, method: 'POST', initData: {} }
options = { clearOnSubmit: 5000, method: 'POST', initData: {}, bearer: null }
) => {
const [status, setStatus] = useState('default')
const [data, setData] = useState({ ...options.initData })
@ -39,11 +39,18 @@ const useForm = (
const onSubmit = e => {
e.preventDefault()
setStatus('submitting')
let header = {}
if (options.bearer) {
header = {
Authorization: `Bearer ${options.bearer}`
}
}
fetch(action, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
'Content-Type': 'application/json',
...header
},
body: JSON.stringify(data)
})
@ -51,17 +58,12 @@ const useForm = (
const response = await r.json()
if (r.ok) {
setStatus('success')
if (callback) callback(r)
if (callback) callback(response)
if (options.clearOnSubmit) {
setTimeout(() => {
setData({})
setStatus('default')
}, options.clearOnSubmit)
} else {
setTimeout(() => {
setData({})
setStatus('default')
}, 3500)
}
} else {
setStatus('error')

View file

@ -60,12 +60,15 @@
"nextjs-current-url": "^1.0.3",
"openai": "^4.42.0",
"pcb-stackup": "^4.2.8",
"rc-dialog": "^9.5.2",
"react": "^17.0.2",
"react-before-after-slider-component": "^1.1.8",
"react-countdown": "^2.3.6",
"react-datepicker": "^4.24.0",
"react-dom": "^17.0.2",
"react-horizontal-scrolling-menu": "^6.0.2",
"react-konami-code": "^2.3.0",
"react-markdown": "^8",
"react-marquee-slider": "^1.1.5",
"react-masonry-css": "^1.0.16",
"react-page-visibility": "^7.0.0",
@ -82,6 +85,7 @@
"react-wrap-balancer": "^1.1.0",
"recharts": "2.12.2",
"remark": "^15.0.1",
"remark-gfm": "^3.0.1",
"remark-html": "^16.0.1",
"styled-components": "^6.1.8",
"swr": "^2.2.4",

View file

@ -0,0 +1,33 @@
import AirtablePlus from "airtable-plus"
const fetchPosts = async () => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: 'Projects',
})
const records = await airtable.read({
filterByFormula: '{Status} = "Shipped"'
})
return records.map(record => ({
id: record.id,
name: record.fields["Name"],
desc: record.fields["Description"],
slack: record.fields["Slack Handle"],
codeLink: record.fields["Github Link"],
playLink: record.fields["Playable Link"],
images: record.fields["Screenshot / Video"],
}))
}
export default async function handler(req, res) {
try {
const data = await fetchPosts();
res.status(200).json(data);
} catch (error) {
console.error(error)
res.status(500).json({ error: 'Failed to fetch posts' });
}
}

View file

@ -0,0 +1,36 @@
import AirtablePlus from "airtable-plus";
import { ensureAuthed } from "../login/test";
export default async function handler(req, res) {
const user = await ensureAuthed(req)
if (user.error) {
return res.status(401).json(user)
}
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: "Showcase"
})
const projects = await airtable.read({
filterByFormula: `AND({User} = '${user.fields['Name']}', NOT({deleted}))`
})
const results = projects.map(p => ({
id: p.id,
title: p.fields['Name'] || '',
desc: p.fields['Description'] || '',
slackLink: p.fields['Slack Link'] || '',
codeLink: p.fields['Code Link'] || '',
slackLink: p.fields['Slack Link'] || '',
playLink: p.fields['Play Link'] || '',
// images: (p.fields['Screenshot'] || []).map(i => i.url),
imageLink: p.fields['ScreenshotLink'] || '',
githubProf: p.fields['Github Profile'] || '',
user: user.fields['Name'],
color: p.fields['color'] || '',
textColor: p.fields['textColor'] || ''
}))
return res.status(200).json({ projects: results, name: user.fields['Name']})
}

View file

@ -0,0 +1,53 @@
import AirtablePlus from "airtable-plus"
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: "Users"
})
async function getUserFromLogin(loginToken) {
// only alphanumeric & '-' characters are allowed in the token
const safeLoginToken = loginToken.replace(/[^a-zA-Z0-9-]/g, '')
const results = await airtable.read({
filterByFormula: `{Login Token} = '${safeLoginToken}'`,
maxRecords: 1
})
return results[0]
}
async function scrubLoginToken(userID) {
console.log(`Scrubbing login token for user ${userID}`)
await airtable.update(userID, {
'Login Token': ''
})
}
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ error: "Method not allowed" })
}
const { token } = req.query
if (!token) {
return res.status(400).json({ error: "Token is required" })
}
const user = await getUserFromLogin(token)
if (!user) {
return res.status(404).json({ error: "User not found" })
}
const authToken = user.fields['Auth Token']
if (!authToken) {
return res.status(500).json({ error: "Auth Token not found" })
}
await scrubLoginToken(user.id)
// return back the user's AuthToken
res.status(200).json({ authToken })
}

View file

@ -0,0 +1,37 @@
import AirtablePlus from "airtable-plus"
export const testAuth = async (authToken) => {
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: "Users"
})
const safeAuthToken = authToken.replace(/[^a-zA-Z0-9-]/g, '')
const results = await airtable.read({
filterByFormula: `AND(NOT({Auth Token} = BLANK()), {Auth Token} = '${safeAuthToken}')`,
maxRecords: 1
})
return results[0]
}
export const ensureAuthed = async (req) => {
const authToken = req.headers['authorization']?.replace('Bearer ', '')
const user = await testAuth(authToken || '')
if (!user) {
return { error: "User not found" }
}
return user
}
export default async function handler(req, res) {
// example of how to ensure a request is authenticated
const result = await ensureAuthed(req)
if (result.error) {
return res.status(401).json(result)
} else {
return res.status(200).json(result)
}
}

View file

@ -0,0 +1,34 @@
import AirtablePlus from "airtable-plus";
import { ensureAuthed } from "../../login/test";
export default async function handler(req, res) {
const user = await ensureAuthed(req);
if (user.error) {
return res.status(401).json(user);
}
const body = req.body;
if (!body) {
return res.status(400).json({ error: "No body provided" });
}
const updatedFields = { deleted: true };
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: "Showcase"
});
const { projectID } = req.query;
try {
await airtable.update(projectID, updatedFields);
// No content returned, only status code 204 for success
return res.status(204).end();
} catch (error) {
console.error("Error updating project:", error);
return res.status(500).json({ error: "Failed to update project" });
}
}

View file

@ -0,0 +1,57 @@
import AirtablePlus from 'airtable-plus'
import { ensureAuthed } from '../../login/test'
export default async function handler(req, res) {
const user = await ensureAuthed(req)
if (user.error) {
return res.status(401).json(user)
}
const body = req.body
if (!body) {
return res.status(400).json({ error: 'No body provided' })
}
const updatedFields = {}
updatedFields['Name'] = body.title
updatedFields['Description'] = body.desc
updatedFields['Slack Link'] = body.slackLink
updatedFields['Code Link'] = body.codeLink
updatedFields['Play Link'] = body.playLink
updatedFields['Screenshot'] = [body.screenshot].map(i => ({ url: i }))
updatedFields['color'] = body.color
updatedFields['textColor'] = body.textColor
updatedFields['ScreenshotLink'] = body.screenshot
updatedFields['VideoLink'] = body.video
updatedFields['ReadMeLink'] = body.readMeLink
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: 'Showcase'
})
const { projectID } = req.query
const project = await airtable.update(projectID, updatedFields)
const results = {
id: project.id,
title: project.fields['Name'] || '',
desc: project.fields['Description'] || '',
slackLink: project.fields['Slack Link'] || '',
codeLink: project.fields['Code Link'] || '',
slackLink: project.fields['Slack Link'] || '',
playLink: project.fields['Play Link'] || '',
// images: (project.fields['Screenshot'] || []).map(i => i.url),
user: user.fields['Name'],
githubProf: project.fields['Github Profile'] || '',
color: project.fields['color'] || '',
textColor: project.fields['textColor'] || '',
screenshot: project.fields['ScreenshotLink'] || '',
video: project.fields['VideoLink'] || '',
readMeLink: project.fields['ReadMeLink'] || ''
}
return res.status(200).json({ project: results })
}

View file

@ -0,0 +1,40 @@
import { useState } from 'react'
const FileInput = ({ onUpload = () => {} }) => {
const [status, setStatus] = useState('')
return (
<>
<input
type="file"
onChange={async event => {
event.preventDefault()
const formData = new FormData()
if (event.target.files.length === 0) {
setStatus('No file selected')
return
}
setStatus('Uploading...')
formData.append('file', event.target.files[0])
const response = await fetch('/api/bucky/', {
method: 'POST',
body: formData
})
// Handle response if necessary
const data = await response.json()
console.log({ data })
if (data.result) {
setStatus('Uploaded!')
onUpload(data.result)
}
}}
/>
<p>{status}</p>
</>
)
}
export default FileInput

View file

@ -0,0 +1,60 @@
import AirtablePlus from 'airtable-plus'
import { ensureAuthed } from '../../login/test'
export default async function handler(req, res) {
const user = await ensureAuthed(req)
if (user.error) {
return res.status(401).json(user)
}
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: 'Showcase'
})
const { projectID } = req.query
const projects = await airtable.read({
filterByFormula: `AND({User} = '${user.fields['Name']}', RECORD_ID() = '${projectID}')`,
maxRecords: 1
})
const p = projects[0]
if (!p) {
return res.status(404).json({ error: 'Project not found' })
}
let screenshot
try {
screenshot = JSON.parse(p.fields['ScreenshotLinks'])
} catch (e) {
screenshot = []
}
let video
try {
video = JSON.parse(p.fields['VideoLinks'])
} catch (e) {
video = []
}
const results = {
id: p.id,
title: p.fields['Name'] || '',
desc: p.fields['Description'] || '',
slackLink: p.fields['Slack Link'] || '',
codeLink: p.fields['Code Link'] || '',
slackLink: p.fields['Slack Link'] || '',
playLink: p.fields['Play Link'] || '',
images: (p.fields['Screenshot'] || []).map(i => i.url),
githubProf: p.fields['Github Profile'] || '',
user: user.fields['Name'],
color: p.fields['color'] || '',
textColor: p.fields['textColor'] || '',
screenshot: p.fields['ScreenshotLink'] || '',
video: p.fields['VideoLink'] || '',
readMeLink: p.fields['ReadMeLink'] || ''
}
return res.status(200).json({ project: results })
}

View file

@ -0,0 +1,49 @@
import AirtablePlus from 'airtable-plus'
import { ensureAuthed } from '../login/test'
export default async function handler(req, res) {
const authToken = req.body?.authToken
if (!authToken) {
return res.status(401).json({ error: 'No auth token provided' })
}
const user = await ensureAuthed({
headers: { authorization: `Bearer ${authToken}` }
})
if (user.error) {
return res.status(401).json(user)
}
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: 'Showcase'
})
if (!req.body.codeLink) {
return res.status(400).json({ error: 'No code link provided' })
}
const org = req.body.codeLink?.split('/')?.[3]
const name = req.body.codeLink?.split('/')?.slice(-1)?.[0]
const ghData = await fetch(
`https://api.github.com/repos/${org}/${name}`
).then(r => r.json())
const description = ghData.description || ''
const playLink = ghData.homepage || ''
const readmeData = await fetch(
`https://api.github.com/repos/${org}/${name}/readme`
).then(r => r.json())
const readmeLink = readmeData.download_url || ''
const project = await airtable.create({
User: [user.id],
'Code Link': req.body.codeLink,
Name: name,
Description: description,
'Play Link': playLink,
color: '#FAEFD6',
ReadMeLink: readmeLink
})
return res.status(200).json({ project: project.id })
}

View file

@ -0,0 +1,36 @@
import AirtablePlus from "airtable-plus";
import { ensureAuthed } from "../login/test";
export default async function handler(req, res) {
const user = await ensureAuthed(req)
if (user.error) {
return res.status(401).json(user)
}
const airtable = new AirtablePlus({
apiKey: process.env.AIRTABLE_API_KEY,
baseID: 'app4kCWulfB02bV8Q',
tableName: "Showcase"
})
const projects = await airtable.read({
filterByFormula: `AND({User} = '${user.fields['Name']}', NOT({deleted}))`
})
const results = projects.map(p => ({
id: p.id,
title: p.fields['Name'] || '',
desc: p.fields['Description'] || '',
slackLink: p.fields['Slack Link'] || '',
codeLink: p.fields['Code Link'] || '',
slackLink: p.fields['Slack Link'] || '',
playLink: p.fields['Play Link'] || '',
// images: (p.fields['Screenshot'] || []).map(i => i.url),
imageLink: p.fields['ScreenshotLink'] || '',
githubProf: p.fields['Github Profile'] || '',
user: user.fields['Name'],
color: p.fields['color'] || '',
textColor: p.fields['textColor'] || ''
}))
return res.status(200).json({ projects: results, name: user.fields['Name']})
}

10
pages/api/bucky.js Normal file
View file

@ -0,0 +1,10 @@
export default async function handler(req, res) {
const result = await fetch("https://bucky.hackclub.com", {
method: 'POST',
body: req.body,
headers: {
'Content-Type': req.headers['content-type']
}
}).then(r => r.text())
res.status(200).json({ result })
}

100
pages/arcade/gallery.js Normal file
View file

@ -0,0 +1,100 @@
import React from 'react'
import Nav from '../../components/Nav'
import Footer from '../../components/arcade/Footer'
import BGImg from '../../components/background-image'
import background from '../../public/home/assemble.jpg'
import { Badge, Box, Button, Card, Container, Grid, Heading, Link, Text } from 'theme-ui'
import SlideDown from '../../components/slide-down'
import Post from '../../components/arcade/showcase/post'
import styles from '../../components/arcade/showcase/post.module.css'
import CohortCard from '../../components/arcade/showcase/cohort-card'
export async function getStaticProps() {
const host = process.env.NODE_ENV === 'development' ? 'http://localhost:3000' : 'https://hackclub.com';
const res = await fetch(`${host}/api/arcade/gallery`);
const posts = await res.json();
const filteredPosts = posts;
return {
props: { posts: filteredPosts,
},
};
}
const gallery = ({ posts }) => {
console.log(posts);
return (
<section>
<Nav />
<BGImg
src={background}
alt="Arcade Gallery BG Img"
priority
/>
<SlideDown duration={768}>
<Heading
as="h1"
variant="ultratitle"
sx={{
color: 'white',
textShadow: 'text',
filter: 'drop-shadow(0 -2px 4px rgba(0,0,0,0.5))',
WebkitFilter: 'drop-shadow(0 -2px 4px rgba(0,0,0,0.5))',
maxWidth: [null, 'copyUltra'],
my: [3, 4],
mx: 'auto',
zIndex: 1
}}
>
<Text
as="span"
sx={{
WebkitTextStroke: 'currentColor',
WebkitTextStrokeWidth: ['2px', '3px'],
WebkitTextFillColor: 'transparent'
}}
>
Arcade Gallery
</Text>
<br />
<Button
as="a"
variant="ctaLg"
href="https://apply.hackclub.com"
target="_blank"
rel="noopener"
>
Add a Project
</Button>
</Heading>
</SlideDown>
<div className={styles.feed}>
<CohortCard/>
{posts.map(post => {
return (
<Post
id={post.id}
title={post.name}
desc={post.desc}
slack={post.slack}
codeLink={post.codeLink}
playable={post.playable}
images={post.images}
githubProf={""}
key={post.id}
/>)
})}
</div>
<Footer />
</section>
)
}
export default gallery

View file

@ -0,0 +1,9 @@
import React from 'react'
const index = () => {
return (
<div>index</div>
)
}
export default index

View file

@ -0,0 +1,64 @@
import { useEffect, useState } from 'react'
const sample = arr => arr[Math.floor(Math.random() * arr.length)]
const languages = "Python Rust COBOL Wasm tailwind ".split(" ")
const tinyEyes = [
"if you can see this, you're too close",
"what are you looking at, tiny-eyes?",
"I see you",
"What is this, a website for ants?",
"plz help, my font size has fallen and it can't get up",
"*small loading sounds*"
]
const flavorText = [
`I would've been faster written in ${sample(languages)}`,
'Wait your turn!',
'Form an orderly queue!',
"I'm a teapo WAIT WRONG ENDPOINT",
"GET outta here with that request!",
"PUT that request back where it came from or so help me",
"POST haste!",
"TODO: Delete this message",
<p style={{fontSize: "3px"}} key="tinyEyes">{sample(tinyEyes)}</p>,
"Caution: objects in loading box are slower than they appear",
"Caution: wet pixels, do not touch",
"*Fax machine noises*",
]
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const LoginPage = ({token}) => {
const [ status, setStatus ] = useState('Loading...')
useEffect(async () => {
const minWaitTime = sleep(3 * 1000)
let data = {}
const getTokenPromise = new Promise(async resolve => {
const response = await fetch(`/api/arcade/showcase/login/${token}`, {method: 'POST'})
data = await response.json()
resolve()
})
const [ _wait, _data ] = await Promise.all([minWaitTime, getTokenPromise])
if (data.error) {
setStatus(data.error)
} else {
setStatus("Redirecting!")
window.localStorage.setItem('arcade.authToken', data.authToken)
await sleep(250)
window.location.href = '/arcade/showcase/my'
}
}, [])
return (
<div>
<p>{status}</p>
<p><em>{sample(flavorText)}</em></p>
</div>
)
}
export default LoginPage
export function getServerSideProps(context) {
const { token } = context.query
return { props: { token } }
}

351
pages/arcade/showcase/my.js Normal file
View file

@ -0,0 +1,351 @@
import { useEffect, useState, useRef } from 'react'
import CohortCard from '../../../components/arcade/showcase/cohort-card'
import ProjectView from '../../../components/arcade/showcase/project-view'
import Nav from '../../../components/Nav'
import Footer from '../../../components/arcade/Footer'
import BGImg from '../../../components/background-image'
import background from '../../../public/arcade/homeBG.svg'
import { Button, Heading, Text, Box, Close } from 'theme-ui'
import SlideDown from '../../../components/slide-down'
import styles from '../../../components/arcade/showcase/my.module.css'
import Countdown from 'react-countdown'
import { StyleSheetContext } from 'styled-components'
import Icon from '@hackclub/icons'
import Flag from '../../../components/flag'
import ProjectAddView from '../../../components/arcade/showcase/project-add'
/** @jsxImportSource theme-ui */
const styled = `
@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap');
body, html {
overflow-x: hidden;
}
.slackey {
font-family: "Slackey", sans-serif;
}
.emblema {
font-family: "Emblema One", system-ui;
}
.gaegu {
font-family: "Gaegu", sans-serif;
}
body {
background-color: #FAEFD6;
min-height: 100vh;
}
@keyframes float {
from,
to {
transform: translate(0%, -37%) rotate(-2deg);
}
25% {
transform: translate(-2%, -40%) rotate(2deg);
}
50% {
transform: translate(0%, -43%) rotate(-1deg);
}
75% {
transform: translate(-1%, -40%) rotate(-1deg);
}
}
a {
color: inherit;
}
`
const ProjectGallery = ({ projects, loadProjects }) => {
return (
<div className={styles.feed}>
<div className={styles.container}>
<Box
target="_blank"
rel="noopener"
className="gaegu"
sx={{
border: '3px dashed #09AFB4',
my: 2,
display: 'flex',
color: '#09AFB4',
borderRadius: '10px',
flexDirection: 'column',
width: '100%',
height: '100%',
textDecoration: 'none',
textAlign: 'center',
py: 2,
alignItems: 'center',
justifyContent: 'center',
cursor: 'pointer',
transitionDuration: '0.4s',
'&:hover': {
background: '#09AFB4',
color: '#F4E7C7'
}
}}
onClick={e => {
document.getElementById('add-project').showModal()
}}
>
<Icon glyph="plus" sx={{ marginX: 'auto' }} />
<Text variant="subtitle" sx={{ mt: 0 }}>
Add a Project
</Text>
</Box>
</div>
{projects.map(project => (
<CohortCard
key={project.id}
id={project.id}
title={project.title}
desc={project.desc}
imageLink={project.imageLink}
personal={true}
reload={loadProjects}
color={project.color}
textColor={project.textColor}
/>
))}
</div>
)
}
const Loading = () => (
<div
sx={{
width: '90vw',
maxWidth: '1200px',
margin: 'auto',
textAlign: 'center'
}}
>
Loading...
</div>
)
const ErrorMessage = () => (
<div
sx={{
width: '90vw',
maxWidth: '1200px',
margin: 'auto',
textAlign: 'center'
}}
>
There was an error loading your projects.
</div>
)
const My = () => {
const [projects, setProjects] = useState([])
const [name, setName] = useState('')
const [status, setStatus] = useState('loading')
const [errorMsg, setError] = useState(null)
const launchDate = new Date(2024, 7, 19, 0, 0, 0, 0)
const renderer = ({ hours, minutes, seconds, completed }) => {
if (completed) {
// Render a completed state
return (
<div sx={{ width: '100%' }}>
<Button
to="https://hackclub.com/arcade/showcase/vote/"
sx={{
backgroundColor: '#FF5C00',
color: '#FAEFD6',
borderRadius: '5px',
border: 'none',
px: '20px',
transitionDuration: '0.3s',
'&:hover': {
transform: 'scale(1.05)'
},
width: 'fit-content'
}}
className="gaegy"
>
Click here to vote now
</Button>
</div>
)
} else {
// Render a countdown
return (
<span sx={{ color: '#FF5C00' }}>
First voting round in {hours > 0 ? `${hours} hours` : ''}{' '}
{minutes > 0 ? `${minutes} minutes` : ''} {seconds} seconds
</span>
)
}
}
// Spotlight effect
const spotlightRef = useRef()
useEffect(() => {
const handler = event => {
var rect = document.getElementById('spotlight').getBoundingClientRect()
var x = event.clientX - rect.left //x position within the element.
var y = event.clientY - rect.top //y position within the element.
spotlightRef.current.style.background = `radial-gradient(
circle at ${x}px ${y}px,
rgba(132, 146, 166, 0) 20px,
rgba(250, 239, 214, 0.9) 120px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
const loadProjects = async () => {
const token = window.localStorage.getItem('arcade.authToken')
const response = await fetch('/api/arcade/showcase/projects/my', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
}).catch(e => {
console.error(e)
setStatus('error')
setError(e)
})
const data = await response.json()
if (data.error) {
setStatus('error')
return
} else {
setProjects(data.projects)
setName(data.name)
setStatus('success')
}
}
useEffect(async () => {
loadProjects()
}, [])
return (
<section>
<Box
id="spotlight"
as="section"
sx={{
backgroundImage: `
linear-gradient(rgba(250, 239, 214, 0.7), rgba(250, 239, 214, 0.7)),
url('https://cloud-o19rieg4g-hack-club-bot.vercel.app/0group_495__1_.svg')
`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
position: 'relative',
minHeight: '100vh'
}}
>
<Box
ref={spotlightRef}
sx={{
position: 'absolute',
zIndex: 2,
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: '#FAEFD6',
pointerEvents: 'none'
}}
/>
<div sx={{ zIndex: 5, position: 'relative' }}>
<img
src="https://cloud-677i45opw-hack-club-bot.vercel.app/0arcade_1.png"
sx={{
width: '30%',
maxWidth: '200px',
position: 'absolute',
top: '20px',
right: '20px'
}}
/>
<SlideDown duration={768}>
<Heading
sx={{
maxWidth: ['90vw', 'copyUltra'],
py: 5,
zIndex: 1,
mx: 'auto',
textAlign: 'center',
display: 'block'
}}
>
<Text className="gaegu" sx={{ color: '#FF5C00' }}>
{status == 'success' ? `Welcome, ${name}` : ''}
</Text>
<div>
<Text
as="h1"
variant="title"
className="slackey"
sx={{
color: '#FF5C00',
mb: 3
}}
>
Your Ships
</Text>
</div>
<div
sx={{
display: 'flex',
flexDirection: 'column',
textAlign: 'center'
}}
className="gaegu"
>
<Countdown date={launchDate} renderer={renderer} />
</div>
</Heading>
</SlideDown>
{status == 'loading' && <Loading />}
{status == 'error' && <ErrorMessage />}
{status == 'success' && (
<ProjectGallery projects={projects} loadProjects={loadProjects} />
)}
<dialog
id="add-project"
sx={{ borderRadius: '10px', border: '3px dashed #09AFB4' }}
className="gaegu"
>
<ProjectAddView />
<Close
sx={{
'&:hover': { cursor: 'pointer' },
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 2,
color: '#09AFB4'
}}
onClick={e => {
document.getElementById('add-project').close()
}}
/>
</dialog>
</div>
</Box>
<style>{styled}</style>
</section>
)
}
export default My

View file

@ -0,0 +1,147 @@
import { useState, useEffect, useRef } from 'react'
import { Box } from 'theme-ui'
import ProjectEditView from '../../../../../components/arcade/showcase/project-edit'
/** @jsxImportSource theme-ui */
const styled = `
@import url("https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap");
body, html {
overflow-x: hidden;
}
.slackey {
font-family: "Slackey", sans-serif;
}
.emblema {
font-family: "Emblema One", system-ui;
}
.gaegu {
font-family: "Gaegu", sans-serif;
}
body {
background-color: #FAEFD6;
min-height: 100vh;
}
a {
color: inherit;
}
`
const Loading = () => <p>Loading...</p>
const ErrorMsg = () => <p>There was an error loading your project!</p>
const Showcase = ({ projectID }) => {
const [status, setStatus] = useState('loading')
const [project, setProject] = useState(null)
// Spotlight effect
const spotlightRef = useRef()
useEffect(() => {
const handler = event => {
var rect = document.getElementById('spotlight').getBoundingClientRect()
var x = event.clientX - rect.left //x position within the element.
var y = event.clientY - rect.top //y position within the element.
spotlightRef.current.style.background = `radial-gradient(
circle at ${x}px ${y}px,
rgba(132, 146, 166, 0) 20px,
rgba(250, 239, 214, 0.9) 120px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
useEffect(() => {
const authToken = window.localStorage.getItem('arcade.authToken')
fetch(`/api/arcade/showcase/projects/${projectID}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${authToken}`
}
})
.then(res => res.json())
.then(data => {
if (data.error) {
throw new Error(data.error)
}
if (data.project === null) {
throw new Error('Project not found')
}
setProject(data.project)
setStatus('success')
})
.catch(e => {
console.error(e)
setStatus('error')
})
}, [])
return (
<>
<Box
id="spotlight"
as="section"
sx={{
backgroundImage: `
linear-gradient(rgba(250, 239, 214, 0.7), rgba(250, 239, 214, 0.7)),
url('https://cloud-o19rieg4g-hack-club-bot.vercel.app/0group_495__1_.svg')
`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
position: 'relative',
minHeight: '100vh'
}}
>
<Box
ref={spotlightRef}
sx={{
position: 'absolute',
zIndex: 2,
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: '#FAEFD6',
pointerEvents: 'none'
}}
/>
<div
sx={{
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
minHeight: '100vh',
zIndex: 5,
position: 'relative'
}}
>
<img
src="https://cloud-677i45opw-hack-club-bot.vercel.app/0arcade_1.png"
sx={{
width: '30%',
maxWidth: '200px',
position: 'absolute',
top: '20px',
right: '20px'
}}
/>
{status === 'loading' && <Loading />}
{status === 'error' && <ErrorMsg />}
{status === 'success' && <ProjectEditView project={project} />}
</div>
<style>{styled}</style>
</Box>
</>
)
}
export default Showcase
export function getServerSideProps(context) {
const { projectID } = context.query
return { props: { projectID } }
}

View file

@ -0,0 +1,120 @@
import { useEffect, useState, useRef } from 'react'
import ProjectView from '../../../../../components/arcade/showcase/project-view'
import Nav from '../../../../../components/Nav'
import Footer from '../../../../../components/arcade/Footer'
import BGImg from '../../../../../components/background-image'
import styles from '../../../../../components/arcade/showcase/project-view.module.css'
import { Box, Text } from 'theme-ui'
import Icon from '@hackclub/icons'
/** @jsxImportSource theme-ui */
const styled = `
@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap');
body, html {
overflow-x: hidden;
}
.slackey {
font-family: "Slackey", sans-serif;
}
.emblema {
font-family: "Emblema One", system-ui;
}
.gaegu {
font-family: "Gaegu", sans-serif;
}
body {
background-color: #FAEFD6;
min-height: 100vh;
}
a {
color: inherit;
}
`
const ProjectShowPage = ({ projectID }) => {
const Loading = () => <div className={styles.loading}>Loading...</div>
const ErrorMessage = () => (
<div>There was an error loading your projects.</div>
)
const [project, setProject] = useState([])
const [status, setStatus] = useState('loading')
const [errorMsg, setError] = useState(null)
useEffect(async () => {
const token = window.localStorage.getItem('arcade.authToken')
const response = await fetch(`/api/arcade/showcase/projects/${projectID}`, {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
}).catch(e => {
console.error(e)
setStatus('error')
setError(e)
})
const data = await response.json()
if (data.error) {
setStatus('error')
return
} else {
setProject(data.project)
setStatus('success')
}
}, [])
return (
<div>
<div sx={{ zIndex: 5, position: 'relative' }}>
<img
src="https://cloud-677i45opw-hack-club-bot.vercel.app/0arcade_1.png"
sx={{
width: '30%',
maxWidth: '200px',
position: 'absolute',
top: '20px',
right: '20px',
zIndex: 10
}}
/>
<div className={styles.min}>
{status == 'loading' && <Loading />}
{status == 'error' && <ErrorMessage />}
{status == 'success' && (
<ProjectView
key={project.id}
id={project.id}
title={project.title}
desc={project.desc}
slack={project.slackLink}
codeLink={project.codeLink}
playLink={project.playLink}
images={project.images}
githubProf={project.githubProf}
user={project.user}
color={project.color}
textColor={project.textColor}
screenshot={project.screenshot}
video={project.video}
readMeLink={project.readMeLink}
/>
)}
</div>
</div>
<style>{styled}</style>
</div>
)
}
export default ProjectShowPage
export function getServerSideProps(context) {
const { projectID } = context.query
return { props: { projectID } }
}

View file

@ -0,0 +1,278 @@
import { useEffect, useState, useRef } from 'react'
import CohortCard from '../../../../components/arcade/showcase/cohort-card'
import ProjectView from '../../../../components/arcade/showcase/project-view'
import Nav from '../../../../components/Nav'
import Footer from '../../../../components/arcade/Footer'
import BGImg from '../../../../components/background-image'
import background from '../../../../public/arcade/homeBG.svg'
import { Button, Heading, Text, Box, Close } from 'theme-ui'
import SlideDown from '../../../../components/slide-down'
import styles from '../../../../components/arcade/showcase/my.module.css'
import Countdown from 'react-countdown'
import { StyleSheetContext } from 'styled-components'
import Icon from '@hackclub/icons'
import Flag from '../../../../components/flag'
import ProjectAddView from '../../../../components/arcade/showcase/project-add'
/** @jsxImportSource theme-ui */
const styled = `
@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap');
body, html {
overflow-x: hidden;
}
.slackey {
font-family: "Slackey", sans-serif;
}
.emblema {
font-family: "Emblema One", system-ui;
}
.gaegu {
font-family: "Gaegu", sans-serif;
}
body {
background-color: #FAEFD6;
min-height: 100vh;
}
@keyframes float {
from,
to {
transform: translate(0%, -37%) rotate(-2deg);
}
25% {
transform: translate(-2%, -40%) rotate(2deg);
}
50% {
transform: translate(0%, -43%) rotate(-1deg);
}
75% {
transform: translate(-1%, -40%) rotate(-1deg);
}
}
a {
color: inherit;
}
`
const ProjectGallery = ({ projects, loadProjects }) => {
return (
<div className={styles.feed}>
{projects.map(project => (
<CohortCard
key={project.id}
id={project.id}
title={project.title}
desc={project.desc}
imageLink={project.imageLink}
personal={false}
reload={loadProjects}
draggable={true}
color={project.color}
textColor={project.textColor}
/>
))}
</div>
)
}
const Loading = () => (
<div
sx={{
width: '90vw',
maxWidth: '1200px',
margin: 'auto',
textAlign: 'center'
}}
>
Loading...
</div>
)
const ErrorMessage = () => (
<div
sx={{
width: '90vw',
maxWidth: '1200px',
margin: 'auto',
textAlign: 'center'
}}
>
There was an error loading your projects.
</div>
)
const My = () => {
const [projects, setProjects] = useState([])
const [name, setName] = useState('')
const [status, setStatus] = useState('loading')
const [errorMsg, setError] = useState(null)
// Spotlight effect
const spotlightRef = useRef()
useEffect(() => {
const handler = event => {
var rect = document.getElementById('spotlight').getBoundingClientRect()
var x = event.clientX - rect.left //x position within the element.
var y = event.clientY - rect.top //y position within the element.
spotlightRef.current.style.background = `radial-gradient(
circle at ${x}px ${y}px,
rgba(132, 146, 166, 0) 20px,
rgba(250, 239, 214, 0.9) 120px
)`
}
window.addEventListener('mousemove', handler)
return () => window.removeEventListener('mousemove', handler)
}, [])
const loadProjects = async () => {
const token = window.localStorage.getItem('arcade.authToken')
const response = await fetch('/api/arcade/showcase/projects/my', {
method: 'GET',
headers: {
Authorization: `Bearer ${token}`
}
}).catch(e => {
console.error(e)
setStatus('error')
setError(e)
})
const data = await response.json()
if (data.error) {
setStatus('error')
return
} else {
setProjects(data.projects)
setName(data.name)
setStatus('success')
}
}
useEffect(async () => {
loadProjects()
}, [])
return (
<section>
<Box
id="spotlight"
as="section"
sx={{
backgroundImage: `
linear-gradient(rgba(250, 239, 214, 0.7), rgba(250, 239, 214, 0.7)),
url('https://cloud-o19rieg4g-hack-club-bot.vercel.app/0group_495__1_.svg')
`,
backgroundSize: 'cover',
backgroundRepeat: 'no-repeat',
position: 'relative',
minHeight: '100vh'
}}
>
<Box
ref={spotlightRef}
sx={{
position: 'absolute',
zIndex: 2,
top: 0,
left: 0,
right: 0,
bottom: 0,
bg: '#FAEFD6',
pointerEvents: 'none'
}}
/>
<div sx={{ zIndex: 5, position: 'relative' }}>
<img
src="https://cloud-677i45opw-hack-club-bot.vercel.app/0arcade_1.png"
sx={{
width: '30%',
maxWidth: '200px',
position: 'absolute',
top: '20px',
right: '20px'
}}
/>
<SlideDown duration={768}>
<Heading
sx={{
maxWidth: ['90vw', 'copyUltra'],
py: 5,
zIndex: 1,
mx: 'auto',
textAlign: 'center',
display: 'block'
}}
>
<Text className="gaegu" sx={{ color: '#FF5C00' }}>
{status == 'success' ? `Welcome, ${name}` : ''}
</Text>
<div>
<Text
as="h1"
variant="title"
className="slackey"
sx={{
color: '#FF5C00',
mb: 3
}}
>
Your Cohort
</Text>
</div>
<div
sx={{
display: 'flex',
flexDirection: 'column',
textAlign: 'center'
}}
className="gaegu"
>
</div>
</Heading>
</SlideDown>
{status == 'loading' && <Loading />}
{status == 'error' && <ErrorMessage />}
{status == 'success' && (
<ProjectGallery projects={projects} loadProjects={loadProjects} />
)}
<dialog
id="add-project"
sx={{ borderRadius: '10px', border: '3px dashed #09AFB4' }}
className="gaegu"
>
<ProjectAddView />
<Close
sx={{
'&:hover': { cursor: 'pointer' },
position: 'absolute',
top: '10px',
right: '10px',
zIndex: 2,
color: '#09AFB4'
}}
onClick={e => {
document.getElementById('add-project').close()
}}
/>
</dialog>
</div>
</Box>
<style>{styled}</style>
</section>
)
}
export default My

View file

@ -0,0 +1,9 @@
import React from 'react'
const Page = () => {
return (
<div>Project</div>
)
}
export default Page

View file

@ -1,6 +1,7 @@
import React from 'react'
import BinPost from '../../components/bin/GalleryPosts'
import styles from '../../public/bin/style/gallery.module.css'
import Script from 'next/script'
import Nav from '../../components/bin/nav'
import Footer from '../../components/footer'
import PartTag from '../../components/bin/PartTag';
@ -72,7 +73,7 @@ function Gallery({ posts = [], tags = [] }) {
<section className='page'>
<div className={styles.background}></div>
<script src="https://awdev.codes/utils/hackclub/orph.js"></script>
<Script src="https://awdev.codes/utils/hackclub/orph.js"></Script>
@ -90,6 +91,7 @@ function Gallery({ posts = [], tags = [] }) {
return (
<PartTag
partID={tag.ID}
key={tag.ID}
search={true}
addFilter={addFilter}
removeFilter={removeFilter}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

1
public/arcade/gridBG.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

1
public/arcade/homeBG.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 2000 1500'><rect fill='#ffffff' width='2000' height='1500'/><defs><rect stroke='#ffffff' stroke-width='.5' width='1' height='1' id='s'/><pattern id='a' width='3' height='3' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><use fill='#fcfcfc' href='#s' y='2'/><use fill='#fcfcfc' href='#s' x='1' y='2'/><use fill='#fafafa' href='#s' x='2' y='2'/><use fill='#fafafa' href='#s'/><use fill='#f7f7f7' href='#s' x='2'/><use fill='#f7f7f7' href='#s' x='1' y='1'/></pattern><pattern id='b' width='7' height='11' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#f5f5f5'><use href='#s'/><use href='#s' y='5' /><use href='#s' x='1' y='10'/><use href='#s' x='2' y='1'/><use href='#s' x='2' y='4'/><use href='#s' x='3' y='8'/><use href='#s' x='4' y='3'/><use href='#s' x='4' y='7'/><use href='#s' x='5' y='2'/><use href='#s' x='5' y='6'/><use href='#s' x='6' y='9'/></g></pattern><pattern id='h' width='5' height='13' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#f5f5f5'><use href='#s' y='5'/><use href='#s' y='8'/><use href='#s' x='1' y='1'/><use href='#s' x='1' y='9'/><use href='#s' x='1' y='12'/><use href='#s' x='2'/><use href='#s' x='2' y='4'/><use href='#s' x='3' y='2'/><use href='#s' x='3' y='6'/><use href='#s' x='3' y='11'/><use href='#s' x='4' y='3'/><use href='#s' x='4' y='7'/><use href='#s' x='4' y='10'/></g></pattern><pattern id='c' width='17' height='13' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#f2f2f2'><use href='#s' y='11'/><use href='#s' x='2' y='9'/><use href='#s' x='5' y='12'/><use href='#s' x='9' y='4'/><use href='#s' x='12' y='1'/><use href='#s' x='16' y='6'/></g></pattern><pattern id='d' width='19' height='17' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#ffffff'><use href='#s' y='9'/><use href='#s' x='16' y='5'/><use href='#s' x='14' y='2'/><use href='#s' x='11' y='11'/><use href='#s' x='6' y='14'/></g><g fill='#efefef'><use href='#s' x='3' y='13'/><use href='#s' x='9' y='7'/><use href='#s' x='13' y='10'/><use href='#s' x='15' y='4'/><use href='#s' x='18' y='1'/></g></pattern><pattern id='e' width='47' height='53' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#00BA05'><use href='#s' x='2' y='5'/><use href='#s' x='16' y='38'/><use href='#s' x='46' y='42'/><use href='#s' x='29' y='20'/></g></pattern><pattern id='f' width='59' height='71' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#00BA05'><use href='#s' x='33' y='13'/><use href='#s' x='27' y='54'/><use href='#s' x='55' y='55'/></g></pattern><pattern id='g' width='139' height='97' patternUnits='userSpaceOnUse' patternTransform='scale(50) translate(-980 -735)'><g fill='#00BA05'><use href='#s' x='11' y='8'/><use href='#s' x='51' y='13'/><use href='#s' x='17' y='73'/><use href='#s' x='99' y='57'/></g></pattern></defs><rect fill='url(#a)' width='100%' height='100%'/><rect fill='url(#b)' width='100%' height='100%'/><rect fill='url(#h)' width='100%' height='100%'/><rect fill='url(#c)' width='100%' height='100%'/><rect fill='url(#d)' width='100%' height='100%'/><rect fill='url(#e)' width='100%' height='100%'/><rect fill='url(#f)' width='100%' height='100%'/><rect fill='url(#g)' width='100%' height='100%'/></svg>

BIN
public/arcade/plus.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 39 KiB

View file

@ -0,0 +1 @@
<svg xmlns='http://www.w3.org/2000/svg' width='100' height='100' viewBox='0 0 100 100'><rect fill='#FFA34A' width='100' height='100'/><g stroke='#CCC' stroke-width='0' stroke-opacity='1'><rect fill='#F5F5F5' x='-60' y='-60' width='110' height='240'/></g></svg>

6286
yarn.lock

File diff suppressed because it is too large Load diff