mirror of
https://github.com/gabehf/Prittee.git
synced 2026-03-11 08:21:16 -07:00
First Commit
This commit is contained in:
parent
d198a9ed8a
commit
229c967128
35 changed files with 1377 additions and 136 deletions
47
src/App.css
47
src/App.css
|
|
@ -1,38 +1,21 @@
|
|||
.App {
|
||||
text-align: center;
|
||||
|
||||
#winnerPostAuthor {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.App-logo {
|
||||
height: 40vmin;
|
||||
pointer-events: none;
|
||||
.prevent-select {
|
||||
-webkit-user-select: none; /* Safari */
|
||||
-ms-user-select: none; /* IE 10 and IE 11 */
|
||||
user-select: none; /* Standard syntax */
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
.App-logo {
|
||||
animation: App-logo-spin infinite 20s linear;
|
||||
}
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
.no-scrollbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.App-header {
|
||||
background-color: #282c34;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: calc(10px + 2vmin);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.App-link {
|
||||
color: #61dafb;
|
||||
}
|
||||
|
||||
@keyframes App-logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
.no-scrollbar {
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
37
src/App.js
37
src/App.js
|
|
@ -1,24 +1,29 @@
|
|||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
import { Box, Container } from '@mui/material';
|
||||
import { Outlet } from 'react-router-dom';
|
||||
import Header from './Components/Header';
|
||||
import Footer from './Components/Footer';
|
||||
|
||||
// TODO:
|
||||
// - Leaderboard past #25 (pagination)
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.js</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
<Container>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
className='h-screen'
|
||||
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
</div>
|
||||
<Header />
|
||||
<Box width={480} id='gameContainer' className='grow'>
|
||||
<Outlet />
|
||||
</Box>
|
||||
<Footer/>
|
||||
</Box>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
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>
|
||||
)
|
||||
}
|
||||
33
src/Pages/CreatePost.jsx
Normal file
33
src/Pages/CreatePost.jsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { TextField } from '@mui/material'
|
||||
import Box from '@mui/material/Box'
|
||||
import LoadingButton from '@mui/lab/LoadingButton'
|
||||
import { useState } from 'react'
|
||||
import { insertPost } from '../utils'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
|
||||
export default function CreatePost() {
|
||||
const [text, setText] = useState('')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const navigate = useNavigate()
|
||||
|
||||
const submitHandler = () => {
|
||||
if (text.length > 80) {
|
||||
return
|
||||
}
|
||||
setLoading(true)
|
||||
insertPost(text)
|
||||
.then(() => {
|
||||
navigate('/')
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<Box className='flex flex-col align-end m-auto' width={300}>
|
||||
<TextField color={text.length > 80 ? 'error' : 'primary'} label="Make a post" multiline rows={3} margin='normal' onChange={(e) => setText(e.target.value)}/>
|
||||
<div className={text.length > 80 ? 'text-red-400' : 'text-neutral-300'}>{text.length}/80</div>
|
||||
<LoadingButton loading={loading} className='grow-0' onClick={submitHandler}>Submit</LoadingButton>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
//i saw a bank that offered "24 hour service" and I thought "it takes that long??"
|
||||
52
src/Pages/Index.jsx
Normal file
52
src/Pages/Index.jsx
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import { Box } from '@mui/material';
|
||||
import PostGrid from '../Components/PostGrid'
|
||||
import { useLoaderData } from 'react-router-dom';
|
||||
import Countdown from '../Components/Countdown';
|
||||
import { useState } from 'react';
|
||||
import { loadCurrentWinner } from '../utils';
|
||||
import WinArchive from '../Components/WinArchive';
|
||||
|
||||
export default function Index({ cw, wa }) {
|
||||
|
||||
const data = useLoaderData()
|
||||
const [winArchive, setWinArchive] = useState(data[0])
|
||||
const [currentWinner, setCurrentWinner] = useState(data[1][0]) // why is the current winner api response an array??
|
||||
const [winnerTime, setWinnerTime] = useState(new Date(currentWinner.won_at)) // why is the current winner api response an array??
|
||||
const [showWinArchive, setShowWinArchive] = useState(false)
|
||||
|
||||
const reloadWinner = async () => {
|
||||
console.log('loading new winner')
|
||||
loadCurrentWinner().then(r => {
|
||||
setCurrentWinner(r[0])
|
||||
setWinnerTime(new Date(currentWinner.won_at))
|
||||
})
|
||||
}
|
||||
|
||||
const toggleWinArchive = () => {
|
||||
setShowWinArchive(s => !s)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Box id="lastWinner" className="
|
||||
bg-gradient-to-tl
|
||||
transition-all
|
||||
duration-300
|
||||
from-purple-500
|
||||
via-sky-500
|
||||
to-emerald-500
|
||||
bg-size-200
|
||||
bg-pos-0
|
||||
hover:bg-pos-100
|
||||
hover:cursor-pointer
|
||||
p-8
|
||||
mb-3" onClick={() => toggleWinArchive()}>
|
||||
<p className="post-text pb-2 prevent-select" id="winnerPostText">{currentWinner.post_text}</p>
|
||||
<p className="post-author prevent-select" id="winnerPostAuthor">by {currentWinner.player_name}</p>
|
||||
<Countdown key={String(winnerTime)} until={winnerTime} onEnd={reloadWinner}/>
|
||||
</Box>
|
||||
<Box id="posts">
|
||||
{showWinArchive ? <WinArchive data={winArchive}></WinArchive>: <PostGrid />}
|
||||
</Box>
|
||||
</>
|
||||
)
|
||||
}
|
||||
16
src/Pages/LeaderboardPage.jsx
Normal file
16
src/Pages/LeaderboardPage.jsx
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import { Box } from "@mui/material";
|
||||
import { useLoaderData, Await } from "react-router-dom";
|
||||
import React from "react";
|
||||
import Leaderboard from "../Components/Leaderboard";
|
||||
import LeaderboardSkeleton from "../Components/LeaderboardSkeleton";
|
||||
|
||||
export default function LeaderboardPage() {
|
||||
const { data } = useLoaderData()
|
||||
return (
|
||||
<React.Suspense fallback={<LeaderboardSkeleton />}>
|
||||
<Await resolve={data}>
|
||||
{(users) => <Leaderboard users={users} />}
|
||||
</Await>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
69
src/Pages/Login.jsx
Normal file
69
src/Pages/Login.jsx
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import TextField from '@mui/material/TextField'
|
||||
import LoadingButton from '@mui/lab/LoadingButton'
|
||||
import Box from '@mui/material/Box'
|
||||
import { Container } from '@mui/material'
|
||||
import Link from '@mui/material/Link'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { loadUser } from '../utils'
|
||||
|
||||
// TODO: change api key and token inputs to one input for Copy -> Request Headers
|
||||
|
||||
export default function Login() {
|
||||
|
||||
const navigate = useNavigate()
|
||||
|
||||
const [ token, setToken ] = useState('')
|
||||
const [ apikey, setApiKey ] = useState(localStorage.getItem("apikey"))
|
||||
const [ failText, setFailText ] = useState('')
|
||||
const [ apiKeyErr, setApiKeyErr ] = useState(false)
|
||||
const [ tokenErr, setTokenErr ] = useState(false)
|
||||
const [ loading, setLoading ] = useState(false)
|
||||
|
||||
const onTokenChange = (e) => setToken(e.target.value);
|
||||
const onApiKeyChange = (e) => setApiKey(e.target.value);
|
||||
|
||||
const addUserToken = async () => {
|
||||
setLoading(true)
|
||||
// reset error values
|
||||
setTokenErr(false)
|
||||
setApiKeyErr(false)
|
||||
// ensure the bearer token provided is valid
|
||||
localStorage.setItem('token', token)
|
||||
localStorage.setItem('apikey', apikey)
|
||||
loadUser().then((r) => {
|
||||
setLoading(false)
|
||||
if (r.success) {
|
||||
navigate('/')
|
||||
} else {
|
||||
if (r.apikeyInvalid) {
|
||||
setApiKeyErr(true)
|
||||
}
|
||||
if (r.tokenInvalid) {
|
||||
setTokenErr(true)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="center"
|
||||
alignItems="center"
|
||||
minHeight="100vh"
|
||||
>
|
||||
<Container>
|
||||
<h1 className='text-center mb-12 text-xl'>Prittee is an <span className='text-red-400 italic'>experimental</span> alternate front end for Pithee<br/>
|
||||
Before using it is recommended you <Link href='https://zircon-stoplight-fbb.notion.site/Prittee-Introduction-0e418deda00242ebb6026d074b397629'>read the introduction</Link> to learn about Prittee's limitations</h1>
|
||||
<p className='text-center'>Add your Pithee api key and authorization token to start using Prittee.<br/>
|
||||
You can find out how to get your info <Link href="https://zircon-stoplight-fbb.notion.site/Prittee-Introduction-0e418deda00242ebb6026d074b397629">here</Link>.</p>
|
||||
<Box component="div" display="flex" flexDirection="row" alignItems="center" justifyContent="center" gap={4} className='mt-6'>
|
||||
<TextField id="apikey" error={apiKeyErr} onChange={onApiKeyChange} label="API Key" variant="outlined" value={localStorage.getItem("apikey")} />
|
||||
<TextField id="auth-token" error={tokenErr} helperText={failText} onChange={onTokenChange} label="Authorization Token" variant="outlined" />
|
||||
<LoadingButton loading={loading} variant="text" size="large" onClick={addUserToken}>Let's Go</LoadingButton>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
61
src/Pages/Profile.jsx
Normal file
61
src/Pages/Profile.jsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Header from "../Components/Header";
|
||||
import Box from '@mui/material/Box'
|
||||
import { LinearProgress, CircularProgress } from "@mui/material";
|
||||
import { useLoaderData, Await } from "react-router-dom";
|
||||
import React from "react";
|
||||
import UserPost from "../Components/UserPost";
|
||||
|
||||
export default function Profile() {
|
||||
const { user, posts } = useLoaderData()
|
||||
|
||||
const loadingFallback = () => {
|
||||
return (
|
||||
<Box className='flex flex-col items-center'>
|
||||
<Box className='flex flex-row justify-around items-center' width={480} height={140}>
|
||||
<Box maxWidth={150}>
|
||||
<CircularProgress color="inherit"/>
|
||||
</Box>
|
||||
<Box width={100}>
|
||||
<LinearProgress color="inherit" />
|
||||
<br/>
|
||||
<LinearProgress color="inherit" />
|
||||
<br/>
|
||||
<LinearProgress color="inherit" />
|
||||
</Box>
|
||||
</Box>
|
||||
<Box maxHeight={400} width={400} className='mt-4 overflow-y-auto no-scrollbar'>
|
||||
<br/>
|
||||
<br/>
|
||||
<LinearProgress color="inherit" />
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<React.Suspense fallback={loadingFallback()}>
|
||||
<Await resolve={Promise.all([user, posts]).then(value => value)}>
|
||||
{(value) => {
|
||||
let [ user, posts ] = value
|
||||
user = user.user
|
||||
return(
|
||||
<Box className='flex flex-col items-center'>
|
||||
<Box className='flex flex-row justify-around items-center' width={480}>
|
||||
<Box maxWidth={150}>
|
||||
<img src={user.avatar_url} alt={`${user.player_name}`} />
|
||||
</Box>
|
||||
<Box>
|
||||
<h2 className="text-2xl font-bold mb-2">{user.player_name}</h2>
|
||||
<p>Total Points: {user.total_points}</p>
|
||||
<p>Posts: {posts.length}</p>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box maxHeight={400} className='mt-4 overflow-y-auto no-scrollbar'>
|
||||
{posts.map((post) => <UserPost post={post} />)}
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
}}
|
||||
</Await>
|
||||
</React.Suspense>
|
||||
)
|
||||
}
|
||||
31
src/contestants.js
Normal file
31
src/contestants.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
let contestants = {}
|
||||
|
||||
const addContestant = function(post) {
|
||||
contestants[post.id] = post
|
||||
}
|
||||
|
||||
const rmContestant = function(post) {
|
||||
delete contestants[post.id]
|
||||
}
|
||||
|
||||
const lenContestants = function() {
|
||||
return Object.keys(contestants).length
|
||||
}
|
||||
|
||||
const getContestants = function() {
|
||||
return Object.keys(contestants)
|
||||
}
|
||||
|
||||
const getFullContestants = function() {
|
||||
// when fetching the full contestants, the indices need
|
||||
// to be reset to prevent duplication in the React renderer
|
||||
let raw = Object.values(contestants)
|
||||
let i = 0
|
||||
for (let post of raw) {
|
||||
post.index = i
|
||||
i++
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
export { addContestant, rmContestant, lenContestants, getContestants, getFullContestants }
|
||||
|
|
@ -1,13 +1,23 @@
|
|||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: #141113;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||
sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
color: white;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
|
||||
monospace;
|
||||
}
|
||||
|
||||
.centralContent {
|
||||
margin: auto;
|
||||
}
|
||||
|
|
|
|||
87
src/index.js
87
src/index.js
|
|
@ -2,13 +2,94 @@ import React from 'react';
|
|||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import Login from './Pages/Login'
|
||||
import Profile from './Pages/Profile'
|
||||
import CreatePost from './Pages/CreatePost'
|
||||
import LeaderboardPage from './Pages/LeaderboardPage';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
redirect,
|
||||
RouterProvider,
|
||||
defer
|
||||
} from 'react-router-dom'
|
||||
import { createTheme, ThemeProvider } from '@mui/material';
|
||||
import { lightBlue, cyan, orange, green, red, purple } from '@mui/material/colors';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { loadCurrentWinner, loadLeaderboardPage, loadUser, loadUserPosts, loadWinArchive } from './utils';
|
||||
import Index from './Pages/Index';
|
||||
|
||||
const theme = createTheme({
|
||||
palette: {
|
||||
mode: 'dark',
|
||||
primary: cyan,
|
||||
secondary: purple,
|
||||
error: red,
|
||||
warning: orange,
|
||||
info: lightBlue,
|
||||
success: green,
|
||||
},
|
||||
typography: {
|
||||
fontSize: 16,
|
||||
}
|
||||
})
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
path: '/',
|
||||
element: <App />,
|
||||
children: [
|
||||
{
|
||||
index: true,
|
||||
element: <Index />,
|
||||
loader: async () => {
|
||||
let c = await loadCurrentWinner()
|
||||
let a = await loadWinArchive()
|
||||
if (!c || !a) {
|
||||
return redirect('/login')
|
||||
}
|
||||
|
||||
return new Promise((resolve) => resolve([a, c]))
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/me',
|
||||
element: <Profile />,
|
||||
loader: async () => {
|
||||
return defer({
|
||||
user: loadUser(),
|
||||
posts: loadUserPosts()
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
path: '/post',
|
||||
element: <CreatePost />
|
||||
},
|
||||
{
|
||||
path: '/leaderboard',
|
||||
element: <LeaderboardPage />,
|
||||
loader: async () => {
|
||||
return defer({
|
||||
data: loadLeaderboardPage(1),
|
||||
})
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
element: <Login />,
|
||||
},
|
||||
])
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>
|
||||
<>
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<RouterProvider router={router} />
|
||||
</ThemeProvider>
|
||||
</>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3"><g fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/><circle cx="420.9" cy="296.5" r="45.7"/><path d="M520.5 78.1z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 2.6 KiB |
28
src/queue.js
Normal file
28
src/queue.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
class Queue {
|
||||
constructor() {
|
||||
this.queue = [];
|
||||
}
|
||||
|
||||
push(element) {
|
||||
this.queue.push(element);
|
||||
}
|
||||
|
||||
pop() {
|
||||
return this.queue.shift();
|
||||
}
|
||||
|
||||
peek() {
|
||||
return this.queue[0];
|
||||
}
|
||||
|
||||
print() {
|
||||
console.log(this.queue)
|
||||
}
|
||||
|
||||
size() {
|
||||
return this.queue.length
|
||||
}
|
||||
}
|
||||
|
||||
export default Queue
|
||||
|
||||
218
src/utils.js
Normal file
218
src/utils.js
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
// NOTE: this file is poorly named
|
||||
import { addContestant, getContestants, rmContestant } from "./contestants"
|
||||
|
||||
const userDataUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/get_player_data'
|
||||
const postsUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/get_posts'
|
||||
const playerPostsUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/get_player_posts'
|
||||
const currentWinnerUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/current_winner?select=*'
|
||||
const winnerArchiveUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/win_archive_view?select=*&limit=50'
|
||||
const ygsUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/ygs'
|
||||
const contestantsUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/update_contestants'
|
||||
const victorUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/update_recent_showdown'
|
||||
const leaderboardUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/get_paginated_leaderboard'
|
||||
const insertPostUrl = 'https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/insert_post'
|
||||
|
||||
|
||||
const tierIds = [
|
||||
'1878f11f-782b-43fe-9aa3-cd34e989b174',
|
||||
'629558be-c63d-4255-9bf5-1e98826fa3de',
|
||||
'402261d8-4db8-4e24-8886-90dc6da6fcd1'
|
||||
]
|
||||
|
||||
|
||||
const loadUser = async function() {
|
||||
return new Promise((resolve) => {
|
||||
fetch('https://oqutjaxxxzzbjtyrfoka.supabase.co/rest/v1/rpc/get_player_data', {
|
||||
method: 'POST', // why post?
|
||||
headers: {
|
||||
"APIKey": localStorage.getItem("apikey"),
|
||||
"Authorization": localStorage.getItem("token")
|
||||
}
|
||||
}).then(r => r.json().then(data => ({status: r.status, body: data})))
|
||||
.then((r) => {
|
||||
// note: when an empty bearer token is provided, the server responds with 200 OK with an empty array as
|
||||
// the body instead of the expected 401. This is a bug with Pithee that we need to account for!
|
||||
if (r.body.length > 0) {
|
||||
resolve({
|
||||
success: true,
|
||||
user: r.body[0]
|
||||
})
|
||||
} else if (r.status == 200) {
|
||||
resolve({
|
||||
success: false,
|
||||
apikeyInvalid: false,
|
||||
tokenInvalid: true,
|
||||
})
|
||||
} else {
|
||||
resolve({
|
||||
success: false,
|
||||
apikeyInvalid: true,
|
||||
tokenInvalid: true,
|
||||
})
|
||||
}
|
||||
}).catch((err) => {
|
||||
alert(err)
|
||||
resolve({
|
||||
success: false,
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const loadUserPosts = async function () {
|
||||
return makePitheeApiCall(playerPostsUrl, 'POST', null) // why POST?
|
||||
}
|
||||
|
||||
const loadCurrentWinner = async function() {
|
||||
return makePitheeApiCall(currentWinnerUrl, 'GET', null)
|
||||
}
|
||||
|
||||
const loadWinArchive = async function() {
|
||||
return makePitheeApiCall(winnerArchiveUrl, 'GET', null)
|
||||
}
|
||||
|
||||
const fetchPosts = async function() {
|
||||
return makePitheeApiCall(postsUrl, 'POST', JSON.stringify({post_quantity: 20}))
|
||||
}
|
||||
|
||||
const loadLeaderboardPage = async function(page) {
|
||||
return makePitheeApiCall(leaderboardUrl, 'POST', JSON.stringify({
|
||||
page_number: page,
|
||||
page_size: 25
|
||||
}))
|
||||
}
|
||||
|
||||
const updateContestants = async function() {
|
||||
return makePitheeNoContentApiCall(contestantsUrl, 'POST', JSON.stringify({contestants: getContestants()}))
|
||||
}
|
||||
|
||||
const showdownVictor = async function(victor_id) {
|
||||
return makePitheeNoContentApiCall(victorUrl, 'POST', JSON.stringify({showdown_victor: victor_id}))
|
||||
}
|
||||
|
||||
const pushPostScore = async function(post) {
|
||||
if (post.tier < 1 || post.tier > 3) {
|
||||
console.log('invalid tier value')
|
||||
return
|
||||
}
|
||||
|
||||
// just going to leave this unattended. sure I should check if the call
|
||||
// failed, but I won't. Maybe I'll fix it later
|
||||
makePitheeNoContentApiCall(ygsUrl, 'POST', JSON.stringify({
|
||||
jacks: post.id,
|
||||
films: tierIds[post.tier-1]
|
||||
})) // good one, guys
|
||||
|
||||
// also update contestants for a three star post
|
||||
// what confuses me is that the contestants for a showdown are stored
|
||||
// client-side, but we still need to push them to the server. why?
|
||||
// if you ask me, the contestants should be all on the server or on
|
||||
// the client. IMO all on the server is better because I don't think
|
||||
// showdown progress is retained upon refresh or login/logout which is silly
|
||||
if (post.tier === 3) {
|
||||
addContestant(post)
|
||||
updateContestants()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const declareVictor = function (post) {
|
||||
// upon showdown victor, another ygs call is made with the tier3 uuid,
|
||||
// as well as a call to update_recent_showdown
|
||||
// then, ONLY the victor is removed from the contestants
|
||||
// this means that the very next post to reach tier3 triggers a showdown
|
||||
// is this intended behavior? idk, but I will mirror it.
|
||||
makePitheeNoContentApiCall(ygsUrl, 'POST', JSON.stringify({
|
||||
jacks: post.id,
|
||||
films: tierIds[2]
|
||||
})) // good one, guys
|
||||
showdownVictor(post.id)
|
||||
rmContestant(post)
|
||||
updateContestants()
|
||||
}
|
||||
|
||||
const insertPost = function (postText) {
|
||||
// the /insert_post endpoint weirdly responds with a string instead of
|
||||
// JSON on success so I can't use the regular makePitheeApiCall function
|
||||
return makePitheeNoContentApiCall(insertPostUrl, 'POST', JSON.stringify({
|
||||
post_text: postText
|
||||
}))
|
||||
}
|
||||
|
||||
// /ygs and /update_contestants endpoints do not serve JSON responses so we need this func
|
||||
// ALSO both of those endpoints return 204 OK when a null authorization is given... ???
|
||||
const makePitheeNoContentApiCall = async function(url, method, body) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"apikey": localStorage.getItem('apikey'),
|
||||
"authorization": localStorage.getItem('token'),
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: body
|
||||
})
|
||||
.then((r) => {
|
||||
if (r.status >= 200 && r.status <= 299) { // OK range
|
||||
resolve(r.body)
|
||||
} else if (r.status === 401) {
|
||||
alert("Your token has expired. Please re-enter your information to keep using Prittee.")
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
// TODO: this is little information for the client.
|
||||
// the server could be responding with a 401 due to expired
|
||||
// key (expected behavior) OR it could be a 500 server error and the client
|
||||
// will not know the difference. This should be fixed!
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// the API annoyingly returns a 200 OK with an empty array response body
|
||||
// when called with a null api key...
|
||||
const makePitheeApiCall = async function(url, method, body) {
|
||||
return new Promise((resolve) => {
|
||||
fetch(url, {
|
||||
method: method,
|
||||
headers: {
|
||||
"apikey": localStorage.getItem('apikey'),
|
||||
"authorization": localStorage.getItem('token'),
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: body
|
||||
})
|
||||
.then(r => r.json().then(data => ({status: r.status, body: data})))
|
||||
.then((r) => {
|
||||
if (r.status >= 200 && r.status <= 299) { // OK range
|
||||
// check if empty array is given (token is null)
|
||||
if (r.body.length == 0) {
|
||||
alert("Your token has expired. Please re-enter your information to keep using Prittee.")
|
||||
window.location.href = '/login'
|
||||
}
|
||||
resolve(r.body)
|
||||
} else if (r.status === 401) {
|
||||
alert("Your token has expired. Please re-enter your information to keep using Prittee.")
|
||||
window.location.href = '/login'
|
||||
} else {
|
||||
// TODO: this is little information for the client.
|
||||
// the server could be responding with a 401 due to expired
|
||||
// key (expected behavior) OR it could be a 500 server error and the client
|
||||
// will not know the difference. This should be fixed!
|
||||
resolve(null)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
loadUser,
|
||||
loadWinArchive,
|
||||
loadCurrentWinner,
|
||||
fetchPosts,
|
||||
pushPostScore,
|
||||
declareVictor,
|
||||
loadUserPosts,
|
||||
loadLeaderboardPage,
|
||||
insertPost,
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue