This commit is contained in:
2026-06-24 15:10:50 +02:00
commit a3e7512f95
212 changed files with 212927 additions and 0 deletions
+38
View File
@@ -0,0 +1,38 @@
import React from 'react'
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import {
HomePage,
MotionGenerator,
AdminPage,
BreakCalculator,
MotionDatabase,
SubmitNewMotion,
DebateKeeper,
About
} from './pages'
import { NavBar } from './core/components'
import { useStyles } from './appStyle'
export default function App() {
const classes = useStyles()
return (
<BrowserRouter>
<div className={classes['App']}>
<NavBar />
<Routes>
<Route exact={true} path='/' element={<HomePage />} />
<Route exact={true} path='generator' element={<MotionGenerator />} />
<Route exact={true} path='admin' element={<AdminPage />} />
<Route exact={true} path='database' element={<MotionDatabase />} />
<Route exact={true} path='new_motion' element={<SubmitNewMotion />} />
<Route exact={true} path='break_calculator' element={<BreakCalculator />} />
<Route path='keeper' element={<DebateKeeper />}>
<Route path=':format' element={<DebateKeeper />} />
</Route>
<Route exact={true} path='about' element={<About />} />
<Route path='*' element={<Navigate to='/' />} />
</Routes>
</div>
</BrowserRouter>
)
}
+38
View File
@@ -0,0 +1,38 @@
import { isBrowser } from 'react-device-detect'
import { makeStyles } from '@mui/styles'
const style = !isBrowser
? {
'App': {
fontFamily: "'Lora', serif",
width: '100vw',
height: '100vh',
overflowX: 'hidden'
}
}
: {
'App': {
fontFamily: "'Lora', serif",
width: '100vw',
height: '100vh',
overflowX: 'hidden',
'&::-webkit-scrollbar': {
backgroundColor: '#F1F1F1 !important',
width: '0.75rem',
},
'&::-webkit-scrollbar-thumb': {
borderRadius: '8px',
backgroundColor: '#C1C1C1',
minHeight: '24px',
border: '3px solid #F1F1F1',
'&:hover': {
backgroundColor: '#A8A8A8',
},
'&:active': {
backgroundColor: '#787878',
}
}
}
}
export const useStyles = makeStyles(style)
@@ -0,0 +1,90 @@
import { useState, useEffect } from "react"
import Select from 'react-select'
import { customTheme } from "../../constants"
import { isObject } from '../../helpers/isObject'
import './style.css'
export const EditableSelector = (props) => {
const { defaultSelectValue, onUpdate, style, options, defaultValue, multi, components, styles, placeholder, isSearchable } = props
const [value, setValue] = useState(defaultValue)
useEffect(() => {
setValue(defaultValue)
}, [defaultValue])
useEffect(() => {
onUpdate(value)
}, [value])
const updateValue = (val) => {
if (multi) {
if (Array.isArray(defaultValue)) {
if (val != undefined && val != null) {
if (val.length == 0) {
setValue([])
}
else {
let tempValue = []
val.forEach(item => {
tempValue.push(item.value)
})
setValue(tempValue)
}
}
}
else if (isObject(defaultValue)) {
if (val != undefined && val != null) {
if (val.length == 0) {
setValue({})
}
else {
let newValue = {}
val.forEach(item => {
Object.keys(item.value).forEach(key => {
newValue[`${key}`] = item.value[`${key}`]
})
})
setValue(newValue)
}
}
}
}
else {
if (val == undefined || val == null) {
setValue("")
}
else {
setValue(val.value)
}
}
}
return (
<>
{
multi ?
<Select className="editable_selector"
isMulti={true}
theme={customTheme}
style={style}
options={options}
defaultValue={defaultSelectValue}
onChange={updateValue}
components={components}
styles={styles}
isSearchable={isSearchable} //false
placeholder={placeholder}
/>
:
<Select className="editable_selector"
isMulti={false}
theme={customTheme}
style={style}
options={options}
defaultValue={defaultSelectValue}
onChange={updateValue}
isClearable={true}
components={components}
styles={styles}
placeholder={placeholder}
isSearchable={isSearchable}
/>
}
</>
)
}
@@ -0,0 +1,7 @@
.editable_selector {
width: 100%;
}
.editable_selector input {
font-family: "Lora", serif;
font-size: 0.7rem !important;
}
@@ -0,0 +1,25 @@
import { useState, useEffect, useRef } from "react"
import _ from "lodash"
import './style.css'
export const EditableText = (props) => {
const { defaultValue, onUpdate, style } = props;
const [value, setValue] = useState(defaultValue)
const onUpdateWithDebounce = _.debounce(onUpdate, 500)
const inputRef = useRef(null)
const unfocusInput = (e) => {
if (e.keyCode == 13) inputRef.current.blur();
}
useEffect(() => {
setValue(defaultValue)
}, [defaultValue])
useEffect(() => {
onUpdateWithDebounce(value);
}, [value])
useEffect(() => {
inputRef && inputRef.current &&
inputRef.current.addEventListener('keyup', unfocusInput);
}, [inputRef])
return (
<input ref={inputRef} className="editable_text" type="text" spellCheck={false} value={value} style={style} onChange={(e) => setValue(e.target.value)} />
)
}
@@ -0,0 +1,13 @@
.editable_text {
border: 1px solid transparent;
outline: none;
width: 100%;
/* display: flex;
justify-content: center;
align-items: center; */
font-family: "Lora", serif;
}
/* .editable_text:focus {
border: 1px solid black;
outline: none;
} */
@@ -0,0 +1,26 @@
import { useState, useEffect, useRef } from "react"
import TextareaAutosize from "react-textarea-autosize"
import _ from "lodash"
import './style.css'
export const EditableTextArea = (props) => {
const { defaultValue, onUpdate, style } = props;
const [value, setValue] = useState(defaultValue)
const onUpdateWithDebounce = _.debounce(onUpdate, 500)
const inputRef = useRef(null)
const unfocusInput = (e) => {
if (e.keyCode == 13) inputRef.current.blur();
}
useEffect(() => {
setValue(defaultValue)
}, [defaultValue])
useEffect(() => {
onUpdateWithDebounce(value);
}, [value])
useEffect(() => {
inputRef && inputRef.current &&
inputRef.current.addEventListener('keyup', unfocusInput);
}, [inputRef])
return (
<TextareaAutosize spellCheck={false} ref={inputRef} className="editable_text_area" type="text" value={value} style={style} onChange={(e) => setValue(e.target.value)} />
)
}
@@ -0,0 +1,13 @@
.editable_text_area {
border: 1px solid transparent;
outline: none;
width: 100%;
height: 100%;
font-family: "Lora", serif;
resize: none;
overflow: hidden;
}
/* .editable_text_area:focus {
border: 1px solid black;
outline: none;
} */
@@ -0,0 +1,68 @@
import { isBrowser } from 'react-device-detect'
import { useStylesPC } from './stylePC'
import { useStylesMobile } from './styleMobile'
export const InformationContainer = () => {
const classesPC = useStylesPC()
const classesMobile = useStylesMobile()
return (
<div
className={
isBrowser
? classesPC.informationContainer
: classesMobile.informationContainer
}
>
<div className='topLane'>
<button>
<a href='/about'>ABOUT</a>
</button>
</div>
<div className='midLane'>
{/* I no longer use Facebook. */}
<a href='about:blank'>
<button>
<i className='fab fa-facebook-square' />
</button>
</a>
{/* I no longer use Twitter. */}
<a href='about:blank'>
<button>
<i className='fab fa-twitter-square' />
</button>
</a>
<a href='https://gitea.elliot-at-zuri.ch/admin'>
<button>
<i className='fab fa-github-square' />
</button>
</a>
{/* I no longer use Patreon. */}
<a href='about:blank'>
<button>
<i className='fab fa-patreon' />
</button>
</a>
</div>
<div className='botLane'>
<div className='introText'>
<div>
Debaters' toolkit is an open-source software licensed under the{' '}
<a href='https://choosealicense.com/licenses/mit/'>
<span>MIT license</span>
</a>{' '}
that aims to be useful to all debaters. Our motions are collected
from various sources. While we strive to update the database as
regularly as possible, we cannot warrant absolute correctness for
all motions. If you have any issue with our content or detect any
bug in our app, please contact us at{' '}
<a href='mailto: quyanh.nguyen@helsinki'>
<span>quyanh.nguyen@helsinki</span>
</a>
.
</div>
<div className='aboutSubHeader'>© 2021 [Quy Anh] «Elliot» Nguyen.</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,128 @@
.informationContainer {
width: 100%;
display: flex;
flex-direction: column;
background-color: #282a35 !important;
height: 38vh;
min-height: 14.3rem;
font-family: "Source Sans Pro", sans-serif;
margin-top: auto;
.topLane {
width: 100%;
height: 20%;
display: flex;
justify-content: center;
align-items: center;
button {
padding: 0.3rem;
border-radius: 5px;
border: 1px solid white;
background-color: transparent;
font-weight: 500;
a {
text-decoration: none;
color: white;
}
}
button:hover {
background-color: white;
a {
color: black;
}
}
}
.midLane {
width: 100%;
height: 20%;
display: flex;
justify-content: center;
align-items: center;
button {
background-color: #282a35;
border: 1px solid white;
border-radius: 5px;
padding: 0.4rem;
margin-left: 0.2rem;
margin-right: 0.2rem;
i {
color: white;
font-size: 2rem;
}
}
button:hover {
background-color: white;
i {
color: black;
}
}
}
.botLane {
width: 100%;
height: 60%;
display: flex;
justify-content: center;
align-items: center;
.introText {
color: white !important;
display: flex;
flex-direction: column;
align-items: center;
width: 90%;
div {
margin: 0.5rem;
text-align: center;
a {
color: white;
span {
text-decoration: underline;
}
span:hover {
color: #4caf50;
}
}
}
}
}
}
@media only screen and (max-width: 379px) {
.informationContainer {
height: 47vh;
.topLane {
height: 12%;
}
.midLane {
height: 10%;
}
.botLane {
height: 78%;
margin-bottom: 0.5rem;
}
}
}
@media only screen and (max-width: 425px) and (min-width: 380px) {
.informationContainer {
height: 40vh;
.topLane {
height: 17%;
}
.midLane {
height: 13%;
}
.botLane {
height: 70%;
margin-bottom: 0.5rem;
}
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
.informationContainer {
height: 25vh;
.topLane {
}
.midLane {
}
.botLane {
}
}
}
@@ -0,0 +1,126 @@
import { makeStyles } from '@mui/styles'
export const useStylesMobile = makeStyles({
'informationContainer': {
width: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#282a35 !important',
height: '38vh',
minHeight: '14.3rem',
fontFamily: '"Source Sans Pro", sans-serif',
marginTop: 'auto',
'& .topLane': {
width: '100%',
height: '20%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& button': {
padding: '0.3rem',
borderRadius: '5px',
border: '1px solid white',
backgroundColor: 'transparent',
fontWeight: 500,
'& a': {
textDecoration: 'none',
color: 'white'
},
'&:hover': {
backgroundColor: 'white',
'& a': {
color: 'black'
}
}
}
},
'& .midLane': {
width: '100%',
height: '20%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& button': {
backgroundColor: '#282a35',
border: '1px solid white',
borderRadius: '5px',
padding: '0.4rem',
marginLeft: '0.2rem',
marginRight: '0.2rem',
'& i': {
color: 'white',
fontSize: '2rem'
},
'&:hover': {
backgroundColor: 'white',
'& i': {
color: 'black'
}
}
}
},
'& .botLane': {
width: '100%',
height: '60%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& .introText': {
color: 'white !important',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '90%',
'& div': {
margin: '0.5rem',
textAlign: 'center',
'& a': {
color: 'white',
'& span': {
textDecoration: 'underline',
'&:hover': {
color: '#4caf50'
}
}
}
}
}
}
},
'@media only screen and (max-width: 379px)': {
'informationContainer': {
height: '47vh',
'& .topLane': {
height: '12%'
},
'& .midLane': {
height: '10%',
},
'& .botLane': {
height: '78%',
marginBottom: '0.5rem'
}
}
},
'@media only screen and (max-width: 425px) and (min-width: 380px)': {
'informationContainer': {
height: '40vh',
'& .topLane': {
height: '17%',
},
'& .midLane': {
height: '13%',
},
'& .botLane': {
height: '70%',
marginBottom: '0.5rem'
}
}
},
'@media only screen and (max-width: 768px) and (min-width: 426px)': {
'informationContainer': {
height: '25vh'
}
}
})
@@ -0,0 +1,138 @@
import { makeStyles } from '@mui/styles'
export const useStylesPC = makeStyles({
'informationContainer': {
width: '100%',
display: 'flex',
flexDirection: 'column',
backgroundColor: '#282a35 !important',
height: '38vh',
minHeight: '14.3rem',
fontFamily: '"Source Sans Pro", sans-serif',
marginTop: 'auto',
'& .topLane': {
width: '100%',
height: '20%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& button': {
padding: '0.3rem',
borderRadius: '5px',
border: '1px solid white',
backgroundColor: 'transparent',
fontWeight: 500,
'& a': {
textDecoration: 'none',
color: 'white'
},
'&:hover': {
backgroundColor: 'white',
'& a': {
color: 'black'
}
}
}
},
'& .midLane': {
width: '100%',
height: '20%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& button': {
backgroundColor: '#282a35',
border: '1px solid white',
borderRadius: '5px',
padding: '0.4rem',
marginLeft: '0.2rem',
marginRight: '0.2rem',
'& i': {
color: 'white',
fontSize: '2rem'
},
'&:hover': {
backgroundColor: 'white',
'& i': {
color: 'black'
}
}
}
},
'& .botLane': {
width: '100%',
height: '60%',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
'& .introText': {
color: 'white !important',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: '90%',
fontSize: '1rem',
'& div': {
margin: '0.5rem',
textAlign: 'center',
'& a': {
color: 'white',
'& span': {
textDecoration: 'underline',
'&:hover': {
color: '#4caf50'
}
}
}
}
}
}
},
'@media only screen and (max-width: 379px)': {
'informationContainer': {
height: '47vh',
'& .topLane': {
height: '12%'
},
'& .midLane': {
height: '10%',
},
'& .botLane': {
height: '78%',
marginBottom: '0.5rem',
'& .introText': {
fontSize: '0.8rem'
}
}
}
},
'@media only screen and (max-width: 425px) and (min-width: 380px)': {
'informationContainer': {
height: '40vh',
'& .topLane': {
height: '17%',
},
'& .midLane': {
height: '13%',
},
'& .botLane': {
height: '70%',
marginBottom: '0.5rem',
'& .introText': {
fontSize: '0.8rem'
}
}
}
},
'@media only screen and (max-width: 768px) and (min-width: 426px)': {
'informationContainer': {
height: '25vh',
'& .botLane': {
'& .introText': {
fontSize: '0.8rem'
}
}
}
}
})
+44
View File
@@ -0,0 +1,44 @@
import './style.css'
import { useState, useEffect } from 'react';
export const Message = (props) => {
const { status, successMessage, failureMessage } = props;
const [show, setShow] = useState(false)
useEffect(() => {
if (status != undefined) {
setShow(true)
}
}, [status])
useEffect(() => {
const resetShow = () => { setShow(false) }
if (show) setTimeout(resetShow, 1500)
return(() => {
clearTimeout(resetShow)
})
}, [show])
return (
<>
{show &&
<div className='message'>
{
<div>{status == true ? <i className="fas fa-check-circle statusIcon" id="successIcon" color="#abe491" /> : <i className="fas fa-times-circle statusIcon" id="failureIcon" color="#e49191" />}</div>
}
<div className="messageBox">
{
<div>{
status == true ?
<div>
<div>{successMessage}</div>
</div>
:
<div>
<div>{failureMessage}</div>
</div>
}</div>
}
</div>
</div>
}
</>
)
}
+31
View File
@@ -0,0 +1,31 @@
.successLineOne {
color: #abe491;
}
.successLineTwo {
color: #b9b9b9;
font-size: 0.8rem;
}
.failureLineOne {
color: #e49191;
}
.failureLineTwo {
color: #b9b9b9;
font-size: 0.8rem;
}
.message {
font-weight: bolder;
font-family: "Source Sans Pro", sans-serif;
display: flex;
width: 100%;
align-items: center;
justify-content: center;
}
#successIcon {
color: #abe491;
}
#failureIcon {
color: #e49191;
}
.statusIcon {
margin-right: 0.6rem;
}
+58
View File
@@ -0,0 +1,58 @@
import { useState, useEffect } from 'react'
import { NavBarItem } from './navBarItem'
import { isBrowser } from 'react-device-detect'
import { useStylesPC } from './stylePC'
import { useStylesMobile } from './styleMobile'
const navBarConfig = [
{
tabID: 'home', to: '/', specificTabName: "home", children:
<>
<i className="fas fa-home" />
</>
},
{
tabID: 'motionGenerator', to: '/generator', children:
<>
<div>Motion </div>
<div>Generator</div>
</>
},
{
tabID: 'database', to: '/database', children:
<>
<div>Motion </div>
<div>Database</div>
</>
},
{
tabID: 'breakCalc', to: '/break_calculator', children:
<>
<div>Break</div>
<div>Calculator</div>
</>
},
{
tabID: 'keeper', to: '/keeper/bp', children:
<>
<div>Timekeeper</div>
</>
}
]
export const NavBar = () => {
const [activeTab, setActiveTab] = useState(`/`)
useEffect(() => {
setActiveTab(window.location.pathname)
}, [window.location.pathname])
const classesPC = useStylesPC()
const classesMobile = useStylesMobile()
return (
<div className={isBrowser ? classesPC.navBar : classesMobile.navBar}>
{navBarConfig.map(config => {
return (
<NavBarItem specificTabName={config.specificTabName} isActive={activeTab === config.to} to={config.to} tabID={config.tabID} setActiveTab={setActiveTab}>{config.children}</NavBarItem>
)
})}
</div>
)
}
+10
View File
@@ -0,0 +1,10 @@
import { Link } from 'react-router-dom'
export const NavBarItem = (props) => {
const { to, tabID, setActiveTab, children, isActive, specificTabName } = props // setActiveTab(to)
return (
<Link to={to} className={`anchor ${specificTabName} ${isActive ? 'active' : ''}`} id={tabID} onClick={() => { setActiveTab(to) }}>
{children}
</Link>
)
}
+77
View File
@@ -0,0 +1,77 @@
import { makeStyles } from '@mui/styles'
export const useStylesMobile = makeStyles({
'navBar': {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#282a35',
fontFamily: '"Source Sans Pro", sans-serif',
height: '7vh',
width: '100%',
'& .anchor': {
color: 'white',
textDecoration: 'none',
display: 'flex',
fontWeight: 'bolder',
fontSize: '0.8rem',
height: '100%',
width: '24vw',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
'&:hover': {
backgroundColor: '#000000'
}
},
'& .home': {
width: '4vw !important',
'& i': {
fontSize: '1.2rem'
}
},
'& .active': {
backgroundColor: '#000000'
}
},
'@media only screen and (max-width: 379px)': {
'navBar': {
'& .anchor': {
fontSize: '0.7rem',
width: '22vw',
},
'& .home': {
width: '12vw !important',
'& i': {
fontSize: '1rem !important'
}
}
}
},
'@media only screen and (max-width: 425px) and (min-width: 380px)': {
'navBar': {
'& .anchor': {
fontSize: '0.8rem',
width: '22vw'
},
'& .home': {
width: '12vw !important',
}
}
},
'@media only screen and (max-width: 768px) and (min-width: 426px)': {
'navBar': {
'& .anchor': {
fontSize: '1.1rem',
width: '22.5vw',
},
'& .home': {
width: '10vw !important',
'& i': {
fontSize: '2rem !important'
}
}
}
}
})
+81
View File
@@ -0,0 +1,81 @@
import { makeStyles } from '@mui/styles'
export const useStylesPC = makeStyles({
'navBar': {
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
backgroundColor: '#282a35',
fontFamily: '"Source Sans Pro", sans-serif',
height: '7vh',
minHeight: '2.63rem', /**/
width: '100%',
minWidth: '48.125rem', /**/
'& .anchor': {
color: 'white',
textDecoration: 'none',
display: 'flex',
fontWeight: 'bolder',
fontSize: '0.8rem !important',
height: '100%',
width: '24vw',
minWidth: '13.5rem', /**/
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
'&:hover': {
backgroundColor: '#000000'
}
},
'& .home': {
width: '4vw !important',
minWidth: '3.375rem !important', /**/
'& i': {
fontSize: '1.2rem !important'
}
},
'& .active': {
backgroundColor: '#000000'
}
},
'@media only screen and (max-width: 379px)': {
'navBar': {
'& .anchor': {
fontSize: '0.7rem',
width: '22vw',
},
'& .home': {
width: '12vw !important',
'& i': {
fontSize: '1rem !important'
}
}
}
},
'@media only screen and (max-width: 425px) and (min-width: 380px)': {
'navBar': {
'& .anchor': {
fontSize: '0.8rem',
width: '22vw'
},
'& .home': {
width: '12vw !important'
}
}
},
'@media only screen and (max-width: 768px) and (min-width: 426px)': {
'navBar': {
'& .anchor': {
fontSize: '1.1rem',
width: '22.5vw',
},
'& .home': {
width: '10vw !important',
'& i': {
fontSize: '2rem'
}
}
}
}
})
@@ -0,0 +1,8 @@
import { components } from 'react-select'
export const ClearIndicator = ({ children, ...props }) => {
return (
<components.ClearIndicator {...props}>
{children}
</components.ClearIndicator>
);
};
@@ -0,0 +1,8 @@
import { components } from 'react-select'
export const DropdownIndicator = ({ children, ...props }) => {
return (
<components.DropdownIndicator {...props}>
{children}
</components.DropdownIndicator>
);
};
@@ -0,0 +1,6 @@
import { components } from 'react-select'
export const Input = props => {
return (
<components.Input {...props} />
);
};
@@ -0,0 +1,6 @@
import { components } from 'react-select'
export const MultiValueContainer = props => {
return (
<components.MultiValueContainer {...props} />
);
};
@@ -0,0 +1,6 @@
import { components } from 'react-select'
export const Option = props => {
return (
<components.Option {...props} />
);
};
@@ -0,0 +1,4 @@
import { components } from 'react-select'
export const Placeholder = props => {
return <components.Placeholder {...props} />;
};
@@ -0,0 +1,8 @@
import { components } from 'react-select'
export const SelectContainer = ({ children, ...props }) => {
return (
<components.SelectContainer {...props}>
{children}
</components.SelectContainer>
);
};
@@ -0,0 +1,4 @@
import { components } from 'react-select'
export const SingleValue = ({ children, ...props }) => (
<components.SingleValue {...props}>{children}</components.SingleValue>
);
@@ -0,0 +1,4 @@
import { components } from 'react-select'
export const ValueContainer = ({ children, ...props }) => (
<components.ValueContainer {...props}>{children}</components.ValueContainer>
);
@@ -0,0 +1,9 @@
export * from './ClearIndicator'
export * from './DropdownIndicator'
export * from './MultiValueContainer'
export * from './SelectContainer'
export * from './ValueContainer'
export * from './Placeholder'
export * from './Option'
export * from './SingleValue'
export * from './Input'
+47
View File
@@ -0,0 +1,47 @@
export const Table = (props) => {
const { dataSource, columns, showActions, names, ref } = props
return (
<>
{
dataSource.length != 0 ?
<table className={names.tableName} ref={ref}>
<tr className={names.headerName}>
{
columns.map(columnItem => {
if (columnItem.type != "action") {
return (
<th className={names.headerCellName} style={{
width: columnItem.width
}}>{columnItem.name}</th>
)
}
})
}
{
showActions && <th width={columns[columns.length - 1].width} className={names.emptyHeaderCellName}></th>
}
</tr>
{
dataSource.map(item => {
return (
<tr className={names.rowName}>
{
columns.map(columnItem => {
return (
<td style={{
width: columnItem.width
}} className={`${names.rowCellName} ${columnItem.type == "action" ? `${names.actionCellName}` : ""}`}>{columnItem.render(item)}</td>
)
})
}
</tr>
)
})
}
</table>
: <></>
}
</>
)
}
+7
View File
@@ -0,0 +1,7 @@
export * from './NavBar'
export * from './EditableSelector'
export * from './EditableText'
export * from './EditableTextArea'
export * from './InformationContainer'
export * from './Message'
export * from './Table'
File diff suppressed because it is too large Load Diff
+237
View File
@@ -0,0 +1,237 @@
[
{
"name": "BY Online Debate Open",
"format": "BP",
"year": "2021"
},
{
"name": "Uhuru Worlds",
"format": "BP",
"year": "2021"
},
{
"name": "Cambridge Asia BP",
"format": "BP",
"year": "2021"
},
{
"name": "Asian English Olympics",
"format": "BP",
"year": "2021"
},
{
"name": "Beihang International Winter Online BP Open",
"format": "BP",
"year": "2021"
},
{
"name": "Philippines Queer Open",
"format": "BP",
"year": "2021"
},
{
"name": "UMT Parliamentary Debate Open",
"format": "BP",
"year": "2021"
},
{
"name": "Trouvaille Debate Open",
"format": "BP",
"year": "2021"
},
{
"name": "DAV IR Cup",
"format": "BP",
"year": "2021"
},
{
"name": "HWS Round Robin",
"format": "BP",
"year": "2021"
},
{
"name": "Korea WUDC",
"format": "BP",
"year": "2021"
},
{
"name": "DTU Parliamentary Debate",
"format": "AP",
"year": "2021"
},
{
"name": "Vietnam University Debating Championship (VUDC)",
"format": "AP",
"year": "2021"
},
{
"name": "NEU Debate Open",
"format": "AP",
"year": "2021"
},
{
"name": "Cogic Debate Online (CODO)",
"format": "AP",
"year": "2021"
},
{
"name": "The Anime Open",
"format": "AP",
"year": "2021"
},
{
"name": "Netflix International Debate",
"format": "AP",
"year": "2021"
},
{
"name": "Da Nang Debate Open",
"format": "AP",
"year": "2021"
},
{
"name": "Asian Online Debating Championship (AODC) - WSDC",
"format": "WSDC",
"year": "2021"
},
{
"name": "Oldham Cup International League",
"format": "WSDC",
"year": "2021"
},
{
"name": "Nanjing Debate Open",
"format": "WSDC",
"year": "2021"
},
{
"name": "The Tabate",
"format": "WSDC",
"year": "2021"
},
{
"name": "FLSS Debate Tournament",
"format": "WSDC",
"year": "2021"
},
{
"name": "Hanoi Debate Tournament (HDT)",
"format": "WSDC",
"year": "2021"
},
{
"name": "Lychee Debate Open",
"format": "WSDC",
"year": "2021"
},
{
"name": "Canopus Debate Championship",
"format": "WSDC",
"year": "2021"
},
{
"name": "Gấu Debate Tournament",
"format": "BP",
"year": "2021"
},
{
"name": "Vietname BP Championship (BP)",
"format": "BP",
"year": "2020"
},
{
"name": "Gấu Online Debating Championship",
"format": "BP",
"year": "2020"
},
{
"name": "Beihang International Online Debating Championship",
"format": "BP",
"year": "2020"
},
{
"name": "Japan BP",
"format": "BP",
"year": "2020"
},
{
"name": "Melbourne Mini",
"format": "BP",
"year": "2020"
},
{
"name": "PKU Pro-Am",
"format": "BP",
"year": "2020"
},
{
"name": "Asian Online Debating Championship (AODC) - WSDC",
"format": "WSDC",
"year": "2020"
},
{
"name": "Taiwan Online Debate Open",
"format": "AP",
"year": "2020"
},
{
"name": "Northern Coast Debate Open",
"format": "AP",
"year": "2020"
},
{
"name": "Hòa Vang Debate Online",
"format": "AP",
"year": "2020"
},
{
"name": "Teen X Debate Online",
"format": "AP",
"year": "2020"
},
{
"name": "Gấu Online Debate Open",
"format": "AP",
"year": "2020"
},
{
"name": "Hong Kong Debate Open",
"format": "WSDC",
"year": "2020"
},
{
"name": "UPenn World Schools Online Debating Tournament",
"format": "WSDC",
"year": "2020"
},
{
"name": "The Debaters VTV7",
"format": "",
"year": "2020"
},
{
"name": "WSDC",
"format": "WSDC",
"year": "2019"
},
{
"name": "Vietnam Schools Debating Championship (VSDC)",
"format": "WSDC",
"year": "2019"
},
{
"name": "Ka Paio Debate Open",
"format": "WSDC",
"year": "2019"
},
{
"name": "Ka Paio Online Debate Open",
"format": "WSDC",
"year": "2020"
},
{
"name": "WSDC",
"format": "WSDC",
"year": "2018"
}
]
File diff suppressed because it is too large Load Diff
+97
View File
@@ -0,0 +1,97 @@
[
{
"name": "Hong Kong Parliamentary Debating Society (HKPDS)",
"format": "BP",
"year": "2020"
},
{
"name": "Asian Online Debating Championship (AODC) - BP",
"format": "BP",
"year": "2020"
},
{
"name": "6th Shanghai International Debate Open (SIDO)",
"format": "BP",
"year": "2020"
},
{
"name": "Trường Teen",
"format": "",
"year": "2020"
},
{
"name": "Cogic Debate Online (CODO)",
"format": "AP",
"year": "2020"
},
{
"name": "Spring KNC",
"format": "AP",
"year": "2020"
},
{
"name": "CNH Debate Open (CDO)",
"format": "WSDC",
"year": "2020"
},
{
"name": "Hanoi Debate Tournament (HDT)",
"format": "WSDC",
"year": "2020"
},
{
"name": "Southern Debate Open (SDO)",
"format": "AP",
"year": "2020"
},
{
"name": "Hong Kong Schools Debate Open (HKSDO)",
"format": "WSDC",
"year": "2020"
},
{
"name": "DAV Debate Open (DDO)",
"format": "AP",
"year": "2020"
},
{
"name": "Vietnam Debate Online (VNDO)",
"format": "WSDC",
"year": "2020"
},
{
"name": "Vietnam Debate Online (VNDO) - BP",
"format": "BP",
"year": "2020"
},
{
"name": "Nghe Tinh Debate Open (NTDO)",
"format": "WSDC",
"year": "2020"
},
{
"name": "Vietnam BP Championship (VBC)",
"format": "BP",
"year": "2021"
},
{
"name": "Pre VBC",
"format": "BP",
"year": "2021"
},
{
"name": "Online WSDC",
"format": "WSDC",
"year": "2020"
},
{
"name": "6th Oldham Cup",
"format": "WSDC",
"year": "2020"
},
{
"name": "MOE Invitational Debating Championship (MIDC)",
"format": "WSDC",
"year": "2020"
}
]
+10
View File
@@ -0,0 +1,10 @@
export function customTheme(theme) {
return {
...theme,
colors: {
...theme.colors,
primary25: 'grey',
primary: 'black',
}
}
}
File diff suppressed because one or more lines are too long
+6
View File
@@ -0,0 +1,6 @@
export const formats = [
{ value: 'BP', label: 'BP' },
{ value: 'AP', label: 'AP' },
{ value: 'WSDC', label: 'WSDC' },
{ value: 'Others', label: 'Others' }
]
+19
View File
@@ -0,0 +1,19 @@
export * as englishIDs from './englishIDs.json'
export * as vietnameseIDs from './vietnameseIDs.json'
export * from './topics'
export * from './topicsForMotions'
export * from './formats'
export * from './languages'
export * from './customTheme'
export * from './tourneys.json'
export * from './PAtourneys.json'
export * from './MOJItourneys.json'
export * as tournamentsFromDatabase from './tournamentsFromDatabase.json'
export * as tournamentOptions from './tournamentOptions.json'
export * from './tournamentData.json'
export * from './motions.json'
export * from './motionDataRaw.json'
export * as motionsFromDatabase from './motionsFromDatabase.json'
export * from './PAmotions.json'
export * from './MOJImotions.json'
export * from './tableClassNames'
+4
View File
@@ -0,0 +1,4 @@
export const languages = [
{ value: 'English', label: 'English' },
{ value: 'Vietnamese', label: 'Vietnamese' }
]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+40
View File
@@ -0,0 +1,40 @@
export const tableClassNames = {
adminLoadTournaments: {
"tableName": "loadedTournamentsTable",
"headerName": "tournamentsTableHeaderRow",
"headerCellName": "tournamentsTableHeader",
"emptyHeaderCellName": "emptyTournamentsTableHeaderCell",
"rowName": "tournamentTableRow",
"rowCellName": "tournamentTableCell",
"actionCellName": "deleteTournamentCell"
},
adminLoadMotions: {
"tableName": "loadedMotionsTable",
"headerName": "motionsTableHeaderRow",
"headerCellName": "motionsTableHeader",
"emptyHeaderCellName": "emptyMotionsTableHeaderCell",
"rowName": "motionTableRow",
"rowCellName": "motionTableCell",
"actionCellName": "deleteMotionCell"
},
adminPendingRequests: {
"tableName": "loadedRequestsTable",
"headerName": "requestsTableHeaderRow",
"headerCellName": "requestsTableHeader",
"emptyHeaderCellName": "emptyRequestsTableHeaderCell",
"rowName": "requestTableRow",
"rowCellName": "requestTableCell",
"actionCellName": "actionRequestCell"
},
userLoadMotions: {
"tableName": "tableDatabase",
"headerName": "databaseHeaderRow",
"headerCellName": "databaseTableHeader",
"emptyHeaderCellName": "",
"rowName": "databaseTableRow",
"rowCellName": "databaseTableCell",
"actionCellName": ""
}
}
+37
View File
@@ -0,0 +1,37 @@
export const topics = [
{ value: 'aac', label: 'Art & Culture' },
{ value: 'ar', label: "Animals' rights" },
{ value: 'business', label: 'Business' },
{ value: 'cjs', label: 'Criminal Justice System' },
{ value: 'development', label: 'Development' },
{ value: 'economics', label: 'Economics' },
{ value: 'education', label: 'Education' },
{ value: 'entertainment', label: 'Entertainment'},
{ value: 'environment', label: 'Environment' },
{ value: 'family', label: 'Family' },
{ value: 'feminism', label: 'Feminism' },
{ value: 'freedoms', label: 'Freedoms' },
{ value: 'fiction', label: 'Fiction' },
{ value: 'funny', label: 'Funny' },
{ value: 'history', label: 'History' },
{ value: 'hr', label: 'Human Relationships' },
{ value: 'ir', label: 'International Relations' },
{ value: 'law', label: 'Law'},
{ value: 'lgbtqia+', label: 'LGBTQIA+' },
{ value: 'media', label: 'Media' },
{ value: 'me', label: 'Medical Ethics' },
{ value: 'mc', label: 'Minority Communities' },
{ value: 'morality', label: 'Morality' },
{ value: 'philosophy', label: 'Philosophy' },
{ value: 'politics', label: 'Politics' },
{ value: 'religion', label: 'Religion' },
{ value: 'sat', label: 'Science & Technology' },
{ value: 'swm', label: 'Security, War and Military' },
{ value: 'sp', label: 'Social Policy' },
{ value: 'sm', label: 'Social Movements' },
{ value: 'sports', label: 'Sports' },
{ value: 'terrorism', label: 'Terrorism' },
{ value: 'the', label: 'The Human Experience' },
{ value: 'others', label: 'Others' },
]
// module.exports = topics
+308
View File
@@ -0,0 +1,308 @@
export const topicsForMotions = [
{
"value": {
"aac": {
"check": true,
"title": "Art & Culture"
}
},
"label": "Art & Culture"
},
{
"value": {
"ar": {
"check": true,
"title": "Animals' rights"
}
},
"label": "Animals' rights"
},
{
"value": {
"business": {
"check": true,
"title": "Business"
}
},
"label": "Business"
},
{
"value": {
"cjs": {
"check": true,
"title": "Criminal Justice System"
}
},
"label": "Criminal Justice System"
},
{
"value": {
"development": {
"check": true,
"title": "Development"
}
},
"label": "Development"
},
{
"value": {
"economics": {
"check": true,
"title": "Economics"
}
},
"label": "Economics"
},
{
"value": {
"education": {
"check": true,
"title": "Education"
}
},
"label": "Education"
},
{
"value": {
"entertainment": {
"check": true,
"title": "Entertainment"
}
},
"label": "Entertainment"
},
{
"value": {
"environment": {
"check": true,
"title": "Environment"
}
},
"label": "Environment"
},
{
"value": {
"family": {
"check": true,
"title": "Family"
}
},
"label": "Family"
},
{
"value": {
"feminism": {
"check": true,
"title": "Feminism"
}
},
"label": "Feminism"
},
{
"value": {
"freedoms": {
"check": true,
"title": "Freedoms"
}
},
"label": "Freedoms"
},
{
"value": {
"fiction": {
"check": true,
"title": "Fiction"
}
},
"label": "Fiction"
},
{
"value": {
"funny": {
"check": true,
"title": "Funny"
}
},
"label": "Funny"
},
{
"value": {
"history": {
"check": true,
"title": "History"
}
},
"label": "History"
},
{
"value": {
"hr": {
"check": true,
"title": "Human Relationships"
}
},
"label": "Human Relationships"
},
{
"value": {
"ir": {
"check": true,
"title": "International Relations"
}
},
"label": "International Relations"
},
{
"value": {
"law": {
"check": true,
"title": "Law"
}
},
"label": "Law"
},
{
"value": {
"lgbtqia+": {
"check": true,
"title": "LGBTQIA+"
}
},
"label": "LGBTQIA+"
},
{
"value": {
"media": {
"check": true,
"title": "Media"
}
},
"label": "Media"
},
{
"value": {
"me": {
"check": true,
"title": "Medical Ethics"
}
},
"label": "Medical Ethics"
},
{
"value": {
"mc": {
"check": true,
"title": "Minority Communities"
}
},
"label": "Minority Communities"
},
{
"value": {
"morality": {
"check": true,
"title": "Morality"
}
},
"label": "Morality"
},
{
"value": {
"philosophy": {
"check": true,
"title": "Philosophy"
}
},
"label": "Philosophy"
},
{
"value": {
"politics": {
"check": true,
"title": "Politics"
}
},
"label": "Politics"
},
{
"value": {
"religion": {
"check": true,
"title": "Religion"
}
},
"label": "Religion"
},
{
"value": {
"sat": {
"check": true,
"title": "Science & Technology"
}
},
"label": "Science & Technology"
},
{
"value": {
"swm": {
"check": true,
"title": "Security, War and Military"
}
},
"label": "Security, War and Military"
},
{
"value": {
"sp": {
"check": true,
"title": "Social Policy"
}
},
"label": "Social Policy"
},
{
"value": {
"sm": {
"check": true,
"title": "Social Movements"
}
},
"label": "Social Movements"
},
{
"value": {
"sports": {
"check": true,
"title": "Sports"
}
},
"label": "Sports"
},
{
"value": {
"terrorism": {
"check": true,
"title": "Terrorism"
}
},
"label": "Terrorism"
},
{
"value": {
"the": {
"check": true,
"title": "The Human Experience"
}
},
"label": "The Human Experience"
},
{
"value": {
"others": {
"check": true,
"title": "Others"
}
},
"label": "Others"
}
]
+87
View File
@@ -0,0 +1,87 @@
[
{
"name": "HKPDS",
"year": "2020",
"format": "BP"
},
{
"name": "AODC",
"year": "2020",
"format": "BP"
},
{
"name": "6th SIDO",
"year": "2020",
"format": "BP"
},
{
"name": "Trường Teen",
"year": "2020",
"format": ""
},
{
"name": "CDO (CNH Debat Open)",
"year": "2020",
"format": "WSDC"
},
{
"name": "Spring KNC",
"year": "2020",
"format": "AP"
},
{
"name": "CODO (Cogic Debate Online)",
"year": "2020",
"format": "AP"
},
{
"name": "HDT",
"year": "2020",
"format": "WSDC"
},
{
"name": "SDO",
"year": "2020",
"format": "BP"
},
{
"name": "HKSDO",
"year": "2020",
"format": "WSDC"
},
{
"name": "DDO",
"year": "2020",
"format": "AP"
},
{
"name": "VNDO",
"year": "2020",
"format": "BP"
},
{
"name": "NTDO (Nghe Tinh Debate Open)",
"year": "2020",
"format": "WSDC"
},
{
"name": "VBC",
"year": "2020",
"format": "BP"
},
{
"name": "Pre VBC",
"year": "2021",
"format": "BP"
},
{
"name": "Online WSDC",
"year": "2020",
"format": "WSDC"
},
{
"name": "6th Oldham Cup",
"year": "2020",
"format": "WSDC"
}
]
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
File diff suppressed because one or more lines are too long
+74
View File
@@ -0,0 +1,74 @@
export function calculateBreaks({format, teamNumber, roundNumber, breakNumber}){
var teamarr1 = [];
var teamarr2 = [];
//All teams at zero points in the beginning.
for (var i=0; i<teamNumber; i++) {
teamarr1.push(0);
teamarr2.push(0);
}
switch(format) {
case 'ap':
for (var j = 0; j < roundNumber; j++) {
for (var i = 0; i < teamNumber; i++) {
if (i % 2 === 0) { //Case 1 : Where all pull ups win
teamarr1[i]+=1;
} else { //Case 2 : Where all pull ups lose
teamarr2[i]+=1;
}
}
teamarr1.sort();
teamarr2.sort();
}
break;
case 'bp':
for (var j = 0; j < roundNumber; j++) {
for (var i = 0; i < teamNumber; i++) {
//Case 1 : Where all pull ups win
if(i%4 === 0){
teamarr1[i]+=3;
teamarr1[i+1]+=2;
teamarr1[i+2]+=1;
}
//Case 2 : Where all pull ups lose
if(i%4 === 0){
teamarr2[i+1]+=1;
teamarr2[i+2]+=2;
teamarr2[i+3]+=3;
}
}
teamarr1.sort();
teamarr2.sort();
}
break;
default:
return false;
}
return({
pullUpLose: output(teamarr1, breakNumber, teamNumber),
pullUpWin: output(teamarr2, breakNumber, teamNumber)
})
}
export function output(teamarr, num_break, num_teams){
var breakTeams = teamarr.slice(-num_break);
var breakMin = breakTeams[0];
var breakMax = breakTeams[num_break-1];
var breakCount = {};
var totalCount = {};
for(var i = breakMin;i <= breakMax;i++) {
breakCount[i] = 0;
totalCount[i] = 0;
}
for (var i = 0;i<breakTeams.length;i++) {
breakCount[breakTeams[i]]++;
}
for (var i = 0;i<num_teams;i++){
if (teamarr[i]>=breakMin){
totalCount[teamarr[i]]++;;
}
}
return({
breakCount, totalCount
})
}
+12
View File
@@ -0,0 +1,12 @@
const motionsFromDatabase = require('./data/motionsFromDatabase.json')
let eng = 0, vn = 0;
motionsFromDatabase.forEach(motion => {
if (motion.language == "English") {
eng++;
}
else {
vn++;
}
})
console.log(`Number of English motions: ${eng}`)
console.log(`Number of Vietnamese motions: ${vn}`)
@@ -0,0 +1,46 @@
const fs = require('fs');
const tournaments = require('../constants/tournamentsFromDatabase.json');
const optionsUnsorted = []
const labels = []
tournaments.forEach(tournament => {
const name = tournament.name
const year = tournament.year
const id = tournament.id
let label
if (year == "") {
label = name
}
else {
label = `${name} ${year}`
}
labels.push(label)
optionsUnsorted.push({
value: id,
label: label
})
})
labels.sort()
let optionsSorted = []
labels.forEach(label => {
let id = undefined
optionsUnsorted.forEach(option => {
if (option.label == label) {
id = option.value
}
})
optionsSorted.push({
value: id,
label: label
})
})
optionsSorted = optionsSorted.filter((obj, index, self) =>
index === self.findIndex((el) => el.value === obj.value)
);
const optionsJSON = JSON.stringify(optionsSorted)
fs.writeFile(`../constants/tournamentOptions.json`, optionsJSON, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
+8
View File
@@ -0,0 +1,8 @@
export const endWithDot = (str) => {
if (str.slice(-1) == ".") {
return (str)
}
else {
return (str + ".")
}
}
+21
View File
@@ -0,0 +1,21 @@
// const fetch = require("node-fetch");
const fs = require('fs');
const run = async () => {
let numCount = 1;
let firstPart = 'https://spreadsheets.google.com/feeds/cells/10_KEaM4jA5tnMPp4OD9eXnR7n_zx3rOtPnw6YW2ww5M/'
let secondPart = '/public/full?alt=json'
for (let i = 1; i<=13; i++) {
const fullURL = `${firstPart}${i}${secondPart}`
const jsonResponse = await fetch(`${fullURL}`);
const jsonData = await jsonResponse.json();
const jsonString = JSON.stringify(jsonData);
fs.writeFile(`data/${i}.json`, jsonString, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
}
}
run();
@@ -0,0 +1,15 @@
import { topics } from '../constants/topics'
export const getFormattedTopicsFromValues = (topicValues) => {
let topicArray = {}
topicValues.forEach(topicValue => {
topics.forEach(topicItem => {
if (topicItem.value == topicValue) {
topicArray[`${topicValue}`] = {
check: true,
title: topicItem.label
}
}
})
})
return topicArray
}
+27
View File
@@ -0,0 +1,27 @@
const fs = require('fs');
const motions = require("./data/motionsFromDatabase.json")
const vietnameseIDs = [], englishIDs = [];
motions.forEach(motion => {
if (motion.language == "English") {
englishIDs.push(motion.id);
}
else if (motion.language == "Vietnamese") {
vietnameseIDs.push (motion.id);
}
})
let englishIDsJSON = JSON.stringify(englishIDs);
let vietnameseIDsJSON = JSON.stringify(vietnameseIDs);
fs.writeFile(`data/vietnameseIDs.json`, vietnameseIDsJSON, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
fs.writeFile(`data/englishIDs.json`, englishIDsJSON, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
+19
View File
@@ -0,0 +1,19 @@
import { firebaseFirestore } from "../../firebase"
export const getTourneyID = async (name, year, format) => {
let tournamentsRef = firebaseFirestore.collection('tournaments').where('name', '==', name)
if (year != '') {
tournamentsRef = tournamentsRef.where('year', '==', year)
}
if (format != '') {
tournamentsRef = tournamentsRef.where('format', '==', format)
}
const tournamentData = await tournamentsRef.get()
if (tournamentData.docs.length == 0) {
await firebaseFirestore.collection("tournaments").add({ name: name, year: year, format: format })
const newTournamentData = await tournamentsRef.get()
return newTournamentData.docs[0].id
}
else {
return tournamentData.docs[0].id
}
}
+18
View File
@@ -0,0 +1,18 @@
import { firebaseFirestore } from "../../firebase"
export const getTourneyInfo = async (id) => {
const tournamentsRef = firebaseFirestore.collection('tournaments').doc(id)
const tournamentData = await tournamentsRef.get();
if (tournamentData.exists == false) {
return {}
}
else {
const name = tournamentData.data().name
const year = tournamentData.data().year
const format = tournamentData.data().format
return {
name,
year,
format
}
}
}
+6
View File
@@ -0,0 +1,6 @@
export * from './breakCalculator'
export * from './endWithDot'
export * from './getFormattedTopicsFromValues'
export * from './getTourneyID'
export * from './getTourneyInfo'
export * from './isObject'
+3
View File
@@ -0,0 +1,3 @@
export function isObject(value) {
return value && typeof value === 'object' && value.constructor === Object;
}
+41
View File
@@ -0,0 +1,41 @@
const fs = require('fs');
const motionDataRaw = require('./data/motionDataRaw.json');
const endWithDot = (str) => {
if (str.slice(-1) == ".") {
return (str)
}
else {
return (str + ".")
}
}
let motions = [];
motionDataRaw.forEach(async (motion) => {
let date = new Date(motion.Date);
let year = date.getFullYear().toString();
if (year == "NaN") {
year = ""
}
let tournament = motion.Tournament;
let lastFour = tournament.slice(-4)
if (!isNaN(lastFour)) {
year = tournament.slice(-4)
tournament = tournament.slice(0, -5)
}
let id = ""
let content = endWithDot(motion.Motion)
let infoSlide = ''
if (motion.InfoSlide != undefined) {
infoSlide = endWithDot(motion.InfoSlide)
}
const curMot = { tournament: tournament, year: year, format: "", content: content, infoSlide: infoSlide, division: '', language: 'English', link: '', round: motion.Round, topic: {}, tournamentID: id }
motions.push(curMot)
})
let motionJSON = JSON.stringify(motions)
fs.writeFile(`data/motions.json`, motionJSON, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
+25
View File
@@ -0,0 +1,25 @@
const topics = require('./data/topics')
const fs = require('fs');
const topicsForMotions = []
topics.forEach(topic => {
const topicForMotion = {
value: {
[`${topic.value}`]: {
"check": true,
"title": `${topic.label}`
}
},
label: `${topic.label}`
}
topicsForMotions.push(topicForMotion)
})
console.log(topicsForMotions)
let topicsForMotionsJSON = JSON.stringify(topicsForMotions, null, 4)
fs.writeFile(`data/topicsForMotions.js`,`export const topicsForMotions = ${topicsForMotionsJSON}`, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
+64
View File
@@ -0,0 +1,64 @@
//motionDataRaw.json was scraped from hellomotions.com which belongs to Jessica Yung
/*
MIT License
Copyright (c) 2016 Jessica Yung.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
const fs = require('fs');
const _ = require('lodash');
const motionDataRaw = require('./data/motionDataRaw.json');
let tourneys = [];
for (let motion of motionDataRaw) {
let date = new Date(motion.Date);
let year = date.getFullYear().toString();
if (year == "NaN") {
year = ""
}
let tournament = motion.Tournament;
let firstFour = tournament.slice(0,4)
let lastFour = tournament.slice(-4)
if (!isNaN(lastFour)) {
year = lastFour
tournament = tournament.slice(0, -5)
}
if (!isNaN(firstFour)) {
year = firstFour
tournament = tournament.slice(5)
}
let newTournament = {
"name": tournament,
"format": "",
"year": year
}
tourneys.push(newTournament)
}
const uniqueTourneys = _.uniqBy(tourneys, (tourney) => {
return tourney.name + tourney.year
})
let tourneyJSON = JSON.stringify(uniqueTourneys)
fs.writeFile(`data/tourneys.json`, tourneyJSON, 'utf8', function (err) {
if (err) {
console.log("An error occured while writing JSON Object to File.");
return console.log(err);
}
console.log("JSON file has been saved.");
});
+4
View File
@@ -0,0 +1,4 @@
export * from './useForm'
export * from './useDeviceBreakPoint'
export * from './useDocumentTitle'
export * from './usePageTracker'
+7
View File
@@ -0,0 +1,7 @@
import { useMediaQuery } from 'react-responsive'
export const useDeviceBreakPoint = () => {
const isExtraSmall = useMediaQuery({ maxWidth: 379 })
const isPhone = useMediaQuery({ minWidth: 380, maxWidth: 425 })
const isTablet = useMediaQuery({ minWidth: 426, maxWidth: 768 })
return { isPhone, isTablet, isExtraSmall }
}
+7
View File
@@ -0,0 +1,7 @@
import { useEffect } from 'react'
export const useDocumentTitle = (title) => {
useEffect(() => {
document.title = title
}, [])
}
+15
View File
@@ -0,0 +1,15 @@
import { useState } from 'react'
export const useForm = (defaultFormValues) => {
const [formValue, setFormValue] = useState(defaultFormValues)
function changeFormValue(fieldName, fieldValue) {
setFormValue({ ...formValue, [fieldName]: fieldValue });
}
function resetFormValue() {
setFormValue(defaultFormValues)
}
return ({
formValue,
changeFormValue,
resetFormValue,
})
}
+10
View File
@@ -0,0 +1,10 @@
import { useEffect } from 'react'
import ReactGA4 from 'react-ga4' // for GA
import ReactGA from 'react-ga' // for UA (old)
export const usePageTracker = () => {
useEffect(() => {
ReactGA4.send('pageview')
ReactGA.pageview(window.location.pathname)
}, [])
}
+16
View File
@@ -0,0 +1,16 @@
import firebase from 'firebase'
import 'firebase/firebase-auth'
import 'firebase/firestore'
const firebaseConfig = {
apiKey: process.env.REACT_APP_FIREBASE_API_KEY,
authDomain: process.env.REACT_APP_FIREBASE_AUTH_DOMAIN,
projectId: process.env.REACT_APP_FIREBASE_PROJECT_ID,
storageBucket: process.env.REACT_APP_FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.REACT_APP_FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.REACT_APP_FIREBASE_APP_ID
}
// Initialize Firebase
const app = firebase.initializeApp(firebaseConfig)
export const firebaseAuth = app.auth()
export const firebaseFirestore = app.firestore()
+9
View File
@@ -0,0 +1,9 @@
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
::selection {
background: #282a35;
color: white;
}
+13
View File
@@ -0,0 +1,13 @@
import { createRoot } from 'react-dom/client'
// import ReactGA from 'react-ga'
// import ReactGA4 from 'react-ga4'
import App from './App'
import './index.css'
const container = document.getElementById('app')
const root = createRoot(container) // createRoot(container!) if you use TypeScript
root.render(<App tab='home' />)
// ReactGA4.initialize(process.env.REACT_APP_GA_TRACKING_ID_WEB)
// ReactGA.initialize('UA-216107831-1', {
// siteSpeedSampleRate: 100
// })
+119
View File
@@ -0,0 +1,119 @@
import { Helmet } from 'react-helmet'
// import MessengerCustomerChat from 'react-messenger-customer-chat'
import { useDeviceBreakPoint, usePageTracker } from '../../core/hooks'
import './style.scss'
export const About = () => {
const { isPhone, isTablet, isExtraSmall } = useDeviceBreakPoint()
usePageTracker()
return (
<div className='about'>
<Helmet>
<title>About</title>
<meta name='description' content='About this toolkit.' />
<link rel='canonical' href='/about' />
</Helmet>
{/* <MessengerCustomerChat
pageId={process.env.REACT_APP_FB_PAGE_ID}
appId={process.env.REACT_APP_FB_APP_ID}
/> */}
<div className='topStuffs'>
<div className='aboutHeader'>About</div>
<div className='aboutSubHeader'>
debaters-toolkit is a handy toolkit for all debaters.
</div>
<div className='aboutSubHeader'>
Search over 7000 motions, calculate break chances or use a free online
timekeeping tool.
</div>
{!isExtraSmall ? (
<div className='aboutSubHeader'>
Help make debaters-toolkit better by contributing code, ideas and
features at its{' '}
<a href='https://gitea.elliot-at-zuri.ch/admin/debaters-toolkit'>
<span>GitHub Repository</span>
</a>
.
</div>
) : (
<></>
)}
<div className='aboutSubHeader'>
Special thanks to the following contributors:
</div>
<div className='contributors'>
<div className='aboutSubHeaderInContributors'>
-{' '}
<a
href='https://www.facebook.com/hoangdieulinh215'
id='specialATag'
>
<span>Ms. Dieu Linh Hoang</span>
</a>
for giving me the idea ;)
</div>
<div className='aboutSubHeaderInContributors'>
-{' '}
<a href='https://www.facebook.com/MojiDebate' id='specialATag'>
<span>Moji Debate</span>
</a>{' '}
for their{' '}
<a href='https://drive.google.com/drive/folders/1OX39izeTiz8DMFWhrw9v3qpk8fg3_ylV'>
<span>Motions for Vietnam Debate Community</span>
</a>
.
</div>
<div className='aboutSubHeaderInContributors'>
-{' '}
<a href='https://www.facebook.com/puzzles.ams' id='specialATag'>
<span>Puzzles Ams</span>
</a>{' '}
for their{' '}
<a href='https://docs.google.com/spreadsheets/d/1e11Rh2G_Bb9mNARLhnA6WjqgDO3Np6QpYnasVqXkGZY/'>
<span>Motion Database 2020-2021</span>
</a>
.
</div>
</div>
<div className='aboutSubHeader'>
Follow me on social media or support my work by becoming a Patron:
</div>
<div className='icons'>
{/* I no longer use Facebook. */}
<a href='about:blank'>
<button>
<i className='fab fa-facebook-square' />
</button>
</a>
{/* I no longer use Twitter. */}
<a href='about:blank'>
<button>
<i className='fab fa-twitter-square' />
</button>
</a>
<a href='https://gitea.elliot-at-zuri.ch/admin'>
<button>
<i className='fab fa-github-square' />
</button>
</a>
{/* I no longer use Patreon. */}
<a href='about:blank'>
<button>
<i className='fab fa-patreon' />
</button>
</a>
</div>
</div>
<div className='bottomStuffs'>
<div className='aboutSubHeader'>
Licensed under
<a href='https://choosealicense.com/licenses/mit/'>
<span>MIT license</span>
</a>
.
</div>
<div className='aboutSubHeader'>© 2021 [Quy Anh] «Elliot» Nguyen.</div>
</div>
</div>
)
}
+168
View File
@@ -0,0 +1,168 @@
.about {
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
background-color: #535353;
height: 93vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.topStuffs {
height: 83vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
.aboutHeader {
color: white;
font-size: 2rem;
font-weight: bold;
display: flex;
justify-content: center;
}
.contributors {
display: flex;
flex-direction: column;
align-items: flex-start;
.aboutSubHeaderInContributors {
color: white;
margin: 0.25rem;
width: 100%;
#specialATag {
margin-right: 0.25rem;
}
a {
text-decoration: none;
span {
color: white;
margin-left: 0.25rem;
text-decoration: underline;
}
span:hover {
color: #4caf50;
}
}
}
}
.aboutSubHeader {
color: white;
margin: 0.5rem;
display: flex;
justify-content: center;
width: 100%;
#specialATag {
margin-right: 0.25rem;
}
a {
text-decoration: none;
span {
color: white;
margin-left: 0.25rem;
text-decoration: underline;
}
span:hover {
color: #4caf50;
}
}
}
button {
background-color: #282a35;
border: 1px solid transparent;
border-radius: 5px;
padding: 0.5rem;
i {
color: white;
font-size: 2rem;
}
}
button:hover {
background-color: white;
i {
color: black;
}
}
.icons {
width: 13rem;
display: flex;
align-items: center;
justify-content: space-between;
}
}
.bottomStuffs {
height: 10vh;
margin-top: auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
.aboutSubHeader {
color: white;
margin: 0.25rem;
display: flex;
justify-content: center;
width: 100%;
a {
text-decoration: none;
span {
color: white;
margin-left: 0.25rem;
text-decoration: underline;
}
span:hover {
color: #4caf50;
}
}
}
// position: relative;
// top: 5rem;
}
}
@media only screen and (max-width: 379px) {
.about {
.topStuffs {
width: 95%;
button {
padding: 0.2rem;
i {
font-size: 1rem;
}
}
.contributors {
font-size: 0.8rem;
}
.aboutSubHeader {
display: flex;
justify-content: center;
flex-direction: column;
text-align: center;
font-size: 0.8rem;
margin: 0.25rem;
}
}
.bottomStuffs {
font-size: 0.8rem;
}
}
}
@media only screen and (max-width: 425px) and (min-width: 380px) {
.about {
.topStuffs {
button {
margin-top: 0.5rem;
padding: 0.3rem;
}
.contributors {
font-size: 0.525rem;
}
.aboutSubHeader {
font-size: 0.525rem;
margin: 0.25rem;
}
}
.bottomStuffs {
margin-bottom: 3rem;
}
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
}
+34
View File
@@ -0,0 +1,34 @@
import { useState } from 'react'
import { Helmet } from 'react-helmet'
import { AddTournament } from './components/AddTournament'
import { AddMotion } from './components/AddMotion'
import { LoadTournaments } from './components/LoadTournaments'
import { PendingRequests } from './components/PendingRequests'
import { LoadMotions } from './components/LoadMotions'
import { SignIn } from './components/SignIn'
import './style.css'
export const AdminPage = () => {
const [loggedIn, setLoggedIn] = useState(false)
return (
<div
className='admin'
style={!loggedIn ? { height: '93vh' } : { height: '100%' }}
>
<Helmet>
<title>Debaters' toolkit | Admin</title>
</Helmet>
{loggedIn ? (
<>
<AddTournament />
<AddMotion />
<PendingRequests />
<LoadTournaments />
<LoadMotions />
</>
) : (
<SignIn auth={setLoggedIn} />
)}
</div>
)
}
@@ -0,0 +1,201 @@
import './style.css'
import Select from 'react-select'
import { firebaseFirestore } from '../../../../firebase'
import { useForm } from '../../../../core/hooks'
import { topics, languages, customTheme } from '../../../../core/constants'
import tournamentOptions from '../../../../core/constants/tournamentOptions.json'
import { Message } from '../../../../core/components'
import { useEffect, useState, useRef } from 'react'
import TextareaAutosize from 'react-textarea-autosize'
import { getTourneyID } from '../../../../core/helpers/getTourneyID'
import _ from 'lodash'
export const AddMotion = () => {
const [sent, setSent] = useState(undefined)
const [saving, setSaving] = useState(false)
const topicRef = useRef(null)
const tournamentRef = useRef(null)
const { formValue, changeFormValue, resetFormValue } = useForm({ content: "", infoSlide: "", topic: {}, language: "English", division: "", tournamentID: "", round: "", link: "" })
const submit = async (e) => {
e.preventDefault()
setSent(true)
if (formValue.content != "" && formValue.tournamentID != "" && formValue.topic != {}) {
setSaving(true)
try {
await firebaseFirestore.collection("motions").add(formValue)
setSaving(false)
setSent(true)
resetFormValue()
topicRef.current.select.setValue([])
tournamentRef.current.select.setValue('')
}
catch (err) {
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
}
}
else {
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
}
}
function changeTopic(val) {
val.forEach(obj => {
changeFormValue('topic', { ...formValue.topic, [obj.value]: { check: true, title: obj.label } })
})
}
function changeLanguage(val) {
if (val != null && val != undefined && val != "") {
changeFormValue('language', val.value)
}
}
function changeTournament(val) {
if (val != null && val != undefined && val != "") {
changeFormValue('tournamentID', val.value)
}
}
function changeTournamentID(e) {
changeFormValue("tournamentID", e.target.value)
let newOption
tournamentOptions.forEach(option => {
if (option.value == e.target.value) {
newOption = tournamentOptions[tournamentOptions.indexOf(option)]
}
})
if (newOption != undefined) {
tournamentRef.current.select.setValue(newOption)
}
else {
tournamentRef.current.select.setValue('')
}
}
const fileInput = useRef(null)
const [selectedFile, setSelectedFile] = useState(undefined)
const [selected, setSelected] = useState(false)
const [motions, setMotions] = useState(undefined)
const changeHandler = (event) => {
setSelectedFile(event.target.files[0])
setSelected(true)
}
useEffect(() => {
if (selectedFile != undefined) {
const reader = new FileReader()
reader.readAsText(selectedFile)
reader.onload = (e) => {
setMotions(JSON.parse(e.target.result))
}
}
}, [selectedFile])
const addMotions = async (e) => {
e.preventDefault()
setSent(true)
setSaving(true)
try {
if (!_.isString(motions[0].tournament) || !_.isString(motions[0].content) || !_.isString(motions[0].year)) {
alert('Invalid JSON Selected !')
setSaving(false)
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
setSelectedFile(undefined)
setSelected(false)
setMotions(undefined)
}
else {
motions.forEach(async (motion) => {
const tournament = motion.tournament
const year = motion.year
const format = motion.format
const id = await getTourneyID(tournament, year, format)
let infoSlide = ""
if (motion.infoSlide != undefined) {
infoSlide = motion.infoSlide
}
let round
if (!_.isString(motion.round)) {
round = motion.round.toString()
}
else {
round = motion.round
}
const curMot = { content: motion.content, infoSlide: infoSlide, division: '', language: motion.language, link: '', round: round, topic: motion.topic, tournamentID: id }
await firebaseFirestore.collection("motions").add(curMot)
//test first b4 uploading
})
setSaving(false)
setSent(true)
setSelectedFile(undefined)
setSelected(false)
setMotions(undefined)
alert("Done !")
}
}
catch (err) {
setSaving(false)
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
setSelectedFile(undefined)
setSelected(false)
setMotions(undefined)
}
}
//---------------------------------------------------------------------------------------------------------
return (
<div className="addMotion">
<div className="addMotionTitle">Add Motion:</div>
<div className="uploadMotionsFromJSON">
<div className="getMotionJSONFile">
<input ref={fileInput} accept=".json" type="file" onChange={changeHandler} style={{ display: "none" }} />
<button className="loadMotionJSONFileButton" onClick={(e) => fileInput.current.click()}>Load JSON</button>
<div>
{
`Current file: ${selectedFile ? selectedFile.name : ""}`
}
</div>
</div>
<div>
<button className="uploadMotionsJSONButton" disabled={selected ? false : true} onClick={addMotions}>Upload JSON to Firestore</button>
</div>
</div>
<form action="" id="motionForm">
<TextareaAutosize type="text" spellCheck={false} className="motionInfoItem largeTextBox" minRows="5" placeholder="Content" value={formValue.content} onChange={(e) => { changeFormValue("content", e.target.value) }} />
<TextareaAutosize type="text" spellCheck={false} className="motionInfoItem largeTextBox" minRows="5" placeholder="Info Slide" value={formValue.infoSlide} onChange={(e) => { changeFormValue("infoSlide", e.target.value) }} />
<Select className="motionInfoItem motionAttributeSelect"
placeholder="Topic"
isSearchable={true}
options={topics}
onChange={changeTopic}
theme={customTheme}
isMulti={true}
ref={topicRef}
/>
<Select className="motionInfoItem motionAttributeSelect"
placeholder="Language"
isSearchable={false}
options={languages}
onChange={changeLanguage}
theme={customTheme}
isMulti={false}
defaultValue={{ value: "English", label: "English" }}
/>
<input type="text" className="motionInfoItem inputMotionBox" spellCheck={false} placeholder="Division" value={formValue.division} onChange={(e) => { changeFormValue("division", e.target.value) }} />
<input type="text" className="motionInfoItem inputMotionBox" spellCheck={false} placeholder="Round" value={formValue.round} onChange={(e) => { changeFormValue("round", e.target.value) }} />
<Select className="motionInfoItem motionAttributeSelect"
placeholder="Tournament"
isSearchable={true}
options={tournamentOptions}
onChange={changeTournament}
theme={customTheme}
isMulti={false}
ref={tournamentRef}
/>
<input type="text" className="motionInfoItem inputMotionBox" spellCheck={false} placeholder="Tournament ID" value={formValue.tournamentID} onChange={changeTournamentID} />
<input type="text" className="motionInfoItem inputMotionBox" spellCheck={false} placeholder="Reference Video URL" value={formValue.link} onChange={(e) => { changeFormValue("link", e.target.value) }} />
<button onClick={submit} className="addMotionButton">Add motion</button>
</form>
<div className="motionAddingResultContainer">
{
saving ? <div>Loading</div> : <Message status={sent} successMessage={<><div className="successLineOne">MOTION ADDED</div> <div className="successLineTwo">MOTION ADDED SUCCESSFULLY</div></>} failureMessage={<><div className="failureLineOne">MOTION NOT ADDED</div> <div className="failureLineTwo">MOTION ADDING FAILED. PLEASE TRY AGAIN</div></>} />
}
</div>
</div>
)
}
@@ -0,0 +1,138 @@
.addMotion {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
border-bottom: 1px solid black;
font-family: "Lora", serif;
}
.addMotionTitle {
display: flex;
width: 100%;
margin-bottom: 1rem;
padding-left: 5rem;
}
.uploadMotionsFromJSON {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 1rem;
margin-top: 0rem;
font-size: 0.9rem;
}
.uploadMotionsFromJSON button {
color: white;
background-color: #282a35;
border: none;
padding: 0.5rem;
border-radius: 10px;
font-weight: bolder;
}
.uploadMotionsFromJSON button:hover {
background-color: #000000;
}
.getMotionJSONFile {
margin-left: 7rem;
display: flex;
align-items: center;
margin-bottom: 0.75rem;
}
.loadMotionJSONFileButton {
margin-right: 1rem;
}
.uploadMotionsJSONButton {
margin-left: 7rem;
}
.addMotionButton {
color: white;
background-color: #282a35;
border: none;
padding: 0.5rem;
border-radius: 10px;
font-weight: bolder;
margin-top: 1rem;
}
.addMotionButton:hover {
background-color: #000000;
}
#motionForm {
display: flex;
flex-direction: column;
align-items: center;
}
.motionInfoItem {
width: 20rem;
margin: 0.3rem;
}
.largeTextBox {
resize: none;
overflow: hidden;
}
.motionAttributeSelect {
font-size: 0.8rem;
}
.motionAttributeSelect input {
font-family: "Lora", serif;
}
#motionForm textarea {
border: 1.5px solid #e1e1e1;
border-radius: 3px;
padding: 0.5rem;
font-family: "Lora", serif;
}
.inputMotionBox {
border: 1.5px solid #e1e1e1;
border-radius: 3px;
padding: 0.5rem;
height: 2.33rem;
font-family: "Lora", serif;
}
.motionAddingResultContainer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-top: 0;
height: 4rem;
}
@media only screen and (max-width: 425px) {
.addMotionTitle {
display: flex;
width: 100%;
justify-content: center;
padding-left: 0;
}
.getMotionJSONFile {
margin-left: 3rem;
}
.uploadMotionsJSONButton {
margin-left: 3rem;
}
.inputMotionBox {
height: 2.33rem;
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
.getMotionJSONFile {
margin-left: 6rem;
}
.uploadMotionsJSONButton {
margin-left: 6rem;
}
}
@@ -0,0 +1,119 @@
import { firebaseFirestore } from '../../../../firebase'
import { useForm } from "../../../../core/hooks"
import { useEffect, useState, useRef } from 'react'
import { Message } from '../../../../core/components'
import _ from "lodash"
import './style.scss'
export const AddTournament = () => {
const { formValue, changeFormValue, resetFormValue } = useForm({ name: "", format: "", year: "" })
const [saving, setSaving] = useState(false)
const [sent, setSent] = useState(undefined)
const submit = async (e) => {
e.preventDefault()
setSent(true)
if (formValue.name != "" && formValue.format != "" && formValue.year != "") {
setSaving(true)
try {
await firebaseFirestore.collection("tournaments").add(formValue)
const tourneys = await firebaseFirestore.collection("tournaments").where('name', '==', formValue.name).where('format', '==', formValue.format).where('year', '==', formValue.year).get()
const id = tourneys.docs[0].id
console.log(id)
setSaving(false)
setSent(true)
resetFormValue()
}
catch (err) {
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
}
}
else {
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
}
}
const [selectedFile, setSelectedFile] = useState(undefined)
const [selected, setSelected] = useState(false)
const [tourneys, setTourneys] = useState(undefined)
const addTournaments = async (e) => {
e.preventDefault()
setSent(true)
setSaving(true)
try {
if (!_.isString(tourneys[0].name) || !_.isString(tourneys[0].format) || !_.isString(tourneys[0].year)) {
alert('Invalid JSON Selected !')
setSaving(false)
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
setSelectedFile(undefined)
setSelected(false)
setTourneys(undefined)
}
else {
tourneys.forEach(async (tournament) => {
const curTour = { name: tournament.name, format: tournament.format, year: tournament.year }
await firebaseFirestore.collection("tournaments").add(curTour)
})
setSaving(false)
setSent(true)
setSelectedFile(undefined)
setSelected(false)
setTourneys(undefined)
alert("Done !")
}
}
catch (err) {
setSaving(false)
setSent(false)
setTimeout(() => { setSent(undefined) }, 1500)
setSelectedFile(undefined)
setSelected(false)
setTourneys(undefined)
}
}
const fileInput = useRef(null)
const changeHandler = (event) => {
setSelectedFile(event.target.files[0])
setSelected(true)
}
useEffect(() => {
if (selectedFile != undefined) {
const reader = new FileReader()
reader.readAsText(selectedFile)
reader.onload = (e) => {
setTourneys(JSON.parse(e.target.result))
}
}
}, [selectedFile])
return (
<div className="addTournament">
<div className="addTournamentTitle">Add Tournament:</div>
<div className="uploadTournamentsFromJSON">
<div className="getTournamentJSONFile">
<input ref={fileInput} accept=".json" type="file" onChange={changeHandler} style={{ display: "none" }} />
<button className="loadTournamentJSONFileButton" onClick={(e) => fileInput.current.click()}>Load JSON</button>
<div>
{
`Current file: ${selectedFile ? selectedFile.name : ""}`
}
</div>
</div>
<div>
<button className="uploadTournamentsJSONButton" disabled={selected ? false : true} onClick={addTournaments}>Upload JSON to Firestore</button>
</div>
</div>
<form action="" id="tournamentForm">
<input type="text" placeholder="Name" value={formValue.name} spellCheck={false} onChange={(e) => { changeFormValue("name", e.target.value) }} />
<input type="text" placeholder="Format" value={formValue.format} spellCheck={false} onChange={(e) => { changeFormValue("format", e.target.value) }} />
<input type="text" placeholder="Year" value={formValue.year} spellCheck={false} onChange={(e) => { changeFormValue("year", e.target.value) }} />
<button onClick={submit} className="addTournamentButton">Add tournament</button>
</form>
<div className="tournamentAddingResultContainer">
{
saving ? <div>Loading</div> : <Message status={sent} successMessage={<><div className="successLineOne">TOURNAMENT(S) ADDED</div> <div className="successLineTwo">TOURNAMENT(S) ADDED SUCCESSFULLY</div></>} failureMessage={<><div className="failureLineOne">TOURNAMENT(S) NOT ADDED</div> <div className="failureLineTwo">TOURNAMENT(S) ADDING FAILED. PLEASE TRY AGAIN</div></>} />
}
</div>
<div className="endingLine"></div>
</div>
)
}
@@ -0,0 +1,129 @@
.addTournament {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
.addTournamentTitle {
width: 100%;
display: flex;
align-items: center;
font-family: "Lora", serif;
margin-top: 1rem;
padding-left: 5rem;
font-size: 1rem;
}
.uploadTournamentsFromJSON {
width: 100%;
display: flex;
flex-direction: column;
align-items: flex-start;
margin: 1rem;
font-size: 0.9rem;
}
.uploadTournamentsFromJSON button {
color: white;
background-color: #282a35;
border: none;
padding: 0.5rem;
border-radius: 10px;
font-weight: bolder;
}
.uploadTournamentsFromJSON button:hover {
background-color: #000000;
}
.getTournamentJSONFile {
margin-left: 7rem;
display: flex;
align-items: center;
margin-bottom: 0.75rem;
}
.loadTournamentJSONFileButton {
margin-right: 1rem;
}
.uploadTournamentsJSONButton {
margin-left: 7rem;
}
#tournamentForm {
display: flex;
margin-top: 0.5rem;
}
#tournamentForm input {
font-family: "Lora", serif;
padding-left: 0.3rem;
}
.addTournamentButton {
color: white;
background-color: #282a35;
border: none;
padding: 0.5rem;
border-radius: 10px;
font-weight: bolder;
margin-left: 1rem;
}
.addTournamentButton:hover {
background-color: #000000;
}
.tournamentAddingResultContainer {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
margin-top: 0;
height: 4rem;
}
.endingLine {
margin-top: 0rem;
margin-bottom: 1rem;
width: 100%;
border-top: 1px solid black;
}
@media only screen and (max-width: 425px) {
.addTournamentTitle {
width: 100%;
display: flex;
justify-content: center;
padding-left: 0;
font-size: 1rem;
}
.getTournamentJSONFile {
margin-left: 3rem;
}
.uploadTournamentsJSONButton {
margin-left: 3rem;
}
#tournamentForm {
display: flex;
flex-direction: column;
align-items: center;
}
#tournamentForm input {
margin: 0.3rem;
height: 2rem;
border: 1.5px solid #e1e1e1;
border-radius: 5px;
}
.addTournamentButton {
margin-top: 1rem;
margin-left: 0;
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
.addTournamentTitle {
font-size: 1rem;
}
.getMotionJSONFile {
margin-left: 6rem;
}
.uploadMotionsJSONButton {
margin-left: 6rem;
}
}
@@ -0,0 +1,149 @@
import { Table, EditableText, EditableTextArea, EditableSelector } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
import { topicsForMotions } from '../../../../../../core/constants/topicsForMotions'
import Select, { components } from 'react-select'
import { customTheme } from '../../../../../../core/constants/customTheme'
import { ValueContainer, MultiValueContainer, Placeholder, Option } from '../../../../../../core/components/SelectComponents'
import { useEffect, useState } from 'react'
export const TablePC = (props) => {
const { updateMotion, del, motions, getDefaultTopic } = props
const fifthColumnOptions = [
{ value: 'language', label: 'Language' },
{ value: 'division', label: 'Division' },
{ value: 'round', label: 'Round' },
{ value: 'link', label: 'Link' },
]
const [fifthColumn, setFifthColumn] = useState('round')
const fifthColumnSelectorStyles = {
valueContainer: base => ({
...base,
display: "flex",
justifyContent: "center",
}),
}
const topicSelectorStyles = {
multiValue: base => ({
...base,
fontSize: '0.7rem'
}),
placeholder: base => ({
...base,
fontSize: '0.7rem'
}),
option: base => ({
...base,
fontSize: '0.7rem'
}),
}
const tableColumns = [
{
name: 'Content',
width: '18%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.content} onUpdate={(newValue) => { updateMotion('content', newValue, motion.id) }} style={{ fontSize: "0.7rem", padding: "0.4rem" }} />
)
}
},
{
name: 'InfoSlide',
width: '18%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.infoSlide} onUpdate={(newValue) => { updateMotion('infoSlide', newValue, motion.id) }} style={{ fontSize: "0.7rem", padding: "0.4rem" }} />
)
}
},
{
name: 'Tournament',
width: '13%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.tournamentID} onUpdate={(newValue) => { updateMotion('tournamentID', newValue, motion.id) }} style={{ textAlign: "center", fontSize: "0.7rem" }} />
)
}
},
{
name: 'ID',
width: '13%',
render: (motion) => {
return (
<div style={{ textAlign: "center", fontSize: "0.7rem" }}>{motion.id}</div>
)
}
},
{
name: <Select className="fifthColumnSelector"
theme={customTheme}
options={fifthColumnOptions}
onChange={(val) => { setFifthColumn(val.value) }}
defaultValue={{ value: 'round', label: 'Round' }}
components={{ ValueContainer }}
styles={fifthColumnSelectorStyles}
/>,
width: '12%',
render: (motion) => {
switch (fifthColumn) {
case 'language':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={motion.language}
onUpdate={(newValue) => { updateMotion('language', newValue, motion.id) }} />
)
case 'division':
return (
<EditableText style={{ textAlign: "center", fontSize: "0.7rem" }} defaultValue={motion.division} onUpdate={(newValue) => { updateMotion('division', newValue, motion.id) }} />
)
case 'round':
return (
<EditableTextArea style={{ textAlign: "center", fontSize: "0.7rem" }} defaultValue={motion.round} onUpdate={(newValue) => { updateMotion('round', newValue, motion.id) }} />
)
case 'link':
return (
<EditableTextArea style={{ fontSize: "0.7rem" }} defaultValue={motion.link} onUpdate={(newValue) => { updateMotion('link', newValue, motion.id) }} />
)
}
},
},
{
name: 'Topic',
width: '24%',
render: (motion) => {
return (
<EditableSelector
defaultValue={motion.topic}
defaultSelectValue={getDefaultTopic(motion.topic)}
onUpdate={(newValue) => { updateMotion('topic', newValue, motion.id) }}
options={topicsForMotions}
multi={true}
placeholder="Select Topic"
components={{ MultiValueContainer, Placeholder, Option }}
styles={topicSelectorStyles}
style={{}}
isSearchable={true}
/>
)
}
},
{
type: "action",
width: '2%',
render: (motion) => {
return (
<button className="removeMotionButton" onClick={() => { del(motion.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]
return (
<Table
columns={tableColumns}
dataSource={motions}
names={tableClassNames.adminLoadMotions}
showActions={true}
/>
)
}
@@ -0,0 +1,52 @@
import { Table, EditableTextArea } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
export const TablePhone = (props) => {
const { updateMotion, del, motions } = props
return (
<Table
columns={[
{
name: 'Content',
width: '35%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.content} onUpdate={(newValue) => { updateMotion('content', newValue, motion.id) }} style={{ fontSize: "0.6rem", padding: "0.4rem" }} />
)
}
},
{
name: 'InfoSlide',
width: '35%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.infoSlide} onUpdate={(newValue) => { updateMotion('infoSlide', newValue, motion.id) }} style={{ fontSize: "0.6rem", padding: "0.4rem" }} />
)
}
},
{
name: 'Tournament',
width: '26%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.tournamentID} onUpdate={(newValue) => { updateMotion('tournamentID', newValue, motion.id) }} style={{ textAlign: "center", fontSize: "0.4rem" }} />
)
}
},
{
type: "action",
width: '4%',
render: (motion) => {
return (
<button className="removeMotionButton" onClick={() => { del(motion.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]}
dataSource={motions}
names={tableClassNames.adminLoadMotions}
showActions={true}
/>
)
}
@@ -0,0 +1,52 @@
import { Table, EditableTextArea } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
export const TableTablet = (props) => {
const {updateMotion, del, motions} = props
return (
<Table
columns={[
{
name: 'Content',
width: '37%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.content} onUpdate={(newValue) => { updateMotion('content', newValue, motion.id) }} style={{ fontSize: "0.7rem", padding: "0.4rem" }} />
)
}
},
{
name: 'InfoSlide',
width: '37%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.infoSlide} onUpdate={(newValue) => { updateMotion('infoSlide', newValue, motion.id) }} style={{ fontSize: "0.7rem", padding: "0.4rem" }} />
)
}
},
{
name: 'Tournament',
width: '22%',
render: (motion) => {
return (
<EditableTextArea defaultValue={motion.tournamentID} onUpdate={(newValue) => { updateMotion('tournamentID', newValue, motion.id) }} style={{ fontSize: "0.7rem", textAlign: 'center' }}/>
)
}
},
{
type: "action",
width: '4%',
render: (motion) => {
return (
<button className="removeMotionButton" onClick={() => { del(motion.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]}
dataSource={motions}
names={tableClassNames.adminLoadMotions}
showActions={true}
/>
)
}
@@ -0,0 +1,3 @@
export * from "./TablePC"
export * from "./TablePhone"
export * from "./TableTablet"
@@ -0,0 +1,261 @@
import './style.css'
import Select from 'react-select'
import { useState, useRef } from 'react'
import { firebaseFirestore } from '../../../../firebase'
import { topics, languages, customTheme, topicsForMotions } from '../../../../core/constants'
import tournamentOptions from '../../../../core/constants/tournamentOptions.json'
import { useDeviceBreakPoint } from '../../../../core/hooks'
import { TablePC, TablePhone, TableTablet } from './components/tables'
import DownloadLink from 'react-download-link'
import _ from 'lodash'
export const LoadMotions = () => {
const { isPhone, isTablet } = useDeviceBreakPoint()
const [topic, setTopic] = useState([])
const [language, setLanguage] = useState('')
const [tournamentID, setTournamentID] = useState('')
const [motionID, setMotionID] = useState('')
const [max, setMax] = useState(10)
const [motions, setMotions] = useState([])
const [loading, setLoading] = useState(false)
const tournamentRef = useRef(null)
function changeTopic(val) {
if (val.length == 0) {
setTopic([])
}
else {
val.forEach(obj => {
setTopic([...topic, obj.value])
})
}
}
function changeLanguage(val) {
if (val == null) {
setLanguage('')
}
else {
setLanguage(val.value)
}
}
function changeTournament(val) {
if (val == null) {
setTournamentID('')
}
else {
setTournamentID(val.value)
}
}
function changeTournamentID(e) {
setTournamentID(e.target.value)
let newOption
tournamentOptions.forEach(option => {
if (option.value == e.target.value) {
newOption = tournamentOptions[tournamentOptions.indexOf(option)]
}
})
if (newOption != undefined) {
tournamentRef.current.select.setValue(newOption)
}
else {
tournamentRef.current.select.setValue('')
}
}
const changeMax = (e) => {
if (e.target.value == "") {
setMax(10)
}
else {
setMax(e.target.value)
}
}
const loadMotions = async () => {
let motionsRef = firebaseFirestore.collection('motions')
setLoading(true)
if (language != '') {
motionsRef = motionsRef.where('language', '==', language)
}
if (tournamentID != '') {
motionsRef = motionsRef.where('tournamentID', '==', tournamentID)
}
if (motionID != '') {
motionsRef = motionsRef.doc(motionID)
}
if (topic != []) {
topic.forEach(key => {
motionsRef = motionsRef.where(`topic.${key}.check`, '==', true)
})
}
if (motionID == '') {
const motionDataRaw = await motionsRef.limit(max).get()
const motionData = []
motionDataRaw.forEach(doc => {
let loadedTopicTemp = { ...doc.data() }
let tempTopics = []
for (const key in loadedTopicTemp.topic) {
tempTopics.push(loadedTopicTemp.topic[key]['title'])
}
motionData.push({ ...doc.data(), id: doc.id, topicList: tempTopics })
})
let motionDataSorted = []
const preliminaryMotions = []
const octofinals = []
const semifinals = []
const quarterfinals = []
const finals = []
const theRest = []
motionData.forEach(motion => {
if (!isNaN(motion.round)) {
preliminaryMotions.push(motion)
}
else if (motion.round.includes("Octofinals")) {
octofinals.push(motion)
}
else if (motion.round.includes("Semifinals")) {
semifinals.push(motion)
}
else if (motion.round.includes("Quarterfinals")) {
quarterfinals.push(motion)
}
else if (motion.round.includes("Final")) {
finals.push(motion)
}
else {
theRest.push(motion)
}
})
preliminaryMotions.sort((a, b) => (a.round > b.round) ? 1 : -1)
octofinals.sort((a, b) => (a.round > b.round) ? 1 : -1)
semifinals.sort((a, b) => (a.round > b.round) ? 1 : -1)
quarterfinals.sort((a, b) => (a.round > b.round) ? 1 : -1)
finals.sort((a, b) => (a.round > b.round) ? 1 : -1)
theRest.sort((a, b) => (a.round > b.round) ? 1 : -1)
motionDataSorted = motionDataSorted.concat(preliminaryMotions, octofinals, semifinals, quarterfinals, finals, theRest)
setMotions(motionDataSorted)
setLoading(false)
}
else {
const motionDataRaw = await motionsRef.get()
if (motionDataRaw.exists == true) {
const content = motionDataRaw._delegate._document.data.value.mapValue.fields.content.stringValue
const division = motionDataRaw._delegate._document.data.value.mapValue.fields.division.stringValue
const infoSlide = motionDataRaw._delegate._document.data.value.mapValue.fields.infoSlide.stringValue
const language = motionDataRaw._delegate._document.data.value.mapValue.fields.language.stringValue
const link = motionDataRaw._delegate._document.data.value.mapValue.fields.link.stringValue
const round = motionDataRaw._delegate._document.data.value.mapValue.fields.round.stringValue
const tournamentID = motionDataRaw._delegate._document.data.value.mapValue.fields.tournamentID.stringValue
const topic = motionDataRaw._delegate._document.data.value.mapValue.fields.topic.mapValue
const motionData = []
motionData.push({ content: content, division: division, infoSlide: infoSlide, language: language, link: link, round: round, tournamentID: tournamentID, topic: topic, id: motionID })
console.log(motionData)
setMotions(motionData)
setLoading(false)
}
else {
setLoading(false)
setMotions([])
}
}
}
const del = async (id) => {
await firebaseFirestore.collection("motions").doc(id).delete()
await loadMotions()
}
const updateMotion = async (fieldName, newValue, id) => {
if (newValue != undefined && newValue != null) {
const motionsRef = firebaseFirestore.collection('motions').doc(id)
await motionsRef.update({
[fieldName]: newValue
})
}
}
const downloadJSON = async () => {
let motionsRef = firebaseFirestore.collection('motions')
const motionDataRaw = await motionsRef.get()
const motionData = []
motionDataRaw.forEach(doc => { motionData.push({ ...doc.data(), id: doc.id }) })
return JSON.stringify(motionData)
}
const getDefaultTopic = (topicMap) => {
const items = []
for (const [key, value] of Object.entries(topicMap)) {
for (let i = 0; i < topicsForMotions.length; i++) {
if (value.title == topicsForMotions[i].label) {
items.push(topicsForMotions[i])
}
}
}
return items
}
const tableProps = { updateMotion, del, motions, getDefaultTopic }
//---------------------------------------------------------------------------------------------------------
const script = async () => {
}
//---------------------------------------------------------------------------------------------------------
return (
<div className="loadMotions">
<div className="loadedMotionsHeaderContainer">
<div className="motionsListTitle">Existing motions: </div>
<button id="fetchMotionsButton" onClick={() => { loadMotions() }}>Refresh</button>
</div>
<div className="filterMotions">
<Select className="motionFilterItem"
theme={customTheme}
placeholder="Topic"
isSearchable={true}
options={topics}
onChange={changeTopic}
isMulti={true}
/>
<Select className="motionFilterItem"
theme={customTheme}
placeholder="Language"
options={languages}
onChange={changeLanguage}
/>
<Select className="motionFilterItem"
theme={customTheme}
placeholder="Tournament"
options={tournamentOptions}
onChange={changeTournament}
isClearable={true}
ref={tournamentRef}
/>
<input className="motionFilterItem motionFilterItemBox" spellCheck={false} type="text" placeholder="Tournament ID" value={tournamentID} onChange={changeTournamentID} />
<input className="motionFilterItem motionFilterItemBox" spellCheck={false} type="text" placeholder="Motion ID" defaultValue={motionID} onChange={(e) => { setMotionID(e.target.value) }} />
<input className="motionFilterItem motionFilterItemBox inputMax" spellCheck={false} type="number" placeholder="Display at max? (Default: 10)" onChange={changeMax} />
</div>
<div className="downloadMotionsJSONButtonContainer">
<DownloadLink
className="downloadMotionsJSON"
label="Download JSON"
tagName="button"
filename="motionsFromDatabase.json"
style={{}}
exportFile={downloadJSON}
/>
</div>
{/*--------------------------------------Hide-this-by-default-------------------------------------------------*/}
{/* <div className="motionScriptContainer">
<button className="runMotionScript" onClick={script}>Run Script</button>
</div> */}
{/*--------------------------------------Ultra-Dangerous-Hidden-Dark-Magic------------------------------------*/}
<div className="displayExistingMotions">
{
loading ? <div className="loadingMotionMessage">Loading</div> :
<div className="motionsTableContainer">
{
isPhone ? <TablePhone {...tableProps} /> :
<>
{
isTablet ? <TableTablet {...tableProps} /> : <TablePC {...tableProps} />
}
</>
}
</div>
}
</div>
</div>
)
}
@@ -0,0 +1,222 @@
.loadMotions {
width: 100%;
font-family: "Lora", serif;
display: flex;
flex-direction: column;
}
.loadedMotionsHeaderContainer {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 1rem;
margin-bottom: 1rem;
}
.motionsListTitle {
padding-left: 5rem;
}
#fetchMotionsButton {
background-color: #282a35;
color: white;
border: none;
font-weight: bolder;
padding: 0.5rem;
border-radius: 10px;
margin-left: 2rem;
}
#fetchMotionsButton:hover {
background-color: #000000;
}
.filterMotions {
display: flex;
margin-left: 6rem;
flex-direction: column;
}
.filterMotions input {
font-family: "Lora", serif;
padding: 0.3rem;
padding-left: 0.6rem;
border-radius: 5px;
border: 1.5px solid #e1e1e1;
}
.motionFilterItem {
width: 25rem;
margin: 0.3rem;
font-size: 0.9rem;
}
.motionFilterItemBox {
height: 2.33rem;
}
/* Chrome, Safari, Edge, Opera */
.inputMax::-webkit-outer-spin-button,
.inputMax::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.inputMax[type=number] {
-moz-appearance: textfield;
}
.displayExistingMotions {
padding-top: 1rem;
}
.loadingMotionMessage {
margin-left: 6rem;
}
.downloadMotionsJSONButtonContainer {
width: 100%;
display: flex;
}
.downloadMotionsJSON {
background-color: #282a35;
color: white;
padding: 0.5rem;
border: none;
border-radius: 10px;
font-weight: bolder;
box-sizing: border-box;
margin-top: 1rem;
margin-left: 6rem;
}
.downloadMotionsJSON:hover {
background-color: #000000;
}
.motionsTableContainer {
width: 100%;
display: flex;
justify-content: center;
}
.motionScriptContainer {
width: 100%;
display: flex;
}
.runMotionScript {
background-color: #282a35;
color: white;
padding: 0.5rem;
border: none;
border-radius: 10px;
font-weight: bolder;
box-sizing: border-box;
margin-top: 1rem;
margin-left: 6rem;
}
.runMotionScript:hover {
background-color: #000000;
}
.loadedMotionsTable {
width: 97%;
border-collapse: collapse;
border: 1px solid black;
margin-bottom: 2rem;
}
.motionsTableHeaderRow {
display: flex;
}
.motionsTableHeader {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
border: 1px solid black;
}
.emptyMotionsTableHeaderCell {
border: 1px solid black;
}
.motionTableRow {
display: flex;
}
.motionTableCell {
margin: 0;
border: 1px solid black;
/* padding: 0.5rem; */
display: flex;
justify-content: center;
align-items: center;
}
.fifthColumnSelector {
width: 100%;
}
.motionTopicSelector {
font-size: 0.65rem;
}
.removeMotionButton {
padding: 0;
color: #e49191;
background-color: transparent;
border: none;
}
.removeMotionButton:hover {
color: rgb(167, 0, 0);
}
.deleteMotionCell {
width: 4%;
}
@media only screen and (max-width: 425px) {
.loadMotionsHeaderContainer {
padding-left: 0;
width: 100%;
display: flex;
justify-content: center;
}
.filterMotions {
margin-left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
}
.displayExistingMotions {
width: 100%;
}
.loadingMotionMessage {
width: 100%;
margin-left: 0;
display: flex;
justify-content: center;
margin-bottom: 2rem;
}
.loadedMotionsTable {
width: 97%;
}
.motionsTableHeader {
font-size: 0.7rem;
display: flex;
justify-content: center;
align-items: center;
padding: 0.2rem;
}
.motionTableCell {
/* font-size: 0.5rem; */
display: flex;
justify-content: center;
align-items: center;
/* padding: 0.4rem; */
}
.removeMotionButton i {
font-size: 0.7rem;
}
.motionFilterItem {
width: 15rem;
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
}
@@ -0,0 +1,65 @@
import { Table, EditableText } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
export const TablePC = (props) => {
const { updateTournament, del, tournaments } = props
return (
<Table
columns={[
{
name: 'Name',
width: '46%', //width of each column
render: (tournament) => {
return (
<EditableText defaultValue={tournament.name} onUpdate={(newValue) => { updateTournament('name', newValue, tournament.id) }} />
)
}
},
{
name: 'Year',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center" }}
defaultValue={tournament.year}
onUpdate={(newValue) => { updateTournament('year', newValue, tournament.id) }} />
)
}
},
{
name: 'Format',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center" }}
defaultValue={tournament.format}
onUpdate={(newValue) => { updateTournament('format', newValue, tournament.id) }} />
)
}
},
{
name: 'ID',
width: '30%',
render: (tournament) => {
return (
<div style={{ textAlign: "center" }}>{tournament.id}</div>
)
}
},
{
type: "action",
width: '4%',
render: (tournament) => {
return (
<button className="removeTournamentButton" onClick={() => { del(tournament.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]}
dataSource={tournaments}
names={tableClassNames.adminLoadTournaments}
showActions={true}
/>
)
}
@@ -0,0 +1,66 @@
import { Table } from '../../../../../../core/components/Table'
import { EditableText } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
export const TablePhone = (props) => {
const { updateTournament, del, tournaments } = props
return (
<Table
columns={[
{
name: 'Name',
width: '46%', //width of each column
render: (tournament) => {
return (
<EditableText defaultValue={tournament.name} onUpdate={(newValue) => { updateTournament('name', newValue, tournament.id) }} />
)
}
},
{
name: 'Year',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center" }}
defaultValue={tournament.year}
onUpdate={(newValue) => { updateTournament('year', newValue, tournament.id) }} />
)
}
},
{
name: 'Format',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center" }}
defaultValue={tournament.format}
onUpdate={(newValue) => { updateTournament('format', newValue, tournament.id) }} />
)
}
},
{
name: 'ID',
width: '30%',
render: (tournament) => {
return (
<div style={{ textAlign: "center" }}>{tournament.id}</div>
)
}
},
{
type: "action",
width: '4%',
render: (tournament) => {
return (
<button className="removeTournamentButton" onClick={() => { del(tournament.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]}
dataSource={tournaments}
names={tableClassNames.adminLoadTournaments}
showActions={true}
/>
)
}
@@ -0,0 +1,68 @@
import { Table, EditableText } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
export const TableTablet = (props) => {
const { updateTournament, del, tournaments } = props
return (
<Table
columns={[
{
name: 'Name',
width: '46%', //width of each column
render: (tournament) => {
return (
<EditableText
style={{ fontSize: '0.7rem' }}
defaultValue={tournament.name}
onUpdate={(newValue) => { updateTournament('name', newValue, tournament.id) }} />
)
}
},
{
name: 'Year',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center", fontSize: '0.7rem' }}
defaultValue={tournament.year}
onUpdate={(newValue) => { updateTournament('year', newValue, tournament.id) }} />
)
}
},
{
name: 'Format',
width: '10%',
render: (tournament) => {
return (
<EditableText style={{ textAlign: "center", fontSize: '0.7rem' }}
defaultValue={tournament.format}
onUpdate={(newValue) => { updateTournament('format', newValue, tournament.id) }} />
)
}
},
{
name: 'ID',
width: '30%',
render: (tournament) => {
return (
<div style={{ textAlign: "center", fontSize: '0.7rem' }}>{tournament.id}</div>
)
}
},
{
type: "action",
width: '4%',
render: (tournament) => {
return (
<button className="removeTournamentButton" onClick={() => { del(tournament.id) }}>
<i className="fas fa-times" />
</button>
)
}
}
]}
dataSource={tournaments}
names={tableClassNames.adminLoadTournaments}
showActions={true}
/>
)
}
@@ -0,0 +1,3 @@
export * from "./TablePC"
export * from "./TablePhone"
export * from "./TableTablet"
@@ -0,0 +1,185 @@
import './style.css'
import Select from 'react-select'
import DownloadLink from 'react-download-link'
import { useState, useRef } from 'react'
import { firebaseFirestore } from '../../../../firebase'
import { formats, customTheme } from '../../../../core/constants'
import tournamentOptions from '../../../../core/constants/tournamentOptions.json'
import { useDeviceBreakPoint } from '../../../../core/hooks'
import { TablePC, TablePhone, TableTablet } from './components/tables'
export const LoadTournaments = () => {
const { isPhone, isTablet } = useDeviceBreakPoint()
const [year, setYear] = useState('')
const [format, setFormat] = useState('')
const [id, setID] = useState('')
const [loading, setLoading] = useState(false)
const [tournaments, setTournaments] = useState([])
const [max, setMax] = useState(10)
const tournamentRef = useRef(null)
const loadTournaments = async () => {
let tournamentsRef = firebaseFirestore.collection('tournaments')
setLoading(true)
let tournamentDataRaw = undefined
if (id == '') {
if (format != '') {
tournamentsRef = tournamentsRef.where('format', '==', format)
}
if (year != '') {
tournamentsRef = tournamentsRef.where('year', '==', year)
}
tournamentDataRaw = await tournamentsRef.limit(max).get()
const tournamentData = []
tournamentDataRaw.forEach(doc => { tournamentData.push({ ...doc.data(), id: doc.id }) })
setTournaments(tournamentData)
setLoading(false)
}
else {
tournamentDataRaw = await tournamentsRef.doc(id).get()
if (tournamentDataRaw.exists == true) {
const name = tournamentDataRaw._delegate._document.data.value.mapValue.fields.name.stringValue
const year = tournamentDataRaw._delegate._document.data.value.mapValue.fields.year.stringValue
const format = tournamentDataRaw._delegate._document.data.value.mapValue.fields.format.stringValue
const tournamentData = []
tournamentData.push({ name: name, year: year, format: format, id: id })
setTournaments(tournamentData)
setLoading(false)
}
else {
setLoading(false)
setTournaments([])
}
}
}
function changeTournament(val) {
if (val == null) {
setID('')
}
else {
setID(val.value)
}
}
function changeTournamentID(e) {
setID(e.target.value)
let newOption
tournamentOptions.forEach(option => {
if (option.value == e.target.value) {
newOption = tournamentOptions[tournamentOptions.indexOf(option)]
}
})
if (newOption != undefined) {
tournamentRef.current.select.setValue(newOption)
}
else {
tournamentRef.current.select.setValue('')
}
}
const changeMax = (e) => {
if (e.target.value == "") {
setMax(10)
}
else {
setMax(e.target.value)
}
}
const del = (id) => {
firebaseFirestore.collection("tournaments").doc(id).delete()
const newTournaments = tournaments.filter(tournament => {
return (tournament.id !== id)
})
setTournaments(newTournaments)
}
function changeFormat(val) {
setFormat(val.value)
}
const updateTournament = async (fieldName, newValue, id) => {
const tournamentsRef = firebaseFirestore.collection('tournaments').doc(id)
const response = await tournamentsRef.update({
[fieldName]: newValue
})
}
const downloadJSON = async () => {
let tournamentsRef = firebaseFirestore.collection('tournaments')
const tournamentDataRaw = await tournamentsRef.get()
const tournamentData = []
tournamentDataRaw.forEach(doc => { tournamentData.push({ ...doc.data(), id: doc.id }) })
return JSON.stringify(tournamentData)
}
const tableProps = { updateTournament, del, tournaments }
//---------------------------------------------------------------------------------------------------------
const script = async () => {
const tournamentsRef = firebaseFirestore.collection('tournaments')
const tournamentDataRaw = await tournamentsRef.get()
const tournamentData = []
tournamentDataRaw.forEach(doc => {
tournamentData.push({ ...doc.data(), id: doc.id })
})
tournamentData.forEach(async (doc) => {
const name = doc.name
const firstFour = name.slice(0, 4)
if (!isNaN(firstFour)) {
const newName = name.slice(5)
await tournamentsRef.doc(doc.id).update({ name: newName })
}
})
alert("Done !")
}
//---------------------------------------------------------------------------------------------------------
return (
<div className="loadTournaments">
<div className="loadedTournamentsHeaderContainer">
<div className="tournamentsListTitle">Existing tournaments: </div>
<button id="fetchTournamentsButton" onClick={loadTournaments}>Refresh</button>
</div>
<div className="filterTournaments">
<Select className="tournamentFilterItem"
theme={customTheme}
placeholder="Format"
options={formats}
onChange={changeFormat}
/>
<input className="tournamentFilterItem tournamentFilterItemBox" spellCheck={false} type="text" placeholder="Year" onChange={(e) => { setYear(e.target.value) }} />
<Select className="tournamentFilterItem"
theme={customTheme}
placeholder="Tournament"
options={tournamentOptions}
onChange={changeTournament}
isClearable={true}
ref={tournamentRef}
/>
<input className="tournamentFilterItem tournamentFilterItemBox" spellCheck={false} type="text" placeholder="ID" value={id} onChange={changeTournamentID} />
<input className="tournamentFilterItem tournamentFilterItemBox maxNum" spellCheck={false} type="text" placeholder="Display at max? (Default: 10)" onChange={changeMax} />
</div>
<div className="downloadTournamentsJSONButtonContainer">
<DownloadLink
className="downloadTournamentsJSON"
label="Download JSON"
tagName="button"
filename="tournamentsFromDatabase.json"
style={{}}
exportFile={downloadJSON}
/>
</div>
{/*--------------------------------------Hide-this-by-default-------------------------------------------------*/}
{/* <div className="tournamentScriptContainer">
<button className="runTournamentScript" onClick={script}>Run Script</button>
</div> */}
{/*--------------------------------------Ultra-Dangerous-Hidden-Dark-Magic------------------------------------*/}
<div className="displayExistingTournaments">
{
loading ? <div className="loadingTournamentMessage">Loading</div> :
<div className="tournamentsTableContainer">
{
isPhone ? <TablePhone {...tableProps} /> :
<>
{
isTablet ? <TableTablet {...tableProps} /> : <TablePC {...tableProps} />
}
</>
}
</div>
}
</div>
</div>
)
}
@@ -0,0 +1,236 @@
.loadTournaments {
width: 100%;
font-family: "Lora", serif;
display: flex;
flex-direction: column;
border-bottom: 1px solid black;
padding-bottom: 2rem;
}
.loadedTournamentsHeaderContainer {
display: flex;
justify-content: flex-start;
align-items: center;
margin-top: 1rem;
}
.tournamentsListTitle {
padding-left: 5rem;
}
#fetchTournamentsButton {
margin-left: 2rem;
background-color: #282a35;
color: white;
padding: 0.5rem;
border: none;
border-radius: 10px;
font-weight: bolder;
}
#fetchTournamentsButton:hover {
background-color: #000000;
}
.displayExistingTournaments {
padding-top: 1rem;
}
.loadingTournamentMessage {
margin-left: 6rem;
}
.filterTournaments {
display: flex;
flex-direction: column;
margin-left: 6rem;
}
.filterTournaments input {
font-family: "Lora", serif;
padding: 0.3rem;
padding-left: 0.6rem;
border-radius: 5px;
border: 1.5px solid #e1e1e1;
}
.tournamentFilterItem {
width: 15rem;
margin: 0.3rem;
font-size: 0.9rem;
}
.tournamentFilterItemBox {
height: 2.33rem;
}
/* Chrome, Safari, Edge, Opera */
.maxNum::-webkit-outer-spin-button,
.maxNum::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* Firefox */
.maxNum[type=number] {
-moz-appearance: textfield;
}
.tournamentsTableContainer {
width: 100%;
}
.downloadTournamentsJSONButtonContainer {
width: 100%;
display: flex;
}
.downloadTournamentsJSON {
background-color: #282a35;
color: white;
padding: 0.5rem;
border: none;
border-radius: 10px;
font-weight: bolder;
box-sizing: border-box;
margin-top: 1rem;
margin-left: 6rem;
}
.downloadTournamentsJSON:hover {
background-color: #000000;
}
.tournamentScriptContainer {
width: 100%;
display: flex;
}
.runTournamentScript {
background-color: #282a35;
color: white;
padding: 0.5rem;
border: none;
border-radius: 10px;
font-weight: bolder;
box-sizing: border-box;
margin-top: 1rem;
margin-left: 6rem;
}
.runTournamentScript:hover {
background-color: #000000;
}
.loadedTournamentsTable {
width: 70%;
margin-left: 6rem;
border: 1px solid black;
border-collapse: collapse;
}
.tournamentsTableHeaderRow {
display: flex;
}
.tournamentsTableHeader {
display: flex;
justify-content: center;
align-items: center;
margin: 0;
border: 1px solid black;
}
.emptyTournamentsTableHeaderCell {
border: 1px solid black;
}
.tournamentTableRow {
display: flex;
font-size: 0.8rem;
}
.tournamentTableCell {
margin: 0;
border: 1px solid black;
padding: 0.5rem;
display: flex;
justify-content: center;
align-items: center;
}
.removeTournamentButton {
padding: 0;
color: #e49191;
background-color: transparent;
border: none;
}
.removeTournamentButton:hover {
color: rgb(167, 0, 0);
}
.deleteTournamentCell {
display: flex;
justify-content: center;
align-items: center;
}
@media only screen and (max-width: 425px) {
.filterTournaments {
margin-left: 0;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1rem;
}
.loadingTournamentMessage {
margin-left: 0;
display: flex;
justify-content: center;
}
.loadedTournamentsTable {
font-size: 0.6rem;
width: 100%;
margin-left: 0.3rem;
margin-right: 0.3rem;
}
.tournamentsTableContainer {
width: 100%;
display: flex;
justify-content: center;
}
.tournamentsTableContainer input {
font-size: 0.47rem;
}
.tournamentsTableHeader {
padding: 0.2rem;
}
.tournamentTableCell {
font-size: 0.47rem;
padding: 0;
}
.removeTournamentButton i {
font-size: 0.6rem;
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
.loadTournaments {
}
.loadedTournamentsTable {
font-size: 1rem;
width: 95%;
margin-left: 0;
}
.tournamentsTableContainer {
width: 100%;
display: flex;
justify-content: center;
}
.tournamentsTableHeader {
padding: 0.4rem;
}
.tournamentTableCell {
padding: 0;
}
.filterTournaments {
width: 100%;
margin-top: 1rem;
display: flex;
justify-content: center;
margin-left: 6rem;
}
}
@@ -0,0 +1,137 @@
import { Table, EditableText, EditableTextArea, EditableSelector } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
import { topics } from '../../../../../../core/constants/topics'
import { formats } from '../../../../../../core/constants/formats'
export const TablePC = (props) => {
const { updateRequest, del, requests, getDefaultFormat, getDefaultTopic, addToDatabase } = props
return (
<Table
columns={[
{
name: 'Motion',
width: '13%',
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.7rem" }} defaultValue={request.motion} onUpdate={(newValue) => { updateRequest('motion', newValue, request.id) }} />
)
}
},
{
name: 'InfoSlide',
width: '13%',
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.7rem" }} defaultValue={request.infoSlide} onUpdate={(newValue) => { updateRequest('infoSlide', newValue, request.id) }} />
)
}
},
{
name: 'Topic',
width: '25%',
render: (request) => {
return (
<EditableSelector
defaultValue={request.topic}
defaultSelectValue={getDefaultTopic(request.topic)}
onUpdate={(newValue) => { updateRequest('topic', newValue, request.id) }}
options={topics}
multi={true}
/>
)
}
},
{
name: 'Language',
width: '7%',
render: (request) => {
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={request.language}
onUpdate={(newValue) => { updateRequest('language', newValue, request.id) }} />
)
}
},
{
name: 'Tournament',
width: '9%',
render: (request) => {
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={request.tournamentName}
onUpdate={(newValue) => { updateRequest('tournamentName', newValue, request.id) }} />
)
}
},
{
name: 'Year',
width: '5%',
render: (request) => {
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={request.year}
onUpdate={(newValue) => { updateRequest('year', newValue, request.id) }} />
)
}
},
{
name: 'Round',
width: '5%',
render: (request) => {
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={request.round}
onUpdate={(newValue) => { updateRequest('round', newValue, request.id) }} />
)
}
},
{
name: 'Format',
width: '12%',
render: (request) => {
return (
<EditableSelector
defaultValue={request.format}
defaultSelectValue={getDefaultFormat(request.format)}
onUpdate={(newValue) => { updateRequest('format', newValue, request.id) }}
options={formats}
multi={false}
/>
)
}
},
{
name: 'Link',
width: '6%',
render: (request) => {
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.7rem" }}
defaultValue={request.link}
onUpdate={(newValue) => { updateRequest('link', newValue, request.id) }} />
)
}
},
{
type: "action",
width: '5%',
render: (request) => {
return (
<div className="actionIcons">
<button onClick={() => { addToDatabase(request) }}><i className="fas fa-check actionIcon tick" /></button>
<button onClick={() => { del(request.id) }}><i className="fas fa-times actionIcon cross" /></button>
</div>
)
}
}
]}
dataSource={requests}
names={tableClassNames.adminPendingRequests}
showActions={true}
/>
)
}
@@ -0,0 +1,187 @@
import { Table, EditableText, EditableTextArea, EditableSelector } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
import { topics } from '../../../../../../core/constants/topics'
import { formats } from '../../../../../../core/constants/formats'
import Select, { components } from 'react-select'
import { ValueContainer, MultiValueContainer, SelectContainer, ClearIndicator, DropdownIndicator } from '../../../../../../core/components/SelectComponents'
import { customTheme } from '../../../../../../core/constants/customTheme'
import { useState } from 'react'
export const TablePhone = (props) => {
const { updateRequest, getDefaultFormat, getDefaultTopic, addToDatabase, del, requests } = props
const thirdColumnOptions = [
{ value: 'name', label: 'Tournament\'s name' },
{ value: 'topic', label: "Topic" },
{ value: 'language', label: 'Language' },
{ value: 'year', label: 'Year' },
{ value: 'round', label: 'Round' },
{ value: 'format', label: 'Format' },
{ value: 'link', label: 'Link' },
]
const [thirdColumn, setThirdColumn] = useState('name')
const IndicatorsContainer = props => {
return (
<div style={{}}>
<components.IndicatorsContainer {...props} />
</div>
)
}
const thirdColumnSelectorStyles = {
valueContainer: base => ({
...base,
display: "flex",
justifyContent: "center",
}),
}
const indicatorSeparatorStyle = {
alignSelf: 'stretch',
backgroundColor: 'hsl(0, 0%, 80%)',
marginBottom: '8px',
marginTop: '8px',
width: '1px',
height: '0.8rem',
}
const IndicatorSeparator = ({ innerProps }) => {
return <span style={indicatorSeparatorStyle} {...innerProps} />
}
const topicSelectorStyles = {
valueContainer: base => ({
...base,
padding: "0.5rem",
}),
clearIndicator: base => ({
...base,
padding: 0.5,
marginTop: 0.7,
transform: 'scaleX(0.85)',
transform: 'scaleY(0.85)',
}),
dropdownIndicator: base => ({
...base,
padding: 0.5,
}),
indicatorsContainer: base => ({
...base,
})
}
return (
<Table
columns={[
{
name: 'Motion',
width: '15.5%',
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.55rem", padding: "0.3rem" }} defaultValue={request.motion} onUpdate={(newValue) => { updateRequest('motion', newValue, request.id) }} />
)
}
},
{
name: 'InfoSlide',
width: '20.5%',
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.55rem", padding: "0.3rem" }} defaultValue={request.infoSlide} onUpdate={(newValue) => { updateRequest('infoSlide', newValue, request.id) }} />
)
}
},
{
name: <Select className="thirdColumnSelector"
theme={customTheme}
options={thirdColumnOptions}
onChange={(val) => { setThirdColumn(val.value) }}
defaultValue={{ value: 'name', label: 'Tournament\'s name' }}
components={{ ValueContainer }}
styles={thirdColumnSelectorStyles}
/>,
width: '57%', //38
render: (request) => {
switch (thirdColumn) {
case 'name':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.6rem" }}
defaultValue={request.tournamentName}
onUpdate={(newValue) => { updateRequest('tournamentName', newValue, request.id) }} />
)
break
case 'language':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.6rem" }}
defaultValue={request.language}
onUpdate={(newValue) => { updateRequest('language', newValue, request.id) }} />
)
break
case 'year':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.6rem" }}
defaultValue={request.year}
onUpdate={(newValue) => { updateRequest('year', newValue, request.id) }} />
)
break
case 'round':
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.6rem" }}
defaultValue={request.round}
onUpdate={(newValue) => { updateRequest('round', newValue, request.id) }} />
)
break
case 'format':
return (
<EditableSelector
defaultValue={request.format}
defaultSelectValue={getDefaultFormat(request.format)}
onUpdate={(newValue) => { updateRequest('format', newValue, request.id) }}
style={{ fontSize: "0.6rem" }}
options={formats}
multi={false}
/>
)
break
case 'link':
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.6rem" }}
defaultValue={request.link}
onUpdate={(newValue) => { updateRequest('link', newValue, request.id) }} />
)
break
case 'topic':
return (
<EditableSelector
defaultValue={request.topic}
defaultSelectValue={getDefaultTopic(request.topic)}
onUpdate={(newValue) => { updateRequest('topic', newValue, request.id) }}
options={topics}
multi={true}
components={{ SelectContainer, ValueContainer, MultiValueContainer, IndicatorsContainer, ClearIndicator, DropdownIndicator, IndicatorSeparator }}
styles={topicSelectorStyles}
/>
)
break
}
},
},
{
type: "action",
width: '7%',
render: (request) => {
return (
<div className="actionIcons">
<button onClick={() => { addToDatabase(request) }}><i className="fas fa-check actionIcon tick" /></button>
<button onClick={() => { del(request.id) }}><i className="fas fa-times actionIcon cross" /></button>
</div>
)
}
}
]}
dataSource={requests}
names={tableClassNames.adminPendingRequests}
showActions={true}
/>
)
}
@@ -0,0 +1,149 @@
import { Table, EditableText, EditableTextArea, EditableSelector } from '../../../../../../core/components'
import { tableClassNames } from '../../../../../../core/constants/tableClassNames'
import { topics } from '../../../../../../core/constants/topics'
import { formats } from '../../../../../../core/constants/formats'
import Select, { components } from 'react-select'
import { customTheme } from '../../../../../../core/constants/customTheme'
import { useState } from 'react'
export const TableTablet = (props) => {
const { updateRequest, getDefaultFormat, getDefaultTopic, addToDatabase, del, requests } = props
const thirdColumnOptions = [
{ value: 'name', label: 'Tournament\'s name' },
{ value: 'topic', label: "Topic" },
{ value: 'language', label: 'Language' },
{ value: 'year', label: 'Year' },
{ value: 'round', label: 'Round' },
{ value: 'format', label: 'Format' },
{ value: 'link', label: 'Link' },
]
const [thirdColumn, setThirdColumn] = useState('name')
const ValueContainer = ({ children, ...props }) => (
<components.ValueContainer {...props}>{children}</components.ValueContainer>
)
const thirdColumnSelectorStyles = {
valueContainer: base => ({
...base,
display: "flex",
justifyContent: "center",
}),
}
return (
<Table
columns={[
{
name: 'Motion',
width: '22%', //25
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.8rem" }} defaultValue={request.motion} onUpdate={(newValue) => { updateRequest('motion', newValue, request.id) }} />
)
}
},
{
name: 'InfoSlide',
width: '27%', //30
render: (request) => {
return (
<EditableTextArea style={{ fontSize: "0.8rem" }} defaultValue={request.infoSlide} onUpdate={(newValue) => { updateRequest('infoSlide', newValue, request.id) }} />
)
}
},
{
name: <Select className="thirdColumnSelector"
theme={customTheme}
options={thirdColumnOptions}
onChange={(val) => { setThirdColumn(val.value) }}
defaultValue={{ value: 'name', label: 'Tournament\'s name' }}
components={{ ValueContainer }}
styles={thirdColumnSelectorStyles}
/>,
width: '44%', //38
render: (request) => {
switch (thirdColumn) {
case 'name':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.8rem" }}
defaultValue={request.tournamentName}
onUpdate={(newValue) => { updateRequest('tournamentName', newValue, request.id) }} />
)
break
case 'language':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.8rem" }}
defaultValue={request.language}
onUpdate={(newValue) => { updateRequest('language', newValue, request.id) }} />
)
break
case 'year':
return (
<EditableText
style={{ textAlign: "center", fontSize: "0.8rem" }}
defaultValue={request.year}
onUpdate={(newValue) => { updateRequest('year', newValue, request.id) }} />
)
break
case 'round':
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.8rem" }}
defaultValue={request.round}
onUpdate={(newValue) => { updateRequest('round', newValue, request.id) }} />
)
break
case 'format':
return (
<EditableSelector
defaultValue={request.format}
defaultSelectValue={getDefaultFormat(request.format)}
onUpdate={(newValue) => { updateRequest('format', newValue, request.id) }}
style={{ fontSize: "0.6rem" }}
options={formats}
multi={false}
/>
)
break
case 'link':
return (
<EditableTextArea
style={{ textAlign: "center", fontSize: "0.8rem" }}
defaultValue={request.link}
onUpdate={(newValue) => { updateRequest('link', newValue, request.id) }} />
)
break
case 'topic':
return (
<EditableSelector
defaultValue={request.topic}
defaultSelectValue={getDefaultTopic(request.topic)}
onUpdate={(newValue) => { updateRequest('topic', newValue, request.id) }}
options={topics}
multi={true}
/>
)
break
}
},
},
{
type: "action",
width: '7%',
render: (request) => {
return (
<div className="actionIcons">
<button onClick={() => { addToDatabase(request) }}><i className="fas fa-check actionIcon tick" /></button>
<button onClick={() => { del(request.id) }}><i className="fas fa-times actionIcon cross" /></button>
</div>
)
}
}
]}
dataSource={requests}
names={tableClassNames.adminPendingRequests}
showActions={true}
/>
)
}
@@ -0,0 +1,3 @@
export * from "./TablePC"
export * from "./TablePhone"
export * from "./TableTablet"
@@ -0,0 +1,104 @@
import './style.css'
import { useState, useEffect } from 'react'
import { firebaseFirestore } from '../../../../firebase'
import { topics, formats } from '../../../../core/constants'
import { getFormattedTopicsFromValues, getTourneyID } from '../../../../core/helpers'
import { useDeviceBreakPoint } from "../../../../core/hooks"
import { TablePC, TablePhone, TableTablet } from './components/tables'
export const PendingRequests = () => {
const { isPhone, isTablet } = useDeviceBreakPoint()
const [requests, setRequests] = useState([])
const loadRequests = async () => {
const requestDataRaw = await firebaseFirestore.collection("requests").get()
const requestData = []
requestDataRaw.forEach(doc => requestData.push({ ...doc.data(), id: doc.id }))
setRequests(requestData)
}
const del = (id) => {
firebaseFirestore.collection("requests").doc(id).delete()
const newRequests = requests.filter(request => {
return (request.id !== id)
})
setRequests(newRequests)
}
useEffect(() => {
loadRequests()
}, [])
const updateRequest = async (fieldName, newValue, id) => {
if (newValue != undefined && newValue != null) {
const requestsRef = firebaseFirestore.collection('requests').doc(id)
await requestsRef.update({
[fieldName]: newValue
})
}
}
const getDefaultTopic = (topicArray) => {
let defaultTopics = []
topicArray.forEach(topic => {
for (let i = 0; i < topics.length; i++) {
if (topics[i].value == topic) {
defaultTopics.push(topics[i])
}
}
})
return defaultTopics
}
const getDefaultFormat = (format) => {
let returnValue
for (let i = 0; i < formats.length; i++) {
if (formats[i].value == format) {
returnValue = formats[i]
}
}
return returnValue
}
const addToDatabase = async (request) => {
const name = request.tournamentName
const year = request.year
const format = request.format
let id = await getTourneyID(name, year, format)
if (id == "") { //no existing tournament
let tournamentFormValue = {
name: name,
format: format,
year: year
}
await firebaseFirestore.collection("tournaments").add(tournamentFormValue)
id = await getTourneyID(name, year, format)
}
console.log(id)
let motionFormValue = {
content: request.motion,
infoSlide: request.infoSlide,
topic: getFormattedTopicsFromValues(request.topic),
language: request.language,
division: request.division,
tournamentID: id,
round: request.round,
link: request.link
}
firebaseFirestore.collection("motions").add(motionFormValue)
del(request.id)
}
const tableProps = { updateRequest, del, requests, getDefaultFormat, getDefaultTopic, addToDatabase }
return (
<div className="pendingRequests">
<div className="pendingMotionRequestTitle">Pending motion requests:</div>
<div className="requestsTableContainer">
{
requests.length != 0 ?
isTablet ?
<TableTablet {...tableProps} />
:
<>
{
isPhone ? <TablePhone {...tableProps} /> : <TablePC {...tableProps} />
}
</>
:
<></>
}
</div>
</div>
)
}
@@ -0,0 +1,156 @@
.pendingRequests {
margin-top: 1rem;
width: 100%;
max-width: 100vw;
font-family: "Lora", serif;
display: flex;
flex-direction: column;
padding-bottom: 1rem;
border-bottom: 1px solid black;
}
.pendingMotionRequestTitle {
width: 100%;
padding-left: 5rem;
}
.requestsTableContainer {
width: 100%;
display: flex;
justify-content: center;
}
.loadedRequestsTable {
width: 98%;
margin: 1rem;
display: flex;
flex-direction: column;
justify-content: center;
border: 1px solid black;
border-collapse: collapse;
border-top: 1px solid transparent;
border-left: 1px solid transparent;
}
.requestsTableHeaderRow {
display: flex;
width: 100%;
}
.requestsTableHeader {
margin: 0;
border: 1px solid black;
display: flex;
justify-content: center;
align-items: center;
font-size: 0.8rem;
}
.emptyRequestsTableHeaderCell {
border: 1px solid black;
}
.requestTableRow {
display: flex;
font-size: 0.7rem;
}
.requestTableCell {
margin: 0;
border: 1px solid black;
padding: 0.3rem;
display: flex;
justify-content: center;
align-items: center;
}
.actionIcons {
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
.actionIcons button {
padding: 0;
border: none;
background-color: transparent;
}
.actionIcon {
margin: 0.5rem;
}
.tick {
color: #abe491;
}
.cross {
color: #e49191;
}
.tick:hover {
color: rgb(0, 173, 0);
}
.cross:hover {
color: rgb(167, 0, 0);
}
@media only screen and (max-width: 425px) {
.pendingMotionRequestTitle {
padding-left: 0;
width: 100%;
display: flex;
justify-content: center;
margin-bottom: 1rem;
}
.requestsTableHeaderRow {}
.requestsTableHeader {
font-size: 0.6rem;
}
.loadedRequestsTable {
margin: 0;
width: 99%;
}
.thirdColumnSelector {
width: 100%;
}
.requestTableRow {
font-size: 0.6rem;
}
.requestTableCell {
display: flex;
justify-content: center;
align-items: center;
padding: 0;
}
.actionIcons {
flex-direction: column;
}
}
@media only screen and (max-width: 768px) and (min-width: 426px) {
.pendingMotionRequestTitle {
margin-bottom: 1rem;
}
.requestsTableHeaderRow {}
.requestsTableHeader {
font-size: 0.9rem;
}
.loadedRequestsTable {
margin: 0;
width: 95%;
}
.thirdColumnSelector {
width: 100%;
}
.requestTableCell {
display: flex;
justify-content: center;
align-items: center;
}
.actionIcons {
flex-direction: column;
}
}
@@ -0,0 +1,42 @@
import './style.scss'
import { firebaseAuth } from '../../../../firebase'
import { useForm } from '../../../../core/hooks'
export const SignIn = (props) => {
const { auth } = props
const { formValue, changeFormValue } = useForm({ email: '', password: '' })
const login = async () => {
const { email, password } = formValue
try {
const response = await firebaseAuth.signInWithEmailAndPassword(
email,
password
)
if (response.user.uid == process.env.REACT_APP_ADMIN_UID) {
auth(true)
}
} catch (error) {
alert(error.message)
}
}
return (
<div className='signIn'>
<input
type='text'
placeholder='Email'
spellCheck={false}
onChange={(e) => {
changeFormValue('email', e.target.value)
}}
/>
<input
type='password'
placeholder='Password'
onChange={(e) => {
changeFormValue('password', e.target.value)
}}
/>
<button onClick={login}>Login</button>
</div>
)
}
@@ -0,0 +1,37 @@
.signIn {
height: 93vh;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
overflow: hidden;
padding-top: 5rem;
input {
font-family: "Lora", serif;
padding: 0.5rem;
border: 1.5px solid #e1e1e1;
border-radius: 5px;
width: 15rem;
margin: 0.3rem;
font-size: 0.8rem;
text-align: center;
}
button {
background-color: #282a35;
color: white;
border: none;
font-weight: bold;
border-radius: 10px;
font-family: "Source Sans Pro", sans-serif;
padding: 0.3rem 0.5rem 0.3rem 0.5rem;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
margin-top: 0.5rem;
font-size: 0.8rem;
}
button:hover {
background-color: #000000;
}
}
+9
View File
@@ -0,0 +1,9 @@
.admin {
display: flex;
flex-direction: column;
align-items: center;
font-family: "Lora", serif;
width: 100%;
}

Some files were not shown because too many files have changed in this diff Show More