Merge pull request #37 from sensebox/backend-connection
Backend connection
This commit is contained in:
commit
a929c3d8e6
5
.env
5
.env
@ -1,3 +1,8 @@
|
||||
REACT_APP_COMPILER_URL=https://compiler.sensebox.de
|
||||
REACT_APP_BOARD=sensebox-mcu
|
||||
REACT_APP_BLOCKLY_API=https://api.blockly.sensebox.de
|
||||
|
||||
REACT_APP_MYBADGES=https://mybadges.org
|
||||
|
||||
# in days
|
||||
REACT_APP_SHARE_LINK_EXPIRES=30
|
||||
|
8
package-lock.json
generated
8
package-lock.json
generated
@ -2581,6 +2581,14 @@
|
||||
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.10.0.tgz",
|
||||
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
|
||||
},
|
||||
"axios": {
|
||||
"version": "0.21.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.0.tgz",
|
||||
"integrity": "sha512-fmkJBknJKoZwem3/IKSSLpkdNXZeBu5Q7GA/aRsr2btgrptmSCxi2oFjZHqGdK9DoTil9PIHlPIZw2EcRJXRvw==",
|
||||
"requires": {
|
||||
"follow-redirects": "^1.10.0"
|
||||
}
|
||||
},
|
||||
"axobject-query": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz",
|
||||
|
@ -32,6 +32,7 @@
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"dev": "set \"REACT_APP_BLOCKLY_API=http://localhost:8080\" && npm start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
|
45
src/App.js
45
src/App.js
@ -1,10 +1,11 @@
|
||||
import React from 'react';
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
import { createBrowserHistory } from "history";
|
||||
|
||||
import { Provider } from 'react-redux';
|
||||
import store from './store';
|
||||
import { loadUser } from './actions/authActions';
|
||||
|
||||
import './App.css';
|
||||
|
||||
@ -12,7 +13,7 @@ import { ThemeProvider, createMuiTheme } from '@material-ui/core/styles';
|
||||
|
||||
import Navbar from './components/Navbar';
|
||||
import Footer from './components/Footer';
|
||||
import Routes from './components/Routes';
|
||||
import Routes from './components/Route/Routes';
|
||||
import Cookies from './components/Cookies';
|
||||
|
||||
const theme = createMuiTheme({
|
||||
@ -23,28 +24,36 @@ const theme = createMuiTheme({
|
||||
},
|
||||
secondary: {
|
||||
main: '#DDDDDD'
|
||||
},
|
||||
button: {
|
||||
compile: '#e27136'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const customHistory = createBrowserHistory();
|
||||
class App extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
store.dispatch(loadUser());
|
||||
}
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<Router history={customHistory}>
|
||||
<div className="wrapper">
|
||||
<Navbar />
|
||||
<Routes />
|
||||
<Cookies />
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
render() {
|
||||
const customHistory = createBrowserHistory();
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<Provider store={store}>
|
||||
<Router history={customHistory}>
|
||||
<div className="wrapper">
|
||||
<Navbar />
|
||||
<Routes />
|
||||
<Cookies />
|
||||
<Footer />
|
||||
</div>
|
||||
</Router>
|
||||
</Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
233
src/actions/authActions.js
Normal file
233
src/actions/authActions.js
Normal file
@ -0,0 +1,233 @@
|
||||
import { MYBADGES_CONNECT, MYBADGES_DISCONNECT, USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types';
|
||||
|
||||
import axios from 'axios';
|
||||
import { returnErrors, returnSuccess } from './messageActions'
|
||||
|
||||
|
||||
// Check token & load user
|
||||
export const loadUser = () => (dispatch) => {
|
||||
// user loading
|
||||
dispatch({
|
||||
type: USER_LOADING
|
||||
});
|
||||
const config = {
|
||||
success: res => {
|
||||
dispatch({
|
||||
type: USER_LOADED,
|
||||
payload: res.data.user
|
||||
});
|
||||
},
|
||||
error: err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status));
|
||||
}
|
||||
dispatch({
|
||||
type: AUTH_ERROR
|
||||
});
|
||||
}
|
||||
};
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/user`, config, dispatch(authInterceptor()))
|
||||
.then(res => {
|
||||
res.config.success(res);
|
||||
})
|
||||
.catch(err => {
|
||||
err.config.error(err);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
var logoutTimerId;
|
||||
const timeToLogout = 14.9*60*1000; // nearly 15 minutes corresponding to the API
|
||||
|
||||
// Login user
|
||||
export const login = ({ email, password }) => (dispatch) => {
|
||||
// Headers
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
// Request Body
|
||||
const body = JSON.stringify({ email, password });
|
||||
axios.post(`${process.env.REACT_APP_BLOCKLY_API}/user`, body, config)
|
||||
.then(res => {
|
||||
// Logout automatically if refreshToken "expired"
|
||||
const logoutTimer = () => setTimeout(
|
||||
() => dispatch(logout()),
|
||||
timeToLogout
|
||||
);
|
||||
logoutTimerId = logoutTimer();
|
||||
dispatch({
|
||||
type: LOGIN_SUCCESS,
|
||||
payload: res.data
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'LOGIN_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGIN_FAIL'));
|
||||
dispatch({
|
||||
type: LOGIN_FAIL
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Connect to MyBadges-Account
|
||||
export const connectMyBadges = ({ username, password }) => (dispatch, getState) => {
|
||||
// Headers
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
// Request Body
|
||||
const body = JSON.stringify({ username, password });
|
||||
axios.post(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`, body, config)
|
||||
.then(res => {
|
||||
var user = getState().auth.user;
|
||||
user.badge = res.data.account;
|
||||
user.badges = res.data.badges;
|
||||
dispatch({
|
||||
type: MYBADGES_CONNECT,
|
||||
payload: user
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'MYBADGES_CONNECT_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'MYBADGES_CONNECT_FAIL'));
|
||||
});
|
||||
};
|
||||
|
||||
// Disconnect MyBadges-Account
|
||||
export const disconnectMyBadges = () => (dispatch, getState) => {
|
||||
// Headers
|
||||
const config = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
};
|
||||
axios.put(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`, config)
|
||||
.then(res => {
|
||||
var user = getState().auth.user;
|
||||
user.badge = null;
|
||||
user.badges = null;
|
||||
dispatch({
|
||||
type: MYBADGES_DISCONNECT,
|
||||
payload: user
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'MYBADGES_DISCONNECT_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'MYBADGES_DISCONNECT_FAIL'));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
// Logout User
|
||||
export const logout = () => (dispatch) => {
|
||||
const config = {
|
||||
success: res => {
|
||||
dispatch({
|
||||
type: LOGOUT_SUCCESS
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'LOGOUT_SUCCESS'));
|
||||
clearTimeout(logoutTimerId);
|
||||
},
|
||||
error: err => {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'LOGOUT_FAIL'));
|
||||
dispatch({
|
||||
type: LOGOUT_FAIL
|
||||
});
|
||||
clearTimeout(logoutTimerId);
|
||||
}
|
||||
};
|
||||
axios.post('https://api.opensensemap.org/users/sign-out', {}, config)
|
||||
.then(res => {
|
||||
res.config.success(res);
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response.status !== 401){
|
||||
err.config.error(err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const authInterceptor = () => (dispatch, getState) => {
|
||||
// Add a request interceptor
|
||||
axios.interceptors.request.use(
|
||||
config => {
|
||||
config.headers['Content-Type'] = 'application/json';
|
||||
const token = getState().auth.token;
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// Add a response interceptor
|
||||
axios.interceptors.response.use(
|
||||
response => {
|
||||
// request was successfull
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
const originalRequest = error.config;
|
||||
const refreshToken = getState().auth.refreshToken;
|
||||
if(refreshToken){
|
||||
// try to refresh the token failed
|
||||
if (error.response.status === 401 && originalRequest._retry) {
|
||||
// router.push('/login');
|
||||
return Promise.reject(error);
|
||||
}
|
||||
// token was not valid and 1st try to refresh the token
|
||||
if (error.response.status === 401 && !originalRequest._retry) {
|
||||
originalRequest._retry = true;
|
||||
const refreshToken = getState().auth.refreshToken;
|
||||
// request to refresh the token, in request-body is the refreshToken
|
||||
axios.post('https://api.opensensemap.org/users/refresh-auth', {"token": refreshToken})
|
||||
.then(res => {
|
||||
if (res.status === 200) {
|
||||
clearTimeout(logoutTimerId);
|
||||
const logoutTimer = () => setTimeout(
|
||||
() => dispatch(logout()),
|
||||
timeToLogout
|
||||
);
|
||||
logoutTimerId = logoutTimer();
|
||||
dispatch({
|
||||
type: REFRESH_TOKEN_SUCCESS,
|
||||
payload: res.data
|
||||
});
|
||||
axios.defaults.headers.common['Authorization'] = 'Bearer ' + getState().auth.token;
|
||||
// request was successfull, new request with the old parameters and the refreshed token
|
||||
return axios(originalRequest)
|
||||
.then(res => {
|
||||
originalRequest.success(res);
|
||||
})
|
||||
.catch(err => {
|
||||
originalRequest.error(err);
|
||||
});
|
||||
}
|
||||
return Promise.reject(error);
|
||||
})
|
||||
.catch(err => {
|
||||
// request failed, token could not be refreshed
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status));
|
||||
}
|
||||
dispatch({
|
||||
type: AUTH_ERROR
|
||||
});
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}
|
||||
}
|
||||
// request status was unequal to 401, no possibility to refresh the token
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
};
|
174
src/actions/projectActions.js
Normal file
174
src/actions/projectActions.js
Normal file
@ -0,0 +1,174 @@
|
||||
import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE, PROJECT_DESCRIPTION } from './types';
|
||||
|
||||
import axios from 'axios';
|
||||
import { workspaceName } from './workspaceActions';
|
||||
import { returnErrors, returnSuccess } from './messageActions';
|
||||
|
||||
export const setType = (type) => (dispatch) => {
|
||||
dispatch({
|
||||
type: PROJECT_TYPE,
|
||||
payload: type
|
||||
});
|
||||
};
|
||||
|
||||
export const setDescription = (description) => (dispatch) => {
|
||||
dispatch({
|
||||
type: PROJECT_DESCRIPTION,
|
||||
payload: description
|
||||
});
|
||||
};
|
||||
|
||||
export const getProject = (type, id) => (dispatch) => {
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
dispatch(setType(type));
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`)
|
||||
.then(res => {
|
||||
var data = type === 'share' ? 'content' : type;
|
||||
var project = res.data[data];
|
||||
if(project){
|
||||
dispatch({
|
||||
type: GET_PROJECT,
|
||||
payload: project
|
||||
});
|
||||
dispatch({
|
||||
type: PROJECT_DESCRIPTION,
|
||||
payload: project.description
|
||||
});
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'GET_PROJECT_SUCCESS'));
|
||||
}
|
||||
else{
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
dispatch(returnErrors(res.data.message, res.status, 'PROJECT_EMPTY'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'GET_PROJECT_FAIL'));
|
||||
}
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
});
|
||||
};
|
||||
|
||||
export const getProjects = (type) => (dispatch) => {
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/${type}`)
|
||||
.then(res => {
|
||||
var data = type === 'project' ? 'projects' : 'galleries';
|
||||
var projects = res.data[data];
|
||||
dispatch({
|
||||
type: GET_PROJECTS,
|
||||
payload: projects
|
||||
});
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
dispatch(returnSuccess(res.data.message, res.status));
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'GET_PROJECTS_FAIL'));
|
||||
}
|
||||
dispatch({type: PROJECT_PROGRESS});
|
||||
});
|
||||
};
|
||||
|
||||
export const updateProject = (type, id) => (dispatch, getState) => {
|
||||
var workspace = getState().workspace;
|
||||
var body = {
|
||||
xml: workspace.code.xml,
|
||||
title: workspace.name
|
||||
};
|
||||
var project = getState().project;
|
||||
if(type==='gallery'){
|
||||
body.description = project.description;
|
||||
}
|
||||
axios.put(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`, body)
|
||||
.then(res => {
|
||||
var project = res.data[type];
|
||||
var projects = getState().project.projects;
|
||||
var index = projects.findIndex(res => res._id === project._id);
|
||||
projects[index] = project;
|
||||
dispatch({
|
||||
type: GET_PROJECTS,
|
||||
payload: projects
|
||||
});
|
||||
if(type === 'project'){
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_UPDATE_SUCCESS'));
|
||||
} else {
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'GALLERY_UPDATE_SUCCESS'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
if(type === 'project'){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'PROJECT_UPDATE_FAIL'));
|
||||
} else {
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'GALLERY_UPDATE_FAIL'));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteProject = (type, id) => (dispatch, getState) => {
|
||||
var project = getState().project;
|
||||
axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/${type}/${id}`)
|
||||
.then(res => {
|
||||
var projects = getState().project.projects;
|
||||
var index = projects.findIndex(res => res._id === id);
|
||||
projects.splice(index, 1)
|
||||
dispatch({
|
||||
type: GET_PROJECTS,
|
||||
payload: projects
|
||||
});
|
||||
if(type === 'project'){
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'PROJECT_DELETE_SUCCESS'));
|
||||
} else {
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'GALLERY_DELETE_SUCCESS'));
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'PROJECT_DELETE_FAIL'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const shareProject = (title, type, id) => (dispatch, getState) => {
|
||||
var body = {
|
||||
title: title
|
||||
};
|
||||
if(type === 'project'){
|
||||
body.projectId = id;
|
||||
} else {
|
||||
body.xml = getState().workspace.code.xml;
|
||||
}
|
||||
axios.post(`${process.env.REACT_APP_BLOCKLY_API}/share`, body)
|
||||
.then(res => {
|
||||
var shareContent = res.data.content;
|
||||
if(body.projectId){
|
||||
var projects = getState().project.projects;
|
||||
var index = projects.findIndex(res => res._id === id);
|
||||
projects[index].shared = shareContent.expiresAt;
|
||||
dispatch({
|
||||
type: GET_PROJECTS,
|
||||
payload: projects
|
||||
});
|
||||
}
|
||||
dispatch(returnSuccess(res.data.message, shareContent._id, 'SHARE_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'SHARE_FAIL'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const resetProject = () => (dispatch) => {
|
||||
dispatch({
|
||||
type: GET_PROJECTS,
|
||||
payload: []
|
||||
});
|
||||
dispatch(setType(''));
|
||||
dispatch(setDescription(''));
|
||||
};
|
@ -1,13 +1,13 @@
|
||||
import { TUTORIAL_PROGRESS, GET_TUTORIAL, GET_TUTORIALS, TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types';
|
||||
import { MYBADGES_DISCONNECT, TUTORIAL_PROGRESS, GET_TUTORIAL, GET_TUTORIALS, TUTORIAL_SUCCESS, TUTORIAL_ERROR, TUTORIAL_CHANGE, TUTORIAL_XML, TUTORIAL_ID, TUTORIAL_STEP } from './types';
|
||||
|
||||
import axios from 'axios';
|
||||
import { returnErrors, returnSuccess } from './messageActions';
|
||||
|
||||
export const getTutorial = (id) => (dispatch, getState) => {
|
||||
dispatch({type: TUTORIAL_PROGRESS});
|
||||
axios.get(`https://api.blockly.sensebox.de/tutorial/${id}`)
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${id}`)
|
||||
.then(res => {
|
||||
var tutorial = res.data;
|
||||
var tutorial = res.data.tutorial;
|
||||
existingTutorial(tutorial, getState().tutorial.status).then(status => {
|
||||
dispatch({
|
||||
type: TUTORIAL_SUCCESS,
|
||||
@ -31,9 +31,10 @@ export const getTutorial = (id) => (dispatch, getState) => {
|
||||
|
||||
export const getTutorials = () => (dispatch, getState) => {
|
||||
dispatch({type: TUTORIAL_PROGRESS});
|
||||
axios.get(`https://api.blockly.sensebox.de/tutorial`)
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/tutorial`)
|
||||
.then(res => {
|
||||
var tutorials = res.data;
|
||||
var tutorials = res.data.tutorials;
|
||||
console.log(tutorials);
|
||||
existingTutorials(tutorials, getState().tutorial.status).then(status => {
|
||||
dispatch({
|
||||
type: TUTORIAL_SUCCESS,
|
||||
@ -55,6 +56,47 @@ export const getTutorials = () => (dispatch, getState) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const assigneBadge = (id) => (dispatch, getState) => {
|
||||
axios.put(`${process.env.REACT_APP_BLOCKLY_API}/user/badge/${id}`)
|
||||
.then(res => {
|
||||
var badge = res.data.badge;
|
||||
var user = getState().auth.user;
|
||||
user.badges.push(badge._id);
|
||||
dispatch({
|
||||
type: MYBADGES_DISCONNECT,
|
||||
payload: user
|
||||
});
|
||||
dispatch(returnSuccess(badge, res.status, 'ASSIGNE_BADGE_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'ASSIGNE_BADGE_FAIL'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteTutorial = (id) => (dispatch, getState) => {
|
||||
var tutorial = getState().tutorial;
|
||||
var id = getState().builder.id;
|
||||
axios.delete(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${id}`)
|
||||
.then(res => {
|
||||
var tutorials = tutorial.tutorials;
|
||||
var index = tutorials.findIndex(res => res._id === id);
|
||||
tutorials.splice(index, 1)
|
||||
dispatch({
|
||||
type: GET_TUTORIALS,
|
||||
payload: tutorials
|
||||
});
|
||||
dispatch(returnSuccess(res.data.message, res.status, 'TUTORIAL_DELETE_SUCCESS'));
|
||||
})
|
||||
.catch(err => {
|
||||
if(err.response){
|
||||
dispatch(returnErrors(err.response.data.message, err.response.status, 'TUTORIAL_DELETE_FAIL'));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
export const resetTutorial = () => (dispatch) => {
|
||||
dispatch({
|
||||
type: GET_TUTORIALS,
|
||||
@ -74,9 +116,9 @@ export const tutorialChange = () => (dispatch) => {
|
||||
|
||||
export const tutorialCheck = (status, step) => (dispatch, getState) => {
|
||||
var tutorialsStatus = getState().tutorial.status;
|
||||
var id = getState().tutorial.tutorials[0].id;
|
||||
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id);
|
||||
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === step.id);
|
||||
var id = getState().tutorial.tutorials[0]._id;
|
||||
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus._id === id);
|
||||
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task._id === step._id);
|
||||
tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = {
|
||||
...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex],
|
||||
type: status
|
||||
@ -86,18 +128,19 @@ export const tutorialCheck = (status, step) => (dispatch, getState) => {
|
||||
payload: tutorialsStatus
|
||||
});
|
||||
dispatch(tutorialChange());
|
||||
dispatch(returnSuccess('','','TUTORIAL_CHECK_SUCCESS'));
|
||||
};
|
||||
|
||||
export const storeTutorialXml = (code) => (dispatch, getState) => {
|
||||
var tutorial = getState().tutorial.tutorials[0];
|
||||
if (tutorial) {
|
||||
var id = tutorial.id;
|
||||
var id = tutorial._id;
|
||||
var activeStep = getState().tutorial.activeStep;
|
||||
var steps = tutorial.steps;
|
||||
if (steps && steps[activeStep].type === 'task') {
|
||||
var tutorialsStatus = getState().tutorial.status;
|
||||
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus.id === id);
|
||||
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task.id === steps[activeStep].id);
|
||||
var tutorialsStatusIndex = tutorialsStatus.findIndex(tutorialStatus => tutorialStatus._id === id);
|
||||
var tasksIndex = tutorialsStatus[tutorialsStatusIndex].tasks.findIndex(task => task._id === steps[activeStep]._id);
|
||||
tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex] = {
|
||||
...tutorialsStatus[tutorialsStatusIndex].tasks[tasksIndex],
|
||||
xml: code
|
||||
@ -125,38 +168,38 @@ const existingTutorials = (tutorials, status) => new Promise(function(resolve, r
|
||||
existingTutorial(tutorial, status).then(status => {
|
||||
newstatus = status;
|
||||
});
|
||||
return tutorial.id;
|
||||
return tutorial._id;
|
||||
});
|
||||
resolve(existingTutorialIds)
|
||||
}).then(existingTutorialIds => {
|
||||
// deleting old tutorials which do not longer exist
|
||||
if (existingTutorialIds.length > 0) {
|
||||
status = newstatus.filter(status => existingTutorialIds.indexOf(status.id) > -1);
|
||||
status = newstatus.filter(status => existingTutorialIds.indexOf(status._id) > -1);
|
||||
}
|
||||
resolve(status);
|
||||
});
|
||||
});
|
||||
|
||||
const existingTutorial = (tutorial, status) => new Promise(function(resolve, reject){
|
||||
var tutorialsId = tutorial.id;
|
||||
var statusIndex = status.findIndex(status => status.id === tutorialsId);
|
||||
var tutorialsId = tutorial._id;
|
||||
var statusIndex = status.findIndex(status => status._id === tutorialsId);
|
||||
if (statusIndex > -1) {
|
||||
var tasks = tutorial.steps.filter(step => step.type === 'task');
|
||||
var existingTaskIds = tasks.map((task, j) => {
|
||||
var tasksId = task.id;
|
||||
if (status[statusIndex].tasks.findIndex(task => task.id === tasksId) === -1) {
|
||||
var tasksId = task._id;
|
||||
if (status[statusIndex].tasks.findIndex(task => task._id === tasksId) === -1) {
|
||||
// task does not exist
|
||||
status[statusIndex].tasks.push({ id: tasksId });
|
||||
status[statusIndex].tasks.push({ _id: tasksId });
|
||||
}
|
||||
return tasksId;
|
||||
});
|
||||
// deleting old tasks which do not longer exist
|
||||
if (existingTaskIds.length > 0) {
|
||||
status[statusIndex].tasks = status[statusIndex].tasks.filter(task => existingTaskIds.indexOf(task.id) > -1);
|
||||
status[statusIndex].tasks = status[statusIndex].tasks.filter(task => existingTaskIds.indexOf(task._id) > -1);
|
||||
}
|
||||
}
|
||||
else {
|
||||
status.push({ id: tutorialsId, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => { return { id: task.id }; }) });
|
||||
status.push({ _id: tutorialsId, tasks: tutorial.steps.filter(step => step.type === 'task').map(task => { return { _id: task._id }; }) });
|
||||
}
|
||||
resolve(status);
|
||||
});
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from './types';
|
||||
import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_BADGE, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP, BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from './types';
|
||||
|
||||
import data from '../data/hardware.json';
|
||||
|
||||
@ -39,6 +39,14 @@ export const tutorialId = (id) => (dispatch) => {
|
||||
dispatch(changeTutorialBuilder());
|
||||
};
|
||||
|
||||
export const tutorialBadge = (badge) => (dispatch) => {
|
||||
dispatch({
|
||||
type: BUILDER_BADGE,
|
||||
payload: badge
|
||||
});
|
||||
dispatch(changeTutorialBuilder());
|
||||
};
|
||||
|
||||
export const addStep = (index) => (dispatch, getState) => {
|
||||
var steps = getState().builder.steps;
|
||||
var step = {
|
||||
@ -180,7 +188,7 @@ export const setSubmitError = () => (dispatch, getState) => {
|
||||
// if(builder.id === undefined || builder.id === ''){
|
||||
// dispatch(setError(undefined, 'id'));
|
||||
// }
|
||||
if (builder.id === undefined || builder.title === '') {
|
||||
if (builder.title === '') {
|
||||
dispatch(setError(undefined, 'title'));
|
||||
}
|
||||
var type = builder.steps.map((step, i) => {
|
||||
@ -189,7 +197,7 @@ export const setSubmitError = () => (dispatch, getState) => {
|
||||
step.id = i + 1;
|
||||
if (i === 0) {
|
||||
if (step.requirements && step.requirements.length > 0) {
|
||||
var requirements = step.requirements.filter(requirement => typeof (requirement) === 'number');
|
||||
var requirements = step.requirements.filter(requirement => /^[0-9a-fA-F]{24}$/.test(requirement));
|
||||
if (requirements.length < step.requirements.length) {
|
||||
dispatch(changeContent(requirements, i, 'requirements'));
|
||||
}
|
||||
@ -243,7 +251,7 @@ export const progress = (inProgress) => (dispatch) => {
|
||||
export const resetTutorial = () => (dispatch, getState) => {
|
||||
dispatch(jsonString(''));
|
||||
dispatch(tutorialTitle(''));
|
||||
dispatch(tutorialId(''));
|
||||
dispatch(tutorialBadge(''));
|
||||
var steps = [
|
||||
{
|
||||
id: 1,
|
||||
@ -274,7 +282,7 @@ export const readJSON = (json) => (dispatch, getState) => {
|
||||
// accept only valid attributes
|
||||
var steps = json.steps.map((step, i) => {
|
||||
var object = {
|
||||
id: step.id,
|
||||
// id: step.id,
|
||||
type: step.type,
|
||||
headline: step.headline,
|
||||
text: step.text
|
||||
@ -298,7 +306,7 @@ export const readJSON = (json) => (dispatch, getState) => {
|
||||
return object;
|
||||
});
|
||||
dispatch(tutorialTitle(json.title));
|
||||
dispatch(tutorialId(json.id));
|
||||
dispatch(tutorialBadge(json.badge));
|
||||
dispatch(tutorialSteps(steps));
|
||||
dispatch(setSubmitError());
|
||||
dispatch(progress(false));
|
||||
|
@ -1,3 +1,16 @@
|
||||
// authentication
|
||||
export const USER_LOADING = 'USER_LOADING';
|
||||
export const USER_LOADED = 'USER_LOADED';
|
||||
export const AUTH_ERROR = 'AUTH_ERROR';
|
||||
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
|
||||
export const LOGIN_FAIL = 'LOGIN_FAIL';
|
||||
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';
|
||||
export const LOGOUT_FAIL = 'LOGOUT_FAIL';
|
||||
export const REFRESH_TOKEN_FAIL = 'REFRESH_TOKEN_FAIL';
|
||||
export const REFRESH_TOKEN_SUCCESS = 'REFRESH_TOKEN_SUCCESS';
|
||||
export const MYBADGES_CONNECT = 'MYBADGES_CONNECT';
|
||||
export const MYBADGES_DISCONNECT = 'MYBADGES_DISCONNECT';
|
||||
|
||||
export const NEW_CODE = 'NEW_CODE';
|
||||
export const CHANGE_WORKSPACE = 'CHANGE_WORKSPACE';
|
||||
export const CREATE_BLOCK = 'CREATE_BLOCK';
|
||||
@ -21,6 +34,7 @@ export const JSON_STRING = 'JSON_STRING';
|
||||
|
||||
export const BUILDER_CHANGE = 'BUILDER_CHANGE';
|
||||
export const BUILDER_TITLE = 'BUILDER_TITLE';
|
||||
export const BUILDER_BADGE = 'BUILDER_BADGE';
|
||||
export const BUILDER_ID = 'BUILDER_ID';
|
||||
export const BUILDER_ADD_STEP = 'BUILDER_ADD_STEP';
|
||||
export const BUILDER_DELETE_STEP = 'BUILDER_DELETE_STEP';
|
||||
@ -37,3 +51,11 @@ export const VISIT = 'VISIT';
|
||||
export const GET_ERRORS = 'GET_ERRORS';
|
||||
export const GET_SUCCESS = 'GET_SUCCESS';
|
||||
export const CLEAR_MESSAGES = 'CLEAR_MESSAGES';
|
||||
|
||||
|
||||
// projects: share, gallery, project
|
||||
export const PROJECT_PROGRESS = 'PROJECT_PROGRESS';
|
||||
export const GET_PROJECT = 'GET_PROJECT';
|
||||
export const GET_PROJECTS = 'GET_PROJECTS';
|
||||
export const PROJECT_TYPE = 'PROJECT_TYPE';
|
||||
export const PROJECT_DESCRIPTION = 'PROJECT_DESCRIPTION';
|
||||
|
34
src/components/Alert.js
Normal file
34
src/components/Alert.js
Normal file
@ -0,0 +1,34 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator';
|
||||
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
const styles = (theme) => ({
|
||||
alert: {
|
||||
marginBottom: '20px',
|
||||
border: `1px solid ${theme.palette.primary.main}`,
|
||||
padding: '10px 20px',
|
||||
borderRadius: '4px',
|
||||
background: fade(theme.palette.primary.main, 0.3),
|
||||
color: 'rgb(70,70,70)'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
export class Alert extends Component {
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div className={this.props.classes.alert}>
|
||||
<Typography>
|
||||
{this.props.children}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default withStyles(styles, { withTheme: true })(Alert);
|
@ -53,7 +53,6 @@ class BlocklySvg extends Component {
|
||||
var css = '<defs><style type="text/css" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[' + cssContent + ']]></style></defs>';
|
||||
var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox();
|
||||
var content = new XMLSerializer().serializeToString(canvas);
|
||||
|
||||
var xml = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="${bbox.width}" height="${bbox.height}" viewBox="${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}">
|
||||
${css}">${content}</svg>`;
|
||||
@ -65,7 +64,7 @@ class BlocklySvg extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div
|
||||
style={{display: 'flex', justifyContent: 'center', transform: 'scale(0.8) translate(0, calc(100% * -0.2 / 2))'}}
|
||||
style={{display: 'inline-flex', justifyContent: 'center', transform: 'scale(0.8) translate(0, calc(100% * -0.2 / 2))'}}
|
||||
dangerouslySetInnerHTML={{ __html: this.state.svg }}
|
||||
/>
|
||||
);
|
||||
|
@ -1,7 +1,14 @@
|
||||
import * as Blockly from 'blockly/core';
|
||||
import { getColour } from '../helpers/colour';
|
||||
|
||||
var apiData = '[{"_id":"5e6073fe57703e001bb99453","createdAt":"2020-03-05T03:37:34.151Z","updatedAt":"2020-10-17T10:49:51.636Z","name":"Vtuzgorodok","currentLocation":{"timestamp":"2020-03-05T03:37:34.146Z","coordinates":[60.658676,56.833041,51],"type":"Point"},"exposure":"outdoor","sensors":[{"title":"PM10","unit":"µg/m³","sensorType":"SDS 011","icon":"osem-cloud","_id":"5e6073fe57703e001bb99458","lastMeasurement":{"value":"3.30","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"PM2.5","unit":"µg/m³","sensorType":"SDS 011","icon":"osem-cloud","_id":"5e6073fe57703e001bb99457","lastMeasurement":{"value":"0.90","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"Temperatur","unit":"°C","sensorType":"BME280","icon":"osem-thermometer","_id":"5e6073fe57703e001bb99456","lastMeasurement":{"value":"6.58","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"rel. Luftfeuchte","unit":"%","sensorType":"BME280","icon":"osem-humidity","_id":"5e6073fe57703e001bb99455","lastMeasurement":{"value":"53.76","createdAt":"2020-10-17T10:49:51.627Z"}},{"title":"Luftdruck","unit":"Pa","sensorType":"BME280","icon":"osem-barometer","_id":"5e6073fe57703e001bb99454","lastMeasurement":{"value":"96937.66","createdAt":"2020-10-17T10:49:51.627Z"}}],"model":"luftdaten_sds011_bme280","lastMeasurementAt":"2020-10-17T10:49:51.627Z","loc":[{"geometry":{"timestamp":"2020-03-05T03:37:34.146Z","coordinates":[60.658676,56.833041,51],"type":"Point"},"type":"Feature"}]}]';
|
||||
import store from '../../../store';
|
||||
|
||||
var boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null;
|
||||
store.subscribe(() => {
|
||||
boxes = store.getState().auth.user ? store.getState().auth.user.boxes : null;
|
||||
});
|
||||
var selectedBox = '';
|
||||
|
||||
|
||||
Blockly.Blocks['sensebox_osem_connection'] = {
|
||||
init: function () {
|
||||
@ -17,10 +24,21 @@ Blockly.Blocks['sensebox_osem_connection'] = {
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField(Blockly.Msg.senseBox_osem_exposure)
|
||||
.appendField(new Blockly.FieldDropdown([[Blockly.Msg.senseBox_osem_stationary, 'Stationary'], [Blockly.Msg.senseBox_osem_mobile, 'Mobile']]), "type");
|
||||
this.appendDummyInput()
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField("senseBox ID")
|
||||
.appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID");
|
||||
if (!boxes) {
|
||||
this.appendDummyInput()
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField("senseBox ID")
|
||||
.appendField(new Blockly.FieldTextInput("senseBox ID"), "BoxID");
|
||||
} else {
|
||||
var dropdown = []
|
||||
for (var i = 0; i < boxes.length; i++) {
|
||||
dropdown.push([boxes[i].name, boxes[i]._id])
|
||||
}
|
||||
this.appendDummyInput()
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField("senseBox ID")
|
||||
.appendField(new Blockly.FieldDropdown(dropdown), 'BoxID');
|
||||
}
|
||||
this.appendDummyInput()
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField(Blockly.Msg.senseBox_osem_access_token)
|
||||
@ -32,14 +50,17 @@ Blockly.Blocks['sensebox_osem_connection'] = {
|
||||
this.setNextStatement(true, null);
|
||||
},
|
||||
onchange: function (e) {
|
||||
let boxID = this.getFieldValue('BoxID');
|
||||
if (boxID !== 'senseBox ID') {
|
||||
fetch('https://api.opensensemap.org/boxes/ ' + boxID)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
apiData = data;
|
||||
})
|
||||
selectedBox = this.getFieldValue('BoxID');
|
||||
console.log(selectedBox)
|
||||
if (selectedBox !== '' && boxes) {
|
||||
var accessToken = boxes.find(element => element._id === selectedBox).access_token
|
||||
if (accessToken !== undefined) {
|
||||
this.getField('access_token').setValue(accessToken)
|
||||
} else {
|
||||
this.getField('access_token').setValue('access_token')
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
mutationToDom: function () {
|
||||
var container = document.createElement('mutation');
|
||||
@ -83,35 +104,42 @@ Blockly.Blocks['sensebox_osem_connection'] = {
|
||||
};
|
||||
Blockly.Blocks['sensebox_send_to_osem'] = {
|
||||
init: function () {
|
||||
|
||||
this.setTooltip(Blockly.Msg.senseBox_send_to_osem_tip);
|
||||
this.setHelpUrl('');
|
||||
this.setColour(getColour().sensebox);
|
||||
this.appendDummyInput()
|
||||
.appendField(Blockly.Msg.senseBox_send_to_osem);
|
||||
this.appendValueInput('Value')
|
||||
.appendField('Phänomen')
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
this.generateOptions), 'SensorID');
|
||||
if (boxes) {
|
||||
this.appendValueInput('Value')
|
||||
.appendField('Phänomen')
|
||||
.appendField(new Blockly.FieldDropdown(
|
||||
this.generateOptions), 'SensorID');
|
||||
} else {
|
||||
this.appendValueInput('Value')
|
||||
.setAlign(Blockly.ALIGN_LEFT)
|
||||
.appendField('Phänomen')
|
||||
.appendField(new Blockly.FieldTextInput(
|
||||
'sensorID'), 'SensorID')
|
||||
}
|
||||
|
||||
this.setPreviousStatement(true, null);
|
||||
this.setNextStatement(true, null);
|
||||
},
|
||||
|
||||
generateOptions: function () {
|
||||
var options = [['', '']];
|
||||
if (apiData.sensors != undefined) {
|
||||
for (var i = 0; i < apiData.sensors.length; i++) {
|
||||
options.push([apiData.sensors[i].title, apiData.sensors[i]._id]);
|
||||
var dropdown = [['', '']];
|
||||
var boxID = selectedBox;
|
||||
if (boxID !== '' && boxes) {
|
||||
|
||||
let box = boxes.find(el => el._id === boxID);
|
||||
if (box !== undefined) {
|
||||
for (var i = 0; i < box.sensors.length; i++) {
|
||||
dropdown.push([box.sensors[i].title, box.sensors[i]._id])
|
||||
}
|
||||
console.log(dropdown)
|
||||
}
|
||||
}
|
||||
if (options.length > 1) {
|
||||
|
||||
var dropdown = options.slice(1)
|
||||
return dropdown;
|
||||
} else
|
||||
return options;
|
||||
|
||||
|
||||
return dropdown
|
||||
},
|
||||
/**
|
||||
* Called whenever anything on the workspace changes.
|
||||
@ -143,4 +171,4 @@ Blockly.Blocks['sensebox_send_to_osem'] = {
|
||||
* Blockly.Blocks['controls_flow_statements'].LOOP_TYPES.push('custom_loop');
|
||||
*/
|
||||
LOOP_TYPES: ['sensebox_osem_connection']
|
||||
};
|
||||
};
|
||||
|
@ -1,118 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import clsx from 'clsx';
|
||||
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { fade } from '@material-ui/core/styles/colorManipulator';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import BlocklyWindow from '../Blockly/BlocklyWindow';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
|
||||
|
||||
const styles = (theme) => ({
|
||||
outerDiv: {
|
||||
position: 'absolute',
|
||||
right: '-30px',
|
||||
bottom: '-30px',
|
||||
width: '160px',
|
||||
height: '160px',
|
||||
color: fade(theme.palette.secondary.main, 0.6)
|
||||
},
|
||||
outerDivError: {
|
||||
stroke: fade(theme.palette.error.dark, 0.6),
|
||||
color: fade(theme.palette.error.dark, 0.6)
|
||||
},
|
||||
outerDivSuccess: {
|
||||
stroke: fade(theme.palette.primary.main, 0.6),
|
||||
color: fade(theme.palette.primary.main, 0.6)
|
||||
},
|
||||
outerDivOther: {
|
||||
stroke: fade(theme.palette.secondary.main, 0.6)
|
||||
},
|
||||
innerDiv: {
|
||||
width: 'inherit',
|
||||
height: 'inherit',
|
||||
display: 'table-cell',
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center'
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
class GalleryHome extends Component {
|
||||
|
||||
state = {
|
||||
gallery: []
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
fetch(process.env.REACT_APP_BLOCKLY_API + this.props.location.pathname)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
this.setState({ gallery: data })
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/gallery', title: 'Gallery' }]} />
|
||||
|
||||
<h1>Gallery</h1>
|
||||
<Grid container spacing={2}>
|
||||
{this.state.gallery.map((gallery, i) => {
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
|
||||
<Link to={`/gallery/${gallery.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Paper style={{ height: '400px', padding: '2rem', position: 'relative', overflow: 'hidden' }}>
|
||||
<h3>{gallery.title}</h3>
|
||||
<Divider />
|
||||
<Grid container spacing={10} style={{ marginBottom: '5px' }}>
|
||||
<Grid item xs={12}>
|
||||
<BlocklyWindow
|
||||
trashcan={false}
|
||||
zoomControls={false}
|
||||
|
||||
blockDisabled
|
||||
blocklyCSS={{ height: '18vH' }}
|
||||
initialXml={gallery.xml}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<p>{gallery.text}</p>
|
||||
<Divider />
|
||||
|
||||
<div className={clsx(this.props.classes.outerDiv)} style={{ width: '160px', height: '160px', border: 0 }}>
|
||||
</div>
|
||||
</Paper>
|
||||
</Link>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
GalleryHome.propTypes = {
|
||||
status: PropTypes.array.isRequired,
|
||||
change: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
change: state.tutorial.change,
|
||||
status: state.tutorial.status
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(GalleryHome));
|
@ -1,37 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 15212,
|
||||
"title": "Das senseBox Buch Kapitel 1",
|
||||
"name": "Mario",
|
||||
"text": "Die Blöcke findest du in der Kategorie \"Schleifen\". Die einfachste Schleife, die du Verwenden kannst, ist der Block \"Wiederhole 10 mal\". Bei diesem Block kannst du die Blöcke, die eine bestimmte Zahl wiederholt werden soll einfach in den offenen Block abschnitt ziehen. ",
|
||||
"xml": "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n <block type=\"controls_repeat_ext\" id=\"!|`dyF$`~*!l~D[TUc4N\" x=\"38\" y=\"32\">\n <value name=\"TIMES\">\n <block type=\"math_number\" id=\"ktgQ[7pD~M{sq;r^kLuz\">\n <field name=\"NUM\">10</field>\n </block>\n </value>\n </block>\n</xml>"
|
||||
},
|
||||
{
|
||||
"id": 25451,
|
||||
"title": "Das senseBox Buch Kapitel 2",
|
||||
"name": "Mario",
|
||||
"text": "",
|
||||
"xml": "<xml xmlns=\"https://developers.google.com/blockly/xml\">\n <block type=\"arduino_functions\" id=\"5KebY,-ltvxB7K^El}1(\" x=\"30\" y=\"34\">\n <statement name=\"LOOP_FUNC\">\n <block type=\"sensebox_led\" id=\"Y(DUYI_.t1V!_9Qf7lgl\">\n <field name=\"PIN\">1</field>\n <field name=\"STAT\">HIGH</field>\n <next>\n <block type=\"time_delay\" id=\"C2.adlos#1/Ns)1@K!Or\">\n <value name=\"DELAY_TIME_MILI\">\n <block type=\"math_number\" id=\")uPzh6_L+SQaIr+v8B,\">\n <field name=\"NUM\">1000</field>\n </block>\n </value>\n <next>\n <block type=\"sensebox_led\" id=\"w215h=etDdQ(Aao86qG2\">\n <field name=\"PIN\">1</field>\n <field name=\"STAT\">LOW</field>\n <next>\n <block type=\"time_delay\" id=\"1pPNxT#ag6fFP|=fy(,%\">\n <value name=\"DELAY_TIME_MILI\">\n <block type=\"math_number\" id=\"%xCj(nr,]@f{ALe^{vH}\">\n <field name=\"NUM\">1000</field>\n </block>\n </value>\n </block>\n </next>\n </block>\n </next>\n </block>\n </next>\n </block>\n </statement>\n </block>\n</xml>"
|
||||
},
|
||||
{
|
||||
"id": 3541512,
|
||||
"title": "Das senseBox Buch Kapitel 3",
|
||||
"name": "Mario",
|
||||
"text": "",
|
||||
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\">\n <block type=\"arduino_functions\" id=\"5KebY,-ltvxB7K^El}1(\" x=\"30\" y=\"34\">\n <statement name=\"SETUP_FUNC\">\n <block type=\"sensebox_display_beginDisplay\" id=\")Z4]CI*ibRqWkqLr^pjb\"></block>\n </statement>\n <statement name=\"LOOP_FUNC\">\n <block type=\"sensebox_display_show\" id=\"r)I#b]xjcH,b@8g*,Pf^\">\n <statement name=\"SHOW\">\n <block type=\"sensebox_display_printDisplay\" id=\"o{6DR6H4f;Qj%n=@XgaW\">\n <field name=\"COLOR\">WHITE,BLACK</field>\n <value name=\"SIZE\">\n <block type=\"math_number\" id=\"u}2t2rIa6Wx2)42.Ij]}\">\n <field name=\"NUM\">1</field>\n </block>\n </value>\n <value name=\"X\">\n <block type=\"math_number\" id=\"6n/}z0xXtJLhHm0E9kF/\">\n <field name=\"NUM\">0</field>\n </block>\n </value>\n <value name=\"Y\">\n <block type=\"math_number\" id=\"Y-NwVU`0,j73ON)ODL^Y\">\n <field name=\"NUM\">0</field>\n </block>\n </value>\n <value name=\"printDisplay\">\n <block type=\"text_join\" id=\"t*`nvkah#B@JrGRge`u[\">\n <mutation items=\"2\"></mutation>\n <value name=\"ADD0\">\n <block type=\"text\" id=\"*n^fD@DZEq^eLPc|CvJ[\">\n <field name=\"TEXT\">Helligkeit:</field>\n </block>\n </value>\n <value name=\"ADD1\">\n <block type=\"sensebox_sensor_uv_light\" id=\"tT8cG:UHho:SUKZQLVG{\">\n <field name=\"NAME\">Illuminance</field>\n </block>\n </value>\n </block>\n </value>\n </block>\n </statement>\n </block>\n </statement>\n </block>\n</xml>"
|
||||
},
|
||||
{
|
||||
"id": 7487454,
|
||||
"title": "Das senseBox Buch Kapitel 4",
|
||||
"name": "Mario",
|
||||
"text": "",
|
||||
"xml": "<xml xmlns=\"http://www.w3.org/1999/xhtml\">\n <block type=\"arduino_functions\" id=\"5KebY,-ltvxB7K^El}1(\" x=\"30\" y=\"34\">\n <statement name=\"SETUP_FUNC\">\n <block type=\"sensebox_display_beginDisplay\" id=\")Z4]CI*ibRqWkqLr^pjb\"></block>\n </statement>\n <statement name=\"LOOP_FUNC\">\n <block type=\"sensebox_display_show\" id=\"r)I#b]xjcH,b@8g*,Pf^\">\n <statement name=\"SHOW\">\n <block type=\"sensebox_display_printDisplay\" id=\"o{6DR6H4f;Qj%n=@XgaW\">\n <field name=\"COLOR\">WHITE,BLACK</field>\n <value name=\"SIZE\">\n <block type=\"math_number\" id=\"u}2t2rIa6Wx2)42.Ij]}\">\n <field name=\"NUM\">1</field>\n </block>\n </value>\n <value name=\"X\">\n <block type=\"math_number\" id=\"6n/}z0xXtJLhHm0E9kF/\">\n <field name=\"NUM\">0</field>\n </block>\n </value>\n <value name=\"Y\">\n <block type=\"math_number\" id=\"Y-NwVU`0,j73ON)ODL^Y\">\n <field name=\"NUM\">0</field>\n </block>\n </value>\n <value name=\"printDisplay\">\n <block type=\"text_join\" id=\"t*`nvkah#B@JrGRge`u[\">\n <mutation items=\"2\"></mutation>\n <value name=\"ADD0\">\n <block type=\"text\" id=\"*n^fD@DZEq^eLPc|CvJ[\">\n <field name=\"TEXT\">Helligkeit:</field>\n </block>\n </value>\n <value name=\"ADD1\">\n <block type=\"sensebox_sensor_uv_light\" id=\"tT8cG:UHho:SUKZQLVG{\">\n <field name=\"NAME\">Illuminance</field>\n </block>\n </value>\n </block>\n </value>\n </block>\n </statement>\n </block>\n </statement>\n </block>\n</xml>"
|
||||
},
|
||||
{
|
||||
"id": 54541251,
|
||||
"title": "Das senseBox Buch Kapitel 5",
|
||||
"name": "Mario",
|
||||
"text": "",
|
||||
"xml": ""
|
||||
}
|
||||
]
|
@ -4,14 +4,15 @@ import { connect } from 'react-redux';
|
||||
import { clearStats, workspaceName } from '../actions/workspaceActions';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
import { createNameId } from 'mnemonic-id';
|
||||
|
||||
import WorkspaceStats from './WorkspaceStats';
|
||||
import WorkspaceFunc from './WorkspaceFunc';
|
||||
import WorkspaceStats from './Workspace/WorkspaceStats';
|
||||
import WorkspaceFunc from './Workspace/WorkspaceFunc';
|
||||
import BlocklyWindow from './Blockly/BlocklyWindow';
|
||||
import CodeViewer from './CodeViewer';
|
||||
import TrashcanButtons from './TrashcanButtons';
|
||||
import { createNameId } from 'mnemonic-id';
|
||||
import TrashcanButtons from './Workspace/TrashcanButtons';
|
||||
import HintTutorialExists from './Tutorial/HintTutorialExists';
|
||||
import Snackbar from './Snackbar';
|
||||
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
@ -47,29 +48,28 @@ class Home extends Component {
|
||||
|
||||
state = {
|
||||
codeOn: false,
|
||||
gallery: [],
|
||||
share: [],
|
||||
projectToLoad: undefined,
|
||||
stats: window.localStorage.getItem('stats'),
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ stats: window.localStorage.getItem('stats') })
|
||||
this.props.workspaceName(createNameId());
|
||||
fetch(process.env.REACT_APP_BLOCKLY_API + this.props.location.pathname)
|
||||
.then(res => res.json())
|
||||
.then((data) => {
|
||||
this.setState({ projectToLoad: data })
|
||||
})
|
||||
this.setState({ stats: window.localStorage.getItem('stats') });
|
||||
if(!this.props.project){
|
||||
this.props.workspaceName(createNameId());
|
||||
}
|
||||
if(this.props.message && this.props.message.id === 'GET_SHARE_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das angefragte geteilte Projekt konnte nicht gefunden werden.`, type: 'error' });
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
componentDidUpdate() {
|
||||
componentDidUpdate(props) {
|
||||
/* Resize and reposition all of the workspace chrome (toolbox, trash,
|
||||
scrollbars etc.) This should be called when something changes that requires
|
||||
recalculating dimensions and positions of the trash, zoom, toolbox, etc.
|
||||
(e.g. window resize). */
|
||||
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
Blockly.svgResize(workspace);
|
||||
}
|
||||
@ -95,7 +95,9 @@ class Home extends Component {
|
||||
<div style={{ float: 'left', height: '40px', position: 'relative' }}><WorkspaceStats /></div>
|
||||
: null
|
||||
}
|
||||
<div style={{ float: 'right', height: '40px', marginBottom: '20px' }}><WorkspaceFunc /></div>
|
||||
<div style={{ float: 'right', height: '40px', marginBottom: '20px' }}>
|
||||
<WorkspaceFunc project={this.props.project} projectType={this.props.projectType}/>
|
||||
</div>
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} md={this.state.codeOn ? 8 : 12} style={{ position: 'relative' }}>
|
||||
<Tooltip title={this.state.codeOn ? 'Code ausblenden' : 'Code anzeigen'} >
|
||||
@ -108,18 +110,24 @@ class Home extends Component {
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<TrashcanButtons />
|
||||
{this.state.projectToLoad ?
|
||||
< BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.state.projectToLoad.xml} /> : < BlocklyWindow blocklyCSS={{ height: '80vH' }} />
|
||||
{this.props.project ?
|
||||
< BlocklyWindow blocklyCSS={{ height: '80vH' }} initialXml={this.props.project.xml} />
|
||||
: < BlocklyWindow blocklyCSS={{ height: '80vH' }} />
|
||||
}
|
||||
|
||||
</Grid>
|
||||
{this.state.codeOn ?
|
||||
<Grid item xs={12} md={4}>
|
||||
<CodeViewer />
|
||||
</Grid>
|
||||
: null}
|
||||
: null}
|
||||
</Grid>
|
||||
<HintTutorialExists />
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -127,8 +135,13 @@ class Home extends Component {
|
||||
|
||||
Home.propTypes = {
|
||||
clearStats: PropTypes.func.isRequired,
|
||||
workspaceName: PropTypes.func.isRequired
|
||||
workspaceName: PropTypes.func.isRequired,
|
||||
message: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.message
|
||||
});
|
||||
|
||||
export default connect(null, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home));
|
||||
|
||||
export default connect(mapStateToProps, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(Home));
|
||||
|
@ -2,6 +2,7 @@ import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { logout } from '../actions/authActions';
|
||||
|
||||
import senseboxLogo from './sensebox_logo.svg';
|
||||
|
||||
@ -20,7 +21,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import LinearProgress from '@material-ui/core/LinearProgress';
|
||||
|
||||
import { faBars, faChevronLeft, faBuilding, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faBars, faChevronLeft, faLayerGroup, faSignInAlt, faSignOutAlt, faCertificate, faUserCircle, faIdCard, faEnvelope, faCog, faChalkboardTeacher, faFolderPlus, faTools, faLightbulb } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
@ -53,7 +54,7 @@ class Navbar extends Component {
|
||||
<div>
|
||||
<AppBar
|
||||
position="relative"
|
||||
style={{ height: '50px', marginBottom: this.props.isLoading ? '0px' : '30px', boxShadow: this.props.isLoading ? 'none' : '0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)' }}
|
||||
style={{ height: '50px', marginBottom: this.props.tutorialIsLoading || this.props.projectIsLoading ? '0px' : '30px', boxShadow: this.props.tutorialIsLoading || this.props.projectIsLoading ? 'none' : '0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)' }}
|
||||
classes={{ root: this.props.classes.appBarColor }}
|
||||
>
|
||||
<Toolbar style={{ height: '50px', minHeight: '50px', padding: 0, color: 'white' }}>
|
||||
@ -99,26 +100,45 @@ class Navbar extends Component {
|
||||
</div>
|
||||
</div>
|
||||
<List>
|
||||
{[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" }, { text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder" }, { text: 'Gallery', icon: faLightbulb, link: "/gallery" }, { text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => (
|
||||
<Link to={item.link} key={index} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem button onClick={this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
))}
|
||||
{[{ text: 'Tutorials', icon: faChalkboardTeacher, link: "/tutorial" },
|
||||
{ text: 'Tutorial-Builder', icon: faTools, link: "/tutorial/builder", restriction: this.props.user && this.props.user.blocklyRole !== 'user' && this.props.isAuthenticated},
|
||||
{ text: 'Galerie', icon: faLightbulb, link: "/gallery" },
|
||||
{ text: 'Projekte', icon: faLayerGroup, link: "/project", restriction: this.props.isAuthenticated }].map((item, index) => {
|
||||
if(item.restriction || Object.keys(item).filter(attribute => attribute === 'restriction').length === 0){
|
||||
return(
|
||||
<Link to={item.link} key={index} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem button onClick={this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
<Divider classes={{ root: this.props.classes.appBarColor }} style={{ marginTop: 'auto' }} />
|
||||
{/* <List>
|
||||
{[{ text: 'Über uns', icon: faBuilding }, { text: 'Kontakt', icon: faEnvelope }, { text: 'Impressum', icon: faIdCard }].map((item, index) => (
|
||||
<ListItem button key={index} onClick={this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
))}
|
||||
</List> */}
|
||||
<List>
|
||||
{[{ text: 'Anmelden', icon: faSignInAlt, link: '/user/login', restriction: !this.props.isAuthenticated },
|
||||
{ text: 'Konto', icon: faUserCircle, link: '/user', restriction: this.props.isAuthenticated },
|
||||
{ text: 'MyBadges', icon: faCertificate, link: '/user/badge', restriction: this.props.isAuthenticated },
|
||||
{ text: 'Abmelden', icon: faSignOutAlt, function: this.props.logout, restriction: this.props.isAuthenticated },
|
||||
{ text: 'Einstellungen', icon: faCog, link: "/settings" }].map((item, index) => {
|
||||
if(item.restriction || Object.keys(item).filter(attribute => attribute === 'restriction').length === 0){
|
||||
return(
|
||||
<Link to={item.link} key={index} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<ListItem button onClick={item.function ? () => {item.function(); this.toggleDrawer();} : this.toggleDrawer}>
|
||||
<ListItemIcon><FontAwesomeIcon icon={item.icon} /></ListItemIcon>
|
||||
<ListItemText primary={item.text} />
|
||||
</ListItem>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</Drawer>
|
||||
{this.props.isLoading ?
|
||||
{this.props.tutorialIsLoading || this.props.projectIsLoading ?
|
||||
<LinearProgress style={{marginBottom: '30px', boxShadow: '0px 2px 4px -1px rgba(0,0,0,0.2),0px 4px 5px 0px rgba(0,0,0,0.14),0px 1px 10px 0px rgba(0,0,0,0.12)'}}/>
|
||||
: null}
|
||||
</div>
|
||||
@ -127,11 +147,17 @@ class Navbar extends Component {
|
||||
}
|
||||
|
||||
Navbar.propTypes = {
|
||||
isLoading: PropTypes.bool.isRequired
|
||||
tutorialIsLoading: PropTypes.bool.isRequired,
|
||||
projectIsLoading: PropTypes.bool.isRequired,
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isLoading: state.tutorial.progress,
|
||||
tutorialIsLoading: state.tutorial.progress,
|
||||
projectIsLoading: state.project.progress,
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Navbar)));
|
||||
export default connect(mapStateToProps, { logout })(withStyles(styles, { withTheme: true })(withRouter(Navbar)));
|
||||
|
103
src/components/Project/Project.js
Normal file
103
src/components/Project/Project.js
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { workspaceName } from '../../actions/workspaceActions';
|
||||
import { getProject, resetProject } from '../../actions/projectActions';
|
||||
import { clearMessages, returnErrors } from '../../actions/messageActions';
|
||||
|
||||
import axios from 'axios';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { createNameId } from 'mnemonic-id';
|
||||
|
||||
import Home from '../Home';
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
|
||||
import Backdrop from '@material-ui/core/Backdrop';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
|
||||
class Project extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.resetProject();
|
||||
this.getProject();
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if(props.location.pathname !== this.props.location.pathname ||
|
||||
props.match.params[`${this.props.type}Id`] !== this.props.match.params[`${this.props.type}Id`]){
|
||||
if(this.props.message.msg){
|
||||
this.props.clearMessages();
|
||||
}
|
||||
this.getProject();
|
||||
}
|
||||
if(this.props.message !== props.message){
|
||||
if(this.props.message.id === 'PROJECT_EMPTY' || this.props.message.id === 'GET_PROJECT_FAIL'){
|
||||
if(this.props.type!=='share'){
|
||||
this.props.returnErrors('', 404, 'GET_PROJECT_FAIL');
|
||||
this.props.history.push(`/${this.props.type}`);
|
||||
} else {
|
||||
this.props.history.push('/');
|
||||
this.props.returnErrors('', 404, 'GET_SHARE_FAIL');
|
||||
}
|
||||
}
|
||||
else if(this.props.message.id === 'GET_PROJECT_SUCCESS'){
|
||||
this.props.workspaceName(this.props.project.title);
|
||||
}
|
||||
else if(this.props.message.id === 'PROJECT_DELETE_SUCCESS' || this.props.message.id === 'GALLERY_DELETE_SUCCESS'){
|
||||
this.props.history.push(`/${this.props.type}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetProject();
|
||||
this.props.workspaceName(null);
|
||||
}
|
||||
|
||||
getProject = () => {
|
||||
var id = this.props.location.pathname.replace(/\/[a-z]{1,}\//,'');
|
||||
var param = this.props.location.pathname.replace(`/${id}`,'').replace('/','');
|
||||
console.log('param', param);
|
||||
console.log(id);
|
||||
this.props.getProject(param, id);
|
||||
}
|
||||
|
||||
render() {
|
||||
var data = this.props.type === 'project' ? 'Projekte' : 'Galerie';
|
||||
return (
|
||||
this.props.progress ?
|
||||
<Backdrop open invisible>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
: this.props.project ?
|
||||
<div>
|
||||
{this.props.type !== 'share' ?
|
||||
<Breadcrumbs content={[{ link: `/${this.props.type}`, title: data },{ link: this.props.location.pathname, title: this.props.project.title }]} />
|
||||
: null}
|
||||
<Home project={this.props.project} projectType={this.props.type}/>
|
||||
</div> : null
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Project.propTypes = {
|
||||
workspaceName: PropTypes.func.isRequired,
|
||||
getProject: PropTypes.func.isRequired,
|
||||
resetProject: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
returnErrors: PropTypes.func.isRequired,
|
||||
project: PropTypes.object.isRequired,
|
||||
type: PropTypes.string.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
progress: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
project: state.project.projects[0],
|
||||
progress: state.project.progress,
|
||||
type: state.project.type,
|
||||
message: state.message
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { workspaceName, getProject, resetProject, clearMessages, returnErrors })(withRouter(Project));
|
164
src/components/Project/ProjectHome.js
Normal file
164
src/components/Project/ProjectHome.js
Normal file
@ -0,0 +1,164 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { getProjects, resetProject } from '../../actions/projectActions';
|
||||
import { clearMessages } from '../../actions/messageActions';
|
||||
|
||||
import axios from 'axios';
|
||||
import { Link, withRouter } from 'react-router-dom';
|
||||
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
import BlocklyWindow from '../Blockly/BlocklyWindow';
|
||||
import Snackbar from '../Snackbar';
|
||||
import WorkspaceFunc from '../Workspace/WorkspaceFunc';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Backdrop from '@material-ui/core/Backdrop';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
|
||||
const styles = (theme) => ({
|
||||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class ProjectHome extends Component {
|
||||
|
||||
state = {
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: ''
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
var type = this.props.location.pathname.replace('/','');
|
||||
this.props.getProjects(type);
|
||||
if(this.props.message){
|
||||
if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Dein Projekt wurde erfolgreich gelöscht.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'GALLERY_DELETE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Dein Galerie-Projekt wurde erfolgreich gelöscht.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'GET_PROJECT_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Dein angefragtes ${type === 'gallery' ? 'Galerie-':''}Projekt konnte nicht gefunden werden.`, type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if(props.location.pathname !== this.props.location.pathname){
|
||||
this.setState({snackbar: false});
|
||||
this.props.getProjects(this.props.location.pathname.replace('/',''));
|
||||
}
|
||||
if(props.message !== this.props.message){
|
||||
if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Dein Projekt wurde erfolgreich gelöscht.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'GALLERY_DELETE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Dein Galerie-Projekt wurde erfolgreich gelöscht.`, type: 'success' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetProject();
|
||||
this.props.clearMessages();
|
||||
}
|
||||
|
||||
render() {
|
||||
var data = this.props.location.pathname === '/project' ? 'Projekte' : 'Galerie';
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: this.props.location.pathname, title: data }]} />
|
||||
|
||||
<h1>{data}</h1>
|
||||
{this.props.progress ?
|
||||
<Backdrop open invisible>
|
||||
<CircularProgress color="primary" />
|
||||
</Backdrop>
|
||||
:
|
||||
<div>
|
||||
{this.props.projects.length > 0 ?
|
||||
<Grid container spacing={2}>
|
||||
{this.props.projects.map((project, i) => {
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} xl={3} key={i}>
|
||||
<Paper style={{ padding: '1rem', position: 'relative', overflow: 'hidden' }}>
|
||||
<Link to={`/${data === 'Projekte' ? 'project' : 'gallery'}/${project._id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<h3 style={{marginTop: 0}}>{project.title}</h3>
|
||||
<Divider style={{marginTop: '1rem', marginBottom: '10px'}}/>
|
||||
<BlocklyWindow
|
||||
svg
|
||||
blockDisabled
|
||||
initialXml={project.xml}
|
||||
/>
|
||||
<Typography variant='body2' style={{fontStyle: 'italic', margin: 0, marginTop: '-10px'}}>{project.description}</Typography>
|
||||
</Link>
|
||||
{this.props.user && this.props.user.email === project.creator ?
|
||||
<div>
|
||||
<Divider style={{marginTop: '10px', marginBottom: '10px'}}/>
|
||||
<div style={{float: 'right'}}>
|
||||
<WorkspaceFunc
|
||||
multiple
|
||||
project={project}
|
||||
projectType={this.props.location.pathname.replace('/','')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
: null}
|
||||
</Paper>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
: <div>
|
||||
<Typography style={{marginBottom: '10px'}}>Es sind aktuell keine Projekte vorhanden.</Typography>
|
||||
{this.props.location.pathname.replace('/','') === 'project' ?
|
||||
<Typography>Erstelle jetzt dein <Link to={'/'} className={this.props.classes.link}>eigenes Projekt</Link> oder lasse dich von Projektbeispielen in der <Link to={'/gallery'} className={this.props.classes.link}>Galerie</Link> inspirieren.</Typography>
|
||||
: null}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ProjectHome.propTypes = {
|
||||
getProjects: PropTypes.func.isRequired,
|
||||
resetProject: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
projects: PropTypes.array.isRequired,
|
||||
progress: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object,
|
||||
message: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
projects: state.project.projects,
|
||||
progress: state.project.progress,
|
||||
user: state.auth.user,
|
||||
message: state.message
|
||||
});
|
||||
|
||||
|
||||
export default connect(mapStateToProps, { getProjects, resetProject, clearMessages })(withStyles(styles, { withTheme: true })(withRouter(ProjectHome)));
|
39
src/components/Route/IsLoggedRoute.js
Normal file
39
src/components/Route/IsLoggedRoute.js
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Route, Redirect } from 'react-router-dom';
|
||||
|
||||
|
||||
class IsLoggedRoute extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Route
|
||||
{...this.props.exact}
|
||||
render={({ location }) =>
|
||||
!this.props.isAuthenticated ? (
|
||||
this.props.children
|
||||
) : (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/",
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
IsLoggedRoute.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(IsLoggedRoute);
|
41
src/components/Route/PrivateRoute.js
Normal file
41
src/components/Route/PrivateRoute.js
Normal file
@ -0,0 +1,41 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Route, Redirect, withRouter } from 'react-router-dom';
|
||||
|
||||
|
||||
class PrivateRoute extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Route
|
||||
{...this.props.exact}
|
||||
render={({ location }) =>
|
||||
this.props.isAuthenticated ? (
|
||||
this.props.children
|
||||
) : (()=>{
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/user/login",
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isAuthenticated: state.auth.isAuthenticated
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withRouter(PrivateRoute));
|
45
src/components/Route/PrivateRouteCreator.js
Normal file
45
src/components/Route/PrivateRouteCreator.js
Normal file
@ -0,0 +1,45 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Route, Redirect, withRouter } from 'react-router-dom';
|
||||
|
||||
|
||||
class PrivateRoute extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Route
|
||||
{...this.props.exact}
|
||||
render={({ location }) =>
|
||||
this.props.isAuthenticated &&
|
||||
this.props.user &&
|
||||
this.props.user.blocklyRole !== 'user' ? (
|
||||
this.props.children
|
||||
) : (()=>{
|
||||
return (
|
||||
<Redirect
|
||||
to={{
|
||||
pathname: "/",
|
||||
state: { from: location }
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
PrivateRoute.propTypes = {
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withRouter(PrivateRoute));
|
83
src/components/Route/Routes.js
Normal file
83
src/components/Route/Routes.js
Normal file
@ -0,0 +1,83 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { visitPage } from '../../actions/generalActions';
|
||||
|
||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||
|
||||
import PrivateRoute from './PrivateRoute';
|
||||
import PrivateRouteCreator from './PrivateRouteCreator';
|
||||
import IsLoggedRoute from './IsLoggedRoute';
|
||||
|
||||
import Home from '../Home';
|
||||
import Tutorial from '../Tutorial/Tutorial';
|
||||
import TutorialHome from '../Tutorial/TutorialHome';
|
||||
import Builder from '../Tutorial/Builder/Builder';
|
||||
import NotFound from '../NotFound';
|
||||
import ProjectHome from '../Project/ProjectHome';
|
||||
import Project from '../Project/Project';
|
||||
import Settings from '../Settings/Settings';
|
||||
import Impressum from '../Impressum';
|
||||
import Privacy from '../Privacy';
|
||||
import Login from '../User/Login';
|
||||
import Account from '../User/Account';
|
||||
import MyBadges from '../User/MyBadges';
|
||||
|
||||
|
||||
class Routes extends Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.visitPage();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ margin: '0 22px' }}>
|
||||
<Switch>
|
||||
<Route path="/" exact component={Home} />
|
||||
{/* Tutorials */}
|
||||
<Route path="/tutorial" exact component={TutorialHome} />
|
||||
<PrivateRouteCreator path="/tutorial/builder" exact>
|
||||
<Builder/>
|
||||
</PrivateRouteCreator>
|
||||
<Route path="/tutorial/:tutorialId" exact component={Tutorial} />
|
||||
{/* Sharing */}
|
||||
<Route path="/share/:shareId" exact component={Project} />
|
||||
{/* Gallery-Projects */}
|
||||
<Route path="/gallery" exact component={ProjectHome} />
|
||||
<Route path="/gallery/:galleryId" exact component={Project} />
|
||||
{/* User-Projects */}
|
||||
<PrivateRoute path="/project" exact>
|
||||
<ProjectHome/>
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path="/project/:projectId" exact>
|
||||
<Project/>
|
||||
</PrivateRoute>
|
||||
{/* User */}
|
||||
<IsLoggedRoute path="/user/login" exact>
|
||||
<Login />
|
||||
</IsLoggedRoute>
|
||||
<PrivateRoute path="/user" exact>
|
||||
<Account />
|
||||
</PrivateRoute>
|
||||
<PrivateRoute path="/user/badge" exact>
|
||||
<MyBadges />
|
||||
</PrivateRoute>
|
||||
{/* settings */}
|
||||
<Route path="/settings" exact component={Settings} />
|
||||
{/* privacy */}
|
||||
<Route path="/impressum" exact component={Impressum} />
|
||||
<Route path="/privacy" exact component={Privacy} />
|
||||
{/* Not Found */}
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Home.propTypes = {
|
||||
visitPage: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { visitPage })(withRouter(Routes));
|
@ -1,50 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { visitPage } from '../actions/generalActions';
|
||||
|
||||
import { Route, Switch, withRouter } from 'react-router-dom';
|
||||
|
||||
import Home from './Home';
|
||||
import Tutorial from './Tutorial/Tutorial';
|
||||
import TutorialHome from './Tutorial/TutorialHome';
|
||||
import Builder from './Tutorial/Builder/Builder';
|
||||
import NotFound from './NotFound';
|
||||
import GalleryHome from './Gallery/GalleryHome';
|
||||
import Settings from './Settings/Settings';
|
||||
import Impressum from './Impressum';
|
||||
import Privacy from './Privacy';
|
||||
|
||||
|
||||
class Routes extends Component {
|
||||
|
||||
componentDidUpdate() {
|
||||
this.props.visitPage();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ margin: '0 22px' }}>
|
||||
<Switch>
|
||||
<Route path="/" exact component={Home} />
|
||||
<Route path="/tutorial" exact component={TutorialHome} />
|
||||
<Route path="/settings" exact component={Settings} />
|
||||
<Route path="/gallery" exact component={GalleryHome} />
|
||||
<Route path="/gallery/:galleryId" exact component={Home} />
|
||||
<Route path="/impressum" exact component={Impressum} />
|
||||
<Route path="/privacy" exact component={Privacy} />
|
||||
<Route path="/share/:shareId" exact component={Home} />
|
||||
<Route path="/tutorial/builder" exact component={Builder} />
|
||||
<Route path="/tutorial/:tutorialId" exact component={Tutorial} />
|
||||
<Route component={NotFound} />
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Home.propTypes = {
|
||||
visitPage: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { visitPage })(withRouter(Routes));
|
@ -35,6 +35,12 @@ class Snackbar extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(){
|
||||
if(!this.state.open){
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
if(this.state.open){
|
||||
clearTimeout(this.timeout);
|
||||
|
@ -5,7 +5,7 @@ import { workspaceName } from '../../actions/workspaceActions';
|
||||
|
||||
import BlocklyWindow from '../Blockly/BlocklyWindow';
|
||||
import CodeViewer from '../CodeViewer';
|
||||
import WorkspaceFunc from '../WorkspaceFunc';
|
||||
import WorkspaceFunc from '../Workspace/WorkspaceFunc';
|
||||
|
||||
import withWidth, { isWidthDown } from '@material-ui/core/withWidth';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
@ -25,10 +25,10 @@ class Assessment extends Component {
|
||||
}
|
||||
|
||||
render() {
|
||||
var tutorialId = this.props.tutorial.id;
|
||||
var tutorialId = this.props.tutorial._id;
|
||||
var currentTask = this.props.step;
|
||||
var status = this.props.status.filter(status => status.id === tutorialId)[0];
|
||||
var taskIndex = status.tasks.findIndex(task => task.id === currentTask.id);
|
||||
var status = this.props.status.filter(status => status._id === tutorialId)[0];
|
||||
var taskIndex = status.tasks.findIndex(task => task._id === currentTask._id);
|
||||
var statusTask = status.tasks[taskIndex];
|
||||
|
||||
return (
|
||||
|
118
src/components/Tutorial/Badge.js
Normal file
118
src/components/Tutorial/Badge.js
Normal file
@ -0,0 +1,118 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { assigneBadge } from '../../actions/tutorialActions';
|
||||
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
|
||||
const styles = (theme) => ({
|
||||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class Badge extends Component {
|
||||
|
||||
state = {
|
||||
open: false,
|
||||
title: '',
|
||||
content: ''
|
||||
};
|
||||
|
||||
componentDidUpdate(props){
|
||||
if(this.props.message.id === 'TUTORIAL_CHECK_SUCCESS'){
|
||||
if(this.props.tutorial.badge){
|
||||
// is connected to MyBadges?
|
||||
if(this.props.isAuthenticated && this.props.user && this.props.user.badge){
|
||||
if(this.props.user.badges && !this.props.user.badges.includes(this.props.tutorial.badge)){
|
||||
if(this.isSuccess()){
|
||||
this.props.assigneBadge(this.props.tutorial.badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if(props.message !== this.props.message){
|
||||
if(this.props.message.id === 'ASSIGNE_BADGE_SUCCESS'){
|
||||
this.setState({title: `Badge: ${this.props.message.msg.name}`, content: `Herzlichen Glückwunsch! Du hast den Badge ${this.props.message.msg.name} erhalten.`, open: true});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isSuccess = () => {
|
||||
var tutorialId = this.props.tutorial._id;
|
||||
var status = this.props.status.filter(status => status._id === tutorialId)[0];
|
||||
var tasks = status.tasks;
|
||||
var success = tasks.filter(task => task.type === 'success').length / tasks.length;
|
||||
if(success===1){
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, title: '', content: '' });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Dialog
|
||||
style={{ zIndex: 99999999 }}
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={() => {this.toggleDialog();}}
|
||||
onClick={() => {this.toggleDialog();}}
|
||||
button={'Schließen'}
|
||||
>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<Paper style={{textAlign: 'center'}}>
|
||||
{this.props.message.msg.image && this.props.message.msg.image.path ?
|
||||
<Avatar src={`${process.env.REACT_APP_MYBADGES}/media/${this.props.message.msg.image.path}`} style={{width: '200px', height: '200px', marginLeft: 'auto', marginRight: 'auto'}}/>
|
||||
: <Avatar style={{width: '200px', height: '200px', marginLeft: 'auto', marginRight: 'auto'}}></Avatar>}
|
||||
<Typography variant='h6' style={{display: 'flex', cursor: 'default', paddingBottom: '6px'}}>
|
||||
<div style={{flexGrow:1, marginLeft: '10px', marginRight: '10px'}}>{this.props.message.msg.name}</div>
|
||||
</Typography>
|
||||
</Paper>
|
||||
<Typography style={{marginTop: '10px'}}>
|
||||
Eine Übersicht über alle erhaltenen Badges im Kontext Blockly for senseBox findest du <Link to={'/user/badge'} className={this.props.classes.link}>hier</Link>.
|
||||
</Typography>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Badge.propTypes = {
|
||||
assigneBadge: PropTypes.func.isRequired,
|
||||
status: PropTypes.array.isRequired,
|
||||
change: PropTypes.number.isRequired,
|
||||
tutorial: PropTypes.object.isRequired,
|
||||
user: PropTypes.object,
|
||||
isAuthenticated: PropTypes.bool.isRequired,
|
||||
message: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
change: state.tutorial.change,
|
||||
status: state.tutorial.status,
|
||||
tutorial: state.tutorial.tutorials[0],
|
||||
user: state.auth.user,
|
||||
isAuthenticated: state.auth.isAuthenticated,
|
||||
message: state.message
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { assigneBadge })(withStyles(styles, { withTheme: true })(Badge));
|
@ -1,10 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { checkError, readJSON, jsonString, progress, resetTutorial } from '../../../actions/tutorialBuilderActions';
|
||||
import { checkError, readJSON, jsonString, progress, tutorialId, resetTutorial as resetTutorialBuilder} from '../../../actions/tutorialBuilderActions';
|
||||
import { getTutorials, resetTutorial, deleteTutorial } from '../../../actions/tutorialActions';
|
||||
import { clearMessages } from '../../../actions/messageActions';
|
||||
|
||||
import axios from 'axios';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../../helpers/whitespace';
|
||||
|
||||
import Breadcrumbs from '../../Breadcrumbs';
|
||||
@ -19,6 +23,13 @@ import Backdrop from '@material-ui/core/Backdrop';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Radio from '@material-ui/core/Radio';
|
||||
import RadioGroup from '@material-ui/core/RadioGroup';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import InputLabel from '@material-ui/core/InputLabel';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
import FormControl from '@material-ui/core/FormControl';
|
||||
import Select from '@material-ui/core/Select';
|
||||
|
||||
const styles = (theme) => ({
|
||||
backdrop: {
|
||||
@ -27,14 +38,24 @@ const styles = (theme) => ({
|
||||
},
|
||||
errorColor: {
|
||||
color: theme.palette.error.dark
|
||||
},
|
||||
errorButton: {
|
||||
marginTop: '5px',
|
||||
height: '40px',
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
'&:hover':{
|
||||
backgroundColor: theme.palette.error.dark
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class Builder extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
tutorial: 'new',
|
||||
open: false,
|
||||
title: '',
|
||||
content: '',
|
||||
@ -46,44 +67,32 @@ class Builder extends Component {
|
||||
this.inputRef = React.createRef();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.reset();
|
||||
componentDidMount() {
|
||||
this.props.getTutorials();
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
if (this.props.id === null) {
|
||||
var randomID = Date.now();
|
||||
} else {
|
||||
randomID = this.props.id;
|
||||
}
|
||||
|
||||
var isError = this.props.checkError();
|
||||
if (isError) {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
else {
|
||||
// export steps without attribute 'url'
|
||||
var steps = this.props.steps.map(step => {
|
||||
if (step.url) {
|
||||
delete step.url;
|
||||
}
|
||||
return step;
|
||||
});
|
||||
var tutorial = {
|
||||
id: randomID,
|
||||
title: this.props.title,
|
||||
steps: steps
|
||||
componentDidUpdate(props, state) {
|
||||
if(props.message !== this.props.message){
|
||||
if(this.props.message.id === 'GET_TUTORIALS_FAIL'){
|
||||
// alert(this.props.message.msg);
|
||||
this.props.clearMessages();
|
||||
}
|
||||
else if(this.props.message.id === 'TUTORIAL_DELETE_SUCCESS'){
|
||||
this.onChange('new');
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich gelöscht.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'TUTORIAL_DELETE_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Tutorials. Versuche es noch einmal.`, type: 'error' });
|
||||
}
|
||||
var blob = new Blob([JSON.stringify(tutorial)], { type: 'text/json' });
|
||||
saveAs(blob, `${detectWhitespacesAndReturnReadableResult(tutorial.title)}.json`);
|
||||
}
|
||||
}
|
||||
|
||||
reset = () => {
|
||||
componentWillUnmount() {
|
||||
this.resetFull();
|
||||
this.props.resetTutorial();
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich zurückgesetzt.`, type: 'success' });
|
||||
window.scrollTo(0, 0);
|
||||
if(this.props.message.msg){
|
||||
this.props.clearMessages();
|
||||
}
|
||||
}
|
||||
|
||||
uploadJsonFile = (jsonFile) => {
|
||||
@ -131,49 +140,218 @@ class Builder extends Component {
|
||||
this.setState({ open: !this.state });
|
||||
}
|
||||
|
||||
onChange = (value) => {
|
||||
this.props.resetTutorialBuilder();
|
||||
this.props.tutorialId('');
|
||||
this.setState({tutorial: value});
|
||||
}
|
||||
|
||||
onChangeId = (value) => {
|
||||
this.props.tutorialId(value);
|
||||
if(this.state.tutorial === 'change'){
|
||||
this.props.progress(true);
|
||||
var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === value)[0];
|
||||
this.props.readJSON(tutorial);
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das ausgewählte Tutorial "${tutorial.title}" wurde erfolgreich übernommen.`, type: 'success' });
|
||||
}
|
||||
}
|
||||
|
||||
resetFull = () => {
|
||||
this.props.resetTutorialBuilder();
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial wurde erfolgreich zurückgesetzt.`, type: 'success' });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
resetTutorial = () => {
|
||||
var tutorial = this.props.tutorials.filter(tutorial => tutorial._id === this.props.id)[0];
|
||||
this.props.readJSON(tutorial);
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Tutorial ${tutorial.title} wurde erfolgreich auf den ursprünglichen Stand zurückgesetzt.`, type: 'success' });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
submit = () => {
|
||||
var isError = this.props.checkError();
|
||||
if (isError) {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Die Angaben für das Tutorial sind nicht vollständig.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
// export steps without attribute 'url'
|
||||
var steps = this.props.steps;
|
||||
var newTutorial = new FormData();
|
||||
newTutorial.append('title', this.props.title);
|
||||
newTutorial.append('badge', this.props.badge);
|
||||
steps.forEach((step, i) => {
|
||||
newTutorial.append(`steps[${i}][type]`, step.type);
|
||||
newTutorial.append(`steps[${i}][headline]`, step.headline);
|
||||
newTutorial.append(`steps[${i}][text]`, step.text);
|
||||
if(i === 0 && step.type === 'instruction'){
|
||||
if(step.requirements){ // optional
|
||||
step.requirements.forEach((requirement, j) => {
|
||||
newTutorial.append(`steps[${i}][requirements][${j}]`, requirement);
|
||||
});
|
||||
}
|
||||
step.hardware.forEach((hardware, j) => {
|
||||
newTutorial.append(`steps[${i}][hardware][${j}]`, hardware);
|
||||
});
|
||||
}
|
||||
if(step.xml){ // optional
|
||||
newTutorial.append(`steps[${i}][xml]`, step.xml);
|
||||
}
|
||||
if(step.media){ // optional
|
||||
if(step.media.youtube){
|
||||
newTutorial.append(`steps[${i}][media][youtube]`, step.media.youtube);
|
||||
}
|
||||
if(step.media.picture){
|
||||
newTutorial.append(`steps[${i}][media][picture]`, step.media.picture);
|
||||
}
|
||||
}
|
||||
});
|
||||
return newTutorial;
|
||||
}
|
||||
}
|
||||
|
||||
submitNew = () => {
|
||||
var newTutorial = this.submit();
|
||||
if(newTutorial){
|
||||
axios.post(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/`, newTutorial)
|
||||
.then(res => {
|
||||
var tutorial = res.data.tutorial;
|
||||
this.props.history.push(`/tutorial/${tutorial._id}`);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen des Tutorials. Versuche es noch einmal.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
submitUpdate = () => {
|
||||
var updatedTutorial = this.submit();
|
||||
if(updatedTutorial){
|
||||
axios.put(`${process.env.REACT_APP_BLOCKLY_API}/tutorial/${this.props.id}`, updatedTutorial)
|
||||
.then(res => {
|
||||
var tutorial = res.data.tutorial;
|
||||
this.props.history.push(`/tutorial/${tutorial._id}`);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Ändern des Tutorials. Versuche es noch einmal.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
var filteredTutorials = this.props.tutorials.filter(tutorial => tutorial.creator === this.props.user.email);
|
||||
return (
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/tutorial', title: 'Tutorial' }, { link: '/tutorial/builder', title: 'Builder' }]} />
|
||||
|
||||
<h1>Tutorial-Builder</h1>
|
||||
|
||||
{/*upload JSON*/}
|
||||
<div ref={this.inputRef}>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
accept="application/json"
|
||||
onChange={(e) => { this.uploadJsonFile(e.target.files[0]) }}
|
||||
id="open-json"
|
||||
type="file"
|
||||
<RadioGroup row value={this.state.tutorial} onChange={(e) => this.onChange(e.target.value)}>
|
||||
<FormControlLabel style={{color: 'black'}}
|
||||
value="new"
|
||||
control={<Radio color="primary" />}
|
||||
label="neues Tutorial erstellen"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
<label htmlFor="open-json">
|
||||
<Button component="span" style={{ marginRight: '10px', marginBottom: '10px' }} variant='contained' color='primary'>Datei laden</Button>
|
||||
</label>
|
||||
<Button style={{ marginRight: '10px', marginBottom: '10px' }} variant='contained' color='primary' onClick={() => this.uploadJsonString()}>String laden</Button>
|
||||
</div>
|
||||
{filteredTutorials.length > 0 ?
|
||||
<div>
|
||||
<FormControlLabel style={{color: 'black'}}
|
||||
disabled={this.props.index === 0}
|
||||
value="change"
|
||||
control={<Radio color="primary" />}
|
||||
label="bestehendes Tutorial ändern"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
<FormControlLabel style={{color: 'black'}}
|
||||
disabled={this.props.index === 0}
|
||||
value="delete"
|
||||
control={<Radio color="primary" />}
|
||||
label="bestehendes Tutorial löschen"
|
||||
labelPlacement="end"
|
||||
/>
|
||||
</div>
|
||||
: null}
|
||||
</RadioGroup>
|
||||
|
||||
<Divider variant='fullWidth' style={{ margin: '10px 0 15px 0' }} />
|
||||
|
||||
{/*Tutorial-Builder-Form*/}
|
||||
{this.props.error.type ?
|
||||
<FormHelperText style={{ lineHeight: 'initial' }} className={this.props.classes.errorColor}>{`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`}</FormHelperText>
|
||||
: null}
|
||||
{/* <Id error={this.props.error.id} value={this.props.id} /> */}
|
||||
<Textfield value={this.props.title} property={'title'} label={'Titel'} error={this.props.error.title} />
|
||||
{this.state.tutorial === 'new' ?
|
||||
/*upload JSON*/
|
||||
<div ref={this.inputRef}>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
accept="application/json"
|
||||
onChange={(e) => { this.uploadJsonFile(e.target.files[0]) }}
|
||||
id="open-json"
|
||||
type="file"
|
||||
/>
|
||||
<label htmlFor="open-json">
|
||||
<Button component="span" style={{ marginRight: '10px', marginBottom: '10px' }} variant='contained' color='primary'>Datei laden</Button>
|
||||
</label>
|
||||
<Button style={{ marginRight: '10px', marginBottom: '10px' }} variant='contained' color='primary' onClick={() => this.uploadJsonString()}>String laden</Button>
|
||||
</div>
|
||||
: <FormControl variant="outlined" style={{width: '100%'}}>
|
||||
<InputLabel id="select-outlined-label">Tutorial</InputLabel>
|
||||
<Select
|
||||
color='primary'
|
||||
labelId="select-outlined-label"
|
||||
value={this.props.id}
|
||||
onChange={(e) => this.onChangeId(e.target.value)}
|
||||
label="Tutorial"
|
||||
>
|
||||
{filteredTutorials.map(tutorial =>
|
||||
<MenuItem value={tutorial._id}>{tutorial.title}</MenuItem>
|
||||
)}
|
||||
</Select>
|
||||
</FormControl>
|
||||
}
|
||||
|
||||
{this.props.steps.map((step, i) =>
|
||||
<Step step={step} index={i} key={i} />
|
||||
)}
|
||||
<Divider variant='fullWidth' style={{ margin: '10px 0 15px 0' }} />
|
||||
|
||||
{/*submit or reset*/}
|
||||
<Divider variant='fullWidth' style={{ margin: '30px 0 10px 0' }} />
|
||||
<Button style={{ marginRight: '10px', marginTop: '10px' }} variant='contained' color='primary' onClick={() => this.submit()}>Tutorial-Vorlage erstellen</Button>
|
||||
<Button style={{ marginTop: '10px' }} variant='contained' onClick={() => this.reset()}>Zurücksetzen</Button>
|
||||
{this.state.tutorial === 'new' || (this.state.tutorial === 'change' && this.props.id !== '') ?
|
||||
/*Tutorial-Builder-Form*/
|
||||
<div>
|
||||
{this.props.error.type ?
|
||||
<FormHelperText style={{ lineHeight: 'initial' }} className={this.props.classes.errorColor}>{`Ein Tutorial muss mindestens jeweils eine Instruktion und eine Aufgabe enthalten.`}</FormHelperText>
|
||||
: null}
|
||||
{/* <Id error={this.props.error.id} value={this.props.id} /> */}
|
||||
<Textfield value={this.props.title} property={'title'} label={'Titel'} error={this.props.error.title} />
|
||||
<Textfield value={this.props.badge} property={'badge'} label={'Badge'} />
|
||||
|
||||
<Backdrop className={this.props.classes.backdrop} open={this.props.isProgress}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
{this.props.steps.map((step, i) =>
|
||||
<Step step={step} index={i} key={i} />
|
||||
)}
|
||||
|
||||
{/*submit or reset*/}
|
||||
<Divider variant='fullWidth' style={{ margin: '30px 0 10px 0' }} />
|
||||
{this.state.tutorial === 'new' ?
|
||||
<div>
|
||||
<Button style={{ marginRight: '10px', marginTop: '10px' }} variant='contained' color='primary' onClick={() => this.submitNew()}>Tutorial erstellen</Button>
|
||||
<Button style={{ marginTop: '10px' }} variant='contained' onClick={() => this.resetFull()}>Zurücksetzen</Button>
|
||||
</div>
|
||||
: <div>
|
||||
<Button style={{ marginRight: '10px', marginTop: '10px' }} variant='contained' color='primary' onClick={() => this.submitUpdate()}>Tutorial ändern</Button>
|
||||
<Button style={{ marginTop: '10px' }} variant='contained' onClick={() => this.resetTutorial()}>Zurücksetzen</Button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<Backdrop className={this.props.classes.backdrop} open={this.props.isProgress}>
|
||||
<CircularProgress color="inherit" />
|
||||
</Backdrop>
|
||||
</div>
|
||||
: null}
|
||||
|
||||
{this.state.tutorial === 'delete' && this.props.id !== '' ?
|
||||
<Button
|
||||
className={this.props.classes.errorButton}
|
||||
variant='contained'
|
||||
color='primary'
|
||||
onClick={() => this.props.deleteTutorial()}>Tutorial löschen</Button>
|
||||
: null}
|
||||
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
@ -211,27 +389,42 @@ class Builder extends Component {
|
||||
}
|
||||
|
||||
Builder.propTypes = {
|
||||
getTutorials: PropTypes.func.isRequired,
|
||||
resetTutorial: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
tutorialId: PropTypes.func.isRequired,
|
||||
checkError: PropTypes.func.isRequired,
|
||||
readJSON: PropTypes.func.isRequired,
|
||||
jsonString: PropTypes.func.isRequired,
|
||||
progress: PropTypes.func.isRequired,
|
||||
resetTutorial: PropTypes.func.isRequired,
|
||||
deleteTutorial: PropTypes.func.isRequired,
|
||||
resetTutorialBuilder: PropTypes.func.isRequired,
|
||||
title: PropTypes.string.isRequired,
|
||||
badge: PropTypes.string.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
steps: PropTypes.array.isRequired,
|
||||
change: PropTypes.number.isRequired,
|
||||
error: PropTypes.object.isRequired,
|
||||
json: PropTypes.string.isRequired,
|
||||
isProgress: PropTypes.bool.isRequired
|
||||
badge: PropTypes.string.isRequired,
|
||||
isProgress: PropTypes.bool.isRequired,
|
||||
tutorials: PropTypes.array.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
title: state.builder.title,
|
||||
badge: state.builder.badge,
|
||||
id: state.builder.id,
|
||||
steps: state.builder.steps,
|
||||
change: state.builder.change,
|
||||
error: state.builder.error,
|
||||
json: state.builder.json,
|
||||
isProgress: state.builder.progress
|
||||
isProgress: state.builder.progress,
|
||||
tutorials: state.tutorial.tutorials,
|
||||
message: state.message,
|
||||
user: state.auth.user,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, resetTutorial })(withStyles(styles, { withTheme: true })(Builder));
|
||||
export default connect(mapStateToProps, { checkError, readJSON, jsonString, progress, tutorialId, resetTutorialBuilder, getTutorials, resetTutorial, clearMessages, deleteTutorial })(withStyles(styles, { withTheme: true })(withRouter(Builder)));
|
||||
|
@ -84,7 +84,7 @@ class Media extends Component {
|
||||
this.setState({ error: false });
|
||||
this.props.changeContent(URL.createObjectURL(pic), this.props.index, 'url');
|
||||
}
|
||||
this.props.changeContent(pic.name, this.props.index, 'media', 'picture');
|
||||
this.props.changeContent(pic, this.props.index, 'media', 'picture');
|
||||
}
|
||||
|
||||
render() {
|
||||
@ -121,8 +121,7 @@ class Media extends Component {
|
||||
<div>
|
||||
{!this.props.error ?
|
||||
<div>
|
||||
<FormHelperText style={{lineHeight: 'initial', marginBottom: '10px'}}>{`Beachte, dass das Foto zusätzlich in den Ordner public/media/tutorial unter dem Namen '${this.props.picture}' abgespeichert werden muss.`}</FormHelperText>
|
||||
<img src={this.props.url ? this.props.url : `/media/tutorial/${this.props.picture}`} alt={this.props.url ? '' : `Das Bild '${this.props.picture}' konnte nicht im Ordner public/media/tutorial gefunden werden und kann daher nicht angezeigt werden.`} style={{maxHeight: '180px', maxWidth: '360px', marginBottom: '5px'}}/>
|
||||
<img src={this.props.url ? this.props.url : this.props.picture ? `${process.env.REACT_APP_BLOCKLY_API}/media/${this.props.picture.path}` : ''} alt={''} style={{maxHeight: '180px', maxWidth: '360px', marginBottom: '5px'}}/>
|
||||
</div>
|
||||
: <div
|
||||
style={{height: '150px', maxWidth: '250px', marginBottom: '5px', justifyContent: "center", alignItems: "center", display:"flex", padding: '20px'}}
|
||||
|
@ -14,27 +14,9 @@ import FormControl from '@material-ui/core/FormControl';
|
||||
|
||||
class Requirements extends Component {
|
||||
|
||||
componentDidMount() {
|
||||
this.props.getTutorials();
|
||||
}
|
||||
|
||||
componentDidUpdate(props, state) {
|
||||
if(this.props.message.id === 'GET_TUTORIALS_FAIL'){
|
||||
alert(this.props.message.msg);
|
||||
this.props.clearMessages();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.resetTutorial();
|
||||
if(this.props.message.msg){
|
||||
this.props.clearMessages();
|
||||
}
|
||||
}
|
||||
|
||||
onChange = (e) => {
|
||||
var requirements = this.props.value;
|
||||
var value = parseInt(e.target.value)
|
||||
var value = e.target.value;
|
||||
if (e.target.checked) {
|
||||
requirements.push(value);
|
||||
}
|
||||
@ -50,13 +32,13 @@ class Requirements extends Component {
|
||||
<FormLabel style={{ color: 'black' }}>Voraussetzungen</FormLabel>
|
||||
<FormHelperText style={{ marginTop: '5px' }}>Beachte, dass die Reihenfolge des Anhakens maßgebend ist.</FormHelperText>
|
||||
<FormGroup>
|
||||
{this.props.tutorials.map((tutorial, i) =>
|
||||
{this.props.tutorials.filter(tutorial => tutorial._id !== this.props.id).map((tutorial, i) =>
|
||||
<FormControlLabel
|
||||
key={i}
|
||||
control={
|
||||
<Checkbox
|
||||
value={tutorial.id}
|
||||
checked={this.props.value.filter(id => id === tutorial.id).length > 0}
|
||||
value={tutorial._id}
|
||||
checked={this.props.value.filter(id => id === tutorial._id).length > 0}
|
||||
onChange={(e) => this.onChange(e)}
|
||||
name="requirements"
|
||||
color="primary"
|
||||
@ -72,16 +54,15 @@ class Requirements extends Component {
|
||||
}
|
||||
|
||||
Requirements.propTypes = {
|
||||
getTutorials: PropTypes.func.isRequired,
|
||||
resetTutorial: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
changeContent: PropTypes.func.isRequired
|
||||
changeContent: PropTypes.func.isRequired,
|
||||
id: PropTypes.string.isRequired,
|
||||
tutorials: PropTypes.array.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
change: state.builder.change,
|
||||
tutorials: state.tutorial.tutorials,
|
||||
message: state.message
|
||||
id: state.builder.id,
|
||||
tutorials: state.tutorial.tutorials
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { changeContent, getTutorials, resetTutorial, clearMessages })(Requirements);
|
||||
export default connect(mapStateToProps, { changeContent })(Requirements);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { tutorialTitle, jsonString, changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions';
|
||||
import { tutorialTitle, tutorialBadge, jsonString, changeContent, setError, deleteError } from '../../../actions/tutorialBuilderActions';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import OutlinedInput from '@material-ui/core/OutlinedInput';
|
||||
@ -42,6 +42,9 @@ class Textfield extends Component {
|
||||
else if(this.props.property === 'json'){
|
||||
this.props.jsonString(value);
|
||||
}
|
||||
else if(this.props.property === 'badge'){
|
||||
this.props.tutorialBadge(value);
|
||||
}
|
||||
else {
|
||||
this.props.changeContent(value, this.props.index, this.props.property, this.props.property2);
|
||||
}
|
||||
@ -86,8 +89,9 @@ class Textfield extends Component {
|
||||
|
||||
Textfield.propTypes = {
|
||||
tutorialTitle: PropTypes.func.isRequired,
|
||||
tutorialBadge: PropTypes.func.isRequired,
|
||||
jsonString: PropTypes.func.isRequired,
|
||||
changeContent: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default connect(null, { tutorialTitle, jsonString, changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(Textfield));
|
||||
export default connect(null, { tutorialTitle, tutorialBadge, jsonString, changeContent, setError, deleteError })(withStyles(styles, { withTheme: true })(Textfield));
|
||||
|
@ -22,11 +22,11 @@ class Instruction extends Component {
|
||||
{isHardware ?
|
||||
<Hardware picture={step.hardware} /> : null}
|
||||
{areRequirements > 0 ?
|
||||
<Requirement tutorialIds={step.requirements} /> : null}
|
||||
<Requirement requirements={step.requirements} /> : null}
|
||||
{step.media ?
|
||||
step.media.picture ?
|
||||
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '5px' }}>
|
||||
<img src={`/media/tutorial/${step.media.picture}`} alt='' style={{ maxHeight: '40vH', maxWidth: '100%' }} />
|
||||
<img src={`${process.env.REACT_APP_BLOCKLY_API}/media/${step.media.picture.path}`} alt='' style={{ maxHeight: '40vH', maxWidth: '100%' }} />
|
||||
</div>
|
||||
: step.media.youtube ?
|
||||
/*16:9; width: 800px; height: width/16*9=450px*/
|
||||
@ -39,7 +39,7 @@ class Instruction extends Component {
|
||||
: null}
|
||||
{step.xml ?
|
||||
<Grid container spacing={2} style={{ marginBottom: '5px' }}>
|
||||
<Grid item xs={12}>
|
||||
<Grid item xs={12} style={{display: 'flex', justifyContent: 'center'}}>
|
||||
<BlocklyWindow
|
||||
svg
|
||||
blockDisabled
|
||||
|
@ -59,15 +59,15 @@ const styles = theme => ({
|
||||
class Requirement extends Component {
|
||||
|
||||
render() {
|
||||
var tutorialIds = this.props.tutorialIds;
|
||||
var requirements = this.props.requirements;
|
||||
var tutorialIds = requirements.map(requirement => requirement._id);
|
||||
return (
|
||||
<div style={{ marginTop: '20px', marginBottom: '5px' }}>
|
||||
<Typography>Bevor du mit diesem Tutorial fortfährst solltest du folgende Tutorials erfolgreich abgeschlossen haben:</Typography>
|
||||
<List component="div">
|
||||
{tutorialIds.map((tutorialId, i) => {
|
||||
// title must be provided together with ids
|
||||
// var title = tutorials.filter(tutorial => tutorial.id === tutorialId)[0].title;
|
||||
var status = this.props.status.filter(status => status.id === tutorialId)[0];
|
||||
var title = requirements[i].title
|
||||
var status = this.props.status.filter(status => status._id === tutorialId)[0];
|
||||
var tasks = status.tasks;
|
||||
var error = status.tasks.filter(task => task.type === 'error').length > 0;
|
||||
var success = status.tasks.filter(task => task.type === 'success').length / tasks.length
|
||||
@ -98,7 +98,7 @@ class Requirement extends Component {
|
||||
</div>
|
||||
</Tooltip>
|
||||
<div style={{ height: '50px', width: 'calc(100% - 25px)', transform: 'translate(25px)' }} className={this.props.classes.hoverLink}>
|
||||
<Typography style={{ margin: 0, position: 'absolute', top: '50%', transform: 'translate(45px, -50%)', maxHeight: '50px', overflow: 'hidden', maxWidth: 'calc(100% - 45px)'/*, textOverflow: 'ellipsis', whiteSpace: 'pre-line', overflowWrap: 'anywhere'*/ }}>{/*title*/}Name hinzufügen über Datenbankeintrag</Typography>
|
||||
<Typography style={{ margin: 0, position: 'absolute', top: '50%', transform: 'translate(45px, -50%)', maxHeight: '50px', overflow: 'hidden', maxWidth: 'calc(100% - 45px)', textOverflow: 'ellipsis', whiteSpace: 'pre-line', overflowWrap: 'anywhere' }}>{title}</Typography>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
@ -112,12 +112,12 @@ class Requirement extends Component {
|
||||
|
||||
Requirement.propTypes = {
|
||||
status: PropTypes.array.isRequired,
|
||||
change: PropTypes.number.isRequired,
|
||||
change: PropTypes.number.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
change: state.tutorial.change,
|
||||
status: state.tutorial.status
|
||||
status: state.tutorial.status,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(withRouter(Requirement)));
|
||||
|
@ -5,7 +5,7 @@ import { tutorialCheck, tutorialStep } from '../../actions/tutorialActions';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Compile from '../Compile';
|
||||
import Compile from '../Workspace/Compile';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import { checkXml } from '../../helpers/compareXml';
|
||||
@ -15,15 +15,15 @@ import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Button from '@material-ui/core/Button';
|
||||
|
||||
import { faPlay } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClipboardCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
compile: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
@ -63,7 +63,7 @@ class SolutionCheck extends Component {
|
||||
style={{ width: '40px', height: '40px', marginRight: '5px' }}
|
||||
onClick={() => this.check()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faPlay} size="xs" />
|
||||
<FontAwesomeIcon icon={faClipboardCheck} size="l" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
@ -50,9 +50,9 @@ const styles = (theme) => ({
|
||||
class StepperHorizontal extends Component {
|
||||
|
||||
render() {
|
||||
var tutorialId = this.props.tutorial.id;
|
||||
var tutorialId = this.props.tutorial._id;
|
||||
var tutorialIndex = this.props.currentTutorialIndex;
|
||||
var status = this.props.status.filter(status => status.id === tutorialId)[0];
|
||||
var status = this.props.status.filter(status => status._id === tutorialId)[0];
|
||||
var tasks = status.tasks;
|
||||
var error = tasks.filter(task => task.type === 'error').length > 0;
|
||||
var success = tasks.filter(task => task.type === 'success').length / tasks.length;
|
||||
|
@ -61,7 +61,7 @@ class StepperVertical extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(props){
|
||||
if (props.tutorial.id !== Number(this.props.match.params.tutorialId)) {
|
||||
if (props.tutorial._id !== this.props.match.params.tutorialId) {
|
||||
this.props.tutorialStep(0);
|
||||
}
|
||||
}
|
||||
@ -69,7 +69,7 @@ class StepperVertical extends Component {
|
||||
render() {
|
||||
var steps = this.props.steps;
|
||||
var activeStep = this.props.activeStep;
|
||||
var tutorialStatus = this.props.status.filter(status => status.id === this.props.tutorial.id)[0];
|
||||
var tutorialStatus = this.props.status.filter(status => status._id === this.props.tutorial._id)[0];
|
||||
return (
|
||||
<div style={{marginRight: '10px'}}>
|
||||
<Stepper
|
||||
@ -79,13 +79,13 @@ class StepperVertical extends Component {
|
||||
classes={{root: this.props.classes.verticalStepper}}
|
||||
>
|
||||
{steps.map((step, i) => {
|
||||
var tasksIndex = tutorialStatus.tasks.findIndex(task => task.id === step.id);
|
||||
var tasksIndex = tutorialStatus.tasks.findIndex(task => task._id === step._id);
|
||||
var taskType = tasksIndex > -1 ? tutorialStatus.tasks[tasksIndex].type : null;
|
||||
var taskStatus = taskType === 'success' ? 'Success' : taskType === 'error' ? 'Error' : 'Other';
|
||||
return (
|
||||
<Step key={i}>
|
||||
<Tooltip title={step.headline} placement='right' arrow >
|
||||
<div style={i === activeStep ? {padding: '5px 0'} : {padding: '5px 0', cursor: 'pointer'}} onClick={i === activeStep ? null : () => {this.props.tutorialStep(i)}}>
|
||||
<div style={i === activeStep ? {padding: '5px 0'} : {padding: '5px 0', cursor: 'pointer'}} onClick={i === activeStep ? null : () => {console.log(i); this.props.tutorialStep(i)}}>
|
||||
<StepLabel
|
||||
StepIconComponent={'div'}
|
||||
classes={{
|
||||
|
@ -10,6 +10,7 @@ import StepperHorizontal from './StepperHorizontal';
|
||||
import StepperVertical from './StepperVertical';
|
||||
import Instruction from './Instruction';
|
||||
import Assessment from './Assessment';
|
||||
import Badge from './Badge';
|
||||
import NotFound from '../NotFound';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace';
|
||||
@ -25,7 +26,7 @@ class Tutorial extends Component {
|
||||
}
|
||||
|
||||
componentDidUpdate(props, state) {
|
||||
if(this.props.tutorial && !this.props.isLoading && this.props.tutorial.id != this.props.match.params.tutorialId) {
|
||||
if(this.props.tutorial && !this.props.isLoading && this.props.tutorial._id != this.props.match.params.tutorialId) {
|
||||
this.props.getTutorial(this.props.match.params.tutorialId);
|
||||
}
|
||||
if(this.props.message.id === 'GET_TUTORIAL_FAIL'){
|
||||
@ -54,9 +55,10 @@ class Tutorial extends Component {
|
||||
var name = `${detectWhitespacesAndReturnReadableResult(tutorial.title)}_${detectWhitespacesAndReturnReadableResult(step.headline)}`;
|
||||
return(
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/tutorial', title: 'Tutorial' }, { link: `/tutorial/${this.props.tutorial.id}`, title: tutorial.title }]} />
|
||||
<Breadcrumbs content={[{ link: '/tutorial', title: 'Tutorial' }, { link: `/tutorial/${this.props.tutorial._id}`, title: tutorial.title }]} />
|
||||
|
||||
<StepperHorizontal />
|
||||
<Badge />
|
||||
|
||||
<div style={{ display: 'flex' }}>
|
||||
<StepperVertical steps={steps} />
|
||||
|
@ -77,14 +77,14 @@ class TutorialHome extends Component {
|
||||
<h1>Tutorial-Übersicht</h1>
|
||||
<Grid container spacing={2}>
|
||||
{this.props.tutorials.map((tutorial, i) => {
|
||||
var status = this.props.status.filter(status => status.id === tutorial.id)[0];
|
||||
var status = this.props.status.filter(status => status._id === tutorial._id)[0];
|
||||
var tasks = status.tasks;
|
||||
var error = status.tasks.filter(task => task.type === 'error').length > 0;
|
||||
var success = status.tasks.filter(task => task.type === 'success').length / tasks.length
|
||||
var tutorialStatus = success === 1 ? 'Success' : error ? 'Error' : 'Other';
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} xl={3} key={i} style={{}}>
|
||||
<Link to={`/tutorial/${tutorial.id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Link to={`/tutorial/${tutorial._id}`} style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Paper style={{ height: '150px', padding: '10px', position: 'relative', overflow: 'hidden' }}>
|
||||
{tutorial.title}
|
||||
<div className={clsx(this.props.classes.outerDiv)} style={{ width: '160px', height: '160px', border: 0 }}>
|
||||
|
129
src/components/User/Account.js
Normal file
129
src/components/User/Account.js
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
import Alert from '../Alert';
|
||||
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { faUser, faAt, faMapMarkerAlt, faCloudSunRain, faBox } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
|
||||
export class Account extends Component {
|
||||
|
||||
render(){
|
||||
const {user} = this.props;
|
||||
return(
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/user', title: 'Account' }]} />
|
||||
|
||||
<h1>Account</h1>
|
||||
<Alert>
|
||||
Alle Angaben stammen von <Link color='primary' rel="noreferrer" target="_blank" href={'https://opensensemap.org/'}>openSenseMap</Link> und können dort verwaltet werden.
|
||||
</Alert>
|
||||
<Paper style={{width: 'max-content', maxWidth: '100%'}}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<Tooltip title='Nutzername'>
|
||||
<ListItemIcon>
|
||||
<FontAwesomeIcon icon={faUser} />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary={user.name} />
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip title='Email'>
|
||||
<ListItemIcon>
|
||||
<FontAwesomeIcon icon={faAt} />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<ListItemText primary={user.email} />
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<Divider style={{marginBottom: '16px', marginTop: '16px'}}/>
|
||||
<div style={{marginBottom: '8px'}}>
|
||||
{this.props.user.boxes.length < 1 ?
|
||||
<Typography>
|
||||
Du hast noch keine senseBox registriert. Besuche <Link color='primary' rel="noreferrer" target="_blank" href={'https://opensensemap.org/'}>openSenseMap</Link> um eine senseBox zu registrieren.
|
||||
</Typography>
|
||||
: <Typography style={{fontWeight: 'bold', fontSize: '1.1rem'}}>
|
||||
Du hast {this.props.user.boxes.length} {this.props.user.boxes.length === 1 ? 'senseBox' : 'senseBoxen'} registriert:
|
||||
</Typography>}
|
||||
</div>
|
||||
<Grid container spacing={2}>
|
||||
{this.props.user.boxes.map((box, i) => {
|
||||
var sensors = box.sensors.map(sensor => sensor.title );
|
||||
return (
|
||||
<Grid item xs={12} sm={6} md={4} xl={3} key={i}>
|
||||
<Link rel="noreferrer" target="_blank" href={`https://opensensemap.org/explore/${box._id}`} color="primary" style={{ textDecoration: 'none', color: 'inherit' }}>
|
||||
<Paper>
|
||||
<List>
|
||||
<ListItem>
|
||||
<Typography style={{fontWeight: 'bold', fontSize: '1.6rem'}}>{box.name}</Typography>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip title='Modell'>
|
||||
<ListItemIcon>
|
||||
<FontAwesomeIcon icon={faBox} />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<Typography style={{fontWeight: 'bold', marginRight: '4px'}}>Modell: </Typography>
|
||||
<Typography>{box.model}</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip title='Standort'>
|
||||
<ListItemIcon>
|
||||
<FontAwesomeIcon icon={faMapMarkerAlt} />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<Typography style={{fontWeight: 'bold', marginRight: '4px'}}>Standort: </Typography>
|
||||
<Typography>{`${box.exposure} (lon: ${box.currentLocation.coordinates[0]}, lat: ${box.currentLocation.coordinates[1]})`}</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<Tooltip title='Sensoren'>
|
||||
<ListItemIcon>
|
||||
<FontAwesomeIcon icon={faCloudSunRain} />
|
||||
</ListItemIcon>
|
||||
</Tooltip>
|
||||
<div>
|
||||
<Typography style={{fontWeight: 'bold', marginRight: '4px'}}>Sensoren: </Typography>
|
||||
<Typography>{sensors.join(', ')}</Typography>
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
</Link>
|
||||
</Grid>
|
||||
)
|
||||
})}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Account.propTypes = {
|
||||
user: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(Account);
|
168
src/components/User/Login.js
Normal file
168
src/components/User/Login.js
Normal file
@ -0,0 +1,168 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { login } from '../../actions/authActions'
|
||||
import { clearMessages } from '../../actions/messageActions'
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
import Alert from '../Alert';
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import CircularProgress from '@material-ui/core/CircularProgress';
|
||||
import Link from '@material-ui/core/Link';
|
||||
|
||||
|
||||
export class Login extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
redirect: props.location.state ? props.location.state.from.pathname : null,
|
||||
email: '',
|
||||
password: '',
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: '',
|
||||
showPassword: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props){
|
||||
console.log(this.state.redirect);
|
||||
const { message } = this.props;
|
||||
if (message !== props.message) {
|
||||
if(message.id === 'LOGIN_SUCCESS'){
|
||||
if(this.state.redirect){
|
||||
this.props.history.push(this.state.redirect);
|
||||
}
|
||||
else{
|
||||
this.props.history.goBack();
|
||||
}
|
||||
}
|
||||
// Check for login error
|
||||
else if(message.id === 'LOGIN_FAIL'){
|
||||
this.setState({ email: '', password: '', snackbar: true, key: Date.now(), message: 'Der Benutzername oder das Passwort ist nicht korrekt.', type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.value });
|
||||
};
|
||||
|
||||
onSubmit = e => {
|
||||
e.preventDefault();
|
||||
const {email, password} = this.state;
|
||||
if(email !== '' && password !== ''){
|
||||
// create user object
|
||||
const user = {
|
||||
email,
|
||||
password
|
||||
};
|
||||
this.props.login(user);
|
||||
} else {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: 'Gib sowohl ein Benutzername als auch ein Passwort ein.', type: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
handleClickShowPassword = () => {
|
||||
this.setState({ showPassword: !this.state.showPassword });
|
||||
};
|
||||
|
||||
handleMouseDownPassword = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/user/login', title: 'Anmelden' }]} />
|
||||
|
||||
<div style={{maxWidth: '500px', marginLeft: 'auto', marginRight: 'auto'}}>
|
||||
<h1>Anmelden</h1>
|
||||
<Alert>
|
||||
Zur Anmeldung ist ein Konto auf <Link color='primary' rel="noreferrer" target="_blank" href={'https://opensensemap.org/'}>openSenseMap</Link> Voraussetzung.
|
||||
</Alert>
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
<TextField
|
||||
style={{marginBottom: '10px'}}
|
||||
// variant='outlined'
|
||||
type='text'
|
||||
label='E-Mail oder Nutzername'
|
||||
name='email'
|
||||
value={this.state.email}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<TextField
|
||||
// variant='outlined'
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
label='Passwort'
|
||||
name='password'
|
||||
value={this.state.password}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment
|
||||
position="end"
|
||||
>
|
||||
<IconButton
|
||||
onClick={this.handleClickShowPassword}
|
||||
onMouseDown={this.handleMouseDownPassword}
|
||||
edge="end"
|
||||
>
|
||||
<FontAwesomeIcon size='xs' icon={this.state.showPassword ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<p>
|
||||
<Button color="primary" variant='contained' onClick={this.onSubmit} style={{width: '100%'}}>
|
||||
{this.props.progress ?
|
||||
<div style={{height: '24.5px'}}><CircularProgress color="inherit" size={20}/></div>
|
||||
: 'Anmelden'}
|
||||
</Button>
|
||||
</p>
|
||||
<p style={{textAlign: 'center', fontSize: '0.8rem'}}>
|
||||
<Link rel="noreferrer" target="_blank" href={'https://opensensemap.org/'} color="primary">Passwort vergessen?</Link>
|
||||
</p>
|
||||
<Divider variant='fullWidth'/>
|
||||
<p style={{textAlign: 'center', paddingRight: "34px", paddingLeft: "34px"}}>
|
||||
Du hast noch kein Konto? Registriere dich auf <Link rel="noreferrer" target="_blank" href={'https://opensensemap.org/'}>openSenseMap</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Login.propTypes = {
|
||||
message: PropTypes.object.isRequired,
|
||||
login: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
progress: PropTypes.bool.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.message,
|
||||
progress: state.auth.progress
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { login, clearMessages })(withRouter(Login));
|
260
src/components/User/MyBadges.js
Normal file
260
src/components/User/MyBadges.js
Normal file
@ -0,0 +1,260 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { connectMyBadges, disconnectMyBadges } from '../../actions/authActions';
|
||||
|
||||
import axios from 'axios';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Breadcrumbs from '../Breadcrumbs';
|
||||
import Alert from '../Alert';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Divider from '@material-ui/core/Divider';
|
||||
import InputAdornment from '@material-ui/core/InputAdornment';
|
||||
import Link from '@material-ui/core/Link';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Grid from '@material-ui/core/Grid';
|
||||
import Avatar from '@material-ui/core/Avatar';
|
||||
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
const styles = (theme) => ({
|
||||
root: {
|
||||
'& label.Mui-focused': {
|
||||
color: '#aed9c8'
|
||||
},
|
||||
'& .MuiOutlinedInput-root': {
|
||||
'&.Mui-focused fieldset': {
|
||||
borderColor: '#aed9c8'
|
||||
},
|
||||
borderRadius: '0.75rem'
|
||||
}
|
||||
},
|
||||
text: {
|
||||
fontFamily: [
|
||||
'"Open Sans"',
|
||||
'BlinkMacSystemFont',
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
fontSize: 16
|
||||
}
|
||||
});
|
||||
|
||||
export class MyBadges extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
showPassword: false,
|
||||
msg: '',
|
||||
badges: [],
|
||||
progress: false
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount(){
|
||||
if(this.props.user.badge){
|
||||
this.getBadges();
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(props){
|
||||
const { message } = this.props;
|
||||
if (message !== props.message) {
|
||||
// Check for login error
|
||||
if(message.id === 'MYBADGES_CONNECT_FAIL'){
|
||||
this.setState({msg: 'Der Benutzername oder das Passwort ist nicht korrekt.', username: '', password: '', showPassword: false});
|
||||
}
|
||||
else if(message.id === 'MYBADGES_CONNECT_SUCCESS'){
|
||||
this.getBadges();
|
||||
}
|
||||
else if(message.id === 'MYBADGES_DISCONNECT_SUCCESS' || message.id === 'MYBADGES_DISCONNECT_FAIL'){
|
||||
this.setState({progress: false});
|
||||
}
|
||||
else {
|
||||
this.setState({msg: null});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getBadges = () => {
|
||||
this.setState({progress: true});
|
||||
axios.get(`${process.env.REACT_APP_BLOCKLY_API}/user/badge`)
|
||||
.then(res => {
|
||||
this.setState({badges: res.data.badges, progress: false});
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({progress: false});
|
||||
console.log(err);
|
||||
});
|
||||
};
|
||||
|
||||
onChange = e => {
|
||||
this.setState({ [e.target.name]: e.target.value, msg: '' });
|
||||
};
|
||||
|
||||
onSubmit = e => {
|
||||
e.preventDefault();
|
||||
const {username, password} = this.state;
|
||||
// create user object
|
||||
const user = {
|
||||
username,
|
||||
password
|
||||
};
|
||||
this.props.connectMyBadges(user);
|
||||
};
|
||||
|
||||
handleClickShowPassword = () => {
|
||||
this.setState({ showPassword: !this.state.showPassword });
|
||||
};
|
||||
|
||||
handleMouseDownPassword = (e) => {
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
render(){
|
||||
return(
|
||||
<div>
|
||||
<Breadcrumbs content={[{ link: '/user/badge', title: 'MyBadges' }]} />
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={12} style={{margin: '4px'}}>
|
||||
{!this.props.user.badge ?
|
||||
<Alert>
|
||||
Du kannst dein Blockly-Konto mit deinem <Link href={`${process.env.REACT_APP_MYBADGES}`}>MyBadges</Link>-Konto verknüpfen, um Badges erwerben zu können.
|
||||
</Alert>
|
||||
: null}
|
||||
<Paper style={{background: '#fffbf5'}}>
|
||||
<div style={{display: 'flex', flexDirection: 'row', alignSelf: 'center', justifyContent: 'center', flexWrap: 'wrap'}}>
|
||||
<div style={!this.props.user.badge ? {margin: '15px 15px 0px 15px'} : {margin: '15px'}}>
|
||||
<img src={`${process.env.REACT_APP_MYBADGES}/static/media/Logo.d1c71fdf.png`} alt="My Badges" style={{maxWidth: '200px', maxHeight: '200px'}}></img>
|
||||
</div>
|
||||
{!this.props.user.badge ?
|
||||
<div style={{maxWidth: '500px', alignSelf: 'center', textAlign: 'center', margin: '15px'}}>
|
||||
{this.state.msg ?
|
||||
<div style={{lineHeight: 1.43, borderRadius: '0.75rem', padding: '14px 16px', marginBottom: '10px', color: 'rgb(97, 26, 21)', backgroundColor: 'rgb(253, 236, 234)', fontFamily: `"Open Sans",BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol"`}}>
|
||||
{this.state.msg}
|
||||
</div> : null
|
||||
}
|
||||
<TextField
|
||||
style={{marginBottom: '10px'}}
|
||||
classes={{root: this.props.classes.root}}
|
||||
variant='outlined'
|
||||
type='text'
|
||||
label='Nutzername'
|
||||
name='username'
|
||||
value={this.state.username}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<TextField
|
||||
classes={{root: this.props.classes.root}}
|
||||
variant='outlined'
|
||||
type={this.state.showPassword ? 'text' : 'password'}
|
||||
label='Passwort'
|
||||
name='password'
|
||||
value={this.state.password}
|
||||
InputProps={{
|
||||
endAdornment:
|
||||
<InputAdornment
|
||||
position="end"
|
||||
>
|
||||
<IconButton
|
||||
onClick={this.handleClickShowPassword}
|
||||
onMouseDown={this.handleMouseDownPassword}
|
||||
edge="end"
|
||||
>
|
||||
<FontAwesomeIcon size='xs' icon={this.state.showPassword ? faEyeSlash : faEye} />
|
||||
</IconButton>
|
||||
</InputAdornment>
|
||||
}}
|
||||
onChange={this.onChange}
|
||||
fullWidth={true}
|
||||
/>
|
||||
<p>
|
||||
<Button variant='contained' onClick={this.onSubmit} className={this.props.classes.text} style={{background: '#aed9c8', borderRadius: '0.75rem', width: '100%'}}>
|
||||
Anmelden
|
||||
</Button>
|
||||
</p>
|
||||
<p className={this.props.classes.text} style={{textAlign: 'center', fontSize: '0.8rem'}}>
|
||||
<Link style={{color: '#aed9c8'}} href={`${process.env.REACT_APP_MYBADGES}/user/password`}>Passwort vergessen?</Link>
|
||||
</p>
|
||||
<Divider variant='fullWidth'/>
|
||||
<p className={this.props.classes.text} style={{textAlign: 'center', paddingRight: "34px", paddingLeft: "34px"}}>
|
||||
Du hast noch kein Konto? <Link style={{color: '#aed9c8'}} href={`${process.env.REACT_APP_MYBADGES}/register`}>Registrieren</Link>
|
||||
</p>
|
||||
</div>
|
||||
: <div style={{margin: '15px', alignSelf: 'center'}}>
|
||||
<Typography style={{fontWeight: 'bold', fontSize: '1.1rem'}}>MyBadges-Konto ist erfolgreich verknüpft.</Typography>
|
||||
<Button variant='outlined' style={{borderColor: '#aed9c8'}} onClick={() => {this.props.disconnectMyBadges(); this.setState({badges: [], progress: true});}}>Konto trennen</Button>
|
||||
</div>}
|
||||
</div>
|
||||
</Paper>
|
||||
</Grid>
|
||||
|
||||
{this.props.user.badge && !this.state.progress ?
|
||||
<Grid container item>
|
||||
<Grid item style={{margin: '4px'}}>
|
||||
{this.state.badges && this.state.badges.length > 0 ?
|
||||
<Typography style={{fontWeight: 'bold'}}>
|
||||
Du hast {this.state.badges.length} {this.state.badges.length === 1 ? 'Badge' : 'Badges'} im Kontext Blockly for senseBox erreicht.
|
||||
</Typography>
|
||||
: null}
|
||||
</Grid>
|
||||
<Grid container item>
|
||||
{this.state.badges && this.state.badges.length > 0 ?
|
||||
this.state.badges.map(badge => (
|
||||
<Grid item xs={12} sm={6} md={4}>
|
||||
<Paper style={{margin: '4px', textAlign: 'center'}}>
|
||||
{badge.image && badge.image.path ?
|
||||
<Avatar src={`${process.env.REACT_APP_MYBADGES}/media/${badge.image.path}`} style={{width: '200px', height: '200px', marginLeft: 'auto', marginRight: 'auto'}}/>
|
||||
: <Avatar style={{width: '200px', height: '200px', marginLeft: 'auto', marginRight: 'auto'}}></Avatar>}
|
||||
<Typography variant='h6' style={{display: 'flex', cursor: 'default', paddingBottom: '6px'}}>
|
||||
<div style={{flexGrow:1, marginLeft: '10px', marginRight: '10px'}}>{badge.name}</div>
|
||||
</Typography>
|
||||
</Paper>
|
||||
</Grid>
|
||||
))
|
||||
:
|
||||
<Grid item style={{margin: '4px'}}>
|
||||
<Typography style={{fontWeight: 'bold'}}>
|
||||
Du hast noch keine Badges im Kontext senseBox for Blockly erreicht.
|
||||
</Typography>
|
||||
</Grid>}
|
||||
</Grid>
|
||||
</Grid>
|
||||
: null}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
MyBadges.propTypes = {
|
||||
connectMyBadges: PropTypes.func.isRequired,
|
||||
disconnectMyBadges: PropTypes.func.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
user: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.message,
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { connectMyBadges, disconnectMyBadges })(withStyles(styles, { withTheme: true })(withRouter(MyBadges)));
|
@ -1,11 +1,11 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { workspaceName } from '../actions/workspaceActions';
|
||||
import { workspaceName } from '../../actions/workspaceActions';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace';
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace';
|
||||
|
||||
import Dialog from './Dialog';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
@ -15,7 +15,7 @@ import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
|
||||
import { faCogs } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faClipboardCheck } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
@ -23,13 +23,21 @@ const styles = (theme) => ({
|
||||
zIndex: theme.zIndex.drawer + 1,
|
||||
color: '#fff',
|
||||
},
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
iconButton: {
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
},
|
||||
button: {
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.button.compile,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
@ -99,7 +107,7 @@ class Compile extends Component {
|
||||
this.download();
|
||||
}
|
||||
else {
|
||||
this.setState({ file: true, open: true, title: 'Blöcke kompilieren', content: 'Bitte gib einen Namen für die Bennenung des zu kompilierenden Programms ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' });
|
||||
this.setState({ file: true, open: true, title: 'Projekt kompilieren', content: 'Bitte gib einen Namen für die Bennenung des zu kompilierenden Programms ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' });
|
||||
}
|
||||
}
|
||||
|
||||
@ -111,17 +119,17 @@ class Compile extends Component {
|
||||
return (
|
||||
<div style={{}}>
|
||||
{this.props.iconButton ?
|
||||
<Tooltip title='Blöcke kompilieren' arrow style={{ marginRight: '5px' }}>
|
||||
<Tooltip title='Projekt kompilieren' arrow style={{ marginRight: '5px' }}>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
className={this.props.classes.iconButton}
|
||||
onClick={() => this.compile()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCogs} size="xs" />
|
||||
<FontAwesomeIcon icon={faClipboardCheck} size="l" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
:
|
||||
<Button style={{ float: 'right', color: 'white' }} variant="contained" color="primary" onClick={() => this.compile()}>
|
||||
<FontAwesomeIcon icon={faCogs} style={{ marginRight: '5px' }} /> Kompilieren
|
||||
<Button style={{ float: 'right', color: 'white' }} variant="contained" className={this.props.classes.button} onClick={() => this.compile()}>
|
||||
<FontAwesomeIcon icon={faClipboardCheck} style={{ marginRight: '5px' }} /> Kompilieren
|
||||
</Button>
|
||||
}
|
||||
<Backdrop className={this.props.classes.backdrop} open={this.state.progress}>
|
89
src/components/Workspace/DeleteProject.js
Normal file
89
src/components/Workspace/DeleteProject.js
Normal file
@ -0,0 +1,89 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { deleteProject } from '../../actions/projectActions';
|
||||
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
buttonTrash: {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.error.dark,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
class DeleteProject extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if(this.props.message !== props.message){
|
||||
if(this.props.message.id === 'PROJECT_DELETE_SUCCESS'){
|
||||
this.props.history.push(`/${this.props.projectType}`);
|
||||
}
|
||||
else if(this.props.message.id === 'PROJECT_DELETE_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Löschen des Projektes. Versuche es noch einmal.`, type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Tooltip title='Projekt löschen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.buttonTrash}
|
||||
onClick={() => this.props.deleteProject(this.props.projectType, this.props.project._id)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faTrashAlt} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
DeleteProject.propTypes = {
|
||||
deleteProject: PropTypes.func.isRequired,
|
||||
message: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
message: state.message,
|
||||
});
|
||||
|
||||
|
||||
export default connect(mapStateToProps, { deleteProject })(withStyles(styles, { withTheme: true })(withRouter(DeleteProject)));
|
66
src/components/Workspace/DownloadProject.js
Normal file
66
src/components/Workspace/DownloadProject.js
Normal file
@ -0,0 +1,66 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { faFileDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class DownloadProject extends Component {
|
||||
|
||||
downloadXmlFile = () => {
|
||||
var code = this.props.xml;
|
||||
var fileName = detectWhitespacesAndReturnReadableResult(this.props.name);
|
||||
fileName = `${fileName}.xml`
|
||||
var blob = new Blob([code], { type: 'text/xml' });
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title='Projekt herunterladen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.downloadXmlFile()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faFileDownload} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
DownloadProject.propTypes = {
|
||||
xml: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
xml: state.workspace.code.xml,
|
||||
name: state.workspace.name
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(DownloadProject));
|
143
src/components/Workspace/OpenProject.js
Normal file
143
src/components/Workspace/OpenProject.js
Normal file
@ -0,0 +1,143 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearStats, workspaceName } from '../../actions/workspaceActions';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import { faUpload } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class OpenProject extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
open: false,
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, title: '', content: '' });
|
||||
}
|
||||
|
||||
uploadXmlFile = (xmlFile) => {
|
||||
if (xmlFile.type !== 'text/xml') {
|
||||
this.setState({ open: true, title: 'Unzulässiger Dateityp', content: 'Die übergebene Datei entsprach nicht dem geforderten Format. Es sind nur XML-Dateien zulässig.' });
|
||||
}
|
||||
else {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(xmlFile);
|
||||
reader.onloadend = () => {
|
||||
var xmlDom = null;
|
||||
try {
|
||||
xmlDom = Blockly.Xml.textToDom(reader.result);
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
var xmlBefore = this.props.xml;
|
||||
workspace.clear();
|
||||
this.props.clearStats();
|
||||
Blockly.Xml.domToWorkspace(xmlDom, workspace);
|
||||
if (workspace.getAllBlocks().length < 1) {
|
||||
Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xmlBefore), workspace)
|
||||
this.setState({ open: true, title: 'Keine Blöcke', content: 'Es wurden keine Blöcke detektiert. Bitte überprüfe den XML-Code und versuche es erneut.' });
|
||||
}
|
||||
else {
|
||||
if (!this.props.assessment) {
|
||||
var extensionPosition = xmlFile.name.lastIndexOf('.');
|
||||
this.props.workspaceName(xmlFile.name.substr(0, extensionPosition));
|
||||
}
|
||||
this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' });
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ open: true, title: 'Ungültige XML', content: 'Die XML-Datei konnte nicht in Blöcke zerlegt werden. Bitte überprüfe den XML-Code und versuche es erneut.' });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<div ref={this.inputRef} style={{ width: 'max-content', height: '40px', marginRight: '5px' }}>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
accept="text/xml"
|
||||
onChange={(e) => { this.uploadXmlFile(e.target.files[0]) }}
|
||||
id="open-blocks"
|
||||
type="file"
|
||||
/>
|
||||
<label htmlFor="open-blocks">
|
||||
<Tooltip title='Projekt öffnen' arrow style={this.props.style}>
|
||||
<div className={this.props.classes.button} style={{
|
||||
borderRadius: '50%', cursor: 'pointer', display: 'table-cell',
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faUpload} style={{ width: '18px', height: '18px' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={this.toggleDialog}
|
||||
onClick={this.toggleDialog}
|
||||
button={'Schließen'}
|
||||
/>
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
OpenProject.propTypes = {
|
||||
clearStats: PropTypes.func.isRequired,
|
||||
workspaceName: PropTypes.func.isRequired,
|
||||
xml: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
xml: state.workspace.code.xml,
|
||||
name: state.workspace.name
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { clearStats, workspaceName })(withStyles(styles, { withTheme: true })(OpenProject));
|
95
src/components/Workspace/ResetWorkspace.js
Normal file
95
src/components/Workspace/ResetWorkspace.js
Normal file
@ -0,0 +1,95 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearStats, onChangeCode, workspaceName } from '../../actions/workspaceActions';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
import { createNameId } from 'mnemonic-id';
|
||||
import { initialXml } from '../Blockly/initialXml.js';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { faShare } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
class ResetWorkspace extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: '',
|
||||
};
|
||||
}
|
||||
|
||||
resetWorkspace = () => {
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
Blockly.Events.disable(); // https://groups.google.com/forum/#!topic/blockly/m7e3g0TC75Y
|
||||
// if events are disabled, then the workspace will be cleared AND the blocks are not in the trashcan
|
||||
const xmlDom = Blockly.Xml.textToDom(initialXml)
|
||||
Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom, workspace);
|
||||
Blockly.Events.enable();
|
||||
workspace.options.maxBlocks = Infinity;
|
||||
this.props.onChangeCode();
|
||||
this.props.clearStats();
|
||||
if (!this.props.assessment) {
|
||||
this.props.workspaceName(createNameId());
|
||||
}
|
||||
this.setState({ snackbar: true, type: 'success', key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title='Workspace zurücksetzen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.resetWorkspace()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShare} size="xs" flip='horizontal' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
ResetWorkspace.propTypes = {
|
||||
clearStats: PropTypes.func.isRequired,
|
||||
onChangeCode: PropTypes.func.isRequired,
|
||||
workspaceName: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
export default connect(null, { clearStats, onChangeCode, workspaceName })(withStyles(styles, { withTheme: true })(ResetWorkspace));
|
200
src/components/Workspace/SaveProject.js
Normal file
200
src/components/Workspace/SaveProject.js
Normal file
@ -0,0 +1,200 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { updateProject, setDescription } from '../../actions/projectActions';
|
||||
|
||||
import axios from 'axios';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import Menu from '@material-ui/core/Menu';
|
||||
import MenuItem from '@material-ui/core/MenuItem';
|
||||
|
||||
import { faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
class SaveProject extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
open: false,
|
||||
description: props.description,
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: '',
|
||||
menuOpen: false,
|
||||
anchor: '',
|
||||
projectType: props.projectType
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if (props.projectType !== this.props.projectType) {
|
||||
this.setState({ projectType: this.props.projectType });
|
||||
}
|
||||
if (props.description !== this.props.description) {
|
||||
this.setState({ description: this.props.description });
|
||||
}
|
||||
if(this.props.message !== props.message){
|
||||
if(this.props.message.id === 'PROJECT_UPDATE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich aktualisiert.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'GALLERY_UPDATE_SUCCESS'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Galerie-Projekt wurde erfolgreich aktualisiert.`, type: 'success' });
|
||||
}
|
||||
else if(this.props.message.id === 'PROJECT_UPDATE_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Projektes. Versuche es noch einmal.`, type: 'error' });
|
||||
}
|
||||
else if(this.props.message.id === 'GALLERY_UPDATE_FAIL'){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Aktualisieren des Galerie-Projektes. Versuche es noch einmal.`, type: 'error' });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleMenu = (e) => {
|
||||
this.setState({ menuOpen: !this.state.menuOpen, anchor: e.currentTarget });
|
||||
};
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, title: '', content: '' });
|
||||
}
|
||||
|
||||
saveProject = () => {
|
||||
var body = {
|
||||
xml: this.props.xml,
|
||||
title: this.props.name
|
||||
};
|
||||
if(this.state.projectType === 'gallery'){
|
||||
body.description = this.state.description;
|
||||
}
|
||||
axios.post(`${process.env.REACT_APP_BLOCKLY_API}/${this.state.projectType}`, body)
|
||||
.then(res => {
|
||||
var project = res.data[this.state.projectType];
|
||||
this.props.history.push(`/${this.state.projectType}/${project._id}`);
|
||||
})
|
||||
.catch(err => {
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Speichern des ${this.state.projectType === 'gallery' ? 'Galerie-':''}Projektes. Versuche es noch einmal.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
setDescription = (e) => {
|
||||
this.setState({ description: e.target.value });
|
||||
}
|
||||
|
||||
workspaceDescription = () => {
|
||||
this.props.setDescription(this.state.description);
|
||||
this.setState({projectType: 'gallery'},
|
||||
() => this.saveProject()
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
console.log(1, this.props);
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title={this.state.projectType === 'project'? 'Projekt aktualisieren':'Projekt speichern'} arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={this.props.user.blocklyRole !== 'user' && (!this.props.project || this.props.user.email === this.props.project.creator) ? (e) => this.toggleMenu(e) : this.state.projectType === 'project' ? () => this.props.updateProject(this.state.projectType, this.props.project._id) : () => {this.setState({projectType: 'project'}, () => this.saveProject())}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSave} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
anchorEl={this.state.anchor}
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
keepMounted
|
||||
transformOrigin={{
|
||||
vertical: 'top',
|
||||
horizontal: 'left',
|
||||
}}
|
||||
open={this.state.menuOpen}
|
||||
onClose={this.toggleMenu}
|
||||
>
|
||||
<MenuItem
|
||||
onClick={this.state.projectType === 'project' ? (e) => {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({projectType: 'project'}, () => this.saveProject())}}
|
||||
>
|
||||
{this.state.projectType === 'project' ? 'Projekt aktualisieren' : 'Projekt erstellen'}
|
||||
</MenuItem>
|
||||
<MenuItem
|
||||
onClick={this.state.projectType === 'gallery' ? (e) => {this.toggleMenu(e); this.props.updateProject(this.state.projectType, this.props.project._id)} : (e) => {this.toggleMenu(e); this.setState({ open: true, title: 'Projekbeschreibung ergänzen', content: 'Bitte gib eine Beschreibung für das Galerie-Projekt ein und bestätige deine Angabe mit einem Klick auf \'Eingabe\'.'});}}
|
||||
>
|
||||
{this.state.projectType === 'gallery' ? 'Galerie-Projekt aktualisieren' : 'Galerie-Projekt erstellen'}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={() => {this.toggleDialog(); this.setState({ description: this.props.description });}}
|
||||
onClick={() => {this.toggleDialog(); this.setState({ description: this.props.description });}}
|
||||
button={'Abbrechen'}
|
||||
>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<TextField autoFocus fullWidth multiline placeholder={'Projektbeschreibung'} value={this.state.description} onChange={this.setDescription} style={{ marginBottom: '10px' }} />
|
||||
<Button disabled={!this.state.description} variant='contained' color='primary' onClick={() => {this.workspaceDescription(); this.toggleDialog();}}>Eingabe</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
SaveProject.propTypes = {
|
||||
updateProject: PropTypes.func.isRequired,
|
||||
setDescription: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
xml: PropTypes.string.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
name: state.workspace.name,
|
||||
description: state.project.description,
|
||||
xml: state.workspace.code.xml,
|
||||
message: state.message,
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { updateProject, setDescription })(withStyles(styles, { withTheme: true })(withRouter(SaveProject)));
|
97
src/components/Workspace/Screenshot.js
Normal file
97
src/components/Workspace/Screenshot.js
Normal file
@ -0,0 +1,97 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../../helpers/whitespace';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
|
||||
import { faCamera } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class Screenshot extends Component {
|
||||
|
||||
getSvg = () => {
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
var canvas = workspace.svgBlockCanvas_.cloneNode(true);
|
||||
|
||||
if (canvas.children[0] !== undefined) {
|
||||
canvas.removeAttribute("transform");
|
||||
// does not work in react
|
||||
// var cssContent = Blockly.Css.CONTENT.join('');
|
||||
var cssContent = '';
|
||||
for (var i = 0; i < document.getElementsByTagName('style').length; i++) {
|
||||
if (/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)) {
|
||||
cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.');
|
||||
}
|
||||
}
|
||||
// ensure that fill-opacity is 1, because there cannot be a replacing
|
||||
// https://github.com/google/blockly/pull/3431/files#diff-00254795773903d3c0430915a68c9521R328
|
||||
cssContent += `.blocklyPath {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
.blocklyPathDark {
|
||||
display: flex;
|
||||
}
|
||||
.blocklyPathLight {
|
||||
display: flex;
|
||||
} `;
|
||||
var css = '<defs><style type="text/css" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[' + cssContent + ']]></style></defs>';
|
||||
var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox();
|
||||
var content = new XMLSerializer().serializeToString(canvas);
|
||||
var xml = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="${bbox.width}" height="${bbox.height}" viewBox="${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}">
|
||||
${css}">${content}</svg>`;
|
||||
var fileName = detectWhitespacesAndReturnReadableResult(this.props.name);
|
||||
// this.props.workspaceName(this.state.name);
|
||||
fileName = `${fileName}.svg`
|
||||
var blob = new Blob([xml], { type: 'image/svg+xml;base64' });
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title='Screenshot erstellen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.getSvg()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCamera} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
Screenshot.propTypes = {
|
||||
name: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
name: state.workspace.name,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(withStyles(styles, { withTheme: true })(Screenshot));
|
157
src/components/Workspace/ShareProject.js
Normal file
157
src/components/Workspace/ShareProject.js
Normal file
@ -0,0 +1,157 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { shareProject } from '../../actions/projectActions';
|
||||
import { clearMessages } from '../../actions/messageActions';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import Dialog from '../Dialog';
|
||||
import Snackbar from '../Snackbar';
|
||||
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import { faShareAlt, faCopy } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
},
|
||||
link: {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'none',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
textDecoration: 'underline'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class WorkspaceFunc extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: '',
|
||||
title: '',
|
||||
content: '',
|
||||
open: false,
|
||||
id: '',
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if(this.props.message !== props.message){
|
||||
if(this.props.message.id === 'SHARE_SUCCESS' && (!this.props.multiple || this.props.message.status === this.props.project._id)){
|
||||
this.setState({ share: true, open: true, title: 'Programm teilen', id: this.props.message.status });
|
||||
}
|
||||
else if(this.props.message.id === 'SHARE_FAIL' && (!this.props.multiple || this.props.message.status === this.props.project._id)){
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Fehler beim Erstellen eines Links zum Teilen deines Programmes. Versuche es noch einmal.`, type: 'error' });
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount(){
|
||||
this.props.clearMessages();
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, title: '', content: '' });
|
||||
}
|
||||
|
||||
shareBlocks = () => {
|
||||
if(this.props.projectType === 'project' && this.props.project.shared){
|
||||
// project is already shared
|
||||
this.setState({ open: true, title: 'Programm teilen', id: this.props.project._id });
|
||||
}
|
||||
else {
|
||||
this.props.shareProject(this.props.name || this.props.project.title, this.props.projectType, this.props.project ? this.props.project._id : undefined);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title='Projekt teilen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.shareBlocks()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShareAlt} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={this.toggleDialog}
|
||||
onClick={this.toggleDialog}
|
||||
button={'Schließen'}
|
||||
>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<Typography>Über den folgenden Link kannst du dein Programm teilen:</Typography>
|
||||
<Link to={`/share/${this.state.id}`} onClick={() => this.toggleDialog()} className={this.props.classes.link}>{`${window.location.origin}/share/${this.state.id}`}</Link>
|
||||
<Tooltip title='Link kopieren' arrow style={{ marginRight: '5px' }}>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(`${window.location.origin}/share/${this.state.id}`);
|
||||
this.setState({ snackbar: true, key: Date.now(), message: 'Link erfolgreich in Zwischenablage gespeichert.', type: 'success' });
|
||||
}}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCopy} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{this.props.project && this.props.project.shared && this.props.message.id !== 'SHARE_SUCCESS' ?
|
||||
<Typography variant='body2' style={{marginTop: '20px'}}>{`Das Projekt wurde bereits geteilt. Der Link ist noch mindestens ${
|
||||
moment(this.props.project.shared).diff(moment().utc(), 'days') === 0 ?
|
||||
moment(this.props.project.shared).diff(moment().utc(), 'hours') === 0 ?
|
||||
`${moment(this.props.project.shared).diff(moment().utc(), 'minutes')} Minuten`
|
||||
: `${moment(this.props.project.shared).diff(moment().utc(), 'hours')} Stunden`
|
||||
: `${moment(this.props.project.shared).diff(moment().utc(), 'days')} Tage`} gültig.`}</Typography>
|
||||
: <Typography variant='body2' style={{marginTop: '20px'}}>{`Der Link ist nun ${process.env.REACT_APP_SHARE_LINK_EXPIRES} Tage gültig.`}</Typography>}
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
WorkspaceFunc.propTypes = {
|
||||
shareProject: PropTypes.func.isRequired,
|
||||
clearMessages: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
message: PropTypes.object.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
name: state.workspace.name,
|
||||
message: state.message
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { shareProject, clearMessages })(withStyles(styles, { withTheme: true })(WorkspaceFunc));
|
94
src/components/Workspace/WorkspaceFunc.js
Normal file
94
src/components/Workspace/WorkspaceFunc.js
Normal file
@ -0,0 +1,94 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import WorkspaceName from './WorkspaceName';
|
||||
import SaveProject from './SaveProject';
|
||||
import Compile from './Compile';
|
||||
import SolutionCheck from '../Tutorial/SolutionCheck';
|
||||
import DownloadProject from './DownloadProject';
|
||||
import OpenProject from './OpenProject';
|
||||
import Screenshot from './Screenshot';
|
||||
import ShareProject from './ShareProject';
|
||||
import ResetWorkspace from './ResetWorkspace';
|
||||
import DeleteProject from './DeleteProject';
|
||||
|
||||
class WorkspaceFunc extends Component {
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ width: 'max-content', display: 'flex' }}>
|
||||
|
||||
{!this.props.assessment ?
|
||||
<WorkspaceName
|
||||
style={{marginRight: '5px'}}
|
||||
multiple={this.props.multiple}
|
||||
project={this.props.project}
|
||||
projectType={this.props.projectType}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{this.props.assessment ?
|
||||
<SolutionCheck />
|
||||
: !this.props.multiple ?
|
||||
<Compile iconButton />
|
||||
: null}
|
||||
|
||||
{this.props.user && !this.props.multiple?
|
||||
<SaveProject
|
||||
style={{marginRight: '5px'}}
|
||||
projectType={this.props.projectType}
|
||||
project={this.props.project}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{!this.props.multiple ?
|
||||
<DownloadProject style={{marginRight: '5px'}} />
|
||||
: null}
|
||||
|
||||
{!this.props.assessment && !this.props.multiple?
|
||||
<OpenProject
|
||||
style={{marginRight: '5px'}}
|
||||
assessment={this.props.assessment}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{!this.props.assessment && !this.props.multiple?
|
||||
<Screenshot style={{marginRight: '5px'}} />
|
||||
: null}
|
||||
|
||||
{this.props.projectType !== 'gallery' && !this.props.assessment ?
|
||||
<ShareProject
|
||||
style={{marginRight: '5px'}}
|
||||
multiple={this.props.multiple}
|
||||
project={this.props.project}
|
||||
projectType={this.props.projectType}
|
||||
/>
|
||||
:null}
|
||||
|
||||
{!this.props.multiple ?
|
||||
<ResetWorkspace style={this.props.projectType === 'project' || this.props.projectType === 'gallery' ? { marginRight: '5px' }:null}
|
||||
/>
|
||||
: null}
|
||||
|
||||
{!this.props.assessment && (this.props.projectType === 'project' || this.props.projectType === 'gallery') && this.props.user && this.props.user.email === this.props.project.creator ?
|
||||
<DeleteProject
|
||||
project={this.props.project}
|
||||
projectType={this.props.projectType}
|
||||
/>
|
||||
:null}
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
WorkspaceFunc.propTypes = {
|
||||
user: PropTypes.object
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
user: state.auth.user
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, null)(WorkspaceFunc);
|
150
src/components/Workspace/WorkspaceName.js
Normal file
150
src/components/Workspace/WorkspaceName.js
Normal file
@ -0,0 +1,150 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { workspaceName } from '../../actions/workspaceActions';
|
||||
import { setDescription, updateProject } from '../../actions/projectActions';
|
||||
|
||||
import Snackbar from '../Snackbar';
|
||||
import Dialog from '../Dialog';
|
||||
|
||||
import withWidth, { isWidthDown } from '@material-ui/core/withWidth';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
|
||||
import { faPen } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
workspaceName: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
borderRadius: '25px',
|
||||
display: 'inline-flex',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
class WorkspaceName extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
open: false,
|
||||
name: props.name,
|
||||
description: props.description,
|
||||
snackbar: false,
|
||||
type: '',
|
||||
key: '',
|
||||
message: ''
|
||||
};
|
||||
}
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if (props.name !== this.props.name) {
|
||||
this.setState({ name: this.props.name });
|
||||
}
|
||||
if (props.description !== this.props.description) {
|
||||
this.setState({ description: this.props.description });
|
||||
}
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, title: '', content: '' });
|
||||
}
|
||||
|
||||
setFileName = (e) => {
|
||||
this.setState({ name: e.target.value });
|
||||
}
|
||||
|
||||
setDescription = (e) => {
|
||||
this.setState({ description: e.target.value });
|
||||
}
|
||||
|
||||
renameWorkspace = () => {
|
||||
this.props.workspaceName(this.state.name);
|
||||
this.toggleDialog();
|
||||
if(this.props.projectType === 'project' || this.props.projectType === 'gallery' || this.state.projectType === 'gallery'){
|
||||
if(this.props.projectType === 'gallery' || this.state.projectType === 'gallery'){
|
||||
this.props.setDescription(this.state.description);
|
||||
}
|
||||
if(this.state.projectType === 'gallery'){
|
||||
this.saveGallery();
|
||||
} else {
|
||||
this.props.updateProject(this.props.projectType, this.props.project._id);
|
||||
}
|
||||
} else {
|
||||
this.setState({ snackbar: true, type: 'success', key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={this.props.style}>
|
||||
<Tooltip title={`Titel des Projektes${this.props.name ? `: ${this.props.name}` : ''}`} arrow style={{height: '100%'}}>
|
||||
<div
|
||||
className={this.props.classes.workspaceName}
|
||||
onClick={() => {if(this.props.multiple){this.props.workspaceName(this.props.project.title);if(this.props.projectType === 'gallery'){this.props.setDescription(this.props.project.description);}} this.setState({ open: true, title: this.props.projectType === 'gallery' ? 'Projektdaten ändern': this.props.projectType === 'project' ? 'Projekt umbenennen' : 'Projekt benennen', content: this.props.projectType === 'gallery' ? 'Bitte gib einen Titel und eine Beschreibung für das Galerie-Projekt ein und bestätige die Angaben mit einem Klick auf \'Eingabe\'.':'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}
|
||||
>
|
||||
{this.props.name && !isWidthDown(this.props.projectType === 'project' || this.props.projectType === 'gallery' ? 'xl':'xs', this.props.width) ?
|
||||
<Typography style={{ margin: 'auto -3px auto 12px' }}>{this.props.name}</Typography>
|
||||
: null}
|
||||
<div style={{ width: '40px', display: 'flex' }}>
|
||||
<FontAwesomeIcon icon={faPen} style={{ height: '18px', width: '18px', margin: 'auto' }} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type={this.state.type}
|
||||
key={this.state.key}
|
||||
/>
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={() => {this.toggleDialog(); this.setState({ name: this.props.name, description: this.props.description });}}
|
||||
onClick={() => {this.toggleDialog(); this.setState({ name: this.props.name, description: this.props.description });}}
|
||||
button={'Abbrechen'}
|
||||
>
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
{this.props.projectType === 'gallery' || this.state.projectType === 'gallery' ?
|
||||
<div>
|
||||
<TextField autoFocus placeholder={this.state.saveXml ? 'Dateiname' : 'Projekttitel'} value={this.state.name} onChange={this.setFileName} style={{marginBottom: '10px'}}/>
|
||||
<TextField fullWidth multiline placeholder={'Projektbeschreibung'} value={this.state.description} onChange={this.setDescription} style={{ marginBottom: '10px' }} />
|
||||
</div>
|
||||
: <TextField autoFocus placeholder={this.state.saveXml ? 'Dateiname' : 'Projekttitel'} value={this.state.name} onChange={this.setFileName} style={{ marginRight: '10px' }} />}
|
||||
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => { this.renameWorkspace(); this.toggleDialog(); }}>Eingabe</Button>
|
||||
</div>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
WorkspaceName.propTypes = {
|
||||
workspaceName: PropTypes.func.isRequired,
|
||||
setDescription: PropTypes.func.isRequired,
|
||||
updateProject: PropTypes.func.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
description: PropTypes.string.isRequired,
|
||||
message: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
name: state.workspace.name,
|
||||
description: state.project.description,
|
||||
message: state.message,
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { workspaceName, setDescription, updateProject })(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceName)));
|
@ -1,395 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { clearStats, onChangeCode, workspaceName } from '../actions/workspaceActions';
|
||||
|
||||
import * as Blockly from 'blockly/core';
|
||||
|
||||
import { saveAs } from 'file-saver';
|
||||
|
||||
import { detectWhitespacesAndReturnReadableResult } from '../helpers/whitespace';
|
||||
import { initialXml } from './Blockly/initialXml.js';
|
||||
|
||||
import Compile from './Compile';
|
||||
import SolutionCheck from './Tutorial/SolutionCheck';
|
||||
import Snackbar from './Snackbar';
|
||||
|
||||
import withWidth, { isWidthDown } from '@material-ui/core/withWidth';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import IconButton from '@material-ui/core/IconButton';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import TextField from '@material-ui/core/TextField';
|
||||
import Typography from '@material-ui/core/Typography';
|
||||
import { createId } from 'mnemonic-id';
|
||||
|
||||
|
||||
import Dialog from './Dialog';
|
||||
// import Dialog from '@material-ui/core/Dialog';
|
||||
import DialogActions from '@material-ui/core/DialogActions';
|
||||
import DialogContent from '@material-ui/core/DialogContent';
|
||||
import DialogContentText from '@material-ui/core/DialogContentText';
|
||||
import DialogTitle from '@material-ui/core/DialogTitle';
|
||||
|
||||
|
||||
|
||||
import { faPen, faSave, faUpload, faCamera, faShare, faShareAlt } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
const styles = (theme) => ({
|
||||
button: {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
width: '40px',
|
||||
height: '40px',
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.primary.main,
|
||||
color: theme.palette.primary.contrastText,
|
||||
}
|
||||
},
|
||||
workspaceName: {
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
borderRadius: '25px',
|
||||
display: 'inline-flex',
|
||||
cursor: 'pointer',
|
||||
'&:hover': {
|
||||
color: theme.palette.primary.main,
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
class WorkspaceFunc extends Component {
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.inputRef = React.createRef();
|
||||
this.state = {
|
||||
title: '',
|
||||
content: '',
|
||||
open: false,
|
||||
file: false,
|
||||
saveFile: false,
|
||||
share: false,
|
||||
name: props.name,
|
||||
snackbar: false,
|
||||
key: '',
|
||||
message: '',
|
||||
id: ''
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
componentDidUpdate(props) {
|
||||
if (props.name !== this.props.name) {
|
||||
this.setState({ name: this.props.name });
|
||||
}
|
||||
}
|
||||
|
||||
toggleDialog = () => {
|
||||
this.setState({ open: !this.state, share: false });
|
||||
}
|
||||
|
||||
saveXmlFile = () => {
|
||||
var code = this.props.xml;
|
||||
this.toggleDialog();
|
||||
var fileName = detectWhitespacesAndReturnReadableResult(this.state.name);
|
||||
this.props.workspaceName(this.state.name);
|
||||
fileName = `${fileName}.xml`
|
||||
var blob = new Blob([code], { type: 'text/xml' });
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
|
||||
shareBlocks = () => {
|
||||
let code = this.props.xml;
|
||||
let requestOptions = '';
|
||||
let id = '';
|
||||
if (this.state.id !== '') {
|
||||
requestOptions = {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: this.state.id,
|
||||
name: this.state.name,
|
||||
xml: code
|
||||
})
|
||||
};
|
||||
fetch(process.env.REACT_APP_BLOCKLY_API + '/share' + this.state.id, requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({ share: true }));
|
||||
}
|
||||
else {
|
||||
id = createId(10);
|
||||
requestOptions = {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
id: id,
|
||||
name: this.state.name,
|
||||
xml: code
|
||||
})
|
||||
};
|
||||
fetch(process.env.REACT_APP_BLOCKLY_API + '/share', requestOptions)
|
||||
.then(response => response.json())
|
||||
.then(data => this.setState({ id: data.id, share: true }));
|
||||
}
|
||||
}
|
||||
|
||||
getSvg = () => {
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
var canvas = workspace.svgBlockCanvas_.cloneNode(true);
|
||||
|
||||
if (canvas.children[0] !== undefined) {
|
||||
canvas.removeAttribute("transform");
|
||||
|
||||
// does not work in react
|
||||
// var cssContent = Blockly.Css.CONTENT.join('');
|
||||
var cssContent = '';
|
||||
for (var i = 0; i < document.getElementsByTagName('style').length; i++) {
|
||||
if (/^blockly.*$/.test(document.getElementsByTagName('style')[i].id)) {
|
||||
cssContent += document.getElementsByTagName('style')[i].firstChild.data.replace(/\..* \./g, '.');
|
||||
}
|
||||
}
|
||||
// ensure that fill-opacity is 1, because there cannot be a replacing
|
||||
// https://github.com/google/blockly/pull/3431/files#diff-00254795773903d3c0430915a68c9521R328
|
||||
cssContent += `.blocklyPath {
|
||||
fill-opacity: 1;
|
||||
}
|
||||
.blocklyPathDark {
|
||||
display: flex;
|
||||
}
|
||||
.blocklyPathLight {
|
||||
display: flex;
|
||||
} `;
|
||||
|
||||
var css = '<defs><style type="text/css" xmlns="http://www.w3.org/1999/xhtml"><![CDATA[' + cssContent + ']]></style></defs>';
|
||||
|
||||
var bbox = document.getElementsByClassName("blocklyBlockCanvas")[0].getBBox();
|
||||
var content = new XMLSerializer().serializeToString(canvas);
|
||||
|
||||
var xml = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
width="${bbox.width}" height="${bbox.height}" viewBox="${bbox.x} ${bbox.y} ${bbox.width} ${bbox.height}">
|
||||
${css}">${content}</svg>`;
|
||||
var fileName = detectWhitespacesAndReturnReadableResult(this.state.name);
|
||||
this.props.workspaceName(this.state.name);
|
||||
fileName = `${fileName}.svg`
|
||||
var blob = new Blob([xml], { type: 'image/svg+xml;base64' });
|
||||
saveAs(blob, fileName);
|
||||
}
|
||||
}
|
||||
|
||||
createFileName = (filetype) => {
|
||||
this.setState({ file: filetype }, () => {
|
||||
if (this.state.name) {
|
||||
this.state.file === 'xml' ? this.saveXmlFile() : this.getSvg()
|
||||
}
|
||||
else {
|
||||
this.setState({ saveFile: true, file: filetype, open: true, title: this.state.file === 'xml' ? 'Blöcke speichern' : 'Screenshot erstellen', content: `Bitte gib einen Namen für die Bennenung der ${this.state.file === 'xml' ? 'XML' : 'SVG'}-Datei ein und bestätige diesen mit einem Klick auf 'Eingabe'.` });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setFileName = (e) => {
|
||||
this.setState({ name: e.target.value });
|
||||
}
|
||||
|
||||
uploadXmlFile = (xmlFile) => {
|
||||
if (xmlFile.type !== 'text/xml') {
|
||||
this.setState({ open: true, file: false, title: 'Unzulässiger Dateityp', content: 'Die übergebene Datei entsprach nicht dem geforderten Format. Es sind nur XML-Dateien zulässig.' });
|
||||
}
|
||||
else {
|
||||
var reader = new FileReader();
|
||||
reader.readAsText(xmlFile);
|
||||
reader.onloadend = () => {
|
||||
var xmlDom = null;
|
||||
try {
|
||||
xmlDom = Blockly.Xml.textToDom(reader.result);
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
var xmlBefore = this.props.xml;
|
||||
workspace.clear();
|
||||
this.props.clearStats();
|
||||
Blockly.Xml.domToWorkspace(xmlDom, workspace);
|
||||
if (workspace.getAllBlocks().length < 1) {
|
||||
Blockly.Xml.domToWorkspace(Blockly.Xml.textToDom(xmlBefore), workspace)
|
||||
this.setState({ open: true, file: false, title: 'Keine Blöcke', content: 'Es wurden keine Blöcke detektiert. Bitte überprüfe den XML-Code und versuche es erneut.' });
|
||||
}
|
||||
else {
|
||||
if (!this.props.assessment) {
|
||||
var extensionPosition = xmlFile.name.lastIndexOf('.');
|
||||
this.props.workspaceName(xmlFile.name.substr(0, extensionPosition));
|
||||
}
|
||||
this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt aus gegebener XML-Datei wurde erfolgreich eingefügt.' });
|
||||
}
|
||||
} catch (err) {
|
||||
this.setState({ open: true, file: false, title: 'Ungültige XML', content: 'Die XML-Datei konnte nicht in Blöcke zerlegt werden. Bitte überprüfe den XML-Code und versuche es erneut.' });
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
renameWorkspace = () => {
|
||||
this.props.workspaceName(this.state.name);
|
||||
this.toggleDialog();
|
||||
this.setState({ snackbar: true, key: Date.now(), message: `Das Projekt wurde erfolgreich in '${this.state.name}' umbenannt.` });
|
||||
}
|
||||
|
||||
resetWorkspace = () => {
|
||||
const workspace = Blockly.getMainWorkspace();
|
||||
Blockly.Events.disable(); // https://groups.google.com/forum/#!topic/blockly/m7e3g0TC75Y
|
||||
// if events are disabled, then the workspace will be cleared AND the blocks are not in the trashcan
|
||||
const xmlDom = Blockly.Xml.textToDom(initialXml)
|
||||
Blockly.Xml.clearWorkspaceAndLoadFromXml(xmlDom, workspace);
|
||||
Blockly.Events.enable();
|
||||
workspace.options.maxBlocks = Infinity;
|
||||
this.props.onChangeCode();
|
||||
this.props.clearStats();
|
||||
if (!this.props.assessment) {
|
||||
this.props.workspaceName(null);
|
||||
}
|
||||
this.setState({ snackbar: true, key: Date.now(), message: 'Das Projekt wurde erfolgreich zurückgesetzt.' });
|
||||
}
|
||||
|
||||
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ width: 'max-content', display: 'flex' }}>
|
||||
{!this.props.assessment ?
|
||||
<Tooltip title={`Name des Projekts${this.props.name ? `: ${this.props.name}` : ''}`} arrow style={{ marginRight: '5px' }}>
|
||||
<div className={this.props.classes.workspaceName} onClick={() => { this.setState({ file: true, open: true, saveFile: false, title: 'Projekt benennen', content: 'Bitte gib einen Namen für das Projekt ein und bestätige diesen mit einem Klick auf \'Eingabe\'.' }) }}>
|
||||
{this.props.name && !isWidthDown('xs', this.props.width) ? <Typography style={{ margin: 'auto -3px auto 12px' }}>{this.props.name}</Typography> : null}
|
||||
<div style={{ width: '40px', display: 'flex' }}>
|
||||
<FontAwesomeIcon icon={faPen} style={{ height: '18px', width: '18px', margin: 'auto' }} />
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
: null}
|
||||
{this.props.assessment ? <SolutionCheck /> : <Compile iconButton />}
|
||||
<Tooltip title='Blöcke speichern' arrow style={{ marginRight: '5px' }}>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => { this.createFileName('xml'); }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faSave} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!this.props.assessment?
|
||||
<div ref={this.inputRef} style={{ width: 'max-content', height: '40px', marginRight: '5px' }}>
|
||||
<input
|
||||
style={{ display: 'none' }}
|
||||
accept="text/xml"
|
||||
onChange={(e) => { this.uploadXmlFile(e.target.files[0]) }}
|
||||
id="open-blocks"
|
||||
type="file"
|
||||
/>
|
||||
<label htmlFor="open-blocks">
|
||||
<Tooltip title='Blöcke öffnen' arrow style={{ marginRight: '5px' }}>
|
||||
<div className={this.props.classes.button} style={{
|
||||
borderRadius: '50%', cursor: 'pointer', display: 'table-cell',
|
||||
verticalAlign: 'middle',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<FontAwesomeIcon icon={faUpload} style={{ width: '18px', height: '18px' }} />
|
||||
</div>
|
||||
</Tooltip>
|
||||
</label>
|
||||
</div>
|
||||
: null}
|
||||
{!this.props.assessment?
|
||||
<Tooltip title='Screenshot erstellen' arrow style={{ marginRight: '5px' }}>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => { this.createFileName('svg'); }}
|
||||
>
|
||||
<FontAwesomeIcon icon={faCamera} size="xs" />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
: null}
|
||||
<Tooltip title='Workspace zurücksetzen' arrow style={{ marginRight: '5px' }}>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.resetWorkspace()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShare} size="xs" flip='horizontal' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{!this.props.assessment?
|
||||
<Tooltip title='Blöcke teilen' arrow>
|
||||
<IconButton
|
||||
className={this.props.classes.button}
|
||||
onClick={() => this.shareBlocks()}
|
||||
>
|
||||
<FontAwesomeIcon icon={faShareAlt} size="xs" flip='horizontal' />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
:null}
|
||||
|
||||
<Dialog open={this.state.share} onClose={this.toggleDialog} aria-labelledby="form-dialog-title">
|
||||
<DialogTitle id="form-dialog-title">Dein Link wurde erstellt.</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>
|
||||
Über den folgenden Link kannst du dein Programm teilen.
|
||||
</DialogContentText>
|
||||
<TextField
|
||||
autoFocus
|
||||
margin="dense"
|
||||
id="name"
|
||||
defaultValue={window.location.origin + "/share/" + this.state.id}
|
||||
label="url"
|
||||
type="email"
|
||||
fullWidth
|
||||
/>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.toggleDialog} color="primary">
|
||||
Cancel
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
|
||||
<Dialog
|
||||
open={this.state.open}
|
||||
title={this.state.title}
|
||||
content={this.state.content}
|
||||
onClose={this.toggleDialog}
|
||||
onClick={this.state.file ? () => { this.toggleDialog(); this.setState({ name: this.props.name }) } : this.toggleDialog}
|
||||
button={this.state.file ? 'Abbrechen' : 'Schließen'}
|
||||
>
|
||||
{this.state.file ?
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
<TextField autoFocus placeholder={this.state.saveXml ? 'Dateiname' : 'Projektname'} value={this.state.name} onChange={this.setFileName} style={{ marginRight: '10px' }} />
|
||||
<Button disabled={!this.state.name} variant='contained' color='primary' onClick={() => { this.state.saveFile ? this.state.file === 'xml' ? this.saveXmlFile() : this.getSvg() : this.renameWorkspace(); this.toggleDialog(); }}>Eingabe</Button>
|
||||
</div>
|
||||
: null}
|
||||
</Dialog>
|
||||
|
||||
<Snackbar
|
||||
open={this.state.snackbar}
|
||||
message={this.state.message}
|
||||
type='success'
|
||||
key={this.state.key}
|
||||
/>
|
||||
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
WorkspaceFunc.propTypes = {
|
||||
arduino: PropTypes.string.isRequired,
|
||||
xml: PropTypes.string.isRequired,
|
||||
name: PropTypes.string,
|
||||
clearStats: PropTypes.func.isRequired,
|
||||
onChangeCode: PropTypes.func.isRequired,
|
||||
workspaceName: PropTypes.func.isRequired
|
||||
};
|
||||
|
||||
const mapStateToProps = state => ({
|
||||
arduino: state.workspace.code.arduino,
|
||||
xml: state.workspace.code.xml,
|
||||
name: state.workspace.name
|
||||
});
|
||||
|
||||
export default connect(mapStateToProps, { clearStats, onChangeCode, workspaceName })(withStyles(styles, { withTheme: true })(withWidth()(WorkspaceFunc)));
|
61
src/reducers/authReducer.js
Normal file
61
src/reducers/authReducer.js
Normal file
@ -0,0 +1,61 @@
|
||||
import { MYBADGES_CONNECT, MYBADGES_DISCONNECT, USER_LOADED, USER_LOADING, AUTH_ERROR, LOGIN_SUCCESS, LOGIN_FAIL, LOGOUT_SUCCESS, LOGOUT_FAIL, REFRESH_TOKEN_SUCCESS } from '../actions/types';
|
||||
|
||||
|
||||
const initialState = {
|
||||
token: localStorage.getItem('token'),
|
||||
refreshToken: localStorage.getItem('refreshToken'),
|
||||
isAuthenticated: null,
|
||||
progress: false,
|
||||
user: null
|
||||
};
|
||||
|
||||
export default function(state = initialState, action){
|
||||
switch(action.type){
|
||||
case USER_LOADING:
|
||||
return {
|
||||
...state,
|
||||
progress: true
|
||||
};
|
||||
case USER_LOADED:
|
||||
return {
|
||||
...state,
|
||||
isAuthenticated: true,
|
||||
progress: false,
|
||||
user: action.payload
|
||||
};
|
||||
case LOGIN_SUCCESS:
|
||||
case REFRESH_TOKEN_SUCCESS:
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
localStorage.setItem('refreshToken', action.payload.refreshToken);
|
||||
return {
|
||||
...state,
|
||||
user: action.payload.user,
|
||||
token: action.payload.token,
|
||||
refreshToken: action.payload.refreshToken,
|
||||
isAuthenticated: true,
|
||||
progress: false
|
||||
};
|
||||
case MYBADGES_CONNECT:
|
||||
case MYBADGES_DISCONNECT:
|
||||
return {
|
||||
...state,
|
||||
user: action.payload
|
||||
};
|
||||
case AUTH_ERROR:
|
||||
case LOGIN_FAIL:
|
||||
case LOGOUT_SUCCESS:
|
||||
case LOGOUT_FAIL:
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
return {
|
||||
...state,
|
||||
token: null,
|
||||
refreshToken: null,
|
||||
user: null,
|
||||
isAuthenticated: false,
|
||||
progress: false
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -3,12 +3,16 @@ import workspaceReducer from './workspaceReducer';
|
||||
import tutorialReducer from './tutorialReducer';
|
||||
import tutorialBuilderReducer from './tutorialBuilderReducer';
|
||||
import generalReducer from './generalReducer';
|
||||
import projectReducer from './projectReducer';
|
||||
import messageReducer from './messageReducer';
|
||||
import authReducer from './authReducer';
|
||||
|
||||
export default combineReducers({
|
||||
auth: authReducer,
|
||||
workspace: workspaceReducer,
|
||||
tutorial: tutorialReducer,
|
||||
builder: tutorialBuilderReducer,
|
||||
project: projectReducer,
|
||||
general: generalReducer,
|
||||
message: messageReducer
|
||||
});
|
||||
|
40
src/reducers/projectReducer.js
Normal file
40
src/reducers/projectReducer.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { PROJECT_PROGRESS, GET_PROJECT, GET_PROJECTS, PROJECT_TYPE, PROJECT_DESCRIPTION } from '../actions/types';
|
||||
|
||||
const initialState = {
|
||||
projects: [],
|
||||
type: '',
|
||||
description: '',
|
||||
progress: false
|
||||
};
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case PROJECT_PROGRESS:
|
||||
return {
|
||||
...state,
|
||||
progress: !state.progress
|
||||
}
|
||||
case GET_PROJECTS:
|
||||
return {
|
||||
...state,
|
||||
projects: action.payload
|
||||
};
|
||||
case GET_PROJECT:
|
||||
return {
|
||||
...state,
|
||||
projects: [action.payload]
|
||||
}
|
||||
case PROJECT_TYPE:
|
||||
return {
|
||||
...state,
|
||||
type: action.payload
|
||||
}
|
||||
case PROJECT_DESCRIPTION:
|
||||
return {
|
||||
...state,
|
||||
description: action.payload
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@ -1,10 +1,11 @@
|
||||
import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP,BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from '../actions/types';
|
||||
import { PROGRESS, JSON_STRING, BUILDER_CHANGE, BUILDER_ERROR, BUILDER_TITLE, BUILDER_BADGE, BUILDER_ID, BUILDER_ADD_STEP, BUILDER_DELETE_STEP, BUILDER_CHANGE_STEP,BUILDER_CHANGE_ORDER, BUILDER_DELETE_PROPERTY } from '../actions/types';
|
||||
|
||||
const initialState = {
|
||||
change: 0,
|
||||
progress: false,
|
||||
json: '',
|
||||
title: '',
|
||||
badge: '',
|
||||
id: '',
|
||||
steps: [
|
||||
{
|
||||
@ -33,6 +34,11 @@ export default function(state = initialState, action){
|
||||
...state,
|
||||
title: action.payload
|
||||
};
|
||||
case BUILDER_BADGE:
|
||||
return {
|
||||
...state,
|
||||
badge: action.payload
|
||||
};
|
||||
case BUILDER_ID:
|
||||
return {
|
||||
...state,
|
||||
|
Loading…
x
Reference in New Issue
Block a user