Added ecstatica generator, file uploader and user tools
This commit is contained in:
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import styles from './SelectOption.module.css';
|
||||
|
||||
const SelectOption = props => {
|
||||
@@ -9,12 +8,4 @@ const SelectOption = props => {
|
||||
);
|
||||
};
|
||||
|
||||
SelectOption.defaultProps = {
|
||||
|
||||
};
|
||||
|
||||
SelectOption.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default SelectOption;
|
||||
@@ -1,4 +1,11 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Progress from './Progress';
|
||||
import Uploader from './Uploader';
|
||||
import Dropzone from './Dropzone';
|
||||
import StepProgressBar from './StepProgressBar';
|
||||
import UserTools from './UserTools';
|
||||
import ToolItem from './ToolItem';
|
||||
import CommandAndUserTools from './CommandAndUserTools';
|
||||
import ImageViewerInner from './ImageViewerInner';
|
||||
import ImageHeader from './ImageHeader';
|
||||
import Image from './Image';
|
||||
@@ -20,6 +27,13 @@ import PageLoader from './molecules/PageLoader';
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Progress,
|
||||
Uploader,
|
||||
Dropzone,
|
||||
StepProgressBar,
|
||||
UserTools,
|
||||
ToolItem,
|
||||
CommandAndUserTools,
|
||||
ImageViewerInner,
|
||||
ImageHeader,
|
||||
Image,
|
||||
|
||||
@@ -29,7 +29,7 @@ const Image = props => {
|
||||
|
||||
return (
|
||||
<Card className={`${classes.card} c-Image`} onClick={() => props.updateBackgroundDispatcher(image.imageUrl)}>
|
||||
<CardHeader avatar={<ImageHeader image={image} />} title={image.imageTitle} />
|
||||
<CardHeader avatar={<ImageHeader image={image} />} title={image.imageTitle} style={{textTransform: 'capitalize'}} />
|
||||
<CardActionArea>
|
||||
<CardMedia
|
||||
className={classes.media}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
.c-ImageContainer {
|
||||
|
||||
.c-Images {
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,12 +24,12 @@ class ImageContainer extends React.Component {
|
||||
new Image('dragonfly3', '/images/GUD_2.JPG', 'sampleDesc3'),
|
||||
new Image('dragonfly4', '/images/GUD_1.JPG', 'sampleDesc4')
|
||||
]
|
||||
const imageRenders = images.map((image) => {
|
||||
return <ImageCard image={image}/>
|
||||
const imageRenders = images.map((image, index) => {
|
||||
return <ImageCard image={image} key={index} />
|
||||
})
|
||||
return (
|
||||
<div class="container-fluid">
|
||||
<div class="row" style={{justifyContent: 'space-between'}}>
|
||||
<div className="container-fluid c-ImageContainer">
|
||||
<div className="row c-Images">
|
||||
{imageRenders}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import React, {useState} from 'react';
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import { createPropsSelector } from 'reselect-immutable-helpers';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import Modal from '@material-ui/core/Modal';
|
||||
import Backdrop from '@material-ui/core/Backdrop';
|
||||
import {Fade, Zoom, Paper} from '@material-ui/core';
|
||||
import {Zoom, Paper} from '@material-ui/core';
|
||||
import { updateModalState } from '../../../../pages/Home/actions';
|
||||
import { getModalState, getBackgroundImage } from '../../../../pages/Home/selectors';
|
||||
import ImageViewerInner from './ImageViewerInner/ImageViewerInner';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
modal: {
|
||||
@@ -59,7 +58,7 @@ const ImageViewer = props => {
|
||||
};
|
||||
|
||||
ImageViewer.propTypes = {
|
||||
handleClose: PropTypes.func,
|
||||
handleCloseDispatcher: PropTypes.func,
|
||||
modalState: PropTypes.bool,
|
||||
selectedImage: PropTypes.string
|
||||
};
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ImageContainer from './ImageContainer';
|
||||
import ImageViewer from './ImageViewer';
|
||||
|
||||
@@ -12,8 +11,4 @@ const PrincipalContent = props => {
|
||||
);
|
||||
};
|
||||
|
||||
PrincipalContent.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default PrincipalContent;
|
||||
@@ -13,7 +13,7 @@ const SideBar = props => {
|
||||
});
|
||||
$('*').on('click', (e) => {
|
||||
// e.stopPropagation();
|
||||
console.log($(e.target).closest('#sidebar'))
|
||||
// console.log($(e.target).closest('#sidebar'))
|
||||
if($(e.target).closest('#sidebar').length === 0) {
|
||||
$('#sidebar').css('transform', 'translateX(0px)');
|
||||
$('#sidebar').css('box-shadow', '');
|
||||
@@ -23,18 +23,19 @@ const SideBar = props => {
|
||||
})
|
||||
|
||||
const toggleSidebar = () => {
|
||||
$('#sidebar').toggleClass('active');
|
||||
$('.hideable').toggleClass('hide');
|
||||
const position = $('#sidebar').css('transform').split(/[()]/)[1]
|
||||
const currentX = +position.split(',')[4]
|
||||
console.log(currentX)
|
||||
console.log($('#sidebar').css('transform'))
|
||||
console.log($('#sidebar').css('transform').split(/[()]/))
|
||||
if (currentX && currentX === 255) {
|
||||
$('#sidebar').css('transform', 'translateX(85px)');
|
||||
} else if (currentX && currentX === 85) {
|
||||
$('#sidebar').css('transform', 'translateX(255px)');
|
||||
}
|
||||
$('#sidebar').toggleClass('active');
|
||||
$('.hideable').toggleClass('hide');
|
||||
// console.log(currentX)
|
||||
// console.log($('#sidebar').css('transform'))
|
||||
// console.log($('#sidebar').css('transform').split(/[()]/))
|
||||
$('.triangle').addClass('animation');
|
||||
}
|
||||
|
||||
const tabsData = [
|
||||
|
||||
@@ -26,7 +26,7 @@ class SocialLogin extends React.Component{
|
||||
|
||||
onSuccess(googleUser) {
|
||||
console.log('Logged in as: ' + googleUser.getBasicProfile().getName());
|
||||
// this.props.history.push('/home')
|
||||
this.props.history.push('/home')
|
||||
}
|
||||
|
||||
onFailure(error) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.c-CommandAndUserTools {
|
||||
.c-commandTools {
|
||||
border-right: 1px solid $theme-supplementer
|
||||
}
|
||||
|
||||
.c-accountTools {
|
||||
right: 2%;
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
import React from 'react';
|
||||
import $ from 'jquery'
|
||||
import PropTypes from 'prop-types';
|
||||
import ToolItem from './ToolItem/ToolItem';
|
||||
import UserTools from './UserTools/UserTools';
|
||||
|
||||
class CommandAndUserTools extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.registerSpeechHandlers = this.registerSpeechHandlers.bind(this)
|
||||
this.startSpeechRecognition = this.startSpeechRecognition.bind(this)
|
||||
this.state = {
|
||||
commandToolItemsData: [
|
||||
{
|
||||
id: 'btnFavorites',
|
||||
icon: 'star_border',
|
||||
iconHovered: 'star',
|
||||
clickHandler: this.props.save
|
||||
},
|
||||
{
|
||||
id: 'btnClearCommand',
|
||||
icon: 'delete_outline',
|
||||
iconHovered: 'delete',
|
||||
clickHandler: this.props.reset
|
||||
},
|
||||
{
|
||||
id: 'btnFireCommand',
|
||||
icon: 'send',
|
||||
iconHovered: 'send',
|
||||
clickHandler: this.props.execute
|
||||
},
|
||||
{
|
||||
id: 'btnSTTCommand',
|
||||
icon: 'mic_none',
|
||||
iconHovered: 'mic',
|
||||
clickHandler: this.startSpeechRecognition
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.registerSpeechHandlers();
|
||||
}
|
||||
|
||||
startSpeechRecognition() {
|
||||
var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
if(isChrome) {
|
||||
this.recognition.start();
|
||||
} else {
|
||||
// this.domOpsService.sho
|
||||
}
|
||||
}
|
||||
|
||||
registerSpeechHandlers() {
|
||||
let SpeechRecognition
|
||||
let instructions = $('#command')
|
||||
try {
|
||||
SpeechRecognition = window.SpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition || window.webkitSpeechRecognition
|
||||
// SpeechRecognition = ''
|
||||
this.recognition = new SpeechRecognition()
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
$('.no-browser-support').show();
|
||||
$('.app').hide();
|
||||
}
|
||||
this.recognition.onstart = function() {
|
||||
instructions.val('Voice recognition activated. Try speaking into the microphone.');
|
||||
}
|
||||
|
||||
this.recognition.onspeechend = function() {
|
||||
instructions.val('You were quiet for a while so voice recognition turned itself off.');
|
||||
}
|
||||
|
||||
this.recognition.onerror = function(event) {
|
||||
if(event.error === 'no-speech') {
|
||||
instructions.val('No speech was detected. Try again.');
|
||||
};
|
||||
}
|
||||
|
||||
this.recognition.onresult = function(event) {
|
||||
let noteContent = "";
|
||||
let current = event.resultIndex;
|
||||
let transcript = event.results[current][0].transcript;
|
||||
let mobileRepeatBug = (current === 1 && transcript === event.results[0][0].transcript);
|
||||
if(!mobileRepeatBug) {
|
||||
noteContent += transcript;
|
||||
$('#command').val(noteContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// processRecastResponse(recastResponse: any) {
|
||||
// let bodyRelevant: any;
|
||||
// let intent: any = "";
|
||||
// let assuredIntentFactor = 0.6;
|
||||
// recastResponse.then((body) => {
|
||||
// bodyRelevant = body.results;
|
||||
// let intents = bodyRelevant ? bodyRelevant.intents : "";
|
||||
// if(intents) {
|
||||
// if(intents.length == 1) {
|
||||
// intent = intents[0];
|
||||
// } else if(intents.length > 1) {
|
||||
// let reducer = (probableIntent, currIntent) => probableIntent.confidence >= currIntent.confidence ? probableIntent : currIntent;
|
||||
// intent = intents.reduce(reducer, intents[0]);
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("Intent is either not Identified or is not supported, please try again with a different text.");
|
||||
// return;
|
||||
// }
|
||||
// if (intent && intent.confidence > assuredIntentFactor) {
|
||||
// let intentSlug = intent.slug;
|
||||
// if (!Object.keys($config.intentSlugToOperations).includes(intentSlug)) {
|
||||
// this.domOpsService.showEmptyCommandMessage("Intent is either not Identified or is not supported, please try again with a different text.");
|
||||
// return;
|
||||
// }
|
||||
// // $(`#${$config.constants.hiddenIntentFieldId}`).val(intent);
|
||||
// window.localStorage.setItem($config.constants.hiddenIntentFieldId, intentSlug);
|
||||
// this.domOpsService.displayIntentBox(intentSlug);
|
||||
// if (intentSlug == "resethistory") {
|
||||
// let card = this.cardsService.getResponseCard($config.intentSlugToOperations.resethistory.cardTitle,
|
||||
// $config.intentSlugToOperations.resethistory.cardMsg, {}, "response");
|
||||
// card.insertionCounter = 0;
|
||||
// let resetHistoryResponseAction = new ClearHistory(card);
|
||||
// this.store.dispatch(resetHistoryResponseAction);
|
||||
// return;
|
||||
// }
|
||||
// this.domOpsService.widgetIdentified.emit({widget: intentSlug, bodyRelevant: bodyRelevant});
|
||||
// this.domOpsService.populateRecastData(intentSlug, bodyRelevant);
|
||||
// // store.dispatch($config.intentSlugToOperations.addquery.action);
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("Couldn't conform with the required confidence level of the intent match, please try again.");
|
||||
// }
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("There seems to be an error in the Recast Response, as intents are not returned.");
|
||||
// return;
|
||||
// }
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
render() {
|
||||
const {commandToolItemsData} = this.state
|
||||
|
||||
const commandToolItems = commandToolItemsData && commandToolItemsData.map((toolData, index) => {
|
||||
return <ToolItem toolData={toolData} key={index} />
|
||||
})
|
||||
return (
|
||||
<div className="collapse navbar-collapse c-CommandAndUserTools" id="navbarSupportedContent">
|
||||
<ul className="nav navbar-nav ml-auto c-commandTools">
|
||||
{commandToolItems}
|
||||
</ul>
|
||||
<UserTools />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommandAndUserTools.propTypes = {
|
||||
execute: PropTypes.func,
|
||||
reset: PropTypes.func,
|
||||
save: PropTypes.func
|
||||
};
|
||||
|
||||
export default CommandAndUserTools;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import CommandAndUserTools from './CommandAndUserTools';
|
||||
|
||||
describe('CommandAndUserTools', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
.c-ToolItem {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import $ from 'jquery'
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
const ToolItem = ({toolData}) => {
|
||||
|
||||
const [icon, setIcon] = useState(toolData.icon)
|
||||
|
||||
const toggleIcon = (event, toolData) => {
|
||||
const iconTag = $(event.target).find('i').addBack('i')
|
||||
const currentIcon = iconTag && iconTag[0] && iconTag[0].innerHTML
|
||||
if (currentIcon === toolData.icon) {
|
||||
setIcon(toolData.iconHovered)
|
||||
} else {
|
||||
setIcon(toolData.icon)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<li id={toolData.id} className="nav-item" onClick={toolData.clickHandler}>
|
||||
<Link to="#" className="nav-link">
|
||||
<i
|
||||
className="material-icons md-light md-36"
|
||||
onMouseEnter={(event) => toggleIcon(event, toolData)}
|
||||
onMouseLeave={(event) => toggleIcon(event, toolData)}>
|
||||
{icon}
|
||||
</i>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
ToolItem.propTypes = {
|
||||
toolData: PropTypes.object
|
||||
};
|
||||
|
||||
export default ToolItem;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import ToolItem from './ToolItem';
|
||||
|
||||
describe('ToolItem', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import ToolItem from './ToolItem';
|
||||
|
||||
export default ToolItem;
|
||||
@@ -0,0 +1,3 @@
|
||||
.c-UserTools {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
import React from 'react';
|
||||
import {connect} from 'react-redux'
|
||||
import {makeStyles} from '@material-ui/core/styles';
|
||||
import {IconButton, Badge, MenuItem, Menu} from '@material-ui/core';
|
||||
import AccountCircle from '@material-ui/icons/AccountCircle';
|
||||
import MailIcon from '@material-ui/icons/Mail';
|
||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||
import MoreIcon from '@material-ui/icons/MoreVert';
|
||||
import {updateUploadModalState} from './../../../../../../pages/Home/actions'
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
grow: {
|
||||
flexGrow: 1,
|
||||
},
|
||||
menuButton: {
|
||||
marginRight: theme.spacing(2),
|
||||
},
|
||||
sectionDesktop: {
|
||||
display: 'none',
|
||||
float: 'right',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'flex',
|
||||
},
|
||||
},
|
||||
sectionMobile: {
|
||||
display: 'flex',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
const UserTools = props => {
|
||||
|
||||
const classes = useStyles();
|
||||
const [anchorEl, setAnchorEl] = React.useState(null);
|
||||
const [mobileMoreAnchorEl, setMobileMoreAnchorEl] = React.useState(null);
|
||||
|
||||
const isMenuOpen = Boolean(anchorEl);
|
||||
const isMobileMenuOpen = Boolean(mobileMoreAnchorEl);
|
||||
|
||||
const handleProfileMenuOpen = (event) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleMobileMenuClose = () => {
|
||||
setMobileMoreAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleMenuClose = (event, uploaderModalStateDispatcher) => {
|
||||
setAnchorEl(null);
|
||||
handleMobileMenuClose();
|
||||
if (event.currentTarget.innerText === 'Upload Image/s') {
|
||||
uploaderModalStateDispatcher(true)
|
||||
}
|
||||
};
|
||||
|
||||
const handleMobileMenuOpen = event => {
|
||||
setMobileMoreAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const mobileMenuId = 'primary-search-account-menu-mobile';
|
||||
const menuId = 'primary-search-account-menu';
|
||||
const renderMenu = (
|
||||
<Menu
|
||||
anchorEl={anchorEl}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
id={menuId}
|
||||
keepMounted
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={isMenuOpen}
|
||||
onClose={handleMenuClose}
|
||||
>
|
||||
<MenuItem onClick={handleMenuClose}>Profile</MenuItem>
|
||||
<MenuItem onClick={handleMenuClose}>My account</MenuItem>
|
||||
<MenuItem onClick={(event) => handleMenuClose(event, props.uploaderModalStateDispatcher)}>Upload Image/s</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
const renderMobileMenu = (
|
||||
<Menu
|
||||
anchorEl={mobileMoreAnchorEl}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
id={mobileMenuId}
|
||||
keepMounted
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
open={isMobileMenuOpen}
|
||||
onClose={handleMobileMenuClose}
|
||||
>
|
||||
<MenuItem>
|
||||
<IconButton aria-label="show 4 new mails" color="inherit">
|
||||
<Badge badgeContent={4} color="secondary">
|
||||
<MailIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<p>Messages</p>
|
||||
</MenuItem>
|
||||
<MenuItem>
|
||||
<IconButton aria-label="show 11 new notifications" color="inherit">
|
||||
<Badge badgeContent={11} color="secondary">
|
||||
<NotificationsIcon />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<p>Notifications</p>
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleProfileMenuOpen}>
|
||||
<IconButton
|
||||
aria-label="account of current user"
|
||||
aria-controls="primary-search-account-menu"
|
||||
aria-haspopup="true"
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle />
|
||||
</IconButton>
|
||||
<p>Profile</p>
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={classes.grow}>
|
||||
<div className={classes.sectionDesktop}>
|
||||
<IconButton aria-label="show 4 new mails" color="inherit">
|
||||
<Badge badgeContent={4} color="secondary">
|
||||
<MailIcon style={{width: '1.5em', height: '1.5em', top: '-5px', position: 'relative', right: '-2px'}} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<IconButton aria-label="show 17 new notifications" color="inherit">
|
||||
<Badge badgeContent={17} color="secondary">
|
||||
<NotificationsIcon style={{width: '1.5em', height: '1.5em', top: '-5px', position: 'relative', right: '-2px'}} />
|
||||
</Badge>
|
||||
</IconButton>
|
||||
<IconButton
|
||||
edge="end"
|
||||
aria-label="account of current user"
|
||||
aria-controls={menuId}
|
||||
aria-haspopup="true"
|
||||
onClick={handleProfileMenuOpen}
|
||||
color="inherit"
|
||||
>
|
||||
<AccountCircle style={{width: '1.5em', height: '1.5em', top: '-5px', position: 'relative', right: '-2px'}} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div className={classes.sectionMobile}>
|
||||
<IconButton
|
||||
aria-label="show more"
|
||||
aria-controls={mobileMenuId}
|
||||
aria-haspopup="true"
|
||||
onClick={handleMobileMenuOpen}
|
||||
color="inherit"
|
||||
>
|
||||
<MoreIcon />
|
||||
</IconButton>
|
||||
</div>
|
||||
{renderMobileMenu}
|
||||
{renderMenu}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const mapDispatchToProps = {
|
||||
uploaderModalStateDispatcher: (uploadModalOpened) => updateUploadModalState(uploadModalOpened)
|
||||
}
|
||||
|
||||
export default connect(
|
||||
null,
|
||||
mapDispatchToProps
|
||||
)(UserTools)
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import UserTools from './UserTools';
|
||||
|
||||
describe('UserTools', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import UserTools from './UserTools';
|
||||
|
||||
export default UserTools;
|
||||
@@ -0,0 +1,3 @@
|
||||
import CommandAndUserTools from './CommandAndUserTools';
|
||||
|
||||
export default CommandAndUserTools;
|
||||
@@ -10,9 +10,14 @@
|
||||
transition: all 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.material-icons {
|
||||
&.md-light{color:rgb(33, 37, 41)}
|
||||
&.md-36{font-size:36px}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
padding: 15px 10px;
|
||||
padding: 5px;
|
||||
// background: #222;
|
||||
background: rgba(120, 120, 120, 0.3);
|
||||
border: none;
|
||||
@@ -282,7 +287,7 @@
|
||||
}
|
||||
|
||||
#command.form-control {
|
||||
width: 87%;
|
||||
width: 45%;
|
||||
}
|
||||
|
||||
#faux {
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as $ from 'jquery'
|
||||
import $ from 'jquery'
|
||||
import CommandAndUserTools from './CommandAndUserTools/CommandAndUserTools';
|
||||
import {Link} from 'react-router-dom';
|
||||
|
||||
class CommandPrompt extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.registerSpeechHandlers = this.registerSpeechHandlers.bind(this)
|
||||
this.registerMouseAndKeyboardHandlers = this.registerMouseAndKeyboardHandlers.bind(this)
|
||||
this.showContextMenu = this.showContextMenu.bind(this)
|
||||
this.initiateDomOpsOnEnter = this.initiateDomOpsOnEnter.bind(this)
|
||||
this.startSpeechRecognition = this.startSpeechRecognition.bind(this)
|
||||
this.timerId = 0
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.registerSpeechHandlers();
|
||||
this.registerMouseAndKeyboardHandlers();
|
||||
}
|
||||
|
||||
toggleClass(event) {
|
||||
$(event.target).toggleClass('fas');
|
||||
}
|
||||
|
||||
initiateDomOpsOnEnter(event) {
|
||||
initiateDomOpsOnEnter(event, func, delay) {
|
||||
const code = (event.keyCode ? event.keyCode : event.which);
|
||||
if (code === 13) {
|
||||
// this.domOpsService.hideNonCards();
|
||||
this.executeCommand();
|
||||
func();
|
||||
clearTimeout(this.timerId)
|
||||
return
|
||||
}
|
||||
clearTimeout(this.timerId)
|
||||
this.timerId = setTimeout(func, delay)
|
||||
}
|
||||
|
||||
saveToFavorites() {
|
||||
|
||||
}
|
||||
|
||||
resetCommand() {
|
||||
@@ -35,139 +37,17 @@ class CommandPrompt extends React.Component {
|
||||
$('#command').focus();
|
||||
}
|
||||
|
||||
startSpeechRecognition() {
|
||||
var isChrome = /Chrome/.test(navigator.userAgent) && /Google Inc/.test(navigator.vendor);
|
||||
if(isChrome) {
|
||||
this.recognition.start();
|
||||
} else {
|
||||
// this.domOpsService.sho
|
||||
}
|
||||
}
|
||||
|
||||
registerSpeechHandlers() {
|
||||
let SpeechRecognition
|
||||
let instructions = $('#command')
|
||||
try {
|
||||
SpeechRecognition = window.SpeechRecognition || window.mozSpeechRecognition || window.msSpeechRecognition || window.webkitSpeechRecognition
|
||||
// SpeechRecognition = ''
|
||||
this.recognition = new SpeechRecognition()
|
||||
}
|
||||
catch(e) {
|
||||
console.error(e);
|
||||
$('.no-browser-support').show();
|
||||
$('.app').hide();
|
||||
}
|
||||
this.recognition.onstart = function() {
|
||||
instructions.val('Voice recognition activated. Try speaking into the microphone.');
|
||||
}
|
||||
|
||||
this.recognition.onspeechend = function() {
|
||||
instructions.val('You were quiet for a while so voice recognition turned itself off.');
|
||||
}
|
||||
|
||||
this.recognition.onerror = function(event) {
|
||||
if(event.error === 'no-speech') {
|
||||
instructions.val('No speech was detected. Try again.');
|
||||
};
|
||||
}
|
||||
|
||||
this.recognition.onresult = function(event) {
|
||||
let noteContent = "";
|
||||
let current = event.resultIndex;
|
||||
let transcript = event.results[current][0].transcript;
|
||||
let mobileRepeatBug = (current === 1 && transcript === event.results[0][0].transcript);
|
||||
if(!mobileRepeatBug) {
|
||||
noteContent += transcript;
|
||||
$('#command').val(noteContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
registerMouseAndKeyboardHandlers() {
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-star').hover(() => {
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-star').toggleClass('fas');
|
||||
});
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-trash-alt').hover(() => {
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-trash-alt').toggleClass('fas');
|
||||
});
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-paper-plane').hover(() => {
|
||||
$('#content nav div.collapse li a.nav-link i.far.fa-paper-plane').toggleClass('fas');
|
||||
});
|
||||
$('*').on('click', (event) => {
|
||||
if(!this.contextMenuFirstDisplay) {
|
||||
$("#inputSelectionContextMenu").removeClass("show").hide();
|
||||
this.contextMenuDisplayed = false;
|
||||
// event.stopPropagation();
|
||||
} else {
|
||||
this.contextMenuFirstDisplay = false;
|
||||
// event.stopPropagation();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
executeCommand() {
|
||||
// this.domOpsService.hideNonCards();
|
||||
// let commandVal = this.command.nativeElement.value;
|
||||
// if(!commandVal) {
|
||||
// this.domOpsService.showEmptyCommandMessage(this.emptyCommandMessage);
|
||||
// } else {
|
||||
// let card = this.cardsService.getCommandCard($config.intentSlugToOperations.command.cardTitle, commandVal, {}, "command");
|
||||
// let commandAction = new Command(card);
|
||||
// this.store.dispatch(commandAction);
|
||||
// const text = { text: commandVal };
|
||||
// let recastResponse = this.recastOpsService.getRecastResponse(commandVal, text);
|
||||
// this.processRecastResponse(recastResponse);
|
||||
// }
|
||||
}
|
||||
|
||||
// processRecastResponse(recastResponse: any) {
|
||||
// let bodyRelevant: any;
|
||||
// let intent: any = "";
|
||||
// let assuredIntentFactor = 0.6;
|
||||
// recastResponse.then((body) => {
|
||||
// bodyRelevant = body.results;
|
||||
// let intents = bodyRelevant ? bodyRelevant.intents : "";
|
||||
// if(intents) {
|
||||
// if(intents.length == 1) {
|
||||
// intent = intents[0];
|
||||
// } else if(intents.length > 1) {
|
||||
// let reducer = (probableIntent, currIntent) => probableIntent.confidence >= currIntent.confidence ? probableIntent : currIntent;
|
||||
// intent = intents.reduce(reducer, intents[0]);
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("Intent is either not Identified or is not supported, please try again with a different text.");
|
||||
// return;
|
||||
// }
|
||||
// if (intent && intent.confidence > assuredIntentFactor) {
|
||||
// let intentSlug = intent.slug;
|
||||
// if (!Object.keys($config.intentSlugToOperations).includes(intentSlug)) {
|
||||
// this.domOpsService.showEmptyCommandMessage("Intent is either not Identified or is not supported, please try again with a different text.");
|
||||
// return;
|
||||
// }
|
||||
// // $(`#${$config.constants.hiddenIntentFieldId}`).val(intent);
|
||||
// window.localStorage.setItem($config.constants.hiddenIntentFieldId, intentSlug);
|
||||
// this.domOpsService.displayIntentBox(intentSlug);
|
||||
// if (intentSlug == "resethistory") {
|
||||
// let card = this.cardsService.getResponseCard($config.intentSlugToOperations.resethistory.cardTitle,
|
||||
// $config.intentSlugToOperations.resethistory.cardMsg, {}, "response");
|
||||
// card.insertionCounter = 0;
|
||||
// let resetHistoryResponseAction = new ClearHistory(card);
|
||||
// this.store.dispatch(resetHistoryResponseAction);
|
||||
// return;
|
||||
// }
|
||||
// this.domOpsService.widgetIdentified.emit({widget: intentSlug, bodyRelevant: bodyRelevant});
|
||||
// this.domOpsService.populateRecastData(intentSlug, bodyRelevant);
|
||||
// // store.dispatch($config.intentSlugToOperations.addquery.action);
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("Couldn't conform with the required confidence level of the intent match, please try again.");
|
||||
// }
|
||||
// } else {
|
||||
// this.domOpsService.showEmptyCommandMessage("There seems to be an error in the Recast Response, as intents are not returned.");
|
||||
// return;
|
||||
// }
|
||||
// });
|
||||
|
||||
// }
|
||||
|
||||
showContextMenu(event) {
|
||||
if(this.contextMenuDisplayed === true) {
|
||||
return;
|
||||
@@ -186,6 +66,23 @@ class CommandPrompt extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
executeCommand() {
|
||||
alert('called')
|
||||
// this.domOpsService.hideNonCards();
|
||||
let commandVal = $('#command').val();
|
||||
console.log(commandVal)
|
||||
if(!commandVal) {
|
||||
// this.domOpsService.showEmptyCommandMessage(this.emptyCommandMessage);
|
||||
} else {
|
||||
// let card = this.cardsService.getCommandCard($config.intentSlugToOperations.command.cardTitle, commandVal, {}, "command");
|
||||
// let commandAction = new Command(card);
|
||||
// this.store.dispatch(commandAction);
|
||||
// const text = { text: commandVal };
|
||||
// let recastResponse = this.recastOpsService.getRecastResponse(commandVal, text);
|
||||
// this.processRecastResponse(recastResponse);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="header" className="c-CommandPrompt headerprompt" onDragEnd={this.showContextMenu}>
|
||||
@@ -202,38 +99,15 @@ class CommandPrompt extends React.Component {
|
||||
<nav className="navbar navbar-expand-lg navbar-light">
|
||||
<div className="container-fluid">
|
||||
<input type="text" className="form-control" placeholder="What you need..."
|
||||
name="command" id="command" onKeyUp={this.initiateDomOpsOnEnter}
|
||||
name="command" id="command" onKeyUp={(event) => this.initiateDomOpsOnEnter(event, this.executeCommand, 2000)}
|
||||
// onClick={showContextMenu} #command>
|
||||
onClick={this.showContextMenu} />
|
||||
<div className="dropdown-menu" aria-labelledby="dropdownMenuButton" id="inputSelectionContextMenu">
|
||||
<a className="dropdown-item">Fire</a>
|
||||
<a className="dropdown-item" onClick={this.resetCommand}>Clear</a>
|
||||
<a className="dropdown-item">Add to Favorites</a>
|
||||
</div>
|
||||
<div className="collapse navbar-collapse" id="navbarSupportedContent">
|
||||
<ul className="nav navbar-nav ml-auto">
|
||||
<li id="btnFavorites" className="nav-item">
|
||||
<a className="nav-link">
|
||||
<i className="far fa-star" onMouseEnter={this.toggleClass} onMouseLeave={this.toggleClass}></i>
|
||||
</a>
|
||||
</li>
|
||||
<li id="btnClearCommand" className="nav-item" onClick={this.resetCommand}>
|
||||
<a className="nav-link">
|
||||
<i className="far fa-trash-alt" onMouseEnter={this.toggleClass} onMouseLeave={this.toggleClass}></i>
|
||||
</a>
|
||||
</li>
|
||||
<li id="btnFireCommand" className="nav-item">
|
||||
<a className="nav-link">
|
||||
<i className="far fa-paper-plane" onMouseEnter={this.toggleClass} onMouseLeave={this.toggleClass}></i>
|
||||
</a>
|
||||
</li>
|
||||
<li id="btnSTTCommand" className="nav-item" onClick={this.startSpeechRecognition}>
|
||||
<a className="nav-link">
|
||||
<i className="far fa-microphone fas"></i>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<Link to="#" className="dropdown-item" onClick={this.executeCommand}>Fire</Link>
|
||||
<Link to="#" className="dropdown-item" onClick={this.resetCommand}>Clear</Link>
|
||||
<Link to="#" className="dropdown-item" onClick={this.saveToFavorites}>Add to Favorites</Link>
|
||||
</div>
|
||||
<CommandAndUserTools execute={this.executeCommand} reset={this.resetCommand} save={this.saveToFavorites} />
|
||||
</div>
|
||||
</nav>
|
||||
<input type="hidden" id="intentHidden" value="createissue" />
|
||||
@@ -242,8 +116,4 @@ class CommandPrompt extends React.Component {
|
||||
}
|
||||
}
|
||||
|
||||
CommandPrompt.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default CommandPrompt;
|
||||
@@ -0,0 +1,23 @@
|
||||
.c-Dropzone {
|
||||
height: 200px;
|
||||
width: 200px;
|
||||
background-color: #fff;
|
||||
border: 2px dashed rgb(187, 186, 186);
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
font-size: 16px;
|
||||
.Icon {
|
||||
opacity: 0.3;
|
||||
height: 64px;
|
||||
width: 64px;
|
||||
}
|
||||
.FileInput {
|
||||
display: none;
|
||||
}
|
||||
&.Highlight {
|
||||
background-color: rgb(188, 185, 236);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class Dropzone extends React.Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = { hightlight: false };
|
||||
this.fileInputRef = React.createRef();
|
||||
this.openFileDialog = this.openFileDialog.bind(this);
|
||||
this.onFilesAdded = this.onFilesAdded.bind(this);
|
||||
this.onDragOver = this.onDragOver.bind(this);
|
||||
this.onDragLeave = this.onDragLeave.bind(this);
|
||||
this.onDrop = this.onDrop.bind(this);
|
||||
}
|
||||
|
||||
openFileDialog() {
|
||||
if (this.props.disabled) return;
|
||||
this.fileInputRef.current.click();
|
||||
}
|
||||
|
||||
onFilesAdded(evt) {
|
||||
if (this.props.disabled) return;
|
||||
const files = evt.target.files;
|
||||
if (this.props.onFilesAdded) {
|
||||
const array = this.fileListToArray(files);
|
||||
this.props.onFilesAdded(array);
|
||||
}
|
||||
}
|
||||
|
||||
fileListToArray(list) {
|
||||
const array = [];
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
array.push(list.item(i));
|
||||
}
|
||||
return array;
|
||||
}
|
||||
|
||||
onDragOver(evt) {
|
||||
evt.preventDefault();
|
||||
if (this.props.disabled) return;
|
||||
this.setState({ hightlight: true });
|
||||
}
|
||||
|
||||
onDragLeave() {
|
||||
this.setState({ hightlight: false });
|
||||
}
|
||||
|
||||
onDrop(event) {
|
||||
event.preventDefault();
|
||||
if (this.props.disabled) return;
|
||||
const files = event.dataTransfer.files;
|
||||
if (this.props.onFilesAdded) {
|
||||
const array = this.fileListToArray(files);
|
||||
this.props.onFilesAdded(array);
|
||||
}
|
||||
this.setState({ hightlight: false });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
className={`c-Dropzone ${this.state.hightlight ? "Highlight" : ""}`}
|
||||
onClick={this.openFileDialog}
|
||||
onDragOver={this.onDragOver}
|
||||
onDragLeave={this.onDragLeave}
|
||||
onDrop={this.onDrop}
|
||||
style={{ cursor: this.props.disabled ? "default" : "pointer" }}
|
||||
>
|
||||
<img
|
||||
alt="upload"
|
||||
className="Icon"
|
||||
src="/images/cloud.svg"
|
||||
/>
|
||||
<input
|
||||
ref={this.fileInputRef}
|
||||
className="FileInput"
|
||||
type="file"
|
||||
multiple
|
||||
onChange={this.onFilesAdded}
|
||||
/>
|
||||
<span>Upload Files</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Dropzone.propTypes = {
|
||||
onFilesAdded: PropTypes.func
|
||||
};
|
||||
|
||||
export default Dropzone;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import Dropzone from './Dropzone';
|
||||
|
||||
describe('Dropzone', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import Dropzone from './Dropzone';
|
||||
|
||||
export default Dropzone;
|
||||
@@ -0,0 +1,13 @@
|
||||
.c-Progress {
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
background-color: rgb(183, 155, 229);
|
||||
border-radius: 5px;
|
||||
|
||||
.Progress {
|
||||
background-color: rgba(103, 58, 183, 1);
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import React, { Component } from 'react'
|
||||
|
||||
class Progress extends Component {
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {}
|
||||
}
|
||||
render() {
|
||||
return (
|
||||
<div className="c-Progress">
|
||||
<div
|
||||
className="Progress"
|
||||
style={{ width: this.props.progress + '%' }}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default Progress
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import Progress from './Progress';
|
||||
|
||||
describe('Progress', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import Progress from './Progress';
|
||||
|
||||
export default Progress;
|
||||
@@ -0,0 +1,3 @@
|
||||
.c-StepProgressBar {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {makeStyles, withStyles} from '@material-ui/core/styles';
|
||||
import clsx from 'clsx';
|
||||
import Stepper from '@material-ui/core/Stepper';
|
||||
import Step from '@material-ui/core/Step';
|
||||
import StepLabel from '@material-ui/core/StepLabel';
|
||||
import Mood from '@material-ui/icons/Mood';
|
||||
import Image from '@material-ui/icons/Image';
|
||||
import QueueMusic from '@material-ui/icons/QueueMusic';
|
||||
import AddCircle from '@material-ui/icons/AddCircle';
|
||||
import StepConnector from '@material-ui/core/StepConnector';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const ColorlibConnector = withStyles({
|
||||
alternativeLabel: {
|
||||
top: 22,
|
||||
},
|
||||
active: {
|
||||
'& $line': {
|
||||
backgroundImage:
|
||||
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
|
||||
},
|
||||
},
|
||||
completed: {
|
||||
'& $line': {
|
||||
backgroundImage:
|
||||
'linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)',
|
||||
},
|
||||
},
|
||||
line: {
|
||||
height: 3,
|
||||
border: 0,
|
||||
backgroundColor: '#eaeaf0',
|
||||
borderRadius: 1,
|
||||
},
|
||||
})(StepConnector);
|
||||
|
||||
const useColorlibStepIconStyles = makeStyles({
|
||||
root: {
|
||||
backgroundColor: '#ccc',
|
||||
zIndex: 1,
|
||||
color: '#fff',
|
||||
width: 50,
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
borderRadius: '50%',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
},
|
||||
active: {
|
||||
backgroundImage:
|
||||
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||
boxShadow: '0 4px 10px 0 rgba(0,0,0,.25)',
|
||||
},
|
||||
completed: {
|
||||
backgroundImage:
|
||||
'linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)',
|
||||
},
|
||||
});
|
||||
|
||||
function ColorlibStepIcon(props) {
|
||||
const classes = useColorlibStepIconStyles();
|
||||
const { active, completed } = props;
|
||||
|
||||
const icons = {
|
||||
1: <Mood />,
|
||||
2: <Image />,
|
||||
3: <QueueMusic />,
|
||||
4: <AddCircle />,
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(classes.root, {
|
||||
[classes.active]: active,
|
||||
[classes.completed]: completed,
|
||||
})}
|
||||
>
|
||||
{icons[String(props.icon)]}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ColorlibStepIcon.propTypes = {
|
||||
active: PropTypes.bool,
|
||||
completed: PropTypes.bool,
|
||||
icon: PropTypes.node,
|
||||
};
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
root: {
|
||||
width: '90%',
|
||||
margin: 'auto'
|
||||
},
|
||||
button: {
|
||||
marginRight: theme.spacing(1),
|
||||
},
|
||||
instructions: {
|
||||
marginTop: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
},
|
||||
}));
|
||||
|
||||
function getSteps() {
|
||||
return ['How\'s your mood?', 'Select images', 'Select music', 'Click next to create Ecstatica'];
|
||||
}
|
||||
|
||||
function getStepContent(step) {
|
||||
switch (step) {
|
||||
case 0:
|
||||
return 'What\'s your mood';
|
||||
case 1:
|
||||
return 'Select Image';
|
||||
case 2:
|
||||
return 'Select Music';
|
||||
case 3:
|
||||
return 'Click next for magic';
|
||||
default:
|
||||
return 'Unknown step';
|
||||
}
|
||||
}
|
||||
|
||||
const StepProgressBar = props => {
|
||||
|
||||
const classes = useStyles();
|
||||
const [activeStep, setActiveStep] = useState(0);
|
||||
const steps = getSteps();
|
||||
|
||||
const handleNext = () => {
|
||||
setActiveStep(prevActiveStep => prevActiveStep + 1);
|
||||
};
|
||||
|
||||
const handleBack = () => {
|
||||
setActiveStep(prevActiveStep => prevActiveStep - 1);
|
||||
};
|
||||
|
||||
const handleReset = () => {
|
||||
setActiveStep(0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classes.root}>
|
||||
<Stepper alternativeLabel activeStep={activeStep} connector={<ColorlibConnector />}>
|
||||
{steps.map(label => (
|
||||
<Step key={label}>
|
||||
<StepLabel StepIconComponent={ColorlibStepIcon}>{label}</StepLabel>
|
||||
</Step>
|
||||
))}
|
||||
</Stepper>
|
||||
<div>
|
||||
{activeStep === steps.length ? (
|
||||
<div>
|
||||
<Typography className={classes.instructions}>
|
||||
All steps completed - you're finished
|
||||
</Typography>
|
||||
<Button onClick={handleReset} className={classes.button}>
|
||||
Reset
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<Typography className={classes.instructions}>{getStepContent(activeStep)}</Typography>
|
||||
<div>
|
||||
<Button disabled={activeStep === 0} onClick={handleBack} className={classes.button}>
|
||||
Back
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={handleNext}
|
||||
className={classes.button}
|
||||
>
|
||||
{activeStep === steps.length - 1 ? 'Finish' : 'Next'}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
StepProgressBar.propTypes = {
|
||||
|
||||
};
|
||||
|
||||
export default StepProgressBar;
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import StepProgressBar from './StepProgressBar';
|
||||
|
||||
describe('StepProgressBar', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import StepProgressBar from './StepProgressBar';
|
||||
|
||||
export default StepProgressBar;
|
||||
@@ -0,0 +1,82 @@
|
||||
.c-Uploader {
|
||||
|
||||
}
|
||||
|
||||
.MuiPaper-root.MuiPaper-elevation4.Uploader-paper-152.MuiPaper-rounded {
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// flex: 1;
|
||||
align-items: flex-start;
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
|
||||
.Content {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
padding-top: 16px;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
.Files {
|
||||
margin-left: 32px;
|
||||
align-items: flex-start;
|
||||
justify-items: flex-start;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
.CheckIcon {
|
||||
opacity: 0.5;
|
||||
margin-left: 32px;
|
||||
}
|
||||
|
||||
.ProgressWrapper {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.Actions {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
align-items: flex-end;
|
||||
flex-direction: column;
|
||||
margin-top: 32px;
|
||||
button {
|
||||
font-family: 'Roboto medium', sans-serif;
|
||||
font-size: 14px;
|
||||
display: inline-block;
|
||||
height: 36px;
|
||||
min-width: 88px;
|
||||
padding: 6px 16px;
|
||||
line-height: 1.42857143;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
-ms-touch-action: manipulation;
|
||||
touch-action: manipulation;
|
||||
cursor: pointer;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
border: 0;
|
||||
border-radius: 2px;
|
||||
background: rgba(103, 58, 183, 1);
|
||||
color: #fff;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background: rgb(189, 189, 189);
|
||||
cursor: default;
|
||||
}
|
||||
}
|
||||
|
||||
.Title {
|
||||
margin-bottom: 32px;
|
||||
color: #555;
|
||||
}
|
||||
}
|
||||
188
client/src/app/components/molecules/common/Uploader/Uploader.js
Normal file
188
client/src/app/components/molecules/common/Uploader/Uploader.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {connect} from 'react-redux';
|
||||
import {createPropsSelector} from 'reselect-immutable-helpers';
|
||||
import {withStyles} from '@material-ui/core/styles';
|
||||
import {updateUploadModalState} from '../../../../pages/Home/actions';
|
||||
import {getUploadModalState} from '../../../../pages/Home/selectors';
|
||||
import Dropzone from './../Dropzone'
|
||||
import Progress from '../Progress';
|
||||
import Modal from '@material-ui/core/Modal';
|
||||
import Backdrop from '@material-ui/core/Backdrop';
|
||||
import {Zoom, Paper} from '@material-ui/core';
|
||||
|
||||
const styles = theme => ({
|
||||
modal: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
},
|
||||
paper: {
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
border: '2px solid #000',
|
||||
boxShadow: theme.shadows[5],
|
||||
padding: theme.spacing(2, 4, 3),
|
||||
width: '40%'
|
||||
}
|
||||
})
|
||||
|
||||
class Uploader extends React.Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props)
|
||||
this.state = {
|
||||
files: [],
|
||||
uploading: false,
|
||||
uploadProgress: {},
|
||||
successfullUploaded: false
|
||||
};
|
||||
this.onFilesAdded = this.onFilesAdded.bind(this);
|
||||
this.uploadFiles = this.uploadFiles.bind(this);
|
||||
this.sendRequest = this.sendRequest.bind(this);
|
||||
this.renderActions = this.renderActions.bind(this);
|
||||
this.renderProgress = this.renderProgress.bind(this);
|
||||
}
|
||||
|
||||
onFilesAdded(files) {
|
||||
this.setState(prevState => ({
|
||||
files: prevState.files.concat(files)
|
||||
}));
|
||||
}
|
||||
|
||||
renderProgress(file) {
|
||||
const uploadProgress = this.state.uploadProgress[file.name];
|
||||
if (this.state.uploading || this.state.successfullUploaded) {
|
||||
return (
|
||||
<div className="ProgressWrapper">
|
||||
<Progress progress={uploadProgress ? uploadProgress.percentage : 0} />
|
||||
<img
|
||||
className="CheckIcon"
|
||||
alt="done"
|
||||
src="baseline-check_circle_outline-24px.svg"
|
||||
style={{
|
||||
opacity:
|
||||
uploadProgress && uploadProgress.state === "done" ? 0.5 : 0
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
renderActions() {
|
||||
if (this.state.successfullUploaded) {
|
||||
return (
|
||||
<button
|
||||
onClick={() =>
|
||||
this.setState({ files: [], successfullUploaded: false })
|
||||
}
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<button
|
||||
disabled={this.state.files.length < 0 || this.state.uploading}
|
||||
onClick={this.uploadFiles}
|
||||
>
|
||||
Upload
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async uploadFiles() {
|
||||
this.setState({ uploadProgress: {}, uploading: true });
|
||||
const promises = [];
|
||||
this.state.files.forEach(file => {
|
||||
promises.push(this.sendRequest(file));
|
||||
});
|
||||
try {
|
||||
await Promise.all(promises);
|
||||
|
||||
this.setState({ successfullUploaded: true, uploading: false });
|
||||
} catch (e) {
|
||||
// Not Production ready! Do some error handling here instead...
|
||||
this.setState({ successfullUploaded: true, uploading: false });
|
||||
}
|
||||
}
|
||||
|
||||
sendRequest(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = new XMLHttpRequest();
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name);
|
||||
|
||||
req.open("POST", "http://localhost:3001/upload");
|
||||
req.send(formData);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes, modalState, handleCloseDispatcher} = this.props
|
||||
return (
|
||||
<div className="c-Uploader">
|
||||
<Modal
|
||||
aria-labelledby="transition-modal-title"
|
||||
aria-describedby="transition-modal-description"
|
||||
className={classes.modal}
|
||||
open={modalState || false}
|
||||
onClose={() => handleCloseDispatcher(false)}
|
||||
closeAfterTransition
|
||||
BackdropComponent={Backdrop}
|
||||
BackdropProps={{
|
||||
timeout: 500,
|
||||
}}
|
||||
>
|
||||
<Zoom in={modalState || false}>
|
||||
<Paper elevation={4} className={classes.paper}>
|
||||
<span className="Title">Upload Files</span>
|
||||
<div className="Content">
|
||||
<div>
|
||||
<Dropzone
|
||||
onFilesAdded={this.onFilesAdded}
|
||||
disabled={this.state.uploading || this.state.successfullUploaded}
|
||||
/>
|
||||
</div>
|
||||
<div className="Files">
|
||||
{this.state.files.map(file => {
|
||||
return (
|
||||
<div key={file.name} className="Row">
|
||||
<span className="Filename">{file.name}</span>
|
||||
{this.renderProgress(file)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<div className="Actions">
|
||||
{this.renderActions()}
|
||||
</div>
|
||||
</Paper>
|
||||
</Zoom>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Uploader.propTypes = {
|
||||
classes: PropTypes.object.isRequired,
|
||||
handleCloseDispatcher: PropTypes.func,
|
||||
modalState: PropTypes.bool
|
||||
};
|
||||
|
||||
const mapStateToProps = createPropsSelector({
|
||||
modalState: getUploadModalState
|
||||
})
|
||||
|
||||
const mapDispatchToProps = {
|
||||
handleCloseDispatcher: updateUploadModalState
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps
|
||||
)(withStyles(styles)(Uploader))
|
||||
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import Uploader from './Uploader';
|
||||
|
||||
describe('Uploader', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,3 @@
|
||||
import Uploader from './Uploader';
|
||||
|
||||
export default Uploader;
|
||||
@@ -1,3 +0,0 @@
|
||||
<p>
|
||||
page-not-found works!
|
||||
</p>
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-page-not-found',
|
||||
templateUrl: './page-not-found.component.html',
|
||||
styleUrls: ['./page-not-found.component.scss']
|
||||
})
|
||||
export class PageNotFoundComponent implements OnInit {
|
||||
|
||||
constructor() { }
|
||||
|
||||
ngOnInit() {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import {getBackgroundImage, getHome} from './selectors'
|
||||
import Sidebar from '../../components/molecules/SideBar'
|
||||
import CommandPrompt from '../../components/molecules/common/CommandPrompt'
|
||||
import PrincipalContent from '../../components/molecules/PrincipalContent/PrincipalContent';
|
||||
import Uploader from '../../components/molecules/common/Uploader/Uploader';
|
||||
|
||||
class Home extends React.Component {
|
||||
constructor(props) {
|
||||
@@ -23,10 +24,12 @@ class Home extends React.Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="Home" style={{backgroundImage: `url(${this.props.backgroundImage})`}}>
|
||||
<div className="Home">
|
||||
<div className="c-background" style={{backgroundImage: `url(${this.props.backgroundImage})`}} />
|
||||
<Sidebar />
|
||||
<CommandPrompt />
|
||||
<PrincipalContent />
|
||||
<Uploader />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
.Home {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 1.0;
|
||||
-webkit-transition: background 1.5s linear;
|
||||
-moz-transition: background 1.5s linear;
|
||||
-o-transition: background 1.5s linear;
|
||||
-ms-transition: background 1.5s linear;
|
||||
transition: background 1.5s linear;
|
||||
-webkit-background-size: contain;
|
||||
-moz-background-size: contain;
|
||||
-o-background-size: contain;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
.c-background {
|
||||
height: 1000px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
opacity: 1.0;
|
||||
transition: background 1.5s linear;
|
||||
background-size: contain;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
background-size: cover;
|
||||
z-index: -20;
|
||||
filter: blur(5px);
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.Card {
|
||||
background-color: white;
|
||||
padding: 32px;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
box-shadow: 0 15px 30px 0 rgba(0, 0, 0, 0.11), 0 5px 15px 0 rgba(0, 0, 0, 0.08);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export const UPDATE_FORM_VALUES = 'UPDATE_BILLING_FORM_VALUES'
|
||||
export const UPDATE_FORM_ERRORS = 'UPDATE_BILLING_FORM_ERRORS'
|
||||
export const UPDATE_BACKGROUND = 'UPDATE_BACKGROUND'
|
||||
export const UPDATE_MODAL_STATE = 'UPDATE_MODAL_STATE'
|
||||
export const UPDATE_UPLOAD_MODAL_STATE = 'UPDATE_UPLOAD_MODAL_STATE'
|
||||
|
||||
export const updateHomeDataState = (payload) => ({type: HOME_DATA_STATE_RECEIVED, payload})
|
||||
|
||||
@@ -42,4 +43,11 @@ export const updateModalState = (modalState) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const updateUploadModalState = (uploadModalState) => {
|
||||
return {
|
||||
type: UPDATE_UPLOAD_MODAL_STATE,
|
||||
payload: {uploadModalOpened: uploadModalState}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import Immutable from 'immutable'
|
||||
|
||||
import {HOME_DATA_STATE_RECEIVED, UPDATE_FORM_ERRORS, UPDATE_FORM_VALUES, UPDATE_BACKGROUND, UPDATE_MODAL_STATE} from './actions'
|
||||
import {
|
||||
HOME_DATA_STATE_RECEIVED,
|
||||
UPDATE_FORM_ERRORS,
|
||||
UPDATE_FORM_VALUES,
|
||||
UPDATE_BACKGROUND,
|
||||
UPDATE_MODAL_STATE,
|
||||
UPDATE_UPLOAD_MODAL_STATE
|
||||
} from './actions'
|
||||
|
||||
const initialState = Immutable.Map({
|
||||
formErrors: [],
|
||||
@@ -14,6 +21,7 @@ const reducer = (state = initialState, action) => {
|
||||
case UPDATE_FORM_VALUES:
|
||||
case UPDATE_BACKGROUND:
|
||||
case UPDATE_MODAL_STATE:
|
||||
case UPDATE_UPLOAD_MODAL_STATE:
|
||||
return state.mergeDeep(action.payload)
|
||||
default:
|
||||
return state
|
||||
|
||||
@@ -12,5 +12,6 @@ export const getHome = createSelector(
|
||||
|
||||
export const getBackgroundImage = createGetSelector(getHome, 'backgroundImage')
|
||||
export const getModalState = createGetSelector(getHome, 'modalOpened')
|
||||
export const getUploadModalState = createGetSelector(getHome, 'uploadModalOpened')
|
||||
export const getFormValues = createGetSelector(getHome, 'formValues')
|
||||
export const getFormErrors = createGetSelector(getHome, 'formErrors')
|
||||
|
||||
14
client/src/app/pages/ecstatica/Creator/Creator.js
Normal file
14
client/src/app/pages/ecstatica/Creator/Creator.js
Normal file
@@ -0,0 +1,14 @@
|
||||
import React from 'react';
|
||||
import StepProgressBar from '../../../components/molecules/common/StepProgressBar/StepProgressBar';
|
||||
import ImageContainer from '../../../components/molecules/PrincipalContent/ImageContainer';
|
||||
|
||||
const Creator = props => {
|
||||
return (
|
||||
<div className='c-Creator'>
|
||||
<StepProgressBar />
|
||||
<ImageContainer />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Creator;
|
||||
@@ -0,0 +1,3 @@
|
||||
.c-Creator {
|
||||
|
||||
}
|
||||
8
client/src/app/pages/ecstatica/Creator/Creator.test.js
Normal file
8
client/src/app/pages/ecstatica/Creator/Creator.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import Creator from './Creator';
|
||||
|
||||
describe('Creator', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
3
client/src/app/pages/ecstatica/Creator/index.js
Normal file
3
client/src/app/pages/ecstatica/Creator/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Creator from './Creator';
|
||||
|
||||
export default Creator;
|
||||
10
client/src/app/pages/ecstatica/Viewer/Viewer.js
Normal file
10
client/src/app/pages/ecstatica/Viewer/Viewer.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
|
||||
const Viewer = props => {
|
||||
return (
|
||||
<div className='c-Viewer'>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Viewer;
|
||||
3
client/src/app/pages/ecstatica/Viewer/Viewer.module.scss
Normal file
3
client/src/app/pages/ecstatica/Viewer/Viewer.module.scss
Normal file
@@ -0,0 +1,3 @@
|
||||
.c-Viewer {
|
||||
|
||||
}
|
||||
8
client/src/app/pages/ecstatica/Viewer/Viewer.test.js
Normal file
8
client/src/app/pages/ecstatica/Viewer/Viewer.test.js
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import Viewer from './Viewer';
|
||||
|
||||
describe('Viewer', () => {
|
||||
it('renders without error', () => {
|
||||
|
||||
});
|
||||
});
|
||||
3
client/src/app/pages/ecstatica/Viewer/index.js
Normal file
3
client/src/app/pages/ecstatica/Viewer/index.js
Normal file
@@ -0,0 +1,3 @@
|
||||
import Viewer from './Viewer';
|
||||
|
||||
export default Viewer;
|
||||
@@ -1,9 +1,13 @@
|
||||
/* PLOP_INJECT_IMPORT */
|
||||
import Viewer from './Viewer';
|
||||
import Creator from './Creator';
|
||||
import Login from './Login';
|
||||
import Home from './Home';
|
||||
|
||||
export {
|
||||
/* PLOP_INJECT_EXPORT */
|
||||
Viewer,
|
||||
Creator,
|
||||
Login,
|
||||
Home,
|
||||
}
|
||||
@@ -15,6 +15,16 @@ export const LoadableLogin = Loadable({
|
||||
loading: PageLoader
|
||||
})
|
||||
|
||||
export const LoadableEcstaticaCreator = Loadable({
|
||||
loader: () => import('./pages/ecstatica/Creator'),
|
||||
loading: PageLoader
|
||||
})
|
||||
|
||||
export const LoadableEcstaticaViewer = Loadable({
|
||||
loader: () => import('./pages/ecstatica/Viewer'),
|
||||
loading: PageLoader
|
||||
})
|
||||
|
||||
class Router extends React.Component {
|
||||
|
||||
render() {
|
||||
@@ -25,6 +35,8 @@ class Router extends React.Component {
|
||||
<Route exact path="/" component={LoadableLogin} />
|
||||
<Route path="/home" component={LoadableHome} />
|
||||
<Route path="/login" component={LoadableLogin} />
|
||||
<Route path="/ecstatica/create" component={LoadableEcstaticaCreator} />
|
||||
<Route path="/ecstatica/view" component={LoadableEcstaticaViewer} />
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
)
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
// ===
|
||||
@import '../components/atoms/InputField/InputField.component';
|
||||
@import '../components/molecules/common/Form/Form.component';
|
||||
@import '../components/molecules/common/Dropzone/Dropzone.component';
|
||||
@import '../components/molecules/common/Uploader/Uploader.component';
|
||||
@import '../components/molecules/common/Progress/Progress.component';
|
||||
@import '../components/molecules/common/Jumbotron/Jumbotron.component';
|
||||
@import '../components/molecules/LoginForm/LoginForm.component';
|
||||
@import '../components/molecules/PageLoader/PageLoader.component';
|
||||
@@ -9,7 +12,9 @@
|
||||
@import '../components/molecules/SideBar/SideBar.component';
|
||||
@import '../components/molecules/SideBar/SideBarTab/SideBarTab.component';
|
||||
@import '../components/molecules/common/CommandPrompt/CommandPrompt.component';
|
||||
@import '../components/molecules/common/CommandPrompt/CommandAndUserTools/CommandAndUserTools.component';
|
||||
@import '../components/molecules/PrincipalContent/PrincipalContent.component';
|
||||
@import '../components/molecules/PrincipalContent/ImageContainer/ImageContainer.component';
|
||||
@import '../components/molecules/PrincipalContent/ImageContainer/Image/Image.component';
|
||||
@import '../components/molecules/PrincipalContent/ImageContainer/Image/ImageHeader/ImageHeader.component';
|
||||
// @import '../preloader/preload';
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
@import url('https://use.fontawesome.com/releases/v5.1.0/css/all.css');
|
||||
@import url('https://fonts.googleapis.com/css?family=Poppins:300,400,500,600,700');
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap');
|
||||
@import url('https://fonts.googleapis.com/icon?family=Material+Icons');
|
||||
|
||||
h1,
|
||||
h2,
|
||||
|
||||
Reference in New Issue
Block a user