Redesign replit site

This commit is contained in:
Malted 2024-08-29 15:11:52 -04:00
parent f225f59947
commit 8a6300f63c
No known key found for this signature in database
7 changed files with 396 additions and 88 deletions

237
components/replit/form.js Normal file
View file

@ -0,0 +1,237 @@
import { useState } from 'react'
import { Box, Button, Card, Link, Input, Text, Flex, Image } from 'theme-ui'
import Icon from '@hackclub/icons'
const ReplitForm = ({ cssDark }) => {
const [currentStep, setCurrentStep] = useState(1)
const [isSubmitted, setIsSubmitted] = useState(false)
const [formData, setFormData] = useState({})
const handleInputChange = e => {
const { name, value } = e.target
setFormData(prev => ({ ...prev, [name]: value }))
}
const submitForm = () => {
console.log('submitting')
}
const stickers = [
'/stickers/orpheus-having-boba.png',
'/stickers/find out.png',
'/stickers/hackers,_assemble!.png',
'/stickers/mo parts mo problems.png',
'/stickers/orphmoji_peefest.png',
'/stickers/skullpup_boba.png',
'/stickers/hackers,_assemble!.png',
'/stickers/orphmoji_yippee.png',
'/replit/replit-fire.png'
]
const fieldStyle = ({ disabled }) => ({
border: '1.5px solid #0002',
cursor: disabled ? 'not-allowed' : 'auto',
opacity: disabled ? 0.5 : 1
})
const buttonStyle = ({ disabled }) => ({
backgroundColor: cssDark,
cursor: disabled ? 'not-allowed' : 'auto',
opacity: disabled ? 0.5 : 1
})
const boxStyle = {
display: 'flex',
flexDirection: 'column',
gap: '.5em',
alignItems: 'flex-start',
position: 'relative'
}
const step1 = () => {
const fieldDisabled = currentStep !== 1 || isSubmitted
const buttonDisabled = fieldDisabled || !formData.email
return (
<Box sx={boxStyle} className="step">
<Text sx={{ fontWeight: '700', opacity: fieldDisabled ? 0.5 : 1 }}>
Email
</Text>
<Input
id="email"
name="email"
type="email"
placeholder="Enter your email"
required
disabled={fieldDisabled}
onChange={handleInputChange}
sx={fieldStyle({ disabled: fieldDisabled })}
/>{' '}
<Button
onClick={() => setCurrentStep(2)}
disabled={buttonDisabled}
sx={{
width: '100%',
...buttonStyle({ disabled: buttonDisabled })
}}
>
<Icon glyph="down-caret" />
Next
</Button>
</Box>
)
}
const step2 = () => {
const fieldDisabled = currentStep !== 2 || isSubmitted
const backButtonDisabled = fieldDisabled
const submitButtonDisabled =
fieldDisabled || !formData.email || !formData.token
return (
<Box sx={boxStyle} className="step">
<Box
sx={{
display: 'flex',
gap: '0.5em',
alignItems: 'baseline',
opacity: fieldDisabled ? 0.5 : 1
}}
>
<Text sx={{ fontWeight: '700' }}>
Replit <code>connect.sid</code> token{' '}
</Text>
<Link sx={{ fontSize: '0.8em' }} href="#instructions">
How do I find this?
</Link>
</Box>
<Input
id="token"
name="token"
placeholder="Enter your replit token"
required
disabled={fieldDisabled}
onChange={handleInputChange}
sx={fieldStyle({ disabled: fieldDisabled })}
/>{' '}
<Flex
sx={{ width: '100%', justifyContent: 'space-between', gap: '1rem' }}
>
<Button
onClick={() => setCurrentStep(1)}
disabled={backButtonDisabled}
sx={{
flexShrink: '0',
...buttonStyle({ disabled: backButtonDisabled })
}}
>
<Icon glyph="up-caret" /> Back
</Button>
<Button
onClick={submitForm}
disabled={submitButtonDisabled}
sx={{
width: '100%',
...buttonStyle({ disabled: submitButtonDisabled })
}}
>
Submit
</Button>
</Flex>
</Box>
)
}
const step3 = () => (
<Box
sx={{
...boxStyle,
opacity: currentStep === 3 ? 1 : 0.5
}}
className="step"
>
<Text sx={{ fontWeight: '700' }}>Stickers</Text>
<Text>
Get free stickers Get free stickers Get free stickers Get free stickers
Get free stickers Get free stickers Get free stickers Get free stickers
Get free stickers Get free stickers{' '}
</Text>
<style>
{`
.sticker {
scale: 1;
filter: drop-shadow(0 0 0.2rem #0008);
transition: scale 0.1s, filter 0.1s;
}
.sticker:hover {
scale: 1.2;
filter: drop-shadow(0 0 0.6rem #0004);
}
`}
</style>
{stickers.map((sticker, idx) => {
const pos = getRandomPointOnUnitSquare()
return (
<Image
src={sticker}
width="64"
height="64"
alt="orpheus dinosaur labelled 'hackers assemble'"
className="sticker"
sx={{
position: 'absolute',
rotate: `${(Math.random() - 0.5) * 80}deg`,
left: `${pos[0] * 100}%`,
top: `${pos[1] * 100}%`,
translate: '-50% -50%'
}}
draggable="false"
key={idx}
/>
)
})}
</Box>
)
function getRandomPointOnUnitSquare() {
const side = Math.floor(Math.random() * 4)
const position = Math.random()
const margin = 0.1
switch (side) {
case 0:
return [-margin, position]
case 1:
return [position, 1 + margin]
case 2:
return [1 + margin, 1 - position]
case 3:
return [1 - position, -margin]
}
}
return (
<Card
sx={{
width: '30rem',
marginX: 'auto',
display: 'flex',
flexDirection: 'column',
gap: '2rem',
overflow: 'initial'
}}
>
<Text sx={{ fontSize: '0.6rem' }}>{formData.email || 'no email'}</Text>
<Text sx={{ fontSize: '0.6rem' }}>{formData.token || 'no token'}</Text>
<style>{`.step { transition: opacity 0.1s; }`}</style>
{step1()}
{step2()}
{step3()}
</Card>
)
}
export default ReplitForm

