Compare commits
1 Commits
master
...
feat/add_g
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9610f0791f |
@@ -1,29 +1,38 @@
|
||||
/* istanbul ignore file */
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable import/extensions */
|
||||
/* eslint-disable import/no-unresolved */
|
||||
/**
|
||||
* This is an example component for an xblock Editor
|
||||
* It uses pre-existing components to handle the saving of a the result of a function into the xblock's data.
|
||||
* To use run npm run-script addXblock <your>
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import {
|
||||
Form,
|
||||
Spinner,
|
||||
Collapsible,
|
||||
Icon,
|
||||
IconButton,
|
||||
Dropdown,
|
||||
} from '@openedx/paragon';
|
||||
import {
|
||||
DeleteOutline,
|
||||
Add,
|
||||
ExpandMore,
|
||||
ExpandLess,
|
||||
InsertPhoto,
|
||||
MoreHoriz,
|
||||
Check,
|
||||
} from '@openedx/paragon/icons';
|
||||
import {
|
||||
actions,
|
||||
selectors,
|
||||
} from '../../data/redux';
|
||||
import {
|
||||
RequestKeys,
|
||||
} from '../../data/constants/requests';
|
||||
import './index.scss';
|
||||
import EditorContainer from '../EditorContainer';
|
||||
// This 'module' self-import hack enables mocking during tests.
|
||||
// See src/editors/decisions/0005-internal-editor-testability-decisions.md. The whole approach to how hooks are tested
|
||||
// should be re-thought and cleaned up to avoid this pattern.
|
||||
// eslint-disable-next-line import/no-self-import
|
||||
import * as module from '.';
|
||||
import { actions, selectors } from '../../data/redux';
|
||||
import { RequestKeys } from '../../data/constants/requests';
|
||||
import SettingsOption from '../ProblemEditor/components/EditProblemView/SettingsWidget/SettingsOption';
|
||||
import Button from '../../sharedComponents/Button';
|
||||
import DraggableList, { SortableItem } from '../../../generic/DraggableList';
|
||||
import messages from './messages';
|
||||
|
||||
export const hooks = {
|
||||
getContent: () => ({
|
||||
@@ -31,77 +40,498 @@ export const hooks = {
|
||||
}),
|
||||
};
|
||||
|
||||
export const ThumbEditor = ({
|
||||
export const GameEditor = ({
|
||||
onClose,
|
||||
// redux
|
||||
blockValue,
|
||||
lmsEndpointUrl,
|
||||
blockFailed,
|
||||
blockFinished,
|
||||
initializeEditor,
|
||||
// eslint-disable-next-line react/prop-types
|
||||
exampleValue,
|
||||
|
||||
// settings
|
||||
settings,
|
||||
shuffleTrue,
|
||||
shuffleFalse,
|
||||
timerTrue,
|
||||
timerFalse,
|
||||
type,
|
||||
updateType,
|
||||
|
||||
// list
|
||||
list,
|
||||
updateTerm,
|
||||
updateTermImage,
|
||||
updateDefinition,
|
||||
updateDefinitionImage,
|
||||
toggleOpen,
|
||||
setList,
|
||||
addCard,
|
||||
removeCard,
|
||||
|
||||
isDirty,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
// State for list
|
||||
const [state, setState] = React.useState(list);
|
||||
React.useEffect(() => { setState(list); }, [list]);
|
||||
|
||||
// Non-reducer functions go here
|
||||
const getDescriptionHeader = () => {
|
||||
// Function to determine what the header will say based on type
|
||||
switch (type) {
|
||||
case 'flashcards':
|
||||
return 'Flashcard terms';
|
||||
case 'matching':
|
||||
return 'Matching terms';
|
||||
default:
|
||||
return 'Undefined';
|
||||
}
|
||||
};
|
||||
|
||||
const getDescription = () => {
|
||||
// Function to determine what the description will say based on type
|
||||
switch (type) {
|
||||
case 'flashcards':
|
||||
return 'Enter your terms and definitions below. Learners will review each card by viewing the term, then flipping to reveal the definition.';
|
||||
case 'matching':
|
||||
return 'Enter your terms and definitions below. Learners must match each term with the correct definition.';
|
||||
default:
|
||||
return 'Undefined';
|
||||
}
|
||||
};
|
||||
|
||||
const saveTermImage = (index) => {
|
||||
const id = `term_image_upload|${index}`;
|
||||
const file = document.getElementById(id).files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
updateTermImage({ index, termImage: event.target.result });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const removeTermImage = (index) => {
|
||||
const id = `term_image_upload|${index}`;
|
||||
document.getElementById(id).value = '';
|
||||
updateTermImage({ index, termImage: '' });
|
||||
};
|
||||
|
||||
const saveDefinitionImage = (index) => {
|
||||
const id = `definition_image_upload|${index}`;
|
||||
const file = document.getElementById(id).files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (event) => {
|
||||
updateDefinitionImage({ index, definitionImage: event.target.result });
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
};
|
||||
|
||||
const removeDefintionImage = (index) => {
|
||||
const id = `definition_image_upload|${index}`;
|
||||
document.getElementById(id).value = '';
|
||||
updateDefinitionImage({ index, definitionImage: '' });
|
||||
};
|
||||
|
||||
const moveCardUp = (index) => {
|
||||
if (index === 0) { return; }
|
||||
const temp = state.slice();
|
||||
[temp[index], temp[index - 1]] = [temp[index - 1], temp[index]];
|
||||
setState(temp);
|
||||
};
|
||||
|
||||
const moveCardDown = (index) => {
|
||||
if (index === state.length - 1) { return; }
|
||||
const temp = state.slice();
|
||||
[temp[index + 1], temp[index]] = [temp[index], temp[index + 1]];
|
||||
setState(temp);
|
||||
};
|
||||
|
||||
const loading = (
|
||||
<div className="text-center p-6">
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
screenreadertext={intl.formatMessage(messages.loadingSpinner)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const termImageDiv = (card, index) => (
|
||||
<div className="card-image-area d-flex align-items-center align-self-stretch">
|
||||
<img className="card-image" src={card.term_image} alt="TERM_IMG" />
|
||||
<IconButton
|
||||
src={DeleteOutline}
|
||||
iconAs={Icon}
|
||||
alt="DEL_IMG"
|
||||
variant="primary"
|
||||
onClick={() => removeTermImage(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const termImageUploadButton = (card, index) => (
|
||||
<IconButton
|
||||
src={InsertPhoto}
|
||||
iconAs={Icon}
|
||||
alt="IMG"
|
||||
variant="primary"
|
||||
onClick={() => document.getElementById(`term_image_upload|${index}`).click()}
|
||||
/>
|
||||
);
|
||||
|
||||
const definitionImageDiv = (card, index) => (
|
||||
<div className="card-image-area d-flex align-items-center align-self-stretch">
|
||||
<img className="card-image" src={card.definition_image} alt="DEF_IMG" />
|
||||
<IconButton
|
||||
src={DeleteOutline}
|
||||
iconAs={Icon}
|
||||
alt="DEL_IMG"
|
||||
variant="primary"
|
||||
onClick={() => removeDefintionImage(index)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
const definitionImageUploadButton = (card, index) => (
|
||||
<IconButton
|
||||
src={InsertPhoto}
|
||||
iconAs={Icon}
|
||||
alt="IMG"
|
||||
variant="primary"
|
||||
onClick={() => document.getElementById(`definition_image_upload|${index}`).click()}
|
||||
/>
|
||||
);
|
||||
|
||||
const timerSettingsOption = (
|
||||
<SettingsOption
|
||||
className="sidebar-timer d-flex flex-column align-items-start align-self-stretch"
|
||||
title="Timer"
|
||||
summary={settings.timer ? 'On' : 'Off'}
|
||||
isCardCollapsibleOpen="true"
|
||||
>
|
||||
<>
|
||||
<div className="settings-description">Measure the time it takes learners to match all terms and definitions. Used to calculate a learner's score.</div>
|
||||
<Button
|
||||
onClick={() => timerFalse()}
|
||||
variant={!settings.timer ? 'primary' : 'outline-primary'}
|
||||
className="toggle-button rounded-0"
|
||||
>
|
||||
Off
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => timerTrue()}
|
||||
variant={settings.timer ? 'primary' : 'outline-primary'}
|
||||
className="toggle-button rounded-0"
|
||||
>
|
||||
On
|
||||
</Button>
|
||||
</>
|
||||
</SettingsOption>
|
||||
);
|
||||
|
||||
const page = (
|
||||
<div className="page-body d-flex align-items-start">
|
||||
<div className="terms d-flex flex-column align-items-start align-self-stretch">
|
||||
<div className="description d-flex flex-column align-items-start align-self-stretch">
|
||||
<div className="description-header">
|
||||
{getDescriptionHeader()}
|
||||
</div>
|
||||
<div className="description-body align-self-stretch">
|
||||
{getDescription()}
|
||||
</div>
|
||||
</div>
|
||||
<DraggableList
|
||||
className="d-flex flex-column align-items-start align-self-stretch"
|
||||
itemList={state}
|
||||
setState={setState}
|
||||
updateOrder={() => (newList) => setList(newList)}
|
||||
>
|
||||
{
|
||||
state.map((card, index) => (
|
||||
<SortableItem
|
||||
id={card.id}
|
||||
key={card.id}
|
||||
buttonClassName="draggable-button"
|
||||
componentStyle={{
|
||||
background: 'white',
|
||||
borderRadius: '6px',
|
||||
padding: '24px',
|
||||
marginBottom: '16px',
|
||||
boxShadow: '0px 1px 5px #ADADAD',
|
||||
position: 'relative',
|
||||
width: '100%',
|
||||
flexDirection: 'column',
|
||||
flexFlow: 'nowrap',
|
||||
}}
|
||||
>
|
||||
<Collapsible.Advanced
|
||||
className="card"
|
||||
defaultOpen
|
||||
onOpen={() => toggleOpen({ index, isOpen: true })}
|
||||
onClose={() => toggleOpen({ index, isOpen: false })}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
id={`term_image_upload|${index}`}
|
||||
hidden
|
||||
onChange={() => saveTermImage(index)}
|
||||
/>
|
||||
<input
|
||||
type="file"
|
||||
id={`definition_image_upload|${index}`}
|
||||
hidden
|
||||
onChange={() => saveDefinitionImage(index)}
|
||||
/>
|
||||
<Collapsible.Trigger className="card-heading">
|
||||
<div className="drag-spacer" />
|
||||
<div className="card-heading d-flex align-items-center align-self-stretch">
|
||||
<div className="card-number">{index + 1}</div>
|
||||
{!card.editorOpen ? (
|
||||
<div className="preview-block position-relative w-100">
|
||||
<span className="align-middle">
|
||||
<span className="preview-term">
|
||||
{type === 'flashcards' ? (
|
||||
<span className="d-inline-block align-middle pr-2">
|
||||
{card.term_image !== ''
|
||||
? <img className="img-preview" src={card.term_image} alt="TERM_IMG_PRV" />
|
||||
: <Icon className="img-preview" src={InsertPhoto} />}
|
||||
</span>
|
||||
)
|
||||
: ''}
|
||||
{card.term !== '' ? card.term : <span className="text-gray">No text</span>}
|
||||
</span>
|
||||
<span className="preview-definition">
|
||||
{type === 'flashcards' ? (
|
||||
<span className="d-inline-block align-middle pr-2">
|
||||
{card.definition_image !== ''
|
||||
? <img className="img-preview" src={card.definition_image} alt="DEF_IMG_PRV" />
|
||||
: <Icon className="img-preview" src={InsertPhoto} />}
|
||||
</span>
|
||||
)
|
||||
: ''}
|
||||
{card.definition !== '' ? card.definition : <span className="text-gray">No text</span>}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
: <div className="card-spacer d-flex align-self-stretch" />}
|
||||
<Dropdown onToggle={(isOpen, e) => e.stopPropagation()}>
|
||||
<Dropdown.Toggle
|
||||
className="card-dropdown"
|
||||
as={IconButton}
|
||||
src={MoreHoriz}
|
||||
iconAs={Icon}
|
||||
variant="primary"
|
||||
/>
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item onClick={() => moveCardUp(index)}>Move up</Dropdown.Item>
|
||||
<Dropdown.Item onClick={() => moveCardDown(index)}>Move down</Dropdown.Item>
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={() => removeCard({ index })}>Delete</Dropdown.Item>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<div>
|
||||
<IconButton
|
||||
src={ExpandMore}
|
||||
iconAs={Icon}
|
||||
alt="EXPAND"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<div>
|
||||
<IconButton
|
||||
src={ExpandLess}
|
||||
iconAs={Icon}
|
||||
alt="COLLAPSE"
|
||||
variant="primary"
|
||||
/>
|
||||
</div>
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<div className="card-body p-0">
|
||||
<Collapsible.Body>
|
||||
<div className="card-body-divider">
|
||||
<div className="card-divider" />
|
||||
</div>
|
||||
<div className="card-term d-flex flex-column align-items-start align-self-stretch">
|
||||
Term
|
||||
{(type !== 'matching' && card.term_image !== '') && termImageDiv(card, index)}
|
||||
<div className="card-input-line d-flex align-items-start align-self-stretch">
|
||||
<Form.Control
|
||||
className="d-flex flex-column align-items-start align-self-stretch"
|
||||
id={`term|${index}`}
|
||||
placeholder="Enter your term"
|
||||
value={card.term}
|
||||
onChange={(e) => updateTerm({ index, term: e.target.value })}
|
||||
/>
|
||||
{type !== 'matching' && termImageUploadButton(card, index)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="card-divider" />
|
||||
<div className="card-definition d-flex flex-column align-items-start align-self-stretch">
|
||||
Definition
|
||||
{(type !== 'matching' && card.definition_image !== '') && definitionImageDiv(card, index)}
|
||||
<div className="card-input-line d-flex align-items-start align-self-stretch">
|
||||
<Form.Control
|
||||
className="d-flex flex-column align-items-start align-self-stretch"
|
||||
id={`definition|${index}`}
|
||||
placeholder="Enter your definition"
|
||||
value={card.definition}
|
||||
onChange={(e) => updateDefinition({ index, definition: e.target.value })}
|
||||
/>
|
||||
{type !== 'matching' && definitionImageUploadButton(card, index)}
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</div>
|
||||
</Collapsible.Advanced>
|
||||
</SortableItem>
|
||||
))
|
||||
}
|
||||
</DraggableList>
|
||||
<Button
|
||||
className="add-button"
|
||||
onClick={() => addCard()}
|
||||
>
|
||||
<IconButton
|
||||
src={Add}
|
||||
iconAs={Icon}
|
||||
alt="ADD"
|
||||
variant="primary"
|
||||
/>
|
||||
Add
|
||||
</Button>
|
||||
</div>
|
||||
<div className="sidebar d-flex flex-column align-items-start flex-shrink-0">
|
||||
<SettingsOption
|
||||
className="sidebar-type d-flex flex-column align-items-start align-self-stretch"
|
||||
title="Type"
|
||||
summary={type.substr(0, 1).toUpperCase() + type.substr(1)}
|
||||
isCardCollapsibleOpen="true"
|
||||
>
|
||||
<Button
|
||||
onClick={() => updateType('flashcards')}
|
||||
className="type-button"
|
||||
>
|
||||
<span className="small text-primary-500">Flashcards</span>
|
||||
<span hidden={type !== 'flashcards'}><Icon src={Check} className="text-success" /></span>
|
||||
</Button>
|
||||
<div className="card-divider" />
|
||||
<Button
|
||||
onClick={() => updateType('matching')}
|
||||
className="type-button"
|
||||
>
|
||||
<span className="small text-primary-500">Matching</span>
|
||||
<span hidden={type !== 'matching'}><Icon src={Check} className="text-success" /></span>
|
||||
</Button>
|
||||
</SettingsOption>
|
||||
<SettingsOption
|
||||
className="sidebar-shuffle d-flex flex-column align-items-start align-self-stretch"
|
||||
title="Shuffle"
|
||||
summary={settings.shuffle ? 'On' : 'Off'}
|
||||
isCardCollapsibleOpen="true"
|
||||
>
|
||||
<>
|
||||
<div className="settings-description">Shuffle the order of terms shown to learners when reviewing.</div>
|
||||
<Button
|
||||
onClick={() => shuffleFalse()}
|
||||
variant={!settings.shuffle ? 'primary' : 'outline-primary'}
|
||||
className="toggle-button rounded-0"
|
||||
>
|
||||
Off
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => shuffleTrue()}
|
||||
variant={settings.shuffle ? 'primary' : 'outline-primary'}
|
||||
className="toggle-button rounded-0"
|
||||
>
|
||||
On
|
||||
</Button>
|
||||
</>
|
||||
</SettingsOption>
|
||||
{type === 'matching' && timerSettingsOption}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
// Page content goes here
|
||||
return (
|
||||
<EditorContainer
|
||||
getContent={module.hooks.getContent}
|
||||
getContent={hooks.getContent}
|
||||
onClose={onClose}
|
||||
isDirty={() => isDirty}
|
||||
>
|
||||
<div>
|
||||
{exampleValue}
|
||||
</div>
|
||||
<div className="editor-body h-75 overflow-auto">
|
||||
{!blockFinished
|
||||
? (
|
||||
<div className="text-center p-6">
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="m-3"
|
||||
// Use a messages.js file for intl messages.
|
||||
screenreadertext={intl.formatMessage('Loading Spinner')}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<p>
|
||||
Your Editor Goes here.
|
||||
You can get at the xblock data with the blockValue field.
|
||||
here is what is in your xblock: {JSON.stringify(blockValue)}
|
||||
</p>
|
||||
)}
|
||||
{!blockFinished ? loading : page}
|
||||
</div>
|
||||
</EditorContainer>
|
||||
);
|
||||
};
|
||||
ThumbEditor.defaultProps = {
|
||||
blockValue: null,
|
||||
lmsEndpointUrl: null,
|
||||
};
|
||||
ThumbEditor.propTypes = {
|
||||
|
||||
GameEditor.propTypes = {
|
||||
onClose: PropTypes.func.isRequired,
|
||||
|
||||
// redux
|
||||
blockValue: PropTypes.shape({
|
||||
data: PropTypes.shape({ data: PropTypes.string }),
|
||||
}),
|
||||
lmsEndpointUrl: PropTypes.string,
|
||||
blockFailed: PropTypes.bool.isRequired,
|
||||
blockFinished: PropTypes.bool.isRequired,
|
||||
initializeEditor: PropTypes.func.isRequired,
|
||||
list: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
|
||||
updateTerm: PropTypes.func.isRequired,
|
||||
updateTermImage: PropTypes.func.isRequired,
|
||||
updateDefinition: PropTypes.func.isRequired,
|
||||
updateDefinitionImage: PropTypes.func.isRequired,
|
||||
toggleOpen: PropTypes.func.isRequired,
|
||||
setList: PropTypes.func.isRequired,
|
||||
addCard: PropTypes.func.isRequired,
|
||||
removeCard: PropTypes.func.isRequired,
|
||||
settings: PropTypes.shape({
|
||||
shuffle: PropTypes.bool.isRequired,
|
||||
timer: PropTypes.bool.isRequired,
|
||||
}).isRequired,
|
||||
shuffleTrue: PropTypes.func.isRequired,
|
||||
shuffleFalse: PropTypes.func.isRequired,
|
||||
timerTrue: PropTypes.func.isRequired,
|
||||
timerFalse: PropTypes.func.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
updateType: PropTypes.func.isRequired,
|
||||
|
||||
isDirty: PropTypes.bool,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
blockValue: selectors.app.blockValue(state),
|
||||
lmsEndpointUrl: selectors.app.lmsEndpointUrl(state),
|
||||
blockFailed: selectors.requests.isFailed(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
blockFinished: selectors.requests.isFinished(state, { requestKey: RequestKeys.fetchBlock }),
|
||||
// TODO fill with redux state here if needed
|
||||
exampleValue: selectors.game.exampleValue(state),
|
||||
settings: selectors.game.settings(state),
|
||||
type: selectors.game.type(state),
|
||||
list: selectors.game.list(state),
|
||||
isDirty: selectors.game.isDirty(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {
|
||||
initializeEditor: actions.app.initializeEditor,
|
||||
// TODO fill with dispatches here if needed
|
||||
|
||||
// shuffle
|
||||
shuffleTrue: actions.game.shuffleTrue,
|
||||
shuffleFalse: actions.game.shuffleFalse,
|
||||
|
||||
// timer
|
||||
timerTrue: actions.game.timerTrue,
|
||||
timerFalse: actions.game.timerFalse,
|
||||
|
||||
// type
|
||||
updateType: actions.game.updateType,
|
||||
|
||||
// list
|
||||
updateTerm: actions.game.updateTerm,
|
||||
updateTermImage: actions.game.updateTermImage,
|
||||
updateDefinition: actions.game.updateDefinition,
|
||||
updateDefinitionImage: actions.game.updateDefinitionImage,
|
||||
toggleOpen: actions.game.toggleOpen,
|
||||
setList: actions.game.setList,
|
||||
addCard: actions.game.addCard,
|
||||
removeCard: actions.game.removeCard,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(ThumbEditor);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(GameEditor);
|
||||
|
||||
275
src/editors/containers/GameEditor/index.scss
Normal file
275
src/editors/containers/GameEditor/index.scss
Normal file
@@ -0,0 +1,275 @@
|
||||
/* Basic styles to support GameEditor layout and classes used in JSX */
|
||||
.editor-body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.page-body {
|
||||
gap: 24px;
|
||||
display: flex;
|
||||
padding: 8px 0 0 24px;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
background: var(--extras-white, #FFFFFF);
|
||||
}
|
||||
|
||||
.terms {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 0 0;
|
||||
gap: 16px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.terms > div {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 320px;
|
||||
display: flex;
|
||||
padding: 8px 24px 16px;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.description-header {
|
||||
color: var(--primary-500, #00262B);
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.draggable-button {
|
||||
cursor: grab;
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
}
|
||||
|
||||
.card-number {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 16px;
|
||||
background: #EEF1F5;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 12px;
|
||||
color: var(--primary-500, #00262B);
|
||||
font-size: 18px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.img-preview {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: cover;
|
||||
max-height: 32px;
|
||||
max-width: 32px;
|
||||
}
|
||||
|
||||
.card-image-area {
|
||||
display: flex;
|
||||
padding: 0 24px 8px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
max-height: 200px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-divider {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 1px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
align-self: stretch;
|
||||
background: var(--light-400, #EAE6E5);
|
||||
}
|
||||
|
||||
.add-button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.type-button {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
margin-right: 8px;
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
.preview-term {
|
||||
margin-right: 8px;
|
||||
display: inline-block;
|
||||
max-width: 45%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.preview-block {
|
||||
margin-right: 8px;
|
||||
bottom: 35%;
|
||||
}
|
||||
|
||||
.preview-definition {
|
||||
display: inline-block;
|
||||
max-width: 45%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
padding-left: 8px;
|
||||
padding-right: 8px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.description-body {
|
||||
align-self: stretch;
|
||||
color: var(--primary-500, #00262B);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
border-radius: 6px;
|
||||
border: var(--extras-white, #FFFFFF);
|
||||
background: var(--extras-white, #FFFFFF);
|
||||
}
|
||||
|
||||
.card-heading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 24px;
|
||||
align-self: stretch;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.card-spacer {
|
||||
flex: 1 0 0;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.card-delete-button, .card-image-button, .image-delete-button {
|
||||
display: flex;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 44px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-body-divider {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.card-term, .card-definition {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
align-self: stretch;
|
||||
gap: 16px;
|
||||
color: var(--primary-500, #00262B);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 28px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.card-image {
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.card-input-line {
|
||||
color: var(--gray-500, #707070);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.card-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
flex: 1 0 0;
|
||||
border: 1px solid var(--gray-500, #707070);
|
||||
background: #FFFFFF;
|
||||
padding: 10px 16px;
|
||||
gap: 10px;
|
||||
align-self: stretch;
|
||||
color: var(--gray-500, #707070);
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.sidebar-type, .sidebar-shuffle, .sidebar-timer {
|
||||
gap: 16px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--light-700, #D7D3D1);
|
||||
background: #FFFFFF;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.drag-spacer {
|
||||
width: 20px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.check {
|
||||
fill: green;
|
||||
}
|
||||
|
||||
.card-dropdown {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.settings-description {
|
||||
padding-bottom: 16px;
|
||||
color: #51565C;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
11
src/editors/containers/GameEditor/messages.ts
Normal file
11
src/editors/containers/GameEditor/messages.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messages = defineMessages({
|
||||
loadingSpinner: {
|
||||
id: 'GameEditor.loadingSpinner',
|
||||
defaultMessage: 'Loading Spinner',
|
||||
description: 'Loading message for spinner screenreader text.',
|
||||
},
|
||||
});
|
||||
|
||||
export default messages;
|
||||
@@ -6,5 +6,5 @@ export const blockTypes = StrictDict({
|
||||
problem: 'problem',
|
||||
// ADDED_EDITORS GO BELOW
|
||||
video_upload: 'video_upload',
|
||||
game: 'game',
|
||||
game: 'games',
|
||||
});
|
||||
|
||||
@@ -1,22 +1,136 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { StrictDict } from '../../../utils';
|
||||
|
||||
const generateId = () => `card-${Date.now()}-${Math.floor(Math.random() * 100000)}`;
|
||||
|
||||
const initialState = {
|
||||
settings: {},
|
||||
// TODO fill in with mock state
|
||||
exampleValue: 'this is an example value from the redux state',
|
||||
settings: {
|
||||
shuffle: false,
|
||||
timer: false,
|
||||
},
|
||||
type: 'flashcards',
|
||||
list: [
|
||||
{
|
||||
id: generateId(),
|
||||
term: '',
|
||||
term_image: '',
|
||||
definition: '',
|
||||
definition_image: '',
|
||||
editorOpen: true,
|
||||
},
|
||||
],
|
||||
isDirty: false,
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const game = createSlice({
|
||||
name: 'game',
|
||||
initialState,
|
||||
reducers: {
|
||||
updateField: (state, { payload }) => ({
|
||||
// settings
|
||||
shuffleTrue: (state) => ({
|
||||
...state,
|
||||
...payload,
|
||||
settings: {
|
||||
...state.settings,
|
||||
shuffle: true,
|
||||
},
|
||||
isDirty: true,
|
||||
}),
|
||||
shuffleFalse: (state) => ({
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
shuffle: false,
|
||||
},
|
||||
isDirty: true,
|
||||
}),
|
||||
timerTrue: (state) => ({
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
timer: true,
|
||||
},
|
||||
isDirty: true,
|
||||
}),
|
||||
timerFalse: (state) => ({
|
||||
...state,
|
||||
settings: {
|
||||
...state.settings,
|
||||
timer: false,
|
||||
},
|
||||
isDirty: true,
|
||||
}),
|
||||
// type
|
||||
updateType: (state, { payload }) => ({
|
||||
...state,
|
||||
type: payload,
|
||||
isDirty: true,
|
||||
}),
|
||||
// list operations
|
||||
updateTerm: (state, { payload }) => {
|
||||
const { index, term } = payload;
|
||||
if (!state.list[index]) { return state; }
|
||||
const newList = state.list.map((item, idx) => (idx === index ? { ...item, term } : item));
|
||||
return { ...state, list: newList, isDirty: true };
|
||||
},
|
||||
updateTermImage: (state, { payload }) => {
|
||||
const { index, termImage } = payload;
|
||||
if (!state.list[index]) { return state; }
|
||||
const newList = state.list.map((item, idx) => (idx === index ? { ...item, term_image: termImage } : item));
|
||||
return { ...state, list: newList, isDirty: true };
|
||||
},
|
||||
updateDefinition: (state, { payload }) => {
|
||||
const { index, definition } = payload;
|
||||
if (!state.list[index]) { return state; }
|
||||
const newList = state.list.map((item, idx) => (idx === index ? { ...item, definition } : item));
|
||||
return { ...state, list: newList, isDirty: true };
|
||||
},
|
||||
updateDefinitionImage: (state, { payload }) => {
|
||||
const { index, definitionImage } = payload;
|
||||
if (!state.list[index]) { return state; }
|
||||
const newList = state.list.map(
|
||||
(item, idx) => (idx === index ? { ...item, definition_image: definitionImage } : item),
|
||||
);
|
||||
return { ...state, list: newList, isDirty: true };
|
||||
},
|
||||
toggleOpen: (state, { payload }) => {
|
||||
const { index, isOpen } = payload;
|
||||
if (!state.list[index]) { return state; }
|
||||
const newList = state.list.map((item, idx) => (idx === index ? { ...item, editorOpen: !!isOpen } : item));
|
||||
return { ...state, list: newList, isDirty: true };
|
||||
},
|
||||
setList: (state, { payload }) => ({
|
||||
...state,
|
||||
list: payload,
|
||||
isDirty: true,
|
||||
}),
|
||||
addCard: (state) => ({
|
||||
...state,
|
||||
list: [
|
||||
...state.list,
|
||||
{
|
||||
id: generateId(),
|
||||
term: '',
|
||||
term_image: '',
|
||||
definition: '',
|
||||
definition_image: '',
|
||||
editorOpen: true,
|
||||
},
|
||||
],
|
||||
isDirty: true,
|
||||
}),
|
||||
removeCard: (state, { payload }) => {
|
||||
const { index } = payload;
|
||||
if (index < 0 || index >= state.list.length) { return state; }
|
||||
return {
|
||||
...state,
|
||||
list: state.list.filter((_, idx) => idx !== index),
|
||||
isDirty: true,
|
||||
};
|
||||
},
|
||||
setDirty: (state, { payload }) => ({
|
||||
...state,
|
||||
isDirty: payload,
|
||||
}),
|
||||
// TODO fill in reducers
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -8,8 +8,10 @@ import * as module from './selectors';
|
||||
export const gameState = (state) => state.game;
|
||||
const mkSimpleSelector = (cb) => createSelector([module.gameState], cb);
|
||||
export const simpleSelectors = {
|
||||
exampleValue: mkSimpleSelector(gameData => gameData.exampleValue),
|
||||
settings: mkSimpleSelector(gameData => gameData.settings),
|
||||
type: mkSimpleSelector(gameData => gameData.type),
|
||||
list: mkSimpleSelector(gameData => gameData.list),
|
||||
isDirty: mkSimpleSelector(gameData => gameData.isDirty),
|
||||
completeState: mkSimpleSelector(gameData => gameData),
|
||||
// TODO fill in with selectors as needed
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@ import TextEditor from './containers/TextEditor';
|
||||
import VideoEditor from './containers/VideoEditor';
|
||||
import ProblemEditor from './containers/ProblemEditor';
|
||||
import VideoUploadEditor from './containers/VideoUploadEditor';
|
||||
import GameEditor from './containers/GameEditor';
|
||||
import GamesEditor from './containers/GameEditor';
|
||||
|
||||
// ADDED_EDITOR_IMPORTS GO HERE
|
||||
|
||||
@@ -14,7 +14,7 @@ const supportedEditors = {
|
||||
[blockTypes.problem]: ProblemEditor,
|
||||
[blockTypes.video_upload]: VideoUploadEditor,
|
||||
// ADDED_EDITORS GO BELOW
|
||||
[blockTypes.game]: GameEditor,
|
||||
[blockTypes.game]: GamesEditor,
|
||||
} as const;
|
||||
|
||||
export default supportedEditors;
|
||||
|
||||
Reference in New Issue
Block a user