├── src ├── setupTests.js ├── index.js ├── index.css ├── Hand.js ├── __tests__ │ ├── Form.test.js │ ├── App.test.js │ ├── component │ │ ├── Hand.test.js │ │ ├── __snapshots__ │ │ │ ├── Hand.test.js.snap │ │ │ └── AnalogClock.test.js.snap │ │ ├── Util.test.js │ │ ├── AnalogClock.test.js │ │ ├── AnalogClock.unit.test.js │ │ └── ClockComponents.test.js │ ├── App.unit.test.js │ ├── __snapshots__ │ │ ├── App.test.js.snap │ │ └── Form.test.js.snap │ └── Form.unit.test.js ├── Util.js ├── App.js ├── AnalogClock.js ├── ClockComponents.js └── Form.js ├── public ├── manifest.json └── index.html ├── .gitignore ├── LICENSE ├── .circleci └── config.yml ├── package.json └── README.md /src/setupTests.js: -------------------------------------------------------------------------------- 1 | import { configure } from "enzyme"; 2 | import Adapter from "enzyme-adapter-react-16"; 3 | 4 | configure({ adapter: new Adapter() }); 5 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "AnalogClock", 3 | "name": "Analog Clock using React", 4 | "start_url": ".", 5 | "display": "standalone" 6 | } 7 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | 6 | ReactDOM.render(, document.getElementById('root')); 7 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /src/Hand.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { ClockHand, ClockHandContainer } from './ClockComponents'; 3 | import Util from './Util'; 4 | 5 | class Hand extends Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {}; 10 | } 11 | 12 | render() { 13 | return ( 14 | 15 | 17 | 18 | ) 19 | } 20 | 21 | } 22 | 23 | export default Hand; 24 | -------------------------------------------------------------------------------- /src/__tests__/Form.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Form from '../Form' 3 | import { shallow } from "enzyme"; 4 | 5 | describe('Form component', () => { 6 | 7 | it('matches snapshot', () => { 8 | let options = { 9 | useCustomTime: false, 10 | width: "300px", 11 | border: true, 12 | borderColor: "#2e2e2e", 13 | baseColor: "#17a2b8", 14 | centerColor: "#459cff", 15 | centerBorderColor: "#fff", 16 | handColors: { 17 | second: "#d81c7a", 18 | minute: "#fff", 19 | hour: "#fff" 20 | }, 21 | } 22 | const wrapper = shallow(
); 23 | expect(wrapper).toMatchSnapshot() 24 | }); 25 | 26 | }) -------------------------------------------------------------------------------- /src/Util.js: -------------------------------------------------------------------------------- 1 | class Util { 2 | static getHandAngle(props) { 3 | let handType = props.type; 4 | let handAngle = 0; 5 | switch (handType) { 6 | case 'second': handAngle = (270 + (props.seconds * 6)); 7 | break; 8 | case 'minute': handAngle = (270 + (props.minutes * 6)); 9 | break; 10 | case 'hour': handAngle = (270 + (props.hours * 30) + ((props.minutes / 60) * 30)); 11 | break; 12 | default: handAngle = 0; 13 | } 14 | return handAngle; 15 | } 16 | 17 | static getHourIn12HrFormat(hour) { 18 | if (hour) { 19 | if (hour > 12) { 20 | hour -= 12; 21 | } 22 | } else { 23 | hour = 0; 24 | } 25 | return hour; 26 | } 27 | } 28 | 29 | export default Util; -------------------------------------------------------------------------------- /src/__tests__/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import App from '../App' 3 | import { shallow } from "enzyme"; 4 | 5 | describe('App component', () => { 6 | 7 | it('matches snapshot', () => { 8 | const wrapper = shallow(); 9 | expect(wrapper).toMatchSnapshot() 10 | }); 11 | 12 | it('matches snapshot when options are changed', () => { 13 | const wrapper = shallow(); 14 | let options = { 15 | useCustomTime: true, 16 | seconds: 10, 17 | minutes: 10, 18 | hours: 10, 19 | baseColor: "red", 20 | border: false, 21 | borderColor: "#fff", 22 | centerBorderColor: "black", 23 | centerColor: "blue" 24 | } 25 | wrapper.setState({ 'options': { ...wrapper.state().options, ...options } }); 26 | expect(wrapper).toMatchSnapshot() 27 | }); 28 | }) -------------------------------------------------------------------------------- /src/__tests__/component/Hand.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "jest-styled-components"; 3 | import { shallow } from "enzyme"; 4 | import "../../setupTests"; 5 | 6 | import Hand from "../../Hand"; 7 | 8 | describe("Hand component", () => { 9 | it("matches second Hand snapshot", () => { 10 | const wrapper = shallow( 11 | 12 | ); 13 | expect(wrapper).toMatchSnapshot(); 14 | }); 15 | 16 | it("matches minute Hand snapshot", () => { 17 | const wrapper = shallow( 18 | 19 | ); 20 | expect(wrapper).toMatchSnapshot(); 21 | }); 22 | 23 | it("matches hour Hand snapshot", () => { 24 | const wrapper = shallow(); 25 | expect(wrapper).toMatchSnapshot(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/__tests__/component/__snapshots__/Hand.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Hand component matches hour Hand snapshot 1`] = ` 4 | 7 | 15 | 16 | `; 17 | 18 | exports[`Hand component matches minute Hand snapshot 1`] = ` 19 | 22 | 30 | 31 | `; 32 | 33 | exports[`Hand component matches second Hand snapshot 1`] = ` 34 | 37 | 45 | 46 | `; 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Vishnu Ramana 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | orbs: 3 | coveralls: coveralls/coveralls@1.0.6 4 | jobs: 5 | build: 6 | docker: 7 | - image: cimg/node:17.0.1 8 | working_directory: ~/analogclock 9 | steps: 10 | - checkout 11 | - run: npm install 12 | - restore_cache: 13 | key: dependency-cache-{{ checksum "package.json" }} 14 | - run: 15 | name: install-npm-wee 16 | command: npm install 17 | - save_cache: 18 | key: dependency-cache-{{ checksum "package.json" }} 19 | paths: 20 | - ./node_modules 21 | - run: 22 | name: test 23 | command: npm run test 24 | - run: 25 | name: code-coverage 26 | command: npm run coverage 27 | - coveralls/upload 28 | - store_artifacts: 29 | path: test-results 30 | prefix: tests 31 | - store_artifacts: 32 | path: coverage 33 | prefix: coverage 34 | - store_test_results: 35 | path: test-results 36 | notify: 37 | webhooks: 38 | - url: https://coveralls.io/webhook?repo_token=${process.env.COVERALLS_REPO_TOKEN} 39 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "analog-clock-react", 3 | "version": "1.2.2", 4 | "devDependencies": { 5 | "enzyme": "^3.11.0", 6 | "enzyme-adapter-react-16": "^1.15.6", 7 | "enzyme-to-json": "^3.6.2", 8 | "gh-pages": "^2.0.1", 9 | "jest-styled-components": "^6.3.4", 10 | "react-scripts": ">=2.1.3" 11 | }, 12 | "dependencies": { 13 | "react": ">=16.7.0", 14 | "react-dom": ">=16.7.0", 15 | "styled-components": ">=4.1.3" 16 | }, 17 | "scripts": { 18 | "predeploy": "yarn build", 19 | "deploy": "gh-pages -d build", 20 | "start": "react-scripts start", 21 | "build": "react-scripts build", 22 | "test": "react-scripts test", 23 | "coverage": "react-scripts test --coverage", 24 | "eject": "react-scripts eject" 25 | }, 26 | "eslintConfig": { 27 | "extends": "react-app" 28 | }, 29 | "jest": { 30 | "snapshotSerializers": [ 31 | "enzyme-to-json/serializer" 32 | ], 33 | "coverageThreshold": { 34 | "global": { 35 | "branches": 90, 36 | "functions": 90, 37 | "lines": 90, 38 | "statements": -10 39 | } 40 | }, 41 | "collectCoverageFrom": [ 42 | "src/**/*.{js}", 43 | "!src/index.js" 44 | ] 45 | }, 46 | "browserslist": [ 47 | ">0.2%", 48 | "not dead", 49 | "not ie <= 11", 50 | "not op_mini all" 51 | ], 52 | "homepage": "https://vishnuramana.github.io/analogclock" 53 | } 54 | -------------------------------------------------------------------------------- /src/__tests__/component/Util.test.js: -------------------------------------------------------------------------------- 1 | import Util from "../../Util"; 2 | 3 | describe("Util tests", () => { 4 | it("returns hand angle as 450 when 30 seconds is passed", () => { 5 | expect(Util.getHandAngle({ type: "second", seconds: "30" })).toBe(450); 6 | }); 7 | 8 | it("returns hand angle as 450 when 30 minutes is passed", () => { 9 | expect(Util.getHandAngle({ type: "minute", minutes: "30" })).toBe(450); 10 | }); 11 | 12 | it("returns hand angle as 450 when 6 hours is passed", () => { 13 | expect(Util.getHandAngle({ type: "hour", hours: "6", minutes: "0" })).toBe(450); 14 | }); 15 | 16 | it("returns hand angle as 450 when 6 hours is passed", () => { 17 | expect(Util.getHandAngle({ type: "hour", hours: "6", minutes: "31" })).toBe(465.5); 18 | }); 19 | 20 | it("returns hand angle as 0 when type is not passed", () => { 21 | expect( 22 | Util.getHandAngle({ hours: "6", minutes: "30", seconds: "30" }) 23 | ).toBe(0); 24 | }); 25 | 26 | it("returns 0 when passed hour is invalid", () => { 27 | expect( 28 | Util.getHourIn12HrFormat(undefined) 29 | ).toBe(0); 30 | }); 31 | 32 | it("returns passed hour when time is less than 12 hours", () => { 33 | expect( 34 | Util.getHourIn12HrFormat(10) 35 | ).toBe(10); 36 | }); 37 | 38 | it("returns passed hour-12 when time is greater than 12 hours", () => { 39 | expect( 40 | Util.getHourIn12HrFormat(13) 41 | ).toBe(1); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /src/__tests__/component/AnalogClock.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "jest-styled-components"; 3 | import { shallow } from "enzyme"; 4 | import "../../setupTests"; 5 | 6 | import AnalogClock from "../../AnalogClock.js"; 7 | 8 | describe("AnalogClock component", () => { 9 | const RealDate = Date; 10 | 11 | afterEach(() => { 12 | global.Date = RealDate; 13 | }); 14 | 15 | it("matches snapshot", () => { 16 | const constantDate = new Date("2021-10-10T10:10:10"); 17 | 18 | Date = class extends Date { 19 | constructor() { 20 | return constantDate; 21 | } 22 | }; 23 | 24 | let options = { 25 | width: "300px", 26 | border: true, 27 | borderColor: "#2e2e2e", 28 | baseColor: "#17a2b8", 29 | centerColor: "#459cff", 30 | centerBorderColor: "#fff", 31 | handColors: { 32 | second: "#d81c7a", 33 | minute: "#fff", 34 | hour: "#fff", 35 | }, 36 | }; 37 | const wrapper = shallow(); 38 | expect(wrapper).toMatchSnapshot(); 39 | }); 40 | 41 | it("matches snapshot when custom time is passed", () => { 42 | let options = { 43 | useCustomTime: true, 44 | seconds: 22, 45 | minutes: 22, 46 | hours: 22, 47 | width: "300px", 48 | border: true, 49 | borderColor: "#2e2e2e", 50 | baseColor: "#17a2b8", 51 | centerColor: "#459cff", 52 | centerBorderColor: "#fff", 53 | handColors: { 54 | second: "#d81c7a", 55 | minute: "#fff", 56 | hour: "#fff", 57 | }, 58 | }; 59 | const wrapper = shallow(); 60 | expect(wrapper).toMatchSnapshot(); 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Analog Clock built using React - Demo 8 | 10 | 11 | 12 | 13 |
14 |
15 |
16 | 19 |
20 |

Analog Clock built using React!

21 |

This is an analog clock completely built using React. It is customizable by passing an 22 | options JSON Object 23 | to the component.

24 |
25 |

You can try the different customization options by using the form below. Once you are done, you can copy 26 | the JSON object generated for your clock !

27 | View on Github 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | Made with 💙 37 |
38 |
39 | 40 | 41 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Form from './Form'; 3 | import AnalogClock from './AnalogClock.js' 4 | 5 | 6 | class App extends Component { 7 | 8 | interval = null; 9 | constructor(props) { 10 | super(props); 11 | this.state = { 12 | options: { 13 | useCustomTime: false, 14 | width: "300px", 15 | border: true, 16 | borderColor: "#2e2e2e", 17 | baseColor: "#17a2b8", 18 | centerColor: "#459cff", 19 | centerBorderColor: "#ffffff", 20 | handColors: { 21 | second: "#d81c7a", 22 | minute: "#ffffff", 23 | hour: "#ffffff" 24 | }, 25 | } 26 | }; 27 | } 28 | 29 | 30 | updateClock() { 31 | let ausTime = new Date().toLocaleString("en-US", { timeZone: "Australia/Brisbane" }); 32 | let date = new Date(ausTime); 33 | 34 | this.setState({ 35 | 'options': { 36 | ...this.state.options, 37 | seconds: date.getSeconds(), 38 | minutes: date.getMinutes(), 39 | hours: date.getHours() 40 | } 41 | }) 42 | } 43 | 44 | customizeClock(options) { 45 | let _options = options; 46 | if (_options.useCustomTime) { 47 | this.interval = setInterval(() => this.updateClock(), 1000); 48 | } else { 49 | clearInterval(this.interval); 50 | delete _options.seconds; 51 | delete _options.minutes; 52 | delete _options.hours; 53 | } 54 | this.setState({ options: { ..._options } }); 55 | } 56 | 57 | render() { 58 | return ( 59 |
60 |
61 | this.customizeClock(options)} /> 62 |
63 |
64 |
65 |

Options

66 | 67 |
68 |
69 |

Preview

70 | 71 | {this.state.options.useCustomTime ?

Timezone: Australia/Brisbane

: null} 72 |
73 |
74 |
75 | ); 76 | } 77 | 78 | componentWillUnmount() { 79 | clearInterval(this.interval); 80 | } 81 | } 82 | 83 | export default App; -------------------------------------------------------------------------------- /src/__tests__/App.unit.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "jest-styled-components"; 3 | import { mount } from "enzyme"; 4 | import "../setupTests"; 5 | 6 | import App from '../App.js'; 7 | 8 | describe("App component", () => { 9 | const renderSpy = jest.spyOn(App.prototype, "render"); 10 | const customizeClockSpy = jest.spyOn(App.prototype, "customizeClock"); 11 | const updateClockSpy = jest.spyOn(App.prototype, "updateClock"); 12 | 13 | beforeEach(() => { 14 | jest.useFakeTimers(); 15 | jest.spyOn(global, "setInterval"); 16 | jest.spyOn(global, "clearInterval"); 17 | }) 18 | 19 | afterEach(() => { 20 | renderSpy.mockClear(); 21 | customizeClockSpy.mockClear(); 22 | updateClockSpy.mockClear(); 23 | jest.useRealTimers(); 24 | }); 25 | 26 | it("mounts the component", () => { 27 | const wrapper = mount(); 28 | expect(renderSpy).toHaveBeenCalledTimes(1); 29 | }); 30 | 31 | it("updates the clock when customize button is clicked", () => { 32 | let options = { 33 | width: "500px", 34 | border: false, 35 | useCustomTime: true, 36 | seconds: 10, 37 | minutes: 10, 38 | hours: 10 39 | }; 40 | const wrapper = mount(); 41 | const formWrapper = wrapper.find('Form'); 42 | formWrapper.setState({ 'options': { ...wrapper.state().options, ...options } }); 43 | formWrapper.find('#build').simulate('click'); 44 | expect(customizeClockSpy).toHaveBeenCalledTimes(1); 45 | const updatedOptions = wrapper.find('#selected-options').text(); 46 | jest.advanceTimersByTime(1000); 47 | wrapper.unmount(); 48 | expect(updatedOptions.indexOf('"useCustomTime": true')).toBeTruthy(); 49 | expect(updateClockSpy).toHaveBeenCalledTimes(1); 50 | }); 51 | 52 | it("uses internal timer when useCustomTime is turned back to false", () => { 53 | let options = { 54 | useCustomTime: true, 55 | seconds: 10, 56 | minutes: 10, 57 | hours: 10 58 | }; 59 | const wrapper = mount(); 60 | const formWrapper = wrapper.find('Form'); 61 | formWrapper.setState({ 'options': { ...wrapper.state().options, ...options } }); 62 | formWrapper.find('#build').simulate('click'); 63 | expect(customizeClockSpy).toHaveBeenCalledTimes(1); 64 | jest.advanceTimersByTime(1000); 65 | expect(updateClockSpy).toHaveBeenCalledTimes(1); 66 | formWrapper.setState({ 'options': { ...wrapper.state().options, ...options, useCustomTime: false } }); 67 | formWrapper.find('#build').simulate('click'); 68 | jest.advanceTimersByTime(500); 69 | expect(updateClockSpy).toHaveBeenCalledTimes(1); 70 | }); 71 | 72 | }); 73 | -------------------------------------------------------------------------------- /src/__tests__/component/__snapshots__/AnalogClock.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`AnalogClock component matches snapshot 1`] = ` 4 | 7 | 11 | 14 | 18 | 29 | 40 | 52 | 53 | 54 | 55 | `; 56 | 57 | exports[`AnalogClock component matches snapshot when custom time is passed 1`] = ` 58 | 61 | 65 | 68 | 72 | 83 | 94 | 106 | 107 | 108 | 109 | `; 110 | -------------------------------------------------------------------------------- /src/AnalogClock.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import Util from './Util'; 3 | import { ClockContainer, ClockBaseBorder, ClockBase, ClockCenter } from './ClockComponents'; 4 | import Hand from './Hand.js'; 5 | 6 | class AnalogClock extends Component { 7 | 8 | constructor(props) { 9 | super(props); 10 | this.state = this.initClock(); 11 | } 12 | 13 | initClock() { 14 | const date = new Date(); 15 | return { 16 | seconds: date.getSeconds(), 17 | minutes: date.getMinutes(), 18 | hours: Util.getHourIn12HrFormat(date.getHours()) 19 | } 20 | } 21 | 22 | setupTime() { 23 | const date = new Date(); 24 | this.setState({ 25 | seconds: date.getSeconds(), 26 | minutes: date.getMinutes(), 27 | hours: Util.getHourIn12HrFormat(date.getHours()) 28 | }) 29 | } 30 | 31 | setupInterval() { 32 | this.interval = setInterval(() => this.setupTime(), 1000); 33 | } 34 | 35 | componentDidMount() { 36 | this.setupInterval(); 37 | } 38 | 39 | componentDidUpdate(prevProps) { 40 | if (prevProps.useCustomTime !== this.props.useCustomTime) { 41 | if (this.props.useCustomTime) { 42 | clearInterval(this.interval); 43 | this.setState({ 44 | seconds: undefined, 45 | minutes: undefined, 46 | hours: undefined 47 | }); 48 | } else { 49 | this.setupInterval(); 50 | } 51 | } 52 | } 53 | 54 | render() { 55 | const { width, border, borderColor, baseColor, centerColor, centerBorderColor, handColors } = this.props; 56 | const { seconds, minutes, hours } = this.props.useCustomTime ? this.props : this.state; 57 | return ( 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | ) 70 | } 71 | 72 | componentWillUnmount() { 73 | clearInterval(this.interval); 74 | } 75 | } 76 | 77 | export default AnalogClock; -------------------------------------------------------------------------------- /src/ClockComponents.js: -------------------------------------------------------------------------------- 1 | import styled, { css } from 'styled-components'; 2 | 3 | const ClockContainer = styled.div` 4 | height: ${props => props.width || "400px"}; 5 | width: ${props => props.width || "400px"}; 6 | `; 7 | 8 | const ClockBaseBorder = styled.div` 9 | position: relative; 10 | box-sizing: border-box; 11 | height: 100%; 12 | width: 100%; 13 | background-color: ${props => props.borderColor ? props.borderColor : "transparent"}; 14 | padding: ${props => props.border ? "5%" : "0"}; 15 | border-radius: 100%; 16 | `; 17 | const ClockBase = styled.div` 18 | position: relative; 19 | height: 100%; 20 | width: 100%; 21 | background-color: ${props => props.baseColor ? props.baseColor : "black"}; 22 | border-radius: 100%; 23 | `; 24 | const ClockCenter = styled.div` 25 | position: absolute; 26 | left: 50%; 27 | top: 50%; 28 | width: 12px; 29 | height: 12px; 30 | border: 2px solid ${props => props.centerBorderColor ? props.centerBorderColor : "#fff"};; 31 | background-color: ${props => props.centerColor ? props.centerColor : "#459cff"}; 32 | border-radius: 100%; 33 | margin-left: -6px; 34 | margin-top: -6px; 35 | z-index: 100; 36 | `; 37 | 38 | const ClockHand = styled.div` 39 | box-sizing: border-box; 40 | height: 1.5%; 41 | min-height: 2px; 42 | max-height: 6px; 43 | 44 | ${props => props.type === 'second' && css` 45 | width: 60%; 46 | margin-left: 40%; 47 | background-color: ${props => props.handColors && props.handColors.second ? props.handColors.second : "#d81c7a"}; 48 | `} 49 | ${props => props.type === 'minute' && css` 50 | width: 45%; 51 | margin-left: 45%; 52 | background-color: ${props => props.handColors && props.handColors.minute ? props.handColors.minute : "#fff"}; 53 | `} 54 | ${props => props.type === 'hour' && css` 55 | width: 35%; 56 | margin-left: 45%; 57 | background-color: ${props => props.handColors && props.handColors.hour ? props.handColors.hour : "#fff"}; 58 | `} 59 | `; 60 | 61 | const ClockHandContainer = styled.div` 62 | position: absolute; 63 | width: 100% 64 | height: 100%; 65 | display: flex; 66 | align-items: center; 67 | transform: rotate(${props => props.handAngle}deg); 68 | transition: ${props => props.handAngle > 270 && 'transform 250ms ease-in-out'}; 69 | `; 70 | 71 | export { ClockContainer, ClockBaseBorder, ClockBase, ClockCenter, ClockHand, ClockHandContainer }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Analog Clock using React 2 | 3 | [![CircleCI](https://circleci.com/gh/vishnuramana/analogclock/tree/dev.svg?style=shield)](https://circleci.com/gh/vishnuramana/analogclock/tree/dev) [![Coverage Status](https://coveralls.io/repos/github/vishnuramana/analogclock/badge.svg?branch=dev&kill_cache=1)](https://coveralls.io/github/vishnuramana/analogclock?branch=dev) [![npm](https://img.shields.io/npm/dw/analog-clock-react)](https://www.npmjs.com/package/analog-clock-react) ![npm](https://img.shields.io/npm/v/analog-clock-react) [![NPM](https://img.shields.io/npm/l/analog-clock-react)](https://github.com/vishnuramana/analogclock/blob/dev/LICENSE) 4 | 5 | This is a customizable analog clock completely built using React. It is customizable by passing an options JSON Object to the component which allows you to modify the clock colors. It also allows you to use multiple timezones. 6 | 7 | ![Clock Preview](https://i.imgur.com/uDyNlVl.png) 8 | 9 | ## Demo 10 | You can customize/view a live demo of the clock [here](http://vishnuramana.github.io/analogclock). 11 | 12 | ## Installation 13 | Install the package using 14 | 15 | npm install --save analog-clock-react 16 | 17 | ## Usage 18 | Import the `AnalogClock` component into your React Component like this 19 | 20 | import AnalogClock from 'analog-clock-react'; 21 | 22 | Then use the `AnalogClock` Component like this 23 | 24 | let options = { 25 | width: "300px", 26 | border: true, 27 | borderColor: "#2e2e2e", 28 | baseColor: "#17a2b8", 29 | centerColor: "#459cff", 30 | centerBorderColor: "#ffffff", 31 | handColors: { 32 | second: "#d81c7a", 33 | minute: "#ffffff", 34 | hour: "#ffffff" 35 | } 36 | }; 37 | ..... 38 | 39 | 40 | You can customize the clock using the different properties in the `options` JSON Object. 41 | 42 | Please visit the [demo](http://vishnuramana.github.io/analogclock) page to get a live preview of the clock. 43 | 44 | ## Features 45 | 46 | - ### Custom Time / Timezone Support 47 | *AnalogClock* now supports using your own custom time/timezone. To enable custom time usage, set `useCustomTime` to `true` and set your own `hours`, `minutes` and `seconds` in the `options` JSON Object like below: 48 | 49 | let options = { 50 | useCustomTime: true, // set this to true 51 | width: "300px", 52 | border: true, 53 | borderColor: "#2e2e2e", 54 | baseColor: "#17a2b8", 55 | centerColor: "#459cff", 56 | centerBorderColor: "#ffffff", 57 | handColors: { 58 | second: "#d81c7a", 59 | minute: "#ffffff", 60 | hour: "#ffffff" 61 | }, 62 | "seconds": 1, // set your 63 | "minutes": 10, // own 64 | "hours": 22 // time here. 65 | }; 66 | 67 | Once you do that, `AnalogClock` will expect you to give it the `hours`, `minutes` and `seconds` value. 68 | 69 | *Note: You will have to use a setInterval in the component where you use `` to update the `options` object with the time that you pass. An example is given below:* 70 | 71 | updateClock = () => { 72 | let ausTime = new Date().toLocaleString("en-US", { timeZone: "Australia/Brisbane" }); 73 | let date = new Date(ausTime); 74 | 75 | this.setState({ 76 | 'options': { 77 | ...this.state.options, 78 | seconds: date.getSeconds(), 79 | minutes: date.getMinutes(), 80 | hours: date.getHours() 81 | } 82 | }) 83 | } 84 | .... 85 | this.interval = setInterval(this.updateClock, 1000); 86 | 87 | ## Change Log 88 | - **v1.3.0** 89 | - Added finer movement of hour hand 90 | 91 | - **v1.2.2** 92 | - Fixed clock hand centering issues 93 | - Removed unwanted code 94 | 95 | ## Contribution 96 | 97 | If you wish to contribute to this project, please use the `dev` branch to add your changes and test. Make sure all the tests are passed and optimal code coverage is present. Once you are done with your changes, please raise a PR. 98 | 99 | ## Issues/Feature Requests 100 | 101 | For any issues/feature-requests, you can create an issue in Github or email me at [me@vishnu.codes](mailto:me@vishnu.codes) 102 | 103 | ## License 104 | 105 | MIT License 106 | -------------------------------------------------------------------------------- /src/__tests__/component/AnalogClock.unit.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "jest-styled-components"; 3 | import { mount } from "enzyme"; 4 | import "../../setupTests"; 5 | 6 | import AnalogClock from "../../AnalogClock.js"; 7 | 8 | describe("AnalogClock component", () => { 9 | const initClockSpy = jest.spyOn(AnalogClock.prototype, "initClock"); 10 | const setupIntervalSpy = jest.spyOn(AnalogClock.prototype, "setupInterval"); 11 | const setupTimeSpy = jest.spyOn(AnalogClock.prototype, "setupTime"); 12 | const componentWillUnmountSpy = jest.spyOn( 13 | AnalogClock.prototype, 14 | "componentWillUnmount" 15 | ); 16 | 17 | beforeEach(() => { 18 | jest.useFakeTimers(); 19 | }) 20 | 21 | 22 | afterEach(() => { 23 | initClockSpy.mockClear(); 24 | setupIntervalSpy.mockClear(); 25 | componentWillUnmountSpy.mockClear(); 26 | setupTimeSpy.mockClear(); 27 | jest.useRealTimers(); 28 | }); 29 | 30 | it("mounts the component", () => { 31 | let options = { 32 | width: "300px", 33 | border: true, 34 | borderColor: "#2e2e2e", 35 | baseColor: "#17a2b8", 36 | centerColor: "#459cff", 37 | centerBorderColor: "#fff", 38 | handColors: { 39 | second: "#d81c7a", 40 | minute: "#fff", 41 | hour: "#fff", 42 | }, 43 | }; 44 | const wrapper = mount(); 45 | expect(initClockSpy).toHaveBeenCalledTimes(1); 46 | expect(setupIntervalSpy).toHaveBeenCalledTimes(1); 47 | expect(setInterval).toHaveBeenCalledTimes(1); 48 | expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 1000); 49 | jest.advanceTimersByTime(1001); 50 | expect(setupTimeSpy).toHaveBeenCalledTimes(1) 51 | }); 52 | 53 | it("uses 12 hr time format", () => { 54 | const constantDate = new Date("2021-10-10T14:14:14"); 55 | 56 | Date = class extends Date { 57 | constructor() { 58 | return constantDate; 59 | } 60 | }; 61 | 62 | let options = { 63 | width: "300px", 64 | border: true, 65 | borderColor: "#2e2e2e", 66 | baseColor: "#17a2b8", 67 | centerColor: "#459cff", 68 | centerBorderColor: "#fff", 69 | handColors: { 70 | second: "#d81c7a", 71 | minute: "#fff", 72 | hour: "#fff", 73 | }, 74 | }; 75 | const wrapper = mount(); 76 | expect(setInterval).toHaveBeenCalledTimes(1); 77 | expect(setInterval).toHaveBeenLastCalledWith(expect.any(Function), 1000); 78 | expect(wrapper.state("hours")).toEqual(2); 79 | jest.advanceTimersByTime(1500); 80 | expect(wrapper.state("hours")).toEqual(2); 81 | }); 82 | 83 | it("unmounts the component", () => { 84 | let options = { 85 | width: "300px", 86 | border: true, 87 | borderColor: "#2e2e2e", 88 | baseColor: "#17a2b8", 89 | centerColor: "#459cff", 90 | centerBorderColor: "#fff", 91 | handColors: { 92 | second: "#d81c7a", 93 | minute: "#fff", 94 | hour: "#fff", 95 | }, 96 | }; 97 | const wrapper = mount(); 98 | wrapper.unmount(); 99 | expect(componentWillUnmountSpy).toHaveBeenCalledTimes(1); 100 | expect(clearInterval).toHaveBeenCalledTimes(1); 101 | }); 102 | 103 | it("sets custom time when useCustomTime is set to true", () => { 104 | let options = { 105 | width: "300px", 106 | border: true, 107 | borderColor: "#2e2e2e", 108 | baseColor: "#17a2b8", 109 | centerColor: "#459cff", 110 | centerBorderColor: "#fff", 111 | handColors: { 112 | second: "#d81c7a", 113 | minute: "#fff", 114 | hour: "#fff", 115 | }, 116 | }; 117 | 118 | const wrapper = mount(); 119 | wrapper.setProps({ 120 | ...options, 121 | useCustomTime: true, 122 | seconds: 22, 123 | minutes: 22, 124 | hours: 22, 125 | }); 126 | expect(setupIntervalSpy).toHaveBeenCalledTimes(1); 127 | expect(clearInterval).toHaveBeenCalledTimes(1); 128 | }); 129 | 130 | it("sets current time when useCustomTime is set to false", () => { 131 | let options = { 132 | useCustomTime: true, 133 | seconds: 22, 134 | minutes: 22, 135 | hours: 22, 136 | width: "300px", 137 | border: true, 138 | borderColor: "#2e2e2e", 139 | baseColor: "#17a2b8", 140 | centerColor: "#459cff", 141 | centerBorderColor: "#fff", 142 | handColors: { 143 | second: "#d81c7a", 144 | minute: "#fff", 145 | hour: "#fff", 146 | }, 147 | }; 148 | 149 | const wrapper = mount(); 150 | wrapper.setProps({ 151 | ...options, 152 | useCustomTime: false, 153 | }); 154 | expect(setupIntervalSpy).toHaveBeenCalledTimes(2); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/App.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`App component matches snapshot 1`] = ` 4 |
5 |
8 | 27 |
28 |
31 |
34 |

35 | 36 | Options 37 | 38 |

39 |