Added device previewer and zoom

This commit is contained in:
Manoj Vivek 2019-08-11 12:58:08 +05:30
parent 2e6c1dedd4
commit c081be2c6d
26 changed files with 477 additions and 22 deletions

View file

@ -2,6 +2,7 @@
import type {GetState, Dispatch} from '../reducers/types';
export const NEW_ADDRESS = 'NEW_ADDRESS';
export const NEW_ZOOM_LEVEL = 'NEW_ZOOM_LEVEL';
export function newAddress(address) {
return {
@ -10,8 +11,14 @@ export function newAddress(address) {
};
}
export function newZoomLevel(zoomLevel) {
return {
type: NEW_ZOOM_LEVEL,
zoomLevel,
};
}
export function onAddressChange(newURL) {
console.log('onAddressChange', newURL);
return (dispatch: Dispatch, getState: GetState) => {
const {address} = getState();
@ -24,3 +31,15 @@ export function onAddressChange(newURL) {
dispatch(newAddress(newURL));
};
}
export function onZoomChange(newLevel) {
return (dispatch: Dispatch, getState: GetState) => {
const {zoomLevel} = getState();
if (newLevel === zoomLevel) {
return;
}
dispatch(newZoomLevel(newLevel));
};
}

View file

@ -1,8 +1,8 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello Electron React!</title>
<meta charset="utf-8" />
<title>Responsively</title>
<script>
(function() {
if (!process.env.HOT) {
@ -12,7 +12,7 @@
// HACK: Writing the script path should be done with webpack
document.getElementsByTagName('head')[0].appendChild(link);
}
}());
})();
</script>
</head>
<body>
@ -30,7 +30,7 @@
// Dynamically insert the bundled app script in the renderer process
const port = process.env.PORT || 1212;
scripts.push(
(process.env.HOT)
process.env.HOT
? 'http://localhost:' + port + '/dist/renderer.dev.js'
: './dist/renderer.prod.js'
);

View file

@ -36,7 +36,7 @@ class AddressBar extends React.Component<Props> {
onChange={e => this.setState({userTypedAddress: e.target.value})}
/>
<button className={styles.goButton} onClick={this._onChange}>
<GoArrowIcon height={30} color="white" />
<GoArrowIcon height={20} color="white" />
</button>
</div>
);

View file

@ -6,7 +6,6 @@
font-size: 16px;
height: 20px;
width: 200px;
margin: 0 20px;
padding: 5px 10px;
border-radius: 20px;
background: unset;
@ -21,4 +20,3 @@
cursor: pointer;
outline: none;
}

View file

@ -0,0 +1,29 @@
// @flow
import React, {Component} from 'react';
import Renderer from '../Renderer';
import cx from 'classnames';
import styles from './style.module.css';
class DevicesPreviewer extends Component {
render() {
console.log('DevicesPreviewer props', this.props);
const {
browser: {devices, address, zoomLevel},
} = this.props;
return (
<div className={cx(styles.container)}>
{devices.map(device => (
<Renderer
key={device.id}
device={device}
src={address}
zoomLevel={zoomLevel}
/>
))}
</div>
);
}
}
export default DevicesPreviewer;

View file

@ -0,0 +1,48 @@
// @flow
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import DevicesPreviewer from './';
const testSrc = 'https://testUrl.com';
const testDevice1 = {
id: 1,
name: 'testDevice1',
width: 100,
height: 100,
};
const testDevice2 = {
id: 2,
name: 'testDevice2',
width: 200,
height: 200,
};
const testDevices = [testDevice1, testDevice2];
describe('<DevicesPreviewer />', () => {
it('Renders the Renderer for all passed array of devices', () => {
const wrapper = shallow(
<DevicesPreviewer devices={testDevices} url={testSrc} />
);
expect(wrapper.find('Renderer')).to.have.lengthOf(testDevices.length);
});
it('Renders the Renderer for all devices passed with the given url', () => {
const wrapper = shallow(
<DevicesPreviewer devices={testDevices} url={testSrc} />
);
wrapper.find('Renderer').forEach(renderer => {
expect(renderer.prop('src')).to.equal(testSrc);
});
});
it('Renders the Renderer for all devices in the given order', () => {
const wrapper = shallow(
<DevicesPreviewer devices={testDevices} url={testSrc} />
);
wrapper.find('Renderer').forEach((renderer, idx) => {
expect(renderer.prop('device')).to.equal(testDevices[idx]);
});
});
});

View file

@ -0,0 +1,6 @@
.container {
display: flex;
}
.device {
}

View file

@ -0,0 +1,23 @@
// @flow
import React, {Component} from 'react';
import ZoomContainer from '../../containers/ZoomContainer';
import AddressBar from '../../containers/AddressBar';
import Grid from '@material-ui/core/Grid';
import cx from 'classnames';
import styles from './style.module.css';
const Header = function(props) {
return (
<Grid container direction="row" justify="space-evenly" alignItems="center">
<Grid item>
<AddressBar />
</Grid>
<Grid item>
<ZoomContainer />
</Grid>
</Grid>
);
};
export default Header;

