├── .babelrc ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── build └── index.js ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js ├── paths.js ├── polyfills.js ├── webpack.config.dev.js ├── webpack.config.prod.demo.js ├── webpack.config.prod.js └── webpackDevServer.config.js ├── package-lock.json ├── package.json ├── public └── index.html ├── scripts ├── build-demo.js ├── build.js ├── start.js └── test.js └── src ├── demo ├── App.js ├── github_logo.png ├── index.css └── index.js └── lib ├── components ├── End │ ├── After.js │ ├── OnDate.js │ └── index.js ├── ReactRRuleGenerator.js ├── Repeat │ ├── Daily │ │ └── index.js │ ├── Hourly │ │ └── index.js │ ├── Monthly │ │ ├── On.js │ │ ├── OnThe.js │ │ └── index.js │ ├── Weekly │ │ └── index.js │ ├── Yearly │ │ ├── On.js │ │ ├── OnThe.js │ │ └── index.js │ └── index.js └── Start │ ├── OnDate.js │ └── index.js ├── constants └── index.js ├── index.js ├── styles ├── index.css └── react-datetime.css ├── translations ├── english.js ├── german.js └── index.js └── utils ├── computeRRule ├── fromString │ ├── computeDailyInterval.js │ ├── computeEndAfter.js │ ├── computeEndMode.js │ ├── computeEndOnDate.js │ ├── computeFrequency.js │ ├── computeHourlyInterval.js │ ├── computeMonthlyInterval.js │ ├── computeMonthlyMode.js │ ├── computeMonthlyOnDay.js │ ├── computeMonthlyOnTheDay.js │ ├── computeMonthlyOnTheWhich.js │ ├── computeRRule.js │ ├── computeStartOnDate.js │ ├── computeWeekStartDay.js │ ├── computeWeeklyDays.js │ ├── computeWeeklyInterval.js │ ├── computeYearlyMode.js │ ├── computeYearlyOnMonth.js │ ├── computeYearlyOnMonthday.js │ ├── computeYearlyOnTheMonth.js │ ├── computeYearlyOnTheMonthday.js │ └── computeYearlyOnTheWhich.js └── toString ├── configureInitialState.js ├── numericalFieldHandler.js └── translateLabel.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react", "es2015"], 3 | "plugins": [ 4 | "transform-class-properties", 5 | "transform-object-rest-spread" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /src/demo/registerServiceWorker.js 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "standard", 5 | "standard-react", 6 | "airbnb" 7 | ], 8 | "plugins": [ 9 | "react" 10 | ], 11 | "rules": { 12 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 13 | "no-trailing-spaces": ["error", { "skipBlankLines": true }], 14 | "max-len": [2, 120, 2], 15 | "import/no-extraneous-dependencies": 0 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # misc 10 | .DS_Store 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | npm-debug.log* 17 | yarn-debug.log* 18 | yarn-error.log* 19 | 20 | .idea/ 21 | /build/ 22 | /build-demo/ 23 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .idea 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Filip Duczyminski 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 RRule Generator 2 | > Recurrence rules generator form built with React 3 | 4 | [![LICENSE](https://img.shields.io/npm/l/express.svg)](LICENSE) 5 | [![npm](https://img.shields.io/npm/dm/localeval.svg)](https://npm-stat.com/charts.html?package=react-rrule-generator) 6 | 7 | ### This project is no longer maintained by me. Thank you for all your past contributions. Let the forks rock it for you. 8 | 9 | 10 | ![Screenshot](https://i.imgur.com/FU3aGlz.png) 11 | 12 | ## Description 13 | 14 | This is [ReactJS](http://facebook.github.io/react/index.html) project based on [Create React Library](https://github.com/UdiliaInc/create-react-library) and using [Bootstrap](https://github.com/twbs/bootstrap) styling. It's built with the help of a great [rrule.js](https://github.com/jakubroztocil/rrule) library. 15 | 16 | It also uses: 17 | * [lodash](https://github.com/lodash/lodash) 18 | * [moment](https://github.com/moment/moment) 19 | * [react-datetime](https://github.com/YouCanBookMe/react-datetime) 20 | 21 | ## Demo 22 | https://fafruch.github.io/react-rrule-generator 23 | 24 | ## Installation 25 | 26 | `npm install --save react-rrule-generator` 27 | 28 | ## Usage 29 | 30 | In your CSS index file don't forget to import styles: 31 | ```css 32 | @import '~bootstrap/dist/css/bootstrap.css'; // this lib uses boostrap (v. 4.0.0-beta.2) 33 | @import '~react-rrule-generator/build/styles.css'; // react-rrule-generator's custom CSS 34 | ``` 35 | 36 | Then you're good to go. 37 | Just use it: 38 | 39 | ```js 40 | import RRuleGenerator from 'react-rrule-generator'; 41 | 42 | // render it as it is 43 | 44 | const SimpleRender = () => ( 45 | console.log(`RRule changed, now it's ${rrule}`)} /> 46 | ); 47 | 48 | 49 | // or with your own forms configuration 50 | 51 | import MyCustomCalendar from './MyCustomCalendar'; 52 | 53 | const CustomizedRender = () => ( 54 | console.log(`RRule changed, now it's ${rrule}`)} 56 | config={{ 57 | repeat: ['Monthly', 'Weekly'], 58 | yearly: 'on the', 59 | monthly: 'on', 60 | end: ['Never', 'On date'], 61 | weekStartsOnSunday: true, 62 | hideError: true, 63 | }} 64 | customCalendar={MyCustomCalendar} 65 | /> 66 | ); 67 | 68 | 69 | // you can also use it as controlled input component and feed it with your own RRule! 70 | 71 | class ControlledRender extends Component { 72 | state = { 73 | rrule: 'SOME REALLY COOL RRULE' 74 | }; 75 | 76 | render() { 77 | return ( 78 | this.setState({ rrule })} 80 | value={this.state.rrule} 81 | /> 82 | ); 83 | } 84 | } 85 | ``` 86 | 87 | ## API 88 | 89 | ### Props 90 | 91 | | Name | Type | Description | 92 | | ------------ | ------- | ----------- | 93 | | **onChange** | `function` | REQUIRED. Callback trigger when the RRule changes. The callback receives newly generated RRule `string`. 94 | | **value** | `string` | You can pass your own RRule value to RRuleGenerator and use it like controlled input component. 95 | | **config** | `object` | Accepts object of what options will be rendered. This object's structure is described in [#config](#config) | 96 | | **translations** | `function` or `object` | Accepts a function or an object with translations for all labels in the component. By default all labels are in English. You can pass your own translation object or function, which has the following signature: `(key: string, replacements: object) => string`. It receives key of the label in form of `'repeat.yearly.on_the'` and an object for placeholder replacements, e.g., `{ value: error.value }`. Example translation objects are placed in `/src/lib/translations/`. | 97 | | **customCalendar** | `React Component` or `stateless function` | This accepts custom calendar / datepicker for choosing a date in EndOnDate view. It receives following props by default:
  • `'aria-label'` with value `'Datetime picker for end on date'`,
  • `value` - date value consumed by app logic,
  • `dateFormat` - by default `'YYYY-MM-DD'`,
  • `locale` - `'en/ca'` or `'en/gb'` depending on if `weekStartsOnSunday` in config is set to `true` or `false`
  • 98 |
    99 | 100 | ### config 101 | `config` is an object which accepts following: 102 | 103 | | Name | Type | Description | 104 | | ------------ | ------- | ----------- | 105 | | **frequency** | `array` of `string` | You can optionally choose if you want to show repeating options `'Yearly'`, `'Monthly'`, `'Weekly'`, `'Daily'`, `'Hourly'`. You can pass for example `['Monthly', 'Weekly']` if you want to show only options for repeating monthly and weekly. | 106 | | **yearly** | `string` | If `'on'` provided, only choosing a particular day of a month is available, if `'on the'` is provided, you have ability to choose for example 'fourth Wednesday of February' | 107 | | **monthly** | `string` | If `'on'` provided, only choosing a particular day of a month is available, if `'on the'` is provided, you have ability to choose for example 'fourth Wednesday' | 108 | | **end** | `array` of `string` | You can optionally choose if you want to show ending options `'Never'`, `'After'`, `'On date'`. You can pass for example `['Never', 'On date']` if you want to show only options for ending never or on a particular date without showint 'After' option. | 109 | | **hideStart** | `boolean` | If `true` start date form is not rendered. Default: `true` | 110 | | **hideEnd** | `boolean` | If `true` ending form is not rendered. Default: `false` | 111 | | **hideError** | `boolean` | If `true` error alert is not rendered. Default: `false` | 112 | | **weekStartsOnSunday** | `boolean` | If set to `true`, weeks starts on Sunday (both for views and RRule string). Default: `false` | 113 | 114 | ## License 115 | MIT 116 | -------------------------------------------------------------------------------- /config/env.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const paths = require('./paths'); 6 | 7 | // Make sure that including paths.js after env.js will read .env variables. 8 | delete require.cache[require.resolve('./paths')]; 9 | 10 | const NODE_ENV = process.env.NODE_ENV; 11 | if (!NODE_ENV) { 12 | throw new Error( 13 | 'The NODE_ENV environment variable is required but was not specified.' 14 | ); 15 | } 16 | 17 | // https://github.com/bkeepers/dotenv#what-other-env-files-can-i-use 18 | var dotenvFiles = [ 19 | `${paths.dotenv}.${NODE_ENV}.local`, 20 | `${paths.dotenv}.${NODE_ENV}`, 21 | // Don't include `.env.local` for `test` environment 22 | // since normally you expect tests to produce the same 23 | // results for everyone 24 | NODE_ENV !== 'test' && `${paths.dotenv}.local`, 25 | paths.dotenv, 26 | ].filter(Boolean); 27 | 28 | // Load environment variables from .env* files. Suppress warnings using silent 29 | // if this file is missing. dotenv will never modify any environment variables 30 | // that have already been set. 31 | // https://github.com/motdotla/dotenv 32 | dotenvFiles.forEach(dotenvFile => { 33 | if (fs.existsSync(dotenvFile)) { 34 | require('dotenv').config({ 35 | path: dotenvFile, 36 | }); 37 | } 38 | }); 39 | 40 | // We support resolving modules according to `NODE_PATH`. 41 | // This lets you use absolute paths in imports inside large monorepos: 42 | // https://github.com/facebookincubator/create-react-app/issues/253. 43 | // It works similar to `NODE_PATH` in Node itself: 44 | // https://nodejs.org/api/modules.html#modules_loading_from_the_global_folders 45 | // Note that unlike in Node, only *relative* paths from `NODE_PATH` are honored. 46 | // Otherwise, we risk importing Node.js core modules into an app instead of Webpack shims. 47 | // https://github.com/facebookincubator/create-react-app/issues/1023#issuecomment-265344421 48 | // We also resolve them to make sure all tools using them work consistently. 49 | const appDirectory = fs.realpathSync(process.cwd()); 50 | process.env.NODE_PATH = (process.env.NODE_PATH || '') 51 | .split(path.delimiter) 52 | .filter(folder => folder && !path.isAbsolute(folder)) 53 | .map(folder => path.resolve(appDirectory, folder)) 54 | .join(path.delimiter); 55 | 56 | // Grab NODE_ENV and REACT_APP_* environment variables and prepare them to be 57 | // injected into the application via DefinePlugin in Webpack configuration. 58 | const REACT_APP = /^REACT_APP_/i; 59 | 60 | function getClientEnvironment(publicUrl) { 61 | const raw = Object.keys(process.env) 62 | .filter(key => REACT_APP.test(key)) 63 | .reduce( 64 | (env, key) => { 65 | env[key] = process.env[key]; 66 | return env; 67 | }, 68 | { 69 | // Useful for determining whether we’re running in production mode. 70 | // Most importantly, it switches React into the correct mode. 71 | NODE_ENV: process.env.NODE_ENV || 'development', 72 | // Useful for resolving the correct path to static assets in `public`. 73 | // For example, . 74 | // This should only be used as an escape hatch. Normally you would put 75 | // images into the `src` and `import` them in code to get their paths. 76 | PUBLIC_URL: publicUrl, 77 | } 78 | ); 79 | // Stringify all values so we can feed into Webpack DefinePlugin 80 | const stringified = { 81 | 'process.env': Object.keys(raw).reduce((env, key) => { 82 | env[key] = JSON.stringify(raw[key]); 83 | return env; 84 | }, {}), 85 | }; 86 | 87 | return { raw, stringified }; 88 | } 89 | 90 | module.exports = getClientEnvironment; 91 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/paths.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const fs = require('fs'); 5 | const url = require('url'); 6 | 7 | // Make sure any symlinks in the project folder are resolved: 8 | // https://github.com/facebookincubator/create-react-app/issues/637 9 | const appDirectory = fs.realpathSync(process.cwd()); 10 | const resolveApp = relativePath => path.resolve(appDirectory, relativePath); 11 | 12 | const envPublicUrl = process.env.PUBLIC_URL; 13 | 14 | function ensureSlash(path, needsSlash) { 15 | const hasSlash = path.endsWith('/'); 16 | if (hasSlash && !needsSlash) { 17 | return path.substr(path, path.length - 1); 18 | } else if (!hasSlash && needsSlash) { 19 | return `${path}/`; 20 | } else { 21 | return path; 22 | } 23 | } 24 | 25 | const getPublicUrl = appPackageJson => 26 | envPublicUrl || require(appPackageJson).homepage; 27 | 28 | // We use `PUBLIC_URL` environment variable or "homepage" field to infer 29 | // "public path" at which the app is served. 30 | // Webpack needs to know it to put the right