View file

@ -16,38 +16,52 @@ import Head from 'next/head'
import Meta from '@hackclub/meta'
import Footer from '../components/footer'
import Nav from '../components/nav'
import { useState, useEffect } from 'react'
import useForm from '../lib/use-form'
import { useState, useEffect, useRef } from 'react'
import Submit from '../components/submit'
import ForceTheme from '../components/force-theme'
import ReplitForm from '../components/replit/form'
const ReplitPage = () => {
const [userDetails, setUserDetails] = useState({ token: null, email: null })
const [token, setToken] = useState('')
const [email, setEmail] = useState('')
const [submitStatus, setSubmitStatus] = useState('default')
const [responseText, setResponseText] = useState('')
const [progressText, setProgressText] = useState(0)
const intervalRef = useRef(null)
useEffect(() => {
const token = localStorage.getItem('token')
const email = localStorage.getItem('email')
setUserDetails({ token, email })
setInterval(() => {
try {
fetch(`/api/replit/progress?token=${localStorage.getItem('token')}`)
.then(res => res.text())
.then(data => {
const split = data.split('/')
console.log(data, split)
setProgressText(split[0] / split[1])
})
} catch (e) {
console.warn(e)
}
}, 5_000)
if (token) setToken(token)
if (email) setEmail(email)
}, [])
useEffect(() => {
if (token) {
intervalRef.current = setInterval(() => {
try {
fetch(`/api/replit/progress?token=${token}`)
.then(res => res.text())
.then(data => {
const split = data.split('/')
setProgressText(split[0] / split[1])
})
} catch (e) {
console.warn(e)
}
}, 5000)
}
return () => {
clearInterval(intervalRef.current)
}
}, [token])
const handleSubmit = async event => {
setSubmitStatus('submitting')
event.preventDefault()
const formData = new FormData(event.target)
const data = {
@ -59,22 +73,31 @@ const ReplitPage = () => {
const response = await fetch('/api/replit/signup', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
'Content-Type': 'application/x-www-form-urlencoded'
},
body: JSON.stringify(data)
body: new URLSearchParams(data).toString()
})
const result = await response.json()
localStorage.setItem('token', result.token)
localStorage.setItem('email', result.email)
setUserDetails({ token: result.token, email: result.email })
setResponseText('Success!')
const result = await response.text()
setResponseText(result)
// Store the email and token in localStorage
localStorage.setItem('token', data.token)
localStorage.setItem('email', data.email)
setSubmitStatus('success')
} catch (error) {
setSubmitStatus('error')
setResponseText('Error submitting form')
console.error('Error:', error)
}
}
const steps = [
'Enter your email',
'Enter your replit token',
'Get free stickers'
]
const tokenSteps = [
{
image: '/replit/aarc1.gif',
@ -90,6 +113,8 @@ const ReplitPage = () => {
}
]
const cssDark = 'hsl(23, 94%, 32%)'
return (
<>
<Meta
@ -97,7 +122,7 @@ const ReplitPage = () => {
title="Export your Repls"
description="Replit free has shut down. Export with Hack Club to GitHub Education's new free codespaces offering"
/>
<style>{`html { scroll-behavior: smooth; }`}</style>
<style>{`html { scroll-behavior: smooth; } body { background-color: hsl(23, 94%, 96%); }`}</style>
<ForceTheme theme="light" />
<Nav />
<Box
@ -109,97 +134,135 @@ const ReplitPage = () => {
paddingTop: ['4rem', null, '6rem'],
paddingBottom: '1rem',
textAlign: 'center',
backgroundColor: 'black',
backgroundColor: cssDark,
color: 'white'
}}
>
<Heading
as="h1"
sx={{
fontSize: '3em'
fontSize: '4em'
}}
onMouseOver={() => {
document.getElementById('og-replit').style.opacity = '0'
document.getElementById('fire-replit').style.opacity = '1'
}}
onMouseOut={() => {
document.getElementById('og-replit').style.opacity = '1'
document.getElementById('fire-replit').style.opacity = '0'
}}
>
Export your{' '}
<Text as="span" sx={{ display: 'inline-flex' }}>
Replit{' '}
<Text
as="span"
sx={{
display: 'inline-flex',
alignItems: 'center',
position: 'relative'
}}
>
Replit <style>{`.replit-fire {transition: opacity 0.1s;`}</style>
<Image
src="/replit/replit.svg"
alt="replit"
sx={{ height: '1em' }}
id="og-replit"
className="replit-fire"
/>
<Image
src="/replit/replit-fire-nooutline.png"
alt="replit"
sx={{
height: '1.35em',
translate: '-0.095em -0.195em',
position: 'absolute',
right: 0
}}
id="fire-replit"
className="replit-fire"
/>
</Text>{' '}
repls
</Heading>
<Text sx={{ maxWidth: '80ch', fontSize: '1.2em', marginY: '1em' }}>
Replit has discontinued its free plan. Previously free features like
unlimited & private repls now cost $10 per month. GitHub Education is
offering free{' '}
<Link href="https://github.com/features/codespaces">Codespaces</Link>{' '}
to all students.
On 25th August, Replit cut down its free plan - it's now unusable.
<br />
Previously, you got unlimited repls for free, for as long as you
wanted.
<br />
Now you get three repls, for 600 minutes per month (20 mins/day).
</Text>
</Box>
<Text sx={{ fontSize: '0.1rem' }}>{JSON.stringify(userDetails)}</Text>
<Box sx={{ maxWidth: '100ch', marginX: 'auto' }}>
<Box sx={{ marginTop: '3rem' }}>
<Heading as="h2" sx={{ marginBottom: '0.5em' }}>
Export your repls
</Heading>
<Card sx={{ background: 'smoke' }}>
<form onSubmit={handleSubmit}>
<Label sx={{ fontSize: 1 }} htmlFor="email">
Email
</Label>
<Input
name="email"
type="email"
defaultValue={userDetails.email}
/>
<Label sx={{ fontSize: 1, pt: 2 }} htmlFor="token">
Replit connect.sid token
</Label>
<Input name="token" defaultValue={userDetails.token} />
<Input
type="submit"
sx={{ backgroundColor: 'black', mt: '0.5rem', color: 'white' }}
text="Submit"
/>
</form>
<Text>{responseText}</Text>
{progressText ? (
<Box as="main" sx={{ maxWidth: '100ch', marginX: 'auto' }}>
<Box
sx={{
display: 'flex',
justifyContent: 'space-between',
marginTop: '2rem',
position: 'relative',
paddingX: [null, null, '6rem']
}}
>
{steps.map((step, idx) => (
<Box
key={idx}
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
fontWeight: '600',
paddingX: '.25em'
}}
>
<Box
sx={{
marginTop: '1rem',
width: '1.5em',
height: '1.5em',
backgroundColor: cssDark,
color: 'white',
borderRadius: '999px',
display: 'flex',
flexDirection: 'column',
gap: '1rem',
alignItems: 'center'
alignItems: 'center',
justifyContent: 'center'
}}
>
<Progress max={1} value={progressText}>
{progressText * 100}%
</Progress>
<Text sx={{ flexShrink: 0 }}>
{progressText * 100}% of your repls have processed.
{progressText <= 0 ? ' Please wait!' : null}
{progressText <= 1 ? ' Check your email!' : null}
</Text>
<Text>{idx + 1}</Text>
</Box>
) : null}
</Card>
<Text sx={{ textAlign: 'center' }}>{step}</Text>
</Box>
))}
<Image
src="/replit/arrow1.svg"
alt=""
sx={{
position: 'absolute',
width: `${7 * 0.9384843737}em`,
bottom: '-1.5em',
left: '32.5%',
translate: '-50% 0'
}}
/>
<Image
src="/replit/arrow2.svg"
alt=""
sx={{
position: 'absolute',
width: '7em',
bottom: '-2em',
left: '67%',
translate: '-50% 0'
}}
/>
</Box>
<Card sx={{ background: 'smoke', marginTop: '3rem' }}>
3: Something about free stickers
</Card>
<Box sx={{ marginTop: '3rem' }}>
<ReplitForm cssDark={cssDark} />
</Box>
<Box sx={{ paddingTop: '5rem' }} id="instructions">
<Heading as="h2" sx={{ marginBottom: '0.5em' }}>
How to get your Replit <code>connect.sid</code> token
</Heading>
@ -215,8 +278,7 @@ const ReplitPage = () => {
<Card
key={idx}
sx={{
lineHeight: 0,
background: 'smoke'
lineHeight: 0
}}
>
<Heading as="h3" sx={{ lineHeight: 1.5 }}>
@ -236,7 +298,7 @@ const ReplitPage = () => {
<Button
sx={{
width: '100%',
backgroundColor: 'black',
backgroundColor: cssDark,
marginTop: '2rem'
}}
>

3
public/replit/arrow.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="200" height="53" viewBox="0 0 200 53" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.67773 13.6281C10.2959 18.1639 16.9038 25.0627 24.8551 30.5819C35.0596 37.665 46.8025 42.0558 58.6554 45.4969C84.7365 53.0688 116.698 54.3311 141.278 41.7413C159.831 32.2389 177.68 13.4214 197.612 7.72643C200.605 6.87139 190.484 2.79867 189.457 2.46861C185.044 1.05016 179.242 2.0394 174.649 2.0394C167.022 2.0394 160.354 -0.452366 168.211 7.40453C176.762 15.9554 183.119 26.0534 190.959 34.874C197.899 42.6813 194.822 26.9622 194.822 21.3538C194.822 15.5595 194.822 9.76518 194.822 3.97085" stroke="black" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 665 B

3
public/replit/arrow1.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="118" height="26" viewBox="0 0 118 26" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 10.8721C7.25157 12.3704 12.0525 15.2994 17.2117 17.1005C30.6611 21.7959 46.1657 24.7985 60.408 24.3072C76.3805 23.7561 89.4938 14.8028 104.561 12.1173M93.3317 2C97.5088 2.61154 113.289 2.26156 115.319 6.16048C118.217 11.7258 107.435 19.5088 104.08 22.6393" stroke="black" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 431 B

3
public/replit/arrow2.svg Normal file
View file

@ -0,0 +1,3 @@
<svg width="125" height="34" viewBox="0 0 125 34" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1.8374 7.50231C6.55355 8.71811 11.0298 13.0652 15.2506 15.5659C24.8783 21.2699 34.5855 25.9934 45.4744 28.8006C64.7149 33.7606 81.1698 33.4312 98.8758 23.7721C104.35 20.7856 109.846 16.4272 114.497 12.2608M103.191 4.15929C106.705 5.05753 109.703 4.1116 113.266 4.03799C115.189 3.99826 121.154 0.78357 122.62 1.64021C123.793 2.32494 122.578 19.9954 121.975 22.353" stroke="black" stroke-width="3" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 536 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB