First Commit

This commit is contained in:
Gabe Farrell 2024-04-02 08:17:59 +00:00
parent d198a9ed8a
commit 229c967128
35 changed files with 1377 additions and 136 deletions

View file

@ -0,0 +1,38 @@
import { Box } from "@mui/material";
import { useState, useEffect } from "react";
export default function Countdown({ until, onEnd }) {
const t1 = new Date()
const t2 = new Date(until) // 10 minutes after win time
const msUntil = (t2.getTime()+10*60000) - t1.getTime();
const secondsUntil = Math.floor(msUntil / 1000)
const [seconds, setSeconds] = useState(secondsUntil)
useEffect(() => {
var interval = setInterval(() => {
setSeconds(s => s-1)
if (seconds < 1) {
clearInterval(interval)
onEnd()
}
}, 1000);
return function cleanup() {
clearInterval(interval);
};
}, [seconds])
const formatTime = (time) => {
if (time <= 0) {
return '0:00'
}
let m = Math.floor(time / 60)
let s = time - (m * 60)
let lead = s < 10 ? '0' : ''
return `${m}:${lead+s}`
}
return (
<Box className='prevent-select'>
{formatTime(seconds)}
</Box>
)
}

10
src/Components/Footer.jsx Normal file
View file

@ -0,0 +1,10 @@
import { Container, Box } from "@mui/material";
export default function Footer() {
return (
<Box width={600} className='flex flex-row justify-around text-sm text-neutral-500 border-t-2 border-neutral-600 p-4 mt-4'>
<a className="hover:text-neutral-300" href="https://github.com/gabehf/Prittee" target="_blank">View the source on GitHub</a>
<a className="hover:text-neutral-300" href="https://forms.gle/a8BkH6yUDnzLJD938" target="_blank">Report a bug</a>
</Box>
)
}

29
src/Components/Header.jsx Normal file
View file

@ -0,0 +1,29 @@
import { Box, Container } from "@mui/material"
import { NavLink } from "react-router-dom";
import { AccountBoxOutlined, PostAdd, LeaderboardOutlined } from '@mui/icons-material';
import { useNavigate } from "react-router-dom";
export default function Header() {
const navigate = useNavigate()
return (
<Container className='mt-6 mb-4'>
<Box id="header" className="mb-2 text-center hover:cursor-pointer" onClick={() => navigate('/')}>
<h1 className='text-4xl font-bold'>Prittee</h1>
<p className="text-md text-slate-300">An <span className='text-sky-400 italic'>unofficial</span> alternate front end for Pithee</p>
</Box>
<Box className='flex flex-row justify-between m-auto mb-6' width={125}>
<Box>
<NavLink to='/me'><AccountBoxOutlined /></NavLink>
</Box>
<Box>
<NavLink to='/leaderboard'><LeaderboardOutlined /></NavLink>
</Box>
<Box>
<NavLink to='/post'><PostAdd /></NavLink>
</Box>
</Box>
</Container>
)
}

View file

@ -0,0 +1,21 @@
import { Box } from "@mui/material";
export default function Leaderboard({ users }) {
let i = 0
return (
<Box maxHeight={500} className='overflow-y-auto no-scrollbar'>
{users.map((user) => {
i++
return (
<Box className='flex flex-row justify-between border-b-2 p-2 mb-3'>
<Box className='flex flex-row gap-8'>
<Box>{i}.</Box>
<img src={user.avatar_url} alt={user.player_name} width={35} height={35}/>
</Box>
<Box>{user.player_name}</Box>
<Box className='text-end'>{user.total_points}</Box>
</Box>
)})}
</Box>
)
}

View file

@ -0,0 +1,9 @@
import { LinearProgress, Box } from "@mui/material";
export default function LeaderboardSkeleton() {
return (
<Box maxHeight={500} className='overflow-y-auto no-scrollbar'>
<LinearProgress color="inherit" />
</Box>
)
}

58
src/Components/Post.jsx Normal file
View file

@ -0,0 +1,58 @@
import { Box } from "@mui/material";
import { LinearProgress } from '@mui/material'
import { Key, Star } from "@mui/icons-material";
export default function Post({ text, tier, index, id, onClick }) {
const renderStars = () => {
const arr = []
for (let i = 0; i < tier; i++) {
arr.push(
<Star fontSize='small' />
)
}
return arr
}
if (text === '') {
return (
<Box className="
transition-all
duration-300
bg-gradient-to-tl
from-indigo-900
via-indigo-600
to-purple-500
bg-size-200
bg-pos-0
hover:bg-pos-100
hover:cursor-pointer
p-8" height={225}>
<LinearProgress sx={{
backgroundColor: 'white',
'& .MuiLinearProgress-bar': {
backgroundColor: 'indigo'
}
}} className="opacity-25"/>
</Box>
)
} else {
return (
<Box className="
transition-all
duration-300
bg-gradient-to-tl
from-indigo-900
via-indigo-600
to-purple-500
bg-size-200
bg-pos-0
hover:bg-pos-100
hover:cursor-pointer
p-8" height={225} onClick={() => onClick({post_text: text, index: index, tier: tier, id: id})}>
<Box sx={{marginTop: -2, marginBottom: 1, marginLeft: -2, display: 'flex'}}>
{renderStars()}
</Box>
<p className='text-clip overflow-hidden prevent-select'>{text}</p>
</Box>
)
}
}

