mirror of
https://github.com/gabehf/Prittee.git
synced 2026-03-12 17:00:35 -07:00
First Commit
This commit is contained in:
parent
d198a9ed8a
commit
229c967128
35 changed files with 1377 additions and 136 deletions
38
src/Components/Countdown.jsx
Normal file
38
src/Components/Countdown.jsx
Normal 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
10
src/Components/Footer.jsx
Normal 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
29
src/Components/Header.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
21
src/Components/Leaderboard.jsx
Normal file
21
src/Components/Leaderboard.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
src/Components/LeaderboardSkeleton.jsx
Normal file
9
src/Components/LeaderboardSkeleton.jsx
Normal 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
58
src/Components/Post.jsx
Normal 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
164
src/Components/PostGrid.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
||||
12
src/Components/PreviousWinner.jsx
Normal file
12
src/Components/PreviousWinner.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
src/Components/UserPost.jsx
Normal file
14
src/Components/UserPost.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
15
src/Components/WinArchive.jsx
Normal file
15
src/Components/WinArchive.jsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue