├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .prettierignore ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── examples ├── next-app │ ├── .gitignore │ ├── index.css │ ├── package-lock.json │ ├── package.json │ └── pages │ │ ├── _app.js │ │ └── index.js └── react-app │ ├── README.md │ ├── package-lock.json │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json │ ├── src │ ├── App.test.tsx │ ├── App.tsx │ ├── index.css │ ├── index.tsx │ ├── react-app-env.d.ts │ └── setupTests.ts │ └── tsconfig.json ├── package-lock.json ├── package.json ├── resources └── demo.gif ├── src ├── .eslintrc ├── FlipClockCountDown.spec.tsx ├── FlipClockCountDown.tsx ├── FlipClockDigit.tsx ├── declaration.d.ts ├── index.ts ├── overrides.d.ts ├── styles.module.css ├── types.ts └── utils.ts ├── tsconfig.json └── tsconfig.test.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | node_modules/ 4 | .snapshots/ 5 | *.min.js 6 | resources/ -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "extends": [ 4 | "standard", 5 | "plugin:prettier/recommended", 6 | "prettier/standard", 7 | "prettier/react", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "plugins": [ 11 | "@typescript-eslint" 12 | ], 13 | "env": { 14 | "node": true 15 | }, 16 | "parserOptions": { 17 | "ecmaVersion": 2020, 18 | "ecmaFeatures": { 19 | "legacyDecorators": true, 20 | "jsx": true 21 | } 22 | }, 23 | "settings": { 24 | "react": { 25 | "version": "16" 26 | } 27 | }, 28 | "rules": { 29 | "space-before-function-paren": 0, 30 | "react/prop-types": 0, 31 | "react/jsx-handler-names": 0, 32 | "react/jsx-fragments": 0, 33 | "react/no-unused-prop-types": 0, 34 | "import/export": 0, 35 | "no-unused-vars": "off", 36 | "@typescript-eslint/no-unused-vars": [ 37 | "error" 38 | ], 39 | "no-use-before-define": "off", 40 | "@typescript-eslint/no-use-before-define": [ 41 | "warn" 42 | ] 43 | } 44 | } -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | # Triggers the workflow on push or pull request events but only for the "master" branch 5 | push: 6 | branches: ['master'] 7 | pull_request: 8 | branches: ['master'] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | strategy: 16 | matrix: 17 | version: [14, 16, 18, 20] 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v2 21 | - uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.version }} 24 | 25 | - name: Get yarn cache directory path 26 | id: yarn-cache-dir-path 27 | run: echo "::set-output name=dir::$(yarn cache dir)" 28 | 29 | - name: Get yarn cache 30 | uses: actions/cache@v2 31 | id: yarn-cache 32 | with: 33 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 34 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 35 | restore-keys: ${{ runner.os }}-yarn- 36 | 37 | - name: Install dependencies 38 | run: npm ci 39 | 40 | - name: Build 41 | run: npm run build 42 | 43 | unit-tests: 44 | strategy: 45 | matrix: 46 | version: [14, 16, 18, 20] 47 | runs-on: ubuntu-latest 48 | steps: 49 | - uses: actions/checkout@v2 50 | - uses: actions/setup-node@v2 51 | with: 52 | node-version: ${{ matrix.version }} 53 | 54 | - name: Get yarn cache directory path 55 | id: yarn-cache-dir-path 56 | run: echo "::set-output name=dir::$(yarn cache dir)" 57 | 58 | - name: Get yarn cache 59 | uses: actions/cache@v2 60 | id: yarn-cache 61 | with: 62 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 63 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 64 | restore-keys: ${{ runner.os }}-yarn- 65 | 66 | - name: Install dependencies 67 | run: npm ci 68 | 69 | - name: Lint 70 | run: npm run test:lint 71 | 72 | - name: Run unit tests 73 | run: npm run test:unit 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | 6 | # builds 7 | build 8 | dist 9 | .rpt2_cache 10 | 11 | # misc 12 | .DS_Store 13 | .env 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no -- commitlint --edit "$1" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint:staged 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | /dist 4 | /build 5 | .dockerignore 6 | .DS_Store 7 | .eslintignore 8 | *.png 9 | *.toml 10 | docker 11 | .editorconfig 12 | Dockerfile* 13 | .gitignore 14 | .prettierignore 15 | LICENSE 16 | .eslintcache 17 | *.lock 18 | yarn-error.log 19 | .history 20 | CNAME 21 | resources/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "singleQuote": true, 4 | "jsxSingleQuote": true, 5 | "semi": true, 6 | "tabWidth": 2, 7 | "bracketSpacing": true, 8 | "jsxBracketSameLine": false, 9 | "arrowParens": "always", 10 | "trailingComma": "none" 11 | } 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 12 4 | - 10 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Nguyen Ba Ngoc 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-flip-clock-countdown 2 | 3 | > A 3D animated countdown component for React. 4 | 5 | [![NPM](https://img.shields.io/npm/v/@leenguyen/react-flip-clock-countdown.svg)](https://www.npmjs.com/package/@leenguyen/react-flip-clock-countdown) [![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com) 6 | 7 |
8 | react flip clock countdown demo 9 |
10 | 11 | ## Install 12 | 13 | ```bash 14 | npm install --save @leenguyen/react-flip-clock-countdown 15 | ``` 16 | 17 | Or 18 | 19 | ```bash 20 | yarn add @leenguyen/react-flip-clock-countdown 21 | ``` 22 | 23 | ## Props 24 | 25 | The FlipClockCountdown has all properties of `div` and additional props below 26 | 27 | | Name | Type | Required | Default | Description | 28 | | :------------------------- | :---------------------------------------: | :------: | :--------------------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 29 | | **to** | Date|string|number | yes | | Date or timestamp in the future. | 30 | | ~~**containerProps**~~ | object | no | undefined | Props apply to the flip clock container. This prop is deprecated, you should apply directly to the FlipClockCountdown component. | 31 | | **onComplete** | func | no | | Callback when countdown ends
**Signature**:
`function() => void` | 32 | | **onTick** | func | no | | Callback on every interval tick
**Signature**:
`function({ timeDelta, completed }) => void` | 33 | | **renderMap** | `Array` | no | [true, true, true, true] | Each element represents the render state of each section (day, hour, minute, second). If `true` section will be rendered, `false` otherwise. | 34 | | **labels** | `Array` | no | ['Days', 'Hours', 'Minutes', 'Seconds'] | Custom array of labels used to represent information for each section (day, hour, minute, second). | 35 | | **showLabels** | boolean | no | true | Set it to `false` if you don't want to show the labels. | 36 | | **showSeparators** | boolean | no | true | Set it to `false` if you don't want to show the separators (colon) between time unit. | 37 | | **labelStyle** | React.CSSProperties | no | undefined | The styles apply to labels `font-size`, `color`, `width`, `height`, etc. | 38 | | **digitBlockStyle** | React.CSSProperties | no | undefined | The styles apply to digit blocks like `font-size`, `color`, `width`, `height`, etc. | 39 | | **separatorStyle** | object | no | undefined | The styles apply to separator (colon), includes `size` and `color`. | 40 | | **dividerStyle** | object | no | undefined | The style will be applied to divider, includes `color` and `height`. | 41 | | **spacing** | object | no | undefined | This prop allows you to modify the clock spacing. | 42 | | **duration** | number | no | 0.7 | Duration (in second) when flip card. Valid value in range (0, 1). | 43 | | **hideOnComplete** | boolean | no | true | By default, the countdown will be hidden when it completed (or show children if provided). This will keep the timer in place and stuck at zeros when the countdown is completed. | 44 | | **stopOnHiddenVisibility** | boolean | no | false | Whether or not to stop the clock when the visibilityState is hidden, enabling this feature will prevent the component gets derailed if we switch between browser tabs. | 45 | | **renderOnServer** | boolean | no | false | Whether or not to render the clock on server. | 46 | 47 | ## Usage 48 | 49 | ### Basic usage 50 | 51 | ```tsx 52 | import React, { Component } from 'react'; 53 | 54 | import FlipClockCountdown from '@leenguyen/react-flip-clock-countdown'; 55 | import '@leenguyen/react-flip-clock-countdown/dist/index.css'; 56 | 57 | class Example extends Component { 58 | render() { 59 | return ; 60 | } 61 | } 62 | ``` 63 | 64 | ### Render a React Component when countdown is complete 65 | 66 | In case you want to change the output of the component, or want to signal that the countdown's work is done, you can do this by either using the onComplete callback or by specifying a React child within ``, which will only be shown once the countdown is complete. 67 | 68 | ```tsx 69 | import React, { Component } from 'react'; 70 | 71 | import FlipClockCountdown from '@leenguyen/react-flip-clock-countdown'; 72 | import '@leenguyen/react-flip-clock-countdown/dist/index.css'; 73 | 74 | class Completed extends Component { 75 | render() { 76 | return The countdown is complete 77 | } 78 | } 79 | 80 | class RenderByUsingReactChild extends Component { 81 | render() { 82 | return ( 83 | 84 | 85 | ; 86 | ) 87 | } 88 | } 89 | 90 | class RenderByUsingCallback extends Component { 91 | constructor(props) { 92 | super(props); 93 | 94 | this.endTime = new Date().getTime() + 24 * 3600 * 1000 + 5000; 95 | this.state = { 96 | isCompleted: false 97 | } 98 | 99 | this.handleComplete = this.handleComplete.bind(this); 100 | } 101 | 102 | handleComplete() { 103 | this.setState({ isCompleted: true }); 104 | } 105 | 106 | render() { 107 | return ( 108 | 109 | {isCompleted && } 110 | 111 | 112 | ) 113 | } 114 | } 115 | ``` 116 | 117 | ### Render a custom countdown 118 | 119 | #### Custom styles 120 | 121 | ```tsx 122 | class Example extends Component { 123 | render() { 124 | return ( 125 | 134 | Finished 135 | 136 | ); 137 | } 138 | } 139 | ``` 140 | 141 | #### Custom styles via css 142 | 143 | ```tsx 144 | import 'styles.css'; 145 | 146 | class Example extends Component { 147 | render() { 148 | return ; 149 | } 150 | } 151 | ``` 152 | 153 | ```css 154 | /* styles.css */ 155 | 156 | .flip-clock { 157 | --fcc-flip-duration: 0.5s; /* transition duration when flip card */ 158 | --fcc-spacing: 8px; /* space between unit times and separators */ 159 | --fcc-digit-block-width: 40px; /* width of digit card */ 160 | --fcc-digit-block-height: 60px; /* height of digit card, highly recommend in even number */ 161 | --fcc-digit-block-radius: 5px; /* border radius of digit card */ 162 | --fcc-digit-block-spacing: 5px; /* space between blocks in each unit of time */ 163 | --fcc-digit-font-size: 30px; /* font size of digit */ 164 | --fcc-digit-color: white; /* color of digit */ 165 | --fcc-label-font-size: 10px; /* font size of label */ 166 | --fcc-label-color: #ffffff; /* color of label */ 167 | --fcc-background: black; /* background of digit card */ 168 | --fcc-divider-color: white; /* color of divider */ 169 | --fcc-divider-height: 1px; /* height of divider */ 170 | --fcc-separator-size: 6px; /* size of colon */ 171 | --fcc-separator-color: red; /* color of colon */ 172 | } 173 | ``` 174 | 175 | #### Custom section to be rendered 176 | 177 | In case you don't want to display the date, use `renderMap` to custom render state of each section 178 | 179 | ```tsx 180 | class Example extends Component { 181 | render() { 182 | return ( 183 | 184 | Finished 185 | 186 | ); 187 | } 188 | } 189 | ``` 190 | 191 | ## Contributing 192 | 193 | The package is made up of 2 main folders: 194 | 195 | - /src contains the FlipClockCountdown 196 | - /examples contains the create-react-app and create-next-app based demo website 197 | 198 | To setup and run a local copy: 199 | 200 | 1. Clone this repo with `https://github.com/sLeeNguyen/react-flip-clock-countdown` 201 | 2. Run `npm install` in the **root** folder 202 | 3. Run `npm install` in the **examples/react-app** folder 203 | 4. In separate terminal windows, run `npm start` in the **root** and **examples/react-app** folders. 204 | 205 | When you're done working on your changes, feel free to send PRs with the details and include a screenshot if you've changed anything visually. 206 | 207 | ## License 208 | 209 | MIT © [leenguyen](https://github.com/sLeenguyen) 210 | -------------------------------------------------------------------------------- /examples/next-app/.gitignore: -------------------------------------------------------------------------------- 1 | .next/ 2 | out/ 3 | -------------------------------------------------------------------------------- /examples/next-app/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 8 | "Droid Sans", "Helvetica Neue", sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | /* display: flex; 13 | justify-content: center; 14 | align-items: center; */ 15 | padding: 0px 24px; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 20 | } 21 | 22 | html, 23 | body { 24 | height: 100vh; 25 | background-color: teal; 26 | color: white; 27 | } 28 | 29 | h2 { 30 | font-size: 24px; 31 | } 32 | 33 | .flip-clock { 34 | --fcc-flip-duration: 0.8s; /* transition duration when flip card */ 35 | --fcc-digit-block-width: 40px; /* width of digit card */ 36 | --fcc-digit-block-height: 64px; /* height of digit card, highly recommend in even number */ 37 | --fcc-digit-font-size: 40px; /* font size of digit */ 38 | --fcc-label-font-size: 16px; /* font size of label */ 39 | --fcc-digit-color: white; /* color of digit */ 40 | --fcc-background: black; /* background of digit card */ 41 | --fcc-label-color: #ffffff; /* color of label */ 42 | --fcc-divider-color: #ffffff66; /* color of divider */ 43 | } 44 | -------------------------------------------------------------------------------- /examples/next-app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fliplock-countdown-example-nextjs", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "react-fliplock-countdown-example-nextjs", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "dependencies": { 12 | "next": ">=12", 13 | "react": "file:../../node_modules/react", 14 | "react-dom": "file:../../node_modules/react-dom", 15 | "react-flip-clock-countdown": "file:../.." 16 | } 17 | }, 18 | "../..": { 19 | "name": "@leenguyen/react-flip-clock-countdown", 20 | "version": "1.4.0", 21 | "license": "MIT", 22 | "dependencies": { 23 | "clsx": "^1.1.1" 24 | }, 25 | "devDependencies": { 26 | "@commitlint/cli": "^16.0.0", 27 | "@commitlint/config-conventional": "^16.0.0", 28 | "@testing-library/jest-dom": ">=4.2.4", 29 | "@testing-library/react": ">=9.5.0", 30 | "@testing-library/user-event": ">=7.2.1", 31 | "@types/jest": ">=25.1.4", 32 | "@types/node": "^12.12.38", 33 | "@types/react": ">=16.9.27", 34 | "@types/react-dom": ">=16.9.7", 35 | "@typescript-eslint/eslint-plugin": "^4.9.1", 36 | "@typescript-eslint/parser": "^4.9.1", 37 | "babel-eslint": "^10.0.3", 38 | "commitizen": "^4.2.4", 39 | "cross-env": "^7.0.2", 40 | "cz-conventional-changelog": "^3.3.0", 41 | "eslint": "^6.8.0 || ^7.5.0", 42 | "eslint-config-prettier": "^6.7.0", 43 | "eslint-config-standard": "^14.1.0", 44 | "eslint-plugin-node": "^11.0.0", 45 | "eslint-plugin-prettier": "^3.1.1", 46 | "eslint-plugin-promise": "^6.0.0", 47 | "eslint-plugin-standard": "^5.0.0", 48 | "gh-pages": "^2.2.0", 49 | "husky": "^7.0.4", 50 | "lint-staged": "^12.1.4", 51 | "microbundle-crl": "^0.13.10", 52 | "npm-run-all": "^4.1.5", 53 | "postcss-flexbugs-fixes": "^5.0.2", 54 | "prettier": "^2.0.4", 55 | "react": ">= 16.13.0", 56 | "react-dom": ">= 16.13.0", 57 | "react-scripts": "^3.4.1 || >= 4.0.3", 58 | "typescript": "^3.7.5" 59 | }, 60 | "engines": { 61 | "node": ">=12" 62 | }, 63 | "peerDependencies": { 64 | "react": ">= 16.13.0" 65 | } 66 | }, 67 | "../../node_modules/react": { 68 | "version": "18.2.0", 69 | "license": "MIT", 70 | "dependencies": { 71 | "loose-envify": "^1.1.0" 72 | }, 73 | "engines": { 74 | "node": ">=0.10.0" 75 | } 76 | }, 77 | "../../node_modules/react-dom": { 78 | "version": "18.2.0", 79 | "license": "MIT", 80 | "dependencies": { 81 | "loose-envify": "^1.1.0", 82 | "scheduler": "^0.23.0" 83 | }, 84 | "peerDependencies": { 85 | "react": "^18.2.0" 86 | } 87 | }, 88 | "node_modules/@next/env": { 89 | "version": "13.4.12", 90 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.12.tgz", 91 | "integrity": "sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==" 92 | }, 93 | "node_modules/@next/swc-darwin-arm64": { 94 | "version": "13.4.12", 95 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz", 96 | "integrity": "sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==", 97 | "cpu": [ 98 | "arm64" 99 | ], 100 | "optional": true, 101 | "os": [ 102 | "darwin" 103 | ], 104 | "engines": { 105 | "node": ">= 10" 106 | } 107 | }, 108 | "node_modules/@next/swc-darwin-x64": { 109 | "version": "13.4.12", 110 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.12.tgz", 111 | "integrity": "sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==", 112 | "cpu": [ 113 | "x64" 114 | ], 115 | "optional": true, 116 | "os": [ 117 | "darwin" 118 | ], 119 | "engines": { 120 | "node": ">= 10" 121 | } 122 | }, 123 | "node_modules/@next/swc-linux-arm64-gnu": { 124 | "version": "13.4.12", 125 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.12.tgz", 126 | "integrity": "sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==", 127 | "cpu": [ 128 | "arm64" 129 | ], 130 | "optional": true, 131 | "os": [ 132 | "linux" 133 | ], 134 | "engines": { 135 | "node": ">= 10" 136 | } 137 | }, 138 | "node_modules/@next/swc-linux-arm64-musl": { 139 | "version": "13.4.12", 140 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.12.tgz", 141 | "integrity": "sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==", 142 | "cpu": [ 143 | "arm64" 144 | ], 145 | "optional": true, 146 | "os": [ 147 | "linux" 148 | ], 149 | "engines": { 150 | "node": ">= 10" 151 | } 152 | }, 153 | "node_modules/@next/swc-linux-x64-gnu": { 154 | "version": "13.4.12", 155 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.12.tgz", 156 | "integrity": "sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==", 157 | "cpu": [ 158 | "x64" 159 | ], 160 | "optional": true, 161 | "os": [ 162 | "linux" 163 | ], 164 | "engines": { 165 | "node": ">= 10" 166 | } 167 | }, 168 | "node_modules/@next/swc-linux-x64-musl": { 169 | "version": "13.4.12", 170 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.12.tgz", 171 | "integrity": "sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==", 172 | "cpu": [ 173 | "x64" 174 | ], 175 | "optional": true, 176 | "os": [ 177 | "linux" 178 | ], 179 | "engines": { 180 | "node": ">= 10" 181 | } 182 | }, 183 | "node_modules/@next/swc-win32-arm64-msvc": { 184 | "version": "13.4.12", 185 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.12.tgz", 186 | "integrity": "sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==", 187 | "cpu": [ 188 | "arm64" 189 | ], 190 | "optional": true, 191 | "os": [ 192 | "win32" 193 | ], 194 | "engines": { 195 | "node": ">= 10" 196 | } 197 | }, 198 | "node_modules/@next/swc-win32-ia32-msvc": { 199 | "version": "13.4.12", 200 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.12.tgz", 201 | "integrity": "sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==", 202 | "cpu": [ 203 | "ia32" 204 | ], 205 | "optional": true, 206 | "os": [ 207 | "win32" 208 | ], 209 | "engines": { 210 | "node": ">= 10" 211 | } 212 | }, 213 | "node_modules/@next/swc-win32-x64-msvc": { 214 | "version": "13.4.12", 215 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.12.tgz", 216 | "integrity": "sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==", 217 | "cpu": [ 218 | "x64" 219 | ], 220 | "optional": true, 221 | "os": [ 222 | "win32" 223 | ], 224 | "engines": { 225 | "node": ">= 10" 226 | } 227 | }, 228 | "node_modules/@swc/helpers": { 229 | "version": "0.5.1", 230 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", 231 | "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", 232 | "dependencies": { 233 | "tslib": "^2.4.0" 234 | } 235 | }, 236 | "node_modules/busboy": { 237 | "version": "1.6.0", 238 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 239 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 240 | "dependencies": { 241 | "streamsearch": "^1.1.0" 242 | }, 243 | "engines": { 244 | "node": ">=10.16.0" 245 | } 246 | }, 247 | "node_modules/caniuse-lite": { 248 | "version": "1.0.30001517", 249 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", 250 | "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==", 251 | "funding": [ 252 | { 253 | "type": "opencollective", 254 | "url": "https://opencollective.com/browserslist" 255 | }, 256 | { 257 | "type": "tidelift", 258 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 259 | }, 260 | { 261 | "type": "github", 262 | "url": "https://github.com/sponsors/ai" 263 | } 264 | ] 265 | }, 266 | "node_modules/client-only": { 267 | "version": "0.0.1", 268 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 269 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 270 | }, 271 | "node_modules/glob-to-regexp": { 272 | "version": "0.4.1", 273 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 274 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 275 | }, 276 | "node_modules/graceful-fs": { 277 | "version": "4.2.11", 278 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 279 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 280 | }, 281 | "node_modules/nanoid": { 282 | "version": "3.3.6", 283 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 284 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", 285 | "funding": [ 286 | { 287 | "type": "github", 288 | "url": "https://github.com/sponsors/ai" 289 | } 290 | ], 291 | "bin": { 292 | "nanoid": "bin/nanoid.cjs" 293 | }, 294 | "engines": { 295 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 296 | } 297 | }, 298 | "node_modules/next": { 299 | "version": "13.4.12", 300 | "resolved": "https://registry.npmjs.org/next/-/next-13.4.12.tgz", 301 | "integrity": "sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==", 302 | "dependencies": { 303 | "@next/env": "13.4.12", 304 | "@swc/helpers": "0.5.1", 305 | "busboy": "1.6.0", 306 | "caniuse-lite": "^1.0.30001406", 307 | "postcss": "8.4.14", 308 | "styled-jsx": "5.1.1", 309 | "watchpack": "2.4.0", 310 | "zod": "3.21.4" 311 | }, 312 | "bin": { 313 | "next": "dist/bin/next" 314 | }, 315 | "engines": { 316 | "node": ">=16.8.0" 317 | }, 318 | "optionalDependencies": { 319 | "@next/swc-darwin-arm64": "13.4.12", 320 | "@next/swc-darwin-x64": "13.4.12", 321 | "@next/swc-linux-arm64-gnu": "13.4.12", 322 | "@next/swc-linux-arm64-musl": "13.4.12", 323 | "@next/swc-linux-x64-gnu": "13.4.12", 324 | "@next/swc-linux-x64-musl": "13.4.12", 325 | "@next/swc-win32-arm64-msvc": "13.4.12", 326 | "@next/swc-win32-ia32-msvc": "13.4.12", 327 | "@next/swc-win32-x64-msvc": "13.4.12" 328 | }, 329 | "peerDependencies": { 330 | "@opentelemetry/api": "^1.1.0", 331 | "fibers": ">= 3.1.0", 332 | "react": "^18.2.0", 333 | "react-dom": "^18.2.0", 334 | "sass": "^1.3.0" 335 | }, 336 | "peerDependenciesMeta": { 337 | "@opentelemetry/api": { 338 | "optional": true 339 | }, 340 | "fibers": { 341 | "optional": true 342 | }, 343 | "sass": { 344 | "optional": true 345 | } 346 | } 347 | }, 348 | "node_modules/picocolors": { 349 | "version": "1.0.0", 350 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 351 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 352 | }, 353 | "node_modules/postcss": { 354 | "version": "8.4.14", 355 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 356 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 357 | "funding": [ 358 | { 359 | "type": "opencollective", 360 | "url": "https://opencollective.com/postcss/" 361 | }, 362 | { 363 | "type": "tidelift", 364 | "url": "https://tidelift.com/funding/github/npm/postcss" 365 | } 366 | ], 367 | "dependencies": { 368 | "nanoid": "^3.3.4", 369 | "picocolors": "^1.0.0", 370 | "source-map-js": "^1.0.2" 371 | }, 372 | "engines": { 373 | "node": "^10 || ^12 || >=14" 374 | } 375 | }, 376 | "node_modules/react": { 377 | "resolved": "../../node_modules/react", 378 | "link": true 379 | }, 380 | "node_modules/react-dom": { 381 | "resolved": "../../node_modules/react-dom", 382 | "link": true 383 | }, 384 | "node_modules/react-flip-clock-countdown": { 385 | "resolved": "../..", 386 | "link": true 387 | }, 388 | "node_modules/source-map-js": { 389 | "version": "1.0.2", 390 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 391 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", 392 | "engines": { 393 | "node": ">=0.10.0" 394 | } 395 | }, 396 | "node_modules/streamsearch": { 397 | "version": "1.1.0", 398 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 399 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 400 | "engines": { 401 | "node": ">=10.0.0" 402 | } 403 | }, 404 | "node_modules/styled-jsx": { 405 | "version": "5.1.1", 406 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 407 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 408 | "dependencies": { 409 | "client-only": "0.0.1" 410 | }, 411 | "engines": { 412 | "node": ">= 12.0.0" 413 | }, 414 | "peerDependencies": { 415 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" 416 | }, 417 | "peerDependenciesMeta": { 418 | "@babel/core": { 419 | "optional": true 420 | }, 421 | "babel-plugin-macros": { 422 | "optional": true 423 | } 424 | } 425 | }, 426 | "node_modules/tslib": { 427 | "version": "2.6.1", 428 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", 429 | "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" 430 | }, 431 | "node_modules/watchpack": { 432 | "version": "2.4.0", 433 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", 434 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", 435 | "dependencies": { 436 | "glob-to-regexp": "^0.4.1", 437 | "graceful-fs": "^4.1.2" 438 | }, 439 | "engines": { 440 | "node": ">=10.13.0" 441 | } 442 | }, 443 | "node_modules/zod": { 444 | "version": "3.21.4", 445 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", 446 | "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==", 447 | "funding": { 448 | "url": "https://github.com/sponsors/colinhacks" 449 | } 450 | } 451 | }, 452 | "dependencies": { 453 | "@next/env": { 454 | "version": "13.4.12", 455 | "resolved": "https://registry.npmjs.org/@next/env/-/env-13.4.12.tgz", 456 | "integrity": "sha512-RmHanbV21saP/6OEPBJ7yJMuys68cIf8OBBWd7+uj40LdpmswVAwe1uzeuFyUsd6SfeITWT3XnQfn6wULeKwDQ==" 457 | }, 458 | "@next/swc-darwin-arm64": { 459 | "version": "13.4.12", 460 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.4.12.tgz", 461 | "integrity": "sha512-deUrbCXTMZ6ZhbOoloqecnUeNpUOupi8SE2tx4jPfNS9uyUR9zK4iXBvH65opVcA/9F5I/p8vDXSYbUlbmBjZg==", 462 | "optional": true 463 | }, 464 | "@next/swc-darwin-x64": { 465 | "version": "13.4.12", 466 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-13.4.12.tgz", 467 | "integrity": "sha512-WRvH7RxgRHlC1yb5oG0ZLx8F7uci9AivM5/HGGv9ZyG2Als8Ij64GC3d+mQ5sJhWjusyU6T6V1WKTUoTmOB0zQ==", 468 | "optional": true 469 | }, 470 | "@next/swc-linux-arm64-gnu": { 471 | "version": "13.4.12", 472 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.4.12.tgz", 473 | "integrity": "sha512-YEKracAWuxp54tKiAvvq73PUs9lok57cc8meYRibTWe/VdPB2vLgkTVWFcw31YDuRXdEhdX0fWS6Q+ESBhnEig==", 474 | "optional": true 475 | }, 476 | "@next/swc-linux-arm64-musl": { 477 | "version": "13.4.12", 478 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.4.12.tgz", 479 | "integrity": "sha512-LhJR7/RAjdHJ2Isl2pgc/JaoxNk0KtBgkVpiDJPVExVWA1c6gzY57+3zWuxuyWzTG+fhLZo2Y80pLXgIJv7g3g==", 480 | "optional": true 481 | }, 482 | "@next/swc-linux-x64-gnu": { 483 | "version": "13.4.12", 484 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.4.12.tgz", 485 | "integrity": "sha512-1DWLL/B9nBNiQRng+1aqs3OaZcxC16Nf+mOnpcrZZSdyKHek3WQh6j/fkbukObgNGwmCoVevLUa/p3UFTTqgqg==", 486 | "optional": true 487 | }, 488 | "@next/swc-linux-x64-musl": { 489 | "version": "13.4.12", 490 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.4.12.tgz", 491 | "integrity": "sha512-kEAJmgYFhp0VL+eRWmUkVxLVunn7oL9Mdue/FS8yzRBVj7Z0AnIrHpTIeIUl1bbdQq1VaoOztnKicAjfkLTRCQ==", 492 | "optional": true 493 | }, 494 | "@next/swc-win32-arm64-msvc": { 495 | "version": "13.4.12", 496 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.4.12.tgz", 497 | "integrity": "sha512-GMLuL/loR6yIIRTnPRY6UGbLL9MBdw2anxkOnANxvLvsml4F0HNIgvnU3Ej4BjbqMTNjD4hcPFdlEow4XHPdZA==", 498 | "optional": true 499 | }, 500 | "@next/swc-win32-ia32-msvc": { 501 | "version": "13.4.12", 502 | "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.4.12.tgz", 503 | "integrity": "sha512-PhgNqN2Vnkm7XaMdRmmX0ZSwZXQAtamBVSa9A/V1dfKQCV1rjIZeiy/dbBnVYGdj63ANfsOR/30XpxP71W0eww==", 504 | "optional": true 505 | }, 506 | "@next/swc-win32-x64-msvc": { 507 | "version": "13.4.12", 508 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.4.12.tgz", 509 | "integrity": "sha512-Z+56e/Ljt0bUs+T+jPjhFyxYBcdY2RIq9ELFU+qAMQMteHo7ymbV7CKmlcX59RI9C4YzN8PgMgLyAoi916b5HA==", 510 | "optional": true 511 | }, 512 | "@swc/helpers": { 513 | "version": "0.5.1", 514 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.1.tgz", 515 | "integrity": "sha512-sJ902EfIzn1Fa+qYmjdQqh8tPsoxyBz+8yBKC2HKUxyezKJFwPGOn7pv4WY6QuQW//ySQi5lJjA/ZT9sNWWNTg==", 516 | "requires": { 517 | "tslib": "^2.4.0" 518 | } 519 | }, 520 | "busboy": { 521 | "version": "1.6.0", 522 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 523 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 524 | "requires": { 525 | "streamsearch": "^1.1.0" 526 | } 527 | }, 528 | "caniuse-lite": { 529 | "version": "1.0.30001517", 530 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz", 531 | "integrity": "sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==" 532 | }, 533 | "client-only": { 534 | "version": "0.0.1", 535 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 536 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" 537 | }, 538 | "glob-to-regexp": { 539 | "version": "0.4.1", 540 | "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", 541 | "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" 542 | }, 543 | "graceful-fs": { 544 | "version": "4.2.11", 545 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 546 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 547 | }, 548 | "nanoid": { 549 | "version": "3.3.6", 550 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", 551 | "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" 552 | }, 553 | "next": { 554 | "version": "13.4.12", 555 | "resolved": "https://registry.npmjs.org/next/-/next-13.4.12.tgz", 556 | "integrity": "sha512-eHfnru9x6NRmTMcjQp6Nz0J4XH9OubmzOa7CkWL+AUrUxpibub3vWwttjduu9No16dug1kq04hiUUpo7J3m3Xw==", 557 | "requires": { 558 | "@next/env": "13.4.12", 559 | "@next/swc-darwin-arm64": "13.4.12", 560 | "@next/swc-darwin-x64": "13.4.12", 561 | "@next/swc-linux-arm64-gnu": "13.4.12", 562 | "@next/swc-linux-arm64-musl": "13.4.12", 563 | "@next/swc-linux-x64-gnu": "13.4.12", 564 | "@next/swc-linux-x64-musl": "13.4.12", 565 | "@next/swc-win32-arm64-msvc": "13.4.12", 566 | "@next/swc-win32-ia32-msvc": "13.4.12", 567 | "@next/swc-win32-x64-msvc": "13.4.12", 568 | "@swc/helpers": "0.5.1", 569 | "busboy": "1.6.0", 570 | "caniuse-lite": "^1.0.30001406", 571 | "postcss": "8.4.14", 572 | "styled-jsx": "5.1.1", 573 | "watchpack": "2.4.0", 574 | "zod": "3.21.4" 575 | } 576 | }, 577 | "picocolors": { 578 | "version": "1.0.0", 579 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", 580 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" 581 | }, 582 | "postcss": { 583 | "version": "8.4.14", 584 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.14.tgz", 585 | "integrity": "sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==", 586 | "requires": { 587 | "nanoid": "^3.3.4", 588 | "picocolors": "^1.0.0", 589 | "source-map-js": "^1.0.2" 590 | } 591 | }, 592 | "react": { 593 | "version": "file:../../node_modules/react", 594 | "requires": { 595 | "loose-envify": "^1.1.0" 596 | } 597 | }, 598 | "react-dom": { 599 | "version": "file:../../node_modules/react-dom", 600 | "requires": { 601 | "loose-envify": "^1.1.0", 602 | "scheduler": "^0.23.0" 603 | } 604 | }, 605 | "react-flip-clock-countdown": { 606 | "version": "file:../..", 607 | "requires": { 608 | "@commitlint/cli": "^16.0.0", 609 | "@commitlint/config-conventional": "^16.0.0", 610 | "@testing-library/jest-dom": ">=4.2.4", 611 | "@testing-library/react": ">=9.5.0", 612 | "@testing-library/user-event": ">=7.2.1", 613 | "@types/jest": ">=25.1.4", 614 | "@types/node": "^12.12.38", 615 | "@types/react": ">=16.9.27", 616 | "@types/react-dom": ">=16.9.7", 617 | "@typescript-eslint/eslint-plugin": "^4.9.1", 618 | "@typescript-eslint/parser": "^4.9.1", 619 | "babel-eslint": "^10.0.3", 620 | "clsx": "^1.1.1", 621 | "commitizen": "^4.2.4", 622 | "cross-env": "^7.0.2", 623 | "cz-conventional-changelog": "^3.3.0", 624 | "eslint": "^6.8.0 || ^7.5.0", 625 | "eslint-config-prettier": "^6.7.0", 626 | "eslint-config-standard": "^14.1.0", 627 | "eslint-plugin-node": "^11.0.0", 628 | "eslint-plugin-prettier": "^3.1.1", 629 | "eslint-plugin-promise": "^6.0.0", 630 | "eslint-plugin-standard": "^5.0.0", 631 | "gh-pages": "^2.2.0", 632 | "husky": "^7.0.4", 633 | "lint-staged": "^12.1.4", 634 | "microbundle-crl": "^0.13.10", 635 | "npm-run-all": "^4.1.5", 636 | "postcss-flexbugs-fixes": "^5.0.2", 637 | "prettier": "^2.0.4", 638 | "react": ">= 16.13.0", 639 | "react-dom": ">= 16.13.0", 640 | "react-scripts": "^3.4.1 || >= 4.0.3", 641 | "typescript": "^3.7.5" 642 | }, 643 | "dependencies": { 644 | "react": { 645 | "version": "18.2.0", 646 | "requires": { 647 | "loose-envify": "^1.1.0" 648 | } 649 | }, 650 | "react-dom": { 651 | "version": "18.2.0", 652 | "requires": { 653 | "loose-envify": "^1.1.0", 654 | "scheduler": "^0.23.0" 655 | } 656 | } 657 | } 658 | }, 659 | "source-map-js": { 660 | "version": "1.0.2", 661 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", 662 | "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" 663 | }, 664 | "streamsearch": { 665 | "version": "1.1.0", 666 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 667 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" 668 | }, 669 | "styled-jsx": { 670 | "version": "5.1.1", 671 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", 672 | "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", 673 | "requires": { 674 | "client-only": "0.0.1" 675 | } 676 | }, 677 | "tslib": { 678 | "version": "2.6.1", 679 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", 680 | "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" 681 | }, 682 | "watchpack": { 683 | "version": "2.4.0", 684 | "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", 685 | "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", 686 | "requires": { 687 | "glob-to-regexp": "^0.4.1", 688 | "graceful-fs": "^4.1.2" 689 | } 690 | }, 691 | "zod": { 692 | "version": "3.21.4", 693 | "resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz", 694 | "integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==" 695 | } 696 | } 697 | } 698 | -------------------------------------------------------------------------------- /examples/next-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-fliplock-countdown-example-nextjs", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.js", 6 | "author": "leenguyen", 7 | "license": "MIT", 8 | "dependencies": { 9 | "next": ">=12", 10 | "react": "file:../../node_modules/react", 11 | "react-dom": "file:../../node_modules/react-dom", 12 | "react-flip-clock-countdown": "file:../.." 13 | }, 14 | "resolutions": { 15 | "react": "file:../../node_modules/react", 16 | "react-dom": "file:../../node_modules/react-dom" 17 | }, 18 | "scripts": { 19 | "dev": "next dev", 20 | "build": "next build", 21 | "start": "next start", 22 | "lint": "next lint" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /examples/next-app/pages/_app.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import 'react-flip-clock-countdown/dist/index.css'; 3 | import '../index.css'; 4 | 5 | function MyApp({ Component, pageProps }) { 6 | return ; 7 | } 8 | 9 | export default MyApp; 10 | -------------------------------------------------------------------------------- /examples/next-app/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import FlipClockCountdown from 'react-flip-clock-countdown'; 4 | 5 | const Example = () => { 6 | return ( 7 | 8 |

