import React, { useState } from 'react'; import { Layout, Table, message, Button, Modal, Transfer, Divider, Input, Space, InputNumber, Card, Select, Form, Upload } from 'antd'; import DatePicker from './../Components/DatePicker'; import { Switch as AntdSwitch } from 'antd'; import dayjs from 'dayjs'; import { ExclamationCircleOutlined, DeleteOutlined, FlagOutlined, EditOutlined, FileUnknownTwoTone, EyeOutlined, EyeInvisibleOutlined, RedoOutlined, SearchOutlined, DownloadOutlined, UploadOutlined } from '@ant-design/icons'; import AdminChallengeCreate from "./adminChallengeCreate.js"; import AdminChallengeEdit from "./adminChallengeEdit.js"; import { Ellipsis } from 'react-spinners-css'; import { Switch, Route, Link } from 'react-router-dom'; const { Column } = Table; const { confirm } = Modal; const { Option } = Select; const { Dragger } = Upload; const { RangePicker } = DatePicker; const fileToBase64 = (file) => new Promise((resolve, reject) => { const reader = new FileReader(); reader.readAsDataURL(file); reader.onload = () => { const pos = reader.result.indexOf("base64,") resolve(reader.result.slice(pos + 7)) }; reader.onerror = error => reject(error); }) const EditCategoryForm = (props) => { const [form] = Form.useForm(); const [fileList, updateFileList] = useState([]) const [editLoading, updateEditLoading] = useState(false) const [time, updateTime] = useState([]) const [useDefaultPic, updateUseDefaultPic] = useState(false) if (form.getFieldValue("name") !== props.initialData.name) { let initialData = JSON.parse(JSON.stringify(props.initialData)) initialData.name = props.initialData.name if (props.initialData.time.length > 0) initialData.time = [dayjs(props.initialData.time[0]), dayjs(props.initialData.time[1])] form.setFieldsValue(initialData) } return ( <Form form={form} onFinish={async (values) => { updateEditLoading(true) let fileData = "" if (useDefaultPic) { fileData = "default" } else { if (fileList.length > 0) { // make a request to update category picture here try { fileData = await fileToBase64(fileList[0].originFileObj) } catch (e) { console.log(e) message.error({ content: "Oops. Unknown error" }) } } } await fetch(window.ipAddress + "/v1/challenge/edit/category", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ "name": props.initialData.name, "new_name": values.name, "categoryImage": fileData, "time": time // time is in UTC+0 }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { message.success({ content: "Edited category \"" + props.initialData.name + "\" successfully!" }) props.handleEditCategoryDone() } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) updateEditLoading(false) }} > <p><b><u>Editing:</u></b> <code>{props.initialData.name}</code></p> <h1>Category Name:</h1> <Form.Item name="name" rules={[{ required: true, message: 'Please enter a category name' }]} > <Input allowClear placeholder="Category name" /> </Form.Item> <h1>Category Cover Image:</h1> {!useDefaultPic ? ( <img src={"/static/category/" + props.initialData.name + ".webp"} /> ) : ( <img src={"/static/category/default.webp"} /> )} <Button danger type="primary" icon={<DeleteOutlined />} disabled={useDefaultPic} style={{ marginTop: "1ch", marginBottom: "5ch" }} onClick={() => { updateUseDefaultPic(true) }}>Reset To Default</Button> {!useDefaultPic && ( <Form.Item name="categoryImage" > <Dragger fileList={fileList} disabled={editLoading} accept=".png, .jpg, .jpeg, .webp" maxCount={1} onChange={(file) => { updateFileList(file.fileList) }} beforeUpload={(file) => { return false }}> <h4>Drag and drop an image file (.png, .jpeg, .webp etc.) to upload.</h4> </Dragger> </Form.Item> )} <h1>Category Competition Time</h1> <Form.Item name="time" > <RangePicker showTime={{ format: 'HH:mm' }} format="YYYY-MM-DD HH:mm" onChange={(date) => { if (date && date.length > 0) updateTime([date[0].toISOString(), date[1].toISOString()]) }} /> </Form.Item> <Button type="primary" htmlType="submit" style={{ marginBottom: "1.5vh" }} loading={editLoading}>Edit Category</Button> </Form> ); }; const UploadChallengesForm = (props) => { const [form] = Form.useForm(); const [fileList, updateFileList] = useState([]) const [editLoading, updateEditLoading] = useState(false) return ( <Form form={form} onFinish={async (values) => { updateEditLoading(true) let fileData = await fileList[0].originFileObj.text() try { JSON.parse(fileData) } catch (e) { console.error(e) message.error("Invalid json.") updateEditLoading(false) return false } await fetch(window.ipAddress + "/v1/challenge/upload", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ "challenges": fileData }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { message.success({ content: "Uploaded challenges successfully" }) props.closeUploadChallenges() props.handleRefresh() } else if (data.error === "failed-insert") { message.warn("Some challenges already exist and were not inserted. Please delete the challenges to insert from the backup.", 3) message.warn("The challenges are: [" + data.failedInsert.join(" ,") + "]", 5) } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) updateEditLoading(false) }} > <Form.Item name="challenges" > <Dragger fileList={fileList} disabled={editLoading} accept=".json" maxCount={1} onChange={(file) => { updateFileList(file.fileList) }} beforeUpload={(file) => { return false }}> <h4>Drag and drop a challenge backup file (.json) to upload challenge(s).</h4> </Dragger> </Form.Item> <span>Please note that hint purchases and solves are <b>not brought over</b> as this is equivalent to "creating a new challenge from a backup"</span> <Button icon={<UploadOutlined/>} type="primary" htmlType="submit" style={{ marginBottom: "1.5vh", marginTop: "3vh" }} loading={editLoading}>Upload Challenge(s)</Button> </Form> ); }; class AdminChallenges extends React.Component { constructor(props) { super(props); this.state = { loading: false, dataSource: [], id: "", modalLoading: false, challengeCreate: false, editChallenge: false, selectedKeys: [], selectedTableKeys: [], disableEditButtons: true, targetKeys: [], allCat: [], transferDisabled: false, refreshLoading: false, disableLoading: false, submissionDisabled: false, selectedRows: [], IDNameMapping: {}, maxSockets: 0, categoryMeta: {}, categoryOptions: [], currentEditCategory: false, categorySelect: "", uploadModalVisible: false } } componentDidUpdate(prevProps) { // Handle any page changes if (this.state.editChallenge && this.props.location.pathname !== "/Admin/Challenges/Edit") { this.setState({ editChallenge: false }) } else if (this.state.challengeCreate && this.props.location.pathname !== "/Admin/Challenges/Create") { this.setState({ challengeCreate: false }) } } componentDidMount = async () => { const location = this.props.location.pathname if (location === "/Admin/Challenges/Create") { await this.setState({ challengeCreate: true }) } else if (location === "/Admin/Challenges/Edit") { message.warn("Please select a challenge from the table to edit") await this.props.history.push("/Admin/Challenges") } this.handleRefresh() this.getDisableStates() } handleCategoryData = async () => { this.setState({ transferDisabled: true }) let invisible = [] let allCat = [] const categoryMeta = await fetch(window.ipAddress + "/v1/challenge/listCategoryInfo", { method: 'get', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) return data.categories else message.error({ content: "Oops. Unknown error" }) }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) // handle visibility manager let categoryOptions = [] for (const cat in categoryMeta) { allCat.push({ "key": cat }) categoryOptions.push((<Option value={cat}>{cat}</Option>)) if (categoryMeta[cat].visibility === false) invisible.push(cat) } this.setState({ targetKeys: invisible, allCat: allCat, transferDisabled: false, categoryMeta: categoryMeta, categoryOptions: categoryOptions }) } handleChange = (nextTargetKeys, direction, moveKeys) => { this.setState({ targetKeys: nextTargetKeys, transferDisabled: true }); if (direction === "right") { this.editCategoryVisibility(false, moveKeys) } else { this.editCategoryVisibility(true, moveKeys) } } handleSelectChange = (sourceSelectedKeys, targetSelectedKeys) => { this.setState({ selectedKeys: [...sourceSelectedKeys, ...targetSelectedKeys] }) } editCategoryVisibility(visibility, categories) { fetch(window.ipAddress + "/v1/challenge/edit/categoryVisibility", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ "visibility": visibility, "category": categories, }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { //console.log(data) if (data.success === true) { message.success("The visibility of (" + categories.join(", ") + ") categories has been updated") } else { message.error({ content: "Oops. Unknown error" }) } this.setState({ transferDisabled: false }) this.fillTableData() }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) } editChallengeVisibility = async (visibility, names, challenges) => { let challengeIDs = [] for (let i = 0; i < challenges.length; i++) { challengeIDs.push(challenges[i]._id) } this.setState({ disableEditButtons: true }) await fetch(window.ipAddress + "/v1/challenge/edit/visibility", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ "visibility": visibility, "challenges": challengeIDs, }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { //console.log(data) if (data.success === true) { message.success("The visibility of (" + names.join(", ") + ") challenge(s) have been updated") } else { message.error({ content: "Oops. Unknown error" }) } this.fillTableData() }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) this.setState({ disableEditButtons: false }) } fillTableData = async () => { this.setState({ loading: true }) await fetch(window.ipAddress + "/v1/challenge/list_all", { method: 'get', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { let IDNameMapping = {} for (var i = 0; i < data.challenges.length; i++) { data.challenges[i].key = data.challenges[i].name if (data.challenges[i].visibility === false) { data.challenges[i].visibility = <span visibility={data.challenges[i].visibility.toString()} style={{ color: "#d32029" }}>Hidden <EyeInvisibleOutlined /></span> } else { data.challenges[i].visibility = <span visibility={data.challenges[i].visibility.toString()} style={{ color: "#49aa19" }}>Visible <EyeOutlined /></span> } if (!data.challenges[i].dynamic) { data.challenges[i].minimum = "N/A" data.challenges[i].initial = "N/A" data.challenges[i].minSolves = "N/A" } IDNameMapping[data.challenges[i]._id] = data.challenges[i].name } this.setState({ dataSource: data.challenges, IDNameMapping: IDNameMapping, loading: false }) } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { message.error({ content: "Oops. There was an issue connecting with the server" }); }) } deleteChallenge = async (close, names, challenges) => { let challengeIDs = [] for (let i = 0; i < challenges.length; i++) { challengeIDs.push(challenges[i]._id) } this.setState({ disableEditButtons: true }) await fetch(window.ipAddress + "/v1/challenge/delete", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ "chall": challengeIDs, }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { //console.log(data) if (data.success === true) { message.success({ content: "Deleted challenges (" + names.join(", ") + ") successfully" }) this.handleRefresh() } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { console.log(error) message.error({ content: "Oops. There was an issue connecting with the server" }); }) this.setState({ selectedTableKeys: [] }) close() } handleBack = async () => { await this.props.history.push("/Admin/Challenges") if (this.props.location.pathname === "/Admin/Challenges") this.setState({ challengeCreate: false }) } handleEditBack = async () => { await this.props.history.push("/Admin/Challenges") if (this.props.location.pathname === "/Admin/Challenges") this.setState({ editChallenge: false }) } handleCreateBack() { this.props.history.push("/Admin/Challenges") this.setState({ challengeCreate: false }) this.handleRefresh() } handleEditChallBack() { this.props.history.push("/Admin/Challenges") this.setState({ editChallenge: false }) this.handleRefresh() } handleRefresh = async () => { this.setState({ categorySelect: "" }) await Promise.all([this.fillTableData(), this.handleCategoryData()]) } handleTableSelect = (selectedRowKeys, selectedRows) => { this.setState({ selectedTableKeys: selectedRowKeys, selectedRows: selectedRows }) if (this.state.disableEditButtons && selectedRowKeys.length > 0) this.setState({ disableEditButtons: false }) else if (!this.state.disableEditButtons && selectedRowKeys.length === 0) this.setState({ disableEditButtons: true }) } handleEditCategoryDone = () => { this.setState({ currentEditCategory: false }) this.handleRefresh() } disableSetting = async (setting, value) => { let settingName = "" if (setting === "submissionDisabled") { settingName = "Challenge submission" this.setState({ disableLoading: true }) } else if (setting === "disableNonCatFB") { settingName = "No Category First Bloods" this.setState({ disableLoading: true }) } await fetch(window.ipAddress + "/v1/adminSettings", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ disable: value, setting: setting }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { if (value) { message.success(settingName + " disabled") } else { message.success(settingName + " enabled") } if (setting === "submissionDisabled") this.setState({ submissionDisabled: value }) else if (setting === "disableNonCatFB") this.setState({ disableNonCatFB: value }) } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { message.error({ content: "Oops. There was an issue connecting with the server" }); }) this.setState({ disableLoading: false }) } getDisableStates = async () => { this.setState({ disableLoading: true }) await fetch(window.ipAddress + "/v1/challenge/disableStates", { method: 'get', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { //console.log(data) this.setState({disableNonCatFB: data.states.disableNonCatFB, submissionDisabled: data.states.submissionDisabled, maxSockets: data.states.maxSockets }) } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { message.error({ content: "Oops. There was an issue connecting with the server" }); }) this.setState({ disableLoading: false }) } changeSetting = async (setting, value) => { let settingName = "" if (setting === "maxSockets") { settingName = "Maximum number of sockets" this.setState({ uploadLoading: true }) } await fetch(window.ipAddress + "/v1/adminSettings", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ disable: value, setting: setting }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { if (setting === "maxSockets") message.success(settingName + " changed to " + value.toString()) } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { message.error({ content: "Oops. There was an issue connecting with the server" }); }) this.setState({ uploadLoading: false }) } downloadChallenges = async (challenges) => { let challengeIDs = [] let challengeNames = [] for (let i = 0; i < challenges.length; i++) { challengeIDs.push(challenges[i]._id) challengeNames.push(challenges[i].name) } await fetch(window.ipAddress + "/v1/challenge/download", { method: 'post', headers: { 'Content-Type': 'application/json', "Authorization": window.IRSCTFToken }, body: JSON.stringify({ challenges: challengeIDs }) }).then((results) => { return results.json(); //return data in JSON (since its JSON data) }).then((data) => { if (data.success === true) { const downloadData = "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify({ challenges: data.challenges })) const downloadAnchorNode = document.createElement('a'); const date = new Date() downloadAnchorNode.setAttribute("href", downloadData); downloadAnchorNode.setAttribute("download", date.toISOString() + "-ChallengesBackup.json"); document.body.appendChild(downloadAnchorNode); // required for firefox downloadAnchorNode.click(); downloadAnchorNode.remove(); message.success("Successfully downloaded the challenges [" + challengeNames.join(", ") + "]") } else { message.error({ content: "Oops. Unknown error" }) } }).catch((error) => { message.error({ content: "Oops. There was an issue connecting with the server" }); }) } openCategoryEditor = async (category) => { const catMeta = this.state.categoryMeta[category] this.setState({ currentEditCategory: { name: category, time: "time" in catMeta ? catMeta.time : [] }, categorySelect: category }) } render() { return ( <Layout style={{ height: "100%", width: "100%", backgroundColor: "rgba(0, 0, 0, 0)" }}> <Modal title={<span><UploadOutlined/> Upload Challenges</span>} visible={this.state.uploadModalVisible} footer={null} onCancel={() => {this.setState({uploadModalVisible: false})}}> <UploadChallengesForm handleRefresh={this.handleRefresh.bind(this)} closeUploadChallenges={() => {this.setState({uploadModalVisible: false})}}/> </Modal> <div style={{ display: (!this.state.challengeCreate && !this.state.editChallenge) ? "initial" : "none" }}> <div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}> <div style={{ display: "flex", alignItems: "center", height: "2ch" }}> <Button type="primary" style={{ marginBottom: "2vh", marginRight: "1ch" }} icon={<FlagOutlined />} onClick={() => { this.setState({ challengeCreate: true }, this.props.history.push("/Admin/Challenges/Create")) }}>Create New Challenge</Button> <Button type="primary" style={{ marginBottom: "2vh", marginRight: "1ch" }} icon={<UploadOutlined />} onClick={() => { this.setState({uploadModalVisible: true}) }}>Upload Challenges</Button> {this.state.loading && ( <div style={{ display: "flex", justifyContent: "center", alignItems: "center" }}> <Ellipsis color="#177ddc" size={60} /> <h1>Loading Challenges</h1> </div> )} </div> <Button loading={this.state.loading} type="primary" shape="circle" size="large" style={{ marginBottom: "2vh", maxWidth: "25ch" }} icon={<RedoOutlined />} onClick={async () => { await this.handleRefresh(); message.success("Challenge list refreshed.") }} /> </div> <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}> <div style={{ display: "flex", alignItems: "center" }}> <Button disabled={this.state.disableEditButtons} type="default" style={{ marginBottom: "2vh", marginRight: "1ch", backgroundColor: "#6e6e6e" }} icon={<EyeOutlined style={{ color: "#49aa19" }} />} onClick={() => { this.editChallengeVisibility(true, this.state.selectedTableKeys, this.state.selectedRows) }}>Show</Button> <Button disabled={this.state.disableEditButtons} type="default" style={{ marginBottom: "2vh", marginRight: "1ch", backgroundColor: "#6e6e6e" }} icon={<EyeInvisibleOutlined style={{ color: "#d32029" }} />} onClick={() => { this.editChallengeVisibility(false, this.state.selectedTableKeys, this.state.selectedRows) }}>Hide</Button> <Button disabled={this.state.disableEditButtons} style={{ marginBottom: "2vh", marginRight: "1ch", backgroundColor: "#a61d24" }} icon={<DeleteOutlined />} onClick={() => { confirm({ confirmLoading: this.state.disableEditButtons, title: 'Are you sure you want to delete the challenge(s) (' + this.state.selectedTableKeys.join(", ") + ')? This action is irreversible.', icon: <ExclamationCircleOutlined />, onOk: (close) => { this.deleteChallenge(close.bind(this), this.state.selectedTableKeys, this.state.selectedRows) }, onCancel: () => { }, }); }}>Delete Challenges</Button> </div> <div> <Button disabled={this.state.disableEditButtons} type="primary" style={{ marginBottom: "2vh", marginRight: "1ch" }} icon={<DownloadOutlined />} onClick={() => { this.downloadChallenges(this.state.selectedRows) }}>Download</Button> </div> </div> <Table rowSelection={{ selectedRowKeys: this.state.selectedTableKeys, onChange: this.handleTableSelect.bind(this) }} style={{ overflow: "auto" }} dataSource={this.state.dataSource} 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%" }}>No Challenges Found/Created</h1> </div> ) }}> <Column title="Name" dataIndex="name" key="name" render={(text, row, index) => { return <Link to={"/Challenges/" + row._id}><a style={{ fontWeight: 700 }}>{text}</a></Link>; }} filterDropdown={({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( <div style={{ padding: 8 }}> <Input autoFocus placeholder="Search Challenge Name" value={selectedKeys[0]} onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} onPressEnter={() => confirm()} style={{ marginBottom: 8, display: 'block' }} /> <Space> <Button type="primary" onClick={() => { confirm() }} icon={<SearchOutlined />} > Search </Button> <Button onClick={() => clearFilters()}> Reset </Button> </Space> </div> )} onFilter={(value, record) => record.name.toLowerCase().trim().includes(value.toLowerCase())} filterIcon={filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />} sorter={(a, b) => { if (a.name < b.name) return -1 else return 1 }} /> <Column filters={this.state.allCat.map(value => { return { text: value.key, value: value.key } })} onFilter={(value, record) => value === record.category} title="Category" dataIndex="category" key="category" render={(text, row, index) => { return <Link to={"/Challenges/" + row.category}><a style={{ fontWeight: 700 }}>{text}</a></Link>; }} /> <Column sorter={(a, b) => a.points - b.points} title="Points" dataIndex="points" key="points" /> <Column sorter={(a, b) => a.points - b.points} title="Initial Points" dataIndex="initial" key="initial" /> <Column sorter={(a, b) => a.points - b.points} title="Solves to Min." dataIndex="minSolves" key="minSolves" /> <Column sorter={(a, b) => a.points - b.points} title="Min. Points" dataIndex="minimum" key="minimum" /> <Column filters={[{ text: "Visible", value: "true" }, { text: "Hidden", value: "false" }]} onFilter={(value, record) => { return value === record.visibility.props.visibility }} title="Visbility" dataIndex="visibility" key="visibility" /> <Column title="Required Challenge" dataIndex="requires" key="requires" render={(text, row, index) => { return <Link to={"/Challenges/" + text}><a style={{ fontWeight: 700 }}>{this.state.IDNameMapping[text]}</a></Link>; }} filterDropdown={({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => ( <div style={{ padding: 8 }}> <Input autoFocus placeholder="Search Challenge Name" value={selectedKeys[0]} onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])} onPressEnter={() => confirm()} style={{ marginBottom: 8, display: 'block' }} /> <Space> <Button type="primary" onClick={() => { confirm() }} icon={<SearchOutlined />} > Search </Button> <Button onClick={() => clearFilters()}> Reset </Button> </Space> </div> )} onFilter={(value, record) => { if (record.requires) return this.state.IDNameMapping[record.requires].toLowerCase().includes(value) }} filterIcon={filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />} sorter={(a, b) => { if (!a.requires) return -1 if (!b.requires) return 1 if (this.state.IDNameMapping[a.requires] < this.state.IDNameMapping[b.requires]) return -1 else return 1 }} /> <Column title="" key="edit" render={(text, record) => ( <Button icon={<EditOutlined />} onClick={() => { this.setState({ editChallenge: true, id: record._id }); this.props.history.push("/Admin/Challenges/Edit") }}> Edit</Button> )} /> </Table> <Divider /> <div style={{ display: "flex", alignItems: "center" }}> <h1 style={{ fontSize: "150%" }}>Category Management </h1>{this.state.transferDisabled && (<Ellipsis color="#177ddc" size={50} />)} </div> <Card className="settings-card"> <h3>Category Meta Information Editor <EyeOutlined /></h3> <p>Select a category to edit info such as Name, Cover Pictures etc.</p> <Select style={{ width: "30ch" }} value={this.state.categorySelect} onChange={this.openCategoryEditor.bind(this)}> {this.state.categoryOptions} </Select> {this.state.currentEditCategory && ( <div style={{ padding: "10px", marginTop: "20px", backgroundColor: "rgba(0, 0, 0, 0.3)", border: "5px solid transparent", borderRadius: "10px" }}> <EditCategoryForm initialData={this.state.currentEditCategory} handleEditCategoryDone={this.handleEditCategoryDone.bind(this)} /> </div> )} </Card> <Card className="settings-card"> <h3>Category Visibility <EyeOutlined /></h3> <Transfer dataSource={this.state.allCat} titles={[<span style={{ color: "#49aa19" }}>Visible Categories <EyeOutlined /></span>, <span style={{ color: "#d32029" }} >Hidden Categories <EyeInvisibleOutlined /></span>]} targetKeys={this.state.targetKeys} selectedKeys={this.state.selectedKeys} onChange={this.handleChange} onSelectChange={this.handleSelectChange} render={item => item.key} pagination disabled={this.state.transferDisabled} /> </Card> <Divider /> <div className="settings-responsive2" style={{ display: "flex", justifyContent: "space-around" }}> <Card className="settings-card"> <h3>Disable Submissions: <AntdSwitch disabled={this.state.disableLoading} onClick={(value) => this.disableSetting("submissionDisabled", value)} checked={this.state.submissionDisabled} /></h3> <p>Prevents users from submitting any new submissions for all challenges. Hints can still be bought</p> </Card> <Divider type="vertical" style={{ height: "inherit" }} /> <Card className="settings-card"> <h3>Set Socket Limit: <InputNumber value={this.state.maxSockets} disabled={this.state.disableLoading} onChange={(value) => this.setState({ maxSockets: value })} onPressEnter={(e) => { this.changeSetting("maxSockets", this.state.maxSockets) }} /></h3> <p>Sets the maximum number of socket connections allowed <b>per account</b> to connect to the live scoreboard. <br /> <b>Press "Enter" to save</b></p> </Card> <Divider type="vertical" style={{ height: "inherit" }} /> <Card className="settings-card"> <h3>Disable First Blood for No Category: <AntdSwitch disabled={this.state.disableLoading} onClick={(value) => this.disableSetting("disableNonCatFB", value)} checked={this.state.disableNonCatFB} /></h3> <p>Prevents people with no categories from attaining first blood. Useful if you want to limit First Blood prizes to only eligible participants.</p> </Card> </div> </div> <Switch> <Route exact path='/Admin/Challenges/Create' render={(props) => <AdminChallengeCreate {...props} challenges={this.state.dataSource} handleBack={this.handleBack.bind(this)} handleCreateBack={this.handleCreateBack.bind(this)} allCat={this.state.allCat} />} /> <Route exact path='/Admin/Challenges/Edit' render={(props) => <AdminChallengeEdit {...props} allCat={this.state.allCat} IDNameMapping={this.state.IDNameMapping} challenges={this.state.dataSource} id={this.state.id} handleEditBack={this.handleEditBack.bind(this)} handleEditChallBack={this.handleEditChallBack.bind(this)} />} /> </Switch> </Layout> ); } } export default AdminChallenges;