Implement account setup

This commit is contained in:
James Nuanez
2020-04-13 14:53:12 -07:00
parent a4fc109eb5
commit a0d86330e6
48 changed files with 4162 additions and 411 deletions

1
.env Normal file
View File

@@ -0,0 +1 @@
REACT_APP_API_BASE_URL=http://3.94.39.15:3000/v1

16
.eslintrc Normal file
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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"

View File

@@ -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
View 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
View 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;

View 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">&times;</span>
</CloseButton>
</NotificationDiv>
))}
</NotificationContainer>
);
};
export default Notification;

View 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;

View 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>
);
}

View 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', },
];

View 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)' },
],
},
];

View 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
View 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
View 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
View 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
View 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
View 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);

View 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
View 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
View 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;

View 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
View 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;

View 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);

View 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
View 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

View 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
View 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

View 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

View File

@@ -48,7 +48,7 @@ code {
}
::selection {
background-color: #D91C5C;
background-color: rgba(217, 28, 92, 0.75);
color: #FFF;
}

View File

@@ -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
View 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;

View File

View File

@@ -0,0 +1,12 @@
import React from 'react';
import InternalTemplate from '../../templates/InternalTemplate';
const AccountsList = () => (
<InternalTemplate
title="Accounts"
>
content
</InternalTemplate>
);
export default AccountsList;

View File

View File

View File

View File

View 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 &mdash; I'l complete later
</Link>
</Form>
</SetupTemplate>
);
};
export default ConfigureAccount;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;