React flip-clock countdown

9 |
10 |

Default

11 | 19 | Finished 20 | 21 |
22 |
23 |

Custom styles

24 |
25 | 34 | Finished 35 | 36 |
37 |
38 | 39 |
40 |
41 |
42 |

Custom labels

43 |
44 | 49 |
50 |
51 | 56 |
57 |
58 |
59 |

Hide separators

60 | 65 |
66 |
67 |

Show/Hide sections

68 | 73 |
74 |
75 | ); 76 | }; 77 | 78 | export default Example; 79 | -------------------------------------------------------------------------------- /examples/react-app/README.md: -------------------------------------------------------------------------------- 1 | This example was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 2 | 3 | It is linked to the react-flip-clock-countdown package in the parent directory for development purposes. 4 | 5 | You can run `npm install` and then `npm start` to test your package. 6 | -------------------------------------------------------------------------------- /examples/react-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flip-clock-countdown-example", 3 | "homepage": ".", 4 | "version": "0.0.0", 5 | "private": true, 6 | "scripts": { 7 | "start": "node ../../node_modules/react-scripts/bin/react-scripts.js start", 8 | "build": "node ../../node_modules/react-scripts/bin/react-scripts.js build", 9 | "test": "node ../../node_modules/react-scripts/bin/react-scripts.js test", 10 | "eject": "node ../../node_modules/react-scripts/bin/react-scripts.js eject" 11 | }, 12 | "dependencies": { 13 | "@testing-library/jest-dom": "file:../../node_modules/@testing-library/jest-dom", 14 | "@testing-library/react": "file:../../node_modules/@testing-library/react", 15 | "@testing-library/user-event": "file:../../node_modules/@testing-library/user-event", 16 | "@types/jest": "file:../../node_modules/@types/jest", 17 | "@types/node": "file:../../node_modules/@types/node", 18 | "@types/react": "file:../../node_modules/@types/react", 19 | "@types/react-dom": "file:../../node_modules/@types/react-dom", 20 | "react": "file:../../node_modules/react", 21 | "react-dom": "file:../../node_modules/react-dom", 22 | "react-flip-clock-countdown": "file:../..", 23 | "react-scripts": "file:../../node_modules/react-scripts", 24 | "typescript": "file:../../node_modules/typescript" 25 | }, 26 | "devDependencies": { 27 | "@babel/plugin-syntax-object-rest-spread": "^7.8.3" 28 | }, 29 | "browserslist": { 30 | "production": [ 31 | ">0.2%", 32 | "not dead", 33 | "not op_mini all" 34 | ], 35 | "development": [ 36 | "last 1 chrome version", 37 | "last 1 firefox version", 38 | "last 1 safari version" 39 | ] 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /examples/react-app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sLeeNguyen/react-flip-clock-countdown/42886dc298009ca2e8d3cca758d32d06684ab966/examples/react-app/public/favicon.ico -------------------------------------------------------------------------------- /examples/react-app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 12 | 16 | 17 | 18 | 27 | react-flip-clock-countdown 28 | 29 | 30 | 31 | 34 | 35 |
36 | 37 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /examples/react-app/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "react-flip-clock-countdown", 3 | "name": "react-flip-clock-countdown", 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 | -------------------------------------------------------------------------------- /examples/react-app/src/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import App from './App'; 4 | 5 | it('renders without crashing', () => { 6 | const div = document.createElement('div'); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /examples/react-app/src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import FlipClockCountdown from 'react-flip-clock-countdown'; 4 | 5 | const App = () => { 6 | return ( 7 | 8 |

React flip-clock countdown

9 |
10 |

Default

11 | Finished 12 |
13 |
14 |

Default without completion component

15 | 16 |
17 |
18 |

Custom styles

19 |
20 | 29 | Finished 30 | 31 |
32 |
33 | 34 |
35 |
36 |
37 |

Custom labels

38 |
39 | 44 |
45 |
46 | 51 |
52 |
53 |
54 |

Hide separators

55 | 60 |
61 |
62 |

Show/Hide sections

63 | 68 |
69 |
70 | ); 71 | }; 72 | 73 | export default App; 74 | -------------------------------------------------------------------------------- /examples/react-app/src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | body { 6 | margin: 0; 7 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", 8 | "Droid Sans", "Helvetica Neue", sans-serif; 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | /* display: flex; 13 | justify-content: center; 14 | align-items: center; */ 15 | padding: 0px 24px; 16 | } 17 | 18 | code { 19 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", monospace; 20 | } 21 | 22 | html, 23 | body { 24 | height: 100vh; 25 | background-color: teal; 26 | color: white; 27 | } 28 | 29 | h2 { 30 | font-size: 24px; 31 | } 32 | 33 | .flip-clock { 34 | --fcc-flip-duration: 0.8s; /* transition duration when flip card */ 35 | --fcc-digit-block-width: 40px; /* width of digit card */ 36 | --fcc-digit-block-height: 64px; /* height of digit card, highly recommend in even number */ 37 | --fcc-digit-font-size: 40px; /* font size of digit */ 38 | --fcc-label-font-size: 16px; /* font size of label */ 39 | --fcc-digit-color: white; /* color of digit */ 40 | --fcc-background: black; /* background of digit card */ 41 | --fcc-label-color: #ffffff; /* color of label */ 42 | --fcc-divider-color: #ffffff66; /* color of divider */ 43 | } 44 | -------------------------------------------------------------------------------- /examples/react-app/src/index.tsx: -------------------------------------------------------------------------------- 1 | import 'react-flip-clock-countdown/dist/index.css'; 2 | import './index.css'; 3 | 4 | import React from 'react'; 5 | import { createRoot } from 'react-dom/client'; 6 | import App from './App'; 7 | 8 | const container = document.getElementById('root'); 9 | const root = createRoot(container!); 10 | root.render(); 11 | -------------------------------------------------------------------------------- /examples/react-app/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /examples/react-app/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom/extend-expect'; 6 | -------------------------------------------------------------------------------- /examples/react-app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "suppressImplicitAnyIndexErrors": true, 16 | "noUnusedLocals": true, 17 | "noUnusedParameters": true, 18 | "allowSyntheticDefaultImports": true, 19 | "target": "es5", 20 | "allowJs": true, 21 | "skipLibCheck": true, 22 | "strict": true, 23 | "forceConsistentCasingInFileNames": true, 24 | "resolveJsonModule": true, 25 | "isolatedModules": true, 26 | "noEmit": true, 27 | "noFallthroughCasesInSwitch": true 28 | }, 29 | "include": ["src"], 30 | "exclude": ["node_modules", "build"] 31 | } 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@leenguyen/react-flip-clock-countdown", 3 | "version": "1.6.1", 4 | "description": "A 3D animated countdown component for React.", 5 | "author": "leenguyen", 6 | "license": "MIT", 7 | "repository": "https://github.com/sLeeNguyen/react-flip-clock-countdown", 8 | "bugs": { 9 | "url": "https://github.com/sLeeNguyen/react-flip-clock-countdown/issues" 10 | }, 11 | "homepage": "https://github.com/sLeeNguyen/react-flip-clock-countdown#readme", 12 | "main": "dist/index.js", 13 | "module": "dist/index.modern.js", 14 | "types": "dist/index.d.ts", 15 | "source": "src/index.ts", 16 | "engines": { 17 | "node": ">=12" 18 | }, 19 | "keywords": [ 20 | "react", 21 | "typescript", 22 | "countdown", 23 | "flip-clock", 24 | "react-component" 25 | ], 26 | "scripts": { 27 | "build": "microbundle-crl --raw --no-generateTypes --format modern,cjs", 28 | "start": "microbundle-crl watch --raw --no-generateTypes --format modern,cjs", 29 | "prepare": "run-s build && husky install", 30 | "test": "run-s test:unit test:lint test:build", 31 | "test:build": "run-s build", 32 | "test:lint": "eslint src/*.{ts,tsx}", 33 | "test:unit": "cross-env CI=1 react-scripts test --env=jsdom", 34 | "test:watch": "react-scripts test --env=jsdom", 35 | "predeploy": "cd examples/react-app && npm install && npm run build", 36 | "deploy": "gh-pages -d examples/build", 37 | "lint:staged": "lint-staged" 38 | }, 39 | "commitlint": { 40 | "extends": [ 41 | "@commitlint/config-conventional" 42 | ] 43 | }, 44 | "lint-staged": { 45 | "*.{js,jsx,ts,tsx}": "eslint --fix", 46 | "*.{ts,js,jsx,tsx,json,yml,md}": [ 47 | "prettier --write" 48 | ] 49 | }, 50 | "dependencies": { 51 | "clsx": "^1.1.1" 52 | }, 53 | "peerDependencies": { 54 | "react": ">= 16.13.0" 55 | }, 56 | "devDependencies": { 57 | "@babel/plugin-proposal-private-property-in-object": "^7.21.11", 58 | "@babel/plugin-proposal-class-properties": "^7", 59 | "@commitlint/cli": "^16.0.0", 60 | "@commitlint/config-conventional": "^16.0.0", 61 | "@testing-library/jest-dom": "^5.17.0", 62 | "@testing-library/react": "^14.0.0", 63 | "@testing-library/user-event": "^14.4.3", 64 | "@types/jest": "^29.5.3", 65 | "@types/node": "^12.12.38", 66 | "@types/react": ">=16.9.27", 67 | "@types/react-dom": ">=16.9.7", 68 | "@typescript-eslint/eslint-plugin": "^4.9.1", 69 | "@typescript-eslint/parser": "^4.9.1", 70 | "babel-eslint": "^10.0.3", 71 | "commitizen": "^4.2.4", 72 | "cross-env": "^7.0.2", 73 | "cz-conventional-changelog": "^3.3.0", 74 | "eslint": "^6.8.0 || ^7.5.0", 75 | "eslint-config-prettier": "^6.7.0", 76 | "eslint-config-standard": "^14.1.0", 77 | "eslint-plugin-node": "^11.0.0", 78 | "eslint-plugin-prettier": "^3.1.1", 79 | "eslint-plugin-promise": "^6.0.0", 80 | "eslint-plugin-standard": "^5.0.0", 81 | "gh-pages": "^2.2.0", 82 | "husky": "^7.0.4", 83 | "lint-staged": "^12.1.4", 84 | "microbundle-crl": "^0.13.10", 85 | "npm-run-all": "^4.1.5", 86 | "postcss-flexbugs-fixes": "^5.0.2", 87 | "postcss-preset-env": "^9.3.0", 88 | "prettier": "^2.0.4", 89 | "react": ">= 16.13.0", 90 | "react-dom": ">= 16.13.0", 91 | "react-scripts": "^3.4.1 || >= 4.0.3", 92 | "typescript": "^3.7.5" 93 | }, 94 | "files": [ 95 | "dist", 96 | "package.json", 97 | "README.md", 98 | "LICENSE" 99 | ] 100 | } 101 | -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sLeeNguyen/react-flip-clock-countdown/42886dc298009ca2e8d3cca758d32d06684ab966/resources/demo.gif -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "jest": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/FlipClockCountDown.spec.tsx: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom/extend-expect'; 2 | import { cleanup, render, screen } from '@testing-library/react'; 3 | import React from 'react'; 4 | import { act } from 'react-dom/test-utils'; 5 | import FlipClockCountdown from './FlipClockCountDown'; 6 | 7 | const originalError = console.error; 8 | beforeAll(() => { 9 | console.error = (...args) => { 10 | if (/Warning: ReactDOM.render is no longer supported in React 18./.test(args[0])) { 11 | return; 12 | } 13 | originalError.call(console, ...args); 14 | }; 15 | }); 16 | 17 | afterAll(() => { 18 | console.error = originalError; 19 | }); 20 | 21 | beforeEach(() => { 22 | jest.useFakeTimers(); 23 | }); 24 | 25 | afterEach(() => { 26 | jest.runOnlyPendingTimers(); 27 | jest.useRealTimers(); 28 | }); 29 | 30 | test('should render a countdown', () => { 31 | render(); 32 | expect(screen.getByTestId('fcc-container')).toBeInTheDocument(); 33 | expect(screen.getByText('Days')).toBeInTheDocument(); 34 | expect(screen.getByText('Hours')).toBeInTheDocument(); 35 | expect(screen.getByText('Minutes')).toBeInTheDocument(); 36 | expect(screen.getByText('Seconds')).toBeInTheDocument(); 37 | }); 38 | 39 | test('should instant render the completed component (children)', () => { 40 | const { container } = render( 41 | 42 |
Completed
43 |
44 | ); 45 | expect(() => screen.getByTestId('fcc-container')).toThrow(); 46 | expect(container.textContent).toBe('Completed'); 47 | }); 48 | 49 | test('should not render completed component if no children set and hideOnComplete is false', () => { 50 | render(); 51 | expect(screen.getByTestId('fcc-container')).toBeInTheDocument(); 52 | 53 | cleanup(); 54 | render(); 55 | act(() => { 56 | jest.advanceTimersByTime(6000); 57 | }); 58 | expect(screen.getByTestId('fcc-container')).toBeInTheDocument(); 59 | }); 60 | 61 | test('should render the countdown and completed component when the countdown is completed', async () => { 62 | render( 63 | 64 |
Completed
65 |
66 | ); 67 | expect(screen.getByTestId('fcc-container')).toBeInTheDocument(); 68 | expect(() => screen.getByText('Completed')).toThrow(); 69 | act(() => { 70 | jest.advanceTimersByTime(5000); 71 | }); 72 | expect(screen.getByText('Completed')).toBeInTheDocument(); 73 | expect(() => screen.getByTestId('fcc-container')).toThrow(); 74 | }); 75 | 76 | test('should render the countdown with custom styles', () => { 77 | render( 78 | 87 | ); 88 | const container = screen.getByTestId('fcc-container'); 89 | expect(container).toBeInTheDocument(); 90 | expect(container).toHaveStyle('--fcc-spacing: 10px'); 91 | expect(container).toHaveStyle('--fcc-flip-duration: 0.5s'); 92 | expect(container).toHaveStyle('--fcc-digit-block-width: 40px'); 93 | expect(container).toHaveStyle('--fcc-digit-block-height: 60px'); 94 | expect(container).toHaveStyle('--fcc-digit-block-radius: 5px'); 95 | expect(container).toHaveStyle('--fcc-digit-block-spacing: 6px'); 96 | expect(container).toHaveStyle('--fcc-digit-font-size: 30px'); 97 | expect(container).toHaveStyle('--fcc-digit-color: red'); 98 | expect(container).toHaveStyle('--fcc-divider-color: red'); 99 | expect(container).toHaveStyle('--fcc-divider-height: 1px'); 100 | expect(container).toHaveStyle('--fcc-label-font-size: 10px'); 101 | expect(container).toHaveStyle('--fcc-separator-size: 6px'); 102 | expect(container).toHaveStyle('--fcc-separator-color: red'); 103 | 104 | const dLabel = screen.getByText('Days'); 105 | expect(dLabel).toBeInTheDocument(); 106 | expect(dLabel).toHaveStyle('font-weight: 500'); 107 | expect(dLabel).toHaveStyle('text-transform: uppercase'); 108 | }); 109 | 110 | test('should render the countdown with default labels', () => { 111 | render(); 112 | expect(screen.getByText('Days')).toBeInTheDocument(); 113 | expect(screen.getByText('Hours')).toBeInTheDocument(); 114 | expect(screen.getByText('Minutes')).toBeInTheDocument(); 115 | expect(screen.getByText('Seconds')).toBeInTheDocument(); 116 | 117 | cleanup(); 118 | // @ts-ignore 119 | render(); 120 | expect(() => screen.getByText('D')).toThrow(); 121 | expect(() => screen.getByText('H')).toThrow(); 122 | expect(screen.getByText('Days')).toBeInTheDocument(); 123 | expect(screen.getByText('Hours')).toBeInTheDocument(); 124 | expect(screen.getByText('Minutes')).toBeInTheDocument(); 125 | expect(screen.getByText('Seconds')).toBeInTheDocument(); 126 | }); 127 | 128 | test('should render the countdown with custom labels', () => { 129 | render(); 130 | expect(() => screen.getByText('Days')).toThrow(); 131 | expect(() => screen.getByText('Hours')).toThrow(); 132 | expect(() => screen.getByText('Minutes')).toThrow(); 133 | expect(() => screen.getByText('Seconds')).toThrow(); 134 | expect(screen.getByText('D')).toBeInTheDocument(); 135 | expect(screen.getByText('H')).toBeInTheDocument(); 136 | expect(screen.getByText('M')).toBeInTheDocument(); 137 | expect(screen.getByText('S')).toBeInTheDocument(); 138 | 139 | cleanup(); 140 | render( 141 | // @ts-ignore 142 | 143 | ); 144 | expect(screen.getByText('D')).toBeInTheDocument(); 145 | expect(screen.getByText('H')).toBeInTheDocument(); 146 | expect(screen.getByText('M')).toBeInTheDocument(); 147 | expect(screen.getByText('S')).toBeInTheDocument(); 148 | expect(() => screen.getByText('MS')).toThrow(); 149 | }); 150 | 151 | test('should render the countdown with no labels', () => { 152 | render(); 153 | expect(() => screen.getByText('Days')).toThrow(); 154 | expect(() => screen.getByText('Hours')).toThrow(); 155 | expect(() => screen.getByText('Minutes')).toThrow(); 156 | expect(() => screen.getByText('Seconds')).toThrow(); 157 | }); 158 | 159 | test('should render the countdown with no separators', () => { 160 | render(); 161 | expect(screen.getByTestId('fcc-container')).toHaveStyle('--fcc-separator-color: transparent'); 162 | }); 163 | 164 | test('should render the countdown with separators', () => { 165 | render(); 166 | const container = screen.getByTestId('fcc-container'); 167 | expect(container).not.toHaveStyle('--fcc-separator-color: transparent'); 168 | }); 169 | 170 | test('show/hide section works', () => { 171 | render( 172 | 173 | ); 174 | const container = screen.getByTestId('fcc-container'); 175 | expect(container.children.length).toEqual(3 + 2); // 3 rendered sections and 2 separators 176 | expect(screen.getByText('Days')).toBeInTheDocument(); 177 | expect(screen.getByText('Hours')).toBeInTheDocument(); 178 | expect(screen.getByText('Minutes')).toBeInTheDocument(); 179 | expect(() => screen.getByText('Seconds')).toThrow(); 180 | 181 | // renderMap reset to default [true, true, true, true] 182 | cleanup(); 183 | // @ts-ignore 184 | render(); 185 | const container2 = screen.getByTestId('fcc-container'); 186 | expect(container2.children.length).toEqual(4 + 3); 187 | expect(screen.getByText('Days')).toBeInTheDocument(); 188 | 189 | cleanup(); 190 | render( 191 | 196 | ); 197 | const container3 = screen.getByTestId('fcc-container'); 198 | expect(container3.children.length).toEqual(2 + 1); 199 | expect(() => screen.getByText('Days')).toThrow(); 200 | expect(() => screen.getByText('Minutes')).toThrow(); 201 | }); 202 | -------------------------------------------------------------------------------- /src/FlipClockCountDown.tsx: -------------------------------------------------------------------------------- 1 | import styles from './styles.module.css'; 2 | // 3 | import clsx from 'clsx'; 4 | import React, { useEffect, useMemo } from 'react'; 5 | import FlipClockDigit from './FlipClockDigit'; 6 | import { FlipClockCountdownProps, FlipClockCountdownState, FlipClockCountdownUnitTimeFormatted } from './types'; 7 | import { calcTimeDelta, convertToPx, isServer, parseTimeDelta } from './utils'; 8 | 9 | const defaultRenderMap = [true, true, true, true]; 10 | const defaultLabels = ['Days', 'Hours', 'Minutes', 'Seconds']; 11 | 12 | /** 13 | * A 3D animated flip clock countdown component for React. 14 | */ 15 | function FlipClockCountdown(props: FlipClockCountdownProps) { 16 | const { 17 | to, 18 | className, 19 | style, 20 | children, 21 | onComplete = () => {}, 22 | onTick = () => {}, 23 | showLabels = true, 24 | showSeparators = true, 25 | labels = defaultLabels, 26 | labelStyle, 27 | digitBlockStyle, 28 | separatorStyle, 29 | dividerStyle, 30 | duration = 0.7, 31 | renderMap = defaultRenderMap, 32 | hideOnComplete = true, 33 | stopOnHiddenVisibility = false, 34 | renderOnServer = false, 35 | spacing, 36 | ...other 37 | } = props; 38 | // eslint-disable-next-line @typescript-eslint/no-use-before-define 39 | const [state, setState] = React.useState(constructState); 40 | const countdownRef = React.useRef(0); 41 | 42 | function clearTimer() { 43 | window.clearInterval(countdownRef.current); 44 | } 45 | 46 | function constructState(): FlipClockCountdownState { 47 | const timeDelta = calcTimeDelta(to); 48 | return { 49 | timeDelta, 50 | completed: timeDelta.total === 0 51 | }; 52 | } 53 | 54 | function tick() { 55 | const newState = constructState(); 56 | setState(newState); 57 | onTick(newState); 58 | if (newState.completed) { 59 | clearTimer(); 60 | onComplete(); 61 | } 62 | } 63 | 64 | useEffect(() => { 65 | if (stopOnHiddenVisibility) { 66 | const visibilityChangeHandler = () => { 67 | if (document.visibilityState === 'visible') { 68 | tick(); 69 | countdownRef.current = window.setInterval(tick, 1000); 70 | } else { 71 | clearTimer(); 72 | } 73 | }; 74 | visibilityChangeHandler(); 75 | document.addEventListener('visibilitychange', visibilityChangeHandler); 76 | return () => { 77 | clearTimer(); 78 | document.removeEventListener('visibilitychange', visibilityChangeHandler); 79 | }; 80 | } else { 81 | clearTimer(); 82 | tick(); 83 | countdownRef.current = window.setInterval(tick, 1000); 84 | return () => { 85 | clearTimer(); 86 | }; 87 | } 88 | }, [to, stopOnHiddenVisibility]); 89 | 90 | const containerStyles = useMemo(() => { 91 | const s = { 92 | '--fcc-flip-duration': `${duration}s`, 93 | '--fcc-spacing': convertToPx(spacing?.clock), 94 | '--fcc-digit-block-width': convertToPx(digitBlockStyle?.width), 95 | '--fcc-digit-block-height': convertToPx(digitBlockStyle?.height), 96 | '--fcc-digit-block-radius': convertToPx(digitBlockStyle?.borderRadius), 97 | '--fcc-digit-block-spacing': convertToPx(spacing?.digitBlock), 98 | '--fcc-shadow': digitBlockStyle?.boxShadow, 99 | '--fcc-digit-font-size': convertToPx(digitBlockStyle?.fontSize), 100 | '--fcc-digit-color': digitBlockStyle?.color, 101 | '--fcc-label-font-size': convertToPx(labelStyle?.fontSize), 102 | '--fcc-label-color': labelStyle?.color, 103 | '--fcc-divider-color': dividerStyle?.color, 104 | '--fcc-divider-height': convertToPx(dividerStyle?.height), 105 | '--fcc-background': digitBlockStyle?.background || digitBlockStyle?.backgroundColor, 106 | '--fcc-separator-size': convertToPx(separatorStyle?.size), 107 | '--fcc-separator-color': showSeparators ? separatorStyle?.color : 'transparent', 108 | ...style 109 | }; 110 | 111 | return s; 112 | }, [style, digitBlockStyle, labelStyle, duration, dividerStyle, separatorStyle, showSeparators, spacing]); 113 | 114 | const _digitBlockStyle = React.useMemo(() => { 115 | if (digitBlockStyle) { 116 | return { 117 | ...digitBlockStyle, 118 | background: undefined, 119 | backgroundColor: undefined, 120 | width: undefined, 121 | height: undefined, 122 | boxShadow: undefined, 123 | fontSize: undefined, 124 | color: undefined, 125 | borderRadius: undefined 126 | }; 127 | } 128 | return undefined; 129 | }, [digitBlockStyle]); 130 | 131 | const sections = React.useMemo(() => { 132 | const formatted = parseTimeDelta(state.timeDelta); 133 | const _renderMap = renderMap.length >= 4 ? renderMap.slice(0, 4) : defaultRenderMap; 134 | const _labels = labels.length >= 4 ? labels.slice(0, 4) : defaultLabels; 135 | const times = Object.values(formatted) as FlipClockCountdownUnitTimeFormatted[]; 136 | const keys = ['day', 'hour', 'minute', 'second']; 137 | return _renderMap 138 | .map<[boolean, string, FlipClockCountdownUnitTimeFormatted, string]>((show, i) => { 139 | return [show, keys[i], times[i], _labels[i]]; 140 | }) 141 | .filter((item) => item[0]); 142 | }, [renderMap, state]); 143 | 144 | if (state?.completed && hideOnComplete) { 145 | return {children}; 146 | } 147 | 148 | if (!renderOnServer && isServer()) { 149 | return ; 150 | } 151 | 152 | return ( 153 |
166 | {sections.map(([render, key, item, label], idx) => { 167 | if (!render) return null; 168 | return ( 169 | 170 |
171 | {showLabels && ( 172 |
173 | {label} 174 |
175 | )} 176 | {item.current.map((cItem, cIdx) => ( 177 | 184 | ))} 185 |
186 | {idx < sections.length - 1 &&
} 187 |
188 | ); 189 | })} 190 |
191 | ); 192 | } 193 | 194 | export default FlipClockCountdown; 195 | -------------------------------------------------------------------------------- /src/FlipClockDigit.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import React from 'react'; 3 | import styles from './styles.module.css'; 4 | import { Digit, FlipClockCountdownProps } from './types'; 5 | 6 | export interface FlipClockDigitProps { 7 | current: Digit; 8 | next: Digit; 9 | className?: string; 10 | style?: FlipClockCountdownProps['digitBlockStyle']; 11 | } 12 | 13 | type FlipClockDigitState = { 14 | current: Digit; 15 | next: Digit; 16 | }; 17 | 18 | export default function FlipClockDigit(props: FlipClockDigitProps) { 19 | const { current, next, className, style } = props; 20 | const [digit, setDigit] = React.useState({ current, next }); 21 | const [flipped, setFlipped] = React.useState(false); 22 | 23 | React.useEffect(() => { 24 | if (digit.current !== current) { 25 | if (digit.current === digit.next) { 26 | setDigit({ ...digit, next }); 27 | } 28 | setFlipped(true); 29 | } else { 30 | setFlipped(false); 31 | } 32 | }, [current, next]); 33 | 34 | const handleTransitionEnd = (): void => { 35 | setDigit({ current, next }); 36 | setFlipped(false); 37 | }; 38 | 39 | return ( 40 |
41 |
{digit.next}
42 |
{digit.current}
43 |
44 |
{digit.current}
45 |
{digit.next}
46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Default CSS definition for typescript, 3 | * will be overridden with file-specific definitions by rollup 4 | */ 5 | declare module '*.css' { 6 | const content: { [className: string]: string }; 7 | export default content; 8 | } 9 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import FlipClockCountdown from './FlipClockCountDown'; 2 | export * from './types'; 3 | export default FlipClockCountdown; 4 | -------------------------------------------------------------------------------- /src/overrides.d.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 2 | import type * as CSS from 'csstype'; 3 | 4 | // extend css properties 5 | // Ref: https://github.com/frenic/csstype#what-should-i-do-when-i-get-type-errors 6 | declare module 'csstype' { 7 | interface Properties { 8 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 9 | [customCssName: string]: any; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/styles.module.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --fcc-flip-duration: 0.7s; 3 | --fcc-spacing: 8px; 4 | --fcc-digit-block-width: 46px; 5 | --fcc-digit-block-height: 80px; 6 | --fcc-digit-block-radius: 4px; 7 | --fcc-digit-block-spacing: 4px; 8 | --fcc-digit-font-size: 50px; 9 | --fcc-label-font-size: 16px; 10 | --fcc-label-color: inherit; 11 | --fcc-background: #0f181a; 12 | --fcc-digit-color: #ffffff; 13 | --fcc-divider-color: #ffffff66; 14 | --fcc-divider-height: 1px; 15 | --fcc-shadow: 0 0 2px 1px rgba(0, 0, 0, 0.1); 16 | --fcc-separator-size: 5px; 17 | --fcc-separator-color: currentColor; 18 | } 19 | 20 | .fcc__container { 21 | font-family: inherit; 22 | -webkit-user-select: none; 23 | -moz-user-select: none; 24 | user-select: none; 25 | cursor: default; 26 | display: flex; 27 | align-items: center; 28 | gap: var(--fcc-spacing); 29 | } 30 | 31 | .fcc__label_show .fcc__digit_block_container { 32 | margin-bottom: calc(2 * var(--fcc-label-font-size)); 33 | } 34 | 35 | .fcc__digit_block_container .fcc__digit_block:not(:last-child) { 36 | margin-right: var(--fcc-digit-block-spacing); 37 | } 38 | 39 | .fcc__digit_block_container { 40 | position: relative; 41 | display: flex; 42 | align-items: center; 43 | } 44 | 45 | .fcc__digit_block_label { 46 | color: var(--fcc-label-color); 47 | line-height: 1; 48 | font-weight: 400; 49 | font-size: var(--fcc-label-font-size); 50 | position: absolute; 51 | bottom: 0; 52 | left: 50%; 53 | transform: translate(-50%, 150%); 54 | } 55 | 56 | .fcc__digit_block { 57 | perspective: 200px; 58 | position: relative; 59 | font-size: var(--fcc-digit-font-size); 60 | color: var(--fcc-digit-color); 61 | font-weight: 500; 62 | line-height: 0; 63 | width: var(--fcc-digit-block-width); 64 | height: var(--fcc-digit-block-height); 65 | box-shadow: var(--fcc-shadow); 66 | border-radius: var(--fcc-digit-block-radius); 67 | } 68 | 69 | .fcc__current_below, 70 | .fcc__next_above { 71 | position: absolute; 72 | width: 100%; 73 | height: 50%; 74 | overflow: hidden; 75 | display: flex; 76 | justify-content: center; 77 | background: var(--fcc-background); 78 | } 79 | 80 | .fcc__next_above { 81 | align-items: flex-end; 82 | top: 0; 83 | border-top-left-radius: inherit; 84 | border-top-right-radius: inherit; 85 | border-bottom: var(--fcc-divider-height) solid var(--fcc-divider-color); 86 | } 87 | 88 | .fcc__current_below { 89 | align-items: flex-start; 90 | bottom: 0; 91 | border-bottom-left-radius: inherit; 92 | border-bottom-right-radius: inherit; 93 | } 94 | 95 | .fcc__card { 96 | position: relative; 97 | z-index: 2; 98 | width: 100%; 99 | height: 50%; 100 | transform-style: preserve-3d; 101 | transform-origin: bottom; 102 | transform: rotateX(0); 103 | border-radius: inherit; 104 | } 105 | 106 | .fcc__card.fcc__flipped { 107 | transition: transform var(--fcc-flip-duration) ease-in-out; 108 | transform: rotateX(-180deg); 109 | } 110 | 111 | .fcc__card_face { 112 | position: absolute; 113 | width: 100%; 114 | height: 100%; 115 | display: flex; 116 | justify-content: center; 117 | overflow: hidden; 118 | backface-visibility: hidden; 119 | background: var(--fcc-background); 120 | } 121 | 122 | .fcc__card_face_front { 123 | align-items: flex-end; 124 | border-top-left-radius: inherit; 125 | border-top-right-radius: inherit; 126 | border-bottom: var(--fcc-divider-height) solid var(--fcc-divider-color); 127 | } 128 | 129 | .fcc__card_face_back { 130 | align-items: flex-start; 131 | transform: rotateX(-180deg); 132 | border-bottom-left-radius: inherit; 133 | border-bottom-right-radius: inherit; 134 | } 135 | 136 | .fcc__colon { 137 | height: var(--fcc-digit-block-height); 138 | display: flex; 139 | flex-direction: column; 140 | justify-content: center; 141 | align-items: center; 142 | } 143 | 144 | .fcc__label_show .fcc__colon { 145 | margin-bottom: calc(2 * var(--fcc-label-font-size)); 146 | } 147 | 148 | .fcc__colon::before, 149 | .fcc__colon::after { 150 | content: ""; 151 | width: var(--fcc-separator-size); 152 | height: var(--fcc-separator-size); 153 | border-radius: 50%; 154 | background-color: var(--fcc-separator-color); 155 | } 156 | 157 | .fcc__colon::before { 158 | margin-bottom: var(--fcc-separator-size); 159 | } 160 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export type Digit = number | string; 4 | 5 | export interface FlipClockCountdownUnitTimeFormatted { 6 | readonly current: Digit[]; 7 | readonly next: Digit[]; 8 | } 9 | 10 | export interface FlipClockCountdownTimeDeltaFormatted { 11 | readonly days: FlipClockCountdownUnitTimeFormatted; 12 | readonly hours: FlipClockCountdownUnitTimeFormatted; 13 | readonly minutes: FlipClockCountdownUnitTimeFormatted; 14 | readonly seconds: FlipClockCountdownUnitTimeFormatted; 15 | } 16 | 17 | export interface FlipClockCountdownTimeDelta { 18 | readonly total: number; 19 | readonly days: number; 20 | readonly hours: number; 21 | readonly minutes: number; 22 | readonly seconds: number; 23 | } 24 | 25 | export interface FlipClockCountdownState { 26 | readonly timeDelta: FlipClockCountdownTimeDelta; 27 | readonly completed: boolean; 28 | } 29 | 30 | export type FlipClockCountdownTimeDeltaFn = (props: FlipClockCountdownState) => void; 31 | 32 | export interface FlipClockCountdownProps 33 | extends React.DetailedHTMLProps, HTMLDivElement> { 34 | readonly to: Date | number | string; 35 | 36 | /** 37 | * By default, the countdown will be hidden when it completed (or show children if provided). 38 | * This will keep the timer in place and stuck at zeros when the countdown is completed. 39 | */ 40 | hideOnComplete?: boolean; 41 | /** 42 | * @deprecated 43 | * Props to be passed to div element that is container for all elements. 44 | * You can use this if you want to style or select the whole container. 45 | */ 46 | readonly containerProps?: React.DetailedHTMLProps, HTMLDivElement>; 47 | /** 48 | * A callback will be called when countdown completed. 49 | */ 50 | readonly onComplete?: () => void; 51 | /** 52 | * A callback will be called every second. 53 | * 54 | * @param timeDelta 55 | * @param completed represents the state of the countdown. `true` if the countdown ended, otherwise `false`. 56 | */ 57 | readonly onTick?: FlipClockCountdownTimeDeltaFn; 58 | /** 59 | * Each element represents the render state of each section (day, hour, minute, second). 60 | * 61 | * If `true` section will be rendered, `false` otherwise. 62 | * 63 | * @default [true, true, true, true] 64 | */ 65 | readonly renderMap?: [boolean, boolean, boolean, boolean]; 66 | /** 67 | * An array of labels used to represent information for each section (day, hour, minute, second). 68 | * 69 | * @default ['Days', 'Hours', 'Minutes', 'Seconds'] 70 | */ 71 | readonly labels?: [string, string, string, string]; 72 | /** 73 | * Set it to `false` if you don't want to show the labels. 74 | * 75 | * @default true 76 | */ 77 | readonly showLabels?: boolean; 78 | /** 79 | * Set it to `false` if you don't want to show the separators (colon) between time unit. 80 | * 81 | * @default true 82 | */ 83 | readonly showSeparators?: boolean; 84 | /** 85 | * The style will be applied to labels like `font-size`, `color`, etc. 86 | */ 87 | labelStyle?: React.CSSProperties; 88 | /** 89 | * The style will be applied to digit blocks like `font-size`, `color`, `width`, `height`, etc. 90 | */ 91 | digitBlockStyle?: React.CSSProperties; 92 | /** 93 | * The style will be applied to separator (colon), includes `size` and `color`. 94 | */ 95 | separatorStyle?: { 96 | color?: React.CSSProperties['color']; 97 | size?: number | string; 98 | }; 99 | /** 100 | * The style will be applied to divider, includes `color` and `height`. 101 | */ 102 | dividerStyle?: { 103 | color?: React.CSSProperties['color']; 104 | height?: React.CSSProperties['borderBottomWidth']; 105 | }; 106 | /** 107 | * Duration (in second) when flip card. Valid value in range (0, 1). 108 | * 109 | * @default 0.7 110 | */ 111 | duration?: number; 112 | /** 113 | * Whether or not to stop the clock when the visibilityState is hidden, 114 | * enabling this feature will prevent the component gets derailed if we 115 | * switch between browser tabs. 116 | * 117 | * @default false 118 | */ 119 | stopOnHiddenVisibility?: boolean; 120 | /** 121 | * Custom clock spacing. 122 | */ 123 | spacing?: { 124 | /** 125 | * Space between unit times and separators. 126 | */ 127 | clock?: number | string; 128 | /** 129 | * Space between blocks in each unit of time. 130 | */ 131 | digitBlock?: number | string; 132 | }; 133 | /** 134 | * Whether or not to render the clock on server. 135 | * 136 | * @default false 137 | */ 138 | renderOnServer?: boolean; 139 | } 140 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { Digit, FlipClockCountdownTimeDelta, FlipClockCountdownTimeDeltaFormatted } from './types'; 2 | 3 | export const defaultTimeDelta = { 4 | total: 0, 5 | days: 0, 6 | hours: 0, 7 | minutes: 0, 8 | seconds: 0 9 | }; 10 | 11 | export function calcTimeDelta(target: Date | number | string): FlipClockCountdownTimeDelta { 12 | const date = new Date(target); 13 | if (isNaN(date.getTime())) { 14 | throw Error('Invalid date'); 15 | } 16 | const now = Date.now(); 17 | let timeLeft = Math.round((date.getTime() - now) / 1000); // convert to seconds 18 | if (timeLeft < 0) timeLeft = 0; 19 | 20 | return { 21 | total: timeLeft, 22 | days: Math.floor(timeLeft / (24 * 60 * 60)), 23 | hours: Math.floor((timeLeft / 3600) % 24), 24 | minutes: Math.floor((timeLeft / 60) % 60), 25 | seconds: Math.floor(timeLeft % 60) 26 | }; 27 | } 28 | 29 | export function pad(n: number): Digit[] { 30 | return ('0'.repeat(Math.max(0, 2 - String(n).length)) + String(n)).split(''); 31 | } 32 | 33 | export function parseTimeDelta(timeDelta: FlipClockCountdownTimeDelta): FlipClockCountdownTimeDeltaFormatted { 34 | const nextTimeDelta = calcTimeDelta(new Date().getTime() + (timeDelta.total - 1) * 1000); 35 | 36 | return { 37 | days: { 38 | current: pad(timeDelta.days), 39 | next: pad(nextTimeDelta.days) 40 | }, 41 | hours: { 42 | current: pad(timeDelta.hours), 43 | next: pad(nextTimeDelta.hours) 44 | }, 45 | minutes: { 46 | current: pad(timeDelta.minutes), 47 | next: pad(nextTimeDelta.minutes) 48 | }, 49 | seconds: { 50 | current: pad(timeDelta.seconds), 51 | next: pad(nextTimeDelta.seconds) 52 | } 53 | }; 54 | } 55 | 56 | export function convertToPx(n?: string | number): string | undefined { 57 | if (n === undefined) return undefined; 58 | if (typeof n === 'string') return n; 59 | return `${n}px`; 60 | } 61 | 62 | export function isServer() { 63 | return typeof window === 'undefined'; 64 | } 65 | 66 | export function isClient() { 67 | return !isServer(); 68 | } 69 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "esnext", 5 | "lib": ["dom", "esnext"], 6 | "moduleResolution": "node", 7 | "jsx": "react", 8 | "sourceMap": true, 9 | "declaration": true, 10 | "esModuleInterop": true, 11 | "noImplicitReturns": true, 12 | "noImplicitThis": true, 13 | "noImplicitAny": true, 14 | "strictNullChecks": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "allowSyntheticDefaultImports": true, 18 | "target": "es5", 19 | "allowJs": true, 20 | "skipLibCheck": true, 21 | "strict": true, 22 | "forceConsistentCasingInFileNames": true, 23 | "resolveJsonModule": true, 24 | "isolatedModules": true, 25 | "noEmit": true, 26 | "noFallthroughCasesInSwitch": true 27 | }, 28 | "include": ["src"], 29 | "exclude": ["node_modules", "dist", "examples", "resources", "src/utils.ts", "**/*.spec.tsx", "**/*.spec.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /tsconfig.test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs" 5 | } 6 | } --------------------------------------------------------------------------------