├── src ├── react-app-env.d.ts ├── assets │ └── images │ │ └── bg.jpg ├── store │ └── configureStore.ts ├── types │ ├── menubar.ts │ ├── chmodCalculator.ts │ ├── passwordGenerator.ts │ ├── epochConverter.ts │ └── crontabGenerator.ts ├── actions │ ├── CrontabGenerator │ │ ├── generateCrontabLine.ts │ │ ├── commandToExecute.ts │ │ ├── Schedule │ │ │ ├── days.ts │ │ │ ├── hours.ts │ │ │ ├── months.ts │ │ │ ├── minutes.ts │ │ │ └── weekday.ts │ │ └── howToHandleOutput.ts │ ├── EpochConverter │ │ ├── unixTimeForm.ts │ │ ├── currentEpochTime.ts │ │ └── dateTimeForm.ts │ ├── ChmodCalculator │ │ └── chmodCalculator.ts │ └── PasswordGenerator │ │ └── passwordGenerator.ts ├── utils │ ├── validators │ │ ├── emailValidator.ts │ │ ├── passwordValidator.ts │ │ └── chmodValidator.ts │ ├── generators │ │ └── passwordGenerator.ts │ ├── calculators │ │ ├── epochConverter.ts │ │ └── chmodCalculator.ts │ └── transformers │ │ ├── map.json │ │ └── humanReadableToCrontab.ts ├── setupTests.ts ├── reducers │ ├── EpochConverter │ │ ├── index.ts │ │ ├── unixTimeForm.ts │ │ ├── currentEpochTime.ts │ │ └── dateTimeForm.ts │ ├── index.ts │ ├── CrontabGenerator │ │ ├── generateCrontabLine.ts │ │ ├── index.ts │ │ ├── commandToExecute.ts │ │ ├── Schedule │ │ │ ├── days.ts │ │ │ ├── hours.ts │ │ │ ├── months.ts │ │ │ ├── minutes.ts │ │ │ └── weekday.ts │ │ └── howToHandleOutput.ts │ ├── ChmodCalculator │ │ └── index.ts │ └── PasswordGenerator │ │ └── index.ts ├── tests │ ├── App.test.tsx │ ├── containers │ │ ├── EpochConverter │ │ │ ├── DateTimeForm.test.tsx │ │ │ ├── UnixTimeForm.test.tsx │ │ │ └── CurrentEpochTime.test.tsx │ │ ├── Menubar │ │ │ └── Menubar.test.tsx │ │ ├── CrontabGenerator │ │ │ ├── GenerateCrontabLine.test.tsx │ │ │ └── Schedule │ │ │ │ ├── Months.test.tsx │ │ │ │ ├── Days.test.tsx │ │ │ │ ├── Hours.test.tsx │ │ │ │ ├── Minutes.test.tsx │ │ │ │ └── Weekday.test.tsx │ │ ├── ChmodCalculator │ │ │ └── ChmodCalculator.test.tsx │ │ └── PasswordGenerator │ │ │ └── PasswordGenerator.test.tsx │ ├── components │ │ ├── common │ │ │ └── CopyToClipboard.test.tsx │ │ ├── PasswordGenerator │ │ │ ├── FormFields │ │ │ │ ├── PasswordLength.test.tsx │ │ │ │ ├── Checkbox.test.tsx │ │ │ │ └── Checkboxes.test.tsx │ │ │ ├── GenerateAndCopy.test.tsx │ │ │ └── PasswordGenerator.test.tsx │ │ ├── EpochConverter │ │ │ ├── CurrentEpochTime.test.tsx │ │ │ ├── Forms │ │ │ │ ├── UnixTimeForm.test.tsx │ │ │ │ └── DateTimeForm.test.tsx │ │ │ └── EpochConverter.test.tsx │ │ ├── CrontabGenerator │ │ │ ├── FormFields │ │ │ │ ├── HowToHandleOutput │ │ │ │ │ ├── Mute.test.tsx │ │ │ │ │ └── FileOrEmail.test.tsx │ │ │ │ └── Schedule │ │ │ │ │ └── Period.test.tsx │ │ │ └── CrontabGenerator.test.tsx │ │ ├── Menubar │ │ │ └── Menubar.test.tsx │ │ └── ChmodCalculator │ │ │ ├── Checkboxes │ │ │ ├── CheckboxGroup.test.tsx │ │ │ └── Checkbox.test.tsx │ │ │ └── ChmodCalculator..test.tsx │ ├── utils │ │ ├── validators │ │ │ ├── passwordValidator.test.ts │ │ │ ├── emailValidator.test.ts │ │ │ └── chmodValidator.test.ts │ │ ├── generators │ │ │ └── passwordGenerator.test.ts │ │ ├── calculators │ │ │ ├── chmodCalculator.test.ts │ │ │ └── epochConverter.test.ts │ │ └── transformers │ │ │ └── humanReadableToCrontab.test.ts │ ├── reducers │ │ ├── CrontabGenerator │ │ │ ├── generateCrontabLine.test.ts │ │ │ ├── commandToExecute.test.ts │ │ │ ├── Schedule │ │ │ │ ├── days.test.ts │ │ │ │ ├── hourss.test.ts │ │ │ │ ├── minutes.test.ts │ │ │ │ ├── months.test.ts │ │ │ │ └── weekday.test.ts │ │ │ └── howToHandleOutput.test.ts │ │ ├── EpochConverter │ │ │ ├── unixTimeForm.test.ts │ │ │ ├── currentEpochTime.test.ts │ │ │ └── dateTimeForm.test.ts │ │ ├── ChmodCalculator │ │ │ └── index.test.ts │ │ └── PasswordGenerator │ │ │ └── index.test.ts │ └── selectors │ │ └── crontabGenerator.test.ts ├── components │ ├── PasswordGenerator │ │ ├── FormFields │ │ │ ├── PasswordLength.tsx │ │ │ ├── Checkbox.tsx │ │ │ └── Checkboxes.tsx │ │ ├── GenerateAndCopy.tsx │ │ └── PasswordGenerator.tsx │ ├── EpochConverter │ │ ├── EpochConverter.tsx │ │ ├── CurrentEpochTime.tsx │ │ └── Forms │ │ │ ├── UnixTimeForm.tsx │ │ │ └── DateTimeForm.tsx │ ├── CrontabGenerator │ │ ├── FormFields │ │ │ ├── Schedule │ │ │ │ ├── RadioButton.tsx │ │ │ │ ├── RadioButtonList.tsx │ │ │ │ ├── RadioButtonWithMultiSelect.tsx │ │ │ │ └── Period.tsx │ │ │ ├── HowToHandleOutput │ │ │ │ ├── Mute.tsx │ │ │ │ └── FileOrEmail.tsx │ │ │ ├── HowToHandleOutput.tsx │ │ │ └── CommandToExecute.tsx │ │ ├── GenerateCrontabLine.tsx │ │ └── CrontabGenerator.tsx │ ├── ChmodCalculator │ │ ├── Checkboxes │ │ │ ├── CheckboxGroup.tsx │ │ │ ├── Checkbox.tsx │ │ │ └── Checkboxes.tsx │ │ └── ChmodCalculator.tsx │ ├── Menubar │ │ └── Menubar.tsx │ └── common │ │ └── CopyToClipboard.tsx ├── index.css ├── App.css ├── index.tsx ├── containers │ ├── Menubar │ │ └── Menubar.tsx │ ├── CrontabGenerator │ │ ├── CommandToExecute.tsx │ │ ├── HowToHandleOutput.tsx │ │ ├── Schedule │ │ │ ├── Days.tsx │ │ │ ├── Weekday.tsx │ │ │ ├── Minutes.tsx │ │ │ ├── Months.tsx │ │ │ └── Hours.tsx │ │ └── GenerateCrontabLine.tsx │ ├── EpochConverter │ │ ├── UnixTimeForm.tsx │ │ ├── CurrentEpochTime.tsx │ │ └── DateTimeForm.tsx │ ├── ChmodCalculator │ │ └── ChmodCalculator.tsx │ └── PasswordGenerator │ │ └── PasswordGenerator.tsx ├── App.tsx ├── selectors │ └── crontabGenerator.js ├── constants │ └── ActionTypes.ts └── serviceWorker.ts ├── .huskyrc.json ├── public ├── robots.txt ├── favicon.ico ├── logo192.png ├── logo512.png ├── manifest.json └── index.html ├── .lintstagedrc.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── .github └── workflows │ ├── node.js.yml │ └── codeql-analysis.yml ├── LICENSE ├── README.md ├── .eslintrc └── package.json /src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.huskyrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "hooks": { 3 | "pre-commit": "lint-staged" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderberg/web-developer-tools/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderberg/web-developer-tools/HEAD/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderberg/web-developer-tools/HEAD/public/logo512.png -------------------------------------------------------------------------------- /src/assets/images/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coderberg/web-developer-tools/HEAD/src/assets/images/bg.jpg -------------------------------------------------------------------------------- /.lintstagedrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "*.+(js|jsx)": ["eslint --fix", "git add"], 3 | "*.+(json|css|md)": ["prettier --write", "git add"] 4 | } 5 | -------------------------------------------------------------------------------- /src/store/configureStore.ts: -------------------------------------------------------------------------------- 1 | import {configureStore} from '@reduxjs/toolkit' 2 | import rootReducer from "../reducers"; 3 | 4 | const store = configureStore({ 5 | reducer: rootReducer 6 | }) 7 | 8 | export default store 9 | -------------------------------------------------------------------------------- /src/types/menubar.ts: -------------------------------------------------------------------------------- 1 | import {MouseEventHandler} from "react"; 2 | 3 | export type MenubarPropsType = { 4 | model: Array, 5 | onLogoClick: MouseEventHandler, 6 | onGitHubClick: MouseEventHandler 7 | } 8 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/generateCrontabLine.ts: -------------------------------------------------------------------------------- 1 | import {GENERATE_CRONTAB_LINE} from "../../constants/ActionTypes"; 2 | 3 | export const setResult = (payload: string) => ({ 4 | type: GENERATE_CRONTAB_LINE, payload 5 | }); 6 | 7 | export default setResult; 8 | -------------------------------------------------------------------------------- /src/utils/validators/emailValidator.ts: -------------------------------------------------------------------------------- 1 | const isEmailValid = (email: string): boolean => { 2 | const pattern = /^[a-z0-9_.-]+@[a-z0-9-]+\.([a-z]{1,6}\.)?[a-z]{2,6}$/i; 3 | 4 | return email.search(pattern) === 0; 5 | } 6 | 7 | export default isEmailValid; 8 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 4 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /src/utils/validators/passwordValidator.ts: -------------------------------------------------------------------------------- 1 | import {CheckboxesType} from "../../types/passwordGenerator"; 2 | 3 | const isChangeAllowed = (checked: CheckboxesType): boolean => 4 | !(!checked.numbers 5 | && !checked.symbols 6 | && !checked.lowercase 7 | && !checked.uppercase) 8 | 9 | export default isChangeAllowed; 10 | -------------------------------------------------------------------------------- /src/reducers/EpochConverter/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import unixTimeForm from "./unixTimeForm"; 3 | import dateTimeForm from "./dateTimeForm"; 4 | import currentEpochTime from "./currentEpochTime"; 5 | 6 | const epochConverter = combineReducers({ 7 | unixTimeForm, 8 | dateTimeForm, 9 | currentEpochTime, 10 | }) 11 | 12 | export default epochConverter 13 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/commandToExecute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_COMMAND_TO_EXECUTE, 3 | SET_COMMAND_HAS_ERROR 4 | } from "../../constants/ActionTypes"; 5 | 6 | export const setCommandToExecuteValue = (payload: string) => ({ 7 | type: SET_COMMAND_TO_EXECUTE, payload 8 | }); 9 | 10 | export const setCommandHasError = (payload: boolean) => ({ 11 | type: SET_COMMAND_HAS_ERROR, payload 12 | }); 13 | -------------------------------------------------------------------------------- /src/actions/EpochConverter/unixTimeForm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_EPOCH_TO_DATE_RESULT, 3 | SET_UNIX_TIME_INPUT_VALUE 4 | } from "../../constants/ActionTypes"; 5 | 6 | export const setUnixTimeInputValue = (payload: string) => ({ 7 | type: SET_UNIX_TIME_INPUT_VALUE, payload 8 | }); 9 | 10 | export const setEpochToDateResult = (payload: string) => ({ 11 | type: SET_EPOCH_TO_DATE_RESULT, payload 12 | }); 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | /.idea 4 | 5 | # dependencies 6 | /node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | 16 | # misc 17 | .DS_Store 18 | .env.local 19 | .env.development.local 20 | .env.test.local 21 | .env.production.local 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /src/actions/EpochConverter/currentEpochTime.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CURRENT_UNIX_TIME, 3 | SET_COPY_TO_CLIPBOARD_BUTTON_STATE 4 | } from "../../constants/ActionTypes"; 5 | 6 | export const setCurrentUnixTime = (payload: string) => ({ 7 | type: SET_CURRENT_UNIX_TIME, payload 8 | }); 9 | 10 | export const setCopyToClipboardButtonState = (payload: 'default' | 'pressed') => ({ 11 | type: SET_COPY_TO_CLIPBOARD_BUTTON_STATE, payload 12 | }); 13 | -------------------------------------------------------------------------------- /src/reducers/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import epochConverter from "./EpochConverter"; 3 | import chmodCalculator from "./ChmodCalculator"; 4 | import crontabGenerator from "./CrontabGenerator"; 5 | import passwordGenerator from "./PasswordGenerator"; 6 | 7 | const rootReducer = combineReducers({ 8 | chmodCalculator, 9 | epochConverter, 10 | crontabGenerator, 11 | passwordGenerator 12 | }) 13 | 14 | export default rootReducer 15 | -------------------------------------------------------------------------------- /src/tests/App.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { render, screen } from '@testing-library/react'; 3 | import {Provider} from "react-redux"; 4 | import App from "../App"; 5 | import store from "../store/configureStore"; 6 | 7 | test('renders Chmod Calculator form', () => { 8 | render(); 9 | const element = screen.getAllByText(/Chmod Calculator/i); 10 | expect(element[1]).toBeInTheDocument(); 11 | }); 12 | -------------------------------------------------------------------------------- /src/components/PasswordGenerator/FormFields/PasswordLength.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Slider} from "primereact/slider"; 3 | 4 | const PasswordLength = ({passwordLength, onPasswordLengthChange}: any) => 5 | 6 | 7 | Password Length: {passwordLength} 8 | 11 | 12 | 13 | export default PasswordLength; 14 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/Schedule/days.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DAYS_RADIO_BUTTON_VALUE, 3 | SET_DAYS_OPTION_VALUE, 4 | SET_DAY_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | 7 | export const setDaysRadioButtonValue = (payload: string) => ({ 8 | type: SET_DAYS_RADIO_BUTTON_VALUE, payload 9 | }); 10 | 11 | export const setDaysOptionValue = (payload: any[]) => ({ 12 | type: SET_DAYS_OPTION_VALUE, payload 13 | }); 14 | 15 | export const setDayHasError = (payload: boolean) => ({ 16 | type: SET_DAY_HAS_ERROR, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/utils/generators/passwordGenerator.ts: -------------------------------------------------------------------------------- 1 | import {CheckboxesType} from "../../types/passwordGenerator"; 2 | 3 | const generator = require('generate-password-browser'); 4 | 5 | const generatePassword = (length: string, options: CheckboxesType) => generator.generate({ 6 | length: parseInt(length, 10), 7 | numbers: options.numbers, 8 | symbols: options.symbols, 9 | lowercase: options.lowercase, 10 | uppercase: options.uppercase, 11 | excludeSimilarCharacters: options.excludeSimilar 12 | }) 13 | 14 | export default generatePassword; 15 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/howToHandleOutput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HOW_TO_HANDLE_OUTPUT, 3 | SET_PATH_TO_FILE_VALUE, 4 | SET_EMAIL_VALUE 5 | } from "../../constants/ActionTypes"; 6 | 7 | export const setHowToHandleOutputValue = (payload: 'mute' | 'file' | 'email') => ({ 8 | type: HOW_TO_HANDLE_OUTPUT, payload 9 | }); 10 | 11 | export const setPathToFileValue = (payload: string) => ({ 12 | type: SET_PATH_TO_FILE_VALUE, payload 13 | }); 14 | 15 | export const setEmailValue = (payload: string) => ({ 16 | type: SET_EMAIL_VALUE, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/Schedule/hours.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_HOURS_RADIO_BUTTON_VALUE, 3 | SET_HOURS_OPTION_VALUE, 4 | SET_HOUR_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | 7 | export const setHoursRadioButtonValue = (payload: string) => ({ 8 | type: SET_HOURS_RADIO_BUTTON_VALUE, payload 9 | }); 10 | 11 | export const setHoursOptionValue = (payload: any[]) => ({ 12 | type: SET_HOURS_OPTION_VALUE, payload 13 | }); 14 | 15 | export const setHourHasError = (payload: boolean) => ({ 16 | type: SET_HOUR_HAS_ERROR, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/Schedule/months.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_MONTHS_RADIO_BUTTON_VALUE, 3 | SET_MONTHS_OPTION_VALUE, 4 | SET_MONTH_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | 7 | export const setMonthsRadioButtonValue = (payload: string) => ({ 8 | type: SET_MONTHS_RADIO_BUTTON_VALUE, payload 9 | }); 10 | 11 | export const setMonthsOptionValue = (payload: any[]) => ({ 12 | type: SET_MONTHS_OPTION_VALUE, payload 13 | }); 14 | 15 | export const setMonthHasError = (payload: boolean) => ({ 16 | type: SET_MONTH_HAS_ERROR, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | background-color: #121212; 9 | background-image: url('assets/images/bg.jpg'); 10 | background-attachment: fixed; 11 | background-size: cover; 12 | } 13 | 14 | code { 15 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 16 | monospace; 17 | } 18 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/Schedule/minutes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_MINUTES_RADIO_BUTTON_VALUE, 3 | SET_MINUTES_OPTION_VALUE, 4 | SET_MINUTE_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | 7 | export const setMinutesRadioButtonValue = (payload: string) => ({ 8 | type: SET_MINUTES_RADIO_BUTTON_VALUE, payload 9 | }); 10 | 11 | export const setMinutesOptionValue = (payload: any[]) => ({ 12 | type: SET_MINUTES_OPTION_VALUE, payload 13 | }); 14 | 15 | export const setMinuteHasError = (payload: boolean) => ({ 16 | type: SET_MINUTE_HAS_ERROR, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/actions/CrontabGenerator/Schedule/weekday.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_WEEKDAY_RADIO_BUTTON_VALUE, 3 | SET_WEEKDAY_OPTION_VALUE, 4 | SET_WEEKDAY_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | 7 | export const setWeekdayRadioButtonValue = (payload: string) => ({ 8 | type: SET_WEEKDAY_RADIO_BUTTON_VALUE, payload 9 | }); 10 | 11 | export const setWeekdayOptionValue = (payload: any[]) => ({ 12 | type: SET_WEEKDAY_OPTION_VALUE, payload 13 | }); 14 | 15 | export const setWeekdayHasError = (payload: boolean) => ({ 16 | type: SET_WEEKDAY_HAS_ERROR, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/actions/EpochConverter/dateTimeForm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DATE_TIME_INPUT_VALUE, 3 | SET_DATE_TIMEZONE_INPUT_VALUE, 4 | SET_DATE_TO_EPOCH_RESULT 5 | } from "../../constants/ActionTypes"; 6 | 7 | export const setDateTimeInputValue = (payload: Date) => ({ 8 | type: SET_DATE_TIME_INPUT_VALUE, payload 9 | }); 10 | 11 | export const setTimezoneInputValue = (payload: 'local' | 'gmt') => ({ 12 | type: SET_DATE_TIMEZONE_INPUT_VALUE, payload 13 | }); 14 | 15 | export const setDateToEpochResult = (payload: string) => ({ 16 | type: SET_DATE_TO_EPOCH_RESULT, payload 17 | }); 18 | -------------------------------------------------------------------------------- /src/actions/ChmodCalculator/chmodCalculator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CHECKED, 3 | SET_NUMBER_FIELD_VALUE, 4 | SET_TEXT_FIELD_VALUE 5 | } from "../../constants/ActionTypes"; 6 | import {CheckboxesType} from "../../types/chmodCalculator"; 7 | 8 | export const setChecked = (payload: CheckboxesType) => ({ 9 | type: SET_CHECKED, payload 10 | }); 11 | 12 | export const setNumberFieldValue = (payload: string) => ({ 13 | type: SET_NUMBER_FIELD_VALUE, payload 14 | }); 15 | 16 | export const setTextFieldValue = (payload: string) => ({ 17 | type: SET_TEXT_FIELD_VALUE, payload 18 | }); 19 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/generateCrontabLine.ts: -------------------------------------------------------------------------------- 1 | import {GENERATE_CRONTAB_LINE,} from "../../constants/ActionTypes"; 2 | 3 | const initialState = { 4 | result: '', 5 | } 6 | 7 | const generateCrontabLine = (state = initialState, action: { type: string; payload: string; }) => { 8 | 9 | switch (action.type) { 10 | 11 | case GENERATE_CRONTAB_LINE: { 12 | return { 13 | ...state, result: action.payload 14 | }; 15 | } 16 | default: 17 | return state; 18 | } 19 | } 20 | 21 | export default generateCrontabLine; 22 | -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Dev Tools", 3 | "name": "Free Online Developer Tools", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /src/tests/containers/EpochConverter/DateTimeForm.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import {Provider} from "react-redux"; 4 | import DateTimeForm from "../../../containers/EpochConverter/DateTimeForm"; 5 | import store from "../../../store/configureStore"; 6 | 7 | test('Human date to Timestamp', () => { 8 | render(); 9 | const btn = screen.getByText("Human date to Timestamp"); 10 | fireEvent.click(btn); 11 | expect(screen.getByText(/Unix Timestamp/i)).toBeInTheDocument(); 12 | }); 13 | -------------------------------------------------------------------------------- /src/components/EpochConverter/EpochConverter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Fieldset} from "primereact/fieldset"; 3 | import CurrentEpochTime from "../../containers/EpochConverter/CurrentEpochTime"; 4 | import UnixTimeForm from "../../containers/EpochConverter/UnixTimeForm"; 5 | import DateTimeForm from "../../containers/EpochConverter/DateTimeForm"; 6 | 7 | const EpochConverter: React.FC = () => 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | export default EpochConverter; 16 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/Schedule/RadioButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {RadioButton as PrimeRadioButton} from "primereact/radiobutton"; 3 | 4 | const RadioButton = ({button, id, checked, onRadioButtonChange}: 5 | { button: string, id: string, checked: boolean, onRadioButtonChange: any }) => 6 | 7 | 8 | 10 | {button} 11 | 12 | 13 | export default RadioButton; 14 | -------------------------------------------------------------------------------- /src/tests/containers/Menubar/Menubar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {render, screen} from "@testing-library/react"; 3 | import {HashRouter as Router} from 'react-router-dom'; 4 | import Menubar from "../../../containers/Menubar/Menubar"; 5 | 6 | const expectedMenuItems: string[] = [ 7 | 'Chmod Calculator', 8 | 'Crontab Generator', 9 | 'Unix Timestamp Converter', 10 | 'Password Generator', 11 | ]; 12 | 13 | render(); 14 | 15 | test('shows the menu items', () => { 16 | expectedMenuItems.forEach((item) => { 17 | expect(screen.getByText(item)).toBeInTheDocument() 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "dom.iterable", 7 | "esnext" 8 | ], 9 | "allowJs": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "allowSyntheticDefaultImports": true, 13 | "strict": true, 14 | "forceConsistentCasingInFileNames": true, 15 | "noFallthroughCasesInSwitch": true, 16 | "module": "esnext", 17 | "moduleResolution": "node", 18 | "resolveJsonModule": true, 19 | "isolatedModules": true, 20 | "noEmit": true, 21 | "jsx": "react-jsx" 22 | }, 23 | "include": [ 24 | "src" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/tests/components/common/CopyToClipboard.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import CopyToClipboard from "../../../components/common/CopyToClipboard"; 4 | 5 | const copyToClipboard = jest.fn(); 6 | 7 | const setup = () => { 8 | render() 9 | const button = screen.getByText('Copy') 10 | return {button} 11 | } 12 | 13 | it('should simulate button click', () => { 14 | const {button} = setup() 15 | fireEvent.click(button); 16 | expect(copyToClipboard).toBeCalledTimes(1) 17 | }); 18 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/HowToHandleOutput/Mute.tsx: -------------------------------------------------------------------------------- 1 | import {RadioButton} from "primereact/radiobutton"; 2 | import React from "react"; 3 | 4 | const Mute = ({value, onChange}: { value: string, onChange: any }) => { 5 | 6 | const label = 'Mute the execution (Don\'t save or send output)'; 7 | 8 | return 9 | 10 | 15 | {label} 16 | 17 | 18 | } 19 | 20 | export default Mute; 21 | -------------------------------------------------------------------------------- /src/components/PasswordGenerator/FormFields/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Checkbox as PrimeCheckbox} from "primereact/checkbox"; 3 | import {CheckboxPropsInterface} from "../../../types/passwordGenerator"; 4 | 5 | const Checkbox = ({ 6 | label, 7 | id, 8 | checked, 9 | onCheckboxChange 10 | }: CheckboxPropsInterface) => 11 | 12 | 13 | 15 | {label} 16 | 17 | 18 | export default Checkbox; 19 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/index.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux' 2 | import commandToExecute from "./commandToExecute"; 3 | import days from "./Schedule/days"; 4 | import generateCrontabLine from "./generateCrontabLine"; 5 | import hours from "./Schedule/hours"; 6 | import minutes from "./Schedule/minutes"; 7 | import months from "./Schedule/months"; 8 | import weekday from "./Schedule/weekday"; 9 | import howToHandleOutput from "./howToHandleOutput"; 10 | 11 | const crontabGenerator = combineReducers({ 12 | commandToExecute, 13 | generateCrontabLine, 14 | howToHandleOutput, 15 | days, 16 | hours, 17 | minutes, 18 | months, 19 | weekday 20 | }) 21 | 22 | export default crontabGenerator 23 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .max-w-760 { 2 | max-width: 760px; 3 | margin: 0 auto; 4 | } 5 | 6 | .max-w-980 { 7 | max-width: 980px; 8 | margin: 0 auto; 9 | } 10 | 11 | .w-100 { 12 | width: 100% 13 | } 14 | 15 | .card { 16 | border: 1px solid transparent; 17 | } 18 | 19 | .card-invalid { 20 | border: 1px solid #ef9a9a; 21 | } 22 | 23 | .border-top { 24 | border-top: 1px solid #00000082; 25 | } 26 | 27 | code { 28 | color: #000; 29 | padding: 2px 10px; 30 | } 31 | 32 | .bg-wisteria { 33 | background-color: #ce93d8; 34 | } 35 | 36 | .p-fieldset { 37 | background: #1e1e1ef2 !important; 38 | } 39 | 40 | @media (max-width: 960px) { 41 | .logo { 42 | display: none !important; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/validators/chmodValidator.ts: -------------------------------------------------------------------------------- 1 | export const isNumberValid = (value: string): boolean => { 2 | const pattern = /^[0-7]*$/; 3 | if (!pattern.test(value)) { 4 | return false; 5 | } 6 | 7 | return value.length <= 3; 8 | } 9 | 10 | export const isStringValid = (value: string): boolean => { 11 | for (let i = 0; i <= 8; i += 1) { 12 | if ((i === 0 || i === 3 || i === 6) && !['r', '-', ''].includes(value.charAt(i)) || 13 | (i === 1 || i === 4 || i === 7) && !['w', '-', ''].includes(value.charAt(i)) || 14 | (i === 2 || i === 5 || i === 8) && !['x', '-', ''].includes(value.charAt(i))) { 15 | return false; 16 | } 17 | } 18 | 19 | return value.length <= 9; 20 | } 21 | -------------------------------------------------------------------------------- /src/components/ChmodCalculator/Checkboxes/CheckboxGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Checkbox from "./Checkbox"; 3 | import {CheckboxGroupType} from "../../../types/chmodCalculator"; 4 | 5 | const CheckboxGroup: React.FC = ({ownership, checked, handleOnChange}) => 6 | 7 | 8 | {ownership} 9 | 10 | {['Read', 'Write', 'Execute'].map(permission => 11 | )} 16 | 17 | 18 | 19 | export default CheckboxGroup; 20 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/GenerateCrontabLine.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../store/configureStore"; 5 | import GenerateCrontabLine 6 | from "../../../containers/CrontabGenerator/GenerateCrontabLine"; 7 | 8 | const setup = () => { 9 | render() 10 | } 11 | 12 | it('should simulate button click', () => { 13 | setup(); 14 | const button = screen.getByText('Generate Crontab Line'); 15 | expect(button).toBeVisible(); 16 | fireEvent.click(button); 17 | expect(screen.queryByText('Please enter a command.')).toBeVisible(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/actions/PasswordGenerator/passwordGenerator.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_PASSWORD, 3 | SET_PASSWORD_LENGTH, 4 | SET_CHECKED_PARAM, 5 | SET_COPY_TO_CLIPBOARD_STATE, 6 | } from "../../constants/ActionTypes"; 7 | import {CheckboxesType} from "../../types/passwordGenerator"; 8 | 9 | export const setPassword = (payload: string) => ({ 10 | type: SET_PASSWORD, payload 11 | }); 12 | 13 | export const setPasswordLength = (payload: string) => ({ 14 | type: SET_PASSWORD_LENGTH, payload 15 | }); 16 | 17 | export const setCheckedParam = (payload: CheckboxesType) => ({ 18 | type: SET_CHECKED_PARAM, payload 19 | }); 20 | 21 | export const setCopyToClipboardState = (payload: 'default' | 'pressed') => ({ 22 | type: SET_COPY_TO_CLIPBOARD_STATE, payload 23 | }); 24 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './index.css'; 3 | import {Provider} from "react-redux"; 4 | import {createRoot} from "react-dom/client"; 5 | import App from './App'; 6 | import * as serviceWorker from './serviceWorker'; 7 | import store from "./store/configureStore"; 8 | 9 | const container = document.getElementById('root'); 10 | const root = createRoot(container!); 11 | root.render( 12 | 13 | 14 | 15 | ); 16 | 17 | // If you want your app to work offline and load faster, you can change 18 | // unregister() to register() below. Note this comes with some pitfalls. 19 | // Learn more about service workers: https://bit.ly/CRA-PWA 20 | serviceWorker.unregister(); 21 | -------------------------------------------------------------------------------- /src/tests/components/PasswordGenerator/FormFields/PasswordLength.test.tsx: -------------------------------------------------------------------------------- 1 | import {render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import PasswordLength 4 | from "../../../../components/PasswordGenerator/FormFields/PasswordLength"; 5 | 6 | const onChange = jest.fn(); 7 | 8 | it('shows the slider title', () => { 9 | render(); 11 | expect(screen.getByText(/Password Length:/i)).toBeVisible(); 12 | }); 13 | 14 | it('shows the password length', () => { 15 | render(); 17 | expect(screen.getByText(/41/i)).toBeVisible(); 18 | }); 19 | -------------------------------------------------------------------------------- /src/tests/components/EpochConverter/CurrentEpochTime.test.tsx: -------------------------------------------------------------------------------- 1 | import {render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import CurrentEpochTime from "../../../components/EpochConverter/CurrentEpochTime"; 4 | 5 | const copyToClipboard = jest.fn(); 6 | 7 | const setup = () => { 8 | render() 11 | } 12 | 13 | it('shows the current unix time', () => { 14 | setup(); 15 | expect(screen.getByText('111222333')).toBeVisible() 16 | }); 17 | 18 | it('shows the "Copy" button', () => { 19 | setup(); 20 | expect(screen.getByText('Copy')).toBeVisible() 21 | }); 22 | -------------------------------------------------------------------------------- /src/tests/containers/EpochConverter/UnixTimeForm.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import {Provider} from "react-redux"; 4 | import UnixTimeForm from "../../../containers/EpochConverter/UnixTimeForm"; 5 | import store from "../../../store/configureStore"; 6 | 7 | test('Timestamp to Human date', () => { 8 | render(); 9 | expect(screen.getByText('Timestamp to Human date')).toBeInTheDocument(); 10 | expect(screen.getByDisplayValue((Math.floor(Date.now() / 1000)).toString())).toBeInTheDocument(); 11 | const btn = screen.getByText("Timestamp to Human date"); 12 | fireEvent.click(btn); 13 | expect(screen.getByText(/GMT/i)).toBeInTheDocument(); 14 | }); 15 | -------------------------------------------------------------------------------- /src/components/Menubar/Menubar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Menubar as PrimeMenubar} from 'primereact/menubar'; 3 | import {Button} from "primereact/button"; 4 | import {MenubarPropsType} from "../../types/menubar"; 5 | 6 | const Menubar: React.FC = ({model, onLogoClick, onGitHubClick}) => { 7 | 8 | const start = ; 9 | 10 | const end = ; 14 | 15 | return 16 | } 17 | 18 | export default Menubar; 19 | -------------------------------------------------------------------------------- /src/components/ChmodCalculator/Checkboxes/Checkbox.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Checkbox as PrimeCheckbox} from 'primereact/checkbox'; 3 | import {CheckboxPropsType} from "../../../types/chmodCalculator"; 4 | 5 | const Checkbox = ({ownership, permission, checked, handleOnChange}: CheckboxPropsType) => 6 | 7 | 13 | 14 | 15 | {permission} 16 | 17 | 18 | 19 | export default Checkbox; 20 | -------------------------------------------------------------------------------- /src/components/ChmodCalculator/Checkboxes/Checkboxes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import CheckboxGroup from "./CheckboxGroup"; 3 | import 'primeflex/primeflex.css'; 4 | import {CheckboxesPropsType} from "../../../types/chmodCalculator"; 5 | 6 | const Checkboxes: React.FC = ({checked, handleOnChange}) => { 7 | 8 | const checkboxGroups = ['owner', 'group', 'others'].map(ownership => 9 | 10 | 11 | 12 | ); 13 | 14 | return 15 | 16 | {checkboxGroups} 17 | 18 | 19 | } 20 | 21 | export default Checkboxes; 22 | -------------------------------------------------------------------------------- /src/tests/utils/validators/passwordValidator.test.ts: -------------------------------------------------------------------------------- 1 | import {CheckboxesType} from "../../../types/passwordGenerator"; 2 | import isChangeAllowed from "../../../utils/validators/passwordValidator"; 3 | 4 | describe('isChangeAllowed() function test', () => { 5 | 6 | const allowedParams: CheckboxesType = { 7 | symbols: false, 8 | numbers: true, 9 | uppercase: false, 10 | lowercase: false, 11 | excludeSimilar: false, 12 | } 13 | 14 | test(`Change is allowed`, () => { 15 | expect(isChangeAllowed(allowedParams)).toBeTruthy(); 16 | }); 17 | 18 | const invalidParams = {...allowedParams} 19 | invalidParams.numbers = false; 20 | 21 | test(`Change is not allowed`, () => { 22 | expect(isChangeAllowed(invalidParams)).toBeFalsy(); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/utils/calculators/epochConverter.ts: -------------------------------------------------------------------------------- 1 | export const getCurrentUnixTime = () => (Math.floor(Date.now() / 1000)).toString(); 2 | 3 | export const epochToHumanReadableDate = (unixTime: string): string => { 4 | const d = new Date(0); 5 | let seconds = Number(unixTime); 6 | 7 | if (unixTime.length > 11) { 8 | seconds = Math.floor(seconds / 1000); 9 | } 10 | 11 | d.setUTCSeconds(seconds); 12 | 13 | return d.toString(); 14 | } 15 | 16 | export const humanReadableDateToEpoch = (datetime: string, timezone: string = 'local'): string => { 17 | const epoch = new Date(datetime).getTime() 18 | const unixTime = Math.floor(epoch / 1000); 19 | const offset = timezone === 'gmt' 20 | ? (new Date(datetime)).getTimezoneOffset() * 60 21 | : 0 22 | 23 | return (unixTime - offset).toString(); 24 | } 25 | -------------------------------------------------------------------------------- /src/tests/components/EpochConverter/Forms/UnixTimeForm.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import UnixTimeForm from "../../../../components/EpochConverter/Forms/UnixTimeForm"; 4 | 5 | const onEpochToDateButtonClick = jest.fn(); 6 | 7 | const setup = () => { 8 | render() 12 | const button = screen.getByText('Timestamp to Human date') 13 | return {button} 14 | } 15 | 16 | it('should simulate button click', () => { 17 | const {button} = setup() 18 | fireEvent.click(button); 19 | expect(onEpochToDateButtonClick).toBeCalledTimes(1) 20 | }); 21 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/generateCrontabLine.test.ts: -------------------------------------------------------------------------------- 1 | import generateCrontabLine 2 | from "../../../reducers/CrontabGenerator/generateCrontabLine"; 3 | import setResult from "../../../actions/CrontabGenerator/generateCrontabLine"; 4 | 5 | describe('generateCrontabLine reducer', () => { 6 | 7 | const initialState = { 8 | result: '', 9 | } 10 | 11 | it("should handle initial state", () => { 12 | expect(generateCrontabLine(undefined, { 13 | type: "undefined", 14 | payload: "" 15 | })).toEqual(initialState); 16 | }); 17 | 18 | it("should handle result change", () => { 19 | const result = '* * * * * /usr/bin/php /var/www/app/cron.php > crontab.log'; 20 | const actual = generateCrontabLine(initialState, setResult(result)); 21 | expect(actual.result).toEqual(result); 22 | }); 23 | 24 | }) 25 | -------------------------------------------------------------------------------- /src/components/common/CopyToClipboard.tsx: -------------------------------------------------------------------------------- 1 | import React, {MouseEventHandler} from "react"; 2 | import {Button} from "primereact/button"; 3 | 4 | type CopyToClipboardType = { 5 | copyToClipboard: MouseEventHandler; 6 | copyToClipboardButtonState: 'default' | 'pressed'; 7 | } 8 | 9 | const CopyToClipboard: React.FC = 10 | ({copyToClipboard, copyToClipboardButtonState}) => { 11 | 12 | const text = copyToClipboardButtonState === 'pressed' ? 'Copied' : 'Copy'; 13 | const icon = copyToClipboardButtonState === 'pressed' ? 'pi pi-check' : 'pi pi-copy'; 14 | 15 | return 20 | } 21 | 22 | export default CopyToClipboard; 23 | -------------------------------------------------------------------------------- /src/tests/utils/generators/passwordGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import generatePassword from "../../../utils/generators/passwordGenerator"; 2 | import {CheckboxesType} from "../../../types/passwordGenerator"; 3 | 4 | describe('generatePassword() function test', () => { 5 | 6 | const options: CheckboxesType = { 7 | symbols: true, 8 | numbers: true, 9 | uppercase: true, 10 | lowercase: true, 11 | excludeSimilar: false, 12 | } 13 | 14 | test(`Password length is 20 characters`, () => { 15 | expect(generatePassword('20', options)).toHaveLength(20) 16 | }); 17 | 18 | test(`Password length is 16 characters`, () => { 19 | expect(generatePassword('16', options)).toHaveLength(16) 20 | }); 21 | 22 | test(`Password length is 43 characters`, () => { 23 | expect(generatePassword('43', options)).toHaveLength(43) 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/Schedule/RadioButtonList.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import RadioButton from "./RadioButton"; 3 | import {RadioButtonListProps} from "../../../../types/crontabGenerator"; 4 | 5 | const RadioButtonList = ({ 6 | radioButtons, 7 | selectedRadio, 8 | onRadioButtonChange 9 | }: RadioButtonListProps) => { 10 | 11 | const result = radioButtons.map((button: string) => 12 | 13 | 19 | ); 20 | 21 | return <>{result}> 22 | } 23 | export default RadioButtonList; 24 | -------------------------------------------------------------------------------- /src/reducers/EpochConverter/unixTimeForm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_EPOCH_TO_DATE_RESULT, 3 | SET_UNIX_TIME_INPUT_VALUE 4 | } from "../../constants/ActionTypes"; 5 | import {UnixTimeFormStateType} from "../../types/epochConverter"; 6 | 7 | const initialState: UnixTimeFormStateType = { 8 | unixTimeInputValue: '*', 9 | epochToDateResult: '', 10 | } 11 | 12 | const unixTimeForm = (state = initialState, action: { type: string; payload: string; }) => { 13 | 14 | switch (action.type) { 15 | 16 | case SET_UNIX_TIME_INPUT_VALUE: { 17 | return { 18 | ...state, unixTimeInputValue: action.payload 19 | }; 20 | } 21 | case SET_EPOCH_TO_DATE_RESULT: { 22 | return { 23 | ...state, epochToDateResult: action.payload 24 | }; 25 | } 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | export default unixTimeForm; 32 | -------------------------------------------------------------------------------- /src/tests/components/EpochConverter/Forms/DateTimeForm.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import DateTimeForm from "../../../../components/EpochConverter/Forms/DateTimeForm"; 4 | 5 | const onDateToEpochButtonClick = jest.fn(); 6 | 7 | const setup = () => { 8 | render() 14 | const button = screen.getByText('Human date to Timestamp') 15 | return {button} 16 | } 17 | 18 | it('should simulate button click', () => { 19 | const {button} = setup() 20 | fireEvent.click(button); 21 | expect(onDateToEpochButtonClick).toBeCalledTimes(1) 22 | }); 23 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 19.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'yarn' 29 | - run: yarn install 30 | - run: yarn test 31 | -------------------------------------------------------------------------------- /src/reducers/EpochConverter/currentEpochTime.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CURRENT_UNIX_TIME, 3 | SET_COPY_TO_CLIPBOARD_BUTTON_STATE 4 | } from "../../constants/ActionTypes"; 5 | import {CurrentEpochTimeContainerState} from "../../types/epochConverter"; 6 | 7 | const initialState: CurrentEpochTimeContainerState = { 8 | currentUnixTime: '', 9 | copyToClipboardButtonState: 'default', 10 | } 11 | 12 | const currentEpochTime = (state = initialState, action: { type: string; payload: string; }) => { 13 | 14 | switch (action.type) { 15 | 16 | case SET_CURRENT_UNIX_TIME: { 17 | return { 18 | ...state, currentUnixTime: action.payload 19 | }; 20 | } 21 | case SET_COPY_TO_CLIPBOARD_BUTTON_STATE: { 22 | return { 23 | ...state, copyToClipboardButtonState: action.payload 24 | }; 25 | } 26 | default: 27 | return state; 28 | } 29 | } 30 | 31 | export default currentEpochTime; 32 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/commandToExecute.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_COMMAND_TO_EXECUTE, 3 | SET_COMMAND_HAS_ERROR 4 | } from "../../constants/ActionTypes"; 5 | import {CommandToExecuteContainerState} from "../../types/crontabGenerator"; 6 | 7 | const initialState: CommandToExecuteContainerState = { 8 | commandToExecuteValue: '', 9 | commandHasError: false, 10 | } 11 | 12 | const commandToExecute = (state = initialState, 13 | action: { type: string; payload: string | boolean; }) => { 14 | 15 | switch (action.type) { 16 | 17 | case SET_COMMAND_TO_EXECUTE: { 18 | return { 19 | ...state, commandToExecuteValue: action.payload 20 | }; 21 | } 22 | case SET_COMMAND_HAS_ERROR: { 23 | return { 24 | ...state, commandHasError: action.payload 25 | }; 26 | } 27 | default: 28 | return state; 29 | } 30 | } 31 | 32 | export default commandToExecute; 33 | -------------------------------------------------------------------------------- /src/components/EpochConverter/CurrentEpochTime.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Chip} from "primereact/chip"; 3 | import {CurrentEpochTimeInterface} from "../../types/epochConverter"; 4 | import CopyToClipboard from "../common/CopyToClipboard"; 5 | 6 | const CurrentEpochTime: React.FC = 7 | ({currentUnixTime, copyToClipboard, copyToClipboardButtonState}) => 8 | 9 | 10 | 11 | The current Unix epoch time is 12 | 13 | 14 | 15 | 16 | 17 | 19 | 20 | 21 | 22 | export default CurrentEpochTime; 23 | -------------------------------------------------------------------------------- /src/tests/components/CrontabGenerator/FormFields/HowToHandleOutput/Mute.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import Mute 4 | from "../../../../../components/CrontabGenerator/FormFields/HowToHandleOutput/Mute"; 5 | 6 | const onChange = jest.fn(); 7 | 8 | const setup = (value: string) => { 9 | render() 10 | } 11 | 12 | describe(' component', () => { 13 | it('should render "mute" radio button', () => { 14 | setup('mute') 15 | expect(screen.getByText(/Mute the execution/i)).toBeInTheDocument(); 16 | expect(screen.getByLabelText(/Mute the execution/i)).toBeChecked(); 17 | }); 18 | 19 | it('should simulate radio button change', () => { 20 | setup('') 21 | const radioButton = screen.getByLabelText(/Mute the execution/i); 22 | expect(radioButton).not.toBeChecked(); 23 | fireEvent.click(radioButton); 24 | expect(onChange).toBeCalledTimes(1) 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/tests/utils/validators/emailValidator.test.ts: -------------------------------------------------------------------------------- 1 | import isEmailValid from "../../../utils/validators/emailValidator"; 2 | 3 | describe('validate email address', () => { 4 | 5 | const validValues = [ 6 | 'cgibbons1@wryzpro.com', 7 | 'Timothy-SGray@yandex.com', 8 | 'ReggieCNewberry@mail.ru', 9 | 'Emily.KLewis@gmail.com', 10 | 'CeceliaFThomas@r-hyta.com' 11 | ]; 12 | 13 | validValues.forEach((email) => { 14 | test(`${email} is valid email`, () => { 15 | expect(isEmailValid(email)).toBe(true); 16 | }); 17 | }); 18 | 19 | const invalidValues = [ 20 | 'RobertRToliver@armyspy', 21 | 'KirkAStewart_rhyta.com', 22 | 'SarahVJeffreys@ya', 23 | '@gmail.com', 24 | '', 25 | 'info@info', 26 | 'EarlDMarquez@jourrapide.111' 27 | ]; 28 | 29 | invalidValues.forEach((email) => { 30 | test(`${email} is invalid email`, () => { 31 | expect(isEmailValid(email)).toBe(false); 32 | }); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /src/tests/components/Menubar/Menubar.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {fireEvent, render, screen} from "@testing-library/react"; 3 | import {HashRouter as Router} from 'react-router-dom'; 4 | import Menubar from '../../../components/Menubar/Menubar'; 5 | 6 | const items = [ 7 | { 8 | label: 'First link', 9 | }, 10 | { 11 | label: 'Second link', 12 | }, 13 | { 14 | label: 'Third link', 15 | } 16 | ]; 17 | 18 | const onGitHubClick = jest.fn(); 19 | 20 | const setup = () => render( 21 | {}} 23 | onGitHubClick={onGitHubClick}/> 24 | ) 25 | 26 | it('shows the menu items', () => { 27 | setup(); 28 | items.forEach((item) => { 29 | expect(screen.getByText(item.label)).toBeInTheDocument() 30 | }); 31 | }); 32 | 33 | it('should simulate button click', () => { 34 | setup(); 35 | const button = screen.getByTestId('GitHub'); 36 | fireEvent.click(button); 37 | expect(onGitHubClick).toBeCalledTimes(1); 38 | }); 39 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/Schedule/Months.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../../store/configureStore"; 5 | import Months from "../../../../containers/CrontabGenerator/Schedule/Months"; 6 | 7 | const setup = () => { 8 | render() 9 | } 10 | 11 | it('should render all radio buttons', () => { 12 | setup(); 13 | [ 14 | 'Every Month', 15 | 'Even Months', 16 | 'Odd Months', 17 | 'Every 4 Months', 18 | 'Every Half Year', 19 | ].forEach((item) => { 20 | const radioButton = screen.getByLabelText(item); 21 | expect(radioButton).toBeVisible(); 22 | if (item === 'Every Month') { 23 | expect(radioButton).toBeChecked(); 24 | } else { 25 | expect(radioButton).not.toBeChecked(); 26 | fireEvent.click(radioButton); 27 | expect(radioButton).toBeChecked(); 28 | } 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/tests/components/ChmodCalculator/Checkboxes/CheckboxGroup.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import React from "react"; 3 | import CheckboxGroup from "../../../../components/ChmodCalculator/Checkboxes/CheckboxGroup"; 4 | 5 | const checked = { 6 | ownerRead: false, 7 | ownerWrite: false, 8 | ownerExecute: true, 9 | groupRead: false, 10 | groupWrite: false, 11 | groupExecute: true, 12 | othersRead: false, 13 | othersWrite: false, 14 | othersExecute: false 15 | } 16 | 17 | it('shows the unchecked checkboxes', () => { 18 | render(); 19 | expect(screen.getByLabelText('Read')).not.toBeChecked(); 20 | expect(screen.getByLabelText('Write')).not.toBeChecked(); 21 | }); 22 | 23 | it('shows the checked checkbox', () => { 24 | render(); 25 | const checkbox = screen.getByLabelText('Execute'); 26 | expect(checkbox).toBeVisible(); 27 | expect(checkbox).toBeChecked(); 28 | }); 29 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/Schedule/Days.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../../store/configureStore"; 5 | import Days from "../../../../containers/CrontabGenerator/Schedule/Days"; 6 | 7 | const setup = () => { 8 | render() 9 | } 10 | 11 | it('should render all radio buttons', () => { 12 | setup(); 13 | [ 14 | 'Every Day', 15 | 'Even Days', 16 | 'Odd Days', 17 | 'Every 5 Days', 18 | 'Every 10 Days', 19 | 'Every Half Month', 20 | ].forEach((item) => { 21 | const radioButton = screen.getByLabelText(item); 22 | expect(radioButton).toBeVisible(); 23 | if (item === 'Every Day') { 24 | expect(radioButton).toBeChecked(); 25 | } else { 26 | expect(radioButton).not.toBeChecked(); 27 | fireEvent.click(radioButton); 28 | expect(radioButton).toBeChecked(); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/Schedule/Hours.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../../store/configureStore"; 5 | import Hours from "../../../../containers/CrontabGenerator/Schedule/Hours"; 6 | 7 | const setup = () => { 8 | render() 9 | } 10 | 11 | it('should render all radio buttons', () => { 12 | setup(); 13 | [ 14 | 'Every Hour', 15 | 'Even Hours', 16 | 'Odd Hours', 17 | 'Every 3 Hours', 18 | 'Every 6 Hours', 19 | 'Every 12 Hours', 20 | ].forEach((item) => { 21 | const radioButton = screen.getByLabelText(item); 22 | expect(radioButton).toBeVisible(); 23 | if (item === 'Every Hour') { 24 | expect(radioButton).toBeChecked(); 25 | } else { 26 | expect(radioButton).not.toBeChecked(); 27 | fireEvent.click(radioButton); 28 | expect(radioButton).toBeChecked(); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/GenerateCrontabLine.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "primereact/button"; 2 | import React, {Fragment} from "react"; 3 | import {GenerateCrontabLineProps} from "../../types/crontabGenerator"; 4 | 5 | const GenerateCrontabLine: 6 | React.FC = ({result, onClick}) => 7 | 8 | <> 9 | {result && 10 | 11 | 12 | 13 | 14 | {result.split('\n').map((item: any, key: any) => 15 | {item} 16 | )} 17 | 18 | 19 | 20 | 21 | } 22 | 23 | 24 | 25 | 26 | > 27 | 28 | 29 | export default GenerateCrontabLine; 30 | -------------------------------------------------------------------------------- /src/tests/components/ChmodCalculator/Checkboxes/Checkbox.test.tsx: -------------------------------------------------------------------------------- 1 | import { render, screen } from "@testing-library/react"; 2 | import React from "react"; 3 | import Checkbox from "../../../../components/ChmodCalculator/Checkboxes/Checkbox"; 4 | 5 | const checked = { 6 | ownerRead: true, 7 | ownerWrite: false, 8 | ownerExecute: false, 9 | groupRead: false, 10 | groupWrite: false, 11 | groupExecute: false, 12 | othersRead: false, 13 | othersWrite: false, 14 | othersExecute: false 15 | } 16 | 17 | it('shows the checked checkbox', () => { 18 | render(); 19 | const checkbox = screen.getByLabelText('Read'); 20 | expect(checkbox).toBeVisible(); 21 | expect(checkbox).toBeChecked(); 22 | }); 23 | 24 | it('shows the unchecked checkbox', () => { 25 | render(); 26 | const checkbox = screen.getByLabelText('Write'); 27 | expect(checkbox).toBeVisible(); 28 | expect(checkbox).not.toBeChecked(); 29 | }); 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021-2022 Valery Maslov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/Schedule/Minutes.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../../store/configureStore"; 5 | import Minutes from "../../../../containers/CrontabGenerator/Schedule/Minutes"; 6 | 7 | const setup = () => { 8 | render() 9 | } 10 | 11 | it('should render all radio buttons', () => { 12 | setup(); 13 | [ 14 | 'Every Minute', 15 | 'Even Minutes', 16 | 'Odd Minutes', 17 | 'Every 5 Minutes', 18 | 'Every 15 Minutes', 19 | 'Every 30 Minutes', 20 | ].forEach((item) => { 21 | const radioButton = screen.getByLabelText(item); 22 | expect(radioButton).toBeVisible(); 23 | if (item === 'Every Minute') { 24 | expect(radioButton).toBeChecked(); 25 | } else { 26 | expect(radioButton).not.toBeChecked(); 27 | fireEvent.click(radioButton); 28 | expect(radioButton).toBeChecked(); 29 | } 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/Schedule/days.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DAYS_RADIO_BUTTON_VALUE, 3 | SET_DAYS_OPTION_VALUE, 4 | SET_DAY_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | import {PeriodContainerStateInterface} from "../../../types/crontabGenerator"; 7 | 8 | const initialState: PeriodContainerStateInterface = { 9 | selectedRadio: 'Every Day', 10 | selectedOption: [], 11 | hasError: false, 12 | } 13 | 14 | const days = (state = initialState, action: { type: string; payload: any; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_DAYS_RADIO_BUTTON_VALUE: { 19 | return { 20 | ...state, selectedRadio: action.payload 21 | }; 22 | } 23 | case SET_DAYS_OPTION_VALUE: { 24 | return { 25 | ...state, selectedOption: action.payload 26 | }; 27 | } 28 | case SET_DAY_HAS_ERROR: { 29 | return { 30 | ...state, hasError: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default days; 39 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/Schedule/hours.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_HOURS_RADIO_BUTTON_VALUE, 3 | SET_HOURS_OPTION_VALUE, 4 | SET_HOUR_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | import {PeriodContainerStateInterface} from "../../../types/crontabGenerator"; 7 | 8 | const initialState: PeriodContainerStateInterface = { 9 | selectedRadio: 'Every Hour', 10 | selectedOption: [], 11 | hasError: false, 12 | } 13 | 14 | const hours = (state = initialState, action: { type: string; payload: any; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_HOURS_RADIO_BUTTON_VALUE: { 19 | return { 20 | ...state, selectedRadio: action.payload 21 | }; 22 | } 23 | case SET_HOURS_OPTION_VALUE: { 24 | return { 25 | ...state, selectedOption: action.payload 26 | }; 27 | } 28 | case SET_HOUR_HAS_ERROR: { 29 | return { 30 | ...state, hasError: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default hours; 39 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/Schedule/months.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_MONTHS_RADIO_BUTTON_VALUE, 3 | SET_MONTHS_OPTION_VALUE, 4 | SET_MONTH_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | import {PeriodContainerStateInterface} from "../../../types/crontabGenerator"; 7 | 8 | const initialState: PeriodContainerStateInterface = { 9 | selectedRadio: 'Every Month', 10 | selectedOption: [], 11 | hasError: false, 12 | } 13 | 14 | const months = (state = initialState, action: { type: string; payload: any; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_MONTHS_RADIO_BUTTON_VALUE: { 19 | return { 20 | ...state, selectedRadio: action.payload 21 | }; 22 | } 23 | case SET_MONTHS_OPTION_VALUE: { 24 | return { 25 | ...state, selectedOption: action.payload 26 | }; 27 | } 28 | case SET_MONTH_HAS_ERROR: { 29 | return { 30 | ...state, hasError: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default months; 39 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/Schedule/minutes.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_MINUTES_RADIO_BUTTON_VALUE, 3 | SET_MINUTES_OPTION_VALUE, 4 | SET_MINUTE_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | import {PeriodContainerStateInterface} from "../../../types/crontabGenerator"; 7 | 8 | const initialState: PeriodContainerStateInterface = { 9 | selectedRadio: 'Every Minute', 10 | selectedOption: [], 11 | hasError: false, 12 | } 13 | 14 | const minutes = (state = initialState, action: { type: string; payload: any; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_MINUTES_RADIO_BUTTON_VALUE: { 19 | return { 20 | ...state, selectedRadio: action.payload 21 | }; 22 | } 23 | case SET_MINUTES_OPTION_VALUE: { 24 | return { 25 | ...state, selectedOption: action.payload 26 | }; 27 | } 28 | case SET_MINUTE_HAS_ERROR: { 29 | return { 30 | ...state, hasError: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default minutes; 39 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/Schedule/weekday.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_WEEKDAY_RADIO_BUTTON_VALUE, 3 | SET_WEEKDAY_OPTION_VALUE, 4 | SET_WEEKDAY_HAS_ERROR 5 | } from "../../../constants/ActionTypes"; 6 | import {PeriodContainerStateInterface} from "../../../types/crontabGenerator"; 7 | 8 | const initialState: PeriodContainerStateInterface = { 9 | selectedRadio: 'Every Weekday', 10 | selectedOption: [], 11 | hasError: false, 12 | } 13 | 14 | const weekday = (state = initialState, action: { type: string; payload: any; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_WEEKDAY_RADIO_BUTTON_VALUE: { 19 | return { 20 | ...state, selectedRadio: action.payload 21 | }; 22 | } 23 | case SET_WEEKDAY_OPTION_VALUE: { 24 | return { 25 | ...state, selectedOption: action.payload 26 | }; 27 | } 28 | case SET_WEEKDAY_HAS_ERROR: { 29 | return { 30 | ...state, hasError: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default weekday; 39 | -------------------------------------------------------------------------------- /src/reducers/CrontabGenerator/howToHandleOutput.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HOW_TO_HANDLE_OUTPUT, 3 | SET_PATH_TO_FILE_VALUE, 4 | SET_EMAIL_VALUE 5 | } from "../../constants/ActionTypes"; 6 | import {HowToHandleOutputContainerState} from "../../types/crontabGenerator"; 7 | 8 | const initialState: HowToHandleOutputContainerState = { 9 | howToHandleOutputValue: 'mute', 10 | pathToFileValue: '', 11 | emailValue: '', 12 | } 13 | 14 | const howToHandleOutput = (state = initialState, action: { type: string; payload: string; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case HOW_TO_HANDLE_OUTPUT: { 19 | return { 20 | ...state, howToHandleOutputValue: action.payload 21 | }; 22 | } 23 | case SET_PATH_TO_FILE_VALUE: { 24 | return { 25 | ...state, pathToFileValue: action.payload 26 | }; 27 | } 28 | case SET_EMAIL_VALUE: { 29 | return { 30 | ...state, emailValue: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default howToHandleOutput; 39 | -------------------------------------------------------------------------------- /src/components/PasswordGenerator/FormFields/Checkboxes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Checkbox from "./Checkbox"; 3 | import {CheckboxesPropsType} from "../../../types/passwordGenerator"; 4 | 5 | const Checkboxes = ({checked, onCheckboxChange}: CheckboxesPropsType) => { 6 | 7 | const checkboxes = [ 8 | {id: "symbols", label: "Symbols"}, 9 | {id: "numbers", label: "Numbers"}, 10 | {id: "uppercase", label: "Uppercase"}, 11 | {id: "lowercase", label: "Lowercase"}, 12 | {id: "excludeSimilar", label: "Exclude Similar Characters"} 13 | ]; 14 | 15 | return 16 | 17 | { 18 | checkboxes.map(checkbox => { 19 | 20 | const className = checkbox.id === "excludeSimilar" ? "p-col-12" : "p-col"; 21 | 22 | return 23 | 26 | 27 | }) 28 | } 29 | 30 | } 31 | export default Checkboxes; 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | Demo: https://coderberg.github.io/web-developer-tools/ 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `yarn start` 10 | 11 | Runs the app in the development mode. 12 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 13 | 14 | The page will reload if you make edits. 15 | You will also see any lint errors in the console. 16 | 17 | ### `yarn test` 18 | 19 | Launches the test runner in the interactive watch mode. 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `yarn build` 23 | 24 | Builds the app for production to the `build` folder. 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes. 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | -------------------------------------------------------------------------------- /src/reducers/EpochConverter/dateTimeForm.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_DATE_TIME_INPUT_VALUE, 3 | SET_DATE_TIMEZONE_INPUT_VALUE, 4 | SET_DATE_TO_EPOCH_RESULT 5 | } from "../../constants/ActionTypes"; 6 | import {DateTimeFormState} from "../../types/epochConverter"; 7 | 8 | const initialState: DateTimeFormState = { 9 | dateTimeInputValue: undefined, 10 | timezoneInputValue: 'local', 11 | dateToEpochResult: '', 12 | } 13 | 14 | const dateTimeForm = (state = initialState, action: { type: string; payload: string | Date | null; }) => { 15 | 16 | switch (action.type) { 17 | 18 | case SET_DATE_TIME_INPUT_VALUE: { 19 | return { 20 | ...state, dateTimeInputValue: action.payload 21 | }; 22 | } 23 | case SET_DATE_TIMEZONE_INPUT_VALUE: { 24 | return { 25 | ...state, timezoneInputValue: action.payload 26 | }; 27 | } 28 | case SET_DATE_TO_EPOCH_RESULT: { 29 | return { 30 | ...state, dateToEpochResult: action.payload 31 | }; 32 | } 33 | default: 34 | return state; 35 | } 36 | } 37 | 38 | export default dateTimeForm; 39 | -------------------------------------------------------------------------------- /src/tests/containers/CrontabGenerator/Schedule/Weekday.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import {Provider} from "react-redux"; 3 | import React from "react"; 4 | import store from "../../../../store/configureStore"; 5 | import Weekday from "../../../../containers/CrontabGenerator/Schedule/Weekday"; 6 | 7 | const setup = () => { 8 | render() 9 | } 10 | 11 | it('should render all radio buttons', () => { 12 | setup(); 13 | [ 14 | 'Every Weekday', 15 | 'Monday-Friday', 16 | 'Weekend Days', 17 | ].forEach((item) => { 18 | const radioButton = screen.getByLabelText(item); 19 | expect(radioButton).toBeVisible(); 20 | if (item === 'Every Weekday') { 21 | expect(radioButton).toBeChecked(); 22 | } else { 23 | expect(radioButton).not.toBeChecked(); 24 | fireEvent.click(radioButton); 25 | expect(radioButton).toBeChecked(); 26 | } 27 | }); 28 | }); 29 | 30 | it('should render multiselect', () => { 31 | setup(); 32 | const input = screen.getByText('Custom period'); 33 | expect(input).toBeVisible(); 34 | }) 35 | -------------------------------------------------------------------------------- /src/containers/Menubar/Menubar.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Menu from '../../components/Menubar/Menubar'; 3 | 4 | const Menubar: React.FC = () => { 5 | 6 | const items: object[] = [ 7 | { 8 | label: 'Chmod Calculator', 9 | icon: 'pi pi-angle-right', 10 | url: '#/chmod_calculator' 11 | }, 12 | { 13 | label: 'Crontab Generator', 14 | icon: 'pi pi-calendar', 15 | url: '#/crontab_generator' 16 | }, 17 | { 18 | label: 'Unix Timestamp Converter', 19 | icon: 'pi pi-clock', 20 | url: '#/unix_timestamp_converter' 21 | }, 22 | { 23 | label: 'Password Generator', 24 | icon: 'pi pi-shield', 25 | url: '#/password_generator' 26 | } 27 | ]; 28 | 29 | const onLogoClick = () => { 30 | window.location.hash = '/' 31 | }; 32 | const onGitHubClick = () => { 33 | window.open('https://github.com/Coderberg/web-developer-tools') 34 | }; 35 | 36 | return 38 | } 39 | 40 | export default Menubar; 41 | -------------------------------------------------------------------------------- /src/components/EpochConverter/Forms/UnixTimeForm.tsx: -------------------------------------------------------------------------------- 1 | import {InputText} from "primereact/inputtext"; 2 | import {Button} from "primereact/button"; 3 | import React from "react"; 4 | import {UnixTimeInputPropsInterface} from "../../../types/epochConverter"; 5 | 6 | const UnixTimeForm: React.FC = 7 | ({ 8 | unixTimeInputValue, onUnixTimeChange, 9 | onEpochToDateButtonClick, epochToDateResult 10 | }) => 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 21 | 22 | 23 | {epochToDateResult && 24 | {epochToDateResult} 25 | } 26 | 27 | 28 | export default UnixTimeForm; 29 | -------------------------------------------------------------------------------- /src/tests/containers/EpochConverter/CurrentEpochTime.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen, waitFor} from "@testing-library/react"; 2 | import React from "react"; 3 | import { Provider } from "react-redux"; 4 | import CurrentEpochTime from "../../../containers/EpochConverter/CurrentEpochTime"; 5 | import store from "../../../store/configureStore"; 6 | 7 | test('is able to see current unix epoch time', () => { 8 | render(); 9 | expect(screen.getByText('The current Unix epoch time is')).toBeInTheDocument(); 10 | expect(screen.getByText((Math.floor(Date.now() / 1000)).toString())).toBeInTheDocument(); 11 | }); 12 | 13 | test('is able to copy current unix epoch time to clipboard', async() => { 14 | render(); 15 | 16 | Object.defineProperty(navigator, "clipboard", { 17 | value: { 18 | writeText: () => {}, 19 | }, 20 | }); 21 | 22 | const btn = screen.getByText("Copy"); 23 | fireEvent.click(btn); 24 | expect(screen.getByText('Copied')).toBeInTheDocument(); 25 | 26 | await waitFor(() => { 27 | expect(screen.getByText('Copy')).toBeInTheDocument(); 28 | }, {timeout: 6000}); 29 | }, 7000); 30 | -------------------------------------------------------------------------------- /src/tests/components/PasswordGenerator/FormFields/Checkbox.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import Checkbox 4 | from "../../../../components/PasswordGenerator/FormFields/Checkbox"; 5 | 6 | const checked = { 7 | symbols: true, 8 | numbers: false, 9 | uppercase: true, 10 | lowercase: false, 11 | excludeSimilar: true 12 | } 13 | 14 | const onCheckboxChange = jest.fn(); 15 | 16 | it('shows the checked checkbox', () => { 17 | render(); 19 | const checkbox = screen.getByLabelText('Exclude Similar Characters'); 20 | expect(checkbox).toBeVisible(); 21 | expect(checkbox).toBeChecked(); 22 | fireEvent.click(checkbox); 23 | expect(onCheckboxChange).toBeCalledTimes(1) 24 | }); 25 | 26 | it('shows the unchecked checkbox', () => { 27 | render(); 29 | const checkbox = screen.getByLabelText('Numbers'); 30 | expect(checkbox).toBeVisible(); 31 | expect(checkbox).not.toBeChecked(); 32 | }); 33 | -------------------------------------------------------------------------------- /src/tests/reducers/EpochConverter/unixTimeForm.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | UnixTimeFormStateType 3 | } from "../../../types/epochConverter"; 4 | import unixTimeForm from "../../../reducers/EpochConverter/unixTimeForm"; 5 | import { 6 | setEpochToDateResult, 7 | setUnixTimeInputValue 8 | } from "../../../actions/EpochConverter/unixTimeForm"; 9 | 10 | describe('unix time form reducer', () => { 11 | 12 | const initialState: UnixTimeFormStateType = { 13 | unixTimeInputValue: '*', 14 | epochToDateResult: '', 15 | } 16 | 17 | it("should handle initial state", () => { 18 | expect(unixTimeForm(undefined, { 19 | type: "undefined", 20 | payload: "" 21 | })).toEqual(initialState); 22 | }); 23 | 24 | it("should unix time change", () => { 25 | const actual = unixTimeForm(initialState, setUnixTimeInputValue('1639937024')); 26 | expect(actual.unixTimeInputValue).toEqual('1639937024'); 27 | }); 28 | 29 | it("should handle result change", () => { 30 | const actual = unixTimeForm(initialState, setEpochToDateResult(new Date('December 22, 2021').toString())); 31 | expect(actual.epochToDateResult).toEqual(new Date('December 22, 2021').toString()); 32 | }); 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /src/tests/utils/calculators/chmodCalculator.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | checkboxesToChmodNumber, chmodNumberToString, chmodStringToCheckboxes 3 | } from "../../../utils/calculators/chmodCalculator"; 4 | 5 | const validPairs = [ 6 | {'number': '000', 'string': '---------'}, 7 | {'number': '644', 'string': 'rw-r--r--'}, 8 | {'number': '444', 'string': 'r--r--r--'}, 9 | {'number': '666', 'string': 'rw-rw-rw-'}, 10 | {'number': '755', 'string': 'rwxr-xr-x'}, 11 | {'number': '777', 'string': 'rwxrwxrwx'} 12 | ]; 13 | 14 | describe('chmod number to chmod symbolic value', () => { 15 | 16 | validPairs.forEach((pair) => { 17 | test(`The chmod number ${pair.number} is correctly converted to ${pair.string}`, () => { 18 | expect(chmodNumberToString(pair.number)) 19 | .toStrictEqual(pair.string); 20 | }); 21 | }); 22 | }); 23 | 24 | describe('chmod symbolic value to chmod number', () => { 25 | 26 | validPairs.forEach((pair) => { 27 | test(`The chmod symbolic value "${pair.number}" is correctly converted to ${pair.string}`, () => { 28 | expect(checkboxesToChmodNumber(chmodStringToCheckboxes(pair.string))) 29 | .toStrictEqual(pair.number); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import './App.css'; 3 | import 'primereact/resources/themes/arya-orange/theme.css' 4 | import 'primereact/resources/primereact.min.css' 5 | import 'primeicons/primeicons.css' 6 | import 'primeflex/primeflex.css'; 7 | import {HashRouter as Router, Routes, Route} from 'react-router-dom'; 8 | import Menubar from "./containers/Menubar/Menubar"; 9 | import ChmodCalculator from "./containers/ChmodCalculator/ChmodCalculator"; 10 | import EpochConverter from "./components/EpochConverter/EpochConverter"; 11 | import CrontabGenerator from "./components/CrontabGenerator/CrontabGenerator"; 12 | import PasswordGenerator 13 | from "./containers/PasswordGenerator/PasswordGenerator"; 14 | 15 | const App = () => 16 | 17 | 18 | 19 | }/> 20 | }/> 21 | }/> 22 | }/> 23 | }/> 24 | 25 | 26 | 27 | 28 | export default App; 29 | -------------------------------------------------------------------------------- /src/tests/reducers/EpochConverter/currentEpochTime.test.ts: -------------------------------------------------------------------------------- 1 | import currentEpochTime 2 | from "../../../reducers/EpochConverter/currentEpochTime"; 3 | import { 4 | setCopyToClipboardButtonState, 5 | setCurrentUnixTime 6 | } from "../../../actions/EpochConverter/currentEpochTime"; 7 | import {CurrentEpochTimeContainerState} from "../../../types/epochConverter"; 8 | 9 | describe('current epoch time reducer', () => { 10 | 11 | const initialState: CurrentEpochTimeContainerState = { 12 | currentUnixTime: '', 13 | copyToClipboardButtonState: 'default', 14 | } 15 | 16 | it("should handle initial state", () => { 17 | expect(currentEpochTime(undefined, { 18 | type: "undefined", 19 | payload: "" 20 | })).toEqual(initialState); 21 | }); 22 | 23 | it("should handle current unix time change", () => { 24 | const actual = currentEpochTime(initialState, setCurrentUnixTime('1639934047')); 25 | expect(actual.currentUnixTime).toEqual('1639934047'); 26 | }); 27 | 28 | it("should handle 'copy to clipboard' button state change", () => { 29 | const actual = currentEpochTime(initialState, setCopyToClipboardButtonState('pressed')); 30 | expect(actual.copyToClipboardButtonState).toEqual('pressed'); 31 | }); 32 | 33 | }); 34 | -------------------------------------------------------------------------------- /src/tests/utils/calculators/epochConverter.test.ts: -------------------------------------------------------------------------------- 1 | import {epochToHumanReadableDate, humanReadableDateToEpoch} from "../../../utils/calculators/epochConverter"; 2 | 3 | const validPairs = [ 4 | {humanDate: '1971-11-12T17:24:15', epoch: '58814655'}, 5 | {humanDate: '1995-10-28T12:10:19', epoch: '814882219'}, 6 | {humanDate: '2021-11-06T11:49:54', epoch: '1636199394'}, 7 | ]; 8 | 9 | describe('human date to timestamp', () => { 10 | validPairs.forEach((pair) => { 11 | test(`The human readable date ${pair.humanDate} is correctly converted to ${pair.epoch}`, () => { 12 | const dateString = new Date(pair.humanDate).toString() 13 | expect(humanReadableDateToEpoch(dateString, 'gmt')) 14 | .toStrictEqual(pair.epoch); 15 | }); 16 | }); 17 | }); 18 | 19 | describe('timestamp to human date', () => { 20 | validPairs.forEach((pair) => { 21 | test(`The timestamp ${pair.epoch} is correctly converted to ${pair.humanDate}`, () => { 22 | const expectedDate = new Date(pair.humanDate); 23 | const offset = expectedDate.getTimezoneOffset() * 60 24 | const date = new Date(epochToHumanReadableDate((Number(pair.epoch) + offset).toString())); 25 | expect(date).toStrictEqual(expectedDate); 26 | }); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/commandToExecute.test.ts: -------------------------------------------------------------------------------- 1 | import commandToExecute 2 | from "../../../reducers/CrontabGenerator/commandToExecute"; 3 | import { 4 | setCommandHasError, 5 | setCommandToExecuteValue 6 | } from "../../../actions/CrontabGenerator/commandToExecute"; 7 | import {CommandToExecuteContainerState} from "../../../types/crontabGenerator"; 8 | 9 | describe('commandToExecute reducer', () => { 10 | 11 | const initialState: CommandToExecuteContainerState = { 12 | commandToExecuteValue: '', 13 | commandHasError: false, 14 | } 15 | 16 | it("should handle initial state", () => { 17 | expect(commandToExecute(undefined, { 18 | type: "undefined", 19 | payload: "" 20 | })).toEqual(initialState); 21 | }); 22 | 23 | it("should handle command change", () => { 24 | const command = '/usr/bin/php /var/www/laravel/artisan schedule:run'; 25 | const actual = commandToExecute(initialState, setCommandToExecuteValue(command)); 26 | expect(actual.commandToExecuteValue).toEqual(command); 27 | }); 28 | 29 | it("should handle 'commandHasError' value change", () => { 30 | const actual = commandToExecute(initialState, setCommandHasError(true)); 31 | expect(actual.commandHasError).toEqual(true); 32 | }); 33 | }) 34 | -------------------------------------------------------------------------------- /src/components/ChmodCalculator/ChmodCalculator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {InputText} from "primereact/inputtext"; 3 | import {Fieldset} from "primereact/fieldset"; 4 | import 'primeflex/primeflex.css'; 5 | import {ChmodCalculatorPropsInterface} from "../../types/chmodCalculator"; 6 | import Checkboxes from "./Checkboxes/Checkboxes"; 7 | 8 | const ChmodCalculator: React.FC = 9 | ({ 10 | checked, onCheckboxChange, numberFieldValue, 11 | onNumberChange, textFieldValue, onTextChange 12 | }) => 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | export default ChmodCalculator; 30 | -------------------------------------------------------------------------------- /src/tests/components/PasswordGenerator/FormFields/Checkboxes.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import Checkboxes 4 | from "../../../../components/PasswordGenerator/FormFields/Checkboxes"; 5 | 6 | const checked = { 7 | symbols: true, 8 | numbers: true, 9 | uppercase: true, 10 | lowercase: false, 11 | excludeSimilar: false 12 | } 13 | 14 | const onCheckboxChange = jest.fn(); 15 | 16 | it('shows the checked checkbox', () => { 17 | render(); 18 | const checkbox = screen.getByLabelText('Symbols'); 19 | expect(checkbox).toBeVisible(); 20 | expect(checkbox).toBeChecked(); 21 | }); 22 | 23 | it('shows the unchecked checkbox', () => { 24 | render(); 25 | const checkbox = screen.getByLabelText('Exclude Similar Characters'); 26 | expect(checkbox).toBeVisible(); 27 | expect(checkbox).not.toBeChecked(); 28 | }); 29 | 30 | it('should handle checkbox change', () => { 31 | render(); 32 | const checkbox = screen.getByLabelText('Lowercase'); 33 | fireEvent.click(checkbox); 34 | expect(onCheckboxChange).toBeCalledTimes(1) 35 | }); 36 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/HowToHandleOutput/FileOrEmail.tsx: -------------------------------------------------------------------------------- 1 | import {RadioButton} from "primereact/radiobutton"; 2 | import {InputText} from "primereact/inputtext"; 3 | import React from "react"; 4 | import {FileOrEmailProps} from "../../../../types/crontabGenerator"; 5 | 6 | const FileOrEmail: React.FC = 7 | ({ 8 | name, 9 | label, 10 | radioButtonValue, 11 | textInputValue, 12 | radioButtonOnChange, 13 | textInputOnChange 14 | }) => 15 | 16 | 17 | 18 | 19 | 22 | 23 | {label} 24 | 25 | 26 | 27 | 28 | 31 | 32 | 33 | 34 | 35 | export default FileOrEmail; 36 | -------------------------------------------------------------------------------- /src/types/chmodCalculator.ts: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export type CheckboxesType = { 4 | [key: string]: boolean 5 | ownerRead: boolean, 6 | ownerWrite: boolean, 7 | ownerExecute: boolean, 8 | groupRead: boolean, 9 | groupWrite: boolean, 10 | groupExecute: boolean, 11 | othersRead: boolean, 12 | othersWrite: boolean, 13 | othersExecute: boolean, 14 | } 15 | 16 | export type ChmodCalculatorContainerStateType = { 17 | checked: CheckboxesType, 18 | numberFieldValue: string, 19 | textFieldValue: string, 20 | } 21 | 22 | export interface ChmodCalculatorContainerPropsInterface extends ChmodCalculatorContainerStateType { 23 | setCheckedValue: Function, 24 | setNumberValue: Function, 25 | setTextValue: Function 26 | } 27 | 28 | export interface ChmodCalculatorPropsInterface extends ChmodCalculatorContainerStateType { 29 | onCheckboxChange: Function, 30 | onNumberChange: React.ChangeEventHandler, 31 | onTextChange: React.ChangeEventHandler 32 | } 33 | 34 | export type CheckboxesPropsType = { 35 | checked: CheckboxesType, 36 | handleOnChange: any 37 | } 38 | 39 | export interface CheckboxGroupType extends CheckboxesPropsType { 40 | ownership: string 41 | } 42 | 43 | export interface CheckboxPropsType extends CheckboxGroupType { 44 | permission: string 45 | } 46 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/CrontabGenerator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {Fieldset} from "primereact/fieldset"; 3 | import 'primeflex/primeflex.css'; 4 | import Minutes from "../../containers/CrontabGenerator/Schedule/Minutes"; 5 | import Hours from "../../containers/CrontabGenerator/Schedule/Hours"; 6 | import Days from "../../containers/CrontabGenerator/Schedule/Days"; 7 | import Months from "../../containers/CrontabGenerator/Schedule/Months"; 8 | import Weekday from "../../containers/CrontabGenerator/Schedule/Weekday"; 9 | import CommandToExecute from "../../containers/CrontabGenerator/CommandToExecute"; 10 | import HowToHandleOutput from "../../containers/CrontabGenerator/HowToHandleOutput"; 11 | import GenerateCrontabLine from "../../containers/CrontabGenerator/GenerateCrontabLine"; 12 | 13 | const CrontabGenerator: React.FC = () => 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | export default CrontabGenerator; 33 | -------------------------------------------------------------------------------- /src/components/PasswordGenerator/GenerateAndCopy.tsx: -------------------------------------------------------------------------------- 1 | import {Chip} from "primereact/chip"; 2 | import {Button} from "primereact/button"; 3 | import React from "react"; 4 | import CopyToClipboard from "../common/CopyToClipboard"; 5 | import {GenerateAndCopyComponentProps} from "../../types/passwordGenerator"; 6 | 7 | const GenerateAndCopy = ({ 8 | result, 9 | copyToClipboardButtonState, 10 | onGenerateButtonClick, 11 | onCopyButtonClick 12 | }: GenerateAndCopyComponentProps) => 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 34 | export default GenerateAndCopy; 35 | -------------------------------------------------------------------------------- /src/tests/utils/validators/chmodValidator.test.ts: -------------------------------------------------------------------------------- 1 | import {isNumberValid, isStringValid} from "../../../utils/validators/chmodValidator"; 2 | 3 | describe('validate chmod number', () => { 4 | 5 | const validValues = ['000', '644', '666', '755', '777']; 6 | 7 | validValues.forEach((number) => { 8 | test(`${number} is valid chmod number`, () => { 9 | expect(isNumberValid(number)).toBe(true); 10 | }); 11 | }); 12 | 13 | const invalidValues = ['778', '0666', '7755', 'abc', '---', '008', '955']; 14 | 15 | invalidValues.forEach((number) => { 16 | test(`${number} is invalid chmod number`, () => { 17 | expect(isNumberValid(number)).toBe(false); 18 | }); 19 | }); 20 | 21 | }); 22 | 23 | describe('validate chmod string', () => { 24 | 25 | const validValues = ['---------', 'rw-r--r--', 'rwxrwxrwx', 'r-x-w----', 'rwx---rwx']; 26 | 27 | validValues.forEach((string) => { 28 | test(`${string} is valid chmod string`, () => { 29 | expect(isStringValid(string)).toBe(true); 30 | }); 31 | }); 32 | 33 | const invalidValues = ['----------', 'xrwxrwxrw', 'rwxrwxc--', 'xxxxxxxxx', 'wwwwwwwww']; 34 | 35 | invalidValues.forEach((string) => { 36 | test(`${string} is invalid chmod number`, () => { 37 | expect(isStringValid(string)).toBe(false); 38 | }); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/components/PasswordGenerator/GenerateAndCopy.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import GenerateAndCopy 4 | from "../../../components/PasswordGenerator/GenerateAndCopy"; 5 | 6 | const onGenerateButtonClick = jest.fn(); 7 | const onCopyButtonClick = jest.fn(); 8 | const password = "yUTBDPggki"; 9 | 10 | const setup = () => { 11 | render() 17 | } 18 | 19 | describe(' component', () => { 20 | 21 | it('shows the password', () => { 22 | setup(); 23 | expect(screen.getByText(password)).toBeVisible(); 24 | }); 25 | 26 | it('should handle Copy button click', () => { 27 | setup(); 28 | const button = screen.getByText('Copy'); 29 | expect(button).toBeVisible(); 30 | fireEvent.click(button); 31 | expect(onCopyButtonClick).toBeCalledTimes(1) 32 | }); 33 | 34 | it('should handle Generate button click', () => { 35 | setup(); 36 | const button = screen.getByText('Generate'); 37 | expect(button).toBeVisible(); 38 | fireEvent.click(button); 39 | expect(onGenerateButtonClick).toBeCalledTimes(1) 40 | }); 41 | 42 | }); 43 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "react-app", 4 | "airbnb", 5 | "plugin:jsx-a11y/recommended", 6 | "prettier", 7 | "plugin:testing-library/react" 8 | ], 9 | "plugins": [ 10 | "jsx-a11y", 11 | "prettier", 12 | "testing-library" 13 | ], 14 | "overrides": [ 15 | { 16 | "files": ["**/tests/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], 17 | "extends": ["plugin:testing-library/react"] 18 | } 19 | ], 20 | "rules": { 21 | "@typescript-eslint/no-use-before-define": ["error"], 22 | "no-use-before-define": "off", 23 | "react/jsx-filename-extension": [2, { "extensions": [".js", ".jsx", ".ts", ".tsx"] }], 24 | "testing-library/await-async-query": "error", 25 | "testing-library/no-await-sync-query": "error", 26 | "testing-library/no-debugging-utils": "warn", 27 | "testing-library/no-dom-import": "off", 28 | "import/extensions": [ 29 | "error", 30 | "ignorePackages", 31 | { 32 | "js": "never", 33 | "jsx": "never", 34 | "ts": "never", 35 | "tsx": "never" 36 | } 37 | ] 38 | }, 39 | "settings": { 40 | "import/resolver": { 41 | "node": { 42 | "extensions": [".js", ".jsx", ".ts", ".tsx"] 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/HowToHandleOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {HowToHandleOutputProps} from "../../../types/crontabGenerator"; 3 | import Mute from "./HowToHandleOutput/Mute"; 4 | import FileOrEmail from "./HowToHandleOutput/FileOrEmail"; 5 | 6 | const HowToHandleOutput: React.FC = 7 | ({ 8 | selectedRadio, 9 | pathToFileValue, 10 | emailValue, 11 | onRadioButtonChange, 12 | onPathToFileChange, 13 | onEmailChange 14 | }) => 15 | 16 | 17 | How to Handle Execution Output 18 | 19 | 20 | 21 | 26 | 27 | 32 | 33 | 34 | 35 | export default HowToHandleOutput; 36 | -------------------------------------------------------------------------------- /src/reducers/ChmodCalculator/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_CHECKED, 3 | SET_NUMBER_FIELD_VALUE, 4 | SET_TEXT_FIELD_VALUE 5 | } from "../../constants/ActionTypes"; 6 | import { 7 | CheckboxesType, 8 | ChmodCalculatorContainerStateType 9 | } from "../../types/chmodCalculator"; 10 | 11 | const initialState: ChmodCalculatorContainerStateType = { 12 | checked: { 13 | ownerRead: false, 14 | ownerWrite: false, 15 | ownerExecute: false, 16 | groupRead: false, 17 | groupWrite: false, 18 | groupExecute: false, 19 | othersRead: false, 20 | othersWrite: false, 21 | othersExecute: false, 22 | }, 23 | numberFieldValue: '000', 24 | textFieldValue: '---------' 25 | } 26 | 27 | const chmodCalculator = (state = initialState, action: { type: string; payload: CheckboxesType | string }) => { 28 | 29 | switch (action.type) { 30 | 31 | case SET_CHECKED: { 32 | return { 33 | ...state, checked: action.payload 34 | }; 35 | } 36 | case SET_NUMBER_FIELD_VALUE: { 37 | return { 38 | ...state, numberFieldValue: action.payload 39 | }; 40 | } 41 | case SET_TEXT_FIELD_VALUE: { 42 | return { 43 | ...state, textFieldValue: action.payload 44 | }; 45 | } 46 | default: 47 | return state; 48 | } 49 | } 50 | 51 | export default chmodCalculator; 52 | -------------------------------------------------------------------------------- /src/tests/components/ChmodCalculator/ChmodCalculator..test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {render, screen} from "@testing-library/react"; 3 | import ChmodCalculator from "../../../components/ChmodCalculator/ChmodCalculator"; 4 | 5 | const checked = { 6 | ownerRead: false, 7 | ownerWrite: false, 8 | ownerExecute: false, 9 | groupRead: false, 10 | groupWrite: false, 11 | groupExecute: false, 12 | othersRead: false, 13 | othersWrite: false, 14 | othersExecute: false 15 | } 16 | 17 | const setup = () => render( {}} onNumberChange={() => {}} onCheckboxChange={() => {}} 21 | />) 22 | 23 | it('shows the user types', () => { 24 | setup(); 25 | ['owner', 'group', 'others'].forEach((item) => { 26 | expect(screen.getByText(item)).toBeInTheDocument() 27 | }); 28 | }); 29 | 30 | it('shows the checkboxes', () => { 31 | setup(); 32 | ['Read', 'Write', 'Execute'].forEach((permission) => { 33 | (screen.getAllByLabelText(permission)).forEach((checkbox) => { 34 | expect(checkbox).toBeInTheDocument(); 35 | expect(checkbox).not.toBeChecked(); 36 | }); 37 | }); 38 | }); 39 | 40 | it('shows the text fields', () => { 41 | setup(); 42 | expect(screen.getByPlaceholderText('rw-rw-rw-')).toBeVisible(); 43 | expect(screen.getByPlaceholderText('666')).toBeVisible(); 44 | }); 45 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/Schedule/days.test.ts: -------------------------------------------------------------------------------- 1 | import days from "../../../../reducers/CrontabGenerator/Schedule/days"; 2 | import { 3 | setDayHasError, 4 | setDaysOptionValue, 5 | setDaysRadioButtonValue 6 | } from "../../../../actions/CrontabGenerator/Schedule/days"; 7 | import {PeriodContainerStateInterface} from "../../../../types/crontabGenerator"; 8 | 9 | describe('days reducer', () => { 10 | 11 | const initialState: PeriodContainerStateInterface = { 12 | selectedRadio: 'Every Day', 13 | selectedOption: [], 14 | hasError: false, 15 | } 16 | 17 | it("should handle initial state", () => { 18 | expect(days(undefined, { 19 | type: "undefined", 20 | payload: "" 21 | })).toEqual(initialState); 22 | }); 23 | 24 | it("should handle radio button change", () => { 25 | const actual = days(initialState, setDaysRadioButtonValue('Every Half Month')); 26 | expect(actual.selectedRadio).toEqual('Every Half Month'); 27 | }); 28 | 29 | const selectedOptions = [{name: 6, code: 6}]; 30 | 31 | it("should handle option change", () => { 32 | const actual = days(initialState, setDaysOptionValue(selectedOptions)); 33 | expect(actual.selectedOption).toEqual(selectedOptions); 34 | }); 35 | 36 | it("should handle 'hasError' value change", () => { 37 | const actual = days(initialState, setDayHasError(true)); 38 | expect(actual.hasError).toEqual(true); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/reducers/EpochConverter/dateTimeForm.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DateTimeFormState 3 | } from "../../../types/epochConverter"; 4 | import dateTimeForm from "../../../reducers/EpochConverter/dateTimeForm"; 5 | import { 6 | setDateTimeInputValue, setDateToEpochResult, 7 | setTimezoneInputValue 8 | } from "../../../actions/EpochConverter/dateTimeForm"; 9 | 10 | describe('date time form reducer', () => { 11 | 12 | const initialState: DateTimeFormState = { 13 | dateTimeInputValue: undefined, 14 | timezoneInputValue: 'local', 15 | dateToEpochResult: '', 16 | } 17 | 18 | it("should handle initial state", () => { 19 | expect(dateTimeForm(undefined, { 20 | type: "undefined", 21 | payload: "" 22 | })).toEqual(initialState); 23 | }); 24 | 25 | it("should handle date time change", () => { 26 | const actual = dateTimeForm(initialState, setDateTimeInputValue(new Date('December 22, 2021'))); 27 | expect(actual.dateTimeInputValue).toEqual(new Date('December 22, 2021')); 28 | }); 29 | 30 | it("should handle Timezone change", () => { 31 | const actual = dateTimeForm(initialState, setTimezoneInputValue('gmt')); 32 | expect(actual.timezoneInputValue).toEqual('gmt'); 33 | }); 34 | 35 | it("should handle result change", () => { 36 | const actual = dateTimeForm(initialState, setDateToEpochResult('1639935411')); 37 | expect(actual.dateToEpochResult).toEqual('1639935411'); 38 | }); 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /src/selectors/crontabGenerator.js: -------------------------------------------------------------------------------- 1 | import transform from "../utils/transformers/humanReadableToCrontab"; 2 | 3 | const getMinutes = formState => [ 4 | formState.minutes.selectedRadio, 5 | formState.minutes.selectedOption, 6 | ] 7 | 8 | const getHours = formState => [ 9 | formState.hours.selectedRadio, 10 | formState.hours.selectedOption, 11 | ] 12 | 13 | const getDays = formState => [ 14 | formState.days.selectedRadio, 15 | formState.days.selectedOption, 16 | ] 17 | 18 | const getMonths = formState => [ 19 | formState.months.selectedRadio, 20 | formState.months.selectedOption, 21 | ] 22 | 23 | const getWeekday = formState => [ 24 | formState.weekday.selectedRadio, 25 | formState.weekday.selectedOption, 26 | ] 27 | 28 | const getCommandToExecute = formState => 29 | formState.commandToExecute.commandToExecuteValue 30 | 31 | const getWayToHandleOutput = formState => [ 32 | formState.howToHandleOutput.howToHandleOutputValue, 33 | formState.howToHandleOutput.pathToFileValue, 34 | formState.howToHandleOutput.emailValue, 35 | ] 36 | 37 | export const getResult = state => 38 | state.crontabGenerator.generateCrontabLine.result 39 | 40 | export const getFormState = state => 41 | state.crontabGenerator 42 | 43 | export const getCronExpression = formState => transform( 44 | ...getMinutes(formState), 45 | ...getHours(formState), 46 | ...getDays(formState), 47 | ...getMonths(formState), 48 | ...getWeekday(formState), 49 | getCommandToExecute(formState), 50 | ...getWayToHandleOutput(formState) 51 | ) 52 | -------------------------------------------------------------------------------- /src/reducers/PasswordGenerator/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SET_PASSWORD, 3 | SET_PASSWORD_LENGTH, 4 | SET_CHECKED_PARAM, 5 | SET_COPY_TO_CLIPBOARD_STATE, 6 | } from "../../constants/ActionTypes"; 7 | import { 8 | CheckboxesType, 9 | PasswordGeneratorStateInterface 10 | } from "../../types/passwordGenerator"; 11 | 12 | const initialState: PasswordGeneratorStateInterface = { 13 | checked: { 14 | symbols: true, 15 | numbers: true, 16 | uppercase: true, 17 | lowercase: true, 18 | excludeSimilar: false, 19 | }, 20 | passwordLength: '16', 21 | copyToClipboardButtonState: 'default', 22 | result: '', 23 | } 24 | 25 | const passwordGenerator = (state = initialState, action: { type: string; payload: CheckboxesType | string }) => { 26 | 27 | switch (action.type) { 28 | case SET_PASSWORD: { 29 | return { 30 | ...state, result: action.payload 31 | }; 32 | } 33 | case SET_PASSWORD_LENGTH: { 34 | return { 35 | ...state, passwordLength: action.payload 36 | }; 37 | } 38 | case SET_CHECKED_PARAM: { 39 | return { 40 | ...state, checked: action.payload 41 | }; 42 | } 43 | case SET_COPY_TO_CLIPBOARD_STATE: { 44 | return { 45 | ...state, copyToClipboardButtonState: action.payload 46 | }; 47 | } 48 | default: 49 | return state; 50 | } 51 | } 52 | 53 | export default passwordGenerator; 54 | -------------------------------------------------------------------------------- /src/types/passwordGenerator.ts: -------------------------------------------------------------------------------- 1 | import {MouseEventHandler} from "react"; 2 | 3 | export type CheckboxesType = { 4 | [key: string]: boolean 5 | symbols: boolean, 6 | numbers: boolean, 7 | uppercase: boolean, 8 | lowercase: boolean, 9 | excludeSimilar: boolean 10 | } 11 | 12 | export type ActionButtonsType = { 13 | onGenerateButtonClick: MouseEventHandler, 14 | onCopyButtonClick: MouseEventHandler 15 | } 16 | 17 | export type CheckboxesPropsType = { 18 | checked: CheckboxesType, 19 | onCheckboxChange: any 20 | } 21 | 22 | export interface CheckboxPropsInterface extends CheckboxesPropsType { 23 | label: string, 24 | id: string 25 | } 26 | 27 | export interface PasswordGeneratorStateInterface { 28 | checked: CheckboxesType, 29 | passwordLength: string, 30 | copyToClipboardButtonState: 'default' | 'pressed', 31 | result: string 32 | } 33 | 34 | export interface GenerateAndCopyComponentProps extends ActionButtonsType { 35 | result: string, 36 | copyToClipboardButtonState: 'default' | 'pressed' 37 | } 38 | 39 | export interface PasswordGeneratorContainerStateInterface 40 | extends PasswordGeneratorStateInterface { 41 | setPasswordValue: Function, 42 | setPasswordLengthValue: Function, 43 | setChecked: Function, 44 | setCopyToClipboardButtonState: Function 45 | } 46 | 47 | export interface PasswordGeneratorPropsInterface 48 | extends PasswordGeneratorStateInterface,ActionButtonsType { 49 | onCheckboxChange: any, 50 | onPasswordLengthChange: Function 51 | } 52 | -------------------------------------------------------------------------------- /src/tests/components/CrontabGenerator/FormFields/HowToHandleOutput/FileOrEmail.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import FileOrEmail 4 | from "../../../../../components/CrontabGenerator/FormFields/HowToHandleOutput/FileOrEmail"; 5 | 6 | const onChange = jest.fn(); 7 | 8 | const setup = (value: string) => { 9 | render() 16 | } 17 | 18 | describe(' component', () => { 19 | 20 | it('should render radio button & text field', () => { 21 | setup('email') 22 | expect(screen.getByText(/Send output to Email/i)).toBeInTheDocument(); 23 | expect(screen.getByLabelText(/Send output to Email/i)).toBeChecked(); 24 | }); 25 | 26 | it('should simulate radio button change', () => { 27 | setup('') 28 | const radioButton = screen.getByLabelText(/Send output to Email/i); 29 | expect(radioButton).not.toBeChecked(); 30 | fireEvent.click(radioButton); 31 | expect(onChange).toBeCalledTimes(1) 32 | }); 33 | 34 | it('should simulate input change', () => { 35 | setup('email') 36 | const input = screen.getByDisplayValue("info@example.com"); 37 | fireEvent.change(input, {target: {value: 'mail@example.com'}}) 38 | expect(onChange).toBeCalledTimes(1); 39 | }); 40 | 41 | }); 42 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/howToHandleOutput.test.ts: -------------------------------------------------------------------------------- 1 | import howToHandleOutput 2 | from "../../../reducers/CrontabGenerator/howToHandleOutput"; 3 | import { 4 | setEmailValue, 5 | setHowToHandleOutputValue, 6 | setPathToFileValue 7 | } from "../../../actions/CrontabGenerator/howToHandleOutput"; 8 | import {HowToHandleOutputContainerState} from "../../../types/crontabGenerator"; 9 | 10 | describe('howToHandleOutput reducer', () => { 11 | 12 | const initialState: HowToHandleOutputContainerState = { 13 | howToHandleOutputValue: 'mute', 14 | pathToFileValue: '', 15 | emailValue: '', 16 | } 17 | 18 | it("should handle initial state", () => { 19 | expect(howToHandleOutput(undefined, { 20 | type: "undefined", 21 | payload: "" 22 | })).toEqual(initialState); 23 | }); 24 | 25 | it("should handle 'How to Handle Execution Output' value change", () => { 26 | const actual = howToHandleOutput(initialState, setHowToHandleOutputValue('email')); 27 | expect(actual.howToHandleOutputValue).toEqual('email'); 28 | }); 29 | 30 | it("should handle 'Path To File' value change", () => { 31 | const actual = howToHandleOutput(initialState, setPathToFileValue('cron.log')); 32 | expect(actual.pathToFileValue).toEqual('cron.log'); 33 | }); 34 | 35 | it("should handle email address change", () => { 36 | const actual = howToHandleOutput(initialState, setEmailValue('info@reactjs.org')); 37 | expect(actual.emailValue).toEqual('info@reactjs.org'); 38 | }); 39 | }) 40 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/Schedule/hourss.test.ts: -------------------------------------------------------------------------------- 1 | import hours from "../../../../reducers/CrontabGenerator/Schedule/hours"; 2 | import { 3 | setHourHasError, 4 | setHoursOptionValue, 5 | setHoursRadioButtonValue 6 | } from "../../../../actions/CrontabGenerator/Schedule/hours"; 7 | import {PeriodContainerStateInterface} from "../../../../types/crontabGenerator"; 8 | 9 | describe('hours reducer', () => { 10 | 11 | const initialState: PeriodContainerStateInterface = { 12 | selectedRadio: 'Every Hour', 13 | selectedOption: [], 14 | hasError: false, 15 | } 16 | 17 | it("should handle initial state", () => { 18 | expect(hours(undefined, { 19 | type: "undefined", 20 | payload: "" 21 | })).toEqual(initialState); 22 | }); 23 | 24 | it("should handle radio button change", () => { 25 | const actual = hours(initialState, setHoursRadioButtonValue('Even Hours')); 26 | expect(actual.selectedRadio).toEqual('Even Hours'); 27 | }); 28 | 29 | const selectedOptions = [ 30 | {name: 'Midnight', code: 'Midnight'}, 31 | {name: '5am', code: '5am'} 32 | ]; 33 | 34 | it("should handle option change", () => { 35 | const actual = hours(initialState, setHoursOptionValue(selectedOptions)); 36 | expect(actual.selectedOption).toEqual(selectedOptions); 37 | }); 38 | 39 | it("should handle 'hasError' value change", () => { 40 | const actual = hours(initialState, setHourHasError(true)); 41 | expect(actual.hasError).toEqual(true); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/Schedule/minutes.test.ts: -------------------------------------------------------------------------------- 1 | import minutes from "../../../../reducers/CrontabGenerator/Schedule/minutes"; 2 | import { 3 | setMinuteHasError, setMinutesOptionValue, 4 | setMinutesRadioButtonValue 5 | } from "../../../../actions/CrontabGenerator/Schedule/minutes"; 6 | import {PeriodContainerStateInterface} from "../../../../types/crontabGenerator"; 7 | 8 | describe('minutes reducer', () => { 9 | 10 | const initialState: PeriodContainerStateInterface = { 11 | selectedRadio: 'Every Minute', 12 | selectedOption: [], 13 | hasError: false, 14 | } 15 | 16 | it("should handle initial state", () => { 17 | expect(minutes(undefined, { 18 | type: "undefined", 19 | payload: "" 20 | })).toEqual(initialState); 21 | }); 22 | 23 | it("should handle radio button change", () => { 24 | const actual = minutes(initialState, setMinutesRadioButtonValue('Every 5 Minutes')); 25 | expect(actual.selectedRadio).toEqual('Every 5 Minutes'); 26 | }); 27 | 28 | const selectedOptions = [ 29 | {name: 2, code: 2}, 30 | {name: 3, code: 3} 31 | ]; 32 | 33 | it("should handle option change", () => { 34 | const actual = minutes(initialState, setMinutesOptionValue(selectedOptions)); 35 | expect(actual.selectedOption).toEqual(selectedOptions); 36 | }); 37 | 38 | it("should handle 'hasError' value change", () => { 39 | const actual = minutes(initialState, setMinuteHasError(true)); 40 | expect(actual.hasError).toEqual(true); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/Schedule/months.test.ts: -------------------------------------------------------------------------------- 1 | import months from "../../../../reducers/CrontabGenerator/Schedule/months"; 2 | import { 3 | setMonthHasError, 4 | setMonthsOptionValue, setMonthsRadioButtonValue 5 | } from "../../../../actions/CrontabGenerator/Schedule/months"; 6 | import {PeriodContainerStateInterface} from "../../../../types/crontabGenerator"; 7 | 8 | describe('months reducer', () => { 9 | 10 | const initialState: PeriodContainerStateInterface = { 11 | selectedRadio: 'Every Month', 12 | selectedOption: [], 13 | hasError: false, 14 | } 15 | 16 | it("should handle initial state", () => { 17 | expect(months(undefined, { 18 | type: "undefined", 19 | payload: "" 20 | })).toEqual(initialState); 21 | }); 22 | 23 | it("should handle radio button change", () => { 24 | const actual = months(initialState, setMonthsRadioButtonValue('Every 4 Months')); 25 | expect(actual.selectedRadio).toEqual('Every 4 Months'); 26 | }); 27 | 28 | const selectedOptions = [ 29 | {name: 'April', code: 'April'}, 30 | {name: 'May', code: 'May'}, 31 | ]; 32 | 33 | it("should handle option change", () => { 34 | const actual = months(initialState, setMonthsOptionValue(selectedOptions)); 35 | expect(actual.selectedOption).toEqual(selectedOptions); 36 | }); 37 | 38 | it("should handle 'hasError' value change", () => { 39 | const actual = months(initialState, setMonthHasError(true)); 40 | expect(actual.hasError).toEqual(true); 41 | }); 42 | 43 | }); 44 | -------------------------------------------------------------------------------- /src/utils/calculators/chmodCalculator.ts: -------------------------------------------------------------------------------- 1 | import { CheckboxesType } from "../../types/chmodCalculator"; 2 | 3 | const matching: any = { 4 | 0: '---', 5 | 1: '--x', 6 | 2: '-w-', 7 | 3: '-wx', 8 | 4: 'r--', 9 | 5: 'r-x', 10 | 6: 'rw-', 11 | 7: 'rwx' 12 | } 13 | 14 | const digitToString = (digit: string): string => matching[digit]; 15 | 16 | const toDigit = (r: boolean, w: boolean, x: boolean): string => { 17 | const read = r ? 4 : 0; 18 | const write = w ? 2 : 0; 19 | const execute = x ? 1 : 0; 20 | 21 | return (read + write + execute).toString(); 22 | } 23 | 24 | export const checkboxesToChmodNumber = (checked: CheckboxesType): string => 25 | toDigit(checked.ownerRead, checked.ownerWrite, checked.ownerExecute) 26 | + toDigit(checked.groupRead, checked.groupWrite, checked.groupExecute) 27 | + toDigit(checked.othersRead, checked.othersWrite, checked.othersExecute) 28 | 29 | export const chmodNumberToString = (number: string): string => 30 | digitToString(number.charAt(0)) 31 | + digitToString(number.charAt(1)) 32 | + digitToString(number.charAt(2)) 33 | 34 | export const chmodStringToCheckboxes = (value: string): CheckboxesType => ({ 35 | 'ownerRead': value.charAt(0) === 'r', 36 | 'ownerWrite': value.charAt(1) === 'w', 37 | 'ownerExecute': value.charAt(2) === 'x', 38 | 'groupRead': value.charAt(3) === 'r', 39 | 'groupWrite': value.charAt(4) === 'w', 40 | 'groupExecute': value.charAt(5) === 'x', 41 | 'othersRead': value.charAt(6) === 'r', 42 | 'othersWrite': value.charAt(7) === 'w', 43 | 'othersExecute': value.charAt(8) === 'x', 44 | }) 45 | -------------------------------------------------------------------------------- /src/tests/reducers/CrontabGenerator/Schedule/weekday.test.ts: -------------------------------------------------------------------------------- 1 | import weekday from "../../../../reducers/CrontabGenerator/Schedule/weekday"; 2 | import { 3 | setWeekdayHasError, 4 | setWeekdayOptionValue, 5 | setWeekdayRadioButtonValue 6 | } from "../../../../actions/CrontabGenerator/Schedule/weekday"; 7 | import {PeriodContainerStateInterface} from "../../../../types/crontabGenerator"; 8 | 9 | describe('weekday reducer', () => { 10 | 11 | const initialState: PeriodContainerStateInterface = { 12 | selectedRadio: 'Every Weekday', 13 | selectedOption: [], 14 | hasError: false, 15 | } 16 | 17 | it("should handle initial state", () => { 18 | expect(weekday(undefined, { 19 | type: "undefined", 20 | payload: "" 21 | })).toEqual(initialState); 22 | }); 23 | 24 | it("should handle radio button change", () => { 25 | const actual = weekday(initialState, setWeekdayRadioButtonValue('Weekend Days')); 26 | expect(actual.selectedRadio).toEqual('Weekend Days'); 27 | }); 28 | 29 | const selectedOptions = [ 30 | {name: 'Sunday', code: 'Sunday'}, 31 | {name: 'Monday', code: 'Monday'}, 32 | ]; 33 | 34 | it("should handle option change", () => { 35 | const actual = weekday(initialState, setWeekdayOptionValue(selectedOptions)); 36 | expect(actual.selectedOption).toEqual(selectedOptions); 37 | }); 38 | 39 | it("should handle 'hasError' value change", () => { 40 | const actual = weekday(initialState, setWeekdayHasError(true)); 41 | expect(actual.hasError).toEqual(true); 42 | }); 43 | 44 | }); 45 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/Schedule/RadioButtonWithMultiSelect.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {RadioButton as PrimeRadioButton} from "primereact/radiobutton"; 3 | import {MultiSelect} from "primereact/multiselect"; 4 | import {RadioButtonWithMultiSelectPropsInterface} from "../../../../types/crontabGenerator"; 5 | 6 | const RadioButtonWithMultiSelect = 7 | ({ 8 | period, 9 | options, 10 | selectedOption, 11 | selectedRadio, 12 | onRadioButtonChange, 13 | onOptionChange 14 | }: 15 | RadioButtonWithMultiSelectPropsInterface) => { 16 | 17 | const arrayOfObjects = options.map(option => ({name: option, code: option})); 18 | const multiselect = 24 | 25 | return 26 | 0)} 28 | onChange={onRadioButtonChange} 29 | /> 30 | 31 | {multiselect} 32 | 33 | 34 | } 35 | 36 | export default RadioButtonWithMultiSelect; 37 | -------------------------------------------------------------------------------- /src/components/PasswordGenerator/PasswordGenerator.tsx: -------------------------------------------------------------------------------- 1 | import {Fieldset} from "primereact/fieldset"; 2 | import React from "react"; 3 | import PasswordLength from "./FormFields/PasswordLength"; 4 | import Checkboxes from "./FormFields/Checkboxes"; 5 | import {PasswordGeneratorPropsInterface} from "../../types/passwordGenerator"; 6 | import GenerateAndCopy from "./GenerateAndCopy"; 7 | 8 | const PasswordGenerator = ({ 9 | result, 10 | checked, 11 | passwordLength, 12 | copyToClipboardButtonState, 13 | onCopyButtonClick, 14 | onGenerateButtonClick, 15 | onCheckboxChange, 16 | onPasswordLengthChange 17 | }: PasswordGeneratorPropsInterface) => 18 | 19 | 20 | 21 | 22 | 27 | 28 | 30 | 31 | 32 | 33 | 34 | 35 | export default PasswordGenerator; 36 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/CommandToExecute.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {InputText} from "primereact/inputtext"; 3 | import {Accordion, AccordionTab} from 'primereact/accordion'; 4 | import {CommandToExecutePropsInterface} from "../../../types/crontabGenerator"; 5 | 6 | const CommandToExecute: React.FC = 7 | ({commandToExecuteValue, commandHasError, onCommandToExecuteChange}) => 8 | 9 | 10 | Command To Execute 11 | 16 | 17 | 18 | 19 | Execute PHP script: 20 | /usr/bin/php 21 | /var/www/app/cron.php 22 | Execute Artisan command: 23 | /usr/bin/php 24 | /var/www/laravel/artisan schedule:run 25 | Access URL: 26 | /usr/bin/wget 27 | --spider "http://site.com/cron.php" 28 | 29 | 30 | 31 | 32 | 33 | 34 | export default CommandToExecute; 35 | -------------------------------------------------------------------------------- /src/components/CrontabGenerator/FormFields/Schedule/Period.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {PeriodPropsInterface} from "../../../../types/crontabGenerator"; 3 | import RadioButtonList from "./RadioButtonList"; 4 | import RadioButtonWithMultiSelect from "./RadioButtonWithMultiSelect"; 5 | 6 | const Period: React.FC = 7 | ({ 8 | period, radioButtons, options, 9 | selectedRadio, selectedOption, hasError, 10 | onRadioButtonChange, onOptionChange 11 | }) => 12 | 13 | 14 | 16 | {period} 17 | 18 | 19 | {/* Standard options */} 20 | 23 | 24 | {/* Custom options */} 25 | 31 | 32 | 33 | 34 | 35 | export default Period; 36 | -------------------------------------------------------------------------------- /src/types/epochConverter.ts: -------------------------------------------------------------------------------- 1 | import {MouseEventHandler} from "react"; 2 | 3 | export type UnixTimeFormStateType = { 4 | unixTimeInputValue: string, 5 | epochToDateResult: string 6 | } 7 | 8 | export type DateTimeFormState = { 9 | dateTimeInputValue: any; 10 | timezoneInputValue: 'gmt' | 'local'; 11 | dateToEpochResult: string; 12 | } 13 | 14 | export interface DateTimeInputPropsInterface extends DateTimeFormState { 15 | onDateTimeChange: any; 16 | onTimezoneChange: any; 17 | onDateToEpochButtonClick: MouseEventHandler; 18 | } 19 | 20 | export interface DateTimeFormContainerPropsInterface extends DateTimeFormState { 21 | setDateTimeInput: Function, 22 | setTimezoneInput: Function, 23 | setDateToEpochResultValue: Function, 24 | } 25 | 26 | export type CurrentEpochTimeContainerState = { 27 | currentUnixTime: string; 28 | copyToClipboardButtonState: 'default' | 'pressed'; 29 | } 30 | 31 | export interface CurrentEpochTimeContainerPropsInterface extends CurrentEpochTimeContainerState { 32 | setCurrentUnixTimeValue: Function, 33 | setCopyToClipboardState: Function, 34 | } 35 | 36 | export interface UnixTimeFormContainerPropsInterface extends UnixTimeFormStateType { 37 | setUnixTimeInput: Function, 38 | setEpochToDateResultValue: Function 39 | } 40 | 41 | export interface UnixTimeInputPropsInterface extends UnixTimeFormStateType { 42 | onUnixTimeChange: any, 43 | onEpochToDateButtonClick: MouseEventHandler 44 | } 45 | 46 | export interface CurrentEpochTimeInterface { 47 | copyToClipboard: MouseEventHandler; 48 | copyToClipboardButtonState: 'default' | 'pressed'; 49 | currentUnixTime: string; 50 | } 51 | -------------------------------------------------------------------------------- /src/tests/components/EpochConverter/EpochConverter.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import {Provider} from "react-redux"; 4 | import EpochConverter from "../../../components/EpochConverter/EpochConverter"; 5 | import store from "../../../store/configureStore"; 6 | 7 | it('should simulate "Copy" button click', () => { 8 | render() 9 | const button = screen.getByText('Copy'); 10 | expect(button).toBeInTheDocument(); 11 | 12 | Object.defineProperty(navigator, "clipboard", { 13 | value: { 14 | writeText: () => { 15 | }, 16 | }, 17 | }); 18 | 19 | fireEvent.click(button); 20 | expect(screen.queryByText('Copy')).toBeNull() 21 | expect(screen.getByText('Copied')).toBeInTheDocument(); 22 | }); 23 | 24 | it('should simulate "Timestamp to Human date" button click', () => { 25 | render() 26 | const button = screen.getByText('Timestamp to Human date'); 27 | expect(button).toBeInTheDocument(); 28 | expect(screen.queryByTestId('epochToDateResult')).not.toBeInTheDocument(); 29 | fireEvent.click(button); 30 | expect(screen.getByTestId('epochToDateResult')).toBeInTheDocument(); 31 | }); 32 | 33 | it('should simulate "Human date to Timestamp" button click', () => { 34 | render() 35 | const button = screen.getByText('Human date to Timestamp'); 36 | expect(button).toBeInTheDocument(); 37 | expect(screen.queryByTestId('dateToEpochResult')).not.toBeInTheDocument(); 38 | fireEvent.click(button); 39 | expect(screen.getByTestId('dateToEpochResult')).toBeInTheDocument(); 40 | }); 41 | -------------------------------------------------------------------------------- /src/tests/selectors/crontabGenerator.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getCronExpression, 3 | getFormState, 4 | getResult 5 | } from "../../selectors/crontabGenerator"; 6 | 7 | const state = { 8 | crontabGenerator: { 9 | minutes: { 10 | selectedRadio: "Odd Minutes", 11 | selectedOption: [], 12 | hasError: false, 13 | }, 14 | hours: { 15 | selectedRadio: "Even Hours", 16 | selectedOption: [], 17 | hasError: false, 18 | }, 19 | days: { 20 | selectedRadio: "Every 10 Days", 21 | selectedOption: [], 22 | hasError: false, 23 | }, 24 | months: { 25 | selectedRadio: "Odd Months", 26 | selectedOption: [], 27 | hasError: false, 28 | }, 29 | weekday: { 30 | selectedRadio: "Every Weekday", 31 | selectedOption: [], 32 | hasError: false, 33 | }, 34 | commandToExecute: { 35 | commandHasError: false, 36 | commandToExecuteValue: "df -P ." 37 | }, 38 | howToHandleOutput: { 39 | emailValue: "", 40 | howToHandleOutputValue: "file", 41 | pathToFileValue: "/root/crontab-log.txt" 42 | }, 43 | generateCrontabLine: { 44 | result: "" 45 | } 46 | } 47 | } 48 | 49 | describe('crontab generator selectors', () => { 50 | it('it should return result', () => { 51 | expect(getResult(state)).toStrictEqual(''); 52 | }); 53 | 54 | it('it should return crontab expression', () => { 55 | expect(getCronExpression(getFormState(state))).toStrictEqual( 56 | '1-59/2 */2 */10 1-11/2 * df -P . > /root/crontab-log.txt' 57 | ); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/CommandToExecute.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {compose} from "redux"; 3 | import {connect} from "react-redux"; 4 | import CommandToExecuteComponent 5 | from "../../components/CrontabGenerator/FormFields/CommandToExecute"; 6 | import { 7 | setCommandToExecuteValue, 8 | setCommandHasError 9 | } from "../../actions/CrontabGenerator/commandToExecute"; 10 | import { 11 | CommandToExecuteContainerPropsInterface, 12 | CommandToExecuteContainerState 13 | } from "../../types/crontabGenerator"; 14 | 15 | const CommandToExecute = ({ 16 | commandToExecuteValue, 17 | commandHasError, 18 | setCommandToExecute, 19 | setCommandHasErrorValue 20 | }: CommandToExecuteContainerPropsInterface) => { 21 | 22 | const onCommandToExecuteChange = ({target: {value}}: { target: { value: string } }) => { 23 | setCommandToExecute(value); 24 | setCommandHasErrorValue(false); 25 | } 26 | 27 | return 32 | } 33 | 34 | const mapStateToProps = (state: { crontabGenerator: { commandToExecute: CommandToExecuteContainerState } }) => ({ 35 | commandToExecuteValue: state.crontabGenerator.commandToExecute.commandToExecuteValue, 36 | commandHasError: state.crontabGenerator.commandToExecute.commandHasError, 37 | }); 38 | 39 | export default compose( 40 | connect(mapStateToProps, { 41 | setCommandToExecute: setCommandToExecuteValue, 42 | setCommandHasErrorValue: setCommandHasError, 43 | }))(CommandToExecute); 44 | -------------------------------------------------------------------------------- /src/utils/transformers/map.json: -------------------------------------------------------------------------------- 1 | { 2 | "Every Minute": "*", 3 | "Even Minutes": "*/2", 4 | "Odd Minutes": "1-59/2", 5 | "Every 5 Minutes": "*/5", 6 | "Every 15 Minutes": "*/15", 7 | "Every 30 Minutes": "*/30", 8 | "Every Hour": "*", 9 | "Even Hours": "*/2", 10 | "Odd Hours": "1-23/2", 11 | "Every 3 Hours": "*/3", 12 | "Every 6 Hours": "*/6", 13 | "Every 12 Hours": "*/12", 14 | "Midnight": "0", 15 | "1am": "1", 16 | "2am": "2", 17 | "3am": "3", 18 | "4am": "4", 19 | "5am": "5", 20 | "6am": "6", 21 | "7am": "7", 22 | "8am": "8", 23 | "9am": "9", 24 | "10am": "10", 25 | "11am": "11", 26 | "Noon": "12", 27 | "1pm": "13", 28 | "2pm": "14", 29 | "3pm": "15", 30 | "4pm": "16", 31 | "5pm": "17", 32 | "6pm": "18", 33 | "7pm": "19", 34 | "8pm": "20", 35 | "9pm": "21", 36 | "10pm": "22", 37 | "11pm": "23", 38 | "Every Day": "*", 39 | "Even Days": "*/2", 40 | "Odd Days": "1-31/2", 41 | "Every 5 Days": "*/5", 42 | "Every 10 Days": "*/10", 43 | "Every Half Month": "*/15", 44 | "Every Month": "*", 45 | "Even Months": "*/2", 46 | "Odd Months": "1-11/2", 47 | "Every 4 Months": "*/4", 48 | "Every Half Year": "*/6", 49 | "January": "1", 50 | "February": "2", 51 | "March": "3", 52 | "April": "4", 53 | "May": "5", 54 | "June": "6", 55 | "July": "7", 56 | "August": "8", 57 | "September": "9", 58 | "October": "10", 59 | "November": "11", 60 | "December": "12", 61 | "Every Weekday": "*", 62 | "Monday-Friday": "1-5", 63 | "Weekend Days": "0,6", 64 | "Sunday": "0", 65 | "Monday": "1", 66 | "Tuesday": "2", 67 | "Wednesday": "3", 68 | "Thursday": "4", 69 | "Friday": "5", 70 | "Saturday": "6" 71 | } 72 | -------------------------------------------------------------------------------- /src/components/EpochConverter/Forms/DateTimeForm.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "primereact/button"; 2 | import React from "react"; 3 | import {Calendar} from 'primereact/calendar'; 4 | import {Dropdown} from "primereact/dropdown"; 5 | import {DateTimeInputPropsInterface} from "../../../types/epochConverter"; 6 | 7 | const timeOptions = [ 8 | {label: 'Local Time', value: 'local'}, 9 | {label: 'GMT', value: 'gmt'}, 10 | ]; 11 | 12 | const DateTimeForm: React.FC = 13 | ({ 14 | dateTimeInputValue, 15 | timezoneInputValue, 16 | onDateTimeChange, 17 | onTimezoneChange, 18 | onDateToEpochButtonClick, 19 | dateToEpochResult 20 | }) => 21 | <> 22 | 23 | 24 | 27 | 28 | 29 | 31 | 32 | 33 | 36 | 37 | 38 | 39 | {dateToEpochResult && 40 | 41 | Unix Timestamp: {dateToEpochResult} 42 | 43 | } 44 | > 45 | 46 | export default DateTimeForm; 47 | -------------------------------------------------------------------------------- /src/tests/components/CrontabGenerator/FormFields/Schedule/Period.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import Period 4 | from "../../../../../components/CrontabGenerator/FormFields/Schedule/Period"; 5 | import {PeriodPropsInterface} from "../../../../../types/crontabGenerator"; 6 | 7 | const period: PeriodPropsInterface = { 8 | period: 'Days', 9 | options: Array.from(Array(31).keys(), n => n + 1), 10 | radioButtons: [ 11 | 'Every Day', 12 | 'Even Days', 13 | 'Odd Days', 14 | 'Every 5 Days', 15 | 'Every 10 Days', 16 | 'Every Half Month', 17 | ], 18 | selectedRadio: 'Every 5 Days', 19 | selectedOption: [{name: '3', code: '3'}], 20 | hasError: false, 21 | onRadioButtonChange: jest.fn(), 22 | onOptionChange: jest.fn() 23 | }; 24 | 25 | const setup = () => { 26 | // eslint-disable-next-line react/jsx-props-no-spreading 27 | render() 28 | } 29 | 30 | describe(' component', () => { 31 | 32 | it('should render all radio buttons', () => { 33 | setup() 34 | period.radioButtons.forEach((item) => { 35 | expect(screen.getByLabelText(item)).toBeInTheDocument() 36 | if (item === period.selectedRadio) { 37 | expect(screen.getByLabelText(item)).toBeChecked(); 38 | } else { 39 | expect(screen.getByLabelText(item)).toBeVisible(); 40 | expect(screen.getByLabelText(item)).not.toBeChecked(); 41 | } 42 | }); 43 | }); 44 | 45 | it('should simulate radio button change', () => { 46 | setup() 47 | const radioButton = screen.getByLabelText('Even Days') 48 | fireEvent.click(radioButton); 49 | expect(period.onRadioButtonChange).toBeCalledTimes(1) 50 | }); 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | Web Developer Tools 28 | 29 | 30 | You need to enable JavaScript to run this app. 31 | 32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /src/tests/containers/ChmodCalculator/ChmodCalculator.test.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {fireEvent, render, screen} from "@testing-library/react"; 3 | import {Provider} from "react-redux"; 4 | import ChmodCalculator from "../../../containers/ChmodCalculator/ChmodCalculator"; 5 | import store from "../../../store/configureStore"; 6 | 7 | test('is able to check checkboxes and get the result', () => { 8 | render(); 9 | const checkboxes = screen.getAllByLabelText('Read'); 10 | checkboxes.forEach((checkbox) => { 11 | expect(checkbox).not.toBeChecked(); 12 | fireEvent.click(checkbox); 13 | }); 14 | 15 | checkboxes.forEach((checkbox) => { 16 | expect(checkbox).toBeChecked(); 17 | }); 18 | 19 | expect(screen.getByDisplayValue('444')).toBeInTheDocument(); 20 | expect(screen.getByDisplayValue('r--r--r--')).toBeInTheDocument(); 21 | }); 22 | 23 | const setup = (placeholderText: string) => { 24 | const utils = render(); 25 | const input = screen.getByPlaceholderText(placeholderText) 26 | return {input, ...utils,} 27 | } 28 | 29 | test('is able to calculate result based on number', () => { 30 | const { input } = setup('666') 31 | fireEvent.change(input, { target: { value: '147' } }) 32 | expect(screen.getByDisplayValue('--xr--rwx')).toBeInTheDocument(); 33 | 34 | fireEvent.change(input, { target: { value: '523' } }) 35 | expect(screen.getByDisplayValue('r-x-w--wx')).toBeInTheDocument(); 36 | }); 37 | 38 | test('is able to calculate result based on symbolic value', () => { 39 | const { input } = setup('rw-rw-rw-') 40 | fireEvent.change(input, { target: { value: 'rwx-w-rwx' } }) 41 | expect(screen.getByDisplayValue('727')).toBeInTheDocument(); 42 | 43 | fireEvent.change(input, { target: { value: 'r-xrw-rwx' } }) 44 | expect(screen.getByDisplayValue('567')).toBeInTheDocument(); 45 | }); 46 | -------------------------------------------------------------------------------- /src/tests/containers/PasswordGenerator/PasswordGenerator.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import {Provider} from "react-redux"; 4 | import store from "../../../store/configureStore"; 5 | import PasswordGenerator 6 | from "../../../containers/PasswordGenerator/PasswordGenerator"; 7 | 8 | const setup = () => { 9 | render(); 10 | } 11 | 12 | describe(' container', () => { 13 | 14 | it('shows the slider title', () => { 15 | setup(); 16 | expect(screen.getByText(/Password Length:/i)).toBeVisible(); 17 | }); 18 | 19 | it('shows the password length', () => { 20 | setup(); 21 | expect(screen.getByText(/16/i)).toBeVisible(); 22 | }); 23 | 24 | it('should handle button click actions', () => { 25 | setup(); 26 | Object.defineProperty(navigator, "clipboard", { 27 | value: { 28 | writeText: () => { 29 | }, 30 | }, 31 | }); 32 | const button = screen.getByText('Copy'); 33 | expect(button).toBeVisible(); 34 | fireEvent.click(button); 35 | expect(screen.getByText('Copied')).toBeVisible(); 36 | const generateButton = screen.getByText('Generate'); 37 | fireEvent.click(generateButton); 38 | expect(screen.getByText('Copy')).toBeVisible(); 39 | }); 40 | 41 | it('shows the unchecked checkbox', () => { 42 | setup(); 43 | const checkbox = screen.getByLabelText('Exclude Similar Characters'); 44 | expect(checkbox).toBeVisible(); 45 | expect(checkbox).not.toBeChecked(); 46 | }); 47 | 48 | it('should handle checkbox click action', () => { 49 | setup(); 50 | const checkbox = screen.getByLabelText('Exclude Similar Characters'); 51 | fireEvent.click(checkbox); 52 | expect(screen.getByLabelText('Exclude Similar Characters')).toBeChecked(); 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /src/tests/reducers/ChmodCalculator/index.test.ts: -------------------------------------------------------------------------------- 1 | import chmodCalculator from "../../../reducers/ChmodCalculator"; 2 | import { 3 | setChecked, 4 | setNumberFieldValue, 5 | setTextFieldValue 6 | } from "../../../actions/ChmodCalculator/chmodCalculator"; 7 | import {ChmodCalculatorContainerStateType} from "../../../types/chmodCalculator"; 8 | 9 | describe('chmod calculator reducer', () => { 10 | 11 | const initialState: ChmodCalculatorContainerStateType = { 12 | checked: { 13 | ownerRead: false, 14 | ownerWrite: false, 15 | ownerExecute: false, 16 | groupRead: false, 17 | groupWrite: false, 18 | groupExecute: false, 19 | othersRead: false, 20 | othersWrite: false, 21 | othersExecute: false, 22 | }, 23 | numberFieldValue: '000', 24 | textFieldValue: '---------' 25 | } 26 | 27 | it("should handle initial state", () => { 28 | expect(chmodCalculator(undefined, { 29 | type: "undefined", 30 | payload: "" 31 | })).toEqual(initialState); 32 | }); 33 | 34 | it("should handle number change", () => { 35 | const actual = chmodCalculator(initialState, setNumberFieldValue('645')); 36 | expect(actual.numberFieldValue).toEqual('645'); 37 | }); 38 | 39 | it("should handle symbolic value change", () => { 40 | const actual = chmodCalculator(initialState, setTextFieldValue('rwxr-xr-x')); 41 | expect(actual.textFieldValue).toEqual('rwxr-xr-x'); 42 | }); 43 | 44 | const newCheckboxState = { 45 | ownerRead: false, 46 | ownerWrite: true, 47 | ownerExecute: false, 48 | groupRead: false, 49 | groupWrite: true, 50 | groupExecute: false, 51 | othersRead: false, 52 | othersWrite: false, 53 | othersExecute: true, 54 | } 55 | 56 | it("should handle checkbox change", () => { 57 | const actual = chmodCalculator(initialState, setChecked(newCheckboxState)); 58 | expect(actual.checked).toEqual(newCheckboxState); 59 | }); 60 | 61 | }); 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web-developer-tools", 3 | "version": "0.1.0", 4 | "private": true, 5 | "homepage": "https://coderberg.github.io/web-developer-tools", 6 | "dependencies": { 7 | "@reduxjs/toolkit": "^1.7.1", 8 | "@testing-library/jest-dom": "^5.16.4", 9 | "@testing-library/react": "^13.3.0", 10 | "@testing-library/user-event": "^14.2.0", 11 | "@types/jest": "^24.9.1", 12 | "@types/node": "^17.0.36", 13 | "@types/react": "^18.0.9", 14 | "@types/react-dom": "^18.0.5", 15 | "@types/react-redux": "^7.1.20", 16 | "@types/react-transition-group": "^4.4.4", 17 | "generate-password-browser": "^1.1.0", 18 | "primeflex": "2.0.0", 19 | "primeicons": "^5.0.0", 20 | "primereact": "^8.1.1", 21 | "react": "^18.1.0", 22 | "react-dom": "^18.1.0", 23 | "react-redux": "^7.2.6", 24 | "react-router-dom": "^6.2.1", 25 | "react-scripts": "5.0.1", 26 | "react-transition-group": "^4.4.2", 27 | "redux": "^4.1.2", 28 | "typescript": "^4.1.6" 29 | }, 30 | "scripts": { 31 | "start": "react-scripts start", 32 | "build": "react-scripts build", 33 | "test": "react-scripts test", 34 | "eject": "react-scripts eject", 35 | "lint": "eslint .", 36 | "predeploy": "npm run build", 37 | "deploy": "gh-pages -d build" 38 | }, 39 | "eslintConfig": { 40 | "extends": [ 41 | "react-app", 42 | "react-app/jest" 43 | ] 44 | }, 45 | "browserslist": { 46 | "production": [ 47 | ">0.2%", 48 | "not dead", 49 | "not op_mini all" 50 | ], 51 | "development": [ 52 | "last 1 chrome version", 53 | "last 1 firefox version", 54 | "last 1 safari version" 55 | ] 56 | }, 57 | "devDependencies": { 58 | "@testing-library/dom": "^8.11.1", 59 | "@types/react-router-dom": "^5.3.2", 60 | "eslint-config-airbnb": "^18.2.1", 61 | "eslint-config-prettier": "^8.3.0", 62 | "eslint-plugin-jsx-a11y": "^6.5.1", 63 | "eslint-plugin-prettier": "^4.0.0", 64 | "eslint-plugin-testing-library": "^5.0.1", 65 | "gh-pages": "^3.2.3", 66 | "husky": "^7.0.4", 67 | "lint-staged": "^12.1.3", 68 | "prettier": "^2.5.1" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/tests/reducers/PasswordGenerator/index.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CheckboxesType, 3 | PasswordGeneratorStateInterface 4 | } from "../../../types/passwordGenerator"; 5 | import passwordGenerator from "../../../reducers/PasswordGenerator"; 6 | import { 7 | setCheckedParam, setCopyToClipboardState, 8 | setPassword, 9 | setPasswordLength 10 | } from "../../../actions/PasswordGenerator/passwordGenerator"; 11 | 12 | describe('password generator reducer', () => { 13 | 14 | const initialState: PasswordGeneratorStateInterface = { 15 | checked: { 16 | symbols: true, 17 | numbers: true, 18 | uppercase: true, 19 | lowercase: true, 20 | excludeSimilar: false, 21 | }, 22 | passwordLength: '16', 23 | copyToClipboardButtonState: 'default', 24 | result: '', 25 | } 26 | 27 | it("should handle initial state", () => { 28 | expect(passwordGenerator(undefined, { 29 | type: "undefined", 30 | payload: "" 31 | })).toEqual(initialState); 32 | }); 33 | 34 | it("should handle Password change", () => { 35 | const actual = passwordGenerator(initialState, setPassword('VZfezXEnWRmK')); 36 | expect(actual.result).toEqual('VZfezXEnWRmK'); 37 | }); 38 | 39 | it("should handle Password Length change", () => { 40 | const actual = passwordGenerator(initialState, setPasswordLength('31')); 41 | expect(actual.passwordLength).toEqual('31'); 42 | }); 43 | 44 | const newCheckboxState: CheckboxesType = { 45 | symbols: false, 46 | numbers: true, 47 | uppercase: false, 48 | lowercase: false, 49 | excludeSimilar: true, 50 | } 51 | 52 | it("should handle checkbox change", () => { 53 | const actual = passwordGenerator(initialState, setCheckedParam(newCheckboxState)); 54 | expect(actual.checked).toEqual(newCheckboxState); 55 | }); 56 | 57 | it("should handle copy button click", () => { 58 | const actual = passwordGenerator(initialState, setCopyToClipboardState('pressed')); 59 | expect(actual.copyToClipboardButtonState).toEqual('pressed'); 60 | }); 61 | 62 | }); 63 | -------------------------------------------------------------------------------- /src/containers/EpochConverter/UnixTimeForm.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import { 5 | epochToHumanReadableDate, 6 | getCurrentUnixTime 7 | } from "../../utils/calculators/epochConverter"; 8 | import UnixTimeFormComponent 9 | from "../../components/EpochConverter/Forms/UnixTimeForm" 10 | import { 11 | setUnixTimeInputValue, 12 | setEpochToDateResult 13 | } from "../../actions/EpochConverter/unixTimeForm"; 14 | import { 15 | UnixTimeFormContainerPropsInterface, 16 | UnixTimeFormStateType 17 | } from "../../types/epochConverter"; 18 | 19 | const UnixTimeForm = ({ 20 | unixTimeInputValue, 21 | epochToDateResult, 22 | setUnixTimeInput, 23 | setEpochToDateResultValue 24 | }: 25 | UnixTimeFormContainerPropsInterface) => { 26 | 27 | useEffect(() => { 28 | if (unixTimeInputValue === '*') { 29 | setUnixTimeInput(getCurrentUnixTime()); 30 | } 31 | }); 32 | 33 | const onUnixTimeChange = ({target: {value}}: { target: { value: string } }) => { 34 | setUnixTimeInput(value); 35 | } 36 | 37 | const onEpochToDateButtonClick = () => { 38 | setEpochToDateResultValue(epochToHumanReadableDate(unixTimeInputValue)); 39 | } 40 | 41 | return ; 46 | 47 | } 48 | 49 | const mapStateToProps = (state: { epochConverter: { unixTimeForm: UnixTimeFormStateType } }) => ({ 50 | unixTimeInputValue: state.epochConverter.unixTimeForm.unixTimeInputValue, 51 | epochToDateResult: state.epochConverter.unixTimeForm.epochToDateResult 52 | }); 53 | 54 | export default compose( 55 | connect(mapStateToProps, { 56 | setUnixTimeInput: setUnixTimeInputValue, 57 | setEpochToDateResultValue: setEpochToDateResult 58 | }))(UnixTimeForm); 59 | -------------------------------------------------------------------------------- /src/containers/EpochConverter/CurrentEpochTime.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import CurrentEpoch 5 | from "../../components/EpochConverter/CurrentEpochTime"; 6 | import {getCurrentUnixTime} from "../../utils/calculators/epochConverter"; 7 | import { 8 | setCurrentUnixTime, 9 | setCopyToClipboardButtonState 10 | } from "../../actions/EpochConverter/currentEpochTime" 11 | import { 12 | CurrentEpochTimeContainerPropsInterface, 13 | CurrentEpochTimeContainerState 14 | } from "../../types/epochConverter"; 15 | 16 | const CurrentEpochTime = ({ 17 | currentUnixTime, 18 | copyToClipboardButtonState, 19 | setCurrentUnixTimeValue, 20 | setCopyToClipboardState 21 | }: CurrentEpochTimeContainerPropsInterface) => { 22 | 23 | useEffect(() => { 24 | if (currentUnixTime === '') { 25 | setCurrentUnixTimeValue(getCurrentUnixTime()); 26 | } 27 | 28 | const secTimer = setInterval(() => { 29 | setCurrentUnixTimeValue(getCurrentUnixTime()); 30 | if (Number(getCurrentUnixTime()) % 5 === 0) { 31 | setCopyToClipboardState('default'); 32 | } 33 | }, 1000) 34 | 35 | return () => clearInterval(secTimer); 36 | }, [currentUnixTime, setCopyToClipboardState, setCurrentUnixTimeValue]); 37 | 38 | const copyToClipboard = () => { 39 | navigator.clipboard.writeText(currentUnixTime); 40 | setCopyToClipboardState('pressed') 41 | } 42 | 43 | return 47 | } 48 | 49 | const mapStateToProps = (state: { epochConverter: { currentEpochTime: CurrentEpochTimeContainerState } }) => ({ 50 | currentUnixTime: state.epochConverter.currentEpochTime.currentUnixTime, 51 | copyToClipboardButtonState: state.epochConverter.currentEpochTime.copyToClipboardButtonState, 52 | }); 53 | 54 | export default compose( 55 | connect(mapStateToProps, { 56 | setCurrentUnixTimeValue: setCurrentUnixTime, 57 | setCopyToClipboardState: setCopyToClipboardButtonState, 58 | }))(CurrentEpochTime); 59 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/HowToHandleOutput.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import HowToHandleOutputComponent 5 | from "../../components/CrontabGenerator/FormFields/HowToHandleOutput"; 6 | import { 7 | setEmailValue, 8 | setHowToHandleOutputValue, setPathToFileValue 9 | } from "../../actions/CrontabGenerator/howToHandleOutput"; 10 | import { 11 | HowToHandleOutputContainerProps, 12 | HowToHandleOutputContainerState 13 | } from "../../types/crontabGenerator"; 14 | 15 | const HowToHandleOutput = ({ 16 | howToHandleOutputValue, 17 | pathToFileValue, 18 | emailValue, 19 | setHowToHandleOutput, 20 | setPathToFile, 21 | setEmail 22 | }: HowToHandleOutputContainerProps) => { 23 | 24 | const onHowToHandleOutputChange = ({target: {value}}: { target: { value: string } }) => { 25 | setHowToHandleOutput(value); 26 | } 27 | 28 | const onPathToFileChange = ({target: {value}}: { target: { value: string } }) => { 29 | setPathToFile(value); 30 | setHowToHandleOutput('file'); 31 | } 32 | 33 | const onEmailChange = ({target: {value}}: { target: { value: string } }) => { 34 | setEmail(value); 35 | setHowToHandleOutput('email'); 36 | } 37 | 38 | return 46 | } 47 | 48 | const mapStateToProps = (state: { crontabGenerator: { howToHandleOutput: HowToHandleOutputContainerState } }) => ({ 49 | howToHandleOutputValue: state.crontabGenerator.howToHandleOutput.howToHandleOutputValue, 50 | pathToFileValue: state.crontabGenerator.howToHandleOutput.pathToFileValue, 51 | emailValue: state.crontabGenerator.howToHandleOutput.emailValue, 52 | }); 53 | 54 | export default compose( 55 | connect(mapStateToProps, { 56 | setHowToHandleOutput: setHowToHandleOutputValue, 57 | setPathToFile: setPathToFileValue, 58 | setEmail: setEmailValue, 59 | }))(HowToHandleOutput); 60 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/Schedule/Days.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import Period 5 | from "../../../components/CrontabGenerator/FormFields/Schedule/Period"; 6 | import { 7 | PeriodContainerPropsInterface, 8 | PeriodContainerStateInterface, 9 | PeriodPropsInterface 10 | } from "../../../types/crontabGenerator"; 11 | import { 12 | setDaysRadioButtonValue, 13 | setDaysOptionValue, 14 | setDayHasError 15 | } from "../../../actions/CrontabGenerator/Schedule/days"; 16 | 17 | const Days = ({ 18 | selectedRadio, 19 | selectedOption, 20 | hasError, 21 | setRadioButtonValue, 22 | setOptionValue, 23 | setHasError 24 | } 25 | : PeriodContainerPropsInterface) => { 26 | 27 | const onRadioButtonChange = ({target: {value}}: { target: { value: string } }) => { 28 | setRadioButtonValue(value); 29 | if (value !== 'Days') { 30 | setOptionValue(''); 31 | } 32 | setHasError(false); 33 | } 34 | 35 | const onOptionChange = ({target: {value}}: { target: { value: string } }) => { 36 | setOptionValue(value); 37 | setRadioButtonValue('Days'); 38 | setHasError(false); 39 | } 40 | 41 | const period: PeriodPropsInterface = { 42 | period: 'Days', 43 | options: Array.from(Array(31).keys(), n => n + 1), 44 | radioButtons: [ 45 | 'Every Day', 46 | 'Even Days', 47 | 'Odd Days', 48 | 'Every 5 Days', 49 | 'Every 10 Days', 50 | 'Every Half Month', 51 | ], 52 | selectedRadio, selectedOption, hasError, 53 | onRadioButtonChange, onOptionChange 54 | }; 55 | 56 | // eslint-disable-next-line react/jsx-props-no-spreading 57 | return 58 | } 59 | 60 | const mapStateToProps = (state: { crontabGenerator: { days: PeriodContainerStateInterface } }) => ({ 61 | selectedRadio: state.crontabGenerator.days.selectedRadio, 62 | selectedOption: state.crontabGenerator.days.selectedOption, 63 | hasError: state.crontabGenerator.days.hasError, 64 | }); 65 | 66 | export default compose( 67 | connect(mapStateToProps, { 68 | setRadioButtonValue: setDaysRadioButtonValue, 69 | setOptionValue: setDaysOptionValue, 70 | setHasError: setDayHasError, 71 | }))(Days); 72 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/Schedule/Weekday.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import Period 5 | from "../../../components/CrontabGenerator/FormFields/Schedule/Period"; 6 | import { 7 | PeriodContainerPropsInterface, 8 | PeriodContainerStateInterface, 9 | PeriodPropsInterface 10 | } from "../../../types/crontabGenerator"; 11 | import { 12 | setWeekdayRadioButtonValue, 13 | setWeekdayOptionValue, 14 | setWeekdayHasError 15 | } from "../../../actions/CrontabGenerator/Schedule/weekday"; 16 | 17 | const Weekday = ({ 18 | selectedRadio, 19 | selectedOption, 20 | hasError, 21 | setRadioButtonValue, 22 | setOptionValue, 23 | setHasError 24 | } 25 | : PeriodContainerPropsInterface) => { 26 | 27 | const onRadioButtonChange = ({target: {value}}: { target: { value: string } }) => { 28 | setRadioButtonValue(value); 29 | if (value !== 'Weekday') { 30 | setOptionValue(''); 31 | } 32 | setHasError(false); 33 | } 34 | 35 | const onOptionChange = ({target: {value}}: { target: { value: string } }) => { 36 | setOptionValue(value); 37 | setRadioButtonValue('Weekday'); 38 | setHasError(false); 39 | } 40 | 41 | const period: PeriodPropsInterface = { 42 | period: 'Weekday', 43 | options: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], 44 | radioButtons: [ 45 | 'Every Weekday', 46 | 'Monday-Friday', 47 | 'Weekend Days', 48 | ], 49 | selectedRadio, selectedOption, hasError, 50 | onRadioButtonChange, onOptionChange 51 | }; 52 | 53 | // eslint-disable-next-line react/jsx-props-no-spreading 54 | return 55 | } 56 | 57 | const mapStateToProps = (state: { crontabGenerator: { weekday: PeriodContainerStateInterface } }) => ({ 58 | selectedRadio: state.crontabGenerator.weekday.selectedRadio, 59 | selectedOption: state.crontabGenerator.weekday.selectedOption, 60 | hasError: state.crontabGenerator.weekday.hasError, 61 | }); 62 | 63 | export default compose( 64 | connect(mapStateToProps, { 65 | setRadioButtonValue: setWeekdayRadioButtonValue, 66 | setOptionValue: setWeekdayOptionValue, 67 | setHasError: setWeekdayHasError, 68 | }))(Weekday); 69 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/Schedule/Minutes.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import Period 5 | from "../../../components/CrontabGenerator/FormFields/Schedule/Period"; 6 | import { 7 | PeriodContainerPropsInterface, 8 | PeriodContainerStateInterface, 9 | PeriodPropsInterface 10 | } from "../../../types/crontabGenerator"; 11 | import { 12 | setMinutesRadioButtonValue, 13 | setMinutesOptionValue, 14 | setMinuteHasError 15 | } from "../../../actions/CrontabGenerator/Schedule/minutes"; 16 | 17 | const Minutes = ({ 18 | selectedRadio, 19 | selectedOption, 20 | hasError, 21 | setRadioButtonValue, 22 | setOptionValue, 23 | setHasError 24 | } 25 | : PeriodContainerPropsInterface) => { 26 | 27 | const onRadioButtonChange = ({target: {value}}: { target: { value: string } }) => { 28 | setRadioButtonValue(value); 29 | if (value !== 'Minutes') { 30 | setOptionValue(''); 31 | } 32 | setHasError(false); 33 | } 34 | 35 | const onOptionChange = ({target: {value}}: { target: { value: string } }) => { 36 | setOptionValue(value); 37 | setRadioButtonValue('Minutes'); 38 | setHasError(false); 39 | } 40 | 41 | const period: PeriodPropsInterface = { 42 | period: 'Minutes', 43 | options: Array.from(Array(60).keys()), 44 | radioButtons: [ 45 | 'Every Minute', 46 | 'Even Minutes', 47 | 'Odd Minutes', 48 | 'Every 5 Minutes', 49 | 'Every 15 Minutes', 50 | 'Every 30 Minutes', 51 | ], 52 | selectedRadio, selectedOption, hasError, 53 | onRadioButtonChange, onOptionChange 54 | }; 55 | 56 | // eslint-disable-next-line react/jsx-props-no-spreading 57 | return 58 | } 59 | 60 | const mapStateToProps = (state: { crontabGenerator: { minutes: PeriodContainerStateInterface } }) => ({ 61 | selectedRadio: state.crontabGenerator.minutes.selectedRadio, 62 | selectedOption: state.crontabGenerator.minutes.selectedOption, 63 | hasError: state.crontabGenerator.minutes.hasError, 64 | }); 65 | 66 | export default compose( 67 | connect(mapStateToProps, { 68 | setRadioButtonValue: setMinutesRadioButtonValue, 69 | setOptionValue: setMinutesOptionValue, 70 | setHasError: setMinuteHasError, 71 | }))(Minutes); 72 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '45 2 * * 6' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v2 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v1 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v1 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v1 71 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/Schedule/Months.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import Period 5 | from "../../../components/CrontabGenerator/FormFields/Schedule/Period"; 6 | import { 7 | PeriodContainerPropsInterface, 8 | PeriodContainerStateInterface, 9 | PeriodPropsInterface 10 | } from "../../../types/crontabGenerator"; 11 | import { 12 | setMonthHasError, 13 | setMonthsOptionValue, 14 | setMonthsRadioButtonValue 15 | } from "../../../actions/CrontabGenerator/Schedule/months"; 16 | 17 | const Months = ({ 18 | selectedRadio, 19 | selectedOption, 20 | hasError, 21 | setRadioButtonValue, 22 | setOptionValue, 23 | setHasError 24 | } 25 | : PeriodContainerPropsInterface) => { 26 | 27 | const onRadioButtonChange = ({target: {value}}: { target: { value: string } }) => { 28 | setRadioButtonValue(value); 29 | if (value !== 'Months') { 30 | setOptionValue(''); 31 | } 32 | setHasError(false); 33 | } 34 | 35 | const onOptionChange = ({target: {value}}: { target: { value: string } }) => { 36 | setOptionValue(value); 37 | setRadioButtonValue('Months'); 38 | setHasError(false); 39 | } 40 | 41 | const period: PeriodPropsInterface = { 42 | period: 'Months', 43 | options: ["January", "February", "March", "April", "May", "June", 44 | "July", "August", "September", "October", "November", "December"], 45 | radioButtons: [ 46 | 'Every Month', 47 | 'Even Months', 48 | 'Odd Months', 49 | 'Every 4 Months', 50 | 'Every Half Year', 51 | ], 52 | selectedRadio, selectedOption, hasError, 53 | onRadioButtonChange, onOptionChange 54 | }; 55 | 56 | // eslint-disable-next-line react/jsx-props-no-spreading 57 | return 58 | } 59 | 60 | const mapStateToProps = (state: { crontabGenerator: { months: PeriodContainerStateInterface } }) => ({ 61 | selectedRadio: state.crontabGenerator.months.selectedRadio, 62 | selectedOption: state.crontabGenerator.months.selectedOption, 63 | hasError: state.crontabGenerator.months.hasError, 64 | }); 65 | 66 | export default compose( 67 | connect(mapStateToProps, { 68 | setRadioButtonValue: setMonthsRadioButtonValue, 69 | setOptionValue: setMonthsOptionValue, 70 | setHasError: setMonthHasError, 71 | }))(Months); 72 | -------------------------------------------------------------------------------- /src/tests/components/PasswordGenerator/PasswordGenerator.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import PasswordGenerator 4 | from "../../../components/PasswordGenerator/PasswordGenerator"; 5 | 6 | const password = 'esn73uctzv2pyk8f'; 7 | const checked = { 8 | symbols: false, 9 | numbers: false, 10 | uppercase: true, 11 | lowercase: false, 12 | excludeSimilar: true 13 | }; 14 | const onCopyButtonClick = jest.fn(); 15 | const onGenerateButtonClick = jest.fn(); 16 | const onCheckboxChange = jest.fn(); 17 | const onPasswordLengthChange = jest.fn(); 18 | 19 | const setup = () => { 20 | render() 30 | } 31 | 32 | describe(' component', () => { 33 | 34 | it('shows the password', () => { 35 | setup(); 36 | expect(screen.getByText(password)).toBeVisible(); 37 | }); 38 | 39 | it('shows the slider title', () => { 40 | setup(); 41 | expect(screen.getByText(/Password Length:/i)).toBeVisible(); 42 | }); 43 | 44 | it('shows the password length', () => { 45 | setup(); 46 | expect(screen.getByText(/17/i)).toBeVisible(); 47 | }); 48 | 49 | it('should handle Copy button click', () => { 50 | setup(); 51 | const button = screen.getByText('Copied'); 52 | expect(button).toBeVisible(); 53 | fireEvent.click(button); 54 | expect(onCopyButtonClick).toBeCalledTimes(1) 55 | }); 56 | 57 | it('should handle Generate button click', () => { 58 | setup(); 59 | const button = screen.getByText('Generate'); 60 | expect(button).toBeVisible(); 61 | fireEvent.click(button); 62 | expect(onGenerateButtonClick).toBeCalledTimes(1) 63 | }); 64 | 65 | it('shows the unchecked checkbox', () => { 66 | setup(); 67 | const checkbox = screen.getByLabelText('Symbols'); 68 | expect(checkbox).toBeVisible(); 69 | expect(checkbox).not.toBeChecked(); 70 | }); 71 | 72 | it('should handle checkbox click action', () => { 73 | setup(); 74 | const checkbox = screen.getByLabelText('Uppercase'); 75 | fireEvent.click(checkbox); 76 | expect(onCheckboxChange).toBeCalledTimes(1) 77 | }) 78 | }) 79 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/Schedule/Hours.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import Period 5 | from "../../../components/CrontabGenerator/FormFields/Schedule/Period"; 6 | import { 7 | PeriodContainerPropsInterface, 8 | PeriodContainerStateInterface, 9 | PeriodPropsInterface 10 | } from "../../../types/crontabGenerator"; 11 | import { 12 | setHoursRadioButtonValue, 13 | setHoursOptionValue, 14 | setHourHasError 15 | } from "../../../actions/CrontabGenerator/Schedule/hours"; 16 | 17 | const Hours = ({ 18 | selectedRadio, 19 | selectedOption, 20 | hasError, 21 | setRadioButtonValue, 22 | setOptionValue, 23 | setHasError 24 | } 25 | : PeriodContainerPropsInterface) => { 26 | 27 | const onRadioButtonChange = ({target: {value}}: { target: { value: string } }) => { 28 | setRadioButtonValue(value); 29 | if (value !== 'Hours') { 30 | setOptionValue(''); 31 | } 32 | setHasError(false); 33 | } 34 | 35 | const onOptionChange = ({target: {value}}: { target: { value: string } }) => { 36 | setOptionValue(value); 37 | setRadioButtonValue('Hours'); 38 | setHasError(false); 39 | } 40 | 41 | const period: PeriodPropsInterface = { 42 | period: 'Hours', 43 | options: ['Midnight', '1am', '2am', '3am', '4am', '5am', '6am', '7am', '8am', '9am', '10am', '11am', 44 | 'Noon', '1pm', '2pm', '3pm', '4pm', '5pm', '6pm', '7pm', '8pm', '9pm', '10pm', '11pm'], 45 | radioButtons: [ 46 | 'Every Hour', 47 | 'Even Hours', 48 | 'Odd Hours', 49 | 'Every 3 Hours', 50 | 'Every 6 Hours', 51 | 'Every 12 Hours', 52 | ], 53 | selectedRadio, selectedOption, hasError, 54 | onRadioButtonChange, onOptionChange 55 | }; 56 | 57 | // eslint-disable-next-line react/jsx-props-no-spreading 58 | return 59 | } 60 | 61 | const mapStateToProps = (state: { crontabGenerator: { hours: PeriodContainerStateInterface }; }) => ({ 62 | selectedRadio: state.crontabGenerator.hours.selectedRadio, 63 | selectedOption: state.crontabGenerator.hours.selectedOption, 64 | hasError: state.crontabGenerator.hours.hasError, 65 | }); 66 | 67 | export default compose( 68 | connect(mapStateToProps, { 69 | setRadioButtonValue: setHoursRadioButtonValue, 70 | setOptionValue: setHoursOptionValue, 71 | setHasError: setHourHasError, 72 | }))(Hours); 73 | -------------------------------------------------------------------------------- /src/types/crontabGenerator.ts: -------------------------------------------------------------------------------- 1 | import {MouseEventHandler} from "react"; 2 | 3 | export type MultiselectOption = { 4 | name: string, 5 | code: string 6 | } 7 | 8 | export type SelectedType = { 9 | selectedOption: MultiselectOption[], 10 | selectedRadio: string, 11 | } 12 | 13 | export type HowToHandleOutputContainerState = { 14 | howToHandleOutputValue: string, 15 | pathToFileValue: string, 16 | emailValue: string 17 | } 18 | 19 | export type GenerateCrontabLineProps = { 20 | result: string, 21 | onClick: MouseEventHandler 22 | } 23 | 24 | export interface HowToHandleOutputContainerProps extends HowToHandleOutputContainerState { 25 | setHowToHandleOutput: Function, 26 | setPathToFile: Function, 27 | setEmail: Function, 28 | } 29 | 30 | export interface PeriodContainerStateInterface extends SelectedType { 31 | hasError: boolean 32 | } 33 | 34 | export interface PeriodContainerPropsInterface extends PeriodContainerStateInterface { 35 | setRadioButtonValue: Function, 36 | setOptionValue: Function, 37 | setHasError: Function 38 | } 39 | 40 | export interface RadioButtonWithMultiSelectPropsInterface extends SelectedType { 41 | period: string, 42 | options: (string | number)[], 43 | onRadioButtonChange: any, 44 | onOptionChange: any 45 | } 46 | 47 | export type CommandToExecuteContainerState = { 48 | commandToExecuteValue: string, 49 | commandHasError: boolean, 50 | } 51 | 52 | export interface CommandToExecutePropsInterface extends CommandToExecuteContainerState { 53 | onCommandToExecuteChange: any 54 | } 55 | 56 | export interface CommandToExecuteContainerPropsInterface extends CommandToExecuteContainerState { 57 | setCommandToExecute: Function, 58 | setCommandHasErrorValue: Function 59 | } 60 | 61 | export type RadioButtonListProps = { 62 | radioButtons: string[], 63 | selectedRadio: string , 64 | onRadioButtonChange: any 65 | } 66 | 67 | export type HowToHandleOutputProps = { 68 | selectedRadio: string, 69 | pathToFileValue: string, 70 | emailValue: string, 71 | onRadioButtonChange: any, 72 | onPathToFileChange: any, 73 | onEmailChange: any 74 | } 75 | 76 | export type FileOrEmailProps = { 77 | name: string, 78 | label: string, 79 | radioButtonValue: string, 80 | textInputValue: string, 81 | radioButtonOnChange: any, 82 | textInputOnChange: any 83 | } 84 | 85 | export type GenerateCrontabLineContainerState = { 86 | formState: any, 87 | result: string, 88 | setResultValue: Function, 89 | setMinuteError: Function, 90 | setHourError: Function, 91 | setDayError: Function, 92 | setMonthError: Function, 93 | setWeekdayError: Function, 94 | setCommandError: Function 95 | } 96 | 97 | export interface PeriodPropsInterface extends RadioButtonWithMultiSelectPropsInterface { 98 | radioButtons: string[], 99 | hasError: boolean 100 | } 101 | -------------------------------------------------------------------------------- /src/containers/ChmodCalculator/ChmodCalculator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import { 5 | checkboxesToChmodNumber, 6 | chmodNumberToString, 7 | chmodStringToCheckboxes 8 | } from "../../utils/calculators/chmodCalculator"; 9 | import {setChecked, setNumberFieldValue, setTextFieldValue} from "../../actions/ChmodCalculator/chmodCalculator"; 10 | import {isNumberValid, isStringValid} from "../../utils/validators/chmodValidator"; 11 | import Calculator from "../../components/ChmodCalculator/ChmodCalculator"; 12 | import { 13 | CheckboxesType, 14 | ChmodCalculatorContainerPropsInterface, 15 | ChmodCalculatorContainerStateType 16 | } from "../../types/chmodCalculator"; 17 | 18 | const ChmodCalculator = ({ 19 | checked, numberFieldValue, textFieldValue, 20 | setCheckedValue, setNumberValue, setTextValue 21 | }: ChmodCalculatorContainerPropsInterface) => { 22 | 23 | const onCheckboxChange = (event: { target: { id: string; checked: boolean } }) => { 24 | 25 | const newCheckboxesState: CheckboxesType = {...checked}; 26 | newCheckboxesState[event.target.id] = event.target.checked; 27 | 28 | // Update state 29 | setCheckedValue(newCheckboxesState); 30 | setNumberValue(checkboxesToChmodNumber(newCheckboxesState)); 31 | setTextValue(chmodNumberToString(checkboxesToChmodNumber(newCheckboxesState))); 32 | } 33 | 34 | const onNumberChange = ({target: {value}}: { target: { value: string } }) => { 35 | if (!isNumberValid(value)) { 36 | return; 37 | } 38 | 39 | setNumberValue(value); 40 | 41 | if (value.length === 3) { 42 | const newCheckboxState = chmodStringToCheckboxes(chmodNumberToString(value)); 43 | setCheckedValue(newCheckboxState); 44 | setTextValue(chmodNumberToString(value)); 45 | } 46 | } 47 | 48 | const onTextChange = ({target: {value}}: { target: { value: string } }) => { 49 | if (!isStringValid(value)) { 50 | return; 51 | } 52 | 53 | setTextValue(value); 54 | 55 | if (value.length === 9) { 56 | const nexCheckboxState = chmodStringToCheckboxes(value); 57 | setCheckedValue(nexCheckboxState); 58 | setNumberValue(checkboxesToChmodNumber(nexCheckboxState)); 59 | } 60 | } 61 | 62 | return 70 | } 71 | 72 | const mapStateToProps = (state: { chmodCalculator: ChmodCalculatorContainerStateType }) => ({ 73 | checked: state.chmodCalculator.checked, 74 | numberFieldValue: state.chmodCalculator.numberFieldValue, 75 | textFieldValue: state.chmodCalculator.textFieldValue, 76 | }); 77 | 78 | export default compose( 79 | connect(mapStateToProps, { 80 | setCheckedValue: setChecked, 81 | setNumberValue: setNumberFieldValue, 82 | setTextValue: setTextFieldValue 83 | }))(ChmodCalculator); 84 | -------------------------------------------------------------------------------- /src/constants/ActionTypes.ts: -------------------------------------------------------------------------------- 1 | // Chmod Calculator 2 | export const SET_CHECKED = 'chmod-calculator/set-checked'; 3 | export const SET_NUMBER_FIELD_VALUE = 'chmod-calculator/set-number-field-value'; 4 | export const SET_TEXT_FIELD_VALUE = 'chmod-calculator/set-text-field-value'; 5 | 6 | // Epoch Converter 7 | export const SET_UNIX_TIME_INPUT_VALUE = 'epoch-converter/unix-time-form/set-unix-time-input-value'; 8 | export const SET_EPOCH_TO_DATE_RESULT = 'epoch-converter/unix-time-form/set-epoch-to-date-result'; 9 | 10 | export const SET_DATE_TIME_INPUT_VALUE = 'epoch-converter/date-time-form/set-date-time-input-value'; 11 | export const SET_DATE_TIMEZONE_INPUT_VALUE = 'epoch-converter/date-time-form/set-timezone-input-value'; 12 | export const SET_DATE_TO_EPOCH_RESULT = 'epoch-converter/date-time-form/set-date-to-epoch-result'; 13 | 14 | export const SET_CURRENT_UNIX_TIME = 'epoch-converter/current-epoch-time/set-current-unix-time'; 15 | export const SET_COPY_TO_CLIPBOARD_BUTTON_STATE = 'epoch-converter/current-epoch-time/set-copy-to-clipboard-button-state'; 16 | 17 | // CrontabGenerator 18 | export const SET_COMMAND_TO_EXECUTE = 'crontab-generator/command-to-execute/set-command-to-execute-value'; 19 | export const SET_COMMAND_HAS_ERROR = 'crontab-generator/command-to-execute/set-command-has-error'; 20 | 21 | export const GENERATE_CRONTAB_LINE = 'crontab-generator/generate-crontab-line/set-result'; 22 | 23 | export const HOW_TO_HANDLE_OUTPUT = 'crontab-generator/how-to-handle-output/set-how-to-handle-output'; 24 | export const SET_PATH_TO_FILE_VALUE = 'crontab-generator/how-to-handle-output/set-path-to-file'; 25 | export const SET_EMAIL_VALUE = 'crontab-generator/how-to-handle-output/set-set-email'; 26 | 27 | export const SET_DAYS_RADIO_BUTTON_VALUE = 'crontab-generator/days/set-radio-button-value'; 28 | export const SET_DAYS_OPTION_VALUE = 'crontab-generator/days/set-option-value'; 29 | export const SET_DAY_HAS_ERROR = 'crontab-generator/days/set-has-error'; 30 | 31 | export const SET_HOURS_RADIO_BUTTON_VALUE = 'crontab-generator/hours/set-radio-button-value'; 32 | export const SET_HOURS_OPTION_VALUE = 'crontab-generator/hours/set-option-value'; 33 | export const SET_HOUR_HAS_ERROR = 'crontab-generator/hours/set-has-error'; 34 | 35 | export const SET_MINUTES_RADIO_BUTTON_VALUE = 'crontab-generator/minutes/set-radio-button-value'; 36 | export const SET_MINUTES_OPTION_VALUE = 'crontab-generator/minutes/set-option-value'; 37 | export const SET_MINUTE_HAS_ERROR = 'crontab-generator/minutes/set-has-error'; 38 | 39 | export const SET_MONTHS_RADIO_BUTTON_VALUE = 'crontab-generator/months/set-radio-button-value'; 40 | export const SET_MONTHS_OPTION_VALUE = 'crontab-generator/months/set-option-value'; 41 | export const SET_MONTH_HAS_ERROR = 'crontab-generator/months/set-has-error'; 42 | 43 | export const SET_WEEKDAY_RADIO_BUTTON_VALUE = 'crontab-generator/weekday/set-radio-button-value'; 44 | export const SET_WEEKDAY_OPTION_VALUE = 'crontab-generator/weekday/set-option-value'; 45 | export const SET_WEEKDAY_HAS_ERROR = 'crontab-generator/weekday/set-has-error'; 46 | 47 | // PasswordGenerator 48 | export const SET_PASSWORD = 'password-generator/set-password'; 49 | export const SET_PASSWORD_LENGTH = 'password-generator/set-password-length'; 50 | export const SET_CHECKED_PARAM = 'password-generator/set-checked'; 51 | export const SET_COPY_TO_CLIPBOARD_STATE = 'password-generator/set-copy-to-clipboard-state'; 52 | -------------------------------------------------------------------------------- /src/containers/EpochConverter/DateTimeForm.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect} from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import {humanReadableDateToEpoch} from "../../utils/calculators/epochConverter"; 5 | import DateTimeFormComponent 6 | from "../../components/EpochConverter/Forms/DateTimeForm" 7 | import { 8 | setDateTimeInputValue, 9 | setTimezoneInputValue, 10 | setDateToEpochResult 11 | } from "../../actions/EpochConverter/dateTimeForm" 12 | import { 13 | DateTimeFormContainerPropsInterface, 14 | DateTimeFormState 15 | } from "../../types/epochConverter"; 16 | 17 | const DateTimeForm = ({ 18 | dateTimeInputValue, 19 | timezoneInputValue, 20 | dateToEpochResult, 21 | setDateTimeInput, 22 | setTimezoneInput, 23 | setDateToEpochResultValue 24 | }: DateTimeFormContainerPropsInterface) => { 25 | 26 | useEffect(() => { 27 | if (!dateTimeInputValue) { 28 | setDateTimeInput(Date()); 29 | } 30 | }, [dateTimeInputValue, setDateTimeInput]); 31 | 32 | const onDateTimeChange = ({target: {value}}: { target: { value: string } }) => { 33 | setDateTimeInput(value ? value.toString() : null); 34 | 35 | if (value === null || value === undefined) { 36 | setDateToEpochResultValue(''); 37 | } else if (dateToEpochResult !== '') { 38 | setDateToEpochResultValue(humanReadableDateToEpoch(value.toString(), timezoneInputValue)); 39 | } 40 | } 41 | 42 | const onTimezoneChange = ({target: {value}}: { target: { value: 'gmt' | 'local' } }) => { 43 | setTimezoneInput(value); 44 | if (dateToEpochResult && dateTimeInputValue) { 45 | setDateToEpochResultValue(humanReadableDateToEpoch(dateTimeInputValue.toString(), value)); 46 | } 47 | } 48 | 49 | const onDateToEpochButtonClick = () => { 50 | if (dateTimeInputValue) { 51 | setDateToEpochResultValue(humanReadableDateToEpoch(dateTimeInputValue.toString(), timezoneInputValue)); 52 | } else { 53 | setDateToEpochResultValue(''); 54 | } 55 | } 56 | 57 | return 64 | 65 | } 66 | 67 | const mapStateToProps = (state: { epochConverter: { dateTimeForm: DateTimeFormState } }) => ({ 68 | dateTimeInputValue: state.epochConverter.dateTimeForm.dateTimeInputValue, 69 | timezoneInputValue: state.epochConverter.dateTimeForm.timezoneInputValue, 70 | dateToEpochResult: state.epochConverter.dateTimeForm.dateToEpochResult, 71 | }); 72 | 73 | export default compose( 74 | connect(mapStateToProps, { 75 | setDateTimeInput: setDateTimeInputValue, 76 | setTimezoneInput: setTimezoneInputValue, 77 | setDateToEpochResultValue: setDateToEpochResult, 78 | }))(DateTimeForm); 79 | -------------------------------------------------------------------------------- /src/containers/PasswordGenerator/PasswordGenerator.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import {connect} from "react-redux"; 3 | import {compose} from "redux"; 4 | import PasswordGeneratorComponent 5 | from "../../components/PasswordGenerator/PasswordGenerator"; 6 | import { 7 | setCheckedParam, setCopyToClipboardState, setPassword, setPasswordLength 8 | } from "../../actions/PasswordGenerator/passwordGenerator"; 9 | import generatePassword from "../../utils/generators/passwordGenerator"; 10 | import { 11 | CheckboxesType, 12 | PasswordGeneratorContainerStateInterface 13 | } from "../../types/passwordGenerator"; 14 | import isChangeAllowed from "../../utils/validators/passwordValidator"; 15 | 16 | const PasswordGenerator = ({ 17 | result, 18 | checked, 19 | passwordLength, 20 | copyToClipboardButtonState, 21 | setPasswordValue, 22 | setPasswordLengthValue, 23 | setChecked, 24 | setCopyToClipboardButtonState 25 | }: PasswordGeneratorContainerStateInterface) => { 26 | 27 | if (result === '') { 28 | setPasswordValue(generatePassword(passwordLength, checked)) 29 | } 30 | 31 | const onCheckboxChange = (event: { target: { name: string; checked: boolean } }) => { 32 | const newCheckboxesState: CheckboxesType = {...checked}; 33 | newCheckboxesState[event.target.name] = event.target.checked; 34 | if (isChangeAllowed(newCheckboxesState)) { 35 | setChecked(newCheckboxesState); 36 | setPasswordValue(generatePassword(passwordLength, newCheckboxesState)); 37 | } 38 | setCopyToClipboardButtonState('default'); 39 | } 40 | 41 | const onPasswordLengthChange = (event: { value: string; }) => { 42 | setPasswordLengthValue(event.value); 43 | setCopyToClipboardButtonState('default'); 44 | if (event.value !== passwordLength) { 45 | setPasswordValue(generatePassword(event.value, checked)); 46 | } 47 | } 48 | 49 | const onCopyButtonClick = () => { 50 | navigator.clipboard.writeText(result); 51 | setCopyToClipboardButtonState('pressed'); 52 | } 53 | 54 | const onGenerateButtonClick = () => { 55 | setPasswordValue(generatePassword(passwordLength, checked)); 56 | setCopyToClipboardButtonState('default'); 57 | } 58 | 59 | return 69 | } 70 | 71 | const mapStateToProps = (state: any) => ({ 72 | result: state.passwordGenerator.result, 73 | checked: state.passwordGenerator.checked, 74 | passwordLength: state.passwordGenerator.passwordLength, 75 | copyToClipboardButtonState: state.passwordGenerator.copyToClipboardButtonState, 76 | }); 77 | 78 | export default compose( 79 | connect(mapStateToProps, { 80 | setPasswordValue: setPassword, 81 | setPasswordLengthValue: setPasswordLength, 82 | setChecked: setCheckedParam, 83 | setCopyToClipboardButtonState: setCopyToClipboardState 84 | }))(PasswordGenerator); 85 | -------------------------------------------------------------------------------- /src/utils/transformers/humanReadableToCrontab.ts: -------------------------------------------------------------------------------- 1 | import {MultiselectOption} from "../../types/crontabGenerator"; 2 | import isEmailValid from "../validators/emailValidator"; 3 | 4 | const map = require('./map.json'); 5 | 6 | export const isNumeric = (string: string): boolean => /^-?\d+$/.test(string) 7 | 8 | export const wordToNumber = (string: string): string => { 9 | if (isNumeric(string)) { 10 | return string; 11 | } 12 | 13 | // @ts-ignore 14 | return map[string]; 15 | } 16 | 17 | const getRanges = (array: any[]): any[] => { 18 | const ranges = []; 19 | let start; 20 | let end; 21 | for (let i = 0; i < array.length; i += 1) { 22 | start = array[i]; 23 | end = start; 24 | while (array[i + 1] - array[i] === 1) { 25 | end = array[i + 1]; 26 | i += 1; 27 | } 28 | ranges.push(start === end ? `${start}` : `${start}-${end}`); 29 | } 30 | return ranges; 31 | } 32 | 33 | export const groupNumbers = (array: string[]): any[] => { 34 | 35 | const arrayOfNumbers = array.map(number => parseInt(number, 10)) 36 | 37 | arrayOfNumbers.sort((a, b) => a - b); 38 | 39 | return getRanges(arrayOfNumbers); 40 | } 41 | 42 | const transformOptions = (optionValue: MultiselectOption[]): string => { 43 | 44 | const array: string[] = optionValue.map(item => wordToNumber(item.code)); 45 | 46 | return groupNumbers(array).toString(); 47 | } 48 | 49 | export const transformPeriod = (radioButtonValue: string, optionValue: any[]): string => { 50 | 51 | if (!['Minutes', 'Hours', 'Days', 'Months', 'Weekday'].includes(radioButtonValue)) { 52 | // @ts-ignore 53 | return map[radioButtonValue]; 54 | } 55 | 56 | if (optionValue.length === 0) { 57 | return ''; 58 | } 59 | 60 | return transformOptions(optionValue); 61 | } 62 | 63 | export const transform = ( 64 | minutesRadio: string, 65 | minutesOptions: any[], 66 | hoursRadio: string, 67 | hoursOptions: any[], 68 | daysRadio: string, 69 | daysOptions: any[], 70 | monthsRadio: string, 71 | monthsOptions: any[], 72 | weekdayRadio: string, 73 | weekdayOptions: any[], 74 | command: string, 75 | output: string, 76 | path: string, 77 | email: string 78 | ): string => { 79 | const minutes = transformPeriod(minutesRadio, minutesOptions); 80 | const hours = transformPeriod(hoursRadio, hoursOptions); 81 | const days = transformPeriod(daysRadio, daysOptions); 82 | const months = transformPeriod(monthsRadio, monthsOptions); 83 | const weekday = transformPeriod(weekdayRadio, weekdayOptions); 84 | 85 | if (!minutes) return 'Invalid Minutes'; 86 | if (!hours) return 'Invalid Hours'; 87 | if (!days) return 'Invalid Days'; 88 | if (!months) return 'Invalid Months'; 89 | if (!weekday) return 'Invalid Weekday'; 90 | if (!command) return 'Invalid Command'; 91 | if (!isEmailValid(email) && output === 'email') return 'Invalid Email'; 92 | if (!path && output === 'file') return 'Invalid Path'; 93 | 94 | let wayToHandleOutput; 95 | 96 | if (output === 'mute') { 97 | wayToHandleOutput = '>/dev/null 2>&1'; 98 | } else if (output === 'file') { 99 | wayToHandleOutput = `> ${path}`; 100 | } else if (output === 'email') { 101 | wayToHandleOutput = `MAILTO="${email}" 102 | `; 103 | } 104 | 105 | const expression = `${output === 'email' ? wayToHandleOutput : ''} ` + 106 | `${minutes} ${hours} ${days} ${months} ${weekday} ` + 107 | `${command} ${output !== 'email' ? wayToHandleOutput : ''}`; 108 | 109 | return expression.trim() 110 | } 111 | 112 | export default transform; 113 | -------------------------------------------------------------------------------- /src/containers/CrontabGenerator/GenerateCrontabLine.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef} from "react"; 2 | import {Toast} from 'primereact/toast'; 3 | import {compose} from "redux"; 4 | import {connect} from "react-redux"; 5 | import GenerateCrontabLineComponent 6 | from '../../components/CrontabGenerator/GenerateCrontabLine'; 7 | import {setResult} from "../../actions/CrontabGenerator/generateCrontabLine"; 8 | import { 9 | getCronExpression, 10 | getFormState, 11 | getResult 12 | } from "../../selectors/crontabGenerator"; 13 | import {setMinuteHasError} from "../../actions/CrontabGenerator/Schedule/minutes"; 14 | import {setDayHasError} from "../../actions/CrontabGenerator/Schedule/days"; 15 | import {setHourHasError} from "../../actions/CrontabGenerator/Schedule/hours"; 16 | import {setMonthHasError} from "../../actions/CrontabGenerator/Schedule/months"; 17 | import {setWeekdayHasError} from "../../actions/CrontabGenerator/Schedule/weekday"; 18 | import {setCommandHasError} from "../../actions/CrontabGenerator/commandToExecute"; 19 | import {GenerateCrontabLineContainerState} from "../../types/crontabGenerator"; 20 | 21 | const GenerateCrontabLine = ({ 22 | formState, 23 | result, 24 | setResultValue, 25 | setMinuteError, 26 | setHourError, 27 | setDayError, 28 | setMonthError, 29 | setWeekdayError, 30 | setCommandError 31 | }: GenerateCrontabLineContainerState) => { 32 | 33 | const toast: React.MutableRefObject = useRef(null); 34 | 35 | const showError = (message: string) => { 36 | toast.current.show({severity: 'error', summary: message}); 37 | } 38 | 39 | const onButtonClick = () => { 40 | 41 | const expression: string = getCronExpression(formState); 42 | 43 | switch (expression) { 44 | case 'Invalid Minutes': 45 | showError('Please choose at least one entry in minutes field.'); 46 | setMinuteError(true); 47 | break; 48 | case 'Invalid Hours': 49 | showError('Please choose at least one entry in hours field.'); 50 | setHourError(true); 51 | break; 52 | case 'Invalid Days': 53 | showError('Please choose at least one entry in days field.'); 54 | setDayError(true) 55 | break; 56 | case 'Invalid Months': 57 | showError('Please choose at least one entry in months field.'); 58 | setMonthError(true); 59 | break; 60 | case 'Invalid Weekday': 61 | showError('Please choose at least one entry in weekday field.'); 62 | setWeekdayError(true); 63 | break; 64 | case 'Invalid Command': 65 | showError('Please enter a command.'); 66 | setCommandError(true); 67 | break; 68 | case 'Invalid Email': 69 | showError('Please enter a valid Email address.'); 70 | break; 71 | case 'Invalid Path': 72 | showError('Please enter a file path for saving output.'); 73 | break; 74 | default: 75 | setResultValue(expression); 76 | } 77 | } 78 | 79 | return <> 80 | 81 | 82 | > 83 | } 84 | 85 | const mapStateToProps = (state: any) => ({ 86 | formState: getFormState(state), 87 | result: getResult(state), 88 | }); 89 | 90 | export default compose( 91 | connect(mapStateToProps, { 92 | setMinuteError: setMinuteHasError, 93 | setHourError: setHourHasError, 94 | setDayError: setDayHasError, 95 | setMonthError: setMonthHasError, 96 | setWeekdayError: setWeekdayHasError, 97 | setCommandError: setCommandHasError, 98 | setResultValue: setResult 99 | }) 100 | )(GenerateCrontabLine); 101 | -------------------------------------------------------------------------------- /src/tests/components/CrontabGenerator/CrontabGenerator.test.tsx: -------------------------------------------------------------------------------- 1 | import {fireEvent, render, screen} from "@testing-library/react"; 2 | import React from "react"; 3 | import {Provider} from "react-redux"; 4 | import CrontabGenerator 5 | from "../../../components/CrontabGenerator/CrontabGenerator"; 6 | import store from "../../../store/configureStore"; 7 | 8 | const setup = () => { 9 | render(); 10 | } 11 | 12 | describe(' component', () => { 13 | 14 | it('should render all radio buttons', () => { 15 | setup(); 16 | [ 17 | 'Every Minute', 18 | 'Every 15 Minutes', 19 | 'Every 12 Hours', 20 | 'Even Days', 21 | 'Every Half Month', 22 | 'Every Month', 23 | 'Even Months', 24 | 'Every Half Year', 25 | 'Monday-Friday', 26 | 'Weekend Days', 27 | ].forEach((item) => { 28 | expect(screen.getByLabelText(item)).toBeVisible(); 29 | }); 30 | }); 31 | 32 | it('should simulate radio button change', () => { 33 | setup(); 34 | [ 35 | 'Odd Minutes', 36 | 'Every 3 Hours', 37 | 'Every 10 Days', 38 | 'Odd Months', 39 | 'Monday-Friday', 40 | ].forEach((item) => { 41 | const radioButton = screen.getByLabelText(item); 42 | expect(radioButton).not.toBeChecked(); 43 | fireEvent.click(radioButton); 44 | expect(radioButton).toBeChecked(); 45 | }); 46 | }); 47 | 48 | it('should generate crontab expression', () => { 49 | setup(); 50 | [ 51 | 'Every 5 Minutes', 52 | 'Odd Hours', 53 | 'Every 5 Days', 54 | 'Every 4 Months', 55 | 'Every Weekday', 56 | ].forEach((item) => { 57 | const radioButton = screen.getByLabelText(item); 58 | fireEvent.click(radioButton); 59 | }); 60 | 61 | const button = screen.getByText('Generate Crontab Line'); 62 | 63 | expect(screen.queryByText('Please enter a command.')).not.toBeInTheDocument(); 64 | fireEvent.click(button); 65 | expect(screen.queryByText('Please enter a command.')).toBeVisible(); 66 | 67 | const command = screen.getByLabelText('Command To Execute'); 68 | fireEvent.change(command, {target: {value: 'df -P .'}}); 69 | fireEvent.click(button); 70 | expect(screen.getByText('*/5 1-23/2 */5 */4 * df -P . >/dev/null 2>&1')).toBeVisible(); 71 | }); 72 | 73 | it('should render toast: Invalid Minutes', () => { 74 | setup(); 75 | const button = screen.getByText('Generate Crontab Line'); 76 | fireEvent.click(screen.getByLabelText('Minutes')); 77 | fireEvent.click(button); 78 | expect(screen.getByText('Please choose at least one entry in minutes field.')).toBeInTheDocument(); 79 | }); 80 | 81 | it('should render toast: Invalid Hours', () => { 82 | setup(); 83 | const button = screen.getByText('Generate Crontab Line'); 84 | fireEvent.click(screen.getByLabelText('Every Minute')); 85 | fireEvent.click(screen.getByLabelText('Hours')); 86 | fireEvent.click(button); 87 | expect(screen.getByText('Please choose at least one entry in hours field.')).toBeInTheDocument(); 88 | }); 89 | 90 | it('should render toast: Invalid Days', () => { 91 | setup(); 92 | const button = screen.getByText('Generate Crontab Line'); 93 | fireEvent.click(screen.getByLabelText('Every Hour')); 94 | fireEvent.click(screen.getByLabelText('Days')); 95 | fireEvent.click(button); 96 | expect(screen.getByText('Please choose at least one entry in days field.')).toBeInTheDocument(); 97 | }); 98 | 99 | it('should render toast: Invalid Months', () => { 100 | setup(); 101 | const button = screen.getByText('Generate Crontab Line'); 102 | fireEvent.click(screen.getByLabelText('Every Day')); 103 | fireEvent.click(screen.getByLabelText('Months')); 104 | fireEvent.click(button); 105 | expect(screen.getByText('Please choose at least one entry in months field.')).toBeInTheDocument(); 106 | }); 107 | 108 | it('should render toast: Invalid Weekday', () => { 109 | setup(); 110 | const button = screen.getByText('Generate Crontab Line'); 111 | fireEvent.click(screen.getByLabelText('Every Month')); 112 | fireEvent.click(screen.getByLabelText('Weekday')); 113 | fireEvent.click(button); 114 | expect(screen.getByText('Please choose at least one entry in weekday field.')).toBeInTheDocument(); 115 | }); 116 | 117 | it('should render toast: Invalid Email', () => { 118 | setup(); 119 | const button = screen.getByText('Generate Crontab Line'); 120 | fireEvent.click(screen.getByLabelText('Every Weekday')); 121 | fireEvent.click(screen.getByLabelText('Send output to Email:')); 122 | const command = screen.getByLabelText('Command To Execute'); 123 | fireEvent.change(command, {target: {value: 'df -P .'}}); 124 | fireEvent.click(button); 125 | expect(screen.getByText('Please enter a valid Email address.')).toBeInTheDocument(); 126 | }); 127 | 128 | it('should render toast: Invalid Path', () => { 129 | setup(); 130 | const button = screen.getByText('Generate Crontab Line'); 131 | fireEvent.click(screen.getByLabelText('Save output to file:')); 132 | const command = screen.getByLabelText('Command To Execute'); 133 | fireEvent.change(command, {target: {value: 'df -P .'}}); 134 | fireEvent.click(button); 135 | expect(screen.getByText('Please enter a file path for saving output.')).toBeInTheDocument(); 136 | }); 137 | }); 138 | -------------------------------------------------------------------------------- /src/tests/utils/transformers/humanReadableToCrontab.test.ts: -------------------------------------------------------------------------------- 1 | import transform, { 2 | groupNumbers, 3 | isNumeric, transformPeriod, wordToNumber 4 | } from "../../../utils/transformers/humanReadableToCrontab"; 5 | 6 | describe('isNumeric() function test', () => { 7 | 8 | const numericValues = ['0', '1', '2', '5', '8', '10', '17', '21', '30', '99']; 9 | const nonNumericValues = ['1st', '2am', '5pm', '8th', 'test10']; 10 | 11 | numericValues.forEach((string) => { 12 | test(`The string ${string} is valid numeric value`, () => { 13 | expect(isNumeric(string)).toBeTruthy(); 14 | }); 15 | }); 16 | 17 | nonNumericValues.forEach((string) => { 18 | test(`The string ${string} is invalid numeric value`, () => { 19 | expect(isNumeric(string)).toBeFalsy(); 20 | }); 21 | }); 22 | }); 23 | 24 | describe('groupNumbers() function test', () => { 25 | 26 | const validPairs = [ 27 | { 28 | input: ['0', '1', '2', '7', '5', '6', '11'], 29 | output: ['0-2', '5-7', '11'] 30 | }, 31 | { 32 | input: ['8', '12', '14', '1', '2', '7', '3', '5', '6', '11'], 33 | output: ['1-3', '5-8', '11-12', '14'] 34 | } 35 | ]; 36 | 37 | validPairs.forEach((pair) => { 38 | test(`Numbers are grouped correctly`, () => { 39 | expect(groupNumbers(pair.input)).toStrictEqual(pair.output); 40 | }); 41 | }); 42 | }); 43 | 44 | describe('wordToNumber() function test', () => { 45 | 46 | const validPairs = [ 47 | {input: 'Every Minute', output: '*'}, 48 | {input: 'Every 15 Minutes', output: '*/15'}, 49 | {input: 'Odd Hours', output: '1-23/2'}, 50 | {input: 'Every Half Month', output: '*/15'}, 51 | {input: 'Even Days', output: '*/2'}, 52 | {input: 'Every 4 Months', output: '*/4'}, 53 | {input: 'July', output: '7'}, 54 | {input: 'Monday-Friday', output: '1-5'}, 55 | {input: 'Weekend Days', output: '0,6'}, 56 | ]; 57 | 58 | validPairs.forEach((pair) => { 59 | test(`The word ${pair.input} is correctly transformed into number ${pair.output}`, () => { 60 | expect(wordToNumber(pair.input)).toStrictEqual(pair.output); 61 | }); 62 | }); 63 | }); 64 | 65 | describe('transformPeriod() function test', () => { 66 | const validPairs = [ 67 | {input: {radio: 'Even Minutes', options: []}, output: '*/2'}, 68 | {input: {radio: 'Hours', options: []}, output: ''}, 69 | { 70 | input: { 71 | radio: 'Minutes', options: [ 72 | {name: '0', code: '0'}, 73 | {name: '3', code: '3'}, 74 | ] 75 | }, output: '0,3' 76 | }, 77 | {input: {radio: 'Every 12 Hours', options: []}, output: '*/12'}, 78 | {input: {radio: 'Every Day', options: []}, output: '*'}, 79 | { 80 | input: { 81 | radio: 'Weekday', options: [ 82 | {name: 'Sunday', code: 'Sunday'}, 83 | {name: 'Monday', code: 'Monday'}, 84 | {name: 'Tuesday', code: 'Tuesday'}, 85 | {name: 'Wednesday', code: 'Wednesday'}, 86 | {name: 'Saturday', code: 'Saturday'}, 87 | ] 88 | }, output: '0-3,6' 89 | }, 90 | ]; 91 | 92 | validPairs.forEach((pair) => { 93 | test(`The selected period is correctly transformed into crontab schedule`, () => { 94 | expect(transformPeriod(pair.input.radio, pair.input.options)) 95 | .toStrictEqual(pair.output); 96 | }); 97 | }); 98 | }); 99 | 100 | describe('transform() function test', () => { 101 | test(`The form data is correctly transformed into crontab expression`, () => { 102 | expect(transform('Every 5 Minutes', [], 103 | 'Hours', [ 104 | {name: '4am', code: '4am'}, 105 | {name: '1pm', code: '1pm'}, 106 | {name: '5am', code: '5am'}, 107 | {name: 'Noon', code: 'Noon'}, 108 | {name: '2pm', code: '2pm'}, 109 | ], 110 | 'Odd Days', [], 111 | 'Months', [ 112 | {name: 'May', code: 'May'}, 113 | {name: 'April', code: 'April'}, 114 | ], 115 | 'Weekend Days', [], 116 | 'mysqldump -u root -p db_name > /root/dump.sql', 117 | 'mute', '', '')) 118 | .toStrictEqual( 119 | '*/5 4-5,12-14 1-31/2 4-5 0,6 mysqldump -u root -p db_name > /root/dump.sql >/dev/null 2>&1' 120 | ); 121 | }); 122 | 123 | test(`The form data is correctly transformed into crontab expression`, () => { 124 | expect(transform('Every Minute', [], 125 | 'Every Hour', [], 126 | 'Every Day', [], 127 | 'Every Month', [], 128 | 'Every Weekday', [], 129 | '/usr/bin/wget --spider "https://site.io/cron.php"', 130 | 'file', '/root/cron.log', '')) 131 | .toStrictEqual( 132 | '* * * * * /usr/bin/wget --spider "https://site.io/cron.php" > /root/cron.log' 133 | ); 134 | }); 135 | 136 | test(`The form data is correctly transformed into crontab expression`, () => { 137 | expect(transform('Every 30 Minutes', [], 138 | 'Every 6 Hours', [], 139 | 'Every 5 Days', [], 140 | 'Even Months', [], 141 | 'Weekend Days', [], 142 | '/usr/bin/wget --spider "https://site.io/cron.php"', 143 | 'email', '/root/cron.log', 'info@example.com')) 144 | .toStrictEqual( 145 | `MAILTO="info@example.com" 146 | */30 */6 */5 */2 0,6 /usr/bin/wget --spider "https://site.io/cron.php"` 147 | ); 148 | }); 149 | }); 150 | -------------------------------------------------------------------------------- /src/serviceWorker.ts: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://bit.ly/CRA-PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === 'localhost' || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === '[::1]' || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | type Config = { 24 | // eslint-disable-next-line no-unused-vars 25 | onSuccess?: (registration: ServiceWorkerRegistration) => void; 26 | // eslint-disable-next-line no-unused-vars 27 | onUpdate?: (registration: ServiceWorkerRegistration) => void; 28 | }; 29 | 30 | export function register(config?: Config) { 31 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) { 32 | // The URL constructor is available in all browsers that support SW. 33 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 34 | if (publicUrl.origin !== window.location.origin) { 35 | // Our service worker won't work if PUBLIC_URL is on a different origin 36 | // from what our page is served on. This might happen if a CDN is used to 37 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 38 | return; 39 | } 40 | 41 | window.addEventListener('load', () => { 42 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 43 | 44 | if (isLocalhost) { 45 | // This is running on localhost. Let's check if a service worker still exists or not. 46 | checkValidServiceWorker(swUrl, config); 47 | 48 | // Add some additional logging to localhost, pointing developers to the 49 | // service worker/PWA documentation. 50 | navigator.serviceWorker.ready.then(() => { 51 | // eslint-disable-next-line no-console 52 | console.log( 53 | 'This web app is being served cache-first by a service ' + 54 | 'worker. To learn more, visit https://bit.ly/CRA-PWA' 55 | ); 56 | }); 57 | } else { 58 | // Is not localhost. Just register service worker 59 | registerValidSW(swUrl, config); 60 | } 61 | }); 62 | } 63 | } 64 | 65 | function registerValidSW(swUrl: string, config?: Config) { 66 | navigator.serviceWorker 67 | .register(swUrl) 68 | .then((registration) => { 69 | // eslint-disable-next-line no-param-reassign 70 | registration.onupdatefound = () => { 71 | const installingWorker = registration.installing; 72 | if (installingWorker == null) { 73 | return; 74 | } 75 | installingWorker.onstatechange = () => { 76 | if (installingWorker.state === 'installed') { 77 | if (navigator.serviceWorker.controller) { 78 | // At this point, the updated precached content has been fetched, 79 | // but the previous service worker will still serve the older 80 | // content until all client tabs are closed. 81 | // eslint-disable-next-line no-console 82 | console.log( 83 | 'New content is available and will be used when all ' + 84 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.' 85 | ); 86 | 87 | // Execute callback 88 | if (config && config.onUpdate) { 89 | config.onUpdate(registration); 90 | } 91 | } else { 92 | // At this point, everything has been precached. 93 | // It's the perfect time to display a 94 | // "Content is cached for offline use." message. 95 | // eslint-disable-next-line no-console 96 | console.log('Content is cached for offline use.'); 97 | 98 | // Execute callback 99 | if (config && config.onSuccess) { 100 | config.onSuccess(registration); 101 | } 102 | } 103 | } 104 | }; 105 | }; 106 | }) 107 | .catch((error) => { 108 | // eslint-disable-next-line no-console 109 | console.error('Error during service worker registration:', error); 110 | }); 111 | } 112 | 113 | function checkValidServiceWorker(swUrl: string, config?: Config) { 114 | // Check if the service worker can be found. If it can't reload the page. 115 | fetch(swUrl, { 116 | headers: { 'Service-Worker': 'script' }, 117 | }) 118 | .then((response) => { 119 | // Ensure service worker exists, and that we really are getting a JS file. 120 | const contentType = response.headers.get('content-type'); 121 | if ( 122 | response.status === 404 || 123 | (contentType != null && contentType.indexOf('javascript') === -1) 124 | ) { 125 | // No service worker found. Probably a different app. Reload the page. 126 | navigator.serviceWorker.ready.then((registration) => { 127 | registration.unregister().then(() => { 128 | window.location.reload(); 129 | }); 130 | }); 131 | } else { 132 | // Service worker found. Proceed as normal. 133 | registerValidSW(swUrl, config); 134 | } 135 | }) 136 | .catch(() => { 137 | // eslint-disable-next-line no-console 138 | console.log( 139 | 'No internet connection found. App is running in offline mode.' 140 | ); 141 | }); 142 | } 143 | 144 | export function unregister() { 145 | if ('serviceWorker' in navigator) { 146 | navigator.serviceWorker.ready 147 | .then((registration) => { 148 | registration.unregister(); 149 | }) 150 | .catch((error) => { 151 | // eslint-disable-next-line no-console 152 | console.error(error.message); 153 | }); 154 | } 155 | } 156 | --------------------------------------------------------------------------------
14 | {result.split('\n').map((item: any, key: any) => 15 | {item} 16 | )} 17 |
Execute PHP script:
/usr/bin/php 21 | /var/www/app/cron.php
Execute Artisan command:
/usr/bin/php 24 | /var/www/laravel/artisan schedule:run
Access URL:
/usr/bin/wget 27 | --spider "http://site.com/cron.php" 28 |
/usr/bin/wget 27 | --spider "http://site.com/cron.php"