import React from 'react'; import { Layout, Card, List, Progress, message, Button, Select } from 'antd'; import { FileUnknownTwoTone, LeftCircleOutlined, EyeInvisibleOutlined } from '@ant-design/icons'; import { Link } from 'react-router-dom'; import ChallengesTagSort from "./challengesTagSort.js"; import { Ellipsis } from 'react-spinners-css'; import { Transition, animated } from '@react-spring/web'; const { Meta } = Card; const { Option } = Select; class Challenges extends React.Component { constructor(props) { super(props); this.child = React.createRef(); this.tagSortRef = React.createRef(); this.state = { categories: [], currentCategory: false, originalData: [], tagData: [], loadingChall: false, currentCategoryChallenges: [], foundChallenge: false, countDownTimerStrings: {}, userCategories: {}, disableNonCatFB: false }; } componentDidMount() { this.setState({ loadingChall: true }) this.fetchCategories() } parseCategories(data, usernameTeamCache) { //iterate through categories let countDownTimerStrings = {} let challengeMetaInfo = [] for (let x = 0; x < data.length; x++) { let currentCategory = data[x].challenges let solvedStats = { challenges: currentCategory.length, solved: 0, percentage: 0 } //iterate through each category's challenges for (let y = 0; y < currentCategory.length; y++) { // Iterate through the solve list of each challenge currentCategory[y].solved = false for (let i = 0; i < currentCategory[y].solves.length; i++) { if (currentCategory[y].solves[i] in usernameTeamCache && usernameTeamCache[currentCategory[y].solves[i]] === this.props.team) { solvedStats.solved += 1 currentCategory[y].solved = true break } else if (currentCategory[y].solves[i] === this.props.username) { solvedStats.solved += 1 currentCategory[y].solved = true break } } } solvedStats.percentage = Math.round((solvedStats.solved / solvedStats.challenges) * 100) challengeMetaInfo.push({ challenges: solvedStats, _id: data[x]._id, meta: data[x].meta }) if ("time" in data[x].meta) { const startTime = new Date(data[x].meta.time[0]) const endTime = new Date(data[x].meta.time[1]) const currentTime = new Date() // Competition hasn't started if (currentTime < startTime) { const timeLeft = Math.ceil((startTime - currentTime) / 1000) countDownTimerStrings[data[x]._id] = this.getTimerString(timeLeft, true) } else { const timeLeft = Math.ceil((endTime - currentTime) / 1000) countDownTimerStrings[data[x]._id] = this.getTimerString(timeLeft, false) } } } this.setState({ countDownTimerStrings: countDownTimerStrings }) setInterval(this.countDownTicker.bind(this), 1000 * 15, data) return [challengeMetaInfo, data]; } countDownTicker(data) { let countDownTimerStrings = {} for (let x = 0; x < data.length; x++) { if ("time" in data[x].meta) { const startTime = new Date(data[x].meta.time[0]) const endTime = new Date(data[x].meta.time[1]) const currentTime = new Date() // Competition hasn't started if (currentTime < startTime) { const timeLeft = Math.ceil((startTime - currentTime) / 1000) countDownTimerStrings[data[x]._id] = this.getTimerString(timeLeft, true) } else { const timeLeft = Math.ceil((endTime - currentTime) / 1000) countDownTimerStrings[data[x]._id] = this.getTimerString(timeLeft, false) } } } this.setState({ countDownTimerStrings: countDownTimerStrings }) } getTimerString(timeLeft, tillStart) { if (timeLeft > 0) { let minutes = Math.ceil(timeLeft / 60) let hours = 0 let days = 0 let months = 0 let years = 0 if (minutes >= 60) { hours = Math.floor(minutes / 60) minutes = minutes - hours * 60 if (hours >= 24) { days = Math.floor(hours / 24) hours = hours - days * 24 if (days >= 30) { months = Math.floor(days / 30) days = days - months * 30 if (months >= 12) { years = Math.floor(months / 12) months = months - years * 12 } } } } let finalTime = " till start." if (!tillStart) finalTime = " remaining." if (minutes !== 0) { finalTime = minutes.toString() + (minutes > 1 ? " minutes " : " minute ") + finalTime } if (hours !== 0) { finalTime = hours.toString() + (hours > 1 ? " hours " : " hour ") + finalTime } if (days !== 0) { finalTime = days.toString() + (days > 1 ? " days " : " day ") + finalTime } if (months !== 0) { finalTime = months.toString() + (months > 1 ? " months " : " month ") + finalTime } if (years !== 0) { finalTime = years.toString() + (years > 1 ? " years " : " year ") + finalTime } return finalTime } else { if (tillStart) return "Competition has started! Refresh this page to see the challenges!" else return "The competition has ended" } } fetchCategories = async () => { await fetch(window.ipAddress + "/v1/challenge/list", { method: 'get', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then(async (data) => { if (data.success === true) { const [categoryMetaInfo, originalData] = this.parseCategories(data.challenges, data.usernameTeamCache) //this statement changes the object data //convert array to dict let originalDataDictionary = {} for (let i = 0; i < originalData.length; i++) { originalDataDictionary[originalData[i]._id] = originalData[i].challenges } this.setState({ disableNonCatFB: data.disableNonCatFB, userCategories: data.userCategories, categories: categoryMetaInfo, originalData: originalDataDictionary, loadingChall: false }) let categoryChall = this.props.match.params.categoryChall; const mongoID = /^[a-f\d]{24}$/i if (typeof categoryChall !== "undefined") { categoryChall = decodeURIComponent(categoryChall) if (mongoID.test(categoryChall)) { if (typeof categoryChall !== "undefined") { const challenge = categoryChall let foundChallenge = false loop1: for (let i = 0; i < originalData.length; i++) { for (let x = 0; x < originalData[i].challenges.length; x++) { if (originalData[i].challenges[x]._id === challenge) { foundChallenge = originalData[i].challenges[x] foundChallenge.category = originalData[i]._id break loop1; } } } if (foundChallenge) { this.setState({ currentCategory: foundChallenge.category, currentCategoryChallenges: [originalDataDictionary[foundChallenge.category]], foundChallenge: foundChallenge }) } else message.error("Challenge with ID '" + challenge + "' not found.") } } else { if (categoryChall in originalDataDictionary) this.setState({ currentCategory: categoryChall, currentCategoryChallenges: [originalDataDictionary[categoryChall]] }) else message.error("Category '" + categoryChall + "' not found.") } } } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) return true } sortCats(value) { this.tagSortRef.current.sortCats(value) } setStateAsync(state) { return new Promise((resolve) => { this.setState(state, resolve) }); } handleRefresh = async () => { await this.fetchCategories() await this.setStateAsync({ currentCategoryChallenges: [this.state.originalData[this.state.currentCategory]] }) await this.props.obtainScore() return true } render() { return ( <Layout className="layout-style"> <div id="Header" className="challenge-header-style"> <h1 style={{ color: "white", fontSize: "280%", letterSpacing: ".3rem", fontWeight: 300, marginBottom: "10px" }}> CHALLENGES </h1> {this.state.currentCategory && (<h4 className="category-header">{this.state.currentCategory}</h4>)} </div> <div style={{ display: "flex", justifyContent: "space-between", alignContent: "center", marginBottom: "10px", textAlign: "center" }}> <Button size="large" disabled={!this.state.currentCategory} icon={<LeftCircleOutlined />} style={{ backgroundColor: "#1f1f1f" }} onClick={() => { this.props.history.push("/Challenges"); this.setState({ currentCategory: false, foundChallenge: false }) }}>Back</Button> {this.state.currentCategory && this.state.countDownTimerStrings[this.state.currentCategory] && ( <h1 style={{ color: "white", fontSize: "170%", letterSpacing: ".3rem", fontWeight: 300, color: "#d89614", }}> {this.state.countDownTimerStrings[this.state.currentCategory]} </h1> )} <div> <Select disabled={!this.state.currentCategory} defaultValue="points" style={{ width: "20ch", backgroundColor: "#1f1f1f" }} onChange={this.sortCats.bind(this)}> <Option value="points">Sort by ↑Points</Option> <Option value="pointsrev">Sort by ↓Points</Option> <Option value="abc">Sort A→Z</Option> <Option value="abcrev">Sort Z→A</Option> </Select> </div> </div> <div> <Transition items={this.state.loadingChall} from={{ opacity: 0 }} enter={{ opacity: 1 }} leave={{ opacity: 0 }}> {(props, toggle) => { if (toggle === true) { return (<animated.div style={{ ...props, position: "absolute", left: "55%", transform: "translate(-55%, 0%)", zIndex: 10 }}> <Ellipsis color="#177ddc" size={120} ></Ellipsis> </animated.div>) } else { return (<animated.div style={{ ...props }}> {!this.state.currentCategory && ( <List grid={{ xs: 1, sm: 2, md: 2, lg: 3, xl: 3, xxl: 4, gutter: 20 }} dataSource={this.state.categories} locale={{ emptyText: ( <div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", marginTop: "10vh" }}> <FileUnknownTwoTone style={{ color: "#177ddc", fontSize: "400%", zIndex: 1 }} /> <h1 style={{ fontSize: "200%" }}>Oops, no challenges have been created.</h1> </div> ) }} renderItem={item => { return ( <List.Item key={item._id}> <Link to={"/Challenges/" + item._id}> <div onClick={() => { this.setState({ currentCategory: item._id, currentSolvedStatus: item.challenges, currentCategoryChallenges: [this.state.originalData[item._id]] }); }}> <Card hoverable type="inner" bordered={true} className="card-design hover" cover={<img style={{ overflow: "hidden" }} alt="Category Card" src={"/static/category/" + item._id + ".webp"} />} > <Meta title={ <div style={{ display: "flex", flexDirection: "column", textAlign: "center" }}> <div id="Title" style={{ display: "flex", color: "#f5f5f5", alignItems: "center", marginBottom: "2ch" }}> <h1 style={{ color: "white", fontSize: "2.5ch", width: "40ch", textOverflow: "ellipsis", overflow: "hidden" }}>{item._id}</h1> <div style={{ display: "flex", justifyContent: "center", alignItems: "center", flexDirection: "column" }}> <h2 style={{ fontSize: "2.5ch", marginLeft: "1vw", color: "#faad14", fontWeight: 700 }}>{item.challenges.solved}/{item.challenges.challenges}</h2> <Progress type="circle" percent={item.challenges.percentage} width="7ch" strokeColor={{ '0%': '#177ddc', '100%': '#49aa19', }} style={{ marginLeft: "1vw", fontSize: "2ch" }} /> </div> </div> <div> {"time" in item.meta && ( <span style={{ color: "#d89614", letterSpacing: ".1rem", fontWeight: 300, whiteSpace: "initial" }}>{this.state.countDownTimerStrings[item._id]}</span> )} {item.meta.visibility === false && ( <h4 style={{ color: "#d9d9d9" }}>Hidden Category <EyeInvisibleOutlined /></h4> )} </div> </div> } /> </Card> {/*Pass entire datasource as prop*/} </div> </Link> </ List.Item> ) } } /> )} {this.state.currentCategory && ( <ChallengesTagSort originalData={this.state.originalData} permissions={this.props.permissions} disableNonCatFB={this.state.disableNonCatFB} userCategories={this.state.userCategories} obtainScore={this.props.obtainScore.bind(this)} match={this.props.match} history={this.props.history} foundChallenge={this.state.foundChallenge} currentCategoryChallenges={this.state.currentCategoryChallenges} handleRefresh={this.handleRefresh.bind(this)} ref={this.tagSortRef} category={this.state.currentCategory}></ChallengesTagSort> )} </animated.div>) } } } </Transition> </div> </Layout> ); } } export default Challenges;