This commit is contained in:
masnn committed 2020-04-08 21:10:24 +08:00
1 parent 777a4bc927
commit 9c45527522
396 files changed
+13772

No files matched your search

@@ -0,0 +1,17 @@
.scratchpad__data-input
border: 0
border-left: 3px solid #E0E0E0
background: #F8F8F8
padding: rem(8px 4px)
font-family: $code-font-family
font-size: rem($font-size-secondary)
outline: 0
resize: none
color: #666
height: 100%
width: 100%
transition: background-color .2s linear, border-color .2s linear
&:focus
background: #FFF
border-color: lighten($primary-color, 70%)
@@ -0,0 +1,34 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function DataInputComponent(props) {
const {
title,
value,
onChange,
className,
...rest
} = props;
const cn = classNames(className, 'flex-col flex-fill');
return (
<div {...rest} className={cn}>
<textarea
className="scratchpad__data-input"
wrap="off"
value={value}
onChange={(ev) => {
ev.stopPropagation();
onChange(ev.target.value);
}}
placeholder={title}
/>
</div>
);
}
DataInputComponent.propTypes = {
title: PropTypes.node,
value: PropTypes.string,
onChange: PropTypes.func,
className: PropTypes.string,
};
+24
View File
@@ -0,0 +1,24 @@
.ReactCodeMirror
flex: 1
position: relative
overflow: hidden
.cm-s-vjcm
position: absolute
left: 0
top: 0
width: 100%
height: 100%
font-family: $code-font-family
font-size: rem($font-size-secondary)
line-height: 1.2
&.CodeMirror-focused .CodeMirror-selected
background: #cee4f2
.CodeMirror-gutters
border-right: 0
background: none
.CodeMirror-linenumber
color: #DDD
+8
View File
@@ -0,0 +1,8 @@
@import './var.inc.styl'
.scratchpad__panel-title
background: $primary-color
line-height: rem($panel-header-height)
padding: rem(0 5px)
font-size: rem($font-size-small)
color: #FFF
@@ -0,0 +1,15 @@
@import './var.inc.styl'
.scratchpad__panel-button
background: none
border: 0
line-height: rem($panel-header-height)
margin: 0
color: #FFF
padding: rem(0 5px)
cursor: pointer
font-size: rem($font-size-small)
outline: 0
&:hover
color: $tab-color
@@ -0,0 +1,19 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function PanelButtonComponent(props) {
const {
className,
children,
...rest
} = props;
const cn = classNames(className, 'scratchpad__panel-button');
return (
<button {...rest} className={cn}>{children}</button>
);
}
PanelButtonComponent.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
@@ -0,0 +1,26 @@
import React from 'react';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
export default function PanelComponent(props) {
const {
title,
className,
children,
...rest
} = props;
const cn = classNames(className, 'flex-col');
return (
<SplitPaneFillOverlay {...rest} className={cn}>
<div className="scratchpad__panel-title">{title}</div>
<div className="flex-col flex-fill">{children}</div>
</SplitPaneFillOverlay>
);
}
PanelComponent.propTypes = {
title: PropTypes.node,
className: PropTypes.string,
children: PropTypes.node,
};
@@ -0,0 +1,63 @@
@import './var.inc.styl'
.scratchpad__panel-tab
margin-top: rem(-($panel-header-height))
&.rc-tabs-top
border: 0
&.rc-tabs-top .rc-tabs-bar
border: 0
.rc-tabs-bar
padding-left: rem(120px)
.rc-tabs-ink-bar
background: $tab-color
.rc-tabs-tab-prev-icon, .rc-tabs-tab-next-icon
display: none
font-family: $icon-font-name !important
line-height: rem($panel-header-height)
font-size: rem($font-size)
color: #FFF
.rc-tabs-tab-arrow-show
display: block
&.rc-tabs-top .rc-tabs-tab-prev-icon:before
content: $icon-chevron_left
&.rc-tabs-top .rc-tabs-tab-next-icon:before
content: $icon-chevron_right
.rc-tabs-content
flex: 1
.rc-tabs-tabpane
display: flex
flex-direction: column
&.rc-tabs-top .rc-tabs-tab
padding: rem(0 5px)
line-height: rem($panel-header-height)
margin: 0
margin-right: rem(5px)
color: rgba(#FFF, 0.8)
&.rc-tabs-top .rc-tabs-tab-active
&, &:hover
color: #FFF
.rc-tabs-bar, .rc-tabs-nav-container
line-height: 1
font-size: rem($font-size-small)
.rc-tabs-tab-btn-disabled
color: rgba(#FFF, 0.7)
.rc-tabs-tabpane-hidden
display: none
.rc-tabs-tabpane
overflow: auto
@@ -0,0 +1,56 @@
import React from 'react';
import { connect } from 'react-redux';
import CodeMirror from 'react-codemirror';
import 'codemirror/addon/edit/matchbrackets';
import 'codemirror/mode/clike/clike';
import 'codemirror/mode/pascal/pascal';
import 'codemirror/mode/python/python';
import 'codemirror/mode/php/php';
import 'codemirror/mode/rust/rust';
import 'codemirror/mode/haskell/haskell';
import 'codemirror/mode/javascript/javascript';
import 'codemirror/mode/go/go';
import 'codemirror/mode/ruby/ruby';
import * as languageEnum from 'vj/constant/language';
const getOptions = lang => ({
lineNumbers: true,
tabSize: 4,
indentUnit: 4,
indentWithTabs: true,
mode: languageEnum.LANG_CODEMIRROR_MODES[lang],
});
const mapStateToProps = state => ({
code: state.editor.code,
lang: state.editor.lang,
});
const mapDispatchToProps = dispatch => ({
handleUpdateCode: (code) => {
dispatch({
type: 'SCRATCHPAD_EDITOR_UPDATE_CODE',
payload: code,
});
},
});
@connect(mapStateToProps, mapDispatchToProps)
export default class ScratchpadEditorContainer extends React.PureComponent {
componentDidMount() {
this.refs.editor.getCodeMirror().setOption('theme', 'vjcm');
}
render() {
return (
<CodeMirror
value={this.props.code}
onChange={code => this.props.handleUpdateCode(code)}
options={getOptions(this.props.lang)}
ref="editor"
/>
);
}
}
@@ -0,0 +1,101 @@
import React from 'react';
import { connect } from 'react-redux';
import Tabs, { TabPane } from 'rc-tabs';
import TabContent from 'rc-tabs/lib/TabContent';
import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
import i18n from 'vj/utils/i18n';
import Icon from 'vj/components/react/IconComponent';
import Panel from './PanelComponent';
import PanelButton from './PanelButtonComponent';
import ScratchpadPretestTabPane from './ScratchpadPretestTabPaneContainer';
const mapStateToProps = state => ({
current: state.pretest.current,
tabs: state.pretest.tabs,
meta: state.pretest.meta,
});
const mapDispatchToProps = dispatch => ({
handleClickAdd() {
dispatch({
type: 'SCRATCHPAD_PRETEST_ADD_DATA',
});
},
handleClickRemove() {
dispatch({
type: 'SCRATCHPAD_PRETEST_REMOVE_DATA',
});
},
handleSwitchData(id) {
dispatch({
type: 'SCRATCHPAD_PRETEST_SWITCH_TO_DATA',
payload: id,
});
},
handleClickClose() {
dispatch({
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
payload: {
uiElement: 'pretest',
visibility: false,
},
});
},
});
@connect(mapStateToProps, mapDispatchToProps)
export default class ScratchpadPretestContainer extends React.PureComponent {
render() {
return (
<Panel
title={(
<span>
<Icon name="edit" />
{' '}
{i18n('Pretest')}
</span>
)}
>
<Tabs
className="scratchpad__panel-tab flex-col flex-fill"
activeKey={this.props.current}
onChange={tabId => this.props.handleSwitchData(tabId)}
animation="slide-horizontal"
renderTabBar={() => (
<ScrollableInkTabBar
extraContent={(
<span>
<PanelButton
data-tooltip={i18n('Add new data')}
onClick={() => this.props.handleClickAdd()}
>
{i18n('Add')}
</PanelButton>
<PanelButton
data-tooltip={i18n('Remove this data')}
onClick={() => this.props.handleClickRemove()}
>
{i18n('Remove')}
</PanelButton>
<PanelButton
onClick={() => this.props.handleClickClose()}
>
<Icon name="close" />
</PanelButton>
</span>
)}
/>
)}
renderTabContent={() => <TabContent />}
>
{this.props.tabs.map(tabId => (
<TabPane tab={this.props.meta[tabId].title} key={tabId}>
<ScratchpadPretestTabPane id={tabId} />
</TabPane>
))}
</Tabs>
</Panel>
);
}
}
@@ -0,0 +1,49 @@
import React from 'react';
import { connect } from 'react-redux';
import i18n from 'vj/utils/i18n';
import DataInput from './DataInputComponent';
const mapStateToProps = state => ({
data: state.pretest.data,
});
const mapDispatchToProps = dispatch => ({
handleDataChange(id, type, value) {
dispatch({
type: 'SCRATCHPAD_PRETEST_DATA_CHANGE',
payload: {
id,
type,
value,
},
});
},
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...dispatchProps,
id: ownProps.id,
input: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].input : '',
output: stateProps.data[ownProps.id] ? stateProps.data[ownProps.id].output : '',
});
@connect(mapStateToProps, mapDispatchToProps, mergeProps)
export default class ScratchpadPretestTabPaneContainer extends React.PureComponent {
render() {
return (
<div className="flex-row flex-fill">
<DataInput
title={i18n('Sample Input')}
value={this.props.input}
onChange={v => this.props.handleDataChange(this.props.id, 'input', v)}
/>
<DataInput
title={i18n('Sample Output')}
value={this.props.output}
onChange={v => this.props.handleDataChange(this.props.id, 'output', v)}
/>
</div>
);
}
}
@@ -0,0 +1,85 @@
import React from 'react';
import { connect } from 'react-redux';
import Tabs, { TabPane } from 'rc-tabs';
import TabContent from 'rc-tabs/lib/TabContent';
import ScrollableInkTabBar from 'rc-tabs/lib/ScrollableInkTabBar';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import Icon from 'vj/components/react/IconComponent';
import Panel from './PanelComponent';
import PanelButton from './PanelButtonComponent';
import ScratchpadRecordsTable from './ScratchpadRecordsTableContainer';
const mapDispatchToProps = dispatch => ({
loadSubmissions() {
dispatch({
type: 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS',
payload: request.get(Context.getSubmissionsUrl),
});
},
handleClickClose() {
dispatch({
type: 'SCRATCHPAD_UI_SET_VISIBILITY',
payload: {
uiElement: 'records',
visibility: false,
},
});
},
handleClickRefresh() {
this.loadSubmissions();
},
});
@connect(null, mapDispatchToProps)
export default class ScratchpadRecordsContainer extends React.PureComponent {
componentDidMount() {
this.props.loadSubmissions();
}
render() {
return (
<Panel
title={(
<span>
<Icon name="flag" />
{' '}
{i18n('Records')}
</span>
)}
>
<Tabs
className="scratchpad__panel-tab flex-col flex-fill"
activeKey="all"
animation="slide-horizontal"
renderTabBar={() => (
<ScrollableInkTabBar
extraContent={(
<span>
<PanelButton
data-tooltip={i18n('Refresh Records')}
data-tooltip-pos="top right"
onClick={() => this.props.handleClickRefresh()}
>
{i18n('Refresh')}
</PanelButton>
<PanelButton
onClick={() => this.props.handleClickClose()}
>
<Icon name="close" />
</PanelButton>
</span>
)}
/>
)}
renderTabContent={() => <TabContent />}
>
<TabPane tab={<span>{i18n('All')}</span>} key="all">
<ScratchpadRecordsTable />
</TabPane>
</Tabs>
</Panel>
);
}
}
@@ -0,0 +1,92 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import TimeAgo from 'timeago-react';
import moment from 'moment';
import _ from 'lodash';
import substitute from 'vj/utils/substitute';
import emulateAnchorClick from 'vj/utils/emulateAnchorClick';
import { parse as parseMongoId } from 'vj/utils/mongoId';
import i18n from 'vj/utils/i18n';
import * as recordEnum from 'vj/constant/record';
const shouldShowDetail = data => recordEnum.STATUS_SCRATCHPAD_SHOW_DETAIL_FLAGS[data.status];
const isPretest = data => data.type === recordEnum.TYPE_PRETEST;
const getRecordDetail = (data) => {
if (!shouldShowDetail(data)) {
return (
<span className={`record-status--text ${recordEnum.STATUS_CODES[data.status]}`}>
{recordEnum.STATUS_TEXTS[data.status]}
</span>
);
}
const stat = _.pick(
_.groupBy(data.cases || [], 'status'),
_.keys(recordEnum.STATUS_SCRATCHPAD_SHORT_TEXTS),
);
return _.map(recordEnum.STATUS_SCRATCHPAD_SHORT_TEXTS, (text, status) => {
const count = (stat[status] && stat[status].length) || 0;
const cn = classNames('icol icol--stat', {
'record-status--text': count > 0,
[recordEnum.STATUS_CODES[data.status]]: count > 0,
});
return (
<span key={text} className={cn}>
{text}: {count}
</span>
);
});
};
const mapStateToProps = state => ({
items: state.records.items,
});
const mergeProps = (stateProps, dispatchProps, ownProps) => ({
...dispatchProps,
data: stateProps.items[ownProps.id],
});
@connect(mapStateToProps, null, mergeProps)
export default class ScratchpadRecordsRowContainer extends React.PureComponent {
handleRowClick(ev, id) {
const url = substitute(
decodeURIComponent(Context.getRecordDetailUrl),
{ rid: id }
);
emulateAnchorClick(ev, url, true);
}
render() {
const { data } = this.props;
const submitAt = parseMongoId(data._id).timestamp * 1000;
return (
<tr onClick={ev => this.handleRowClick(ev, data._id)}>
<td className={`col--detail record-status--border ${recordEnum.STATUS_CODES[data.status]}`}>
<span className={`icon record-status--icon ${recordEnum.STATUS_CODES[data.status]}`}></span>
<span className="icol icol--pretest">
{isPretest(data)
? <span className={`flag record-status--background ${recordEnum.STATUS_CODES[data.status]}`}>{i18n('Pretest')}</span>
: ''
}
</span>
{getRecordDetail(data)}
</td>
<td className="col--memory">
{shouldShowDetail(data) ? `${Math.ceil(data.memory_kb / 1000)} MB` : '-'}
</td>
<td className="col--time">
{shouldShowDetail(data) ? `${(data.time_ms / 1000).toFixed(1)}s` : '-'}
</td>
<td className="col--at">
<time data-tooltip={moment(submitAt).format('YYYY-MM-DD HH:mm:ss')}>
<TimeAgo datetime={submitAt} locale={i18n('timeago_locale')} />
</time>
</td>
</tr>
);
}
}
@@ -0,0 +1,33 @@
import React from 'react';
import classNames from 'classnames';
import { connect } from 'react-redux';
import ScratchpadRecordsRow from './ScratchpadRecordsRowContainer';
const mapStateToProps = state => ({
rows: state.records.rows,
isLoading: state.ui.records.isLoading,
});
@connect(mapStateToProps)
export default class ScratchpadRecordsTableContainer extends React.PureComponent {
render() {
const cn = classNames('data-table is--full-row scratchpad__records__table', {
loading: this.props.isLoading,
});
return (
<table className={cn}>
<colgroup>
<col className="col--detail" />
<col className="col--memory" />
<col className="col--time" />
<col className="col--at" />
</colgroup>
<tbody>
{this.props.rows.map(rowId => (
<ScratchpadRecordsRow key={rowId} id={rowId} />
))}
</tbody>
</table>
);
}
}
@@ -0,0 +1,148 @@
import _ from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import i18n from 'vj/utils/i18n';
import request from 'vj/utils/request';
import * as languageEnum from 'vj/constant/language';
import Icon from 'vj/components/react/IconComponent';
import Toolbar, {
ToolbarItemComponent as ToolbarItem,
ToolbarButtonComponent as ToolbarButton,
ToolbarSplitComponent as ToolbarSplit,
} from './ToolbarComponent';
function isTestCaseDataValid(data) {
return data.input.trim().length > 0 && data.output.trim().length > 0;
}
function isPretestValid(state) {
return _.some(state.tabs, id => isTestCaseDataValid(state.data[id]));
}
const mapStateToProps = state => ({
pretestVisible: state.ui.pretest.visible,
recordsVisible: state.ui.records.visible,
isPosting: state.ui.isPosting,
editorLang: state.editor.lang,
pretestValid: isPretestValid(state.pretest),
});
const mapDispatchToProps = dispatch => ({
togglePanel(uiElement) {
dispatch({
type: 'SCRATCHPAD_UI_TOGGLE_VISIBILITY',
payload: { uiElement },
});
},
setEditorLanguage(lang) {
dispatch({
type: 'SCRATCHPAD_EDITOR_SET_LANG',
payload: lang,
});
},
postPretest(context) {
const state = context.store.getState();
const { pretest } = state;
const testCases = pretest.tabs
.filter(tabId => isTestCaseDataValid(pretest.data[tabId]));
// const titles = testCases.map(tabId => pretest.meta[tabId].title);
const inputs = testCases.map(tabId => pretest.data[tabId].input);
const outputs = testCases.map(tabId => pretest.data[tabId].output);
const req = request.post(Context.postPretestUrl, {
lang: state.editor.lang,
code: state.editor.code,
data_input: inputs,
data_output: outputs,
});
dispatch({
type: 'SCRATCHPAD_POST_PRETEST',
payload: req,
});
},
postSubmit(context) {
const state = context.store.getState();
const req = request.post(Context.postSubmitUrl, {
lang: state.editor.lang,
code: state.editor.code,
});
dispatch({
type: 'SCRATCHPAD_POST_SUBMIT',
payload: req,
});
},
});
@connect(mapStateToProps, mapDispatchToProps)
export default class ScratchpadToolbarContainer extends React.PureComponent {
static contextTypes = {
store: PropTypes.object,
};
render() {
return (
<Toolbar>
<ToolbarButton
disabled={this.props.isPosting || !this.props.pretestValid}
className="scratchpad__toolbar__pretest"
onClick={() => this.props.postPretest(this.context)}
data-global-hotkey="f9"
data-tooltip={`${i18n('Pretest Your Code')} (F9)`}
>
<Icon name="debug" />
{' '}
{i18n('Run Pretest')}
{' '}
(F9)
</ToolbarButton>
<ToolbarButton
disabled={this.props.isPosting}
className="scratchpad__toolbar__submit"
onClick={() => this.props.postSubmit(this.context)}
data-global-hotkey="f10"
data-tooltip={`${i18n('Submit Your Code')} (F10)`}
>
<Icon name="play" />
{' '}
{i18n('Submit Solution')}
{' '}
(F10)
</ToolbarButton>
<ToolbarItem>
<select
className="select"
disabled={this.props.isPosting}
value={this.props.editorLang}
onChange={ev => this.props.setEditorLanguage(ev.target.value)}
>
{_.map(languageEnum.LANG_TEXTS, (val, key) => (
<option value={key} key={key}>{val}</option>
))}
</select>
</ToolbarItem>
<ToolbarSplit />
<ToolbarButton
activated={this.props.pretestVisible}
onClick={() => this.props.togglePanel('pretest')}
data-global-hotkey="alt+p"
data-tooltip={`${i18n('Toggle Pretest Panel')} (Alt+P)`}
>
<Icon name="edit" />
{' '}
{i18n('Pretest')}
</ToolbarButton>
<ToolbarButton
activated={this.props.recordsVisible}
onClick={() => this.props.togglePanel('records')}
data-global-hotkey="alt+r"
data-tooltip={`${i18n('Toggle Records Panel')} (Alt+R)`}
>
<Icon name="flag" />
{' '}
{i18n('Records')}
</ToolbarButton>
</Toolbar>
);
}
}
@@ -0,0 +1,53 @@
.scratchpad__toolbar
background: $toolbar-bg
padding: rem(8px)
font-size: rem($font-size-small)
font-weight: normal
.scratchpad__toolbar__item
margin-right: rem(5px)
.select
margin: 0
height: auto
line-height: 1
.scratchpad__toolbar__button
padding: rem(5px)
font-size: rem($font-size-small)
font-weight: normal
background: none
border: 1px solid transparent
color: $toolbar-fore
outline: 0
transition: .2s linear opacity
// Hack for Firefox bug #984869
&>div
display: flex
flex-direction: row
align-items: center
&.enabled
cursor: pointer
.icon
font-size: rem($font-size-icon)
margin-right: 2px
&.enabled:hover
color: $toolbar-fore-hover
background: $toolbar-bg-hover
&.activated
color: $toolbar-fore-selected !important
background: $toolbar-bg-selected !important
&.disabled
pointer-events: none
opacity: $toolbar-disabled-opacity
.scratchpad__toolbar__split
width: 1px
height: 1rem
background: #DDD
@@ -0,0 +1,90 @@
import PropTypes from 'prop-types';
import classNames from 'classnames';
export default function ToolbarComponent(props) {
const {
className,
children,
...rest
} = props;
const cn = classNames(className, 'scratchpad__toolbar flex-row flex-cross-center');
return (
<div {...rest} className={cn}>{children}</div>
);
}
ToolbarComponent.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
export function ToolbarButtonComponent(props) {
const {
activated,
disabled,
onClick,
className,
children,
...rest
} = props;
const cn = classNames(className, 'scratchpad__toolbar__item scratchpad__toolbar__button', {
activated,
disabled,
enabled: !disabled,
});
return (
<button
{...rest}
tabIndex="-1"
className={cn}
onClick={() => !disabled && onClick && onClick()}
>
<div>{children}</div>
</button>
);
}
ToolbarButtonComponent.propTypes = {
activated: PropTypes.bool,
disabled: PropTypes.bool,
onClick: PropTypes.func,
className: PropTypes.string,
children: PropTypes.node,
};
ToolbarButtonComponent.defaultProps = {
activated: false,
disabled: false,
};
export function ToolbarSplitComponent(props) {
const {
className,
...rest
} = props;
const cn = classNames(className, 'scratchpad__toolbar__item scratchpad__toolbar__split');
return (
<div {...rest} className={cn} />
);
}
ToolbarSplitComponent.propTypes = {
className: PropTypes.string,
};
export function ToolbarItemComponent(props) {
const {
className,
children,
...rest
} = props;
const cn = classNames(className, 'scratchpad__toolbar__item');
return (
<div {...rest} className={cn}>{children}</div>
);
}
ToolbarItemComponent.propTypes = {
className: PropTypes.string,
children: PropTypes.node,
};
+98
View File
@@ -0,0 +1,98 @@
import React from 'react';
import SplitPane from 'react-split-pane';
import { connect } from 'react-redux';
import _ from 'lodash';
import SplitPaneFillOverlay from 'vj/components/react-splitpane/SplitPaneFillOverlayComponent';
import Dom from 'vj/components/react/DomComponent';
import ScratchpadToolbar from './ScratchpadToolbarContainer';
import ScratchpadEditor from './ScratchpadEditorContainer';
import ScratchpadPretest from './ScratchpadPretestContainer';
import ScratchpadRecords from './ScratchpadRecordsContainer';
/*
a: React.Component
panes: [{
id: string,
element: React.Component,
props: {
visible: bool,
size: string|int,
},
}]
*/
function buildNestedPane([a, ...panes]) {
const elements = [
a,
...panes.filter(p => p.props.visible),
];
if (elements.length === 1) {
return a;
}
return elements
.reduce((prev, curr) => (
<SplitPane
split="horizontal"
primary="second"
defaultSize={curr.props.size}
key={curr.element.key}
onChange={curr.onChange}
>
{prev}
{curr.element}
</SplitPane>
));
}
const mapStateToProps = state => ({
ui: state.ui,
});
const mapDispatchToProps = dispatch => ({
changeUiSize: _.debounce((uiElement, size) => {
dispatch({
type: 'SCRATCHPAD_UI_CHANGE_SIZE',
payload: {
uiElement,
size,
},
});
}, 500),
});
@connect(mapStateToProps, mapDispatchToProps)
export default class ScratchpadContainer extends React.PureComponent {
handleChangeSize(uiElement, size) {
this.props.changeUiSize(uiElement, size);
$('#scratchpad').trigger('vjScratchpadRelayout');
}
render() {
return (
<SplitPane
defaultSize={this.props.ui.main.size}
minSize={250}
split="vertical"
primary="second"
onChange={size => this.handleChangeSize('main', size)}
>
<Dom className="scratchpad__problem" childDom={$('.problem-content').get(0)} />
{buildNestedPane([
<SplitPaneFillOverlay key="editor" className="flex-col">
<ScratchpadToolbar />
<ScratchpadEditor />
</SplitPaneFillOverlay>,
{
props: this.props.ui.pretest,
onChange: size => this.handleChangeSize('pretest', size),
element: <ScratchpadPretest key="pretest" />,
},
{
props: this.props.ui.records,
onChange: size => this.handleChangeSize('records', size),
element: <ScratchpadRecords key="records" />,
},
])}
</SplitPane>
);
}
}
@@ -0,0 +1,21 @@
export default function reducer(state = {
lang: Context.code_lang,
code: Context.code_template,
}, action) {
switch (action.type) {
case 'SCRATCHPAD_EDITOR_UPDATE_CODE': {
return {
...state,
code: action.payload,
};
}
case 'SCRATCHPAD_EDITOR_SET_LANG': {
return {
...state,
lang: action.payload,
};
}
default:
return state;
}
}
@@ -0,0 +1,14 @@
import { combineReducers } from 'redux';
import ui from './ui';
import editor from './editor';
import pretest from './pretest';
import records from './records';
const reducer = combineReducers({
ui,
editor,
pretest,
records,
});
export default reducer;
@@ -0,0 +1,103 @@
import uuid from 'uuid/v4';
import _ from 'lodash';
const initialId = uuid();
export default function reducer(state = {
counter: 1,
current: initialId,
tabs: [initialId],
meta: {
[initialId]: {
id: initialId,
title: '#1',
},
},
data: {
[initialId]: {
id: initialId,
title: '#1',
input: '',
output: '',
},
},
}, action) {
switch (action.type) {
case 'SCRATCHPAD_PRETEST_SWITCH_TO_DATA': {
const currentId = action.payload;
return {
...state,
current: currentId,
};
}
case 'SCRATCHPAD_PRETEST_ADD_DATA': {
const newCounter = state.counter + 1;
const newId = uuid();
return {
...state,
counter: newCounter,
current: newId,
tabs: [...state.tabs, newId],
meta: {
...state.meta,
[newId]: {
id: newId,
title: `#${newCounter}`,
},
},
data: {
...state.data,
[newId]: {
id: newId,
input: '',
output: '',
},
},
};
}
case 'SCRATCHPAD_PRETEST_REMOVE_DATA': {
const orgIdx = state.tabs.indexOf(state.current);
let newCounter = state.counter;
const newTabs = _.without(state.tabs, state.current);
const newMeta = _.omit(state.meta, state.current);
const newData = _.omit(state.data, state.current);
if (newTabs.length === 0) {
// keep at least one data
const id = uuid();
newTabs.push(id);
newMeta[id] = {
id,
title: `#${++newCounter}`,
};
newData[id] = {
input: '',
output: '',
};
}
const newIdx = (orgIdx < newTabs.length) ? orgIdx : orgIdx - 1;
return {
...state,
counter: newCounter,
current: newTabs[newIdx],
tabs: newTabs,
meta: newMeta,
data: newData,
};
}
case 'SCRATCHPAD_PRETEST_DATA_CHANGE': {
const { id, type, value } = action.payload;
return {
...state,
data: {
...state.data,
[id]: {
...state.data[id],
[type]: value,
},
},
};
}
default:
return state;
}
}
@@ -0,0 +1,36 @@
import _ from 'lodash';
export default function reducer(state = {
rows: [],
items: {},
}, action) {
switch (action.type) {
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
const { rdocs } = action.payload;
return {
...state,
rows: _.map(rdocs, '_id'),
items: _.keyBy(rdocs, '_id'),
};
}
case 'SCRATCHPAD_RECORDS_PUSH': {
const { rdoc } = action.payload;
if (rdoc.uid !== UserContext.uid
|| rdoc.domain_id !== UserContext.domain
|| rdoc.pid !== Context.problemId
) {
return state;
}
return {
...state,
rows: [rdoc._id, ...state.rows],
items: {
...state.items,
[rdoc._id]: rdoc,
},
};
}
default:
return state;
}
}
+102
View File
@@ -0,0 +1,102 @@
import Notification from 'vj/components/notification';
export default function reducer(state = {
main: {
size: '50%',
},
pretest: {
visible: false,
size: 200,
},
records: {
visible: true,
size: 200,
isLoading: false,
},
isPosting: false,
}, action) {
switch (action.type) {
case 'SCRATCHPAD_UI_CHANGE_SIZE': {
const { uiElement, size } = action.payload;
return {
...state,
[uiElement]: {
...state[uiElement],
size,
},
};
}
case 'SCRATCHPAD_UI_SET_VISIBILITY': {
const { uiElement, visibility } = action.payload;
return {
...state,
[uiElement]: {
...state[uiElement],
visible: visibility,
},
};
}
case 'SCRATCHPAD_UI_TOGGLE_VISIBILITY': {
const { uiElement } = action.payload;
return {
...state,
[uiElement]: {
...state[uiElement],
visible: !state[uiElement].visible,
},
};
}
case 'SCRATCHPAD_POST_PRETEST_PENDING':
case 'SCRATCHPAD_POST_SUBMIT_PENDING': {
return {
...state,
isPosting: true,
};
}
case 'SCRATCHPAD_POST_PRETEST_FULFILLED':
case 'SCRATCHPAD_POST_SUBMIT_FULFILLED': {
return {
...state,
isPosting: false,
};
}
case 'SCRATCHPAD_POST_PRETEST_REJECTED':
case 'SCRATCHPAD_POST_SUBMIT_REJECTED': {
Notification.error(action.payload.message);
return {
...state,
isPosting: false,
};
}
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_PENDING': {
return {
...state,
records: {
...state.records,
isLoading: true,
},
};
}
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_REJECTED': {
Notification.error(action.payload.message);
return {
...state,
records: {
...state.records,
isLoading: false,
},
};
}
case 'SCRATCHPAD_RECORDS_LOAD_SUBMISSIONS_FULFILLED': {
return {
...state,
records: {
...state.records,
isLoading: false,
},
};
}
default:
return state;
}
}
@@ -0,0 +1,68 @@
.scratchpad__sidebar
z-index: 9999
opacity: 0
transition: opacity .2s linear
mask-image: linear-gradient(to bottom, rgba(0,0,0,1), rgba(0,0,0,0) 50%)
&.visible
opacity: 0.3
&:hover
opacity: 1
mask-image: none
.scratchpad-container
position: absolute
background: #FFF
#scratchpad
position: absolute
left: 0
top: $nav-item-height
width: 100%
bottom: 0
.scratchpad__problem
position: absolute
left: 0
top: 0
width: 100%
height: 100%
overflow: auto
.scratchpad__toolbar__pretest.enabled:hover .icon
color: red
.scratchpad__toolbar__submit.enabled:hover .icon
color: green
.scratchpad__records__table
&.loading
// make a flash by setting opacity, otherwise users
// won't think that refreshing is completed!
opacity: 0.6
.col--at
width: rem(120px)
.col--time,
.col--memory
width: rem(70px)
.col--detail
border-right: 1px solid $table-border-color
.icol
display: inline-block
.icol--pretest
width: rem(60px)
.flag
font-size: rem(12px)
color: #FFF
padding: rem(2px 3px)
.icol--stat
color: #AAA
width: rem(60px)
+2
View File
@@ -0,0 +1,2 @@
$panel-header-height = 34px
$tab-color = #fff170