diff --git a/public/locales/en/translations.json b/public/locales/en/translations.json index 02220f0..920996b 100644 --- a/public/locales/en/translations.json +++ b/public/locales/en/translations.json @@ -20,7 +20,8 @@ "About": "About" }, "General": { - "Close": "Close" + "Close": "Close", + "Send": "Send" }, "Session": { "Title": "Sessions", @@ -59,6 +60,10 @@ "Domain": "Domain", "ActiveSession": "Active session", "ActiveSessionSubtitle": "Expand to see forwards", - "DDNSProvider": "Dynamic DNS Provider" + "DDNSProvider": "Dynamic DNS Provider", + "Actions": { + "SendMessageToAuthor": "Send message to author" + }, + "MessageForAuthor": "Message for author" } } diff --git a/public/locales/ro/translations.json b/public/locales/ro/translations.json index 37e323f..586381d 100644 --- a/public/locales/ro/translations.json +++ b/public/locales/ro/translations.json @@ -11,7 +11,8 @@ "About": "Despre" }, "General": { - "Close": "Închide" + "Close": "Închide", + "Send": "Trimite" }, "Session": { "Title": "Sesiuni", @@ -50,6 +51,10 @@ "Domain": "Domeniu", "ActiveSession": "Sesiune activă", "ActiveSessionSubtitle": "Extindeţi pentru a vedea redirectările", - "DDNSProvider": "Furnizor DNS dinamic" + "DDNSProvider": "Furnizor DNS dinamic", + "Actions": { + "SendMessageToAuthor": "Trimite mesaj către autor" + }, + "MessageForAuthor": "Mesaj pentru autor" } } diff --git a/src/components/App.js b/src/components/App.js index d41ba35..37c40f3 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,4 +1,5 @@ -import React, { Suspense } from "react"; +import React, { Suspense, useEffect } from "react"; +import PropTypes from "prop-types"; import { Route, Switch } from "react-router-dom"; import HomePage from "./home/HomePage"; import Header from "./layout/Header"; @@ -8,8 +9,15 @@ import "react-toastify/dist/ReactToastify.css"; import SessionContainer from "../features/session/components/SessionContainer"; import ReleaseNotesContainer from "../features/releaseNotes/components/ReleaseNotesContainer"; import AboutContainer from "../features/about/components/AboutContainer"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import { loadFrontendSession } from "../features/frontendSession/actionCreators"; + +function App({ actions }) { + useEffect(() => { + actions.loadFrontendSession(); + }, []); -function App() { const contentStyle = { paddingLeft: "30px", paddingRight: "30px" @@ -33,4 +41,18 @@ function App() { ); } -export default App; +App.propTypes = { + actions: PropTypes.object.isRequired +}; + +function mapStateToProps() { + return {}; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ loadFrontendSession }, dispatch) + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(App); diff --git a/src/features/frontendSession/actionCreators.js b/src/features/frontendSession/actionCreators.js new file mode 100644 index 0000000..8bc5654 --- /dev/null +++ b/src/features/frontendSession/actionCreators.js @@ -0,0 +1,18 @@ +import * as types from "./actionTypes"; +import api from "./api"; +import { sendHttpRequest } from "../../redux/actions/httpActions"; + +export function loadFrontendSession() { + return async function (dispatch) { + try { + dispatch({ type: types.INITIALIZE_FRONTEND_SESSION_STARTED }); + const data = await dispatch(sendHttpRequest(api.getFrontendSession())); + dispatch({ + type: types.INITIALIZE_FRONTEND_SESSION_SUCCESS, + payload: data + }); + } catch (error) { + throw error; + } + }; +} diff --git a/src/features/frontendSession/actionTypes.js b/src/features/frontendSession/actionTypes.js new file mode 100644 index 0000000..81a6c6f --- /dev/null +++ b/src/features/frontendSession/actionTypes.js @@ -0,0 +1,5 @@ +export const INITIALIZE_FRONTEND_SESSION_STARTED = + "INITIALIZE_FRONTEND_SESSION_STARTED"; + +export const INITIALIZE_FRONTEND_SESSION_SUCCESS = + "INITIALIZE_FRONTEND_SESSION_SUCCESS"; diff --git a/src/features/frontendSession/api.js b/src/features/frontendSession/api.js new file mode 100644 index 0000000..2e1f8ab --- /dev/null +++ b/src/features/frontendSession/api.js @@ -0,0 +1,8 @@ +import { get } from "../../api/axiosApi"; +const baseUrl = process.env.REVERSE_PROXY_API_URL; + +const getFrontendSession = () => get(`${baseUrl}/system/frontend-session`); + +export default { + getFrontendSession +}; diff --git a/src/features/frontendSession/reducer.js b/src/features/frontendSession/reducer.js new file mode 100644 index 0000000..1f42d0f --- /dev/null +++ b/src/features/frontendSession/reducer.js @@ -0,0 +1,26 @@ +import * as types from "./actionTypes"; +import initialState from "../../redux/reducers/initialState"; + +export default function frontendSessionReducer( + state = initialState.frontendSession, + action +) { + switch (action.type) { + case types.INITIALIZE_FRONTEND_SESSION_STARTED: + return { + loading: true, + loaded: false + }; + + case types.INITIALIZE_FRONTEND_SESSION_SUCCESS: + return { + ...state, + ...action.payload, + loading: false, + loaded: true + }; + + default: + return state; + } +} diff --git a/src/features/messageForAuthor/actionCreators.js b/src/features/messageForAuthor/actionCreators.js new file mode 100644 index 0000000..d7dd6df --- /dev/null +++ b/src/features/messageForAuthor/actionCreators.js @@ -0,0 +1,17 @@ +import * as types from "./actionTypes"; +import api from "./api"; +import { sendHttpRequest } from "../../redux/actions/httpActions"; + +export function saveMessageForAuthor(messageContent) { + return async function (dispatch, getState) { + try { + const sessionId = getState().frontendSession.sessionId; + const data = await dispatch( + sendHttpRequest(api.saveMessageForAuthor(sessionId, messageContent)) + ); + dispatch({ type: types.SAVE_MESSAGE_FOR_AUTHOR_SUCCESS, payload: data }); + } catch (error) { + throw error; + } + }; +} diff --git a/src/features/messageForAuthor/actionTypes.js b/src/features/messageForAuthor/actionTypes.js new file mode 100644 index 0000000..71d7550 --- /dev/null +++ b/src/features/messageForAuthor/actionTypes.js @@ -0,0 +1,2 @@ +export const SAVE_MESSAGE_FOR_AUTHOR_SUCCESS = + "SAVE_MESSAGE_FOR_AUTHOR_SUCCESS"; diff --git a/src/features/messageForAuthor/api.js b/src/features/messageForAuthor/api.js new file mode 100644 index 0000000..734599c --- /dev/null +++ b/src/features/messageForAuthor/api.js @@ -0,0 +1,9 @@ +import { post } from "../../api/axiosApi"; +const baseUrl = process.env.REVERSE_PROXY_API_URL; + +const saveMessageForAuthor = (sessionId, messageContent) => + post(`${baseUrl}/system/message-for-author`, { sessionId, messageContent }); + +export default { + saveMessageForAuthor +}; diff --git a/src/features/messageForAuthor/components/MessageForAuthorContainer.js b/src/features/messageForAuthor/components/MessageForAuthorContainer.js new file mode 100644 index 0000000..440a0cd --- /dev/null +++ b/src/features/messageForAuthor/components/MessageForAuthorContainer.js @@ -0,0 +1,52 @@ +import React, { useState } from "react"; +import { connect } from "react-redux"; +import { bindActionCreators } from "redux"; +import PropTypes from "prop-types"; +import MessageForAuthorDialog from "./MessageForAuthorDialog"; +import { saveMessageForAuthor } from "../actionCreators"; + +const MessageForAuthorContainer = ({ actions, open, handleClose }) => { + const [messageForAuthor, setMessageForAuthor] = useState(""); + + const onMessageForAuthorChanged = (event) => { + const value = event.target.value; + setMessageForAuthor(value); + }; + + const saveMessage = () => { + actions.saveMessageForAuthor(messageForAuthor); + setMessageForAuthor(""); + handleClose(); + }; + + return ( + + ); +}; + +MessageForAuthorContainer.propTypes = { + actions: PropTypes.object.isRequired, + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired +}; + +function mapStateToProps() { + return {}; +} + +function mapDispatchToProps(dispatch) { + return { + actions: bindActionCreators({ saveMessageForAuthor }, dispatch) + }; +} + +export default connect( + mapStateToProps, + mapDispatchToProps +)(MessageForAuthorContainer); diff --git a/src/features/messageForAuthor/components/MessageForAuthorDialog.js b/src/features/messageForAuthor/components/MessageForAuthorDialog.js new file mode 100644 index 0000000..4763e19 --- /dev/null +++ b/src/features/messageForAuthor/components/MessageForAuthorDialog.js @@ -0,0 +1,69 @@ +import React from "react"; +import PropTypes from "prop-types"; +import TextField from "@material-ui/core/TextField"; +import { + Dialog, + DialogContent, + IconButton, + Grid, + Tooltip +} from "@material-ui/core"; +import SendRoundedIcon from "@material-ui/icons/SendRounded"; +import { useTranslation } from "react-i18next"; + +const MessageForAuthorDialog = ({ + open, + handleClose, + messageForAuthor, + onMessageForAuthorChanged, + saveMessage +}) => { + const { t } = useTranslation(); + + return ( +
+ + + + + + + + + + + + + + + + +
+ ); +}; + +MessageForAuthorDialog.propTypes = { + open: PropTypes.bool.isRequired, + handleClose: PropTypes.func.isRequired, + messageForAuthor: PropTypes.string.isRequired, + onMessageForAuthorChanged: PropTypes.func.isRequired, + saveMessage: PropTypes.func.isRequired +}; + +export default MessageForAuthorDialog; diff --git a/src/features/server/components/ServerComponent.js b/src/features/server/components/ServerComponent.js index 4f6aa0a..3322d6d 100644 --- a/src/features/server/components/ServerComponent.js +++ b/src/features/server/components/ServerComponent.js @@ -10,13 +10,14 @@ import { Collapse, Avatar, IconButton, - Typography + Typography, + Tooltip } from "@material-ui/core"; -import FavoriteIcon from "@material-ui/icons/Favorite"; import ShareIcon from "@material-ui/icons/Share"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import MoreVertIcon from "@material-ui/icons/MoreVert"; import DnsRoundedIcon from "@material-ui/icons/DnsRounded"; +import MessageRoundedIcon from "@material-ui/icons/MessageRounded"; import styles from "../../../components/common/styles/expandableCardStyles"; import ServerSummary from "./ServerSummary"; import { useTranslation } from "react-i18next"; @@ -27,7 +28,8 @@ const ServerComponent = ({ data, serverHost, openAbout, - handleOpenInNewTab + handleOpenInNewTab, + showMessageForAuthor }) => { const classes = useStyles(); const { t } = useTranslation(); @@ -65,9 +67,11 @@ const ServerComponent = ({ )} - - - + + + + + @@ -121,7 +125,8 @@ ServerComponent.propTypes = { data: PropTypes.object.isRequired, serverHost: PropTypes.string, openAbout: PropTypes.func.isRequired, - handleOpenInNewTab: PropTypes.func.isRequired + handleOpenInNewTab: PropTypes.func.isRequired, + showMessageForAuthor: PropTypes.func.isRequired }; export default ServerComponent; diff --git a/src/features/server/components/ServerContainer.js b/src/features/server/components/ServerContainer.js index 1fe6601..4ca94b3 100644 --- a/src/features/server/components/ServerContainer.js +++ b/src/features/server/components/ServerContainer.js @@ -1,12 +1,23 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { connect } from "react-redux"; import { bindActionCreators } from "redux"; import PropTypes from "prop-types"; import { loadServerData, loadSystemVersion } from "../actionCreators"; import ServerComponent from "./ServerComponent"; import { withRouter } from "react-router-dom"; +import MessageForAuthorContainer from "../../messageForAuthor/components/MessageForAuthorContainer"; const ServerContainer = ({ actions, data, serverHost, history }) => { + const [openMessageForAuthor, setOpenMessageForAuthor] = useState(false); + + const closeMessageForAuthor = () => { + setOpenMessageForAuthor(false); + }; + + const showMessageForAuthor = () => { + setOpenMessageForAuthor(true); + }; + useEffect(() => { actions.loadServerData(); actions.loadSystemVersion(); @@ -23,12 +34,19 @@ const ServerContainer = ({ actions, data, serverHost, history }) => { }; return ( - + <> + + + ); }; diff --git a/src/redux/reducers/index.js b/src/redux/reducers/index.js index 4eee89e..e08ff3e 100644 --- a/src/redux/reducers/index.js +++ b/src/redux/reducers/index.js @@ -6,8 +6,10 @@ import { forwardsReducer } from "../../features/session/reducers"; import releaseNotesReducer from "../../features/releaseNotes/reducer"; +import frontendSessionReducer from "../../features/frontendSession/reducer"; const rootReducer = combineReducers({ + frontendSession: frontendSessionReducer, server: serverReducer, sessions: sessionsReducer, forwards: forwardsReducer, diff --git a/src/redux/reducers/initialState.js b/src/redux/reducers/initialState.js index 37e728f..52e380e 100644 --- a/src/redux/reducers/initialState.js +++ b/src/redux/reducers/initialState.js @@ -1,4 +1,5 @@ export default { + frontendSession: { loading: false, loaded: false }, server: { data: { loading: false, loaded: false }, activeSession: { loading: false, loaded: false }