diff --git a/components/arcade/footer.js b/components/arcade/footer.js new file mode 100644 index 00000000..b5c8116f --- /dev/null +++ b/components/arcade/footer.js @@ -0,0 +1,43 @@ +import { Box, Heading, Text, Link } from 'theme-ui' +import Footer from '../footer' + +const Description = () => ( + + + A project by Hack Club. + + + Prelaunch site. + + + Hack Club is a registered 501(c)3 nonprofit organization that supports a + network of 20k+ technical high schoolers. We believe you learn best by + building so we're creating community and providing grants so you can make. In the past few years, we've{' '} + + given away 100k+ in hardware grants + + ,{' '} + + hosted the world's longest hackathon on land + + , and{' '} + + brought 183 teenagers to SF for a hackathon + + . + + +) + +const ArcadeFooter = () => { + return ( + + ) +} + +export default ArcadeFooter diff --git a/components/arcade/join.js b/components/arcade/join.js new file mode 100644 index 00000000..ce2ed90e --- /dev/null +++ b/components/arcade/join.js @@ -0,0 +1,320 @@ +import JSConfetti from 'js-confetti' +import React, { useEffect, useRef, useState } from 'react' +import { Box, Flex, Text } from 'theme-ui' + +/** @jsxImportSource theme-ui */ +const Join = ({ fold, last, showForm, setForm, formSent, setFormSent }) => { + const [email, setEmail] = useState('') + const [highschool, setHighschool] = useState(false) + + let jsConfetti = useRef() + + useEffect(() => { + jsConfetti.current = new JSConfetti() + }, []) + + const handleFormSubmit = async e => { + e.preventDefault() + + try { + const response = await fetch('/api/arcade/slack', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + userEmail: email + }) + }) + + if (!response.ok) { + const errorData = await response.json() + console.error('Error:', errorData) + // Optionally handle the error here (e.g., show a message to the user) + return + } + + setEmail('') + setFormSent(true) + jsConfetti.current.addConfetti({ + confettiColors: [ + // Hack Club colours! + '#09AFB4', + '#FF5C00' + ] + }) + } catch (error) { + console.error('Error:', error) + } + } + return ( + + {showForm ? ( + formSent ? ( + fold ? ( + + Email from The Arcade
arriving in your inbox soon! +
+ ) : ( + + Free Stickers + + ) + ) : ( +
+ + + + Get your Slack invite to start. + + {/* */} + + Already in Slack? Join #arcade and you're in! + + setEmail(e.target.value)} + required + className="gaegu" + sx={{ + height: '70px', + pl: '10px', + border: '#FF5C00 2px solid', + color: '#FF5C00', + background: '#FAEFD6', + width: '100%', + borderRadius: '5px', + fontSize: ['24px', '27px', '30px'] + }} + /> + + + +
+ ) + ) : ( + { + setForm(true) + }} + target="_blank" + className="slackey" + sx={{ + justifyContent: 'center', + alignItems: 'center', + textDecoration: 'none', + backgroundColor: '#FF5C00', + cursor: 'pointer', + color: '#FAEFD6', + width: 'fit-content', + paddingX: ['8px', '10px', '15px'], + paddingY: ['5px', '7px', '10px'], + fontSize: ['24px', '27px', '30px'], + borderRadius: '5px', + textAlign: 'center', + // margin: 'auto', + // mt: 3, + zIndex: 2, + transitionDuration: '0.3s', + '&:hover': { + transform: 'scale(1.05)' + } + }} + > + Join ARCADE! + + )} + + {fold ? ( + showForm ? ( + formSent ? ( + + Free Stickers + + ) : ( + <> + ) + ) : ( + + Free Stickers + + ) + ) : ( + <> + )} +
+ ) +} + +export default Join diff --git a/components/arcade/prizes.js b/components/arcade/prizes.js new file mode 100644 index 00000000..0882da03 --- /dev/null +++ b/components/arcade/prizes.js @@ -0,0 +1,119 @@ +import React, { useState, useRef, useEffect } from 'react' +import { Box, Button, Text, Flex, Grid, Card, Link } from 'theme-ui' +import Balancer from 'react-wrap-balancer' +import Quantity from './quantity' +/** @jsxImportSource theme-ui */ +const Prizes = ({ + img, + text, + subtext, + cost, + polaroidRotation, + ticketRotation, + link, + quantity, + onQuantityChange, + index, + ...props +}) => { + return ( + + + {text} + + + {text} + + {/* + {text} + */} + + + {subtext} + + + + {link ? ( + <> + + + You can order {quantity} of these + + + + + + + + ) : ( + <> + )} + + {cost} {link ? '🎟️' : cost == 1 ? 'ticket' : 'tickets'} + + + ) +} + +export default Prizes diff --git a/components/arcade/projects.js b/components/arcade/projects.js new file mode 100644 index 00000000..97f2f2a0 --- /dev/null +++ b/components/arcade/projects.js @@ -0,0 +1,315 @@ +import React, { useState } from 'react' +import styled from '@emotion/styled' +import { + Box, + Button, + Container, + Flex, + Heading, + Card, + Grid, + Link as A, + Text, + Avatar, + Image +} from 'theme-ui' +import Photo from '../../components/photo' +import NextImage from 'next/image' +import Marquee from 'react-marquee-slider' +import Photo1 from '../../public/winter/1.jpeg' +import Photo2 from '../../public/winter/2.png' +import Photo3 from '../../public/winter/3.jpeg' +import Photo4 from '../../public/winter/4.jpeg' +import Photo5 from '../../public/winter/5.jpeg' +import Photo6 from '../../public/winter/6.jpeg' +import Photo7 from '../../public/winter/7.jpeg' +import Photo8 from '../../public/winter/8.jpeg' +import Photo9 from '../../public/winter/9.jpeg' +import Photo10 from '../../public/winter/10.jpeg' +import Photo12 from '../../public/winter/12.jpeg' +import Photo13 from '../../public/winter/13.jpeg' +import Photo14 from '../../public/winter/14.jpeg' +import Photo15 from '../../public/winter/15.jpeg' +import Photo16 from '../../public/winter/16.jpeg' +import Photo17 from '../../public/winter/17.jpeg' +import Photo18 from '../../public/winter/18.jpeg' +import Photo19 from '../../public/winter/19.jpeg' +import Photo20 from '../../public/winter/20.jpeg' +import Photo21 from '../../public/winter/21.jpeg' +import Photo22 from '../../public/winter/22.jpeg' +import Photo23 from '../../public/winter/23.jpeg' +import Photo24 from '../../public/winter/24.jpeg' +import Photo25 from '../../public/winter/25.jpeg' +import Photo26 from '../../public/winter/26.jpeg' +import Photo27 from '../../public/winter/27.jpeg' +import Photo28 from '../../public/winter/28.jpeg' +import Photo29 from '../../public/winter/29.jpeg' +import Photo30 from '../../public/winter/30.jpeg' +import Photo31 from '../../public/winter/31.png' + +/** @jsxImportSource theme-ui */ + +const Header = styled(Box)` + background: url('/pattern.svg'); +` + +const Sheet = styled(Card)` + position: relative; + overflow: hidden; + border-radius: 8px; + width: 100%; + color: white; +` +Sheet.defaultProps = { + sx: { + bg: 'rgba(255, 255, 255, 0.875)', + p: [3, 4], + color: 'black', + width: 1, + mb: 4 + } +} + +Sheet.defaultProps = { + sx: { + maxWidth: '52rem', + fontSize: 3, + p: [4, 5], + color: 'white' + } +} +const PhotoRow = ({ photos }) => ( + + + + {photos.map((photo, index) => ( + + ))} + + + + + {photos.map((photo, index) => ( + + ))} + + + +) + +const Cards = ({ avatar, username, description, image }) => { + return ( + + + + + + @{username} + + + + div': { width: 18, height: 18 } + }} + > + {description} + + + {/* */} + + + ) +} + +export default function Projects() { + const [count, setCount] = useState(0) + + let list = [ + 'Drawing robot', + '3D printer', + "DIY Electric Skateboard", + 'Pixel art display', + "Smart Garden", + 'CNC machine', + "Interactive LED Art", + "VR Escape Room", + "Image Recognition App", + 'DIY Camera', + "Multiplayer AR Game", + "Drone Swarm Choreography" + ] + + if (count === list.length - 1) { + setCount(0) + } + + let project_idea = list[count] + + return ( + +
+ + + + + You could be building a + setCount(count + 1)} + > + + {project_idea} + + + + + + + + + + + + + + + + + + +
+
+ ) +} diff --git a/components/arcade/quantity.js b/components/arcade/quantity.js new file mode 100644 index 00000000..c78070c1 --- /dev/null +++ b/components/arcade/quantity.js @@ -0,0 +1,67 @@ +import React, { useState, useEffect } from 'react' +/** @jsxImportSource theme-ui */ +const styled = ` +@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap'); + +.slackey { + font-family: "Slackey", sans-serif; + } + + .gaegu { + font-family: "Gaegu", sans-serif; +} + +body { + background-color: #FAEFD6; +} + +` +const Quantity = ({ lbl, numOptions, onQuantityChange, id }) => { + const [options, setOptions] = useState([]) + const [selectedOption, setSelectedOption] = useState('') + + useEffect(() => { + // Generate options based on the provided number + function generateArray(n) { + return Array.from({ length: n }, (_, i) => i + 1) + } + + // Example usage: + const n = numOptions + const options = generateArray(n) + + setOptions(options) + }, [numOptions]) // Dependency array includes numOptions to regenerate options if it changes + + const handleChange = event => { + setSelectedOption(event.target.value) + onQuantityChange(id, parseInt(event.target.value)) + } + + return ( + <> + + + + ) +} + +export default Quantity diff --git a/components/arcade/rsvp.js b/components/arcade/rsvp.js new file mode 100644 index 00000000..04df0dfe --- /dev/null +++ b/components/arcade/rsvp.js @@ -0,0 +1,5 @@ +import { Box, Button, Text } from 'theme-ui' + + + +export default RSVP \ No newline at end of file diff --git a/components/arcade/shop-component.js b/components/arcade/shop-component.js new file mode 100644 index 00000000..f1757775 --- /dev/null +++ b/components/arcade/shop-component.js @@ -0,0 +1,99 @@ +import React, { useState, useEffect } from 'react'; +import { Text, Grid } from 'theme-ui'; +import Prizes from './prizes'; + +/** @jsxImportSource theme-ui */ + +const styled = ` +@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap'); + +.slackey { + font-family: "Slackey", sans-serif; +} + +.gaegu { + font-family: "Gaegu", sans-serif; +} + +body { + background-color: #FAEFD6; +} +`; + +export default function ShopComponent({ + availableItems, + userAirtableID = null, + hoursBalance = null +}) { + // State to manage quantity for each item + const [quantities, setQuantities] = useState({}); + const [pRotate, setPRotate] = useState(0) + const [tRotate, setTRotate] = useState(0) + + // Function to update quantity for an item + const handleQuantityChange = (itemID, quantity) => { + setQuantities({ ...quantities, [itemID]: quantity }); + }; + + function buyLink(itemID) { + const quantity = quantities[itemID] || 1; // Default quantity is 1 if not set + return `https://forms.hackclub.com/arcade-order?user_id=${userAirtableID}&item_id=${itemID}&quantity=${quantity}`; + } + + const includeBuyLink = userAirtableID !== null; + useEffect(() => { + setPRotate(2 + Math.random() * 4) * (Math.random() > 0.5 ? 1 : -1) + setTRotate(5 + Math.random() * 14) * (Math.random() > 0.5 ? 1 : -1) + }, []); + + return ( + <> + + Shop for {userAirtableID} + + + {availableItems + .sort((a, b) => a['Cost Hours'] - b['Cost Hours']) + .map((item) => ( + handleQuantityChange(item.id, q)} // Pass handler to update quantity + /> + ))} + + + + ); +} diff --git a/next.config.mjs b/next.config.mjs index feef0c1e..b2961bdd 100755 --- a/next.config.mjs +++ b/next.config.mjs @@ -28,6 +28,7 @@ const nextConfig = { return config }, async redirects() { + return [ { source: '/bank/:path*', @@ -190,6 +191,11 @@ const nextConfig = { source: '/blot/', destination: 'https://blot.hackclub.com', permanent: false + }, + { + source: '/slack', + destination: '/arcade?param=slack', + permanent: false } ] }, @@ -304,8 +310,8 @@ const nextConfig = { destination: '/bin/selector/index.html' }, { - source: '/arcade/', - destination: '/arcade/index.html' + source: '/arcade/power-hour', + destination: '/arcade/power-hour/index.html' }, ] }, diff --git a/package.json b/package.json index 0c393a52..3f2f4269 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "geopattern": "^1.2.3", "globby": "^11.0.4", "graphql": "^16.8.1", + "howler": "^2.2.4", "js-confetti": "^0.12.0", "jszip": "^3.10.1", "jszip-utils": "^0.1.0", diff --git a/pages/api/arcade/[userAirtableID]/index.js b/pages/api/arcade/[userAirtableID]/index.js new file mode 100644 index 00000000..cfcd8844 --- /dev/null +++ b/pages/api/arcade/[userAirtableID]/index.js @@ -0,0 +1,25 @@ +import AirtablePlus from "airtable-plus" + +export async function getArcadeUser(recordID) { + const airtable = new AirtablePlus({ + apiKey: process.env.AIRTABLE_API_KEY, + baseID: 'app4kCWulfB02bV8Q', + tableName: "Users" + }) + + return await airtable.find(recordID) +} + +export default async function handler(req, res) { + const { userAirtableID } = req.query + + const user = await getArcadeUser(userAirtableID) + + if (!user) { + return res.status(404).json({ error: "User not found" }) + } + + const hoursBalance = user.fields["Balance (Hours)"] + + res.json({ userAirtableID, hoursBalance }) +} \ No newline at end of file diff --git a/pages/api/arcade/inventory.js b/pages/api/arcade/hack-hour/inventory.js similarity index 100% rename from pages/api/arcade/inventory.js rename to pages/api/arcade/hack-hour/inventory.js diff --git a/pages/api/arcade/openai.js b/pages/api/arcade/openai.js new file mode 100644 index 00000000..3ed0e6f8 --- /dev/null +++ b/pages/api/arcade/openai.js @@ -0,0 +1,35 @@ +import OpenAI from 'openai' + +const sample = arr => arr[Math.floor(Math.random() * arr.length)] + +const messageStarters = [ + 'you could build a', + 'what if you built a', + 'how about a', + 'you could make a', + "as a dino, i'd build a", + "since it's summer, i'd make a", + "i've been dreaming of creating a", + 'picture this:', + 'oh, oh, oh! a', + 'i dare you to make a', +] + +const generateProjectIdea = async () => { + let prompt = `You are a software engineer that wants to bring joy through chaos. Something different every time. Please propose a funky simple project that will take under 6 hours to complete in 1 quick sentence. You can also suggest projects for a family member. Keep it at less than 15 words. The funkier, stupidier, and sillier your ideas the better. Your response must start with "${sample(messageStarters)} +` + // expects OPENAI_API_KEY + const openai = new OpenAI(process.env.OPENAI_API_KEY) + const chatCompletion = await openai.chat.completions.create({ + messages: [{ role: 'user', content: prompt }], + model: 'gpt-3.5-turbo' + }) + + return chatCompletion.choices[0].message.content +} + +export default async function handler(req, res) { + const recommendation = await generateProjectIdea() + + res.send({ recommendation }) +} diff --git a/pages/api/arcade/shop.js b/pages/api/arcade/shop.js new file mode 100644 index 00000000..ea5b6224 --- /dev/null +++ b/pages/api/arcade/shop.js @@ -0,0 +1,28 @@ +import AirtablePlus from "airtable-plus" + +export const shopParts = async () => { + const airtable = new AirtablePlus({ + apiKey: process.env.AIRTABLE_API_KEY, + baseID: "app4kCWulfB02bV8Q", + tableName: "Shop Items" + }) + + const records = await airtable.read() + return records.map(record => ({id: record.id, ...record.fields})) +} + +export default async function handler(req, res) { + const data = await shopParts() + + const filteredData = data.filter(record => record["Enabled"]).map(record => { + return { + name: record['Name'], + smallName: record['Small Name'], + description: record['Description'], + hours: record['Cost Hours'], + imageURL: record['Image URL'], + } + }) + + res.json(filteredData) +} \ No newline at end of file diff --git a/pages/api/arcade/slack.js b/pages/api/arcade/slack.js new file mode 100644 index 00000000..65631d19 --- /dev/null +++ b/pages/api/arcade/slack.js @@ -0,0 +1,52 @@ +import AirtablePlus from 'airtable-plus' + +async function inviteToArcadius({ email }) { + const response = await fetch('https://arcadius.hackclub.com/slack-invite', { + body: JSON.stringify({ + email + }), + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${process.env.SLACK_KEY}` + } + }) + + return response +} +async function inviteToAirtable({ email, ip }) { + const airtable = new AirtablePlus({ + baseID: 'appaqcJtn33vb59Au', + apiKey: process.env.AIRTABLE_API_KEY, + tableName: 'Arcade Joins' + }) + return await airtable.create({ 'Email': email, 'IP': ip }) +} + +export default async function handler(req, res) { + if (req.method === 'POST') { + try { + const data = req.body + + const email = data.userEmail + const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress + + const result = await Promise.all([ + inviteToArcadius({ email }), + inviteToAirtable({ email, ip }) + ]) + + if (result[0]?.response?.ok) { + res.json({ status: 200, message: 'Invitation sent!' }) + } else { + const errorData = await result[0]?.response?.json() + res.json({ status: 400, error: errorData }) + } + } catch (error) { + console.error(error) + res.json({ status: 400, error }) + } + } else { + res.json({ status: 405, error: 'POST method required' }) + } +} diff --git a/pages/arcade/[userAirtableID]/shop.js b/pages/arcade/[userAirtableID]/shop.js new file mode 100644 index 00000000..63dc581f --- /dev/null +++ b/pages/arcade/[userAirtableID]/shop.js @@ -0,0 +1,91 @@ +import ShopComponent from "../../../components/arcade/shop-component" +import { getArcadeUser } from "../../api/arcade/[userAirtableID]" +import { shopParts } from "../../api/arcade/shop" +import { Image, Link, Text } from 'theme-ui' +import { Balancer } from "react-wrap-balancer" +import Meta from '@hackclub/meta' +import Head from 'next/head' + +/** @jsxImportSource theme-ui */ + +const styled = ` +@import url('https://fonts.googleapis.com/css2?family=Slackey&family=Emblema+One&family=Gaegu&display=swap'); + +.slackey { + font-family: "Slackey", sans-serif; + } + + .gaegu { + font-family: "Gaegu", sans-serif; +} + +body { + background-color: #FAEFD6; +} + +` + +export default function Shop({ availableItems, userAirtableID = null, hoursBalance = 0 }) { + + return ( + <> + + +

+ Welcome to the shop +

+
+ Your current balance is {Math.floor(hoursBalance)} 🎟️ + + + ) +} + +export async function getStaticPaths() { + return { + paths: [], + fallback: 'blocking' + } +} + +export async function getStaticProps({params}) { + const { userAirtableID } = params + + const props = { userAirtableID } + + await Promise.all([ + shopParts().then(items => { + const availableItems = items.filter(item => item['Enabled']).map(item => ({ + 'Name': item['Name'] || null, + 'Description': item['Description'] || null, + 'Cost Hours': item['Cost Hours'] || 0, + id: item.id, + 'Image URL': item['Image URL'] || null, + 'Max Order Quantity': item['Max Order Quantity'] || 1 + })) + props.availableItems = availableItems + }), + getArcadeUser(userAirtableID).then(user => { + const hoursBalance = user.fields["Balance (Hours)"] || 0 + props.hoursBalance = hoursBalance + }) + ]) + + return { props } +} \ No newline at end of file diff --git a/pages/arcade/index.js b/pages/arcade/index.js new file mode 100644 index 00000000..541f1cf0 --- /dev/null +++ b/pages/arcade/index.js @@ -0,0 +1,1889 @@ +import React, { useState } from 'react' +import { useRouter } from 'next/router' +import Head from 'next/head' +import Nav from '../../components/nav' +import Meta from '@hackclub/meta' +import { Box, Text, Flex, Grid, Card, Close, Divider, Heading } from 'theme-ui' +import Image from 'next/image' +import fs from 'fs' +import path from 'path' +import { startCase } from 'lodash' +import Projects from '../../components/arcade/projects' +import { Howl } from 'howler' +import Ticker from 'react-ticker' +import PageVisibility from 'react-page-visibility' +import ArcadeFooter from '../../components/arcade/footer' +import Balancer from 'react-wrap-balancer' +import { Fade } from 'react-reveal' +import Join from '../../components/arcade/join' +import Announcement from '../../components/announcement' +import Link from 'next/link' +import { shopParts } from '../api/arcade/shop' +/** @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; + } + + .arcade { + text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6, + -2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6, + -1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6, + 1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6, + 3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4, + -5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4, + -2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4, + 5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4, + 6px 6px #09AFB4, 8px 8px #09AFB4, -8px -8px #09AFB4, 9px 9px #09AFB4, -9px -9px #09AFB4, 10px 10px #09AFB4, -10px -10px #09AFB4; + } + + .arcade2 { + text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6, + -2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6, + -1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6, + 1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6, + 3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4, + -5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4, + -2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4, + 5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4, + 6px 6px #09AFB4; + } + + .arcade3 { + text-shadow: -4px -4px#FAEFD6,-3px -3px #FAEFD6, -2px -2px #FAEFD6, + -2px -2px #FAEFD6, -1px -1px #FAEFD6, -1px -1px #FAEFD6, + -1px -1px #FAEFD6, 1px 1px #FAEFD6, 1px 1px #FAEFD6, + 1px 1px #FAEFD6, 2px 2px #FAEFD6, 4px 4px #FAEFD6, + 3px 3px #FAEFD6, -8px -8px #09AFB4, -6px -6px #09AFB4, + -5px -5px #09AFB4, -4px -4px #09AFB4, -3px -3px #09AFB4, + -2px -2px #09AFB4, 2px 2px #09AFB4, 3px 3px #09AFB4, + 5px 5px #09AFB4, 4px 4px #09AFB4, 7px 7px #09AFB4, + 6px 6px #09AFB4; + } + .emblema { + font-family: "Emblema One", system-ui; + } + + .gaegu { + font-family: "Gaegu", sans-serif; + } + + body { + background-color: #FAEFD6; + } + + /* CSS from https://codepen.io/quadbaup/details/rKOKQv */ +.thought { + display: flex; + background-color: #F8F3F3; + padding: 20px; + border-radius: 30px; + min-width: 40px; + max-width: 180px; + opacity: 0; + height: 100px; + margin: 20px; + margin-left: -10px; + position: relative; + align-items: center; + justify-content: center; + font-size: 12px; + /* text-align:center; */ +} + +.thought:before, +.thought:after { + content: ""; + background-color: #F8F3F3; + border-radius: 50%; + display: block; + position: absolute; + z-index: -1; +} + +.thought:before { + width: 44px; + height: 44px; + top: -12px; + left: 28px; + box-shadow: -50px 30px 0 -12px #F8F3F3; +} + +.thought:after { + bottom: -10px; + right: 26px; + width: 30px; + height: 30px; + box-shadow: 40px -34px 0 0 #F8F3F3, + -28px -6px 0 -2px #F8F3F3, + -24px 17px 0 -6px #F8F3F3, + -5px 25px 0 -10px #F8F3F3; +} + +#generate-project-idea { + margin-top: -100px; +} + +.talking { + animation: talking 1s infinite; +} +@keyframes talking { + 0% { + transform: translateY(0px); + } + + 50% { + transform: translateY(-10px); + } + + 100% { + transform: translateY(0px); + } +} + +.floaty { + animation: float 6s ease-in-out infinite; +} + +@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 Powerups = ({ + img, + text, + subtext, + fullName, + cost, + description, + fulfillmentDescription, + polaroidRotation, + ticketRotation, + extraTags, + ...props +}) => { + const parsedFulfillmentDesc = fulfillmentDescription?.replace( + /\[(.*?)\]\((.*?)\)/g, + '$1' + ) + return ( + + + {text} + + + + {text} + + + {subtext} + + + + {cost} {cost == 1 ? 'ticket' : 'tickets'} + + {extraTags?.map((tag, i) => { + if (tag == 'Limited Supply') { + return ( + + Limited! + + ) + } + })} + { + document.getElementById(`${text}-info`).showModal() + }} + > + 📦 + + + { + document.getElementById(`${text}-info`).close() + }} + /> + + {text} + + + {fullName} + + + + + + + {description} + + + + + + {cost} {cost == 1 ? 'ticket' : 'tickets'} + + + + ) +} + +const Intro = ({ title, num, text, img, third, ...props }) => { + return ( + + + {title} + + + {text} + + + {num} + + Dino drawing + + ) +} + +const Tickets = ({ title, num, text, link, bugEater, ...props }) => { + return ( + + + {title} + + + {text} + + {bugEater && ( + <> + + + Click me for ideas! + + + Click me for ideas! + + + + + Need an idea? + + + + + + )} + + ) +} + +const Sticker = ({ st }) => { + return ( + + + + {st.split('.')[0]} + + {startCase(st.replace(/\.(svg|png)/, ''))} + + + + + + ) +} + +const Item = ({ name, img, cost }) => { + return ( + + + {cost}h + + {name} + + ) +} + +const FAQ = ({ question, answer }) => { + const parsedAnswer = answer?.replace( + /\[(.*?)\]\((.*?)\)/g, + '$1' + ) + return ( + + + {question} + + + + + ) +} +function getRandomInt(max) { + return Math.floor(Math.random() * max) +} + +let yap_sounds = { + // ty caleb! + thinking: [ + new Howl({ src: '/bin/yapping/thonk1.wav' }), + new Howl({ src: '/bin/yapping/thonk2.wav' }), + new Howl({ src: '/bin/yapping/thonk3.wav' }) + ], + talking: { + // these sounds and most of the yapping code are adapted from https://github.com/equalo-official/animalese-generator + a: new Howl({ src: '/bin/yapping/a.wav', volume: 0.16 }), + b: new Howl({ src: '/bin/yapping/b.wav', volume: 0.16 }), + c: new Howl({ src: '/bin/yapping/c.wav', volume: 0.16 }), + d: new Howl({ src: '/bin/yapping/d.wav', volume: 0.16 }), + e: new Howl({ src: '/bin/yapping/e.wav', volume: 0.16 }), + f: new Howl({ src: '/bin/yapping/f.wav', volume: 0.16 }), + g: new Howl({ src: '/bin/yapping/g.wav', volume: 0.16 }), + h: new Howl({ src: '/bin/yapping/h.wav', volume: 0.16 }), + i: new Howl({ src: '/bin/yapping/i.wav', volume: 0.16 }), + j: new Howl({ src: '/bin/yapping/j.wav', volume: 0.16 }), + k: new Howl({ src: '/bin/yapping/k.wav', volume: 0.16 }), + l: new Howl({ src: '/bin/yapping/l.wav', volume: 0.16 }), + m: new Howl({ src: '/bin/yapping/m.wav', volume: 0.16 }), + n: new Howl({ src: '/bin/yapping/n.wav', volume: 0.16 }), + o: new Howl({ src: '/bin/yapping/o.wav', volume: 0.16 }), + p: new Howl({ src: '/bin/yapping/p.wav', volume: 0.16 }), + q: new Howl({ src: '/bin/yapping/q.wav', volume: 0.16 }), + r: new Howl({ src: '/bin/yapping/r.wav', volume: 0.16 }), + s: new Howl({ src: '/bin/yapping/s.wav', volume: 0.16 }), + t: new Howl({ src: '/bin/yapping/t.wav', volume: 0.16 }), + u: new Howl({ src: '/bin/yapping/u.wav', volume: 0.16 }), + v: new Howl({ src: '/bin/yapping/v.wav', volume: 0.16 }), + w: new Howl({ src: '/bin/yapping/w.wav', volume: 0.16 }), + x: new Howl({ src: '/bin/yapping/x.wav', volume: 0.16 }), + y: new Howl({ src: '/bin/yapping/y.wav', volume: 0.16 }), + z: new Howl({ src: '/bin/yapping/z.wav', volume: 0.16 }), + th: new Howl({ src: '/bin/yapping/th.wav', volume: 0.16 }), + sh: new Howl({ src: '/bin/yapping/sh.wav', volume: 0.16 }), + _: new Howl({ src: '/bin/yapping/_.wav', volume: 0.16 }) + } +} + +async function yap(text, letterCallback) { + text = text.toLowerCase() + const yap_queue = [] + for (let i = 0; i < text.length; i++) { + const char = text[i] + try { + if (char === 's' && text[i + 1] === 'h') { + // test for 'sh' sound + yap_queue.push(yap_sounds.talking['sh']) + continue + } else if (char === 't' && text[i + 1] === 'h') { + // test for 'th' sound + yap_queue.push(yap_sounds.talking['th']) + continue + } else if (char === 'h' && (text[i - 1] === 's' || text[i - 1] === 't')) { + // test if previous letter was 's' or 't' and current letter is 'h' + yap_queue.push(yap_sounds.talking['_']) + continue + } else if (char === ',' || char === '?' || char === '.') { + yap_queue.push(yap_sounds.talking['_']) + continue + } else if (char === text[i - 1]) { + // skip repeat letters + yap_queue.push(yap_sounds.talking['_']) + continue + } + } catch (e) { + // who cares. pick up a foot ball + } + if (!char.match(/[a-zA-Z.]/)) { + yap_queue.push(yap_sounds.talking['_']) + continue // skip characters that are not letters or periods + } + yap_queue.push(yap_sounds.talking[char]) + } + + function next_yap() { + letterCallback(yap_queue.length) + if (yap_queue.length === 0) return + let noise = yap_queue.shift() + noise.rate(2 * (Math.random() * 0.5 + 3.5)) + noise.once('end', next_yap) + noise.play() + } + + next_yap() +} + +async function generateProjectIdea() { + if ( + document + .querySelector('#generate-project-idea') + .classList.contains('disabled') + ) { + return + } + + yap_sounds.thinking[getRandomInt(yap_sounds.thinking.length)].play() + document.querySelector('#generate-project-idea').style.marginTop = '0px' + document.querySelector('#console').style.marginTop = '-50px' + document.querySelector('#console2').style.opacity = '0' + document.querySelector('#project-idea').style.opacity = '1' + document.querySelector('#generate-project-idea').classList.add('disabled') + document.querySelector('#project-idea').innerHTML = + '' + thinkingWords() + '...' + '' + document.querySelector('#generate-project-idea').src = + 'https://cloud-g5g5sistf-hack-club-bot.vercel.app/1untitled_artwork_8_1.png' + let text = '' + const res = await fetch('/api/arcade/openai/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify() + }) + + const json = await res.json() + text = json.recommendation + document.querySelector('#project-idea').innerHTML = '' + document.querySelector('#generate-project-idea').src = + 'https://cloud-81d1s66l7-hack-club-bot.vercel.app/0untitled_artwork_9_1.png' + document.querySelector('#generate-project-idea').classList.remove('disabled') + // document.querySelector('#generate-project-idea').classList.add('talking') + yap(text, i => { + document.querySelector('#project-idea').innerHTML = text.slice( + 0, + Math.max(text.length - i + 1, 0) + ) + }) +} + +function thinkingWords() { + const arr = [ + 'thinking', + 'single neuron activated', + '2 braincells rubbing together', + 'ponderosourus', + 'contemplatosaurus', + 'dinosaur brain activated', + 'thinking about trash', + 'rummaging through my thoughts' + ] + return arr[Math.floor(Math.random() * arr.length)] +} + +const Arcade = ({ + stickers = [], + carousel = [], + highlightedItems = [] +}) => { + const [showComponent, setShowComponent] = useState(false) + const [showNum, setNum] = useState(false) + const [showForm, setForm] = useState(false) + const [formSent, setFormSent] = useState(false) + const [isRevealed, setIsRevealed] = useState(false) + + const handleButtonClick = () => { + setIsRevealed(!isRevealed) + } + + const router = useRouter() + const { query } = router + + const slack = query.param + + const generateRandomNumber = () => { + const newRandomNumber = Math.floor(Math.random() * stickers.length) // Generate a random number between 0 and 99 + setNum(newRandomNumber) + } + + const handleMouseEnter = () => { + setShowComponent(true) + } + + const handleMouseLeave = () => { + setShowComponent(false) + } + + const mouseEnter = () => { + handleMouseEnter() + generateRandomNumber() + } + + const [pageIsVisible, setPageIsVisible] = useState(true) + const handleVisibilityChange = isVisible => { + setPageIsVisible(isVisible) + } + return ( + <> + + + + + +