View file

@ -0,0 +1,14 @@
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import Header from './';
describe('<Header />', () => {
it('renders a h1 and BrowserZoom components', () => {
const wrapper = shallow(<Header />);
expect(wrapper.find('h1')).to.have.lengthOf(1);
expect(wrapper.find('ZoomInput')).to.have.lengthOf(1);
})
});

View file

@ -0,0 +1,5 @@
.container {
display: flex;
padding: 2rem;
justify-content: space-around;
}

View file

@ -0,0 +1,38 @@
// @flow
import React, {Component} from 'react';
import cx from 'classnames';
import styles from './style.module.css';
class Renderer extends Component {
render() {
console.log('Renderer this.props', this.props);
return (
<div className={cx(styles.container)}>
<h2>{this.props.device.name}</h2>
<div
className={cx(styles.deviceWrapper)}
style={{
width: this.props.device.width * this.props.zoomLevel,
heigth: this.props.device.height * this.props.zoomLevel,
}}
>
<webview
className={cx(styles.device)}
title={this.props.device.name}
src={this.props.src}
width={this.props.device.width}
height={this.props.device.height}
style={{
width: this.props.device.width,
height: this.props.device.height,
transform: `scale(${this.props.zoomLevel})`,
}}
/>
</div>
</div>
);
}
}
export default Renderer;

View file

@ -0,0 +1,46 @@
// @flow
import React from 'react';
import {shallow} from 'enzyme';
import {expect} from 'chai';
import Renderer from './';
const testSrc = 'https://testUrl.com';
const testDevice1 = {
name: 'testDevice1',
width: 100,
height: 100,
};
describe('<Renderer />', () => {
it('Renders the header and the iframe', () => {
const wrapper = shallow(<Renderer src={testSrc} device={testDevice1} />);
expect(wrapper.find('iframe')).to.have.lengthOf(1);
expect(wrapper.find('h2')).to.have.lengthOf(1);
});
it('Renders the header with the device name', () => {
const wrapper = shallow(<Renderer src={testSrc} device={testDevice1} />);
expect(wrapper.find('h2').text()).to.equal(testDevice1.name);
});
it('Renders the iframe with the given device dimensions', () => {
const wrapper = shallow(<Renderer src={testSrc} device={testDevice1} />);
expect(wrapper.find('iframe').prop('width')).to.equal(testDevice1.width);
expect(wrapper.find('iframe').prop('height')).to.equal(testDevice1.height);
});
it('Renders the iframe with the given url', () => {
const wrapper = shallow(<Renderer src={testSrc} device={testDevice1} />);
expect(wrapper.find('iframe').prop('src')).to.equal(testSrc);
});
/*it('Calls the callback with a number value', () => {
const onChange = sinon.spy();
const wrapper = mount(<BrowserZoom onChange={onChange} />);
wrapper.find('.MuiSlider-thumb').simulate('mousedown');
wrapper.find('.MuiSlider-thumb').simulate('mouseup');
console.log('spy.args', onChange.args);
assert(onChange.calledWith(100));
});*/
});

View file

@ -0,0 +1,10 @@
.container {
margin: 10px;
}
.deviceWrapper {
}
.device {
transform-origin: top left;
}

View file

@ -0,0 +1,58 @@
// @flow
import React, {Component} from 'react';
import Slider from '@material-ui/core/Slider';
import ZoomInIcon from '@material-ui/icons/ZoomIn';
import ZoomOutIcon from '@material-ui/icons/ZoomOut';
import Grid from '@material-ui/core/Grid';
import styles from './styles.module.css';
import './otherStyles.css';
const marks = [
{
value: 25,
label: '25%',
},
{
value: 50,
label: '50%',
},
{
value: 100,
label: '100%',
},
{
value: 200,
label: '200%',
},
];
class BrowserZoom extends Component {
render() {
return (
<div className={styles.zoomSlider}>
<Grid container spacing={1}>
<Grid item>
<ZoomOutIcon />
</Grid>
<Grid item xs>
<Slider
defaultValue={this.props.value}
valueLabelDisplay="auto"
min={10}
max={100}
onChange={(_, value) =>
this.props.onChange && this.props.onChange(value)
}
/>
</Grid>
<Grid item>
<ZoomInIcon />
</Grid>
</Grid>
</div>
);
}
}
export default BrowserZoom;

View file

