mirror of
https://github.com/System-End/site.git
synced 2026-04-19 23:22:49 +00:00
Merge branch 'arcade-gallery'
This commit is contained in:
commit
0cceaaab0b
46 changed files with 5895 additions and 3135 deletions
201
components/arcade/showcase/cohort-card.js
Normal file
201
components/arcade/showcase/cohort-card.js
Normal 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
|
||||
53
components/arcade/showcase/cohort-card.module.css
Normal file
53
components/arcade/showcase/cohort-card.module.css
Normal 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;
|
||||
}
|
||||
18
components/arcade/showcase/create-card.js
Normal file
18
components/arcade/showcase/create-card.js
Normal 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
|
||||
14
components/arcade/showcase/create-card.module.css
Normal file
14
components/arcade/showcase/create-card.module.css
Normal 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;
|
||||
|
||||
}
|
||||
66
components/arcade/showcase/my.module.css
Normal file
66
components/arcade/showcase/my.module.css
Normal 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;
|
||||
}
|
||||
61
components/arcade/showcase/post.js
Normal file
61
components/arcade/showcase/post.js
Normal 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
|
||||
88
components/arcade/showcase/post.module.css
Normal file
88
components/arcade/showcase/post.module.css
Normal 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;
|
||||
}
|
||||
86
components/arcade/showcase/project-add.js
Normal file
86
components/arcade/showcase/project-add.js
Normal 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
|
||||
255
components/arcade/showcase/project-edit.js
Normal file
255
components/arcade/showcase/project-edit.js
Normal 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
|
||||
242
components/arcade/showcase/project-view.js
Normal file
242
components/arcade/showcase/project-view.js
Normal 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
|
||||
68
components/arcade/showcase/project-view.module.css
Normal file
68
components/arcade/showcase/project-view.module.css
Normal 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;
|
||||
}
|
||||
}
|
||||
19
components/arcade/showcase/random-not-found-img.js
Normal file
19
components/arcade/showcase/random-not-found-img.js
Normal 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
|
||||
14
components/arcade/showcase/readme-renderer.js
Normal file
14
components/arcade/showcase/readme-renderer.js
Normal 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
|
||||
3
components/arcade/showcase/readme-renderer.module.css
Normal file
3
components/arcade/showcase/readme-renderer.module.css
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
.reactMarkDown img {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
33
pages/api/arcade/gallery.js
Normal file
33
pages/api/arcade/gallery.js
Normal 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' });
|
||||
}
|
||||
}
|
||||
36
pages/api/arcade/showcase/cohort.js
Normal file
36
pages/api/arcade/showcase/cohort.js
Normal 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']})
|
||||
}
|
||||
53
pages/api/arcade/showcase/login/[token].js
Normal file
53
pages/api/arcade/showcase/login/[token].js
Normal 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 })
|
||||
}
|
||||
37
pages/api/arcade/showcase/login/test.js
Normal file
37
pages/api/arcade/showcase/login/test.js
Normal 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)
|
||||
}
|
||||
}
|
||||
34
pages/api/arcade/showcase/projects/[projectID]/delete.js
Normal file
34
pages/api/arcade/showcase/projects/[projectID]/delete.js
Normal 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" });
|
||||
}
|
||||
}
|
||||
57
pages/api/arcade/showcase/projects/[projectID]/edit.js
Normal file
57
pages/api/arcade/showcase/projects/[projectID]/edit.js
Normal 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 })
|
||||
}
|
||||
40
pages/api/arcade/showcase/projects/[projectID]/file-input.js
Normal file
40
pages/api/arcade/showcase/projects/[projectID]/file-input.js
Normal 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
|
||||
60
pages/api/arcade/showcase/projects/[projectID]/index.js
Normal file
60
pages/api/arcade/showcase/projects/[projectID]/index.js
Normal 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 })
|
||||
}
|
||||
49
pages/api/arcade/showcase/projects/add.js
Normal file
49
pages/api/arcade/showcase/projects/add.js
Normal 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 })
|
||||
}
|
||||
36
pages/api/arcade/showcase/projects/my.js
Normal file
36
pages/api/arcade/showcase/projects/my.js
Normal 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
10
pages/api/bucky.js
Normal 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
100
pages/arcade/gallery.js
Normal 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
|
||||
9
pages/arcade/showcase/index.js
Normal file
9
pages/arcade/showcase/index.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const index = () => {
|
||||
return (
|
||||
<div>index</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default index
|
||||
64
pages/arcade/showcase/login/[token].js
Normal file
64
pages/arcade/showcase/login/[token].js
Normal 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
351
pages/arcade/showcase/my.js
Normal 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
|
||||
147
pages/arcade/showcase/project/[projectID]/edit.js
Normal file
147
pages/arcade/showcase/project/[projectID]/edit.js
Normal 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 } }
|
||||
}
|
||||
120
pages/arcade/showcase/project/[projectID]/index.js
Normal file
120
pages/arcade/showcase/project/[projectID]/index.js
Normal 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 } }
|
||||
}
|
||||
278
pages/arcade/showcase/vote/index.js
Normal file
278
pages/arcade/showcase/vote/index.js
Normal 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
|
||||
9
pages/arcade/showcase/vote/project/[ID]/index.js
Normal file
9
pages/arcade/showcase/vote/project/[ID]/index.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React from 'react'
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>Project</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Page
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
BIN
public/arcade/background-pattern.png
Normal file
BIN
public/arcade/background-pattern.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
1
public/arcade/gridBG.svg
Normal file
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
1
public/arcade/homeBG.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 47 KiB |
BIN
public/arcade/pattern-background.webp
Normal file
BIN
public/arcade/pattern-background.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
1
public/arcade/pattern-randomized.svg
Normal file
1
public/arcade/pattern-randomized.svg
Normal 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
BIN
public/arcade/plus.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
1
public/arcade/projectBG.svg
Normal file
1
public/arcade/projectBG.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 39 KiB |
1
public/arcade/subtle-stripes.svg
Normal file
1
public/arcade/subtle-stripes.svg
Normal 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>
|
||||
Loading…
Add table
Reference in a new issue