164
src/Components/PostGrid.jsx Normal file
View file

@ -0,0 +1,164 @@
import { Grid } from '@mui/material';
import Post from './Post';
import { declareVictor, fetchPosts, pushPostScore } from '../utils'
import { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import Queue from '../queue'
import { getFullContestants, lenContestants, rmContestant } from '../contestants';
const postsSkeleton = [
{
post_text: '',
index: 0,
id: '1',
tier: 0,
},
{
post_text: '',
index: 1,
id: '2',
tier: 0,
},
{
post_text: '',
index: 2,
id: '3',
tier: 0,
},
{
post_text: '',
index: 3,
id: '4',
tier: 0,
},
]
const PostQueue = new Queue()
export default function PostGrid() {
const navigate = useNavigate()
const [selected, setSelected] = useState(null)
const [posts, setPosts] = useState(postsSkeleton)
const [loading, setLoading] = useState(false)
// every time data state changes i.e. every time a user clicks on post,
// update posts with their selection
// note: this also will load the initial posts
useEffect(()=> {
//
// rebuild post array with kept post
// render new array
updatePosts(selected)
}, [selected])
// handler passed to Post children to let us know when one has been selected
const selectPost = (post) => {
post.tier += 1
if (post.tier == 4) { // triggers on a showdown winner
declareVictor(post)
setSelected(null)
// return early because we already handled selecting a showdown winner
return
}
// one of these if statements will trigger when a
// post has reached the end of its ranking,
// or if it has reached max-tier respectively
if (selected != null && post.id != selected.id && selected.tier != 3) {
pushPostScore(selected)
}
if (post.tier == 3){
pushPostScore(post)
}
// triggers when it's time for a showdown
if (lenContestants() >= 4) {
setPosts(getFullContestants())
// return early so as to not trigger the updatePosts logic and
// instead render the showdown
return
}
setSelected(post)
}
// handles updating the post state
const refreshPosts = (keep) => {
// this line prevents trying to refresh posts on mount, when
// no posts have actually been loaded yet
if (PostQueue.size() < 4) { return }
const arr = []
for (var i = 0; i < 4; i++) {
if (keep != null && keep.index == i && keep.tier < 3) {
arr.push(keep)
} else {
let p = PostQueue.pop()
arr.push(
{
post_text: p.post_text,
index: i,
id: p.post_id,
tier: 0,
}
)
}
}
setPosts(arr)
}
// handles refilling the queue when needed, then calls refreshPosts
const updatePosts = (keep) => {
console.log(`loading: ${loading}`)
// if the queue is really low, we want to stop the user from making any more
// selections to let the queue reload
if (PostQueue.size() <= 4) {
if (loading) {
console.log('user is clicking too fast!')
// if we are already loading in new posts and the user
// is basically spam clicking, just return right away to
// prevent it from requesting more posts
return
}
setLoading(true)
console.log('loading last-second posts...')
fetchPosts().then((r) => {
if (r == null) {
} else {
r.forEach(npost => {
PostQueue.push(npost)
});
refreshPosts(keep)
setLoading(false)
}
})
} else if (PostQueue.size() <= 11 && !loading) {
// somewhat low queue -> fetch more posts in the background
setLoading(true)
console.log('loading more posts...')
fetchPosts().then((r) => {
if (r == null) {
alert("Your token has expired. Please re-enter your information to keep using Prittee.")
navigate('/login')
} else {
r.forEach(npost => {
PostQueue.push(npost)
});
setLoading(false)
}
})
refreshPosts(keep)
} else {
// full queue -> keep going as normal
refreshPosts(keep)
}
}
return (
<Grid container spacing={1}>
{posts.map((p) => {
return(
<Grid item xs={6} key={'post-grid-'+p.index}>
<Post text={p.post_text} key={p.id} id={p.id} tier={p.tier} index={p.index} onClick={selectPost}/>
</Grid>
)})}
</Grid>
)
}

View file

@ -0,0 +1,12 @@
import { Box } from "@mui/material";
export default function PreviousWinner({ post }) {
let dateString = new Date(post.won_at).toLocaleString()
return (
<Box className='border-b-2 border-white p-4'>
<Box>{post.post_text}</Box>
<Box className='text-sm'>by {post.player_name}</Box>
<Box className='text-sm'>Won: {dateString}</Box>
</Box>
)
}

View file

@ -0,0 +1,14 @@
import { Box } from "@mui/material";
export default function UserPost({ post }) {
let dateString = new Date(post.created_at).toLocaleString()
return (
<Box className='border-b-2 border-white p-4'>
<Box>{post.post_text}</Box>
<Box className='text-sm'>Points: {post.points}</Box>
<Box className='text-sm'>Posted: {dateString}</Box>
{/* I actually don't know if won_at is the right key because i haven't won yet LOL */}
{post.won_at ? <Box className='text-sm'>Posted: {dateString}</Box> : <></>}
</Box>
)
}

View file

@ -0,0 +1,15 @@
import { Box } from "@mui/material";
import PreviousWinner from "./PreviousWinner";
export default function WinArchive({ data }) {
return(
<Box id="lastWinner" className="
p-4
pt-0
mb-6
overflow-y-auto
no-scrollbar" height={434}>
{data.map((post) => <PreviousWinner post={post}/>)}
</Box>
)
}