├── src ├── lib │ ├── index.js │ ├── browser.js │ ├── validators.js │ └── utils.js ├── components │ ├── Card │ │ ├── index.js │ │ ├── Image.jsx │ │ ├── Card.module.scss │ │ └── Card.jsx │ ├── Modal │ │ ├── index.js │ │ ├── Modal.module.scss │ │ └── Modal.jsx │ ├── Avatar │ │ ├── index.js │ │ ├── Avatar.module.scss │ │ └── Avatar.jsx │ ├── Button │ │ ├── index.js │ │ ├── Button.jsx │ │ └── Button.module.scss │ ├── Dropdown │ │ ├── index.js │ │ ├── Dropdown.module.scss │ │ └── Dropdown.jsx │ ├── Loader │ │ ├── index.js │ │ ├── Loader.jsx │ │ └── Loader.module.scss │ ├── Switch │ │ ├── index.js │ │ ├── SwitchButton.jsx │ │ ├── Switch.jsx │ │ └── Switch.module.scss │ ├── TextField │ │ ├── index.js │ │ ├── TextField.module.scss │ │ └── TextField.jsx │ ├── Typography │ │ ├── index.js │ │ ├── Typography.jsx │ │ └── Typography.module.scss │ ├── Notification │ │ ├── index.js │ │ ├── Notification.module.scss │ │ ├── NotificationBase.module.scss │ │ ├── NotificationBase.jsx │ │ └── Notification.jsx │ ├── CircleIndicator │ │ ├── index.js │ │ ├── CircleIndicator.module.scss │ │ └── CircleIndicator.jsx │ ├── PasswordStrength │ │ ├── index.js │ │ ├── PasswordStrength.module.scss │ │ └── PasswordStrength.jsx │ ├── Space.jsx │ └── index.js ├── containers │ ├── Login │ │ ├── index.js │ │ └── Login.module.scss │ ├── Devices │ │ ├── index.js │ │ ├── Devices.module.scss │ │ └── Devices.jsx │ ├── Landing │ │ ├── index.js │ │ ├── Landing.module.scss │ │ └── Landing.jsx │ ├── Loading │ │ ├── index.js │ │ ├── Loading.module.scss │ │ └── Loading.jsx │ ├── Navbar │ │ ├── index.js │ │ ├── Navbar.jsx │ │ ├── NavbarMin.module.scss │ │ ├── NavbarMax.module.scss │ │ ├── NavbarMobile.module.scss │ │ ├── NavbarMin.jsx │ │ ├── NavbarMax.jsx │ │ └── NavbarMobile.jsx │ ├── Profile │ │ ├── index.js │ │ └── Profile.module.scss │ ├── Applications │ │ ├── index.js │ │ ├── Link.jsx │ │ ├── Applications.module.scss │ │ └── Applications.jsx │ └── index.js ├── styles │ ├── variables.scss │ ├── mixins.scss │ ├── index.scss │ └── colors.scss ├── assets │ ├── masq_loading.gif │ ├── bg-landing.svg │ ├── check-circle.svg │ ├── plus-square.svg │ ├── background.svg │ ├── back-to-maps.svg │ ├── cubes-sidebar-min.svg │ ├── windows-masq.svg │ ├── cubes-sidebar.svg │ ├── shield.svg │ ├── cubes.svg │ ├── head-landing-mobile.svg │ ├── head-landing.svg │ ├── logo-sidebar.svg │ ├── logo.svg │ ├── qwant.svg │ ├── hard-disk.svg │ └── apps-maps.svg ├── hooks │ ├── index.js │ ├── useWindowWidth.js │ └── useWindowHeight.js ├── App.module.scss ├── reducers │ ├── index.js │ ├── loading.js │ ├── notification.js │ └── masq.js ├── modals │ ├── PersistentStorageRequest │ │ ├── PersistentStorageRequest.module.scss │ │ └── PersistentStorageRequest.jsx │ ├── UnsupportedBrowser │ │ ├── UnsupportedBrowser.module.scss │ │ └── UnsupportedBrowser.jsx │ ├── PasswordEdit │ │ ├── PasswordEdit.module.scss │ │ └── PasswordEdit.jsx │ ├── DeleteAppDialog │ │ ├── DeleteAppDialog.module.scss │ │ └── DeleteAppDialog.jsx │ ├── DeleteProfileDialog │ │ ├── DeleteProfileDialog.module.scss │ │ └── DeleteProfileDialog.jsx │ ├── SyncDevice │ │ ├── SyncDevice.module.scss │ │ └── SyncDevice.jsx │ ├── index.js │ ├── QRCodeModal │ │ ├── QRCodeModal.module.scss │ │ └── QRCodeModal.jsx │ ├── ConfirmDialog │ │ ├── ConfirmDialog.module.scss │ │ └── ConfirmDialog.jsx │ ├── Signup │ │ └── Signup.module.scss │ └── AuthApp │ │ └── AuthApp.module.scss ├── i18n.js ├── index.js ├── actions │ └── index.js ├── serviceWorker.js └── App.jsx ├── .travis.yml ├── public ├── favicon.png ├── manifest.json └── index.html ├── .storybook ├── preview-head.html ├── addons.js ├── config.js └── webpack.config.js ├── .env ├── .env.production ├── .eslintrc.js ├── stories ├── Landing.stories.js ├── Loader.stories.js ├── CircleIndicator.stories.js ├── Icons.stories.js ├── NotificationBase.stories.js ├── Typography.stories.js ├── PasswordStrength.stories.js ├── Avatar.stories.js ├── TextField.stories.js ├── Button.stories.js ├── Card.stories.js └── Switch.stories.js ├── .gitignore ├── i18next-scanner.config.js ├── karma.conf.js ├── test ├── browser.test.js └── validators.test.js ├── README.md └── package.json /src/lib/index.js: -------------------------------------------------------------------------------- 1 | export { default as Masq } from './masq' 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: node_js 3 | node_js: 4 | - "lts/*" 5 | -------------------------------------------------------------------------------- /src/components/Card/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Card' 2 | -------------------------------------------------------------------------------- /src/components/Modal/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Modal' 2 | -------------------------------------------------------------------------------- /src/containers/Login/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Login' 2 | -------------------------------------------------------------------------------- /src/components/Avatar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Avatar' 2 | -------------------------------------------------------------------------------- /src/components/Button/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Button' 2 | -------------------------------------------------------------------------------- /src/components/Dropdown/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Dropdown' 2 | -------------------------------------------------------------------------------- /src/components/Loader/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Loader' 2 | -------------------------------------------------------------------------------- /src/components/Switch/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Switch' 2 | -------------------------------------------------------------------------------- /src/containers/Devices/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Devices' 2 | -------------------------------------------------------------------------------- /src/containers/Landing/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Landing' 2 | -------------------------------------------------------------------------------- /src/containers/Loading/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Loading' 2 | -------------------------------------------------------------------------------- /src/containers/Navbar/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Navbar' 2 | -------------------------------------------------------------------------------- /src/containers/Profile/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Profile' 2 | -------------------------------------------------------------------------------- /src/components/TextField/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './TextField' 2 | -------------------------------------------------------------------------------- /src/components/Typography/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Typography' 2 | -------------------------------------------------------------------------------- /src/components/Notification/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Notification' 2 | -------------------------------------------------------------------------------- /src/containers/Applications/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './Applications' 2 | -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deiu/masq-app/master/public/favicon.png -------------------------------------------------------------------------------- /src/components/CircleIndicator/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './CircleIndicator' 2 | -------------------------------------------------------------------------------- /src/components/PasswordStrength/index.js: -------------------------------------------------------------------------------- 1 | export { default } from './PasswordStrength' 2 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/styles/variables.scss: -------------------------------------------------------------------------------- 1 | $mobile-width: 700px; 2 | 3 | :export { mobileWidth: $mobile-width } 4 | -------------------------------------------------------------------------------- /src/assets/masq_loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deiu/masq-app/master/src/assets/masq_loading.gif -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-actions/register'; 2 | import '@storybook/addon-links/register'; 3 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | REACT_APP_SIGNALHUB_URLS='localhost:8080' 2 | REACT_APP_REMOTE_WEBRTC=false 3 | # REACT_APP_STUN_URLS 4 | # REACT_APP_TURN_URLS 5 | -------------------------------------------------------------------------------- /src/components/Notification/Notification.module.scss: -------------------------------------------------------------------------------- 1 | .Notification { 2 | position: absolute; 3 | z-index: 3; 4 | width: 100%; 5 | } 6 | -------------------------------------------------------------------------------- /src/components/CircleIndicator/CircleIndicator.module.scss: -------------------------------------------------------------------------------- 1 | .CircleIndicator { 2 | width: 4px; 3 | height: 4px; 4 | border-radius: 50%; 5 | } 6 | -------------------------------------------------------------------------------- /src/hooks/index.js: -------------------------------------------------------------------------------- 1 | export { default as useWindowWidth } from './useWindowWidth' 2 | export { default as useWindowHeight } from './useWindowHeight' 3 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | REACT_APP_SIGNALHUB_URLS='wss://signalhub-jvunerwwrg.now.sh' 2 | REACT_APP_REMOTE_WEBRTC=false 3 | # REACT_APP_STUN_URLS 4 | # REACT_APP_TURN_URLS 5 | -------------------------------------------------------------------------------- /src/App.module.scss: -------------------------------------------------------------------------------- 1 | @import './styles/variables.scss'; 2 | 3 | .layout { 4 | display: flex; 5 | @media (max-width: $mobile-width) { 6 | display: block; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/styles/mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin font($size: 12px, $height: 1.25, $weight: 600, $spacing: normal) { 2 | font-size: $size; 3 | font-weight: $weight; 4 | letter-spacing: $spacing; 5 | line-height: $height; 6 | margin: 0; 7 | } 8 | -------------------------------------------------------------------------------- /src/assets/bg-landing.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import masq from './masq' 3 | import notification from './notification' 4 | import loading from './loading' 5 | 6 | export default combineReducers({ 7 | masq, 8 | notification, 9 | loading 10 | }) 11 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './colors.scss'; 2 | 3 | body { 4 | margin: 0; 5 | padding: 0; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: $color-background; 9 | * { 10 | font-family: Asap; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['standard', 'standard-react'], 3 | plugins: ['mocha'], 4 | env: { 5 | 'mocha': true 6 | }, 7 | overrides: [{ 8 | files: '*.test.js', 9 | rules: { 10 | 'no-unused-expressions': 'off' 11 | } 12 | }] 13 | } 14 | -------------------------------------------------------------------------------- /src/modals/PersistentStorageRequest/PersistentStorageRequest.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .PermanentStorage { 4 | display: flex; 5 | height: 100%; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | text-align: center; 10 | } 11 | -------------------------------------------------------------------------------- /src/reducers/loading.js: -------------------------------------------------------------------------------- 1 | const loading = (state = { 2 | loading: false 3 | }, action) => { 4 | switch (action.type) { 5 | case 'SET_LOADING': 6 | return { 7 | ...state, 8 | loading: action.loading 9 | } 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export default loading 16 | -------------------------------------------------------------------------------- /src/assets/check-circle.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import styles from './Loader.module.scss' 4 | 5 | const Loader = () => ( 6 |
7 |
8 |
9 |
10 |
11 | ) 12 | 13 | export default Loader 14 | -------------------------------------------------------------------------------- /src/modals/UnsupportedBrowser/UnsupportedBrowser.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .UnsupportedBrowser { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: space-between; 8 | text-align: center; 9 | height: 100%; 10 | 11 | .fontMedium { 12 | font-weight: 500; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /stories/Landing.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { Landing } from '../src/containers' 5 | 6 | Landing.displayName = 'Landing' 7 | 8 | storiesOf('Landing', module) 9 | .add('landing', () => ( 10 |
11 | 12 |
13 | )) 14 | -------------------------------------------------------------------------------- /src/containers/index.js: -------------------------------------------------------------------------------- 1 | export { default as Login } from './Login' 2 | export { default as Applications } from './Applications' 3 | export { default as Navbar } from './Navbar' 4 | export { default as Profile } from './Profile' 5 | export { default as Devices } from './Devices' 6 | export { default as Landing } from './Landing' 7 | export { default as Loading } from './Loading' 8 | -------------------------------------------------------------------------------- /src/components/Card/Image.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import styles from './Card.module.scss' 5 | 6 | const Image = ({ image }) => ( 7 |
8 | ) 9 | 10 | Image.propTypes = { 11 | image: PropTypes.string.isRequired 12 | } 13 | 14 | export default Image 15 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | } 10 | ], 11 | "start_url": ".", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /stories/Loader.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { Loader } from '../src/components' 5 | 6 | Loader.displayName = 'Loader' 7 | 8 | storiesOf('Loader', module) 9 | .addParameters({ 10 | info: 'A Loader to show that an operation is in progress.' 11 | }) 12 | .add('loader', () => ( 13 | 14 | )) 15 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import { configure } from '@storybook/react' 2 | 3 | import 'typeface-asap' 4 | import 'typeface-asap-condensed' 5 | 6 | // automatically import all files ending in *.stories.js 7 | const req = require.context('../stories', true, /.stories.js$/) 8 | function loadStories() { 9 | req.keys().forEach(filename => req(filename)) 10 | } 11 | 12 | configure(loadStories, module) 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | .vscode/ 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # testing 8 | /coverage 9 | 10 | # production 11 | /build 12 | 13 | # misc 14 | .DS_Store 15 | .env.local 16 | .env.development.local 17 | .env.test.local 18 | .env.production.local 19 | 20 | npm-debug.log* 21 | yarn-debug.log* 22 | yarn-error.log* 23 | -------------------------------------------------------------------------------- /src/reducers/notification.js: -------------------------------------------------------------------------------- 1 | const notification = (state = { 2 | currentNotification: null // { title: 'test', error: true } 3 | }, action) => { 4 | switch (action.type) { 5 | case 'SET_NOTIFICATION': 6 | return { 7 | ...state, 8 | currentNotification: action.notification 9 | } 10 | default: 11 | return state 12 | } 13 | } 14 | 15 | export default notification 16 | -------------------------------------------------------------------------------- /src/components/CircleIndicator/CircleIndicator.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import styles from './CircleIndicator.module.scss' 5 | 6 | const CircleIndicator = ({ color }) => ( 7 |
8 | ) 9 | 10 | CircleIndicator.propTypes = { 11 | color: PropTypes.string.isRequired 12 | } 13 | 14 | export default CircleIndicator 15 | -------------------------------------------------------------------------------- /stories/CircleIndicator.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { CircleIndicator } from '../src/components' 5 | 6 | CircleIndicator.displayName = 'CircleIndicator' 7 | 8 | storiesOf('CircleIndicator', module) 9 | .addParameters({ 10 | info: 'A simple colored circle to indicate a status' 11 | }) 12 | .add('circleIndicator', () => ( 13 | 14 | )) 15 | -------------------------------------------------------------------------------- /src/containers/Loading/Loading.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .Login { 4 | background-color: $color-dark-blue-100; 5 | position: relative; 6 | min-height: 100vh; 7 | display: flex; 8 | justify-content: space-between; 9 | flex-direction: column; 10 | align-items: center; 11 | text-align: center; 12 | overflow: hidden; 13 | 14 | .Background { 15 | background-color: $color-dark-blue-100; 16 | flex-shrink: 0; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useWindowWidth.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const useWindowWidth = () => { 4 | const [width, setWidth] = useState(window.innerWidth) 5 | 6 | useEffect(() => { 7 | const handleResize = () => setWidth(window.innerWidth) 8 | window.addEventListener('resize', handleResize) 9 | return () => { 10 | window.removeEventListener('resize', handleResize) 11 | } 12 | }) 13 | 14 | return width 15 | } 16 | 17 | export default useWindowWidth 18 | -------------------------------------------------------------------------------- /src/hooks/useWindowHeight.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react' 2 | 3 | const useWindowHeight = () => { 4 | const [width, setWidth] = useState(window.innerHeight) 5 | 6 | useEffect(() => { 7 | const handleResize = () => setWidth(window.innerHeight) 8 | window.addEventListener('resize', handleResize) 9 | return () => { 10 | window.removeEventListener('resize', handleResize) 11 | } 12 | }) 13 | 14 | return width 15 | } 16 | 17 | export default useWindowHeight 18 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.module.scss: -------------------------------------------------------------------------------- 1 | .Dropdown { 2 | display: flex; 3 | width: 150px; 4 | right: 14px; 5 | align-items: center; 6 | top: 64px; 7 | position: absolute; 8 | border-radius: 3px; 9 | background-color: white; 10 | height: 35px; 11 | z-index: 1; 12 | padding-left: 12px; 13 | 14 | font-size: 13px; 15 | line-height: 1.23; 16 | color: #353c52; 17 | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.2); 18 | 19 | :first-child { 20 | margin-right: 8px; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | // Export a function. Accept the base config as the only param. 4 | module.exports = async ({ config, mode }) => { 5 | // `mode` has a value of 'DEVELOPMENT' or 'PRODUCTION' 6 | // You can change the configuration based on that. 7 | // 'PRODUCTION' is used when building the static version of storybook. 8 | 9 | // Make whatever fine-grained changes you need 10 | config.node = { 11 | fs: 'empty' 12 | } 13 | 14 | // Return the altered config 15 | return config 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Space.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { capitalize } from '../lib/utils' 5 | 6 | const Space = ({ size, direction }) => ( 7 |
8 | ) 9 | 10 | Space.defaultProps = { 11 | direction: 'bottom' 12 | } 13 | 14 | Space.propTypes = { 15 | size: PropTypes.number.isRequired, 16 | direction: PropTypes.oneOf([ 17 | 'top', 18 | 'right', 19 | 'bottom', 20 | 'left' 21 | ]) 22 | } 23 | 24 | export default Space 25 | -------------------------------------------------------------------------------- /stories/Icons.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { Camera, Paperclip, Trash, Phone, Settings, Grid, List, LogOut } from 'react-feather' 4 | 5 | storiesOf('Icons', module) 6 | .addParameters({ 7 | info: 'Some icons from feather used across Masq' 8 | }) 9 | .add('feather icons', () => ( 10 |
11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | )) 21 | -------------------------------------------------------------------------------- /src/components/Dropdown/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { LogOut } from 'react-feather' 3 | import PropTypes from 'prop-types' 4 | 5 | import { useTranslation } from 'react-i18next' 6 | import styles from './Dropdown.module.scss' 7 | 8 | const Dropdown = ({ onClick }) => { 9 | const { t } = useTranslation() 10 | return ( 11 |
12 | 13 |

{t('Sign out')}

14 |
15 | ) 16 | } 17 | 18 | Dropdown.propTypes = { 19 | onClick: PropTypes.func 20 | } 21 | 22 | export default Dropdown 23 | -------------------------------------------------------------------------------- /src/modals/PasswordEdit/PasswordEdit.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .PasswordEdit { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | height: 100%; 10 | } 11 | 12 | .buttons { 13 | @media (min-width: $mobile-width + 1px) { 14 | >:first-child { 15 | margin-right: 16px; 16 | } 17 | } 18 | 19 | @media (max-width: $mobile-width) { 20 | display: flex; 21 | flex-direction: column-reverse; 22 | >:first-child { 23 | margin-top: 12px; 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/containers/Devices/Devices.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/variables.scss'; 2 | 3 | .Devices { 4 | width: 100%; 5 | margin: 48px 48px 48px; 6 | 7 | @media (max-width: $mobile-width) { 8 | margin: 16px; 9 | width: unset; 10 | } 11 | 12 | .topSection { 13 | display: flex; 14 | height: 50px; 15 | justify-content: space-between; 16 | } 17 | 18 | .cards { 19 | display: grid; 20 | grid-gap: 28px; 21 | @media (min-width: 1200px) { 22 | margin-right: 365px; 23 | } 24 | 25 | @media (min-width: 1400px) { 26 | grid-template-columns: repeat(2, 1fr); 27 | } 28 | } 29 | 30 | .sidebar { 31 | text-align: left; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /stories/NotificationBase.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { action } from '@storybook/addon-actions' 4 | 5 | import NotificationBase from '../src/components/Notification/NotificationBase.jsx' 6 | 7 | NotificationBase.displayName = 'NotificationBase' 8 | 9 | storiesOf('NotificationBase', module) 10 | .addParameters({ 11 | info: 'Notification to alerts of success/error events.' 12 | }) 13 | .add('default success', () => ( 14 | 15 | )) 16 | .add('error', () => ( 17 | 18 | )) 19 | -------------------------------------------------------------------------------- /src/containers/Navbar/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react' 2 | import MediaQuery from 'react-responsive' 3 | 4 | import NavbarMin from './NavbarMin' 5 | import NavbarMax from './NavbarMax' 6 | import NavBarMobile from './NavbarMobile' 7 | 8 | const Navbar = (props) => { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | export default Navbar 25 | -------------------------------------------------------------------------------- /src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Avatar } from './Avatar' 2 | export { default as Button } from './Button' 3 | export { default as Card } from './Card' 4 | export { default as CircleIndicator } from './CircleIndicator' 5 | export { default as Dropdown } from './Dropdown' 6 | export { default as Loader } from './Loader' 7 | export { default as Modal } from './Modal' 8 | export { default as Notification } from './Notification' 9 | export { default as Switch } from './Switch' 10 | export { default as TextField } from './TextField' 11 | export { default as Typography } from './Typography' 12 | export { default as Space } from './Space' 13 | export { default as PasswordStrength } from './PasswordStrength' 14 | -------------------------------------------------------------------------------- /src/modals/DeleteAppDialog/DeleteAppDialog.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .ConfirmDialog { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | text-align: center; 10 | height: 100%; 11 | 12 | .buttons { 13 | display: flex; 14 | width: 100%; 15 | align-items: center; 16 | @media (min-width: $mobile-width + 1px) { 17 | justify-content: space-between; 18 | } 19 | 20 | @media (max-width: $mobile-width) { 21 | display: flex; 22 | flex-direction: column; 23 | >:first-child { 24 | margin-bottom: 12px; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Switch/SwitchButton.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import cx from 'classnames' 3 | import PropTypes from 'prop-types' 4 | 5 | import styles from './Switch.module.scss' 6 | 7 | const SwitchButton = ({ checked, secondary, onChange, color }) => ( 8 | 12 | ) 13 | 14 | SwitchButton.propTypes = { 15 | checked: PropTypes.bool, 16 | secondary: PropTypes.bool, 17 | onChange: PropTypes.func, 18 | color: PropTypes.string.isRequired 19 | } 20 | 21 | export default SwitchButton 22 | -------------------------------------------------------------------------------- /src/modals/DeleteProfileDialog/DeleteProfileDialog.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .ConfirmDialog { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | text-align: center; 10 | height: 100%; 11 | 12 | .buttons { 13 | display: flex; 14 | width: 100%; 15 | align-items: center; 16 | @media (min-width: $mobile-width + 1px) { 17 | justify-content: space-between; 18 | } 19 | 20 | @media (max-width: $mobile-width) { 21 | display: flex; 22 | flex-direction: column; 23 | >:first-child { 24 | margin-bottom: 12px; 25 | } 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/assets/plus-square.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/lib/browser.js: -------------------------------------------------------------------------------- 1 | import { detect } from 'detect-browser' 2 | 3 | const SUPPORTED_BROWSERS_CODES = [ 4 | 'firefox', 5 | 'chrome', 6 | 'safari', 7 | 'ios', 8 | 'android', 9 | 'crios', 10 | 'fxios', 11 | 'samsung' 12 | ] 13 | 14 | // Recommended browsers displayed in the modal 15 | // if the current browser is unsupported 16 | const SUPPORTED_BROWSERS = [ 17 | 'firefox', 18 | 'brave', // brave will be detected as chrome 19 | 'chrome', 20 | 'safari' 21 | ] 22 | 23 | const isBrowserSupported = () => { 24 | const browser = detect() 25 | if (!browser) return false 26 | return !!SUPPORTED_BROWSERS_CODES.includes(browser.name) 27 | } 28 | 29 | export { 30 | isBrowserSupported, 31 | SUPPORTED_BROWSERS, 32 | SUPPORTED_BROWSERS_CODES 33 | } 34 | -------------------------------------------------------------------------------- /src/modals/SyncDevice/SyncDevice.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .SyncDevice { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | height: 100%; 8 | width: 100%; 9 | 10 | input { 11 | font-size: 14px; 12 | font-weight: 600; 13 | letter-spacing: 0.7px; 14 | text-align: center; 15 | color: $color-blue; 16 | margin-bottom: 48px; 17 | outline: none; 18 | border: 0; 19 | } 20 | 21 | .loader { 22 | margin-top: 32px; 23 | } 24 | 25 | .refeshIcon { 26 | animation: rotation 3s infinite linear; 27 | } 28 | 29 | @keyframes rotation { 30 | from { 31 | transform: rotate(0deg); 32 | } 33 | to { 34 | transform: rotate(359deg); 35 | } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/modals/index.js: -------------------------------------------------------------------------------- 1 | export { default as Signup } from './Signup/Signup' 2 | export { default as AuthApp } from './AuthApp/AuthApp' 3 | export { default as QRCodeModal } from './QRCodeModal/QRCodeModal' 4 | export { default as SyncDevice } from './SyncDevice/SyncDevice' 5 | export { default as PersistentStorageRequest } from './PersistentStorageRequest/PersistentStorageRequest' 6 | export { default as ConfirmDialog } from './ConfirmDialog/ConfirmDialog' 7 | export { default as DeleteProfileDialog } from './DeleteProfileDialog/DeleteProfileDialog' 8 | export { default as DeleteAppDialog } from './DeleteAppDialog/DeleteAppDialog' 9 | export { default as PasswordEdit } from './PasswordEdit/PasswordEdit' 10 | export { default as UnsupportedBrowser } from './UnsupportedBrowser/UnsupportedBrowser' 11 | -------------------------------------------------------------------------------- /src/modals/QRCodeModal/QRCodeModal.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .QRCode { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | height: 100%; 8 | width: 100%; 9 | 10 | input { 11 | font-size: 14px; 12 | font-weight: 600; 13 | } 14 | 15 | .input { 16 | input { 17 | color: $color-blue; 18 | } 19 | 20 | } 21 | .copied { 22 | input { 23 | color: white; 24 | background-color: $color-blue; 25 | border: solid 1px #353c52; 26 | } 27 | svg { 28 | color: white; 29 | } 30 | } 31 | } 32 | 33 | .pill { 34 | height: 28px; 35 | width: 120px; 36 | line-height: 28px; 37 | background-color: $color-green; 38 | color: white; 39 | border-radius: 50px; 40 | font-size: 14px; 41 | text-align: center; 42 | } 43 | -------------------------------------------------------------------------------- /src/components/Loader/Loader.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | $color-blue-grey: #697496; 4 | 5 | @keyframes dot-keyframes { 6 | 0% { 7 | opacity: 1; 8 | } 9 | 50% { 10 | opacity: 0; 11 | } 12 | 100% { 13 | opacity: 1; 14 | } 15 | } 16 | 17 | .Loader { 18 | display: flex; 19 | 20 | .dot { 21 | width: 16px; 22 | height: 16px; 23 | border-radius: 100%; 24 | animation: dot-keyframes 2s infinite ease-in-out; 25 | 26 | &:nth-child(1) { 27 | margin-right: 10px; 28 | background-color: black; 29 | } 30 | 31 | &:nth-child(2) { 32 | margin-right: 10px; 33 | background-color: black; 34 | animation-delay: .5s 35 | } 36 | 37 | &:nth-child(3) { 38 | background-color: black; 39 | animation-delay: 1s; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/containers/Applications/Link.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { ExternalLink } from 'react-feather' 4 | import { useTranslation } from 'react-i18next' 5 | 6 | import { Typography } from '../../components' 7 | import styles from './Applications.module.scss' 8 | 9 | const Link = ({ url, label }) => { 10 | const { t } = useTranslation() 11 | 12 | return ( 13 |
14 | 15 | 16 | {label || t('Open application')} 17 | 18 |
19 | ) 20 | } 21 | 22 | Link.propTypes = { 23 | url: PropTypes.string, 24 | label: PropTypes.string 25 | } 26 | 27 | export default Link 28 | -------------------------------------------------------------------------------- /src/modals/ConfirmDialog/ConfirmDialog.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .ConfirmDialog { 4 | display: flex; 5 | flex-direction: column; 6 | align-items: center; 7 | justify-content: center; 8 | text-align: center; 9 | margin-top: 80px; 10 | 11 | .title { 12 | font-size: 24px; 13 | font-weight: bold; 14 | line-height: 1.04; 15 | text-align: center; 16 | margin-bottom: 32px; 17 | color: $color-dark-blue-100; 18 | } 19 | 20 | p { 21 | width: 400px; 22 | font-size: 14px; 23 | font-weight: 500; 24 | line-height: 1.57; 25 | margin-bottom: 48px; 26 | } 27 | 28 | Button { 29 | width: 185px; 30 | margin-right: 16px; 31 | margin-left: 16px; 32 | } 33 | 34 | .buttons { 35 | width: 100%; 36 | display: flex; 37 | justify-content: space-evenly; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | input: [ 3 | 'src/**/*.{js,jsx}', 4 | // Use ! to filter out files or directories 5 | '!**/node_modules/**' 6 | ], 7 | options: { 8 | debug: true, 9 | removeUnusedKeys: true, 10 | func: { 11 | list: ['i18next.t', 'i18n.t', 't'], 12 | extensions: ['.js', '.jsx'] 13 | }, 14 | lngs: ['en', 'fr'], 15 | defaultLng: 'en', 16 | defaultNs: 'resource', 17 | defaultValue: '__STRING_NOT_TRANSLATED__', 18 | resource: { 19 | loadPath: 'public/locales/{{lng}}.json', 20 | savePath: 'public/locales/{{lng}}.json', 21 | jsonIndent: 2, 22 | lineEnding: '\n' 23 | }, 24 | nsSeparator: false, // namespace separator 25 | keySeparator: false, // key separator 26 | interpolation: { 27 | prefix: '{{', 28 | suffix: '}}' 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | process.env.CHROME_BIN = require('puppeteer').executablePath() 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | frameworks: ['mocha', 'chai'], 6 | files: ['test/**/*.js'], 7 | reporters: ['progress'], 8 | port: 9876, // karma web server port 9 | colors: true, 10 | logLevel: config.LOG_INFO, 11 | browsers: ['ChromeHeadless'], 12 | autoWatch: false, 13 | singleRun: true, 14 | concurrency: Infinity, 15 | preprocessors: { 16 | 'test/**/*.js': [ 'webpack', 'sourcemap' ] 17 | }, 18 | webpack: { 19 | mode: 'development', 20 | node: { 21 | fs: 'empty' 22 | } 23 | }, 24 | webpackMiddleware: { 25 | stats: 'errors-only', 26 | devtool: 'inline-source-map' 27 | }, 28 | browserDisconnectTimeout: 60000, 29 | browserNoActivityTimeout: 60000 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Card/Card.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/mixins.scss'; 2 | @import '../../styles/colors.scss'; 3 | 4 | .Card { 5 | border: solid 1px $color-grey-200; 6 | border-radius: 3px; 7 | background-color: white; 8 | width: 100%; 9 | 10 | .content { 11 | margin-left: 24px; 12 | margin-right: 24px; 13 | margin-bottom: 24px; 14 | } 15 | 16 | .Image { 17 | height: 58px; 18 | background-repeat: no-repeat; 19 | background-position: center; 20 | background-size: cover; 21 | } 22 | 23 | .Header { 24 | display: flex; 25 | align-items: center; 26 | justify-content: space-between; 27 | 28 | .marker { 29 | margin-top: 24px; 30 | margin-bottom: 16px; 31 | width: 40px; 32 | height: 4px; 33 | border-radius: 2px; 34 | } 35 | } 36 | 37 | .Description { 38 | margin-top: 8px; 39 | margin-bottom: 16px; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/Notification/NotificationBase.module.scss: -------------------------------------------------------------------------------- 1 | .Notification { 2 | display: flex; 3 | align-items: center; 4 | justify-content: space-between; 5 | height: 48px; 6 | background-color: #495063; 7 | 8 | .title { 9 | margin: auto; 10 | display: flex; 11 | align-items: center; 12 | 13 | p { 14 | color: white; 15 | font-size: 14px; 16 | font-weight: 500; 17 | font-style: normal; 18 | font-stretch: normal; 19 | line-height: 1.21; 20 | letter-spacing: normal; 21 | } 22 | } 23 | 24 | .iconContainer { 25 | height: 16px; 26 | width: 16px; 27 | margin-right: 16px; 28 | border-radius: 100%; 29 | display: flex; 30 | align-items: center; 31 | justify-content: center; 32 | background-color: white; 33 | } 34 | 35 | .closeBtn { 36 | margin-right: 20px; 37 | color: white; 38 | cursor: pointer; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/containers/Loading/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { ReactComponent as Cubes } from '../../assets/cubes.svg' 4 | import { ReactComponent as Logo } from '../../assets/logo.svg' 5 | import { Space, Typography } from '../../components' 6 | import animation from '../../assets/masq_loading.gif' 7 | 8 | import styles from './Loading.module.scss' 9 | 10 | const Loading = () => { 11 | const { t } = useTranslation() 12 | return ( 13 |
14 |
15 | 16 | 17 | 18 |
19 | loading 20 | {t('Loading...')} 21 | 22 |
23 | ) 24 | } 25 | 26 | export default Loading 27 | -------------------------------------------------------------------------------- /src/components/Notification/NotificationBase.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Check, X } from 'react-feather' 5 | 6 | import styles from './NotificationBase.module.scss' 7 | 8 | const NotificationBase = ({ error, title, onClose }) => { 9 | const Icon = error 10 | ? 11 | : 12 | 13 | return ( 14 |
15 |
16 |
17 | {Icon} 18 |
19 |

{title}

20 |
21 | 22 |
23 | ) 24 | } 25 | 26 | NotificationBase.propTypes = { 27 | error: PropTypes.bool, 28 | onClose: PropTypes.func, 29 | title: PropTypes.string.isRequired 30 | } 31 | 32 | export default NotificationBase 33 | -------------------------------------------------------------------------------- /src/modals/Signup/Signup.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .Signup { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | justify-content: space-between; 9 | min-height: 100%; 10 | 11 | .avatar { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: center; 15 | } 16 | } 17 | 18 | .user { 19 | font-size: 18px; 20 | font-weight: 600; 21 | line-height: 1.11; 22 | text-align: center; 23 | color: $color-dark-blue-100 !important; 24 | } 25 | 26 | .buttons { 27 | @media (min-width: $mobile-width + 1px) { 28 | >:nth-child(2) { 29 | margin-left: 12px; 30 | } 31 | } 32 | 33 | @media (max-width: $mobile-width) { 34 | display: flex; 35 | flex-direction: column-reverse; 36 | >:first-child { 37 | margin-top: 12px; 38 | } 39 | } 40 | } 41 | 42 | .TextField { 43 | text-align: left; 44 | } 45 | 46 | .fontMedium { 47 | font-weight: 500; 48 | } 49 | -------------------------------------------------------------------------------- /src/modals/AuthApp/AuthApp.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .AuthApp { 5 | display: flex; 6 | flex-direction: column; 7 | align-items: center; 8 | height: 100%; 9 | width: 100%; 10 | text-align: center; 11 | justify-content: space-between; 12 | 13 | .appTitle { 14 | font-size: 32px; 15 | font-weight: bold; 16 | letter-spacing: 0.2px; 17 | text-align: center; 18 | color: $color-dark-blue-100; 19 | margin: 0; 20 | margin-bottom: 16px; 21 | } 22 | 23 | .buttons { 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | width: 100%; 28 | @media (min-width: $mobile-width + 1px) { 29 | justify-content: space-between; 30 | } 31 | 32 | @media (max-width: $mobile-width) { 33 | margin-top: 0; 34 | display: flex; 35 | flex-direction: column-reverse; 36 | >:first-child { 37 | margin-top: 12px; 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/styles/colors.scss: -------------------------------------------------------------------------------- 1 | $color-grey-100: #f4f6fa; 2 | $color-grey-200: #e0e1e6; 3 | $color-grey-300: #c8cbd3; 4 | :export { colorGrey100: $color-grey-100 } 5 | :export { colorGrey200: $color-grey-200 } 6 | :export { colorGrey300: $color-grey-300; } 7 | 8 | $color-dark-blue-100: #252a39; 9 | $color-dark-blue-200: #171c2f; 10 | :export { colorDarkBlue100: $color-dark-blue-100; } 11 | :export { colorDarkBlue200: $color-dark-blue-200; } 12 | 13 | $color-cyan: #00cafc; 14 | $color-blue: #03a0f9; 15 | $color-green: #40ae6c; 16 | $color-red: #e53b5b; 17 | $color-blue-grey: #5c6f84; 18 | $color-yellow: #f3b100; 19 | $color-purple: #a3005c; 20 | :export { colorCyan: $color-cyan } 21 | :export { colorBlue: $color-blue; } 22 | :export { colorGreen: $color-green; } 23 | :export { colorRed: $color-red; } 24 | :export { colorBlueGrey: $color-blue-grey; } 25 | :export { colorYellow: $color-yellow; } 26 | :export { colorPurple: $color-purple; } 27 | 28 | $color-background: $color-grey-100; 29 | :export { colorBackground: $color-background; } 30 | -------------------------------------------------------------------------------- /stories/Typography.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { Typography } from '../src/components' 5 | 6 | Typography.displayName = 'Typography' 7 | 8 | storiesOf('Typography', module) 9 | .addParameters({ 10 | info: 'Typography' 11 | }) 12 | .add('titleModal', () => ( 13 | Title modal 14 | )) 15 | .add('title', () => ( 16 | Title 17 | )) 18 | .add('paragraph', () => ( 19 | Text 20 | )) 21 | .add('paragraph-modal', () => ( 22 | Text 23 | )) 24 | .add('username', () => ( 25 | Username 26 | )) 27 | .add('label', () => ( 28 | Label 29 | )) 30 | .add('label-nav', () => ( 31 | Label nav 32 | )) 33 | -------------------------------------------------------------------------------- /src/lib/validators.js: -------------------------------------------------------------------------------- 1 | const isName = str => /^$|^[A-zÀ-ú\- ]+$/.test(str) 2 | 3 | const isUsername = str => /^[\w!?$#@()\-*]+$/.test(str) 4 | 5 | const getForce = str => { 6 | const info = getPasswordInfo(str) 7 | return str.length < 6 ? 0 : computeScore(info) 8 | } 9 | 10 | const isForceEnough = (str) => getForce(str) > 1 11 | 12 | const containUppercase = str => /[A-Z]/.test(str) 13 | 14 | const containLowercase = str => /[a-z]/.test(str) 15 | 16 | const containNumber = str => /[0-9]/.test(str) 17 | 18 | const containSpecialCharacter = str => /[="!?$#%@()\\\-_/@^+*&:<>{};~'`.|[\]]/.test(str) 19 | 20 | const getPasswordInfo = str => ({ 21 | lowercase: containLowercase(str), 22 | uppercase: containUppercase(str), 23 | number: containNumber(str), 24 | specialCharacter: containSpecialCharacter(str), 25 | secureLength: str.length >= 12 26 | }) 27 | 28 | const computeScore = info => Object.keys(info).reduce((acc, cur) => acc + (info[cur] ? 1 : 0), 0) 29 | 30 | export { isName, isUsername, getPasswordInfo, getForce, isForceEnough } 31 | -------------------------------------------------------------------------------- /stories/PasswordStrength.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { PasswordStrength } from '../src/components' 5 | 6 | storiesOf('PasswordStrength', module) 7 | .addParameters({ 8 | info: 'A Card can contain two children: actions displayed in the top right corner, and a footer.' 9 | }) 10 | .add('an empty password', () => ( 11 | 12 | )) 13 | .add('a password with only lowercases: ag', () => ( 14 | 15 | )) 16 | .add('a password with also an uppercase : agzE', () => ( 17 | 18 | )) 19 | .add('a password with also a number : agzE28', () => ( 20 | 21 | )) 22 | .add('a password with also a special character : agzE28#', () => ( 23 | 24 | )) 25 | .add('a password like : agtyzE28#58956', () => ( 26 | 27 | )) 28 | -------------------------------------------------------------------------------- /src/components/Notification/Notification.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { connect } from 'react-redux' 4 | 5 | import NotificationBase from './NotificationBase' 6 | import { setNotification } from '../../actions/index' 7 | 8 | import styles from './Notification.module.scss' 9 | 10 | const TIMEOUT = 5000 11 | 12 | const Notification = ({ setNotification, ...props }) => { 13 | if (!props.error) { 14 | // Close notification after timeout only if it is not an error 15 | setTimeout(() => setNotification(null), TIMEOUT) 16 | } 17 | 18 | return ( 19 |
20 | setNotification(null)} /> 21 |
22 | ) 23 | } 24 | 25 | const mapDispatchToProps = dispatch => ({ 26 | setNotification: notif => dispatch(setNotification(notif)) 27 | }) 28 | 29 | Notification.propTypes = { 30 | setNotification: PropTypes.func, 31 | error: PropTypes.bool 32 | } 33 | 34 | export default connect(null, mapDispatchToProps)(Notification) 35 | -------------------------------------------------------------------------------- /src/components/Switch/Switch.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Typography from '../Typography' 5 | import SwitchButton from './SwitchButton' 6 | 7 | import styles from './Switch.module.scss' 8 | 9 | const Switch = (props) => ( 10 | props.label 11 | ? ( 12 |
13 | {props.label} 14 | 15 |
16 | ) 17 | : 18 | ) 19 | 20 | Switch.defaultProps = { 21 | checked: false, 22 | secondary: false, 23 | label: '' 24 | } 25 | 26 | Switch.propTypes = { 27 | /** Initial checked state */ 28 | checked: PropTypes.bool, 29 | 30 | /** secondary style */ 31 | secondary: PropTypes.bool, 32 | 33 | /** Label to display */ 34 | label: PropTypes.string, 35 | 36 | /** Current color */ 37 | color: PropTypes.string.isRequired, 38 | 39 | /** onChange handler */ 40 | onChange: PropTypes.func 41 | } 42 | 43 | export default Switch 44 | -------------------------------------------------------------------------------- /src/i18n.js: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import Backend from 'i18next-xhr-backend' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | import { initReactI18next } from 'react-i18next' 5 | 6 | i18n 7 | // load translation using xhr -> see /public/locales 8 | // learn more: https://github.com/i18next/i18next-xhr-backend 9 | .use(Backend) 10 | // detect user language 11 | // learn more: https://github.com/i18next/i18next-browser-languageDetector 12 | .use(LanguageDetector) 13 | // pass the i18n instance to react-i18next. 14 | .use(initReactI18next) 15 | // init i18next 16 | // for all options read: https://www.i18next.com/overview/configuration-options 17 | .init({ 18 | fallbackLng: 'en', 19 | nsSeparator: false, 20 | keySeparator: false, 21 | debug: process.env.NODE_ENV === 'development', 22 | load: 'languageOnly', // en, fr ... 23 | interpolation: { 24 | escapeValue: false // not needed for react as it escapes by default 25 | }, 26 | backend: { 27 | loadPath: '/locales/{{lng}}.json' 28 | } 29 | }) 30 | 31 | export default i18n 32 | -------------------------------------------------------------------------------- /src/reducers/masq.js: -------------------------------------------------------------------------------- 1 | const masq = (state = { 2 | syncStep: 0, 3 | users: [], 4 | apps: [], 5 | devices: [], 6 | currentUser: null, 7 | currentAppRequest: null 8 | }, action) => { 9 | switch (action.type) { 10 | case 'SIGNIN': 11 | return { ...state, currentUser: action.profile } 12 | case 'SIGNOUT': 13 | return { ...state, currentUser: null } 14 | case 'RECEIVE_USERS': 15 | return { ...state, users: action.users } 16 | case 'RECEIVE_APPS': 17 | return { ...state, apps: action.apps } 18 | case 'SET_CURRENT_APP_REQUEST': 19 | return { ...state, currentAppRequest: action.app } 20 | case 'UPDATE_CURRENT_APP_REQUEST': 21 | return { ...state, currentAppRequest: { ...state.currentAppRequest, ...action.update } } 22 | case 'ADD_APP': 23 | return { ...state, apps: [...state.apps, action.app] } 24 | case 'ADD_DEVICE': 25 | return { ...state, devices: [...state.devices, action.device] } 26 | case 'SET_SYNC_STEP': 27 | return { ...state, syncStep: action.syncStep } 28 | default: 29 | return state 30 | } 31 | } 32 | 33 | export default masq 34 | -------------------------------------------------------------------------------- /src/modals/ConfirmDialog/ConfirmDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { useTranslation } from 'react-i18next' 4 | import { Button, Modal } from '../../components' 5 | 6 | import styles from './ConfirmDialog.module.scss' 7 | 8 | const ConfirmDialog = ({ title, text, onConfirm, onCancel, onClose }) => { 9 | const { t } = useTranslation() 10 | 11 | return ( 12 | 13 |
14 | {title} 15 |

{text}

16 |
17 | 18 | 19 |
20 |
21 |
22 | ) 23 | } 24 | 25 | ConfirmDialog.propTypes = { 26 | title: PropTypes.string.isRequired, 27 | text: PropTypes.string.isRequired, 28 | onConfirm: PropTypes.func.isRequired, 29 | onCancel: PropTypes.func.isRequired, 30 | onClose: PropTypes.func.isRequired 31 | } 32 | 33 | export default ConfirmDialog 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from 'react' 2 | import { render } from 'react-dom' 3 | import { createStore, applyMiddleware, compose } from 'redux' 4 | import thunkMiddleware from 'redux-thunk' 5 | import { Provider } from 'react-redux' 6 | 7 | import 'typeface-asap' 8 | import 'typeface-asap-condensed' 9 | 10 | import './i18n' 11 | import * as serviceWorker from './serviceWorker' 12 | import rootReducer from './reducers' 13 | import App from './App' 14 | 15 | import './styles/index.scss' 16 | 17 | const composeEnhancer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose 18 | 19 | const store = createStore( 20 | rootReducer, 21 | composeEnhancer(applyMiddleware(thunkMiddleware)) 22 | ) 23 | 24 | render( 25 | 26 | 27 | 28 | 29 | , 30 | document.getElementById('root') 31 | ) 32 | 33 | // If you want your app to work offline and load faster, you can change 34 | // unregister() to register() below. Note this comes with some pitfalls. 35 | // Learn more about service workers: http://bit.ly/CRA-PWA 36 | serviceWorker.unregister() 37 | -------------------------------------------------------------------------------- /test/browser.test.js: -------------------------------------------------------------------------------- 1 | import * as sinon from 'sinon' 2 | import DetectBrowser from 'detect-browser' 3 | 4 | import { isBrowserSupported, SUPPORTED_BROWSERS_CODES } from '../src/lib/browser' 5 | 6 | const { expect } = require('chai') 7 | 8 | describe('Browser support', () => { 9 | let stub = null 10 | 11 | const stubBrowser = (name) => { 12 | stub = sinon 13 | .stub(DetectBrowser, 'detect') 14 | .callsFake(() => ({ name })) 15 | } 16 | 17 | afterEach(() => { 18 | stub.restore() 19 | }) 20 | 21 | it('should return true for browsers in whitelist', () => { 22 | SUPPORTED_BROWSERS_CODES.forEach((browser) => { 23 | stubBrowser(browser) 24 | expect(isBrowserSupported()).to.be.true 25 | stub.restore() 26 | }) 27 | }) 28 | 29 | it('should return false for non-whitelisted browsers', () => { 30 | stubBrowser('ie') 31 | expect(isBrowserSupported()).to.be.false 32 | }) 33 | 34 | it('should return false for non-detected browsers', () => { 35 | stub = sinon 36 | .stub(DetectBrowser, 'detect') 37 | .callsFake(() => null) 38 | 39 | expect(isBrowserSupported()).to.be.false 40 | }) 41 | }) 42 | -------------------------------------------------------------------------------- /src/lib/utils.js: -------------------------------------------------------------------------------- 1 | import Compressor from 'compressorjs' 2 | 3 | const MAX_IMAGE_SIZE = 100000 // 100 KB 4 | 5 | const isUsernameAlreadyTaken = (username, id) => { 6 | const ids = Object 7 | .keys(window.localStorage) 8 | .filter(k => k.split('-')[0] === 'profile') 9 | 10 | if (!ids) return false 11 | 12 | const publicProfiles = ids.map(id => JSON.parse(window.localStorage.getItem(id))) 13 | 14 | return publicProfiles.find(p => 15 | (id && p.id === id) ? false : p.username === username 16 | ) 17 | } 18 | 19 | const compressImage = (file) => { 20 | return new Promise((resolve, reject) => { 21 | const image = new Compressor(file, { // eslint-disable-line no-unused-vars 22 | quality: 0.8, 23 | width: 512, 24 | height: 512, 25 | convertSize: 0, 26 | success (result) { 27 | resolve(result) 28 | }, 29 | error (err) { 30 | reject(err.message) 31 | } 32 | }) 33 | }) 34 | } 35 | 36 | const capitalize = (string) => ( 37 | string.charAt(0).toUpperCase() + string.slice(1) 38 | ) 39 | 40 | export { 41 | isUsernameAlreadyTaken, 42 | compressImage, 43 | capitalize, 44 | MAX_IMAGE_SIZE 45 | } 46 | -------------------------------------------------------------------------------- /src/components/Button/Button.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import cx from 'classnames' 4 | 5 | import styles from './Button.module.scss' 6 | 7 | const Button = ({ onClick, color, children, width, height, borderRadius, secondary }) => ( 8 | 24 | ) 25 | 26 | Button.defaultProps = { 27 | color: 'primary', 28 | height: 50, 29 | borderRadius: 6 30 | } 31 | 32 | Button.propTypes = { 33 | onClick: PropTypes.func, 34 | color: PropTypes.oneOf([ 35 | 'primary', 36 | 'success', 37 | 'danger', 38 | 'neutral', 39 | 'light' 40 | ]), 41 | width: PropTypes.number, 42 | height: PropTypes.number, 43 | secondary: PropTypes.bool, 44 | children: PropTypes.string, 45 | borderRadius: PropTypes.number 46 | } 47 | 48 | export default Button 49 | -------------------------------------------------------------------------------- /stories/Avatar.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { Avatar } from '../src/components' 5 | 6 | Avatar.displayName = 'Avatar' 7 | 8 | storiesOf('Avatar', module) 9 | .add('without image', () => ( 10 | 11 | )) 12 | .add('with an image', () => ( 13 | 14 | )) 15 | .add('with upload', () => ( 16 | 17 | )) 18 | .add('with custom size', () => ( 19 | 20 | )) 21 | .add('default avatars based on username', () => ( 22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 |
30 | )) 31 | -------------------------------------------------------------------------------- /stories/TextField.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { action } from '@storybook/addon-actions' 4 | import { Eye } from 'react-feather' 5 | 6 | import { TextField, Typography } from '../src/components' 7 | 8 | TextField.displayName = 'TextField' 9 | 10 | storiesOf('TextField', module) 11 | .addParameters({ 12 | info: 'A TextField with a label, which can be either idle, focused, or in an error state.' 13 | }) 14 | .add('default', () => ( 15 | 19 | )) 20 | .add('error', () => ( 21 | 26 | )) 27 | .add('password field', () => ( 28 | } 33 | onChange={action('onChange')} 34 | /> 35 | )) 36 | .add('with a button', () => ( 37 | 43 | )) 44 | -------------------------------------------------------------------------------- /src/assets/background.svg: -------------------------------------------------------------------------------- 1 | 2 | 6 | 10 | 14 | 18 | 19 | -------------------------------------------------------------------------------- /stories/Button.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | import { action } from '@storybook/addon-actions' 4 | 5 | import { Button } from '../src/components' 6 | 7 | Button.displayName = 'Button' 8 | 9 | storiesOf('Button', module) 10 | .addParameters({ 11 | info: 'A button of type primary (default) or secondary.' 12 | }) 13 | .add('primary', () => ( 14 | 20 | )) 21 | .add('success', () => ( 22 | 28 | )) 29 | .add('danger', () => ( 30 | 36 | )) 37 | .add('neutral', () => ( 38 | 44 | )) 45 | .add('secondary', () => ( 46 | 52 | )) 53 | -------------------------------------------------------------------------------- /src/containers/Navbar/NavbarMin.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .Sidebar { 4 | height: 100vh; 5 | width: 100px; 6 | display: flex; 7 | flex-direction: column; 8 | align-items: center; 9 | background-color: $color-dark-blue-100; 10 | justify-content: space-between; 11 | 12 | .header { 13 | width: 100px; 14 | height: 212px; 15 | display: flex; 16 | flex-direction: column; 17 | justify-content: center; 18 | align-items: center; 19 | background-color: $color-dark-blue-200; 20 | } 21 | 22 | .nav { 23 | padding-top: 32px; 24 | display: flex; 25 | align-items: center; 26 | justify-content: center; 27 | justify-items: center; 28 | flex-direction: column; 29 | >:not(:last-child) { 30 | margin-bottom: 40px; 31 | } 32 | } 33 | 34 | .navElement { 35 | cursor: pointer; 36 | z-index: 1; 37 | &:hover * { 38 | opacity: 1; 39 | } 40 | text-decoration: none; 41 | color: white; 42 | } 43 | 44 | .active * { 45 | opacity: 1; 46 | color: $color-blue; 47 | } 48 | 49 | .logout { 50 | z-index: 1; 51 | } 52 | 53 | .cubes { 54 | position: absolute; 55 | bottom: 0; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/assets/back-to-maps.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/modals/PersistentStorageRequest/PersistentStorageRequest.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { useTranslation } from 'react-i18next' 5 | import { Button, Modal, Typography, Space } from '../../components' 6 | import styles from './PersistentStorageRequest.module.scss' 7 | 8 | const PermanentStorageRequest = ({ onClose }) => { 9 | const { t } = useTranslation() 10 | return ( 11 | 12 |
13 |
14 | {t('Storage authorization')} /> 15 | 16 | 17 | {`${t('In order to store your data securely,')} 18 | ${t('please authorize Masq to use the persistent storage of the browser.')} 19 | ${t('This notification will appear again if necessary.')}`} 20 | 21 |
22 | 23 | 24 |
25 |
26 | ) 27 | } 28 | 29 | PermanentStorageRequest.propTypes = { 30 | onClose: PropTypes.func.isRequired 31 | } 32 | 33 | export default PermanentStorageRequest 34 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .Modal { 5 | @media (max-width: $mobile-width) { 6 | position: relative; 7 | } 8 | 9 | .overlay { 10 | position: fixed; 11 | top: 0; 12 | left: 0; 13 | width: 100%; 14 | height: 100%; 15 | background-image: linear-gradient(to bottom, #6c7694, #353c52); 16 | z-index: 2; 17 | } 18 | 19 | .modal { 20 | position: fixed; 21 | top: 0; 22 | left: 0; 23 | z-index: 2; 24 | background-color: white; 25 | box-shadow: 0 2px 16px 0 rgba(0, 0, 0, 0.16); 26 | opacity: 1; 27 | max-width: calc(100vw - 56px); 28 | padding-left: 28px; 29 | padding-right: 28px; 30 | padding-top: 64px; 31 | padding-bottom: 32px; 32 | 33 | @media (min-width: $mobile-width + 1px) { 34 | border-radius: 6px; 35 | top: 50%; 36 | left: 50%; 37 | transform: translate(-50%, -50%); 38 | } 39 | 40 | @media (max-width: $mobile-width) { 41 | height: calc(100% - 96px); 42 | overflow-y: auto; 43 | } 44 | } 45 | 46 | .close { 47 | position: absolute; 48 | top: 20px; 49 | right: 20px; 50 | color: #191919; 51 | opacity: 0.4; 52 | cursor: pointer; 53 | margin-bottom: 44px; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/modals/UnsupportedBrowser/UnsupportedBrowser.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { Modal, Typography, Space } from '../../components' 4 | import { SUPPORTED_BROWSERS } from '../../lib/browser' 5 | import { capitalize } from '../../lib/utils' 6 | import { AlertOctagon } from 'react-feather' 7 | 8 | import styles from './UnsupportedBrowser.module.scss' 9 | 10 | const UnsupportedBrowser = () => { 11 | const { t } = useTranslation() 12 | 13 | return ( 14 | 15 |
16 | {t('Browser not supported')} 17 | 18 | 19 | 20 | {t('Your browser is not compatible with Masq. Please, try Masq with one of the compatible browsers:')} 21 | 22 |
23 | {SUPPORTED_BROWSERS.map((browser, index) => ( 24 | - {capitalize(browser)} 25 | ))} 26 |
27 |
28 |
29 | ) 30 | } 31 | 32 | export default UnsupportedBrowser 33 | -------------------------------------------------------------------------------- /src/components/Typography/Typography.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import cx from 'classnames' 4 | 5 | import styles from './Typography.module.scss' 6 | 7 | const Typography = ({ className, maxWidth, type, children, color, align, line }) => ( 8 |

21 | {children} 22 |

23 | ) 24 | 25 | Typography.propTypes = { 26 | className: PropTypes.string, 27 | maxWidth: PropTypes.number, 28 | children: PropTypes.any.isRequired, 29 | color: PropTypes.string, 30 | type: PropTypes.oneOf([ 31 | 'title', 32 | 'title-landing', 33 | 'title-landing2', 34 | 'title-modal', 35 | 'title-page', 36 | 'subtitle-page', 37 | 'title-card', 38 | 'paragraph', 39 | 'paragraph-landing', 40 | 'paragraph-landing-dark', 41 | 'paragraph-modal', 42 | 'username', 43 | 'username-alt', 44 | 'label', 45 | 'label-nav', 46 | 'footer', 47 | 'textFieldButton', 48 | 'avatarUsername' 49 | ]).isRequired, 50 | align: PropTypes.string, 51 | line: PropTypes.bool 52 | } 53 | 54 | export default Typography 55 | -------------------------------------------------------------------------------- /src/assets/cubes-sidebar-min.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/containers/Applications/Applications.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .Apps { 5 | flex: 1; 6 | margin: 48px 48px 48px; 7 | 8 | @media (max-width: $mobile-width) { 9 | margin: 16px; 10 | width: unset; 11 | } 12 | 13 | .topSection { 14 | display: flex; 15 | // height: 50px; 16 | justify-content: space-between; 17 | } 18 | 19 | .cards { 20 | display: grid; 21 | grid-gap: 28px; 22 | @media (min-width: 1200px) { 23 | margin-right: 365px; 24 | } 25 | 26 | @media (min-width: 1400px) { 27 | grid-template-columns: repeat(2, 1fr); 28 | } 29 | } 30 | 31 | .Card { 32 | .trashIcon { 33 | cursor: pointer; 34 | color: $color-grey-300; 35 | &:hover { 36 | color: $color-blue-grey; 37 | } 38 | } 39 | } 40 | 41 | .recommendedApps { 42 | display: flex; 43 | flex-direction: column; 44 | width: 180px; 45 | align-items: center; 46 | 47 | .icon { 48 | cursor: pointer; 49 | @media (max-width: $mobile-width) { 50 | width: 100px; 51 | height: 100px; 52 | } 53 | } 54 | } 55 | } 56 | 57 | .Link { 58 | display: flex; 59 | align-items: center; 60 | :first-child { 61 | margin-right: 8px; 62 | } 63 | 64 | a { 65 | color: inherit; 66 | text-decoration: inherit; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/assets/windows-masq.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/containers/Navbar/NavbarMax.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .Sidebar { 4 | height: 100vh; 5 | width: 252px; 6 | min-width: 252px; 7 | background-color: $color-dark-blue-100; 8 | 9 | .header { 10 | height: 190px; 11 | display: flex; 12 | width: 100%; 13 | flex-direction: column; 14 | justify-content: center; 15 | align-items: center; 16 | background-color: $color-dark-blue-200; 17 | } 18 | 19 | .nav { 20 | padding-top: 42px; 21 | display: flex; 22 | flex-direction: column; 23 | >:not(:last-child) { 24 | margin-bottom: 40px; 25 | } 26 | } 27 | 28 | .navElement { 29 | display: flex; 30 | margin-left: 70px; 31 | align-items: center; 32 | cursor: pointer; 33 | opacity: 0.8; 34 | z-index: 1; 35 | &:hover { 36 | opacity: 1; 37 | } 38 | :first-child { 39 | margin-right: 12px; 40 | } 41 | text-decoration: none; 42 | color: white; 43 | } 44 | 45 | .active * { 46 | color: $color-blue; 47 | } 48 | 49 | .active.navElement { 50 | opacity: 1; 51 | &::before { 52 | position: absolute; 53 | content: ''; 54 | left: 44px; 55 | height: 21px; 56 | width: 2px; 57 | border-radius: 1px; 58 | background-color: $color-blue; 59 | } 60 | } 61 | 62 | .logout { 63 | z-index: 1; 64 | position: absolute; 65 | bottom: 64px; 66 | } 67 | 68 | .cubes { 69 | position: absolute; 70 | bottom: 0; 71 | z-index: 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/containers/Profile/Profile.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .Profile { 5 | width: 100%; 6 | justify-content: space-between; 7 | margin: 48px 48px 16px 48px; 8 | // height: 100%; 9 | 10 | @media (max-width: $mobile-width) { 11 | margin: 16px; 12 | width: unset; 13 | } 14 | 15 | .titleContainer { 16 | margin-bottom: 32px; 17 | } 18 | 19 | .page { 20 | display: flex; 21 | height: 100%; 22 | justify-content: space-between; 23 | 24 | @media (max-width: 700px) { 25 | flex-direction: column; 26 | align-items: center; 27 | justify-content: unset; 28 | } 29 | 30 | .content { 31 | display: flex; 32 | flex-direction: row; 33 | 34 | .inputs { 35 | height: 264px; 36 | margin-right: 32px; 37 | margin-left: 64px; 38 | display: flex; 39 | flex-direction: column; 40 | justify-content: space-between; 41 | } 42 | 43 | @media (max-width: 1024px) { 44 | flex-direction: column; 45 | align-items: center; 46 | 47 | .inputs { 48 | margin-top: 24px; 49 | margin-left: 0; 50 | margin-right: 0; 51 | margin-bottom: 32px; 52 | } 53 | } 54 | } 55 | 56 | .rightSection { 57 | .deleteButton { 58 | cursor: pointer; 59 | .trashIcon { 60 | vertical-align: text-bottom; 61 | margin-right: 4px; 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | -------------------------------------------------------------------------------- /src/containers/Login/Login.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | 3 | .Login { 4 | background-color: $color-dark-blue-100; 5 | position: relative; 6 | min-height: 100vh; 7 | display: flex; 8 | justify-content: space-between; 9 | flex-direction: column; 10 | align-items: center; 11 | text-align: center; 12 | 13 | .content { 14 | width: 100%; 15 | } 16 | 17 | .Background { 18 | background-color: $color-dark-blue-100; 19 | flex-shrink: 0; 20 | } 21 | } 22 | 23 | .usersSelection { 24 | width: 100%; 25 | text-align: center; 26 | } 27 | 28 | .users { 29 | width: 100%; 30 | display: grid; 31 | grid-template-columns: repeat(auto-fit, minmax(120px, 120px)); 32 | grid-gap: 64px; 33 | text-align: center; 34 | justify-content: center; 35 | 36 | .PlusSquare { 37 | margin-right: auto; 38 | margin-left: auto; 39 | cursor: pointer; 40 | opacity: 0.5; 41 | &:hover { 42 | opacity: 1; 43 | } 44 | } 45 | 46 | .user { 47 | cursor: pointer; 48 | text-decoration: none; 49 | } 50 | } 51 | 52 | .userPassword { 53 | text-align: center; 54 | margin: auto; 55 | width: 320px; 56 | .passwordSection { 57 | display: flex; 58 | flex-direction: column; 59 | align-items: center; 60 | } 61 | } 62 | 63 | .goback { 64 | text-decoration: none; 65 | display: grid; 66 | color: white; 67 | grid-column-gap: 8px; 68 | align-items: center; 69 | justify-content: center; 70 | grid-template-columns: auto auto; 71 | margin-bottom: 16px; 72 | cursor: pointer; 73 | } 74 | -------------------------------------------------------------------------------- /src/modals/DeleteProfileDialog/DeleteProfileDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | import { AlertCircle } from 'react-feather' 4 | import { useTranslation } from 'react-i18next' 5 | import { Button, Modal, Typography, Space } from '../../components' 6 | 7 | import styles from './DeleteProfileDialog.module.scss' 8 | 9 | const ConfirmDialog = ({ username, onConfirm, onCancel, onClose }) => { 10 | const { t } = useTranslation() 11 | return ( 12 | 13 |
14 | {t('Deletion of the profile')} 15 | 16 | 17 | 18 | 19 | {`${t('You are at the point of deleting the profile ')} « ${username} ». 20 | ${t(' All your personal data of Masq will be lost.')}`} 21 | 22 | 23 |
24 | 25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | 32 | ConfirmDialog.propTypes = { 33 | username: PropTypes.string.isRequired, 34 | onConfirm: PropTypes.func.isRequired, 35 | onCancel: PropTypes.func.isRequired, 36 | onClose: PropTypes.func.isRequired 37 | } 38 | 39 | export default ConfirmDialog 40 | -------------------------------------------------------------------------------- /src/components/Button/Button.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/mixins.scss'; 2 | @import '../../styles/colors.scss'; 3 | @import '../../styles/variables.scss'; 4 | 5 | .Button { 6 | height: 50px; 7 | padding: 0; 8 | outline: none; 9 | border: none; 10 | cursor: pointer; 11 | transition: box-shadow 0.5s; 12 | 13 | @media (max-width: $mobile-width) { 14 | &:not(.secondary) { 15 | width: 320px !important; 16 | max-width: 100% !important; 17 | } 18 | } 19 | 20 | span { 21 | text-transform: uppercase; 22 | font-size: 12px; 23 | font-weight: 600; 24 | line-height: 2; 25 | letter-spacing: 0.5px; 26 | text-align: center; 27 | color: white; 28 | } 29 | } 30 | 31 | .secondary { 32 | opacity: 0.8; 33 | border: solid 1px $color-grey-300; 34 | background-color: $color-grey-100; 35 | 36 | span { 37 | color: $color-grey-300; 38 | } 39 | 40 | &:hover { 41 | border: solid 1px $color-blue-grey; 42 | span { 43 | color: $color-blue-grey; 44 | } 45 | } 46 | } 47 | 48 | .primary { 49 | background-color: $color-blue; 50 | &:hover { 51 | box-shadow: 0 5px 16px 0 rgba(69, 179, 248, 0.4); 52 | } 53 | } 54 | 55 | .success { 56 | background-color: $color-green; 57 | &:hover { 58 | box-shadow: 0 5px 16px 0 rgba(64, 174, 108, 0.4); 59 | } 60 | } 61 | 62 | .danger { 63 | background-color: $color-red; 64 | &:hover { 65 | box-shadow: 0 5px 16px 0 rgba(229, 59, 91, 0.4); 66 | } 67 | } 68 | 69 | .neutral { 70 | background-color: $color-blue-grey; 71 | &:hover { 72 | box-shadow: 0 5px 16px 0 rgba(92, 111, 132, 0.4) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/components/Card/Card.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import Image from './Image' 5 | import { Typography } from '..' 6 | 7 | import styles from './Card.module.scss' 8 | import Space from '../Space' 9 | 10 | const Description = ({ description }) => ( 11 |
12 | {description} 13 |
14 | ) 15 | 16 | Description.propTypes = { 17 | description: PropTypes.string.isRequired 18 | } 19 | 20 | const Card = ({ minHeight, image, title, actions, description, footer, color, width }) => ( 21 |
22 | {image && } 23 |
24 | {(actions || color) && ( 25 |
26 |
27 | {actions} 28 |
29 | )} 30 | {!color && !actions && } 31 | {title && {title}} 32 | {description && } 33 | {footer} 34 |
35 |
36 | ) 37 | 38 | Card.defaultProps = { 39 | minHeight: 50 40 | } 41 | 42 | Card.propTypes = { 43 | width: PropTypes.number, 44 | minHeight: PropTypes.number, 45 | image: PropTypes.string, 46 | title: PropTypes.string, 47 | actions: PropTypes.object, 48 | description: PropTypes.string, 49 | footer: PropTypes.object, 50 | color: PropTypes.string 51 | } 52 | 53 | export default Card 54 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Masq-app 2 | 3 | [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 4 | 5 | `AKASHA.id` is the Web application that offers a user-centric identity management. This application was based on an intitial implementation called [Masq](https://github.com/QwantResearch/masq-app). 6 | 7 | ## Development 8 | 9 | ```bash 10 | npm install 11 | npm start 12 | ``` 13 | 14 | ## Deploy to Github pages 15 | 16 | ```bash 17 | npm install 18 | npm run deploy 19 | ``` 20 | 21 | ## Production 22 | 23 | To deploy in production: 24 | 25 | - Edit `.env.production` to point to the correct signalhubws server(s) 26 | - Set the `homepage` field in `package.json` to point to the url where masq-app will be accessible, then run: 27 | 28 | ```bash 29 | npm install 30 | npm run build 31 | ``` 32 | 33 | Then deploy the static files in the `build/` folder. 34 | 35 | ## Environment variables 36 | 37 | For now we have to specify an env variable with the urls of the signalhubs (signalling server) that will be used. 38 | ` 39 | REACT_APP_SIGNALHUB_URLS 40 | ` 41 | 42 | # Remote webrtc 43 | 44 | For Masq app to display a button showing the remote connection link and QR code and possibly use a STUN or TURN server, you will need to specify the following env variable. 45 | 46 | ` 47 | REACT_APP_REMOTE_WEBRTC 48 | ` 49 | 50 | To be able to connect between different devices, a STUN and TURN server might be needed, ou can set stun and turn servers with the following env variables. Multiple URLs can be specified as a single comma separated string. 51 | 52 | ` 53 | REACT_APP_STUN_URLS 54 | REACT_APP_TURN_URLS 55 | ` 56 | -------------------------------------------------------------------------------- /src/components/TextField/TextField.module.scss: -------------------------------------------------------------------------------- 1 | @import '../../styles/colors.scss'; 2 | @import '../../styles/variables.scss'; 3 | 4 | .TextField { 5 | position: relative; 6 | width: 302px; 7 | display: flex; 8 | flex-direction: column-reverse; 9 | 10 | @media (max-width: $mobile-width) { 11 | width: 320px; 12 | } 13 | 14 | label { 15 | font-size: 12px; 16 | line-height: 1.25; 17 | &:first-letter { 18 | text-transform: capitalize; 19 | } 20 | } 21 | 22 | input { 23 | height: 29px; 24 | border-radius: 3px; 25 | outline: none; 26 | padding-left: 16px; 27 | padding-right: 16px; 28 | } 29 | 30 | .button { 31 | position: absolute; 32 | right: 14px; 33 | display: flex; 34 | flex-direction: column; 35 | justify-content: center; 36 | height: 32px; 37 | cursor: pointer; 38 | color: $color-dark-blue-100; 39 | } 40 | } 41 | 42 | .large { 43 | input { 44 | height: 46px; 45 | font-size: 20px; 46 | color: $color-dark-blue-100; 47 | } 48 | 49 | .button { 50 | height: 50px; 51 | } 52 | } 53 | 54 | .default { 55 | label { 56 | color: $color-blue-grey; 57 | } 58 | 59 | input { 60 | border: solid 1px $color-grey-300; 61 | &:focus { 62 | border: solid 1px $color-blue; 63 | + label { 64 | color: $color-blue; 65 | } 66 | } 67 | } 68 | } 69 | 70 | .error { 71 | label { 72 | color: $color-red; 73 | } 74 | 75 | input { 76 | border: solid 1px $color-red; 77 | &:focus { 78 | border: solid 1px $color-red; 79 | } 80 | } 81 | } 82 | 83 | .password { 84 | input { 85 | padding-right: 48px; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/modals/DeleteAppDialog/DeleteAppDialog.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import PropTypes from 'prop-types' 3 | 4 | import { Button, Modal, Typography, Space, Card } from '../../components' 5 | import { useTranslation } from 'react-i18next' 6 | import styles from './DeleteAppDialog.module.scss' 7 | 8 | const ConfirmDialog = ({ app, onConfirm, onCancel, onClose }) => { 9 | const { t } = useTranslation() 10 | return ( 11 | 12 |
13 | {`${t('Deletion of the application')} ${app.appId}`} 14 | 15 | 16 | {`${t('You are at the point of deleting the application ')} "${app.appId}" ${t('from Masq with all the associated data.')} 17 | ${t(' All the data will be lost.')}`} 18 | 19 | 20 | 24 | 25 |
26 | 27 | 28 |
29 |
30 |
31 | ) 32 | } 33 | 34 | ConfirmDialog.propTypes = { 35 | app: PropTypes.object.isRequired, 36 | onConfirm: PropTypes.func.isRequired, 37 | onCancel: PropTypes.func.isRequired, 38 | onClose: PropTypes.func.isRequired 39 | } 40 | 41 | export default ConfirmDialog 42 | -------------------------------------------------------------------------------- /src/components/Modal/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import PropTypes from 'prop-types' 3 | import MediaQuery from 'react-responsive' 4 | 5 | import { X } from 'react-feather' 6 | 7 | import styles from './Modal.module.scss' 8 | 9 | const Modal = ({ onClose, width, padding, children }) => ( 10 |
11 |
12 |
19 | {onClose && } 20 | {children} 21 |
22 |
23 | ) 24 | 25 | const ResponsiveModal = ({ onClose, width, children, padding }) => { 26 | useEffect(() => { 27 | window.document.body.style.overflow = 'hidden' 28 | 29 | // cleanup 30 | return () => { 31 | window.document.body.style.overflow = 'unset' 32 | } 33 | }, []) 34 | 35 | return ( 36 |
37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | ) 45 | } 46 | 47 | Modal.propTypes = 48 | ResponsiveModal.propTypes = { 49 | onClose: PropTypes.func, 50 | children: PropTypes.object, 51 | width: PropTypes.oneOfType([ 52 | PropTypes.string, 53 | PropTypes.number 54 | ]), 55 | padding: PropTypes.oneOfType([ 56 | PropTypes.string, 57 | PropTypes.number 58 | ]) 59 | } 60 | 61 | export default ResponsiveModal 62 | -------------------------------------------------------------------------------- /stories/Card.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { storiesOf } from '@storybook/react' 3 | 4 | import { Card, Switch, Typography } from '../src/components' 5 | 6 | const Actions = () => ( 7 |
8 | 9 |
10 |
11 | ) 12 | 13 | const Footer = () => ( 14 | Footer 15 | ) 16 | 17 | Actions.displayName = 'Actions' 18 | Footer.displayName = 'Footer' 19 | 20 | storiesOf('Card', module) 21 | .addParameters({ 22 | info: 'A Card can contain two children: actions displayed in the top right corner, and a footer.' 23 | }) 24 | .add('with title and description', () => ( 25 | 30 | )) 31 | .add('with image', () => ( 32 | } 38 | /> 39 | )) 40 | .add('with actions and a footer', () => ( 41 | } 47 | footer={