mirror of
https://github.com/jambonz/jambonz-webapp.git
synced 2025-12-19 05:37:43 +00:00
Implement account setup
This commit is contained in:
16
.eslintrc
Normal file
16
.eslintrc
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"extends": "react-app",
|
||||
"rules": {
|
||||
"linebreak-style": [
|
||||
"error",
|
||||
"unix"
|
||||
],
|
||||
"semi": [
|
||||
"error",
|
||||
"always"
|
||||
],
|
||||
"no-trailing-spaces": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
||||
945
package-lock.json
generated
945
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -6,6 +6,7 @@
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
"@testing-library/react": "^9.5.0",
|
||||
"@testing-library/user-event": "^7.2.1",
|
||||
"axios": "^0.19.2",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-router-dom": "^5.1.2",
|
||||
@@ -13,7 +14,7 @@
|
||||
"styled-components": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "PORT=3001 react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
|
||||
30
src/App.js
30
src/App.js
@@ -1,10 +1,32 @@
|
||||
import React from 'react';
|
||||
import React, { useContext } from 'react';
|
||||
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
|
||||
import { NotificationStateContext } from './contexts/NotificationContext';
|
||||
import Login from './pages/Login';
|
||||
import CreatePassword from './pages/setup/CreatePassword';
|
||||
import ConfigureAccount from './pages/setup/ConfigureAccount';
|
||||
import CreateApplication from './pages/setup/CreateApplication';
|
||||
import ConfigureSipTrunk from './pages/setup/ConfigureSipTrunk';
|
||||
import SetupComplete from './pages/setup/SetupComplete';
|
||||
import AccountsList from './pages/internal/AccountsList';
|
||||
import Notification from './blocks/Notification';
|
||||
import Nav from './blocks/Nav';
|
||||
|
||||
function App() {
|
||||
const notifications = useContext(NotificationStateContext);
|
||||
return (
|
||||
<div>
|
||||
<h1>Jambonz</h1>
|
||||
</div>
|
||||
<Router>
|
||||
<Notification notifications={notifications} />
|
||||
<Nav />
|
||||
<Switch>
|
||||
<Route exact path="/"><Login /></Route>
|
||||
<Route exact path="/create-password"><CreatePassword /></Route>
|
||||
<Route exact path="/configure-account"><ConfigureAccount /></Route>
|
||||
<Route exact path="/create-application"><CreateApplication /></Route>
|
||||
<Route exact path="/configure-sip-trunk"><ConfigureSipTrunk /></Route>
|
||||
<Route exact path="/setup-complete"><SetupComplete /></Route>
|
||||
<Route exact path="/internal/accounts"><AccountsList /></Route>
|
||||
</Switch>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
45
src/blocks/FormError.js
Normal file
45
src/blocks/FormError.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ErrorIcon } from '../images/ErrorIcon.svg';
|
||||
|
||||
const FormErrorContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.25rem;
|
||||
background: RGBA(217, 28, 92, 0.2);
|
||||
${props => !props.grid && `margin-bottom: 1rem;`}
|
||||
color: #76042A;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
${props => props.grid && `grid-column: 2;`}
|
||||
& > span {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
& ul {
|
||||
margin: 0.25rem 0 0;
|
||||
padding-left: 1.5rem;
|
||||
}
|
||||
& li {
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const FormError = props => (
|
||||
<FormErrorContainer {...props}>
|
||||
<ErrorIcon />
|
||||
<span>
|
||||
{typeof props.message === 'object' && props.message.length ? (
|
||||
<ul>
|
||||
{props.message.map((message, i) => (
|
||||
<li key={i}>{message}</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
props.message
|
||||
)}
|
||||
</span>
|
||||
</FormErrorContainer>
|
||||
);
|
||||
|
||||
export default FormError;
|
||||
64
src/blocks/Nav.js
Normal file
64
src/blocks/Nav.js
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { useHistory, useLocation } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../contexts/NotificationContext';
|
||||
import Button from '../elements/Button';
|
||||
|
||||
const StyledNav = styled.nav`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.12);
|
||||
`;
|
||||
|
||||
const NavH1 = styled.h1`
|
||||
margin: 1.25rem 0 1.25rem 2rem;
|
||||
font-size: 1.5rem;
|
||||
font-weight: normal;
|
||||
line-height: 1em;
|
||||
`;
|
||||
|
||||
const LogOutContainer = styled.div`
|
||||
margin-right: 3rem;
|
||||
@media (max-width: 34rem) {
|
||||
margin-right: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const Nav = () => {
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
const logOut = () => {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: "You've successfully logged out",
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledNav>
|
||||
<NavH1>Jambonz</NavH1>
|
||||
{location.pathname !== '/' && (
|
||||
<LogOutContainer>
|
||||
<Button
|
||||
large
|
||||
gray
|
||||
text
|
||||
onClick={logOut}
|
||||
>
|
||||
Log Out
|
||||
</Button>
|
||||
</LogOutContainer>
|
||||
)}
|
||||
</StyledNav>
|
||||
);
|
||||
};
|
||||
|
||||
export default Nav;
|
||||
98
src/blocks/Notification.js
Normal file
98
src/blocks/Notification.js
Normal file
@@ -0,0 +1,98 @@
|
||||
import React, { useContext } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { NotificationDispatchContext } from '../contexts/NotificationContext';
|
||||
import { ReactComponent as CheckGreen } from '../images/CheckGreen.svg';
|
||||
|
||||
const NotificationContainer = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
padding-top: 2rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
pointer-events: none;
|
||||
z-index: 100;
|
||||
`;
|
||||
|
||||
const NotificationDiv = styled.div`
|
||||
margin-bottom: 1rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 28rem;
|
||||
padding: 0.75rem;
|
||||
background: #fff;
|
||||
border: 1px solid #61c43e;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
pointer-events: auto;
|
||||
& svg {
|
||||
flex-shrink: 0;
|
||||
margin-right: 0.75rem;
|
||||
}
|
||||
`;
|
||||
|
||||
const CloseButton = styled.button`
|
||||
display: flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
|
||||
flex-shrink: 0;
|
||||
margin-left: 0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 1.5rem;
|
||||
color: #767676;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
height: 2rem;
|
||||
width: 2rem;
|
||||
padding: 0.25rem;
|
||||
border: 2px solid transparent;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
border-color: #767676;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
color: #d91c5c;
|
||||
}
|
||||
`;
|
||||
|
||||
const Notification = props => {
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
return (
|
||||
<NotificationContainer>
|
||||
{props.notifications.map(n => (
|
||||
<NotificationDiv key={n.id}>
|
||||
<CheckGreen />
|
||||
<span>{n.message}</span>
|
||||
<CloseButton
|
||||
onClick={() => {
|
||||
dispatch({
|
||||
type: 'REMOVE',
|
||||
id: n.id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
<span tabIndex="-1">×</span>
|
||||
</CloseButton>
|
||||
</NotificationDiv>
|
||||
))}
|
||||
</NotificationContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Notification;
|
||||
113
src/blocks/ProgressVisualization.js
Normal file
113
src/blocks/ProgressVisualization.js
Normal file
@@ -0,0 +1,113 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Container = styled.div`
|
||||
height: 8rem;
|
||||
width: 45rem;
|
||||
max-width: 100%;
|
||||
padding: 3.5rem;
|
||||
@media (max-width: 30rem) {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const Step = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.8rem;
|
||||
font-weight: bold;
|
||||
color: #FFF;
|
||||
height: 1.5rem;
|
||||
width: 1.5rem;
|
||||
border-radius: 50%;
|
||||
${props => props.incomplete ? `
|
||||
border: 0.25rem solid #A6A6A6;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
inset 0 0 3px rgba(0, 0, 0, 0.08);
|
||||
` : `
|
||||
background: #D91C5C;
|
||||
box-shadow: 0 3px 3px rgba(0, 0, 0, 0.08),
|
||||
0 0 3px rgba(0, 0, 0, 0.08);
|
||||
`
|
||||
}
|
||||
z-index: 2;
|
||||
`;
|
||||
|
||||
const Line = styled.div`
|
||||
width: 50%;
|
||||
height: 0.25rem;
|
||||
margin: -2px;
|
||||
background: ${props => props.incomplete
|
||||
? '#A6A6A6'
|
||||
: '#D91C5C'
|
||||
};
|
||||
`;
|
||||
|
||||
const Checkmark = styled.div`
|
||||
height: 6px;
|
||||
width: 11px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
`;
|
||||
|
||||
const Title = styled.span`
|
||||
position: absolute;
|
||||
top: 2.5rem;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
@media (max-width: 30rem) {
|
||||
white-space: normal;
|
||||
}
|
||||
color: ${props => props.active ? '#D91C5C' : '#767676'};
|
||||
font-weight: ${props => props.active ? 'bold' : 'normal'};
|
||||
`;
|
||||
|
||||
const ProgressVisualization = props => (
|
||||
!props.progress
|
||||
? <Container />
|
||||
: <Container>
|
||||
<Step>
|
||||
{props.progress === 1
|
||||
? '1'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 1}>
|
||||
Configure Account
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 2} />
|
||||
<Step incomplete={props.progress < 2} >
|
||||
{props.progress < 2
|
||||
? null
|
||||
: props.progress === 2
|
||||
? '2'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 2}>
|
||||
Create Application
|
||||
</Title>
|
||||
</Step>
|
||||
<Line incomplete={props.progress < 3} />
|
||||
<Step incomplete={props.progress < 3} >
|
||||
{props.progress < 3
|
||||
? null
|
||||
: props.progress === 3
|
||||
? '3'
|
||||
: <Checkmark />
|
||||
}
|
||||
<Title active={props.progress === 3}>
|
||||
Configure SIP Trunk
|
||||
</Title>
|
||||
</Step>
|
||||
</Container>
|
||||
);
|
||||
|
||||
export default ProgressVisualization;
|
||||
27
src/contexts/NotificationContext.js
Normal file
27
src/contexts/NotificationContext.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import React, { createContext, useReducer } from 'react';
|
||||
import NotificationReducer from '../reducers/NotificationReducer';
|
||||
|
||||
export const NotificationStateContext = createContext();
|
||||
export const NotificationDispatchContext = createContext();
|
||||
|
||||
export function NotificationProvider(props) {
|
||||
|
||||
/*
|
||||
sample notification format
|
||||
{
|
||||
id: 1234,
|
||||
level: 'info',
|
||||
message: 'hello',
|
||||
};
|
||||
*/
|
||||
|
||||
const [state, dispatch] = useReducer(NotificationReducer, []);
|
||||
|
||||
return (
|
||||
<NotificationStateContext.Provider value={state}>
|
||||
<NotificationDispatchContext.Provider value={dispatch}>
|
||||
{props.children}
|
||||
</NotificationDispatchContext.Provider>
|
||||
</NotificationStateContext.Provider>
|
||||
);
|
||||
}
|
||||
130
src/data/SpeechRecognizerLanguageGoogle.js
Normal file
130
src/data/SpeechRecognizerLanguageGoogle.js
Normal file
@@ -0,0 +1,130 @@
|
||||
export default [
|
||||
{ name: 'Afrikaans (South Africa)', code: 'af-ZA', },
|
||||
{ name: 'Albanian (Albania)', code: 'sq-AL', },
|
||||
{ name: 'Amharic (Ethiopia)', code: 'am-ET', },
|
||||
{ name: 'Arabic (Algeria)', code: 'ar-DZ', },
|
||||
{ name: 'Arabic (Bahrain)', code: 'ar-BH', },
|
||||
{ name: 'Arabic (Egypt)', code: 'ar-EG', },
|
||||
{ name: 'Arabic (Iraq)', code: 'ar-IQ', },
|
||||
{ name: 'Arabic (Israel)', code: 'ar-IL', },
|
||||
{ name: 'Arabic (Jordan)', code: 'ar-JO', },
|
||||
{ name: 'Arabic (Kuwait)', code: 'ar-KW', },
|
||||
{ name: 'Arabic (Lebanon)', code: 'ar-LB', },
|
||||
{ name: 'Arabic (Morocco)', code: 'ar-MA', },
|
||||
{ name: 'Arabic (Oman)', code: 'ar-OM', },
|
||||
{ name: 'Arabic (Qatar)', code: 'ar-QA', },
|
||||
{ name: 'Arabic (Saudi Arabia)', code: 'ar-SA', },
|
||||
{ name: 'Arabic (State of Palestine)', code: 'ar-PS', },
|
||||
{ name: 'Arabic (Tunisia)', code: 'ar-TN', },
|
||||
{ name: 'Arabic (United Arab Emirates)', code: 'ar-AE', },
|
||||
{ name: 'Armenian (Armenia)', code: 'hy-AM', },
|
||||
{ name: 'Azerbaijani (Azerbaijan)', code: 'az-AZ', },
|
||||
{ name: 'Basque (Spain)', code: 'eu-ES', },
|
||||
{ name: 'Bengali (Bangladesh)', code: 'bn-BD', },
|
||||
{ name: 'Bengali (India)', code: 'bn-IN', },
|
||||
{ name: 'Bulgarian (Bulgaria)', code: 'bg-BG', },
|
||||
{ name: 'Burmese (Myanmar)', code: 'my-MM', },
|
||||
{ name: 'Catalan (Spain)', code: 'ca-ES', },
|
||||
{ name: 'Chinese, Cantonese (Traditional, Hong Kong)', code: 'yue-Hant-HK', },
|
||||
{ name: 'Chinese, Mandarin (Simplified, China)', code: 'zh', },
|
||||
{ name: 'Chinese, Mandarin (Simplified, Hong Kong)', code: 'zh-HK', },
|
||||
{ name: 'Chinese, Mandarin (Simplified, Taiwan)', code: 'zh-TW', },
|
||||
{ name: 'Croatian (Croatia)', code: 'hr-HR', },
|
||||
{ name: 'Czech (Czech Republic)', code: 'cs-CZ', },
|
||||
{ name: 'Danish (Denmark)', code: 'da-DK', },
|
||||
{ name: 'Dutch (Belgium)', code: 'nl-BE', },
|
||||
{ name: 'Dutch (Netherlands)', code: 'nl-NL', },
|
||||
{ name: 'English (Australia)', code: 'en-AU', },
|
||||
{ name: 'English (Canada)', code: 'en-CA', },
|
||||
{ name: 'English (Ghana)', code: 'en-GH', },
|
||||
{ name: 'English (India)', code: 'en-IN', },
|
||||
{ name: 'English (Ireland)', code: 'en-IE', },
|
||||
{ name: 'English (Kenya)', code: 'en-KE', },
|
||||
{ name: 'English (New Zealand)', code: 'en-NZ', },
|
||||
{ name: 'English (Nigeria)', code: 'en-NG', },
|
||||
{ name: 'English (Philippines)', code: 'en-PH', },
|
||||
{ name: 'English (Singapore)', code: 'en-SG', },
|
||||
{ name: 'English (South Africa)', code: 'en-ZA', },
|
||||
{ name: 'English (Tanzania)', code: 'en-TZ', },
|
||||
{ name: 'English (United Kingdom)', code: 'en-GB', },
|
||||
{ name: 'English (United States)', code: 'en-US', },
|
||||
{ name: 'Estonian (Estonia)', code: 'et-EE', },
|
||||
{ name: 'Filipino (Philippines)', code: 'fil-PH', },
|
||||
{ name: 'Finnish (Finland)', code: 'fi-FI', },
|
||||
{ name: 'French (Canada)', code: 'fr-CA', },
|
||||
{ name: 'French (France)', code: 'fr-FR', },
|
||||
{ name: 'Galician (Spain)', code: 'gl-ES', },
|
||||
{ name: 'Georgian (Georgia)', code: 'ka-GE', },
|
||||
{ name: 'German (Germany)', code: 'de-DE', },
|
||||
{ name: 'Greek (Greece)', code: 'el-GR', },
|
||||
{ name: 'Gujarati (India)', code: 'gu-IN', },
|
||||
{ name: 'Hebrew (Israel)', code: 'he-IL', },
|
||||
{ name: 'Hindi (India)', code: 'hi-IN', },
|
||||
{ name: 'Hungarian (Hungary)', code: 'hu-HU', },
|
||||
{ name: 'Icelandic (Iceland)', code: 'is-IS', },
|
||||
{ name: 'Indonesian (Indonesia)', code: 'id-ID', },
|
||||
{ name: 'Italian (Italy)', code: 'it-IT', },
|
||||
{ name: 'Japanese (Japan)', code: 'ja-JP', },
|
||||
{ name: 'Javanese (Indonesia)', code: 'jv-ID', },
|
||||
{ name: 'Kannada (India)', code: 'kn-IN', },
|
||||
{ name: 'Khmer (Cambodia)', code: 'km-KH', },
|
||||
{ name: 'Korean (South Korea)', code: 'ko-KR', },
|
||||
{ name: 'Lao (Laos)', code: 'lo-LA', },
|
||||
{ name: 'Latvian (Latvia)', code: 'lv-LV', },
|
||||
{ name: 'Lithuanian (Lithuania)', code: 'lt-LT', },
|
||||
{ name: 'Macedonian (North Macedonia)', code: 'mk-MK', },
|
||||
{ name: 'Malay (Malaysia)', code: 'ms-MY', },
|
||||
{ name: 'Malayalam (India)', code: 'ml-IN', },
|
||||
{ name: 'Marathi (India)', code: 'mr-IN', },
|
||||
{ name: 'Mongolian (Mongolia)', code: 'mn-MN', },
|
||||
{ name: 'Nepali (Nepal)', code: 'ne-NP', },
|
||||
{ name: 'Norwegian Bokmål (Norway)', code: 'nb-NO', },
|
||||
{ name: 'Persian (Iran)', code: 'fa-IR', },
|
||||
{ name: 'Polish (Poland)', code: 'pl-PL', },
|
||||
{ name: 'Portuguese (Brazil)', code: 'pt-BR', },
|
||||
{ name: 'Portuguese (Portugal)', code: 'pt-PT', },
|
||||
{ name: 'Punjabi (Gurmukhi, India)', code: 'pa-guru-IN', },
|
||||
{ name: 'Romanian (Romania)', code: 'ro-RO', },
|
||||
{ name: 'Russian (Russia)', code: 'ru-RU', },
|
||||
{ name: 'Serbian (Serbia)', code: 'sr-RS', },
|
||||
{ name: 'Sinhala (Sri Lanka)', code: 'si-LK', },
|
||||
{ name: 'Slovak (Slovakia)', code: 'sk-SK', },
|
||||
{ name: 'Slovenian (Slovenia)', code: 'sl-SI', },
|
||||
{ name: 'Spanish (Argentina)', code: 'es-AR', },
|
||||
{ name: 'Spanish (Bolivia)', code: 'es-BO', },
|
||||
{ name: 'Spanish (Chile)', code: 'es-CL', },
|
||||
{ name: 'Spanish (Colombia)', code: 'es-CO', },
|
||||
{ name: 'Spanish (Costa Rica)', code: 'es-CR', },
|
||||
{ name: 'Spanish (Dominican Republic)', code: 'es-DO', },
|
||||
{ name: 'Spanish (Ecuador)', code: 'es-EC', },
|
||||
{ name: 'Spanish (El Salvador)', code: 'es-SV', },
|
||||
{ name: 'Spanish (Guatemala)', code: 'es-GT', },
|
||||
{ name: 'Spanish (Honduras)', code: 'es-HN', },
|
||||
{ name: 'Spanish (Mexico)', code: 'es-MX', },
|
||||
{ name: 'Spanish (Nicaragua)', code: 'es-NI', },
|
||||
{ name: 'Spanish (Panama)', code: 'es-PA', },
|
||||
{ name: 'Spanish (Paraguay)', code: 'es-PY', },
|
||||
{ name: 'Spanish (Peru)', code: 'es-PE', },
|
||||
{ name: 'Spanish (Puerto Rico)', code: 'es-PR', },
|
||||
{ name: 'Spanish (Spain)', code: 'es-ES', },
|
||||
{ name: 'Spanish (United States)', code: 'es-US', },
|
||||
{ name: 'Spanish (Uruguay)', code: 'es-UY', },
|
||||
{ name: 'Spanish (Venezuela)', code: 'es-VE', },
|
||||
{ name: 'Sundanese (Indonesia)', code: 'su-ID', },
|
||||
{ name: 'Swahili (Kenya)', code: 'sw-KE', },
|
||||
{ name: 'Swahili (Tanzania)', code: 'sw-TZ', },
|
||||
{ name: 'Swedish (Sweden)', code: 'sv-SE', },
|
||||
{ name: 'Tamil (India)', code: 'ta-IN', },
|
||||
{ name: 'Tamil (Malaysia)', code: 'ta-MY', },
|
||||
{ name: 'Tamil (Singapore)', code: 'ta-SG', },
|
||||
{ name: 'Tamil (Sri Lanka)', code: 'ta-LK', },
|
||||
{ name: 'Telugu (India)', code: 'te-IN', },
|
||||
{ name: 'Thai (Thailand)', code: 'th-TH', },
|
||||
{ name: 'Turkish (Turkey)', code: 'tr-TR', },
|
||||
{ name: 'Ukrainian (Ukraine)', code: 'uk-UA', },
|
||||
{ name: 'Urdu (India)', code: 'ur-IN', },
|
||||
{ name: 'Urdu (Pakistan)', code: 'ur-PK', },
|
||||
{ name: 'Uzbek (Uzbekistan)', code: 'uz-UZ', },
|
||||
{ name: 'Vietnamese (Vietnam)', code: 'vi-VN', },
|
||||
{ name: 'Zulu (South Africa)', code: 'zu-ZA', },
|
||||
];
|
||||
237
src/data/SpeechSynthesisLanguageAws.js
Normal file
237
src/data/SpeechSynthesisLanguageAws.js
Normal file
@@ -0,0 +1,237 @@
|
||||
export default [
|
||||
{
|
||||
code: 'arb',
|
||||
name: 'Arabic',
|
||||
voices: [
|
||||
{ value: 'Zeina', name: 'Zeina (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'cmn-CN',
|
||||
name: 'Chinese, Mandarin',
|
||||
voices: [
|
||||
{ value: 'Zhiyu', name: 'Zhiyu (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'da-DK',
|
||||
name: 'Danish',
|
||||
voices: [
|
||||
{ value: 'Naja', name: 'Naja (Female)'},
|
||||
{ value: 'Mads', name: 'Mads (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'nl-NL',
|
||||
name: 'Dutch',
|
||||
voices: [
|
||||
{ value: 'Lotte', name: 'Lotte (Female)'},
|
||||
{ value: 'Ruben', name: 'Ruben (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-AU',
|
||||
name: 'English (Australian)',
|
||||
voices: [
|
||||
{ value: 'Nicole', name: 'Nicole (Female)'},
|
||||
{ value: 'Russell', name: 'Russell (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-GB',
|
||||
name: 'English (British)',
|
||||
voices: [
|
||||
{ value: 'Amy', name: 'Amy (Female)'},
|
||||
{ value: 'Emma', name: 'Emma (Female)'},
|
||||
{ value: 'Brian', name: 'Brian (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-IN',
|
||||
name: 'English (Indian)',
|
||||
voices: [
|
||||
{ value: 'Aditi', name: 'Aditi (Female)'},
|
||||
{ value: 'Raveena', name: 'Raveena (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: [
|
||||
{ value: 'Joanna', name: 'Joanna (Female)'},
|
||||
{ value: 'Kendra', name: 'Kendra (Female)'},
|
||||
{ value: 'Kimberly', name: 'Kimberly (Female)'},
|
||||
{ value: 'Ivy', name: 'Ivy (Female child)'},
|
||||
{ value: 'Salli', name: 'Salli (Female)'},
|
||||
{ value: 'Joey', name: 'Joey (Male)'},
|
||||
{ value: 'Matthew', name: 'Matthew (Male)'},
|
||||
{ value: 'Justin', name: 'Justin (Male child)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-GB-WLS',
|
||||
name: 'English (Welsh)',
|
||||
voices: [
|
||||
{ value: 'Geraint', name: 'Geraint (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fr-FR',
|
||||
name: 'French',
|
||||
voices: [
|
||||
{ value: 'Celine', name: 'Céline (Female)'},
|
||||
{ value: 'Lea', name: 'Léa (Female)'},
|
||||
{ value: 'Mathieu', name: 'Mathieu (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fr-CA',
|
||||
name: 'French (Canadian)',
|
||||
voices: [
|
||||
{ value: 'Chantal', name: 'Chantal (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'de-DE',
|
||||
name: 'German',
|
||||
voices: [
|
||||
{ value: 'Marlene', name: 'Marlene (Female)'},
|
||||
{ value: 'Vicki', name: 'Vicki (Female)'},
|
||||
{ value: 'Hans', name: 'Hans (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'hi-IN',
|
||||
name: 'Hindi',
|
||||
voices: [
|
||||
{ value: 'Aditi', name: 'Aditi (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'is-IS',
|
||||
name: 'Icelandic',
|
||||
voices: [
|
||||
{ value: 'Dora', name: 'Dóra (Female)'},
|
||||
{ value: 'Karl', name: 'Karl (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'it-IT',
|
||||
name: 'Italian',
|
||||
voices: [
|
||||
{ value: 'Carla', name: 'Carla (Female)'},
|
||||
{ value: 'Bianca', name: 'Bianca (Female)'},
|
||||
{ value: 'Giorgio', name: 'Giorgio (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ja-JP',
|
||||
name: 'Japanese',
|
||||
voices: [
|
||||
{ value: 'Mizuki', name: 'Mizuki (Female)'},
|
||||
{ value: 'Takumi', name: 'Takumi (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ko-KR',
|
||||
name: 'Korean',
|
||||
voices: [
|
||||
{ value: 'Seoyeon', name: 'Seoyeon (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'nb-NO',
|
||||
name: 'Norwegian',
|
||||
voices: [
|
||||
{ value: 'Liv', name: 'Liv (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pl-PL',
|
||||
name: 'Polish',
|
||||
voices: [
|
||||
{ value: 'Ewa', name: 'Ewa (Female)'},
|
||||
{ value: 'Maja', name: 'Maja (Female)'},
|
||||
{ value: 'Jacek', name: 'Jacek (Male)'},
|
||||
{ value: 'Jan', name: 'Jan (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pt-BR',
|
||||
name: 'Portuguese (Brazilian)',
|
||||
voices: [
|
||||
{ value: 'Camila', name: 'Camila (Female)'},
|
||||
{ value: 'Vitoria', name: 'Vitória (Female)'},
|
||||
{ value: 'Ricardo', name: 'Ricardo (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pt-PT',
|
||||
name: 'Portuguese (European)',
|
||||
voices: [
|
||||
{ value: 'Ines', name: 'Inês (Female)'},
|
||||
{ value: 'Cristiano', name: 'Cristiano (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ro-RO',
|
||||
name: 'Romanian',
|
||||
voices: [
|
||||
{ value: 'Carmen', name: 'Carmen (Female)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ru-RU',
|
||||
name: 'Russian',
|
||||
voices: [
|
||||
{ value: 'Tatyana', name: 'Tatyana (Female)'},
|
||||
{ value: 'Maxim', name: 'Maxim (Male)'},
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'es-ES',
|
||||
name: 'Spanish (European)',
|
||||
voices: [
|
||||
{ value: 'Conchita', name: 'Conchita (Female)' },
|
||||
{ value: 'Lucia', name: 'Lucia (Female)' },
|
||||
{ value: 'Enrique', name: 'Enrique (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'es-MX',
|
||||
name: 'Spanish (Mexican)',
|
||||
voices: [
|
||||
{ value: 'Mia', name: 'Mia (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'es-US',
|
||||
name: 'Spanish (US)',
|
||||
voices: [
|
||||
{ value: 'Lupe', name: 'Lupe (Female)' },
|
||||
{ value: 'Penelope', name: 'Penélope (Female)' },
|
||||
{ value: 'Miguel', name: 'Miguel (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'sv-SE',
|
||||
name: 'Swedish',
|
||||
voices: [
|
||||
{ value: 'Astrid', name: 'Astrid (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'tr-TR',
|
||||
name: 'Turkish',
|
||||
voices: [
|
||||
{ value: 'Filiz', name: 'Filiz (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'cy-GB',
|
||||
name: 'Welsh',
|
||||
voices: [
|
||||
{ value: 'Gwyneth', name: 'Gwyneth (Female)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
410
src/data/SpeechSynthesisLanguageGoogle.js
Normal file
410
src/data/SpeechSynthesisLanguageGoogle.js
Normal file
@@ -0,0 +1,410 @@
|
||||
export default [
|
||||
{
|
||||
code: 'ar-XA',
|
||||
name: 'Arabic',
|
||||
voices: [
|
||||
{ value: 'ar-XA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ar-XA-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ar-XA-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ar-XA-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'ar-XA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ar-XA-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ar-XA-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'cs-CZ',
|
||||
name: 'Czech (Czech Republic)',
|
||||
voices: [
|
||||
{ value: 'cs-CZ-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'cs-CZ-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'da-DK',
|
||||
name: 'Danish (Denmark)',
|
||||
voices: [
|
||||
{ value: 'da-DK-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'da-DK-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'nl-NL',
|
||||
name: 'Dutch (Netherlands)',
|
||||
voices: [
|
||||
{ value: 'nl-NL-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'nl-NL-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'nl-NL-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'nl-NL-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'nl-NL-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'nl-NL-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'nl-NL-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'nl-NL-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-AU',
|
||||
name: 'English (Australia)',
|
||||
voices: [
|
||||
{ value: 'en-AU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-AU-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-AU-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-AU-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-AU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-AU-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-AU-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-AU-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-IN',
|
||||
name: 'English (India)',
|
||||
voices: [
|
||||
{ value: 'en-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-IN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'en-IN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'en-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-IN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'en-IN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-GB',
|
||||
name: 'English (UK)',
|
||||
voices: [
|
||||
{ value: 'en-GB-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'en-GB-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-GB-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-GB-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-GB-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'en-GB-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-GB-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-GB-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'en-US',
|
||||
name: 'English (US)',
|
||||
voices: [
|
||||
{ value: 'en-US-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'en-US-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'en-US-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'en-US-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'en-US-Wavenet-A', name: 'Wavenet-A (Male)' },
|
||||
{ value: 'en-US-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'en-US-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'en-US-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'en-US-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
{ value: 'en-US-Wavenet-F', name: 'Wavenet-F (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fil-PH',
|
||||
name: 'Filipino (Philippines)',
|
||||
voices: [
|
||||
{ value: 'fil-PH-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fil-PH-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fi-FI',
|
||||
name: 'Finnish (Finland)',
|
||||
voices: [
|
||||
{ value: 'fi-FI-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fi-FI-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fr-CA',
|
||||
name: 'French (Canada)',
|
||||
voices: [
|
||||
{ value: 'fr-CA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fr-CA-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'fr-CA-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'fr-CA-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'fr-CA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'fr-CA-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'fr-CA-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'fr-CA-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'fr-FR',
|
||||
name: 'French (France)',
|
||||
voices: [
|
||||
{ value: 'fr-FR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'fr-FR-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'fr-FR-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'fr-FR-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'fr-FR-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'fr-FR-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'fr-FR-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'fr-FR-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'de-DE',
|
||||
name: 'German (Germany)',
|
||||
voices: [
|
||||
{ value: 'de-DE-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'de-DE-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'de-DE-Standard-E', name: 'Standard-E (Male)' },
|
||||
{ value: 'de-DE-Standard-F', name: 'Standard-F (Female)' },
|
||||
{ value: 'de-DE-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'de-DE-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'de-DE-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'de-DE-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'de-DE-Wavenet-E', name: 'Wavenet-E (Male)' },
|
||||
{ value: 'de-DE-Wavenet-F', name: 'Wavenet-F (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'el-GR',
|
||||
name: 'Greek (Greece)',
|
||||
voices: [
|
||||
{ value: 'el-GR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'el-GR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'hi-IN',
|
||||
name: 'Hindi (India)',
|
||||
voices: [
|
||||
{ value: 'hi-IN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'hi-IN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'hi-IN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'hi-IN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'hi-IN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'hi-IN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'hi-IN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'hi-IN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'hu-HU',
|
||||
name: 'Hungarian (Hungary)',
|
||||
voices: [
|
||||
{ value: 'hu-HU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'hu-HU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'id-ID',
|
||||
name: 'Indonesian (Indonesia)',
|
||||
voices: [
|
||||
{ value: 'id-ID-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'id-ID-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'id-ID-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'id-ID-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'id-ID-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'id-ID-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'id-ID-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'id-ID-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'it-IT',
|
||||
name: 'Italian (Italy)',
|
||||
voices: [
|
||||
{ value: 'it-IT-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'it-IT-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'it-IT-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'it-IT-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'it-IT-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'it-IT-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'it-IT-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'it-IT-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ja-JP',
|
||||
name: 'Japanese (Japan)',
|
||||
voices: [
|
||||
{ value: 'ja-JP-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ja-JP-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'ja-JP-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ja-JP-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ja-JP-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ja-JP-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'ja-JP-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'ja-JP-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ko-KR',
|
||||
name: 'Korean (South Korea)',
|
||||
voices: [
|
||||
{ value: 'ko-KR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ko-KR-Standard-B', name: 'Standard-B (Female)' },
|
||||
{ value: 'ko-KR-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'ko-KR-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ko-KR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ko-KR-Wavenet-B', name: 'Wavenet-B (Female)' },
|
||||
{ value: 'ko-KR-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'ko-KR-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'cmn-CN',
|
||||
name: 'Mandarin Chinese',
|
||||
voices: [
|
||||
{ value: 'cmn-CN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'cmn-CN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'cmn-CN-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'cmn-CN-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'cmn-CN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'cmn-CN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'cmn-CN-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'cmn-CN-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'cmn-TW',
|
||||
name: 'Mandarin Chinese (Traditional)',
|
||||
voices: [
|
||||
{ value: 'cmn-TW-Standard-A-Alpha', name: 'Standard-A-Alpha (Female)' },
|
||||
{ value: 'cmn-TW-Standard-B-Alpha', name: 'Standard-B-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Standard-C-Alpha', name: 'Standard-C-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Wavenet-A-Alpha', name: 'Wavenet-A-Alpha (Female)' },
|
||||
{ value: 'cmn-TW-Wavenet-B-Alpha', name: 'Wavenet-B-Alpha (Male)' },
|
||||
{ value: 'cmn-TW-Wavenet-C-Alpha', name: 'Wavenet-C-Alpha (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'nb-NO',
|
||||
name: 'Norwegian (Norway)',
|
||||
voices: [
|
||||
{ value: 'nb-NO-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'nb-NO-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'nb-NO-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'nb-NO-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'nb-no-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'nb-NO-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'nb-NO-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'nb-no-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pl-PL',
|
||||
name: 'Polish (Poland)',
|
||||
voices: [
|
||||
{ value: 'pl-PL-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pl-PL-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pl-PL-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'pl-PL-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'pl-PL-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pl-PL-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'pl-PL-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'pl-PL-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pt-BR',
|
||||
name: 'Portuguese (Brazil)',
|
||||
voices: [
|
||||
{ value: 'pt-BR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pt-BR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'pt-PT',
|
||||
name: 'Portuguese (Portugal)',
|
||||
voices: [
|
||||
{ value: 'pt-PT-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'pt-PT-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'pt-PT-Standard-C', name: 'Standard-C (Male)' },
|
||||
{ value: 'pt-PT-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'pt-PT-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'pt-PT-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'pt-PT-Wavenet-C', name: 'Wavenet-C (Male)' },
|
||||
{ value: 'pt-PT-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'ru-RU',
|
||||
name: 'Russian (Russia)',
|
||||
voices: [
|
||||
{ value: 'ru-RU-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'ru-RU-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'ru-RU-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'ru-RU-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'ru-RU-Standard-E', name: 'Standard-E (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'ru-RU-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'ru-RU-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
{ value: 'ru-RU-Wavenet-E', name: 'Wavenet-E (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'sk-SK',
|
||||
name: 'Slovak (Slovakia)',
|
||||
voices: [
|
||||
{ value: 'sk-SK-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'sk-SK-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'es-ES',
|
||||
name: 'Spanish (Spain)',
|
||||
voices: [
|
||||
{ value: 'es-ES-Standard-A', name: 'Standard-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'sv-SE',
|
||||
name: 'Swedish (Sweden)',
|
||||
voices: [
|
||||
{ value: 'sv-SE-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'sv-SE-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'tr-TR',
|
||||
name: 'Turkish (Turkey)',
|
||||
voices: [
|
||||
{ value: 'tr-TR-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'tr-TR-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'tr-TR-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'tr-TR-Standard-D', name: 'Standard-D (Female)' },
|
||||
{ value: 'tr-TR-Standard-E', name: 'Standard-E (Male)' },
|
||||
{ value: 'tr-TR-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'tr-TR-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-D', name: 'Wavenet-D (Female)' },
|
||||
{ value: 'tr-TR-Wavenet-E', name: 'Wavenet-E (Male)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'uk-UA',
|
||||
name: 'Ukrainian (Ukraine)',
|
||||
voices: [
|
||||
{ value: 'uk-UA-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'uk-UA-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
],
|
||||
},
|
||||
{
|
||||
code: 'vi-VN',
|
||||
name: 'Vietnamese (Vietnam)',
|
||||
voices: [
|
||||
{ value: 'vi-VN-Standard-A', name: 'Standard-A (Female)' },
|
||||
{ value: 'vi-VN-Standard-B', name: 'Standard-B (Male)' },
|
||||
{ value: 'vi-VN-Standard-C', name: 'Standard-C (Female)' },
|
||||
{ value: 'vi-VN-Standard-D', name: 'Standard-D (Male)' },
|
||||
{ value: 'vi-VN-Wavenet-A', name: 'Wavenet-A (Female)' },
|
||||
{ value: 'vi-VN-Wavenet-B', name: 'Wavenet-B (Male)' },
|
||||
{ value: 'vi-VN-Wavenet-C', name: 'Wavenet-C (Female)' },
|
||||
{ value: 'vi-VN-Wavenet-D', name: 'Wavenet-D (Male)' },
|
||||
],
|
||||
},
|
||||
];
|
||||
134
src/elements/Button.js
Normal file
134
src/elements/Button.js
Normal file
@@ -0,0 +1,134 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
grid-column: 2;
|
||||
${props => props.fullWidth
|
||||
? `width: 100%;`
|
||||
: `justify-self: start;`
|
||||
}
|
||||
${props => props.bottomGap && `
|
||||
margin-bottom: 1rem;
|
||||
`}
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
height: ${
|
||||
props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
${props => props.fullWidth && `
|
||||
width: 100%;
|
||||
`}
|
||||
${props => props.square
|
||||
? `width: 2.25rem;`
|
||||
: `padding: 0 1rem;`
|
||||
}
|
||||
border-radius: 0.25rem;
|
||||
background: ${
|
||||
props => props.text
|
||||
? 'none'
|
||||
: props.gray
|
||||
? '#E3E3E3'
|
||||
: '#D91C5C'
|
||||
};
|
||||
color: ${
|
||||
props => props.gray
|
||||
? '#565656'
|
||||
: props.text
|
||||
? '#D91C5C'
|
||||
: '#FFF'
|
||||
};
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12),
|
||||
inset 0 0 0
|
||||
${props => props.text
|
||||
? '0.125rem'
|
||||
: '0.25rem'
|
||||
}
|
||||
${props => props.gray
|
||||
? '#767676'
|
||||
: '#890934'
|
||||
};
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
${props => props.text
|
||||
? `background: #E3E3E3;`
|
||||
: 'background: #BD164E;'
|
||||
}
|
||||
}
|
||||
|
||||
&:active > span {
|
||||
${props => props.text
|
||||
? `background: #D5D5D5;`
|
||||
: 'background: #A40D40;'
|
||||
}
|
||||
}
|
||||
|
||||
${props => props.formLink && `
|
||||
justify-self: start;
|
||||
|
||||
& > span {
|
||||
height: auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
background: none;
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:active > span {
|
||||
background: none;
|
||||
}
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
`;
|
||||
|
||||
const Button = (props, ref) => {
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Button);
|
||||
141
src/elements/Checkbox.js
Normal file
141
src/elements/Checkbox.js
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import Label from './Label';
|
||||
|
||||
const CheckboxContainer = styled.div`
|
||||
margin-left: ${props => props.invalid
|
||||
? '0.5rem'
|
||||
: '1rem'
|
||||
};
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 3rem;
|
||||
${props => props.invalid && `
|
||||
margin-right: -0.5rem;
|
||||
padding: 0 0.5rem;
|
||||
border: 1px solid #D91C5C;
|
||||
border-radius: 0.125rem;
|
||||
background: RGBA(217,28,92,0.2);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledCheckbox = styled.input`
|
||||
margin: 0.25rem;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
`;
|
||||
|
||||
const StyledLabel = styled(Label)`
|
||||
margin-left: 0.5rem;
|
||||
cursor: pointer;
|
||||
${props => props.tooltip && `
|
||||
& > span {
|
||||
border-bottom: 1px dotted;
|
||||
border-left: 1px solid transparent;
|
||||
cursor: help;
|
||||
}
|
||||
`}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
left: ${props => props.invalid
|
||||
? '0.5rem'
|
||||
: '0'
|
||||
};
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border: 1px solid #A5A5A5;
|
||||
border-radius: 0.125rem;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
input:focus + &::before {
|
||||
border-color: #565656;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
input:checked + &::before {
|
||||
background: #D91C5C;
|
||||
border-color: #D91C5C;
|
||||
}
|
||||
|
||||
input:checked:focus + &::before {
|
||||
border: 3px solid #890934;
|
||||
}
|
||||
|
||||
input:checked + &::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 1.1rem;
|
||||
left: ${props => props.invalid
|
||||
? '0.75rem'
|
||||
: '0.25rem'
|
||||
};
|
||||
height: 8px;
|
||||
width: 15px;
|
||||
border-left: 2px solid #FFF;
|
||||
border-bottom: 2px solid #FFF;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
`;
|
||||
|
||||
const Tooltip = styled.span`
|
||||
display: none;
|
||||
label > span:hover > & {
|
||||
display: inline;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
bottom: calc(100%);
|
||||
right: calc(50% - 1rem);
|
||||
transform: translateX(50%);
|
||||
padding: 0.75rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
background: #FFF;
|
||||
box-shadow: 0 0.375rem 0.25rem rgba(0, 0, 0, 0.12),
|
||||
0 0 0.25rem rgba(0, 0, 0, 0.18);
|
||||
z-index: 101;
|
||||
}
|
||||
`;
|
||||
|
||||
const Checkbox = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<CheckboxContainer
|
||||
invalid={props.invalid}
|
||||
>
|
||||
<StyledCheckbox
|
||||
id={props.id}
|
||||
name={props.id}
|
||||
type="checkbox"
|
||||
checked={props.checked}
|
||||
onChange={props.onChange}
|
||||
ref={inputRef}
|
||||
/>
|
||||
<StyledLabel
|
||||
htmlFor={props.id}
|
||||
tooltip={props.tooltip}
|
||||
invalid={props.invalid}
|
||||
>
|
||||
<span>
|
||||
{props.label}
|
||||
{
|
||||
props.tooltip &&
|
||||
<Tooltip>
|
||||
{props.tooltip}
|
||||
</Tooltip>
|
||||
}
|
||||
</span>
|
||||
</StyledLabel>
|
||||
</CheckboxContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Checkbox);
|
||||
37
src/elements/Form.js
Normal file
37
src/elements/Form.js
Normal file
@@ -0,0 +1,37 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Form = styled.form`
|
||||
width: ${props => props.large
|
||||
? '61rem'
|
||||
: '32rem'
|
||||
};
|
||||
text-align: right;
|
||||
@media (max-width: ${props => props.large
|
||||
? '61rem'
|
||||
: '32rem'
|
||||
}) {
|
||||
width: 100%;
|
||||
}
|
||||
padding: 2rem;
|
||||
${props => !props.large && `
|
||||
& input {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`}
|
||||
& hr {
|
||||
margin: 0 -2rem;
|
||||
background: none;
|
||||
border: 0;
|
||||
border-top: 1px solid #C6C6C6;
|
||||
grid-column: 1 / 3;
|
||||
}
|
||||
${props => props.large && `
|
||||
display: grid;
|
||||
grid-template-columns: 1.3fr 10fr;
|
||||
grid-row-gap: 1rem;
|
||||
grid-column-gap: 0.75rem;
|
||||
align-items: center;
|
||||
`}
|
||||
`;
|
||||
|
||||
export default Form;
|
||||
9
src/elements/H1.js
Normal file
9
src/elements/H1.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const H1 = styled.h1`
|
||||
font-size: 3rem;
|
||||
margin: 1rem 0 0;
|
||||
font-weight: normal;
|
||||
`;
|
||||
|
||||
export default H1;
|
||||
106
src/elements/Input.js
Normal file
106
src/elements/Input.js
Normal file
@@ -0,0 +1,106 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as ViewPassword } from '../images/ViewPassword.svg';
|
||||
import { ReactComponent as HidePassword } from '../images/HidePassword.svg';
|
||||
|
||||
const Container = styled.div`
|
||||
position: relative;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
${props => props.width && `
|
||||
width: ${props.width};
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledInput = styled.input`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
width: 100%;
|
||||
padding: 0 1rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
${props => props.invalid && `
|
||||
background: RGBA(217,28,92,0.2);
|
||||
border-color: #D91C5C;
|
||||
`}
|
||||
border-radius: 0.125rem;
|
||||
color: inherit;
|
||||
&:focus {
|
||||
border-color: ${props => props.invalid
|
||||
? '#890934;'
|
||||
: '#565656;'
|
||||
}
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
`;
|
||||
|
||||
const PasswordButton = styled.button`
|
||||
position: absolute;
|
||||
top: 0.375rem;
|
||||
right: 1rem;
|
||||
height: 2.25rem;
|
||||
width: 2.5rem;
|
||||
cursor: pointer;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
padding: 0;
|
||||
|
||||
& > span {
|
||||
height: 2.25rem;
|
||||
width: 2.5rem;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
outline: 0;
|
||||
border-radius: 0.25rem;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
fill: #BD164E;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #890934;
|
||||
}
|
||||
`;
|
||||
|
||||
const Input = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<Container
|
||||
width={props.width}
|
||||
>
|
||||
<StyledInput
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
/>
|
||||
{
|
||||
props.allowShowPassword &&
|
||||
<PasswordButton
|
||||
type="button"
|
||||
onClick={props.toggleShowPassword}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
{
|
||||
props.showPassword
|
||||
? <HidePassword />
|
||||
: <ViewPassword />
|
||||
}
|
||||
</span>
|
||||
</PasswordButton>
|
||||
}
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(Input);
|
||||
9
src/elements/InputGroup.js
Normal file
9
src/elements/InputGroup.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const InputGroup = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
grid-column: 2;
|
||||
`;
|
||||
|
||||
export default InputGroup;
|
||||
9
src/elements/Label.js
Normal file
9
src/elements/Label.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Label = styled.label`
|
||||
color: #767676;
|
||||
${props => props.indented && `margin-right: 0.5rem;`}
|
||||
${props => props.middle && `margin: 0 0.5rem 0 1rem;`}
|
||||
`;
|
||||
|
||||
export default Label;
|
||||
60
src/elements/Link.js
Normal file
60
src/elements/Link.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const FilteredLink = ({ formLink, right, ...props }) => (
|
||||
<Link {...props}>{props.children}</Link>
|
||||
);
|
||||
|
||||
const StyledReactRouterLink = styled(FilteredLink)`
|
||||
display: inline-flex;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: none;
|
||||
cursor: pointer;
|
||||
color: #D91C5C;
|
||||
font-weight: 500;
|
||||
text-decoration: none;
|
||||
|
||||
& > span {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
&:focus > span {
|
||||
padding: 0.25rem;
|
||||
margin: -0.25rem;
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 0 0 0 0.125rem #D91C5C;
|
||||
}
|
||||
|
||||
&:hover > span {
|
||||
box-shadow: 0 0.125rem 0 #D91C5C;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:active > span {}
|
||||
|
||||
${props => props.formLink && `
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
`}
|
||||
|
||||
${props => props.right && `
|
||||
justify-self: end;
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledLink = props => (
|
||||
<StyledReactRouterLink {...props}>
|
||||
<span tabIndex="-1">
|
||||
{props.children}
|
||||
</span>
|
||||
</StyledReactRouterLink>
|
||||
);
|
||||
|
||||
export default StyledLink;
|
||||
35
src/elements/PasswordInput.js
Normal file
35
src/elements/PasswordInput.js
Normal file
@@ -0,0 +1,35 @@
|
||||
import React, { useState, useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import Input from '../elements/Input';
|
||||
|
||||
const PasswordInput = (props, ref) => {
|
||||
const inputRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}));
|
||||
|
||||
const [ showPassword, setShowPassword ] = useState(false);
|
||||
const toggleShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...props}
|
||||
ref={inputRef}
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={props.password}
|
||||
onChange={e => props.setPassword(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
props.setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
props.setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(PasswordInput);
|
||||
20
src/elements/Select.js
Normal file
20
src/elements/Select.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import styled from 'styled-components/macro';
|
||||
|
||||
const Select = styled.select`
|
||||
height: ${props => props.large
|
||||
? '3rem'
|
||||
: '2.25rem'
|
||||
};
|
||||
padding: 0 0.75rem;
|
||||
border: 1px solid #B6B6B6;
|
||||
border-radius: 0.125rem;
|
||||
background: #fff;
|
||||
color: inherit;
|
||||
&:focus {
|
||||
border-color: #565656;
|
||||
outline: none;
|
||||
box-shadow: 0 0.125rem 0.25rem rgba(0,0,0,0.12);
|
||||
}
|
||||
`;
|
||||
|
||||
export default Select;
|
||||
55
src/elements/TrashButton.js
Normal file
55
src/elements/TrashButton.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import React, { useRef, forwardRef, useImperativeHandle } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import { ReactComponent as TrashIcon } from '../images/TrashIcon.svg';
|
||||
|
||||
const StyledButton = styled.button`
|
||||
display: flex;
|
||||
background: none;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
margin-left: 0.5rem;
|
||||
fill: #949494;
|
||||
& > span {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 2.2rem;
|
||||
width: 1.9rem;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
border-radius: 0.25rem;
|
||||
outline: 0;
|
||||
}
|
||||
&:focus > span {
|
||||
box-shadow: inset 0 0 0 0.125rem #D91C5C;
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:hover > span {
|
||||
fill: #D91C5C;
|
||||
}
|
||||
&:active > span {
|
||||
}
|
||||
`;
|
||||
|
||||
const TrashButton = (props, ref) => {
|
||||
const buttonRef = useRef();
|
||||
useImperativeHandle(ref, () => ({
|
||||
focus: () => {
|
||||
buttonRef.current.focus();
|
||||
}
|
||||
}));
|
||||
return (
|
||||
<StyledButton
|
||||
type="button"
|
||||
{...props}
|
||||
ref={buttonRef}
|
||||
>
|
||||
<span tabIndex="-1">
|
||||
<TrashIcon />
|
||||
</span>
|
||||
</StyledButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default forwardRef(TrashButton);
|
||||
4
src/images/CheckGreen.svg
Normal file
4
src/images/CheckGreen.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle cx="12" cy="12" r="12" fill="#61C43E"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.0724 9.44766L10.4292 18.0909L5.57719 13.239L7.69851 11.1176L10.4292 13.8483L16.9511 7.32634L19.0724 9.44766Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
4
src/images/ErrorIcon.svg
Normal file
4
src/images/ErrorIcon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="27" height="24" viewBox="0 0 27 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11.8941 0.932038C12.6078 -0.310681 14.3922 -0.310679 15.1059 0.93204L26.7488 21.2039C27.4625 22.4466 26.5704 24 25.1429 24H1.85712C0.429636 24 -0.462537 22.4466 0.251203 21.2039L11.8941 0.932038Z" fill="#D91C5C"/>
|
||||
<path d="M15 13L14.5307 16H12.5L12 13V9H15V13ZM13.4774 17.2017C13.9225 17.2017 14.2736 17.3111 14.5307 17.5298C14.7878 17.7485 14.9164 18.0567 14.9164 18.4544C14.9164 18.8421 14.7878 19.1453 14.5307 19.364C14.2736 19.5827 13.9225 19.6921 13.4774 19.6921C13.0324 19.6921 12.6813 19.5827 12.4242 19.364C12.1769 19.1453 12.0533 18.8421 12.0533 18.4544C12.0533 18.0567 12.1769 17.7485 12.4242 17.5298C12.6813 17.3111 13.0324 17.2017 13.4774 17.2017Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 789 B |
5
src/images/HidePassword.svg
Normal file
5
src/images/HidePassword.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="27" height="25" viewBox="0 0 27 25" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 2C19.4027 2 24.7506 5.9502 26.6402 11.8302C26.6755 11.9403 26.6755 12.0597 26.6403 12.1698C24.7506 18.0498 19.4028 22 13.3334 22C7.26392 22 1.9161 18.0498 0.0264456 12.1698C-0.0088152 12.0597 -0.0088152 11.9403 0.0264456 11.8302C1.9161 5.9502 7.26387 2 13.3333 2ZM5.55546 12C5.55546 16.2887 9.04451 19.7778 13.3333 19.7778C17.6221 19.7778 21.1111 16.2887 21.1111 12C21.1111 7.71134 17.622 4.22229 13.3333 4.22229C9.04456 4.22229 5.55546 7.71134 5.55546 12Z"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7037 16.0741C15.9538 16.0741 17.7778 14.25 17.7778 12C17.7778 9.74993 15.9538 7.9259 13.7037 7.9259C11.4537 7.9259 9.62964 9.74993 9.62964 12C9.62964 14.25 11.4537 16.0741 13.7037 16.0741Z"/>
|
||||
<path d="M23.6274 0L25.7488 2.12132L3.12135 24.7487L1.00002 22.6274L23.6274 0Z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 936 B |
5
src/images/TrashIcon.svg
Normal file
5
src/images/TrashIcon.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="24" viewBox="0 0 20 24" fill="767676" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="2" width="20" height="3" rx="1"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3 6C2.44772 6 2 6.44772 2 7V23C2 23.5523 2.44772 24 3 24H17C17.5523 24 18 23.5523 18 23V7C18 6.44772 17.5523 6 17 6H3ZM7 9H5V21H7V9ZM9 9H11V21H9V9ZM15 9H13V21H15V9Z"/>
|
||||
<rect x="6" width="8" height="4" rx="1"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 408 B |
4
src/images/ViewPassword.svg
Normal file
4
src/images/ViewPassword.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="27" height="20" viewBox="0 0 27 20" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.3333 0C19.4027 0 24.7506 3.9502 26.6402 9.83018C26.6755 9.94034 26.6755 10.0597 26.6403 10.1698C24.7506 16.0498 19.4028 20 13.3334 20C7.26392 20 1.9161 16.0498 0.0264456 10.1698C-0.0088152 10.0597 -0.0088152 9.94034 0.0264456 9.83018C1.9161 3.9502 7.26387 0 13.3333 0ZM5.55546 10C5.55546 14.2887 9.04451 17.7778 13.3333 17.7778C17.6221 17.7778 21.1111 14.2887 21.1111 10C21.1111 5.71134 17.622 2.22229 13.3333 2.22229C9.04456 2.22229 5.55546 5.71134 5.55546 10Z" />
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M13.7037 14.0741C15.9538 14.0741 17.7778 12.25 17.7778 9.99998C17.7778 7.74993 15.9538 5.9259 13.7037 5.9259C11.4537 5.9259 9.62964 7.74993 9.62964 9.99998C9.62964 12.25 11.4537 14.0741 13.7037 14.0741Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 866 B |
@@ -48,7 +48,7 @@ code {
|
||||
}
|
||||
|
||||
::selection {
|
||||
background-color: #D91C5C;
|
||||
background-color: rgba(217, 28, 92, 0.75);
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import './index.css';
|
||||
import { NotificationProvider } from './contexts/NotificationContext';
|
||||
import App from './App';
|
||||
|
||||
ReactDOM.render(
|
||||
<App />,
|
||||
<NotificationProvider>
|
||||
<App />
|
||||
</NotificationProvider>,
|
||||
document.getElementById('root')
|
||||
);
|
||||
|
||||
217
src/pages/Login.js
Normal file
217
src/pages/Login.js
Normal file
@@ -0,0 +1,217 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import SetupTemplate from '../templates/SetupTemplate';
|
||||
import Form from '../elements/Form';
|
||||
import Button from '../elements/Button';
|
||||
import Input from '../elements/Input';
|
||||
import PasswordInput from '../elements/PasswordInput';
|
||||
import FormError from '../blocks/FormError';
|
||||
|
||||
const Login = props => {
|
||||
let history = useHistory();
|
||||
|
||||
// Refs
|
||||
const refUsername = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ username, setUsername ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidUsername, setInvalidUsername ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidUsername(false);
|
||||
setInvalidPassword(false);
|
||||
|
||||
if (!username && !password) {
|
||||
setErrorMessage('Username and password are required');
|
||||
setInvalidUsername(true);
|
||||
setInvalidPassword(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
if (!username) {
|
||||
setErrorMessage('Username is required');
|
||||
setInvalidUsername(true);
|
||||
refUsername.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
setErrorMessage('Password is required');
|
||||
setInvalidPassword(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
// Log in
|
||||
const response = await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/login',
|
||||
data: { username, password },
|
||||
});
|
||||
|
||||
// New account, password change required
|
||||
if (response.data.force_change) {
|
||||
// `user_sid` and `old_password` are needed for the new password form.
|
||||
// They're saved to sessionStorage so that the data does not persist.
|
||||
sessionStorage.setItem('user_sid', response.data.user_sid);
|
||||
sessionStorage.setItem('old_password', password);
|
||||
history.push('/create-password');
|
||||
return;
|
||||
}
|
||||
|
||||
// Save API key
|
||||
if (response.data.token) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Get account data
|
||||
//-----------------------------------------------------------------------------
|
||||
const serviceProvidersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/serviceProviders',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const accountsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const applicationsPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/applications',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const voipCarriersPromise = axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/voipCarriers',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const promiseAllValues = await Promise.all([
|
||||
serviceProvidersPromise,
|
||||
accountsPromise,
|
||||
applicationsPromise,
|
||||
voipCarriersPromise,
|
||||
]);
|
||||
|
||||
const serviceProviders = promiseAllValues[0].data;
|
||||
const accounts = promiseAllValues[1].data;
|
||||
const applications = promiseAllValues[2].data;
|
||||
const voipCarriers = promiseAllValues[3].data;
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Determine where to route user
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
(serviceProviders.length > 1) ||
|
||||
(accounts.length > 1) ||
|
||||
(applications.length > 1) ||
|
||||
(voipCarriers.length > 0)
|
||||
) {
|
||||
history.push('/internal/accounts');
|
||||
return;
|
||||
}
|
||||
|
||||
const { sip_realm, registration_hook } = accounts[0];
|
||||
|
||||
if (
|
||||
(!sip_realm || !registration_hook) &&
|
||||
!applications.length
|
||||
) {
|
||||
history.push('/configure-account');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!applications.length) {
|
||||
history.push('/create-application');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!voipCarriers.length) {
|
||||
history.push('/configure-sip-trunk');
|
||||
return;
|
||||
}
|
||||
|
||||
history.push('/internal/accounts');
|
||||
|
||||
} catch (err) {
|
||||
// 400 --> one or both fields are empty (prevented by above error checking)
|
||||
// 403 --> username or password are incorrect
|
||||
if (err.response.status > 399 && err.response.status < 500) {
|
||||
setErrorMessage('Login credentials are incorrect');
|
||||
} else {
|
||||
setErrorMessage('Something went wrong, please try again');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate title="Log In">
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
large
|
||||
fullWidth
|
||||
type="text"
|
||||
name="username"
|
||||
id="username"
|
||||
placeholder="Username"
|
||||
value={username}
|
||||
onChange={e => setUsername(e.target.value)}
|
||||
ref={refUsername}
|
||||
invalid={invalidUsername}
|
||||
autoFocus
|
||||
/>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="Password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
ref={refPassword}
|
||||
invalid={invalidPassword}
|
||||
/>
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
>
|
||||
Log In
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default Login;
|
||||
0
src/pages/internal/AccountsAddEdit.js
Normal file
0
src/pages/internal/AccountsAddEdit.js
Normal file
12
src/pages/internal/AccountsList.js
Normal file
12
src/pages/internal/AccountsList.js
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react';
|
||||
import InternalTemplate from '../../templates/InternalTemplate';
|
||||
|
||||
const AccountsList = () => (
|
||||
<InternalTemplate
|
||||
title="Accounts"
|
||||
>
|
||||
content
|
||||
</InternalTemplate>
|
||||
);
|
||||
|
||||
export default AccountsList;
|
||||
0
src/pages/internal/ApplicationsAddEdit.js
Normal file
0
src/pages/internal/ApplicationsAddEdit.js
Normal file
0
src/pages/internal/ApplicationsList.js
Normal file
0
src/pages/internal/ApplicationsList.js
Normal file
0
src/pages/internal/PhoneNumbersAddEdit.js
Normal file
0
src/pages/internal/PhoneNumbersAddEdit.js
Normal file
0
src/pages/internal/PhoneNumbersList.js
Normal file
0
src/pages/internal/PhoneNumbersList.js
Normal file
0
src/pages/internal/SipTrunksAddEdit.js
Normal file
0
src/pages/internal/SipTrunksAddEdit.js
Normal file
0
src/pages/internal/SipTrunksList.js
Normal file
0
src/pages/internal/SipTrunksList.js
Normal file
264
src/pages/setup/ConfigureAccount.js
Normal file
264
src/pages/setup/ConfigureAccount.js
Normal file
@@ -0,0 +1,264 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Form from '../../elements/Form';
|
||||
import Input from '../../elements/Input';
|
||||
import Label from '../../elements/Label';
|
||||
import Select from '../../elements/Select';
|
||||
import InputGroup from '../../elements/InputGroup';
|
||||
import PasswordInput from '../../elements/PasswordInput';
|
||||
import FormError from '../../blocks/FormError';
|
||||
import Button from '../../elements/Button';
|
||||
import Link from '../../elements/Link';
|
||||
|
||||
const ConfigureAccount = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refSipRealm = useRef(null);
|
||||
const refRegWebhook = useRef(null);
|
||||
const refUser = useRef(null);
|
||||
const refPassword = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ sipRealm, setSipRealm ] = useState('');
|
||||
const [ regWebhook, setRegWebhook ] = useState('');
|
||||
const [ method, setMethod ] = useState('POST');
|
||||
const [ user, setUser ] = useState('');
|
||||
const [ password, setPassword ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidSipRealm, setInvalidSipRealm ] = useState(false);
|
||||
const [ invalidRegWebhook, setInvalidRegWebhook ] = useState(false);
|
||||
const [ invalidUser, setInvalidUser ] = useState(false);
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showAuth, setShowAuth ] = useState(false);
|
||||
const toggleAuth = () => setShowAuth(!showAuth);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
const handleSumit = async (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidSipRealm(false);
|
||||
setInvalidRegWebhook(false);
|
||||
setInvalidUser(false);
|
||||
setInvalidPassword(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!sipRealm) {
|
||||
errorMessages.push('Please enter a SIP Realm or click the link below to skip this step.');
|
||||
setInvalidSipRealm(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refSipRealm.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!regWebhook) {
|
||||
errorMessages.push('Please enter a Registration Webhook or click the link below to skip this step.');
|
||||
setInvalidRegWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refRegWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((user && !password) || (!user && password)) {
|
||||
errorMessages.push('Username and password must be either both filled out or both empty.');
|
||||
setInvalidUser(true);
|
||||
setInvalidPassword(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!user) {
|
||||
refUser.current.focus();
|
||||
} else {
|
||||
refPassword.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Account SID in order to update it
|
||||
const account = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const { account_sid } = account.data[0];
|
||||
|
||||
await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/Accounts/${account_sid}`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
sip_realm: sipRealm,
|
||||
registration_hook: {
|
||||
url: regWebhook,
|
||||
method: method,
|
||||
username: user || null,
|
||||
password: password || null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
history.push('/create-application');
|
||||
|
||||
} catch (err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage('Something went wrong, please try again');
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Configure Account"
|
||||
progress={1}
|
||||
>
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSumit}
|
||||
>
|
||||
<Label htmlFor="sipRealm">SIP Realm</Label>
|
||||
<Input
|
||||
large
|
||||
name="sipRealm"
|
||||
id="sipRealm"
|
||||
value={sipRealm}
|
||||
onChange={e => setSipRealm(e.target.value)}
|
||||
placeholder="The domain name that SIP devices will register with"
|
||||
invalid={invalidSipRealm}
|
||||
autoFocus
|
||||
ref={refSipRealm}
|
||||
/>
|
||||
|
||||
<Label htmlFor="regWebhook">Registration Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large
|
||||
name="regWebhook"
|
||||
id="regWebhook"
|
||||
value={regWebhook}
|
||||
onChange={e => setRegWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that handles registrations"
|
||||
invalid={invalidRegWebhook}
|
||||
ref={refRegWebhook}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="method"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large
|
||||
name="method"
|
||||
id="method"
|
||||
value={method}
|
||||
onChange={e => setMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="user">User</Label>
|
||||
<Input
|
||||
large
|
||||
name="user"
|
||||
id="user"
|
||||
value={user}
|
||||
onChange={e => setUser(e.target.value)}
|
||||
invalid={invalidUser}
|
||||
ref={refUser}
|
||||
/>
|
||||
<Label htmlFor="password" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="password"
|
||||
id="password"
|
||||
password={password}
|
||||
setPassword={setPassword}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidPassword}
|
||||
ref={refPassword}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
<Button large grid fullWidth>
|
||||
Save and Continue
|
||||
</Button>
|
||||
|
||||
<Link
|
||||
formLink
|
||||
right
|
||||
to="/create-application"
|
||||
>
|
||||
Skip for now — I'l complete later
|
||||
</Link>
|
||||
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureAccount;
|
||||
435
src/pages/setup/ConfigureSipTrunk.js
Normal file
435
src/pages/setup/ConfigureSipTrunk.js
Normal file
@@ -0,0 +1,435 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Form from '../../elements/Form';
|
||||
import Input from '../../elements/Input';
|
||||
import Label from '../../elements/Label';
|
||||
import Checkbox from '../../elements/Checkbox';
|
||||
import InputGroup from '../../elements/InputGroup';
|
||||
import FormError from '../../blocks/FormError';
|
||||
import Button from '../../elements/Button';
|
||||
import TrashButton from '../../elements/TrashButton';
|
||||
|
||||
const ConfigureSipTrunk = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refName = useRef(null);
|
||||
const refIp = useRef([]);
|
||||
const refPort = useRef([]);
|
||||
const refInbound = useRef([]);
|
||||
const refOutbound = useRef([]);
|
||||
const refTrash = useRef([]);
|
||||
const refAdd = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ name, setName ] = useState('');
|
||||
const [ nameInvalid, setNameInvalid ] = useState(false);
|
||||
const [ description, setDescription ] = useState('');
|
||||
const [ sipGateways, setSipGateways ] = useState([
|
||||
{
|
||||
ip: '',
|
||||
port: '',
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
]);
|
||||
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
const addSipGateway = () => {
|
||||
const newSipGateways = [
|
||||
...sipGateways,
|
||||
{
|
||||
ip: '',
|
||||
port: '',
|
||||
inbound: true,
|
||||
outbound: true,
|
||||
invalidIp: false,
|
||||
invalidPort: false,
|
||||
invalidInbound: false,
|
||||
invalidOutbound: false,
|
||||
}
|
||||
];
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const removeSipGateway = index => {
|
||||
const newSipGateways = sipGateways.filter((s,i) => i !== index);
|
||||
setSipGateways(newSipGateways);
|
||||
setErrorMessage('');
|
||||
};
|
||||
|
||||
const updateSipGateways = (e, i, key) => {
|
||||
const newSipGateways = [...sipGateways];
|
||||
const newValue =
|
||||
key === 'invalidIp' ||
|
||||
key === 'invalidPort' ||
|
||||
key === 'invalidInbound' ||
|
||||
key === 'invalidOutbound'
|
||||
? true
|
||||
: (key === 'inbound') || (key === 'outbound')
|
||||
? e.target.checked
|
||||
: e.target.value;
|
||||
newSipGateways[i][key] = newValue;
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const resetInvalidFields = () => {
|
||||
setNameInvalid(false);
|
||||
const newSipGateways = [...sipGateways];
|
||||
newSipGateways.forEach((s, i) => {
|
||||
newSipGateways[i].invalidIp = false;
|
||||
newSipGateways[i].invalidPort = false;
|
||||
newSipGateways[i].invalidInbound = false;
|
||||
newSipGateways[i].invalidOutbound = false;
|
||||
});
|
||||
setSipGateways(newSipGateways);
|
||||
};
|
||||
|
||||
const handleSumit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
resetInvalidFields();
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!name) {
|
||||
errorMessages.push('Please enter a name for this SIP Trunk.');
|
||||
setNameInvalid(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refName.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!sipGateways.length) {
|
||||
errorMessages.push('You must provide at least one SIP Gateway.');
|
||||
if (!focusHasBeenSet) {
|
||||
refAdd.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
const regIp = /^((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]?[0-9])$/;
|
||||
const regFqdn = /^([a-zA-Z][^.]*)(\.[^.]+){2,}$/;
|
||||
const regFqdnTopLevel = /^([a-zA-Z][^.]*)(\.[^.]+)$/;
|
||||
const regPort = /^[0-9]+$/;
|
||||
|
||||
sipGateways.forEach(async (gateway, i) => {
|
||||
//-----------------------------------------------------------------------------
|
||||
// IP validation
|
||||
//-----------------------------------------------------------------------------
|
||||
const type = regIp.test(gateway.ip)
|
||||
? 'ip'
|
||||
: regFqdn.test(gateway.ip)
|
||||
? 'fqdn'
|
||||
: regFqdnTopLevel.test(gateway.ip)
|
||||
? 'fqdn-top-level'
|
||||
: 'invalid';
|
||||
|
||||
if (!gateway.ip) {
|
||||
errorMessages.push('The IP Address cannot be blank. Please provide an IP address or delete the row.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'fqdn-top-level') {
|
||||
errorMessages.push('When using an FQDN, you must use a subdomain (e.g. sip.example.com).');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (type === 'invalid') {
|
||||
errorMessages.push('Please provide a valid IP address or fully qualified domain name.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (!focusHasBeenSet) {
|
||||
refIp.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// Port validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (
|
||||
gateway.port && (
|
||||
!(regPort.test(gateway.port))
|
||||
|| (parseInt(gateway.port) < 0)
|
||||
|| (parseInt(gateway.port) > 65535)
|
||||
)
|
||||
) {
|
||||
errorMessages.push('Please provide a valid port number between 0 and 65535');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refPort.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// inbound/outbound validation
|
||||
//-----------------------------------------------------------------------------
|
||||
if (type === 'fqdn' && (!gateway.outbound || gateway.inbound)) {
|
||||
errorMessages.push('A fully qualified domain name may only be used for outbound calls.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
if (gateway.inbound) updateSipGateways(null, i, 'invalidInbound');
|
||||
if (!gateway.outbound) updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
if (gateway.inbound) {
|
||||
refInbound.current[i].focus();
|
||||
} else {
|
||||
refOutbound.current[i].focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
else if (!gateway.inbound && !gateway.outbound) {
|
||||
errorMessages.push('Each SIP Gateway must accept inbound calls, outbound calls, or both.');
|
||||
updateSipGateways(null, i, 'invalidInbound');
|
||||
updateSipGateways(null, i, 'invalidOutbound');
|
||||
if (!focusHasBeenSet) {
|
||||
refInbound.current[i].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
//-----------------------------------------------------------------------------
|
||||
// duplicates validation
|
||||
//-----------------------------------------------------------------------------
|
||||
sipGateways.forEach((otherGateway, j) => {
|
||||
if (i >= j) return;
|
||||
if (!gateway.ip) return;
|
||||
if (type === 'invalid') return;
|
||||
if (gateway.ip === otherGateway.ip && gateway.port === otherGateway.port) {
|
||||
errorMessages.push('Each row must have a unique IP/Port combination. Please delete the duplicate row.');
|
||||
updateSipGateways(null, i, 'invalidIp');
|
||||
updateSipGateways(null, i, 'invalidPort');
|
||||
updateSipGateways(null, j, 'invalidIp');
|
||||
updateSipGateways(null, j, 'invalidPort');
|
||||
if (!focusHasBeenSet) {
|
||||
refTrash.current[j].focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// remove duplicate error messages
|
||||
for (let i = 0; i < errorMessages.length; i++) {
|
||||
for (let j = 0; j < errorMessages.length; j++) {
|
||||
if (i >= j) continue;
|
||||
if (errorMessages[i] === errorMessages[j]) {
|
||||
errorMessages.splice(j, 1);
|
||||
j = j - 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create SIP Trunk / VoIP Carrier
|
||||
const voipCarrier = await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/VoipCarriers`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
name,
|
||||
description,
|
||||
},
|
||||
});
|
||||
|
||||
// Create SIP Gateways
|
||||
sipGateways.forEach(async s => {
|
||||
if (!s.ip) return;
|
||||
try {
|
||||
await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/SipGateways`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
voip_carrier_sid: voipCarrier.data.sid,
|
||||
ipv4: s.ip,
|
||||
port: s.port,
|
||||
inbound: s.inbound,
|
||||
outbound: s.outbound,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
setErrorMessage(err.response.data.msg || 'Something went wrong, please try again');
|
||||
console.log(err.response);
|
||||
}
|
||||
});
|
||||
|
||||
history.push('/setup-complete');
|
||||
|
||||
} catch(err) {
|
||||
if (err.response && err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage((err.response && err.response.data.msg) || 'Something went wrong, please try again');
|
||||
console.log(err.response);
|
||||
console.log(err);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Configure SIP Trunk"
|
||||
progress={3}
|
||||
>
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSumit}
|
||||
>
|
||||
<Label htmlFor="name">Name</Label>
|
||||
<Input
|
||||
large
|
||||
name="name"
|
||||
id="name"
|
||||
value={name}
|
||||
onChange={e => setName(e.target.value)}
|
||||
placeholder="SIP trunk provider name"
|
||||
invalid={nameInvalid}
|
||||
autoFocus
|
||||
ref={refName}
|
||||
/>
|
||||
<Label htmlFor="description">Description</Label>
|
||||
<Input
|
||||
large
|
||||
name="description"
|
||||
id="description"
|
||||
value={description}
|
||||
onChange={e => setDescription(e.target.value)}
|
||||
placeholder="Optional"
|
||||
/>
|
||||
<hr style={{ margin: '0.5rem -2rem' }} />
|
||||
<div
|
||||
style={{ whiteSpace: 'nowrap' }}
|
||||
>SIP Gateways</div>
|
||||
{
|
||||
sipGateways.length
|
||||
? <div>{/* for CSS grid layout */}</div>
|
||||
: null
|
||||
}
|
||||
{sipGateways.map((g, i) => (
|
||||
<React.Fragment key={i}>
|
||||
<Label htmlFor={`sipGatewaysIp[${i}]`}>IP Address</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large
|
||||
name={`sipGatewaysIp[${i}]`}
|
||||
id={`sipGatewaysIp[${i}]`}
|
||||
value={sipGateways[i].ip}
|
||||
onChange={e => updateSipGateways(e, i, 'ip')}
|
||||
placeholder={i === 0 ? "1.2.3.4" : 'Optional'}
|
||||
invalid={sipGateways[i].invalidIp}
|
||||
ref={ref => refIp.current[i] = ref}
|
||||
/>
|
||||
<Label
|
||||
middle
|
||||
htmlFor={`sipGatewaysPort[${i}]`}
|
||||
>
|
||||
Port
|
||||
</Label>
|
||||
<Input
|
||||
large
|
||||
width="5rem"
|
||||
name={`sipGatewaysPort[${i}]`}
|
||||
id={`sipGatewaysPort[${i}]`}
|
||||
value={sipGateways[i].port}
|
||||
onChange={e => updateSipGateways(e, i, 'port')}
|
||||
placeholder="5060"
|
||||
invalid={sipGateways[i].invalidPort}
|
||||
ref={ref => refPort.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
id={`inbound[${i}]`}
|
||||
label="Inbound"
|
||||
tooltip="Sends us calls"
|
||||
checked={sipGateways[i].inbound}
|
||||
onChange={e => updateSipGateways(e, i, 'inbound')}
|
||||
invalid={sipGateways[i].invalidInbound}
|
||||
ref={ref => refInbound.current[i] = ref}
|
||||
/>
|
||||
<Checkbox
|
||||
id={`outbound[${i}]`}
|
||||
label="Outbound"
|
||||
tooltip="Accepts calls from us"
|
||||
checked={sipGateways[i].outbound}
|
||||
onChange={e => updateSipGateways(e, i, 'outbound')}
|
||||
invalid={sipGateways[i].invalidOutbound}
|
||||
ref={ref => refOutbound.current[i] = ref}
|
||||
/>
|
||||
<TrashButton
|
||||
onClick={() => removeSipGateway(i)}
|
||||
ref={ref => refTrash.current[i] = ref}
|
||||
/>
|
||||
</InputGroup>
|
||||
</React.Fragment>
|
||||
))}
|
||||
<Button
|
||||
square
|
||||
type="button"
|
||||
onClick={addSipGateway}
|
||||
ref={refAdd}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
<Button large grid fullWidth>
|
||||
Save and Continue
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConfigureSipTrunk;
|
||||
494
src/pages/setup/CreateApplication.js
Normal file
494
src/pages/setup/CreateApplication.js
Normal file
@@ -0,0 +1,494 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Form from '../../elements/Form';
|
||||
import Input from '../../elements/Input';
|
||||
import Label from '../../elements/Label';
|
||||
import Select from '../../elements/Select';
|
||||
import InputGroup from '../../elements/InputGroup';
|
||||
import PasswordInput from '../../elements/PasswordInput';
|
||||
import FormError from '../../blocks/FormError';
|
||||
import Button from '../../elements/Button';
|
||||
import SpeechSynthesisLanguageGoogle from '../../data/SpeechSynthesisLanguageGoogle';
|
||||
import SpeechSynthesisLanguageAws from '../../data/SpeechSynthesisLanguageAws';
|
||||
import SpeechRecognizerLanguageGoogle from '../../data/SpeechRecognizerLanguageGoogle';
|
||||
|
||||
const CreateApplication = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refCallWebhook = useRef(null);
|
||||
const refCallWebhookUser = useRef(null);
|
||||
const refCallWebhookPass = useRef(null);
|
||||
const refStatusWebhook = useRef(null);
|
||||
const refStatusWebhookUser = useRef(null);
|
||||
const refStatusWebhookPass = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ callWebhook, setCallWebhook ] = useState('');
|
||||
const [ callWebhookMethod, setCallWebhookMethod ] = useState('POST');
|
||||
const [ callWebhookUser, setCallWebhookUser ] = useState('');
|
||||
const [ callWebhookPass, setCallWebhookPass ] = useState('');
|
||||
const [ statusWebhook, setStatusWebhook ] = useState('');
|
||||
const [ statusWebhookMethod, setStatusWebhookMethod ] = useState('POST');
|
||||
const [ statusWebhookUser, setStatusWebhookUser ] = useState('');
|
||||
const [ statusWebhookPass, setStatusWebhookPass ] = useState('');
|
||||
const [ speechSynthesisVendor, setSpeechSynthesisVendor ] = useState('google');
|
||||
const [ speechSynthesisLanguage, setSpeechSynthesisLanguage ] = useState('en-US');
|
||||
const [ speechSynthesisVoice, setSpeechSynthesisVoice ] = useState('en-US-Standard-C');
|
||||
const [ speechRecognizerVendor, setSpeechRecognizerVendor ] = useState('google');
|
||||
const [ speechRecognizerLanguage, setSpeechRecognizerLanguage ] = useState('en-US');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidCallWebhook, setInvalidCallWebhook ] = useState(false);
|
||||
const [ invalidCallWebhookUser, setInvalidCallWebhookUser ] = useState(false);
|
||||
const [ invalidCallWebhookPass, setInvalidCallWebhookPass ] = useState(false);
|
||||
const [ invalidStatusWebhook, setInvalidStatusWebhook ] = useState(false);
|
||||
const [ invalidStatusWebhookUser, setInvalidStatusWebhookUser ] = useState(false);
|
||||
const [ invalidStatusWebhookPass, setInvalidStatusWebhookPass ] = useState(false);
|
||||
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
const [ showCallAuth, setShowCallAuth ] = useState(false);
|
||||
const toggleCallAuth = () => setShowCallAuth(!showCallAuth);
|
||||
|
||||
const [ showStatusAuth, setShowStatusAuth ] = useState(false);
|
||||
const toggleStatusAuth = () => setShowStatusAuth(!showStatusAuth);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
const handleSumit = async (e) => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidCallWebhook(false);
|
||||
setInvalidCallWebhookUser(false);
|
||||
setInvalidCallWebhookPass(false);
|
||||
setInvalidStatusWebhook(false);
|
||||
setInvalidStatusWebhookUser(false);
|
||||
setInvalidStatusWebhookPass(false);
|
||||
let errorMessages = [];
|
||||
let focusHasBeenSet = false;
|
||||
|
||||
if (!callWebhook) {
|
||||
errorMessages.push('Please enter a Calling Webhook.');
|
||||
setInvalidCallWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refCallWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!statusWebhook) {
|
||||
errorMessages.push('Please enter a Call Status Webhook.');
|
||||
setInvalidStatusWebhook(true);
|
||||
if (!focusHasBeenSet) {
|
||||
refStatusWebhook.current.focus();
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((callWebhookUser && !callWebhookPass) || (!callWebhookUser && callWebhookPass)) {
|
||||
errorMessages.push('Calling Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidCallWebhookUser(true);
|
||||
setInvalidCallWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!callWebhookUser) {
|
||||
refCallWebhookUser.current.focus();
|
||||
} else {
|
||||
refCallWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((statusWebhookUser && !statusWebhookPass) || (!statusWebhookUser && statusWebhookPass)) {
|
||||
errorMessages.push('Call Status Webhook username and password must be either both filled out or both empty.');
|
||||
setInvalidStatusWebhookUser(true);
|
||||
setInvalidStatusWebhookPass(true);
|
||||
if (!focusHasBeenSet) {
|
||||
if (!statusWebhookUser) {
|
||||
refStatusWebhookUser.current.focus();
|
||||
} else {
|
||||
refStatusWebhookPass.current.focus();
|
||||
}
|
||||
focusHasBeenSet = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (errorMessages.length > 1) {
|
||||
setErrorMessage(errorMessages);
|
||||
return;
|
||||
} else if (errorMessages.length === 1) {
|
||||
setErrorMessage(errorMessages[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Get Account SID in order to assign new application to it
|
||||
const account = await axios({
|
||||
method: 'get',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: '/Accounts',
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
});
|
||||
|
||||
const { account_sid } = account.data[0];
|
||||
|
||||
await axios({
|
||||
method: 'post',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/Applications`,
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem('token')}`,
|
||||
},
|
||||
data: {
|
||||
account_sid,
|
||||
name: "default application",
|
||||
call_hook: {
|
||||
url: callWebhook,
|
||||
method: callWebhookMethod,
|
||||
username: callWebhookUser || null,
|
||||
password: callWebhookPass || null,
|
||||
},
|
||||
call_status_hook: {
|
||||
url: statusWebhook,
|
||||
method: statusWebhookMethod,
|
||||
username: statusWebhookUser || null,
|
||||
password: statusWebhookPass || null,
|
||||
},
|
||||
speech_synthesis_vendor: speechSynthesisVendor,
|
||||
speech_synthesis_language: speechSynthesisLanguage,
|
||||
speech_synthesis_voice: speechSynthesisVoice,
|
||||
speech_recognizer_vendor: speechRecognizerVendor,
|
||||
speech_recognizer_language: speechRecognizerLanguage,
|
||||
},
|
||||
});
|
||||
|
||||
history.push('/configure-sip-trunk');
|
||||
|
||||
} catch (err) {
|
||||
if (err.response.status === 401) {
|
||||
localStorage.removeItem('token');
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in and try again',
|
||||
});
|
||||
} else {
|
||||
setErrorMessage('Something went wrong, please try again');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Create Application"
|
||||
progress={2}
|
||||
>
|
||||
<Form
|
||||
large
|
||||
onSubmit={handleSumit}
|
||||
>
|
||||
<Label htmlFor="callWebhook">Calling Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large
|
||||
name="callWebhook"
|
||||
id="callWebhook"
|
||||
value={callWebhook}
|
||||
onChange={e => setCallWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will handle calls"
|
||||
invalid={invalidCallWebhook}
|
||||
ref={refCallWebhook}
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="callWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large
|
||||
name="callWebhookMethod"
|
||||
id="callWebhookMethod"
|
||||
value={callWebhookMethod}
|
||||
onChange={e => setCallWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showCallAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="callWebhookUser">User</Label>
|
||||
<Input
|
||||
large
|
||||
name="callWebhookUser"
|
||||
id="callWebhookUser"
|
||||
value={callWebhookUser}
|
||||
onChange={e => setCallWebhookUser(e.target.value)}
|
||||
invalid={invalidCallWebhookUser}
|
||||
ref={refCallWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="callWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="callWebhookPass"
|
||||
id="callWebhookPass"
|
||||
password={callWebhookPass}
|
||||
setPassword={setCallWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidCallWebhookPass}
|
||||
ref={refCallWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleCallAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="statusWebhook">Call Status Webhook</Label>
|
||||
<InputGroup>
|
||||
<Input
|
||||
large
|
||||
name="statusWebhook"
|
||||
id="statusWebhook"
|
||||
value={statusWebhook}
|
||||
onChange={e => setStatusWebhook(e.target.value)}
|
||||
placeholder="URL for your web application that will receive call status"
|
||||
invalid={invalidStatusWebhook}
|
||||
ref={refStatusWebhook}
|
||||
/>
|
||||
|
||||
<Label
|
||||
middle
|
||||
htmlFor="statusWebhookMethod"
|
||||
>
|
||||
Method
|
||||
</Label>
|
||||
<Select
|
||||
large
|
||||
name="statusWebhookMethod"
|
||||
id="statusWebhookMethod"
|
||||
value={statusWebhookMethod}
|
||||
onChange={e => setStatusWebhookMethod(e.target.value)}
|
||||
>
|
||||
<option value="POST">POST</option>
|
||||
<option value="GET">GET</option>
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{showStatusAuth ? (
|
||||
<InputGroup>
|
||||
<Label indented htmlFor="statusWebhookUser">User</Label>
|
||||
<Input
|
||||
large
|
||||
name="statusWebhookUser"
|
||||
id="statusWebhookUser"
|
||||
value={statusWebhookUser}
|
||||
onChange={e => setStatusWebhookUser(e.target.value)}
|
||||
invalid={invalidStatusWebhookUser}
|
||||
ref={refStatusWebhookUser}
|
||||
/>
|
||||
<Label htmlFor="statusWebhookPass" middle>Password</Label>
|
||||
<PasswordInput
|
||||
large
|
||||
allowShowPassword
|
||||
name="statusWebhookPass"
|
||||
id="statusWebhookPass"
|
||||
password={statusWebhookPass}
|
||||
setPassword={setStatusWebhookPass}
|
||||
setErrorMessage={setErrorMessage}
|
||||
invalid={invalidStatusWebhookPass}
|
||||
ref={refStatusWebhookPass}
|
||||
/>
|
||||
</InputGroup>
|
||||
) : (
|
||||
<Button
|
||||
text
|
||||
formLink
|
||||
type="button"
|
||||
onClick={toggleStatusAuth}
|
||||
>
|
||||
Use HTTP Basic Authentication
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechSynthesisVendor">Speech Synthesis Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
name="speechSynthesisVendor"
|
||||
id="speechSynthesisVendor"
|
||||
value={speechSynthesisVendor}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisVendor(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
e.target.value === 'google' &&
|
||||
speechSynthesisLanguage === 'en-US'
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
// Google and AWS have different voice lists. See if the newly
|
||||
// chosen vendor has the same language as what was already in use.
|
||||
let newLang = e.target.value === 'google'
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === speechSynthesisLanguage
|
||||
));
|
||||
|
||||
// if not, use en-US as fallback.
|
||||
if (!newLang) {
|
||||
setSpeechSynthesisLanguage('en-US');
|
||||
|
||||
if (e.target.value === 'google') {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
newLang = SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === 'en-US'
|
||||
));
|
||||
}
|
||||
|
||||
// Update state to reflect first voice option for language
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
}}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
<option value="aws">AWS</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisLanguage">Language</Label>
|
||||
<Select
|
||||
name="speechSynthesisLanguage"
|
||||
id="speechSynthesisLanguage"
|
||||
value={speechSynthesisLanguage}
|
||||
onChange={e => {
|
||||
setSpeechSynthesisLanguage(e.target.value);
|
||||
|
||||
// When using Google and en-US, ensure "Standard-C" is used as default
|
||||
if (
|
||||
(speechSynthesisVendor === 'google')
|
||||
&& (e.target.value === 'en-US')
|
||||
) {
|
||||
setSpeechSynthesisVoice('en-US-Standard-C');
|
||||
return;
|
||||
}
|
||||
|
||||
const newLang = speechSynthesisVendor === 'google'
|
||||
? SpeechSynthesisLanguageGoogle.find(l => (
|
||||
l.code === e.target.value
|
||||
))
|
||||
: SpeechSynthesisLanguageAws.find(l => (
|
||||
l.code === e.target.value
|
||||
));
|
||||
|
||||
setSpeechSynthesisVoice(newLang.voices[0].value);
|
||||
|
||||
}}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
SpeechSynthesisLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))
|
||||
)}
|
||||
</Select>
|
||||
<Label middle htmlFor="speechSynthesisVoice">Voice</Label>
|
||||
<Select
|
||||
name="speechSynthesisVoice"
|
||||
id="speechSynthesisVoice"
|
||||
value={speechSynthesisVoice}
|
||||
onChange={e => setSpeechSynthesisVoice(e.target.value)}
|
||||
>
|
||||
{speechSynthesisVendor === 'google' ? (
|
||||
SpeechSynthesisLanguageGoogle
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
) : (
|
||||
SpeechSynthesisLanguageAws
|
||||
.filter(l => l.code === speechSynthesisLanguage)
|
||||
.map(m => m.voices.map(v => (
|
||||
<option key={v.value} value={v.value}>{v.name}</option>
|
||||
)))
|
||||
)}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
<hr />
|
||||
|
||||
<Label htmlFor="speechRecognizerVendor">Speech Recognizer Vendor</Label>
|
||||
<InputGroup>
|
||||
<Select
|
||||
name="speechRecognizerVendor"
|
||||
id="speechRecognizerVendor"
|
||||
value={speechRecognizerVendor}
|
||||
onChange={e => setSpeechRecognizerVendor(e.target.value)}
|
||||
>
|
||||
<option value="google">Google</option>
|
||||
</Select>
|
||||
<Label middle htmlFor="speechRecognizerLanguage">Language</Label>
|
||||
<Select
|
||||
name="speechRecognizerLanguage"
|
||||
id="speechRecognizerLanguage"
|
||||
value={speechRecognizerLanguage}
|
||||
onChange={e => setSpeechRecognizerLanguage(e.target.value)}
|
||||
>
|
||||
{SpeechRecognizerLanguageGoogle.map(l => (
|
||||
<option key={l.code} value={l.code}>{l.name}</option>
|
||||
))}
|
||||
</Select>
|
||||
</InputGroup>
|
||||
|
||||
{errorMessage && (
|
||||
<FormError grid message={errorMessage} />
|
||||
)}
|
||||
<Button large grid fullWidth>
|
||||
Save and Continue
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreateApplication;
|
||||
216
src/pages/setup/CreatePassword.js
Normal file
216
src/pages/setup/CreatePassword.js
Normal file
@@ -0,0 +1,216 @@
|
||||
import React, { useState, useEffect, useContext, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Form from '../../elements/Form';
|
||||
import Button from '../../elements/Button';
|
||||
import Input from '../../elements/Input';
|
||||
import FormError from '../../blocks/FormError';
|
||||
|
||||
const CreatePassword = () => {
|
||||
let history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
// Refs
|
||||
const refPassword = useRef(null);
|
||||
const refPasswordConfirm = useRef(null);
|
||||
|
||||
// Form inputs
|
||||
const [ password, setPassword ] = useState('');
|
||||
const [ passwordConfirm, setPasswordConfirm ] = useState('');
|
||||
|
||||
// Invalid form inputs
|
||||
const [ invalidPassword, setInvalidPassword ] = useState(false);
|
||||
const [ invalidPasswordConfirm, setInvalidPasswordConfirm ] = useState(false);
|
||||
|
||||
const [ errorMessage, setErrorMessage ] = useState('');
|
||||
|
||||
// Handle Password visibility
|
||||
// (not using PasswordInput because need to manage if password is visible here
|
||||
// because showing password hides passwordConfirm input)
|
||||
const [ showPassword, setShowPassword ] = useState(false);
|
||||
const toggleShowPassword = () => setShowPassword(!showPassword);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sessionStorage.getItem('user_sid')) {
|
||||
|
||||
if (localStorage.getItem('token')) {
|
||||
history.push('/internal/accounts');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'info',
|
||||
message: 'That page is only valid when first creating an account.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
const handleSubmit = async e => {
|
||||
try {
|
||||
e.preventDefault();
|
||||
setErrorMessage('');
|
||||
setInvalidPassword(false);
|
||||
setInvalidPasswordConfirm(false);
|
||||
|
||||
if (!password) {
|
||||
setErrorMessage('Please provide a password');
|
||||
setInvalidPassword(true);
|
||||
if (!passwordConfirm) {
|
||||
setInvalidPasswordConfirm(true);
|
||||
}
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showPassword && !passwordConfirm) {
|
||||
setErrorMessage('Both fields are required');
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPasswordConfirm.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!showPassword && password !== passwordConfirm) {
|
||||
setErrorMessage('Passwords do not match');
|
||||
setInvalidPassword(true);
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
(password.length < 6) ||
|
||||
(!/\d/.test(password)) ||
|
||||
(!/[a-zA-Z]/.test(password))
|
||||
) {
|
||||
setErrorMessage(
|
||||
<div>
|
||||
Password must:
|
||||
<ul>
|
||||
<li>Be at least 6 characters</li>
|
||||
<li>Contain at least one letter</li>
|
||||
<li>Contain at least one number</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
setInvalidPassword(true);
|
||||
setInvalidPasswordConfirm(true);
|
||||
refPassword.current.focus();
|
||||
return;
|
||||
}
|
||||
|
||||
const user_sid = sessionStorage.getItem('user_sid');
|
||||
const old_password = sessionStorage.getItem('old_password');
|
||||
|
||||
if (!old_password) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'Your session has expired. Please log in again',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await axios({
|
||||
method: 'put',
|
||||
baseURL: process.env.REACT_APP_API_BASE_URL,
|
||||
url: `/Users/${user_sid}`,
|
||||
data: {
|
||||
old_password,
|
||||
new_password: password,
|
||||
},
|
||||
});
|
||||
|
||||
sessionStorage.removeItem('user_sid');
|
||||
sessionStorage.removeItem('old_password');
|
||||
|
||||
if (response.data.user_sid) {
|
||||
localStorage.setItem('token', response.data.token);
|
||||
}
|
||||
|
||||
history.push('/configure-account');
|
||||
|
||||
} catch(err) {
|
||||
console.log(err);
|
||||
localStorage.clear();
|
||||
sessionStorage.clear();
|
||||
history.push('/');
|
||||
setErrorMessage('something went wrong, please log in and try again');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Create Password"
|
||||
subtitle="You must create a new password"
|
||||
>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<Input
|
||||
large
|
||||
allowShowPassword={showPassword}
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type={showPassword ? "text" : "password"}
|
||||
name="password"
|
||||
id="password"
|
||||
placeholder="New Password"
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
ref={refPassword}
|
||||
invalid={invalidPassword}
|
||||
autoFocus
|
||||
/>
|
||||
{!showPassword && (
|
||||
<Input
|
||||
large
|
||||
allowShowPassword
|
||||
showPassword={showPassword}
|
||||
toggleShowPassword={toggleShowPassword}
|
||||
type="password"
|
||||
name="confirmPassword"
|
||||
id="confirmPassword"
|
||||
placeholder="Confirm New Password"
|
||||
value={passwordConfirm}
|
||||
onChange={e => setPasswordConfirm(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
if (!showPassword && e.getModifierState('CapsLock')) {
|
||||
setErrorMessage('CAPSLOCK is enabled!');
|
||||
} else {
|
||||
setErrorMessage('');
|
||||
}
|
||||
}}
|
||||
ref={refPasswordConfirm}
|
||||
invalid={invalidPasswordConfirm}
|
||||
/>
|
||||
)}
|
||||
{errorMessage && (
|
||||
<FormError message={errorMessage} />
|
||||
)}
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
>
|
||||
Create Password
|
||||
</Button>
|
||||
</Form>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default CreatePassword;
|
||||
40
src/pages/setup/SetupComplete.js
Normal file
40
src/pages/setup/SetupComplete.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../../contexts/NotificationContext';
|
||||
import SetupTemplate from '../../templates/SetupTemplate';
|
||||
import Button from '../../elements/Button';
|
||||
|
||||
const SetupComplete = () => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
return (
|
||||
<SetupTemplate
|
||||
title="Setup Complete!"
|
||||
progress={4}
|
||||
>
|
||||
<Button
|
||||
large
|
||||
fullWidth
|
||||
onClick={() => {
|
||||
history.push('/internal/accounts');
|
||||
}}
|
||||
>
|
||||
Continue to account
|
||||
</Button>
|
||||
</SetupTemplate>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupComplete;
|
||||
20
src/reducers/NotificationReducer.js
Normal file
20
src/reducers/NotificationReducer.js
Normal file
@@ -0,0 +1,20 @@
|
||||
|
||||
const NotificationReducer = (state, action) => {
|
||||
switch (action.type) {
|
||||
case 'ADD':
|
||||
return [
|
||||
{
|
||||
id: Date.now(),
|
||||
level: action.level,
|
||||
message: action.message,
|
||||
},
|
||||
...state,
|
||||
];
|
||||
case 'REMOVE':
|
||||
return state.filter(s => s.id !== action.id);
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export default NotificationReducer;
|
||||
67
src/templates/InternalTemplate.js
Normal file
67
src/templates/InternalTemplate.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import React, { useEffect, useContext } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { NotificationDispatchContext } from '../contexts/NotificationContext';
|
||||
import styled from 'styled-components/macro';
|
||||
import H1 from '../elements/H1';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
display: flex;
|
||||
`;
|
||||
|
||||
const SideMenu = styled.div`
|
||||
width: 15rem;
|
||||
min-height: calc(100vh - 4rem);
|
||||
background: #FFF;
|
||||
z-index: -1;
|
||||
`;
|
||||
|
||||
const PageMain = styled.main`
|
||||
padding: 2.5rem 3rem;
|
||||
`;
|
||||
|
||||
const P = styled.p`
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
background: #FFF;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.1),
|
||||
0px 0px 0.25rem rgba(0, 0, 0, 0.1);
|
||||
@media (max-width: 34rem) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const SetupTemplate = props => {
|
||||
const history = useHistory();
|
||||
const dispatch = useContext(NotificationDispatchContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!localStorage.getItem('token')) {
|
||||
history.push('/');
|
||||
dispatch({
|
||||
type: 'ADD',
|
||||
level: 'error',
|
||||
message: 'You must log in to view that page.',
|
||||
});
|
||||
}
|
||||
}, [history, dispatch]);
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<SideMenu />
|
||||
<PageMain>
|
||||
<H1>{props.title}</H1>
|
||||
{
|
||||
typeof props.subtitle === 'object'
|
||||
? props.subtitle
|
||||
: <P>{props.subtitle}</P>
|
||||
}
|
||||
<ContentContainer>{props.children}</ContentContainer>
|
||||
</PageMain>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupTemplate;
|
||||
45
src/templates/SetupTemplate.js
Normal file
45
src/templates/SetupTemplate.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import ProgressVisualization from '../blocks/ProgressVisualization';
|
||||
import H1 from '../elements/H1';
|
||||
|
||||
const PageContainer = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0 1rem 8rem;
|
||||
`;
|
||||
|
||||
const StyledH1 = styled(H1)`
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const P = styled.p`
|
||||
margin: 0.75rem 0 1.5rem;
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
const ContentContainer = styled.div`
|
||||
background: #FFF;
|
||||
border-radius: 0.5rem;
|
||||
box-shadow: 0 0.25rem 0.25rem rgba(0, 0, 0, 0.1),
|
||||
0px 0px 0.25rem rgba(0, 0, 0, 0.1);
|
||||
@media (max-width: 34rem) {
|
||||
width: 100%;
|
||||
}
|
||||
`;
|
||||
|
||||
const SetupTemplate = props => (
|
||||
<PageContainer>
|
||||
<ProgressVisualization progress={props.progress} />
|
||||
<StyledH1>{props.title}</StyledH1>
|
||||
{
|
||||
typeof props.subtitle === 'object'
|
||||
? props.subtitle
|
||||
: <P>{props.subtitle}</P>
|
||||
}
|
||||
<ContentContainer>{props.children}</ContentContainer>
|
||||
</PageContainer>
|
||||
);
|
||||
|
||||
export default SetupTemplate;
|
||||
Reference in New Issue
Block a user