@ -0,0 +1,32 @@
// @flow
import React from 'react';
import {shallow, mount} from 'enzyme';
import {expect, assert} from 'chai';
import sinon from 'sinon';
import BrowserZoom from './';
import Slider from '@material-ui/core/Slider';
describe('<BrowserZoom />', () => {
it('Renders label and the slider component ', () => {
const wrapper = shallow(<BrowserZoom />);
expect(wrapper.find(Slider)).to.have.lengthOf(1);
});
it('Calls the callback on slider change', () => {
const onChange = sinon.spy();
const wrapper = mount(<BrowserZoom onChange={onChange} />);
wrapper.find('.MuiSlider-thumb').simulate('mousedown');
wrapper.find('.MuiSlider-thumb').simulate('mouseup');
expect(onChange).to.have.property('callCount', 1);
});
/*it('Calls the callback with a number value', () => {
const onChange = sinon.spy();
const wrapper = mount(<BrowserZoom onChange={onChange} />);
wrapper.find('.MuiSlider-thumb').simulate('mousedown');
wrapper.find('.MuiSlider-thumb').simulate('mouseup');
console.log('spy.args', onChange.args);
assert(onChange.calledWith(100));
});*/
});

View file

@ -0,0 +1,4 @@
.MuiSlider-markLabelActive,
.MuiSlider-markLabel {
color: white !important;
}

View file

@ -0,0 +1,8 @@
.zoomSlider {
width: 150px;
}
.label {
font-size: 1rem;
margin-right: 1rem;
}

View file

@ -0,0 +1,15 @@
// @flow
export type Device = {
id: number,
width: number,
height: number,
name: string,
};
export default [
{id: 1, width: 320, height: 568, name: 'iPhone SE'},
{id: 2, width: 375, height: 812, name: 'iPhone X'},
{id: 3, width: 768, height: 1024, name: 'iPad'},
{id: 4, width: 1024, height: 1366, name: 'iPad Pro'},
{id: 5, width: 1440, height: 900, name: 'Laptop'},
];

View file

@ -0,0 +1,4 @@
export default {
HOME: '/',
COUNTER: '/counter',
};

View file

@ -1,4 +0,0 @@
{
"HOME": "/",
"COUNTER": "/counter"
}

View file

@ -0,0 +1,31 @@
// @flow
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import AddressInput from '../../components/AddressInput';
import * as BrowserActions from '../../actions/browser';
const AddressBar = function(props) {
return (
<AddressInput
address={props.browser.address}
onChange={props.onAddressChange}
/>
);
};
function mapStateToProps(state) {
return {
browser: state.browser,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(BrowserActions, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(AddressBar);

View file

@ -1,8 +1,9 @@
// @flow
import React from 'react';
import React, {Fragment} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import AddressBar from '../../components/AddressBar';
import Header from '../../components/Header';
import DevicePreviewerContainer from '../DevicePreviewerContainer';
import * as BrowserActions from '../../actions/browser';
type Props = {};
@ -13,9 +14,14 @@ class Browser extends React.Component<Props> {
render() {
console.log('Props', this.props);
return (
<div>
<AddressBar onChange={this.props.onAddressChange} />
</div>
<Fragment>
<div>
<Header />
</div>
<div>
<DevicePreviewerContainer />
</div>
</Fragment>
);
}
}

View file

@ -0,0 +1,21 @@
// @flow
import React from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import DevicesPreviewer from '../../components/DevicesPreviewer';
import * as BrowserActions from '../../actions/browser';
function mapStateToProps(state) {
return {
browser: state.browser,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(BrowserActions, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(DevicesPreviewer);

View file

@ -0,0 +1,31 @@
// @flow
import React from 'react';
import {connect} from 'react-redux';
import {bindActionCreators} from 'redux';
import ZoomInput from '../../components/ZoomInput';
import * as BrowserActions from '../../actions/browser';
const ZoomController = props => {
return (
<ZoomInput
value={props.browser.zoomLevel * 100}
onChange={val => props.onZoomChange(val / 100)}
/>
);
};
function mapStateToProps(state) {
return {
browser: state.browser,
};
}
function mapDispatchToProps(dispatch) {
return bindActionCreators(BrowserActions, dispatch);
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(ZoomController);

View file

@ -1,11 +1,24 @@
// @flow
import {NEW_ADDRESS} from '../actions/browser';
import {NEW_ADDRESS, NEW_ZOOM_LEVEL} from '../actions/browser';
import type {Action} from './types';
import devices from '../constants/devices';
import type {Device} from '../constants/devices';
export default function counter(state: Object = {}, action: Action) {
export type BrowserStateType = {
devices: Array<Device>,
address: string,
zoomLevel: number,
};
export default function counter(
state: BrowserStateType = {devices, zoomLevel: 0.75},
action: Action
) {
switch (action.type) {
case NEW_ADDRESS:
return {...state, address: action.address};
case NEW_ZOOM_LEVEL:
return {...state, zoomLevel: action.zoomLevel};
default:
return state;
}

View file

@ -1,11 +1,11 @@
import type { Dispatch as ReduxDispatch, Store as ReduxStore } from 'redux';
import type {Dispatch as ReduxDispatch, Store as ReduxStore} from 'redux';
export type counterStateType = {
+counter: number
+counter: number,
};
export type Action = {
+type: string
+type: string,
};
export type GetState = () => counterStateType;