├── .babelrc.js ├── .eslintignore ├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ ├── phpcs.yml │ └── phpunit.yml ├── .gitignore ├── .php_cs ├── .wp-env.json ├── LICENSE.txt ├── README.md ├── Rest-API-Docs.MD ├── assets ├── images │ ├── wp-react-kit-logo-full.png │ └── wp-react-kit-logo.png └── js │ ├── version-replace.js │ └── zip.js ├── changelog.txt ├── composer.json ├── composer.lock ├── includes ├── Abstracts │ ├── BaseModel.php │ ├── DBMigrator.php │ ├── DBSeeder.php │ └── RESTController.php ├── Admin │ └── Menu.php ├── Assets │ └── Manager.php ├── Blocks │ └── Manager.php ├── Common │ └── Keys.php ├── Databases │ ├── Migrations │ │ ├── JobTypeMigration.php │ │ └── JobsMigration.php │ └── Seeder │ │ ├── JobTypeSeeder.php │ │ ├── JobsSeeder.php │ │ └── Manager.php ├── Jobs │ ├── Job.php │ ├── JobStatus.php │ ├── JobType.php │ └── Manager.php ├── REST │ ├── Api.php │ ├── CompaniesController.php │ ├── JobTypesController.php │ └── JobsController.php ├── Setup │ └── Installer.php ├── Traits │ ├── InputSanitizer.php │ └── Queryable.php └── User │ └── Hooks.php ├── jest-unit.config.js ├── job-place.php ├── languages └── jobplace.pot ├── package-lock.json ├── package.json ├── phpcs.xml ├── phpunit.xml.dist ├── postcss.config.js ├── src ├── App.tsx ├── blocks │ └── header │ │ ├── block.json │ │ ├── edit.tsx │ │ ├── editor.scss │ │ ├── index.ts │ │ ├── save.tsx │ │ └── style.scss ├── components │ ├── badge │ │ ├── Badge.stories.tsx │ │ ├── Badge.tsx │ │ └── __tests__ │ │ │ └── Badge.test.tsx │ ├── button │ │ ├── Button.stories.tsx │ │ └── Button.tsx │ ├── dashboard │ │ └── Dashboard.tsx │ ├── date-picker │ │ ├── DatePicker.stories.tsx │ │ ├── DatePicker.tsx │ │ └── DatePickerData.ts │ ├── inputs │ │ ├── Input.stories.tsx │ │ ├── Input.tsx │ │ ├── InputLabel.tsx │ │ ├── Select2Input.stories.tsx │ │ ├── Select2Input.tsx │ │ ├── SwitchCheckbox.tsx │ │ ├── SwitchInput.stories.tsx │ │ └── TextEditor.tsx │ ├── jobs │ │ ├── JobCard.tsx │ │ ├── JobForm.tsx │ │ ├── JobFormSidebar.tsx │ │ ├── JobSubmit.tsx │ │ ├── ListItemMenu.tsx │ │ ├── SelectCheckBox.tsx │ │ └── use-table-data.tsx │ ├── layout │ │ ├── Header.tsx │ │ ├── Layout.tsx │ │ ├── NavMenu.tsx │ │ └── PageHeading.tsx │ ├── loading │ │ ├── BarChartLoading.stories.tsx │ │ ├── BarChartLoading.tsx │ │ ├── DashboardCardLoading.stories.tsx │ │ ├── DashboardCardLoading.tsx │ │ ├── LineChartLoading.stories.tsx │ │ ├── LineChartLoading.tsx │ │ ├── Loading.stories.tsx │ │ ├── Loading.tsx │ │ ├── OverlayLoading.tsx │ │ ├── SettingsLoading.stories.tsx │ │ ├── SettingsLoading.tsx │ │ ├── SettingsSectionLoading.tsx │ │ ├── TableLoading.stories.tsx │ │ └── TableLoading.tsx │ ├── modal │ │ ├── Modal.stories.tsx │ │ └── Modal.tsx │ ├── page-partials │ │ ├── SelectListItem.tsx │ │ └── SelectedItem.tsx │ ├── pagination │ │ ├── Pagination.stories.tsx │ │ └── Pagination.tsx │ ├── spinner │ │ ├── Spinner.stories.tsx │ │ └── Spinner.tsx │ ├── svg │ │ ├── LogoIcon.tsx │ │ ├── SvgCircleDefaultIcon.tsx │ │ ├── SvgCirclePrimaryIcon.tsx │ │ ├── SvgCircleSuccessIcon.tsx │ │ └── SvgCircleWarningIcon.tsx │ ├── tab │ │ ├── Tab.stories.tsx │ │ └── Tab.tsx │ ├── table │ │ ├── Table.stories.tsx │ │ ├── Table.tsx │ │ └── TableInterface.ts │ └── tooltip │ │ ├── ProNotExistTooltip.tsx │ │ ├── Tooltip.tsx │ │ └── tooltip-style.scss ├── data │ ├── jobs │ │ ├── actions.ts │ │ ├── controls.ts │ │ ├── default-state.ts │ │ ├── endpoint.ts │ │ ├── index.ts │ │ ├── reducer.ts │ │ ├── resolvers.ts │ │ ├── selectors.ts │ │ ├── types.ts │ │ └── utils.ts │ └── store.ts ├── hooks │ ├── use-window-width.tsx │ ├── useConfirmReload.tsx │ ├── useMenuFix.tsx │ └── useOutsideClick.tsx ├── index.tsx ├── interfaces │ ├── index.ts │ └── jobs.ts ├── pages │ ├── HomePage.tsx │ └── jobs │ │ ├── CreateJob.tsx │ │ ├── EditJob.tsx │ │ └── JobsPage.tsx ├── routes │ └── index.ts ├── style │ ├── main.scss │ └── tailwind.css └── utils │ ├── DateHelper.ts │ ├── MenuFix.ts │ ├── NumberFormat.ts │ ├── Select2Helper.ts │ ├── StringHelper.ts │ ├── global-data.ts │ ├── http.ts │ └── text-parser.ts ├── tailwind.config.js ├── templates ├── app.php └── blocks │ └── header │ └── markup.php ├── tests ├── e2e │ ├── global.setup.ts │ ├── gutenberg-test-plugin-disables-the-css-animations │ │ └── gutenberg-test-plugin-disables-the-css-animations.php │ ├── playwright.config.ts │ ├── specs │ │ ├── blocks │ │ │ └── header.spec.ts │ │ ├── env.spec.ts │ │ └── example.spec.ts │ └── unit │ │ └── example.test.ts ├── phpunit │ ├── Api │ │ ├── CompanyRestApiTest.php │ │ └── JobRestApiTest.php │ ├── Install │ │ └── RunnerTest.php │ ├── Jobs │ │ └── JobManagerTest.php │ ├── bootstrap.php │ └── wp-config.php └── unit │ └── config │ └── testing-library.js ├── tsconfig.json └── webpack.config.js /.babelrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | [ 4 | '@wordpress/babel-plugin-import-jsx-pragma', 5 | { 6 | scopeVariable: 'createElement', 7 | scopeVariableFrag: 'Fragment', 8 | source: '@wordpress/element', 9 | isDefault: false, 10 | }, 11 | ], 12 | [ 13 | '@babel/plugin-transform-react-jsx', 14 | { 15 | pragma: 'createElement', 16 | pragmaFrag: 'Fragment', 17 | }, 18 | ], 19 | ], 20 | presets: [ 21 | ['@babel/preset-env', { targets: { node: 'current' } }], 22 | ['@babel/preset-typescript'], 23 | ], 24 | }; -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | **/*.min.js 2 | **/node_modules/** 3 | **/vendor/** 4 | **/build/** 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:@wordpress/eslint-plugin/recommended" 4 | ], 5 | "rules": { 6 | "prettier/prettier": [ 7 | "error", 8 | { 9 | "endOfLine": "auto", 10 | "parenSpacing": true, 11 | "tabWidth": 4, 12 | "useTabs": false, 13 | "singleQuote": true, 14 | "trailingComma": "es5", 15 | "bracketSpacing": true, 16 | "jsxBracketSameLine": false, 17 | "semi": true, 18 | "arrowParens": "always" 19 | } 20 | ], 21 | "@wordpress/i18n-text-domain": [ 22 | "error", 23 | { 24 | "allowedTextDomain": [ 25 | "jobplace" 26 | ] 27 | } 28 | ] 29 | } 30 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: maniruzzaman 4 | -------------------------------------------------------------------------------- /.github/workflows/phpcs.yml: -------------------------------------------------------------------------------- 1 | name: PHPCS check 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - trunk 7 | - develop 8 | - main 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | phpcs: 16 | name: PHPCS check 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Checkout code 20 | uses: actions/checkout@v3 21 | 22 | - name: Setup PHP 23 | uses: "shivammathur/setup-php@v2" 24 | with: 25 | php-version: "7.4" 26 | ini-values: "memory_limit=1G" 27 | coverage: none 28 | tools: cs2pr 29 | 30 | - name: Install Composer dependencies 31 | uses: "ramsey/composer-install@v2" 32 | 33 | - name: Run PHPCS checks 34 | continue-on-error: true 35 | run: composer run phpcs --report-checkstyle=./phpcs-report.xml 36 | 37 | - name: Show PHPCS results in PR 38 | run: cs2pr ./phpcs-report.xml -------------------------------------------------------------------------------- /.github/workflows/phpunit.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - trunk 8 | - develop 9 | - main 10 | 11 | # Cancels all previous workflow runs for pull requests that have not completed. 12 | concurrency: 13 | # The concurrency group contains the workflow name and the branch name for pull requests 14 | # or the commit hash for any other events. 15 | group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | test: 20 | name: phpunit tests 21 | runs-on: ubuntu-latest 22 | strategy: 23 | fail-fast: false 24 | 25 | steps: 26 | - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 27 | 28 | - name: Use desired version of php (7.4 with xdebug) 29 | uses: shivammathur/setup-php@v2 30 | with: 31 | php-version: '7.4' 32 | coverage: xdebug 33 | 34 | - name: Composer install and build 35 | run: | 36 | composer install 37 | composer update 38 | composer dump-autoload -o 39 | 40 | - name: Running the phpunit tests 41 | continue-on-error: true 42 | run: composer run test 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /vendor 3 | /build 4 | .phpunit.result.cache 5 | .DS_Store 6 | /dist 7 | 8 | # Playwright 9 | /test-results/ 10 | /playwright-report/ 11 | /playwright/.cache/ 12 | /artifacts/ -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude( 'node_modules' ) 7 | ->exclude( 'vendors' ) 8 | ->exclude( 'assets' ) 9 | ->exclude( 'languages' ) 10 | ->exclude( 'src' ) 11 | ->exclude( 'bin' ) 12 | ->in( __DIR__ ); 13 | 14 | $config = PhpCsFixer\Config::create() 15 | ->registerCustomFixers( [ 16 | new WeDevs\Fixer\SpaceInsideParenthesisFixer(), 17 | new WeDevs\Fixer\BlankLineAfterClassOpeningFixer(), 18 | ] ) 19 | ->setRiskyAllowed( true ) 20 | ->setUsingCache( false ) 21 | ->setRules( WeDevs\Fixer\Fixer::rules() ) 22 | ->setFinder( $finder ); 23 | 24 | return $config; 25 | -------------------------------------------------------------------------------- /.wp-env.json: -------------------------------------------------------------------------------- 1 | { 2 | "phpVersion": "7.4", 3 | "plugins": [ 4 | "." 5 | ], 6 | "config": { 7 | "WP_DEBUG_LOG": true, 8 | "WP_DEBUG_DISPLAY": true 9 | }, 10 | "env": { 11 | "development": {}, 12 | "tests": { 13 | "port": 8889, 14 | "plugins": [ 15 | ".", 16 | "./tests/e2e/gutenberg-test-plugin-disables-the-css-animations" 17 | ], 18 | "config": { 19 | "WP_TESTS_DOMAIN": "localhost" 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Maniruzzaman Akash 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/images/wp-react-kit-logo-full.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManiruzzamanAkash/wp-react-kit/17ee15156914638201cd70c6c6c7af2b2297805b/assets/images/wp-react-kit-logo-full.png -------------------------------------------------------------------------------- /assets/images/wp-react-kit-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManiruzzamanAkash/wp-react-kit/17ee15156914638201cd70c6c6c7af2b2297805b/assets/images/wp-react-kit-logo.png -------------------------------------------------------------------------------- /assets/js/version-replace.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const replace = require('replace-in-file'); 3 | 4 | const pluginFiles = ['includes/**/*', 'templates/*', 'src/*', 'job-place.php']; 5 | 6 | const { version } = JSON.parse(fs.readFileSync('package.json')); 7 | 8 | replace({ 9 | files: pluginFiles, 10 | from: /JOBPLACE_SINCE/g, 11 | to: version, 12 | }); 13 | -------------------------------------------------------------------------------- /assets/js/zip.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const { exec } = require('child_process'); 4 | const util = require('util'); 5 | const chalk = require('chalk'); 6 | const _ = require('lodash'); 7 | 8 | const asyncExec = util.promisify(exec); 9 | 10 | const pluginFiles = [ 11 | 'assets/', 12 | 'build/', 13 | 'includes/', 14 | 'languages/', 15 | 'templates/', 16 | 'changelog.txt', 17 | 'job-place.php', 18 | ]; 19 | 20 | const removeFiles = [ 21 | 'assets/js/version-replace.js', 22 | 'assets/js/zip.js', 23 | 'composer.json', 24 | 'composer.lock', 25 | ]; 26 | 27 | const dest = `dist/`; 28 | const zipFile = `wp-react-kit.zip`; 29 | const allowedVendorFiles = {}; 30 | fs.removeSync(`${dest}${zipFile}`); 31 | 32 | exec( 33 | 'rm -rf versions && rm *.zip', 34 | { 35 | cwd: 'dist', 36 | }, 37 | () => { 38 | const composerFile = `composer.json`; 39 | fs.removeSync(dest); 40 | const fileList = [...pluginFiles]; 41 | fs.mkdirp(dest); 42 | 43 | fileList.forEach((file) => { 44 | fs.copySync(file, `${dest}/${file}`); 45 | }); 46 | 47 | // copy composer.json file 48 | try { 49 | if (fs.pathExistsSync(composerFile)) { 50 | fs.copySync(composerFile, `${dest}/composer.json`); 51 | } else { 52 | fs.copySync(`composer.json`, `${dest}/composer.json`); 53 | } 54 | } catch (err) { 55 | console.error(err); 56 | } 57 | 58 | console.log(`Finished copying files.`); 59 | 60 | asyncExec( 61 | 'composer install --optimize-autoloader --no-dev', 62 | { 63 | cwd: dest, 64 | }, 65 | () => { 66 | console.log( 67 | `Installed composer packages in ${dest} directory.` 68 | ); 69 | 70 | removeFiles.forEach((file) => { 71 | fs.removeSync(`${dest}/${file}`); 72 | }); 73 | 74 | // Put vendor files. 75 | Object.keys(allowedVendorFiles).forEach((composerPackage) => { 76 | const packagePath = path.resolve( 77 | `${dest}/vendor/${composerPackage}` 78 | ); 79 | 80 | if (!fs.existsSync(packagePath)) { 81 | return; 82 | } 83 | 84 | const list = fs.readdirSync(packagePath); 85 | const deletables = _.difference( 86 | list, 87 | allowedVendorFiles[composerPackage] 88 | ); 89 | 90 | deletables.forEach((deletable) => { 91 | fs.removeSync(path.resolve(packagePath, deletable)); 92 | }); 93 | }); 94 | 95 | console.log(`Making zip file ${zipFile}...`); 96 | 97 | asyncExec( 98 | `zip -r ${zipFile} ${dest} *`, 99 | { 100 | cwd: dest, 101 | }, 102 | () => { 103 | fileList.forEach((file) => { 104 | fs.removeSync(`${dest}/${file}`); 105 | }); 106 | fs.removeSync(`${dest}/vendor`); 107 | console.log( 108 | chalk.green( 109 | `${zipFile} is ready inside ${dest} folder.` 110 | ) 111 | ); 112 | } 113 | ).catch((error) => { 114 | console.log(chalk.red(`Could not make ${zipFile}.`)); 115 | console.log(error); 116 | }); 117 | } 118 | ).catch((error) => { 119 | console.log( 120 | chalk.red(`Could not install composer in ${dest} directory.`) 121 | ); 122 | console.log(error); 123 | }); 124 | } 125 | ); 126 | -------------------------------------------------------------------------------- /changelog.txt: -------------------------------------------------------------------------------- 1 | ### Changelogs 2 | **v0.9.0 - 20/12/2024** 3 | 4 | 1. **Fix:** Updated PHP version support > 8.0 and some more library support 5 | 1. **Fix:** When Editing a Job, last job is being edited 6 | 1. **Update:** Tested upto WordPress 6.7.1 7 | 8 | **v0.8.0 - 24/05/2023** 9 | 10 | 1. New feature: WordPress Playwright test-e2e-utils added. 11 | 1. New feature: Some Gutenberg blocks has support for Playwright test. 12 | 13 | **v0.7.0 - 01/01/2023** 14 | 15 | 1. Fix: Dynamic block renderer issue. 16 | 1. Fix: Asset registering multiple times issue. 17 | 18 | **v0.5.0 - 14/11/2022** 19 | 20 | 1. New Feature : Job Create. 21 | 2. New Feature : Job Update. 22 | 3. New Feature : Job Delete. 23 | 4. New Feature : Job Status change. 24 | 5. New API: Company dropdown list. 25 | 6. New: Updated logo icon and plugin name. 26 | 7. New Components: Input Text-Editor, Improved design. 27 | 8. Refactor: Refactored codebase and updated docs. 28 | 9. New: Job type seeder. 29 | 10. Chore: Zip file generator. 30 | 11. Chore: i18n localization generator. 31 | 32 | **v0.4.1 - 18/08/2022** 33 | 34 | 1. Added Jest Unit Test Setup. 35 | 2. Added some dummy Jest Unit Test. 36 | 3. Fix #11 - Version naming while installing. 37 | 38 | **v0.4.0 - 12/08/2022** 39 | 40 | 1. Added many re-usable general components. 41 | 1. Header Component refactored and re-designed. 42 | 1. WP-Data setup and made Job Store. 43 | 1. Job List Page frontend added. 44 | 45 | **v0.3.1 - 11/08/2022** 46 | 47 | 1. PHPUnit Test cases setup. 48 | 1. PHPUnit Test cases added for Job Manager and Job REST API's. 49 | 50 | **v0.3.0 - 02/08/2022** 51 | 52 | 1. Necessary traits to handle - sanitization, query. 53 | 1. Advanced setup for migration, seeder, REST API. 54 | 1. Jobs, Job Types REST API developed. -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "akash/wp-react-kit", 3 | "description": "A simple starter kit to work in WordPress plugin development using WordPress Rest API, WP-script and many more...", 4 | "type": "wordpress-plugin", 5 | "license": "GPL-2.0-or-later", 6 | "authors": [ 7 | { 8 | "name": "ManiruzzamanAkash", 9 | "email": "manirujjamanakash@gmail.com" 10 | } 11 | ], 12 | "config": { 13 | "allow-plugins": { 14 | "dealerdirect/phpcodesniffer-composer-installer": true 15 | } 16 | }, 17 | "require": { 18 | "php": ">=7.0.0" 19 | }, 20 | "require-dev": { 21 | "wp-coding-standards/wpcs": "^3.0", 22 | "dealerdirect/phpcodesniffer-composer-installer": "^0.7 || ^1.0", 23 | "tareq1988/wp-php-cs-fixer": "dev-master", 24 | "phpcompatibility/phpcompatibility-wp": "dev-master", 25 | "wp-phpunit/wp-phpunit": "^6.0", 26 | "yoast/phpunit-polyfills": "^1.0", 27 | "phpunit/phpunit": "^9.5" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Akash\\JobPlace\\": "includes/", 32 | "Akash\\JobPlace\\Tests\\": "tests/phpunit/" 33 | } 34 | }, 35 | "scripts": { 36 | "phpcs": [ 37 | "phpcs -p -s" 38 | ], 39 | "phpcbf": [ 40 | "phpcbf -p" 41 | ], 42 | "test": [ 43 | "vendor/bin/phpunit" 44 | ], 45 | "test:all": [ 46 | "phpcs -p -s & vendor/bin/phpunit" 47 | ] 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /includes/Abstracts/BaseModel.php: -------------------------------------------------------------------------------- 1 | db = $wpdb; 68 | $this->table = $wpdb->prefix . $this->table; 69 | } 70 | 71 | /** 72 | * Convert item dataset to array. 73 | * 74 | * @since 0.3.0 75 | * 76 | * @param object $item 77 | * 78 | * @return array 79 | */ 80 | abstract public static function to_array( object $item ): array; 81 | } 82 | -------------------------------------------------------------------------------- /includes/Abstracts/DBMigrator.php: -------------------------------------------------------------------------------- 1 | header( 'X-WP-Total', (int) $total_items ); 59 | 60 | $max_pages = ceil( $total_items / $per_page ); 61 | 62 | $response->header( 'X-WP-TotalPages', (int) $max_pages ); 63 | $base = add_query_arg( $request->get_query_params(), rest_url( sprintf( '/%s/%s', $this->namespace, $this->base ) ) ); 64 | 65 | if ( $page > 1 ) { 66 | $prev_page = $page - 1; 67 | if ( $prev_page > $max_pages ) { 68 | $prev_page = $max_pages; 69 | } 70 | $prev_link = add_query_arg( 'page', $prev_page, $base ); 71 | $response->link_header( 'prev', $prev_link ); 72 | } 73 | if ( $max_pages > $page ) { 74 | $next_page = $page + 1; 75 | $next_link = add_query_arg( 'page', $next_page, $base ); 76 | $response->link_header( 'next', $next_link ); 77 | } 78 | 79 | return $response; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /includes/Admin/Menu.php: -------------------------------------------------------------------------------- 1 | register_styles( $this->get_styles() ); 31 | $this->register_scripts( $this->get_scripts() ); 32 | } 33 | 34 | /** 35 | * Get all styles. 36 | * 37 | * @since 0.2.0 38 | * 39 | * @return array 40 | */ 41 | public function get_styles(): array { 42 | return [ 43 | 'job-place-css' => [ 44 | 'src' => JOB_PLACE_BUILD . '/index.css', 45 | 'version' => JOB_PLACE_VERSION, 46 | 'deps' => [], 47 | ], 48 | ]; 49 | } 50 | 51 | /** 52 | * Get all scripts. 53 | * 54 | * @since 0.2.0 55 | * 56 | * @return array 57 | */ 58 | public function get_scripts(): array { 59 | $dependency = require_once JOB_PLACE_DIR . '/build/index.asset.php'; 60 | 61 | return [ 62 | 'job-place-app' => [ 63 | 'src' => JOB_PLACE_BUILD . '/index.js', 64 | 'version' => $dependency['version'], 65 | 'deps' => $dependency['dependencies'], 66 | 'in_footer' => true, 67 | ], 68 | ]; 69 | } 70 | 71 | /** 72 | * Register styles. 73 | * 74 | * @since 0.2.0 75 | * 76 | * @return void 77 | */ 78 | public function register_styles( array $styles ) { 79 | foreach ( $styles as $handle => $style ) { 80 | wp_register_style( $handle, $style['src'], $style['deps'], $style['version'] ); 81 | } 82 | } 83 | 84 | /** 85 | * Register scripts. 86 | * 87 | * @since 0.2.0 88 | * 89 | * @return void 90 | */ 91 | public function register_scripts( array $scripts ) { 92 | foreach ( $scripts as $handle =>$script ) { 93 | wp_register_script( $handle, $script['src'], $script['deps'], $script['version'], $script['in_footer'] ); 94 | } 95 | } 96 | 97 | /** 98 | * Enqueue admin styles and scripts. 99 | * 100 | * @since 0.2.0 101 | * @since 0.3.0 Loads the JS and CSS only on the Job Place admin page. 102 | * 103 | * @return void 104 | */ 105 | public function enqueue_admin_assets() { 106 | // Check if we are on the admin page and page=jobplace. 107 | if ( ! is_admin() || ! isset( $_GET['page'] ) || sanitize_text_field( wp_unslash( $_GET['page'] ) ) !== 'jobplace' ) { 108 | return; 109 | } 110 | 111 | wp_enqueue_style( 'job-place-css' ); 112 | wp_enqueue_script( 'job-place-app' ); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /includes/Blocks/Manager.php: -------------------------------------------------------------------------------- 1 | register_block_metas(); 39 | 40 | if ( $is_pre_wp_6 ) { 41 | // Remove the filter after we register the blocks 42 | remove_filter( 'plugins_url', [ $this, 'filter_plugins_url' ], 10, 2 ); 43 | } 44 | } 45 | 46 | /** 47 | * Register all block by block jsons. 48 | * 49 | * @since 0.7.0 50 | * 51 | * @return void 52 | */ 53 | protected function register_block_metas(): void { 54 | $blocks = [ 55 | 'header/', 56 | ]; 57 | 58 | foreach ( $blocks as $block ) { 59 | $block_folder = JOB_PLACE_PATH . '/build/blocks/' . $block; 60 | $block_options = []; 61 | $markup_file_path = JOB_PLACE_TEMPLATE_PATH . '/blocks/' . $block . 'markup.php'; 62 | 63 | if ( file_exists( $markup_file_path ) ) { 64 | $block_options['render_callback'] = function( $attributes, $content, $block ) use ( $markup_file_path ) { 65 | $context = $block->context; 66 | ob_start(); 67 | include $markup_file_path; 68 | return ob_get_clean(); 69 | }; 70 | } 71 | 72 | register_block_type_from_metadata( $block_folder, $block_options ); 73 | } 74 | } 75 | 76 | /** 77 | * Filter the plugins_url to allow us to use assets from theme. 78 | * 79 | * @since 0.7.0 80 | * 81 | * @param string $url The plugins url 82 | * @param string $path The path to the asset. 83 | * 84 | * @return string The overridden url to the block asset. 85 | */ 86 | public function filter_plugins_url( string $url, string $path ): string { 87 | $file = preg_replace( '/\.\.\//', '', $path ); 88 | return trailingslashit( get_stylesheet_directory_uri() ) . $file; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /includes/Common/Keys.php: -------------------------------------------------------------------------------- 1 | get_charset_collate(); 23 | 24 | $schema_job_types = "CREATE TABLE IF NOT EXISTS `{$wpdb->jobplace_job_types}` ( 25 | `id` int(11) NOT NULL AUTO_INCREMENT, 26 | `name` varchar(255) NOT NULL, 27 | `slug` varchar(255) NOT NULL, 28 | `description` varchar(255) NOT NULL, 29 | `created_at` datetime NOT NULL, 30 | `updated_at` datetime NOT NULL, 31 | PRIMARY KEY (`id`), 32 | UNIQUE KEY `slug` (`slug`) 33 | ) $charset_collate;"; 34 | 35 | // Create the tables. 36 | dbDelta( $schema_job_types ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /includes/Databases/Migrations/JobsMigration.php: -------------------------------------------------------------------------------- 1 | get_charset_collate(); 23 | 24 | $schema_jobs = "CREATE TABLE IF NOT EXISTS `{$wpdb->jobplace_jobs}` ( 25 | `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, 26 | `title` varchar(255) NOT NULL, 27 | `slug` varchar(255) NOT NULL, 28 | `company_id` bigint(20) unsigned NOT NULL, 29 | `job_type_id` int(10) unsigned NOT NULL, 30 | `description` mediumtext NOT NULL, 31 | `is_active` tinyint(1) NOT NULL DEFAULT 1, 32 | `created_by` bigint(20) unsigned NOT NULL, 33 | `updated_by` bigint(20) unsigned NULL, 34 | `deleted_by` bigint(20) unsigned NULL, 35 | `created_at` datetime NOT NULL, 36 | `updated_at` datetime NOT NULL, 37 | `deleted_at` datetime NULL, 38 | PRIMARY KEY (`id`), 39 | KEY `company_id` (`company_id`), 40 | UNIQUE KEY `slug` (`slug`), 41 | KEY `is_active` (`is_active`), 42 | KEY `job_type_id` (`job_type_id`), 43 | KEY `created_by` (`created_by`), 44 | KEY `updated_by` (`updated_by`) 45 | ) $charset_collate"; 46 | 47 | // Create the tables. 48 | dbDelta( $schema_jobs ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /includes/Databases/Seeder/JobTypeSeeder.php: -------------------------------------------------------------------------------- 1 | 'Full time', 35 | 'slug' => 'full-time', 36 | 'description' => 'This is a full time job post.', 37 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 38 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 39 | ], 40 | [ 41 | 'name' => 'Part time', 42 | 'slug' => 'part-time', 43 | 'description' => 'This is a part time job post.', 44 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 45 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 46 | ], 47 | [ 48 | 'name' => 'Remote', 49 | 'slug' => 'remote', 50 | 'description' => 'This is a remote job post.', 51 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 52 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 53 | ], 54 | [ 55 | 'name' => 'Contractual', 56 | 'slug' => 'contractual', 57 | 'description' => 'This is a contractual job post.', 58 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 59 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 60 | ], 61 | ]; 62 | 63 | // Create each of the job_types. 64 | foreach ( $job_types as $job_type ) { 65 | $wpdb->insert( 66 | $wpdb->prefix . 'jobplace_job_types', 67 | $job_type 68 | ); 69 | } 70 | 71 | // Update that seeder already runs. 72 | update_option( Keys::JOB_TYPE_SEEDER_RAN, true ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /includes/Databases/Seeder/JobsSeeder.php: -------------------------------------------------------------------------------- 1 | 'First Job Post', 35 | 'slug' => 'first-job-post', 36 | 'description' => 'This is a simple job post.', 37 | 'is_active' => 1, 38 | 'company_id' => 1, 39 | 'job_type_id' => 1, 40 | 'created_by' => get_current_user_id(), 41 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 42 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 43 | ], 44 | ]; 45 | 46 | // Create each of the jobs. 47 | foreach ( $jobs as $job ) { 48 | $wpdb->insert( 49 | $wpdb->prefix . 'jobplace_jobs', 50 | $job 51 | ); 52 | } 53 | 54 | // Update that seeder already runs. 55 | update_option( Keys::JOB_SEEDER_RAN, true ); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /includes/Databases/Seeder/Manager.php: -------------------------------------------------------------------------------- 1 | run(); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /includes/Jobs/Job.php: -------------------------------------------------------------------------------- 1 | '', 32 | 'slug' => '', 33 | 'description' => '', 34 | 'company_id' => 0, 35 | 'is_active' => 1, 36 | 'job_type_id' => null, 37 | 'created_by' => get_current_user_id(), 38 | 'created_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 39 | 'updated_at' => current_datetime()->format( 'Y-m-d H:i:s' ), 40 | ]; 41 | 42 | $data = wp_parse_args( $data, $defaults ); 43 | 44 | // Sanitize template data 45 | return [ 46 | 'title' => $this->sanitize( $data['title'], 'text' ), 47 | 'slug' => $this->sanitize( $data['slug'], 'text' ), 48 | 'description' => $this->sanitize( $data['description'], 'block' ), 49 | 'company_id' => $this->sanitize( $data['company_id'], 'number' ), 50 | 'is_active' => $this->sanitize( $data['is_active'], 'switch' ), 51 | 'job_type_id' => $this->sanitize( $data['job_type_id'], 'number' ), 52 | 'created_by' => $this->sanitize( $data['created_by'], 'number' ), 53 | 'created_at' => $this->sanitize( $data['created_at'], 'text' ), 54 | 'updated_at' => $this->sanitize( $data['updated_at'], 'text' ), 55 | ]; 56 | } 57 | 58 | /** 59 | * Jobs item to a formatted array. 60 | * 61 | * @since 0.3.0 62 | * 63 | * @param object $job 64 | * 65 | * @return array 66 | */ 67 | public static function to_array( ?object $job ): array { 68 | $job_type = static::get_job_type( $job ); 69 | 70 | $data = [ 71 | 'id' => (int) $job->id, 72 | 'title' => $job->title, 73 | 'slug' => $job->slug, 74 | 'job_type' => $job_type, 75 | 'is_remote' => static::get_is_remote( $job_type ), 76 | 'status' => JobStatus::get_status_by_job( $job ), 77 | 'company' => static::get_job_company( $job ), 78 | 'description' => $job->description, 79 | 'created_at' => $job->created_at, 80 | 'updated_at' => $job->updated_at, 81 | ]; 82 | 83 | return $data; 84 | } 85 | 86 | /** 87 | * Get job type of a job. 88 | * 89 | * @since 0.3.0 90 | * 91 | * @param object $job 92 | * 93 | * @return object|null 94 | */ 95 | public static function get_job_type( ?object $job ): ?object { 96 | $job_type = new JobType(); 97 | 98 | $columns = 'id, name, slug'; 99 | return $job_type->get( (int) $job->job_type_id, $columns ); 100 | } 101 | 102 | /** 103 | * Get if job is a remote job or not. 104 | * 105 | * We'll fetch this from job_type_id. 106 | * If job type is for remote, then it's a remote job. 107 | * 108 | * @param object $job_type 109 | * @return boolean 110 | */ 111 | public static function get_is_remote( ?object $job_type ): bool { 112 | if ( empty( $job_type ) ) { 113 | return false; 114 | } 115 | 116 | return $job_type->slug === 'remote'; 117 | } 118 | 119 | /** 120 | * Get company of a job. 121 | * 122 | * @since 0.3.0 123 | * 124 | * @param object $job 125 | * 126 | * @return null | array 127 | */ 128 | public static function get_job_company( ?object $job ): ?array { 129 | if ( empty( $job->company_id ) ) { 130 | return null; 131 | } 132 | 133 | $user = get_user_by( 'id', $job->company_id ); 134 | 135 | if ( empty( $user ) ) { 136 | return null; 137 | } 138 | 139 | return [ 140 | 'id' => $job->company_id, 141 | 'name' => $user->display_name, 142 | 'avatar_url' => get_avatar_url( $user->ID ), 143 | ]; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /includes/Jobs/JobStatus.php: -------------------------------------------------------------------------------- 1 | deleted_at ) ) { 42 | return self::TRASHED; 43 | } 44 | 45 | if ( $job->is_active ) { 46 | return self::PUBLISHED; 47 | } 48 | 49 | return self::DRAFT; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /includes/Jobs/JobType.php: -------------------------------------------------------------------------------- 1 | (int) $job_type->id, 33 | 'name' => $job_type->name, 34 | 'slug' => $job_type->slug, 35 | 'description' => $job_type->description, 36 | 'created_at' => $job_type->created_at, 37 | 'updated_at' => $job_type->updated_at, 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /includes/REST/Api.php: -------------------------------------------------------------------------------- 1 | class_map = apply_filters( 32 | 'jobplace_rest_api_class_map', 33 | [ 34 | \Akash\JobPlace\REST\JobTypesController::class, 35 | \Akash\JobPlace\REST\JobsController::class, 36 | \Akash\JobPlace\REST\CompaniesController::class, 37 | ] 38 | ); 39 | 40 | // Init REST API routes. 41 | add_action( 'rest_api_init', array( $this, 'register_rest_routes' ), 10 ); 42 | } 43 | 44 | /** 45 | * Register REST API routes. 46 | * 47 | * @since 0.3.0 48 | * 49 | * @return void 50 | */ 51 | public function register_rest_routes(): void { 52 | foreach ( $this->class_map as $controller ) { 53 | $this->$controller = new $controller(); 54 | $this->$controller->register_routes(); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /includes/REST/CompaniesController.php: -------------------------------------------------------------------------------- 1 | namespace, '/' . $this->base . '/dropdown//', 40 | [ 41 | [ 42 | 'methods' => WP_REST_Server::READABLE, 43 | 'callback' => [ $this, 'get_items_dropdown' ], 44 | 'permission_callback' => [ $this, 'check_permission' ], 45 | ], 46 | ] 47 | ); 48 | } 49 | 50 | /** 51 | * Retrieves a collection of companies for dropdown. 52 | * 53 | * @since 0.5.0 54 | * 55 | * @param WP_REST_Request $request Full details about the request. 56 | * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. 57 | */ 58 | public function get_items_dropdown( $request ): ?WP_REST_Response { 59 | //phpcs:disable 60 | $query = new WP_User_Query( 61 | [ 62 | 'meta_key' => 'user_type', 63 | 'meta_value' => 'company', 64 | 'fields' => [ 65 | 'ID', 66 | 'user_login', 67 | 'user_email', 68 | 'display_name', 69 | ], 70 | ] 71 | ); 72 | //phpcs:enable 73 | 74 | $users = []; 75 | 76 | foreach ( $query->results as $user ) { 77 | $users[] = $this->prepare_dropdown_response_for_collection( $user, $request ); 78 | } 79 | 80 | return rest_ensure_response( $users ); 81 | } 82 | 83 | /** 84 | * Prepare dropdown response for collection. 85 | * 86 | * @since 0.5.0 87 | * 88 | * @param WP_User $item User object. 89 | * @param WP_REST_Request $request Request object. 90 | * 91 | * @return array 92 | */ 93 | public function prepare_dropdown_response_for_collection( $item, $request ) { 94 | $user = $item; 95 | $data = []; 96 | $data['id'] = $user->id; 97 | $data['name'] = $user->display_name; 98 | $data['email'] = $user->user_email; 99 | $data['username'] = $user->user_login; 100 | 101 | return $data; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /includes/Setup/Installer.php: -------------------------------------------------------------------------------- 1 | add_version(); 24 | 25 | // Register and create tables. 26 | $this->register_table_names(); 27 | $this->create_tables(); 28 | 29 | // Make this administrator user as company. 30 | $this->make_admin_as_company(); 31 | 32 | // Run the database seeders. 33 | $seeder = new \Akash\JobPlace\Databases\Seeder\Manager(); 34 | $seeder->run(); 35 | } 36 | 37 | /** 38 | * Make administrator user as company. 39 | * 40 | * @since 0.5.0 41 | * 42 | * @return void 43 | */ 44 | private function make_admin_as_company() { 45 | update_user_meta( get_current_user_id(), 'user_type', 'company' ); 46 | } 47 | 48 | /** 49 | * Register table names. 50 | * 51 | * @since 0.3.0 52 | * 53 | * @return void 54 | */ 55 | private function register_table_names(): void { 56 | global $wpdb; 57 | 58 | // Register the tables to wpdb global. 59 | $wpdb->jobplace_job_types = $wpdb->prefix . 'jobplace_job_types'; 60 | $wpdb->jobplace_jobs = $wpdb->prefix . 'jobplace_jobs'; 61 | } 62 | 63 | /** 64 | * Add time and version on DB. 65 | * 66 | * @since 0.3.0 67 | * @since 0.4.1 Fixed #11 - Version Naming. 68 | * 69 | * @return void 70 | */ 71 | public function add_version(): void { 72 | $installed = get_option( Keys::JOB_PLACE_INSTALLED ); 73 | 74 | if ( ! $installed ) { 75 | update_option( Keys::JOB_PLACE_INSTALLED, time() ); 76 | } 77 | 78 | update_option( Keys::JOB_PLACE_VERSION, JOB_PLACE_VERSION ); 79 | } 80 | 81 | /** 82 | * Create necessary database tables. 83 | * 84 | * @since JOB_PLACE_ 85 | * 86 | * @return void 87 | */ 88 | public function create_tables() { 89 | if ( ! function_exists( 'dbDelta' ) ) { 90 | require_once ABSPATH . 'wp-admin/includes/upgrade.php'; 91 | } 92 | 93 | // Run the database table migrations. 94 | \Akash\JobPlace\Databases\Migrations\JobTypeMigration::migrate(); 95 | \Akash\JobPlace\Databases\Migrations\JobsMigration::migrate(); 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /includes/Traits/InputSanitizer.php: -------------------------------------------------------------------------------- 1 | ID ) ) { 39 | $user_type = get_user_meta( $user->ID, 'user_type', true ); 40 | } else { 41 | $user_type = ''; 42 | } 43 | ?> 44 | 45 | 46 | 47 | 56 | 57 |
48 | 53 | 54 | 55 |
58 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | ./tests/phpunit/ 16 | ./tests/phpunit/wp-config.php 17 | 18 | 19 | 20 | 26 | 27 | inc 28 | 29 | 30 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('autoprefixer'), 4 | require('tailwindcss'), 5 | require('cssnano'), 6 | ], 7 | }; 8 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { HashRouter, Routes, Route } from 'react-router-dom'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Header from './components/layout/Header'; 10 | import routes from './routes'; 11 | 12 | const App = () => { 13 | return ( 14 | 15 |
16 |
17 |
18 | 19 | {routes.map((route, index) => ( 20 | } 24 | /> 25 | ))} 26 | 27 |
28 |
29 | ); 30 | }; 31 | 32 | export default App; 33 | -------------------------------------------------------------------------------- /src/blocks/header/block.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/block.json", 3 | "apiVersion": 2, 4 | "title": "ReactKit Header", 5 | "name": "wrc/header", 6 | "category": "formatting", 7 | "attributes": { 8 | "title": { 9 | "type": "string", 10 | "default": "WP React Kit Text Header" 11 | }, 12 | "description": { 13 | "type": "string", 14 | "default": "WP React Kit Description" 15 | }, 16 | "bgColor": { 17 | "type": "string", 18 | "default": "#f5f5f5" 19 | }, 20 | "padding": { 21 | "type": "object", 22 | "default": { 23 | "top": "10px", 24 | "left": "30px", 25 | "right": "30px", 26 | "bottom": "10px" 27 | } 28 | } 29 | }, 30 | "example": { 31 | "attributes": { 32 | "title": "WP React Kit Text Header", 33 | "description": "WP React Kit Description", 34 | "bgColor": "#f5f5f5", 35 | "padding": { 36 | "top": "10px", 37 | "left": "30px", 38 | "right": "30px", 39 | "bottom": "10px" 40 | } 41 | } 42 | }, 43 | "supports": { 44 | "html": false 45 | }, 46 | "editorScript": "file:./index.js", 47 | "style": "file:./style-index.css" 48 | } 49 | -------------------------------------------------------------------------------- /src/blocks/header/edit.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { __ } from "@wordpress/i18n"; 5 | import { InspectorControls, RichText, useBlockProps } from "@wordpress/block-editor"; 6 | import { PanelBody, ColorPicker, __experimentalBoxControl as BoxControl } from '@wordpress/components'; 7 | 8 | /** 9 | * Internal dependencies. 10 | */ 11 | import "./editor.scss"; 12 | 13 | /** 14 | * The edit function describes the structure of your block in the context of the 15 | * editor. This represents what the editor will render when the block is used. 16 | * 17 | * @see https://developer.wordpress.org/block-editor/developers/block-api/block-edit-save/#edit 18 | * @return {WPElement} Element to render. 19 | */ 20 | export default function Edit({ attributes, setAttributes }) { 21 | const { title, description, bgColor, padding } = attributes; 22 | 23 | return ( 24 |
31 | setAttributes({ title })} 37 | /> 38 | 39 | setAttributes({ description })} 45 | /> 46 | 47 | 48 | 52 | setAttributes({ bgColor })} 55 | enableAlpha 56 | defaultValue={bgColor} 57 | clearable={false} 58 | /> 59 | 60 | 64 | setAttributes({ padding })} 68 | /> 69 | 70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /src/blocks/header/editor.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManiruzzamanAkash/wp-react-kit/17ee15156914638201cd70c6c6c7af2b2297805b/src/blocks/header/editor.scss -------------------------------------------------------------------------------- /src/blocks/header/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Registers a new block provided a unique name and an object defining its behavior. 3 | * 4 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ 5 | */ 6 | import { registerBlockType } from '@wordpress/blocks'; 7 | 8 | /** 9 | * Lets webpack process CSS, SASS or SCSS files referenced in JavaScript files. 10 | * All files containing `style` keyword are bundled together. The code used 11 | * gets applied both to the front of your site and to the editor. 12 | * 13 | * @see https://www.npmjs.com/package/@wordpress/scripts#using-css 14 | */ 15 | import './style.scss'; 16 | 17 | /** 18 | * Internal dependencies 19 | */ 20 | import edit from './edit'; 21 | import save from './save'; 22 | import json from './block.json'; 23 | 24 | const { name, ...settings } = json; 25 | 26 | /** 27 | * Every block starts by registering a new block type definition. 28 | * 29 | * @see https://developer.wordpress.org/block-editor/reference-guides/block-api/block-registration/ 30 | */ 31 | registerBlockType(name, { 32 | ...settings, 33 | 34 | /** 35 | * @see ./edit.js 36 | */ 37 | edit, 38 | 39 | /** 40 | * @see ./save.js 41 | */ 42 | save, 43 | }); 44 | -------------------------------------------------------------------------------- /src/blocks/header/save.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * The save function defines the way in which the different attributes should 3 | * be combined into the final markup, which is then serialized by the block 4 | * editor into `post_content`. 5 | * 6 | * @see https://developer.wordpress.org/block-editor/developers/block-api/block-edit-save/#save 7 | * @return {WPElement} Element to render. 8 | */ 9 | export default function save() { 10 | return null; 11 | } -------------------------------------------------------------------------------- /src/blocks/header/style.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ManiruzzamanAkash/wp-react-kit/17ee15156914638201cd70c6c6c7af2b2297805b/src/blocks/header/style.scss -------------------------------------------------------------------------------- /src/components/badge/Badge.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | 3 | import Badge from './Badge'; 4 | import { faPlusCircle } from '@fortawesome/free-solid-svg-icons'; 5 | 6 | export default { 7 | title: 'Common/Badge', 8 | component: Badge, 9 | } as ComponentMeta; 10 | 11 | const Template: ComponentStory = (args) => ; 12 | 13 | export const DefaultBadge = Template.bind({}); 14 | DefaultBadge.args = { 15 | text: 'Simple Badge', 16 | type: 'default', 17 | }; 18 | 19 | export const PrimaryBadge = Template.bind({}); 20 | PrimaryBadge.args = { 21 | text: 'Primary Badge', 22 | type: 'primary', 23 | }; 24 | 25 | export const WarningBadge = Template.bind({}); 26 | WarningBadge.args = { 27 | text: 'Warning Badge', 28 | type: 'warning', 29 | }; 30 | 31 | export const IconBadge = Template.bind({}); 32 | IconBadge.args = { 33 | text: 'Icon Badge', 34 | type: 'primary', 35 | icon: faPlusCircle, 36 | hasIcon: true, 37 | customClass: 'border border-solid p-2 border-gray', 38 | }; 39 | -------------------------------------------------------------------------------- /src/components/badge/Badge.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect, useState } from '@wordpress/element'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import SvgCircleDefaultIcon from '../svg/SvgCircleDefaultIcon'; 10 | import SvgCirclePrimaryIcon from '../svg/SvgCirclePrimaryIcon'; 11 | import SvgCircleSuccessIcon from '../svg/SvgCircleSuccessIcon'; 12 | import SvgCircleWarningIcon from '../svg/SvgCircleWarningIcon'; 13 | 14 | export interface BadgeProps { 15 | /** 16 | * Badge text. 17 | */ 18 | text: string | JSX.Element; 19 | 20 | /** 21 | * Badge type - primary, success, warning, default. 22 | */ 23 | type?: string; 24 | 25 | /** 26 | * Custom class for badge area. 27 | */ 28 | customClass?: string; 29 | 30 | /** 31 | * Will icon show or not. 32 | */ 33 | hasIcon?: boolean; 34 | 35 | /** 36 | * Icon if any icon shows. 37 | */ 38 | icon?: undefined | JSX.Element; 39 | } 40 | 41 | /** 42 | * Get Badge Default Props. 43 | */ 44 | export const BadgeDefaultProps = { 45 | text: '', 46 | type: 'default', 47 | customClass: '', 48 | hasIcon: false, 49 | icon: undefined, 50 | }; 51 | 52 | const Badge = (props: BadgeProps) => { 53 | const { text, type, customClass, hasIcon, icon } = props; 54 | 55 | const [svgIcon, setSvgIcon] = useState(<>); 56 | 57 | useEffect(() => { 58 | if (typeof icon !== 'undefined' && icon !== <>) { 59 | setSvgIcon(icon); 60 | } 61 | 62 | switch (type) { 63 | case 'success': 64 | setSvgIcon(SvgCircleSuccessIcon); 65 | break; 66 | 67 | case 'warning': 68 | setSvgIcon(SvgCircleWarningIcon); 69 | break; 70 | 71 | case 'primary': 72 | setSvgIcon(SvgCirclePrimaryIcon); 73 | break; 74 | 75 | case 'default': 76 | setSvgIcon(SvgCircleDefaultIcon); 77 | break; 78 | 79 | default: 80 | setSvgIcon(SvgCirclePrimaryIcon); 81 | break; 82 | } 83 | }, [type]); 84 | 85 | const getBadgeClassName = () => { 86 | let className = 87 | 'rounded-md ml-0 px-3 text-center py-2 w-auto min-w-[80px] whitespace-nowrap inline-block'; 88 | 89 | switch (type) { 90 | case 'success': 91 | className += ' bg-success-lite'; 92 | break; 93 | 94 | case 'warning': 95 | className += ' bg-warning-lite'; 96 | break; 97 | 98 | case 'default': 99 | className += ' bg-gray-liter'; 100 | break; 101 | 102 | default: 103 | className += ' bg-white'; 104 | break; 105 | } 106 | 107 | if (typeof customClass !== 'undefined' && customClass.length) { 108 | className += ` ${customClass}`; 109 | } 110 | 111 | return className; 112 | }; 113 | 114 | return ( 115 | 116 | {hasIcon && {svgIcon}} 117 | 118 | {text} 119 | 120 | ); 121 | }; 122 | 123 | Badge.defaultProps = BadgeDefaultProps; 124 | 125 | export default Badge; 126 | -------------------------------------------------------------------------------- /src/components/badge/__tests__/Badge.test.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render, screen } from '@testing-library/react'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import Badge from '../Badge'; 10 | import SvgCirclePrimaryIcon from '../../svg/SvgCirclePrimaryIcon'; 11 | 12 | const props = { 13 | text: 'Simple Badge', 14 | type: 'primary', 15 | }; 16 | 17 | const renderBadge = (customProps = {}) => { 18 | return render(); 19 | }; 20 | 21 | describe('Badge', () => { 22 | it('should render without crashing', () => { 23 | renderBadge(); 24 | const badgeText = screen.getByText(props.text); 25 | expect(badgeText).toBeInTheDocument(); 26 | }); 27 | 28 | // Check hasIcon prop and svgIcon is given or not. 29 | it('should render svg icon if hasIcon is true', () => { 30 | const { container } = renderBadge({ 31 | hasIcon: true, 32 | svgIcon: SvgCirclePrimaryIcon, 33 | }); 34 | 35 | const svgIcon = container.querySelector('svg'); 36 | expect(svgIcon).toBeInTheDocument(); 37 | }); 38 | 39 | // Check if customClass is given and the class is present or not. 40 | it('should render with custom class if it has been given', () => { 41 | const customClass = 'bg-red-500'; 42 | 43 | const { container } = renderBadge({ 44 | customClass, 45 | }); 46 | 47 | expect(container.firstChild).toHaveClass(customClass); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /src/components/button/Button.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | import { faPlus } from '@fortawesome/free-solid-svg-icons'; 3 | 4 | import Button from './Button'; 5 | 6 | // More on default export: https://storybook.js.org/docs/react/writing-stories/introduction#default-export 7 | export default { 8 | title: 'Common/Button', 9 | component: Button, 10 | // More on argTypes: https://storybook.js.org/docs/react/api/argtypes 11 | argTypes: { 12 | backgroundColor: { control: 'color' }, 13 | }, 14 | } as ComponentMeta; 15 | 16 | // More on component templates: https://storybook.js.org/docs/react/writing-stories/introduction#using-args 17 | const Template: ComponentStory = (args) => 48 | 49 | ); 50 | }; 51 | 52 | export default SelectListItem; 53 | -------------------------------------------------------------------------------- /src/components/page-partials/SelectedItem.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | interface ISelectedItem { 7 | checked: Array; 8 | } 9 | 10 | const SelectedItem: React.FC = ({ checked }) => { 11 | return ( 12 | 13 | {checked.length} 14 | 15 | {checked.length > 1 16 | ? __('items', 'jobplace') 17 | : __('item', 'jobplace')}{' '} 18 | {__('selected', 'jobplace')} 19 | 20 | 21 | ); 22 | }; 23 | 24 | export default SelectedItem; 25 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | import Pagination from './Pagination'; 3 | 4 | export default { 5 | title: 'Common/Pagination', 6 | component: Pagination 7 | } as ComponentMeta; 8 | 9 | const Template: ComponentStory = (args) => ; 10 | 11 | export const DefaultPagination = Template.bind({}); 12 | DefaultPagination.args = { 13 | perPage: 10, 14 | total: 1000, 15 | // paginate: () => { } 16 | } 17 | 18 | export const PaginationSmallItems = Template.bind({}); 19 | PaginationSmallItems.args = { 20 | perPage: 20, 21 | total: 6, 22 | // paginate: () => { } 23 | } 24 | -------------------------------------------------------------------------------- /src/components/pagination/Pagination.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useState } from '@wordpress/element'; 5 | import ReactPaginate from 'react-paginate'; 6 | 7 | interface IPagination { 8 | /** 9 | * How many items per page. 10 | */ 11 | perPage: number; 12 | 13 | /** 14 | * Total items. 15 | */ 16 | total: number; 17 | 18 | /** 19 | * Current page. 20 | */ 21 | currentPage: number; 22 | 23 | /** 24 | * Paginate handler. 25 | */ 26 | paginate: Function; 27 | } 28 | 29 | const defaultProps = { 30 | perPage: 10, 31 | total: 0, 32 | currentPage: 1, 33 | paginate: () => {}, 34 | }; 35 | 36 | const Pagination = (props: IPagination) => { 37 | const { perPage, total, paginate, currentPage } = props; 38 | const [current, setCurrent] = useState(currentPage); 39 | const boxClassName = `relative inline-flex items-center border text-sm font-medium`; 40 | const activeClassName = `bg-white border-slate-300 text-primary hover:text-primary-dark hover:bg-blue-200`; 41 | const inactiveClassName = `bg-white border-slate-300 text-gray-500 hover:bg-blue-200 hover:text-primary-dark`; 42 | const totalPage = Math.ceil(total / perPage); 43 | 44 | return ( 45 |
46 |
47 |

48 | 49 | {total} items -{' '} 50 | 51 | 52 | Page {current} 53 | 54 | {totalPage > 0 && ( 55 | 56 | of{' '} 57 | {totalPage} 58 | 59 | )} 60 |

61 |
62 | 63 | {totalPage > 1 && ( 64 | 96 | )} 97 |
98 | ); 99 | }; 100 | 101 | Pagination.defaultProps = defaultProps; 102 | 103 | export default Pagination; 104 | -------------------------------------------------------------------------------- /src/components/spinner/Spinner.stories.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 2 | import Spinner from './Spinner'; 3 | 4 | export default { 5 | title: 'Common/Spinner', 6 | component: Spinner, 7 | } as ComponentMeta; 8 | 9 | const Template: ComponentStory = (args) => ( 10 | 11 | ); 12 | 13 | export const DefaultSpinner = Template.bind({}); 14 | DefaultSpinner.args = { 15 | align: 'left', 16 | }; 17 | -------------------------------------------------------------------------------- /src/components/spinner/Spinner.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | export interface ISpinner { 7 | /** 8 | * Spinner size 9 | */ 10 | size?: string; 11 | 12 | /** 13 | * Spinner text alignment. 14 | */ 15 | align?: string; 16 | 17 | /** 18 | * Spinner text. 19 | */ 20 | text?: string; 21 | } 22 | 23 | const SpinnerDefaultProps = { 24 | size: 'sm', 25 | align: 'center', 26 | text: __('Loading…', 'cp'), 27 | }; 28 | 29 | const Spinner = ({ align, text }: ISpinner) => { 30 | const textAlignClassName = 31 | typeof align !== 'undefined' 32 | ? `text-${align}` 33 | : `text-${SpinnerDefaultProps.align}`; 34 | const spinnerText = 35 | typeof text !== 'undefined' ? text : SpinnerDefaultProps.text; 36 | 37 | return ( 38 |
39 | 45 | 53 | 58 | 59 | {spinnerText} 60 |
61 | ); 62 | }; 63 | 64 | Spinner.defaultProps = SpinnerDefaultProps; 65 | 66 | export default Spinner; 67 | -------------------------------------------------------------------------------- /src/components/svg/LogoIcon.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { getGlobalData } from '../../utils/global-data'; 5 | 6 | export default function LogoIcon({ 7 | className = '', 8 | isFull = false, 9 | style = {}, 10 | }) { 11 | return ( 12 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /src/components/svg/SvgCircleDefaultIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function SvgCircleDefaultIcon() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/svg/SvgCirclePrimaryIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function SvgCirclePrimaryIcon() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/svg/SvgCircleSuccessIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function SvgCircleSuccessIcon() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/svg/SvgCircleWarningIcon.tsx: -------------------------------------------------------------------------------- 1 | export default function SvgCircleWarningIcon() { 2 | return ( 3 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /src/components/tab/Tab.stories.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from '@wordpress/element'; 2 | import { ComponentStory, ComponentMeta } from '@storybook/react'; 3 | import Tab from './Tab'; 4 | 5 | export default { 6 | title: 'Common/Tab', 7 | component: Tab, 8 | } as ComponentMeta; 9 | 10 | const Template: ComponentStory = (args) => ; 11 | 12 | const groupTabs = [ 13 | { 14 | title: 'All Carts', 15 | key: 'all', 16 | }, 17 | { 18 | title: 'Abandoned Carts', 19 | key: 'abandoned', 20 | }, 21 | { 22 | title: 'Recovered Carts', 23 | key: 'recovered', 24 | }, 25 | ]; 26 | 27 | export const ControlledTab = () => { 28 | const [activeTab, setActiveTab] = useState(groupTabs[0]); 29 | return ( 30 | 35 | ); 36 | }; 37 | 38 | export const DefaultTab = Template.bind({}); 39 | 40 | DefaultTab.args = { 41 | groups: groupTabs, 42 | }; 43 | -------------------------------------------------------------------------------- /src/components/tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useState, useEffect } from '@wordpress/element'; 5 | 6 | interface ITabItem { 7 | /** 8 | * Tab Item Title. 9 | */ 10 | title: string; 11 | 12 | /** 13 | * Tab Item slug or key. 14 | */ 15 | key: string; 16 | } 17 | 18 | interface ITab { 19 | /** 20 | * Table items. 21 | */ 22 | groups: Array; 23 | 24 | /** 25 | * On click tab group item. 26 | */ 27 | onClickGroup?: (group: ITabItem) => void; 28 | 29 | /** 30 | * Active tab item group. 31 | */ 32 | activeTab?: ITabItem | string; 33 | 34 | /** 35 | * Set active tab group. 36 | */ 37 | setActiveTab?: (group: ITabItem) => void; 38 | 39 | /** 40 | * Custom tab wrapper class if any. 41 | */ 42 | customTabWrapperClass?: string; 43 | } 44 | 45 | export const defaultTabProps = { 46 | groups: [], 47 | onClickGroup: () => {}, 48 | activeTab: '', 49 | setActiveTab: () => {}, 50 | customTabWrapperClass: '', 51 | }; 52 | 53 | const Tab = (props: ITab) => { 54 | const { 55 | groups, 56 | onClickGroup, 57 | activeTab, 58 | setActiveTab, 59 | customTabWrapperClass, 60 | } = props; 61 | const [groupClassName, setGroupClassName] = useState(''); 62 | const [customTabWrapperClassName, setCustomTabWrapperClassName] = 63 | useState(''); 64 | 65 | useEffect(() => { 66 | setGroupClassName( 67 | 'flex-none text-base pb-2 w-auto pl-5 pr-5 cursor-pointer' 68 | ); 69 | 70 | setCustomTabWrapperClassName( 71 | typeof customTabWrapperClass !== 'undefined' 72 | ? customTabWrapperClass 73 | : '' 74 | ); 75 | }, [groups]); 76 | 77 | const onSelectTab = (group: ITabItem) => { 78 | if ( 79 | typeof setActiveTab === 'function' && 80 | typeof onClickGroup === 'function' 81 | ) { 82 | setActiveTab(group); 83 | onClickGroup(group); 84 | } 85 | }; 86 | 87 | return ( 88 |
91 |
92 | {groups.map((group, index: number) => ( 93 |
onSelectTab(group)} 101 | > 102 | {group.title} 103 |
104 | ))} 105 |
106 |
107 | ); 108 | }; 109 | 110 | Tab.defaultProps = defaultTabProps; 111 | 112 | export default Tab; 113 | -------------------------------------------------------------------------------- /src/components/table/TableInterface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Table Header Definition. 3 | */ 4 | export interface ITableHeader { 5 | key: string; 6 | title: string; 7 | className?: string; 8 | } 9 | 10 | /** 11 | * Table Cell Definition. 12 | */ 13 | export interface ITableCell { 14 | key: string; 15 | value: string | number | JSX.Element; 16 | className?: string; 17 | } 18 | 19 | /** 20 | * Table Row Definition. 21 | */ 22 | export interface ITableRow { 23 | id: number; 24 | cells: Array; 25 | } 26 | 27 | /** 28 | * Table Header Prop Definition. 29 | */ 30 | export interface ITableLoading { 31 | headers: Array; 32 | count?: number; 33 | showPagination?: boolean; 34 | hasCheckbox?: boolean; 35 | responsiveColumns?: Array; 36 | } 37 | 38 | /** 39 | * Table Prop Definition. 40 | */ 41 | export interface ITable { 42 | headers: Array; 43 | rows: Array; 44 | showPagination?: boolean; 45 | totalItems?: number; 46 | perPage?: number; 47 | currentPage?: number; 48 | checkedAll?: boolean; 49 | onChangePage?: Function; 50 | onCheckAll?: Function; 51 | noDataMessage?: string; 52 | responsiveColumns?: Array; 53 | } 54 | -------------------------------------------------------------------------------- /src/components/tooltip/ProNotExistTooltip.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { __ } from '@wordpress/i18n'; 5 | 6 | /** 7 | * Internal dependencies 8 | */ 9 | import Tooltip from './Tooltip'; 10 | import Button from '../button/Button'; 11 | import { hasPro } from '../../../utils/global-data'; 12 | 13 | export interface IProNotExistTooltip { 14 | /** 15 | * Pro not exist extra description. 16 | */ 17 | desc?: string; 18 | } 19 | 20 | const ProNotExistTooltip = ({ desc }: IProNotExistTooltip) => { 21 | return ( 22 | 23 | {!hasPro && ( 24 | <> 25 | 36 | } 37 | > 38 | {desc} 39 | {__( 40 | 'This feature is only available in Pro version.', 41 | 'cp' 42 | )} 43 | 44 | 45 | )} 46 | 47 | ); 48 | }; 49 | 50 | ProNotExistTooltip.defaultProps = { 51 | desc: __("Sorry, You can't view this.", 'cp'), 52 | }; 53 | 54 | export default ProNotExistTooltip; 55 | -------------------------------------------------------------------------------- /src/components/tooltip/Tooltip.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { faInfo } from '@fortawesome/free-solid-svg-icons'; 5 | import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import './tooltip-style.scss'; 11 | 12 | interface ITooltip { 13 | /** 14 | * Tooltip child content. 15 | */ 16 | innerContent?: string | JSX.Element; 17 | 18 | /** 19 | * Custom class name for content-area. 20 | */ 21 | className?: string; 22 | } 23 | 24 | const defaultTooltipProps = { 25 | innerContent: '', 26 | className: '', 27 | }; 28 | 29 | const Tooltip = (props: ITooltip) => { 30 | const { children, innerContent, className } = props; 31 | 32 | return ( 33 |
34 | {innerContent === '' || innerContent === <> ? ( 35 | 36 | 41 | 42 | ) : ( 43 | innerContent 44 | )} 45 | 46 | 50 |
51 | ); 52 | }; 53 | 54 | Tooltip.defaultProps = defaultTooltipProps; 55 | 56 | export default Tooltip; 57 | -------------------------------------------------------------------------------- /src/components/tooltip/tooltip-style.scss: -------------------------------------------------------------------------------- 1 | .cp-tooltip { 2 | &:hover { 3 | .cp-tooltip-text { 4 | visibility: visible !important; 5 | 6 | a { 7 | color: #4F46E5; 8 | } 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/data/jobs/controls.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import apiFetch from '@wordpress/api-fetch'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import { jobsEndpoint } from './endpoint'; 10 | 11 | const controls = { 12 | FETCH_FROM_API(action) { 13 | return apiFetch({ path: action.path }); 14 | }, 15 | 16 | FETCH_FROM_API_UNPARSED(action: { path: any }) { 17 | return apiFetch({ path: action.path, parse: false }).then( 18 | (response: { headers: object; json: any }) => 19 | Promise.all([response.headers, response.json()]).then( 20 | ([headers, data]) => ({ headers, data }) 21 | ) 22 | ); 23 | }, 24 | 25 | CREATE_JOBS(action) { 26 | return apiFetch({ 27 | path: jobsEndpoint, 28 | method: 'POST', 29 | data: action.payload, 30 | }); 31 | }, 32 | 33 | UPDATE_JOBS(action) { 34 | const path = jobsEndpoint + '/' + action.payload.id; 35 | return apiFetch({ path, method: 'PUT', data: action.payload }); 36 | }, 37 | 38 | DELETE_JOBS(action) { 39 | const path = jobsEndpoint; 40 | return apiFetch({ path, method: 'DELETE', data: action.payload }); 41 | }, 42 | }; 43 | 44 | export default controls; 45 | -------------------------------------------------------------------------------- /src/data/jobs/default-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { IJobs } from '../../interfaces'; 5 | 6 | export const jobDefaultFormData = { 7 | id: 0, 8 | title: '', 9 | description: '', 10 | job_type_id: 0, 11 | company_id: 0, 12 | is_active: 1, 13 | }; 14 | 15 | export const jobDefaultState: IJobs = { 16 | jobs: [], 17 | job: { 18 | ...jobDefaultFormData, 19 | }, 20 | jobTypes: [], 21 | loadingJobs: false, 22 | jobsSaving: false, 23 | jobsDeleting: false, 24 | totalPage: 0, 25 | total: 0, 26 | filters: {}, 27 | form: { 28 | ...jobDefaultFormData, 29 | }, 30 | companyDropdowns: [], 31 | }; 32 | -------------------------------------------------------------------------------- /src/data/jobs/endpoint.ts: -------------------------------------------------------------------------------- 1 | export const restBase = '/job-place/v1/'; 2 | export const jobsEndpoint = `${restBase}jobs`; 3 | export const jobTypesEndpoint = `${restBase}job-types`; 4 | export const companiesDropdownEndpoint = `${restBase}companies/dropdown`; 5 | -------------------------------------------------------------------------------- /src/data/jobs/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { createReduxStore } from '@wordpress/data'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import reducer from './reducer'; 10 | import actions from './actions'; 11 | import selectors from './selectors'; 12 | import controls from './controls'; 13 | import resolvers from './resolvers'; 14 | 15 | const jobStore = createReduxStore('wp-react/jobs', { 16 | reducer, 17 | actions, 18 | selectors, 19 | controls, 20 | resolvers, 21 | }); 22 | 23 | export default jobStore; 24 | -------------------------------------------------------------------------------- /src/data/jobs/reducer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import * as Types from './types'; 5 | import { jobDefaultState } from './default-state'; 6 | 7 | const reducer = (state = jobDefaultState, action: any) => { 8 | switch (action.type) { 9 | case Types.GET_JOBS: 10 | return { 11 | ...state, 12 | jobs: action.jobs, 13 | }; 14 | 15 | case Types.GET_JOB_DETAIL: 16 | return { 17 | ...state, 18 | job: action.job, 19 | }; 20 | 21 | case Types.GET_JOB_TYPES: 22 | return { 23 | ...state, 24 | jobTypes: action.jobTypes, 25 | }; 26 | 27 | case Types.GET_COMPANIES_DROPDOWN: 28 | return { 29 | ...state, 30 | companyDropdowns: action.companyDropdowns, 31 | }; 32 | 33 | case Types.SET_LOADING_JOBS: 34 | return { 35 | ...state, 36 | loadingJobs: action.loadingJobs, 37 | }; 38 | 39 | case Types.SET_TOTAL_JOBS: 40 | return { 41 | ...state, 42 | total: action.total, 43 | }; 44 | 45 | case Types.SET_TOTAL_JOBS_PAGE: 46 | return { 47 | ...state, 48 | totalPage: action.totalPage, 49 | }; 50 | 51 | case Types.SET_JOBS_FILTER: 52 | return { 53 | ...state, 54 | filters: action.filters, 55 | }; 56 | 57 | case Types.SET_JOB_FORM_DATA: 58 | return { 59 | ...state, 60 | form: action.form, 61 | }; 62 | 63 | case Types.SET_JOBS_SAVING: 64 | return { 65 | ...state, 66 | jobsSaving: action.jobsSaving, 67 | }; 68 | } 69 | 70 | return state; 71 | }; 72 | 73 | export default reducer; 74 | -------------------------------------------------------------------------------- /src/data/jobs/resolvers.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import actions from './actions'; 5 | import { 6 | companiesDropdownEndpoint, 7 | jobsEndpoint, 8 | jobTypesEndpoint, 9 | } from './endpoint'; 10 | import { 11 | ICompanyDropdown, 12 | IJobFilter, 13 | IJobTypes, 14 | IResponseGenerator, 15 | } from '../../interfaces'; 16 | import { formatSelect2Data } from '../../utils/Select2Helper'; 17 | import { prepareJobDataForDatabase } from './utils'; 18 | 19 | const resolvers = { 20 | *getJobs(filters: IJobFilter) { 21 | if (filters === undefined) { 22 | filters = {}; 23 | } 24 | 25 | const queryParam = new URLSearchParams( 26 | filters as URLSearchParams 27 | ).toString(); 28 | 29 | const response: IResponseGenerator = yield actions.fetchFromAPIUnparsed( 30 | `${jobsEndpoint}?${queryParam}` 31 | ); 32 | let totalPage = 0; 33 | let totalCount = 0; 34 | 35 | if (response.headers !== undefined) { 36 | totalPage = response.headers.get('X-WP-TotalPages'); 37 | totalCount = response.headers.get('X-WP-Total'); 38 | } 39 | 40 | yield actions.setJobs(response.data); 41 | yield actions.setTotalPage(totalPage); 42 | yield actions.setTotal(totalCount); 43 | return actions.setLoadingJobs(false); 44 | }, 45 | 46 | *getJobDetail(id: number) { 47 | yield actions.setLoadingJobs(true); 48 | const path = `${jobsEndpoint}/${id}`; 49 | const response = yield actions.fetchFromAPI(path); 50 | 51 | if (response.id) { 52 | const data = prepareJobDataForDatabase(response); 53 | 54 | yield actions.setFormData(data); 55 | } 56 | 57 | return actions.setLoadingJobs(false); 58 | }, 59 | 60 | *getJobTypes() { 61 | const response: IResponseGenerator = yield actions.fetchFromAPIUnparsed( 62 | jobTypesEndpoint 63 | ); 64 | 65 | const jobTypes: Array = response.data; 66 | 67 | yield actions.setJobTypes(formatSelect2Data(jobTypes)); 68 | }, 69 | 70 | *getCompaniesDropdown() { 71 | const response: IResponseGenerator = yield actions.fetchFromAPIUnparsed( 72 | companiesDropdownEndpoint 73 | ); 74 | 75 | const companyDropdowns: Array = response.data; 76 | 77 | yield actions.setCompanyDropdowns(formatSelect2Data(companyDropdowns)); 78 | }, 79 | }; 80 | 81 | export default resolvers; 82 | -------------------------------------------------------------------------------- /src/data/jobs/selectors.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | 5 | import { IJobs } from '../../interfaces'; 6 | 7 | const selectors = { 8 | getJobs(state: IJobs) { 9 | const { jobs } = state; 10 | 11 | return jobs; 12 | }, 13 | 14 | getJobDetail(state: IJobs) { 15 | const { job } = state; 16 | 17 | return job; 18 | }, 19 | 20 | getJobTypes(state: IJobs) { 21 | const { jobTypes } = state; 22 | 23 | return jobTypes; 24 | }, 25 | 26 | getJobsSaving(state: IJobs) { 27 | const { jobsSaving } = state; 28 | 29 | return jobsSaving; 30 | }, 31 | 32 | getJobsDeleting(state: IJobs) { 33 | const { jobsDeleting } = state; 34 | 35 | return jobsDeleting; 36 | }, 37 | 38 | getLoadingJobs(state: IJobs) { 39 | const { loadingJobs } = state; 40 | 41 | return loadingJobs; 42 | }, 43 | 44 | getTotalPage(state: IJobs) { 45 | const { totalPage } = state; 46 | 47 | return totalPage; 48 | }, 49 | 50 | getTotal(state: IJobs) { 51 | const { total } = state; 52 | 53 | return total; 54 | }, 55 | 56 | getFilter(state: IJobs) { 57 | const { filters } = state; 58 | 59 | return filters; 60 | }, 61 | 62 | getForm(state: IJobs) { 63 | const { form } = state; 64 | 65 | return form; 66 | }, 67 | 68 | getCompaniesDropdown(state: IJobs) { 69 | const { companyDropdowns } = state; 70 | 71 | return companyDropdowns; 72 | }, 73 | }; 74 | 75 | export default selectors; 76 | -------------------------------------------------------------------------------- /src/data/jobs/types.ts: -------------------------------------------------------------------------------- 1 | export const GET_JOBS = 'GET_JOBS'; 2 | export const GET_JOB_DETAIL = 'GET_JOB_DETAIL'; 3 | export const GET_JOB_TYPES = 'GET_JOB_TYPES'; 4 | export const GET_COMPANIES_DROPDOWN = 'GET_COMPANIES_DROPDOWN'; 5 | export const SET_LOADING_JOBS = 'SET_LOADING_JOBS'; 6 | export const SET_TOTAL_JOBS_PAGE = 'SET_TOTAL_JOBS_PAGE'; 7 | export const SET_JOBS_FILTER = 'SET_JOBS_FILTER'; 8 | export const SET_TOTAL_JOBS = 'SET_TOTAL_JOBS'; 9 | export const CREATE_JOBS = 'CREATE_JOBS'; 10 | export const SET_JOBS_SAVING = 'SET_JOBS_SAVING'; 11 | export const SET_JOBS_DELETING = 'SET_JOBS_DELETING'; 12 | export const SET_JOB_FORM_DATA = 'SET_JOB_FORM_DATA'; 13 | export const UPDATE_JOBS = 'UPDATE_JOBS'; 14 | export const DELETE_JOBS = 'DELETE_JOBS'; 15 | export const FETCH_AFTER_DELETING_JOBS = 'FETCH_AFTER_DELETING_JOBS'; 16 | 17 | export const FETCH_FROM_API = 'FETCH_FROM_API'; 18 | export const FETCH_FROM_API_UNPARSED = 'FETCH_FROM_API_UNPARSED'; 19 | -------------------------------------------------------------------------------- /src/data/jobs/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { IJob } from '../../interfaces'; 5 | 6 | export const prepareJobDataForDatabase = (job: IJob) => { 7 | const data = { 8 | ...job, 9 | job_type_id: job.job_type.id, 10 | company_id: job.company.id, 11 | }; 12 | 13 | if (job.is_active !== undefined) { 14 | data.is_active = job.is_active; 15 | } else { 16 | data.is_active = 1; 17 | } 18 | 19 | // Remove unnecessary data. 20 | delete data.company; 21 | delete data.job_type; 22 | delete data.status; 23 | delete data._links; 24 | 25 | return data; 26 | }; 27 | -------------------------------------------------------------------------------- /src/data/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { register } from '@wordpress/data'; 5 | 6 | /** 7 | * Internal dependencies. 8 | */ 9 | import JobStore from './jobs'; 10 | 11 | /** 12 | * Register stores. 13 | */ 14 | register(JobStore); 15 | -------------------------------------------------------------------------------- /src/hooks/use-window-width.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies. 3 | */ 4 | import { useEffect, useState } from '@wordpress/element'; 5 | import { debounce } from 'lodash'; 6 | 7 | function useWindowWidth(delay: number = 500) { 8 | const [width, setWidth] = useState(window.innerWidth); 9 | 10 | useEffect(() => { 11 | const handleResize = () => setWidth(window.innerWidth); 12 | const debouncedHandleResize = debounce(handleResize, delay); 13 | window.addEventListener('resize', debouncedHandleResize); 14 | 15 | return () => { 16 | window.removeEventListener('resize', debouncedHandleResize); 17 | }; 18 | }, [delay]); 19 | 20 | return width; 21 | } 22 | 23 | export default useWindowWidth; 24 | -------------------------------------------------------------------------------- /src/hooks/useConfirmReload.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from '@wordpress/element'; 5 | 6 | export default function useConfirmReload() { 7 | // If user tries to reload or close the tab, 8 | // then show him a warning 9 | useEffect(() => { 10 | const unloadCallback = (event: any) => { 11 | event.preventDefault(); 12 | event.returnValue = ''; 13 | return ''; 14 | }; 15 | 16 | window.addEventListener('beforeunload', unloadCallback); 17 | return () => window.removeEventListener('beforeunload', unloadCallback); 18 | }, []); 19 | 20 | return null; 21 | } 22 | -------------------------------------------------------------------------------- /src/hooks/useMenuFix.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from '@wordpress/element'; 5 | import { useLocation } from 'react-router-dom'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import { menuFix } from '../utils/MenuFix'; 11 | 12 | export default function useMenuFix() { 13 | const location = useLocation(); 14 | 15 | /** 16 | * Call menuFix after any route changes. 17 | * 18 | * fix the admin menu for the slug 19 | */ 20 | useEffect(() => { 21 | menuFix(); 22 | }, [location.pathname]); 23 | 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useOutsideClick.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from '@wordpress/element'; 5 | 6 | export default function useOutsideClick( 7 | triggerRef: any, 8 | areaRef: any, 9 | callback: () => void 10 | ) { 11 | // Close on outside click. 12 | useEffect(() => { 13 | const clickHandler = (e: any) => { 14 | if (areaRef === null || areaRef?.current === null) { 15 | if (triggerRef.current.contains(e.target as Node)) { 16 | return; 17 | } 18 | } else if ( 19 | triggerRef?.current?.contains(e.target as Node) || 20 | areaRef?.current?.contains(e.target as Node) 21 | ) { 22 | return; 23 | } 24 | 25 | callback(); 26 | }; 27 | 28 | document.addEventListener('click', clickHandler); 29 | return () => document.removeEventListener('click', clickHandler); 30 | }, [triggerRef, areaRef, callback]); 31 | 32 | // Close on escape key press. 33 | useEffect(() => { 34 | const keyHandler = ({ keyCode }) => { 35 | if (keyCode !== 27) return; 36 | 37 | callback(); 38 | }; 39 | 40 | document.addEventListener('keydown', keyHandler); 41 | return () => document.removeEventListener('keydown', keyHandler); 42 | }); 43 | 44 | return null; 45 | } 46 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { render } from '@wordpress/element'; 5 | import './data/store'; 6 | 7 | /** 8 | * Internal dependencies 9 | */ 10 | import App from './App'; 11 | 12 | // Import the stylesheet for the plugin. 13 | import './style/tailwind.css'; 14 | import './style/main.scss'; 15 | 16 | // Render the App component into the DOM 17 | const jobPlaceElement = document.getElementById('jobplace'); 18 | 19 | if (jobPlaceElement) { 20 | render(, jobPlaceElement); 21 | } 22 | -------------------------------------------------------------------------------- /src/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export interface IAction { 2 | /** 3 | * Action type string key name. 4 | */ 5 | type: string; 6 | } 7 | 8 | export interface IResponseGenerator { 9 | config?: any; 10 | data?: any; 11 | headers?: any; 12 | request?: any; 13 | status?: number; 14 | statusText?: string; 15 | } 16 | 17 | // Job types 18 | export * from './jobs'; 19 | -------------------------------------------------------------------------------- /src/interfaces/jobs.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { ISelect2Input } from '../components/inputs/Select2Input'; 5 | 6 | export interface IJob { 7 | /** 8 | * Job ID. 9 | */ 10 | id: number; 11 | 12 | /** 13 | * Job title. 14 | */ 15 | title: string; 16 | 17 | /** 18 | * Job description. 19 | */ 20 | description: string; 21 | 22 | /** 23 | * Job Type ID. 24 | */ 25 | job_type_id: number; 26 | 27 | /** 28 | * Company ID. 29 | */ 30 | company_id: number; 31 | 32 | /** 33 | * Status published or draft 34 | */ 35 | is_active: boolean | number; 36 | 37 | /** 38 | * Job status. 39 | */ 40 | status?: 'draft' | 'published' | 'trashed'; 41 | } 42 | 43 | export interface IJobFormData extends IJob {} 44 | 45 | export interface IJobs { 46 | /** 47 | * All company list dropdown as array of {label, value}. 48 | */ 49 | companyDropdowns: Array; 50 | 51 | /** 52 | * All jobs as array of IJob. 53 | */ 54 | jobs: Array; 55 | 56 | /** 57 | * Job detail. 58 | */ 59 | job: IJob; 60 | 61 | /** 62 | * Job saving or not. 63 | */ 64 | jobsSaving: boolean; 65 | 66 | /** 67 | * Job deleting or not. 68 | */ 69 | jobsDeleting: boolean; 70 | 71 | /** 72 | * All job types as array of {label, value}. 73 | */ 74 | jobTypes: Array; 75 | 76 | /** 77 | * Is jobs loading. 78 | */ 79 | loadingJobs: boolean; 80 | 81 | /** 82 | * Count total page. 83 | */ 84 | totalPage: number; 85 | 86 | /** 87 | * Count total number of data. 88 | */ 89 | total: number; 90 | 91 | /** 92 | * Job list filter. 93 | */ 94 | filters: object; 95 | 96 | /** 97 | * Job Form data. 98 | */ 99 | form: IJobFormData; 100 | } 101 | 102 | export interface IJobFilter { 103 | /** 104 | * Job filter by page no. 105 | */ 106 | page?: number; 107 | 108 | /** 109 | * Job search URL params. 110 | */ 111 | search?: string; 112 | } 113 | 114 | export interface IJobTypes { 115 | /** 116 | * Job type id. 117 | */ 118 | id: number; 119 | 120 | /** 121 | * Job type name. 122 | */ 123 | name: string; 124 | 125 | /** 126 | * Job type slug. 127 | */ 128 | slug: string; 129 | 130 | /** 131 | * Job type description. 132 | */ 133 | description: string | null; 134 | } 135 | 136 | export interface ICompanyDropdown { 137 | /** 138 | * Company id. 139 | */ 140 | id: number; 141 | 142 | /** 143 | * Company name. 144 | */ 145 | name: string; 146 | 147 | /** 148 | * Company email. 149 | */ 150 | email: string; 151 | 152 | /** 153 | * Username. 154 | */ 155 | username: string; 156 | } 157 | -------------------------------------------------------------------------------- /src/pages/HomePage.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import Dashboard from '../components/dashboard/Dashboard'; 5 | 6 | const HomePage = () => { 7 | return ( 8 | <> 9 | 10 | 11 | ); 12 | }; 13 | 14 | export default HomePage; 15 | -------------------------------------------------------------------------------- /src/pages/jobs/CreateJob.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useEffect } from '@wordpress/element'; 5 | import { dispatch } from '@wordpress/data'; 6 | import { useNavigate } from 'react-router-dom'; 7 | import { __ } from '@wordpress/i18n'; 8 | 9 | /** 10 | * Internal dependencies 11 | */ 12 | import Layout from '../../components/layout/Layout'; 13 | import PageHeading from '../../components/layout/PageHeading'; 14 | import JobForm from '../../components/jobs/JobForm'; 15 | import JobSubmit from '../../components/jobs/JobSubmit'; 16 | import jobStore from '../../data/jobs'; 17 | import { jobDefaultFormData } from '../../data/jobs/default-state'; 18 | 19 | export default function CreateJob() { 20 | const navigate = useNavigate(); 21 | 22 | const backToJobsPage = () => { 23 | navigate('/jobs'); 24 | }; 25 | 26 | useEffect(() => { 27 | dispatch(jobStore).setFormData({ 28 | ...jobDefaultFormData, 29 | }); 30 | }, []); 31 | 32 | /** 33 | * Get Page Content - Title and New Job button. 34 | * 35 | * @return JSX.Element 36 | */ 37 | const pageTitleContent = ( 38 |
39 |
40 | 46 |
47 |
48 | 49 |
50 |
51 | ); 52 | 53 | /** 54 | * Get Right Side Content - Create Job form data. 55 | */ 56 | const pageRightSideContent = ( 57 |
58 | 59 |
60 | ); 61 | 62 | return ( 63 | 69 | 70 | 71 | ); 72 | } 73 | -------------------------------------------------------------------------------- /src/pages/jobs/EditJob.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { useSelect } from '@wordpress/data'; 5 | import { useNavigate, useParams } from 'react-router-dom'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal dependencies 10 | */ 11 | import Layout from '../../components/layout/Layout'; 12 | import PageHeading from '../../components/layout/PageHeading'; 13 | import JobForm from '../../components/jobs/JobForm'; 14 | import JobSubmit from '../../components/jobs/JobSubmit'; 15 | import jobStore from '../../data/jobs'; 16 | import { IJob } from '../../interfaces'; 17 | 18 | export default function EditJob() { 19 | const navigate = useNavigate(); 20 | const { id } = useParams(); 21 | 22 | const backToJobsPage = () => { 23 | navigate('/jobs'); 24 | }; 25 | 26 | const jobDetails: IJob = useSelect( 27 | (select) => select(jobStore).getJobDetail(id), 28 | [] 29 | ); 30 | 31 | /** 32 | * Get Page Content - Title and New Job button. 33 | * 34 | * @return JSX.Element 35 | */ 36 | const pageTitleContent = ( 37 |
38 |
39 | 45 |
46 |
47 | 48 |
49 |
50 | ); 51 | 52 | /** 53 | * Get Right Side Content - Create Job form data. 54 | */ 55 | const pageRightSideContent = ( 56 |
57 | 58 |
59 | ); 60 | 61 | return ( 62 | 68 | 69 | 70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /src/routes/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import HomePage from '../pages/HomePage'; 5 | import JobsPage from '../pages/jobs/JobsPage'; 6 | import CreateJob from '../pages/jobs/CreateJob'; 7 | import EditJob from '../pages/jobs/EditJob'; 8 | 9 | const routes = [ 10 | { 11 | path: '/', 12 | element: HomePage, 13 | }, 14 | { 15 | path: '/jobs', 16 | element: JobsPage, 17 | }, 18 | { 19 | path: '/jobs/new', 20 | element: CreateJob, 21 | }, 22 | { 23 | path: '/jobs/edit/:id', 24 | element: EditJob, 25 | }, 26 | ]; 27 | 28 | export default routes; 29 | -------------------------------------------------------------------------------- /src/style/main.scss: -------------------------------------------------------------------------------- 1 | 2 | body, 3 | html { 4 | background-color: #f0f0f1; 5 | } 6 | 7 | #wpcontent { 8 | background-color: #f0f0f1; 9 | } 10 | 11 | li > ul, 12 | li > ol { 13 | margin-left: auto; 14 | } 15 | 16 | .wp-submenu-wrap { 17 | margin-left: 0px; 18 | } 19 | 20 | #jobplace { 21 | background-color: #f0f0f1 !important; 22 | margin-left: -20px; 23 | margin-top: -4px; 24 | box-sizing: border-box; 25 | 26 | .app-title { 27 | font-size: 1.5em; 28 | font-weight: bold; 29 | margin-bottom: 1em; 30 | } 31 | 32 | input[type="checkbox"]:checked::before { 33 | margin: 0; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/style/tailwind.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Custom Styles */ 6 | @layer components { 7 | 8 | } -------------------------------------------------------------------------------- /src/utils/DateHelper.ts: -------------------------------------------------------------------------------- 1 | import { format, subDays, addDays } from 'date-fns'; 2 | 3 | /** 4 | * Get default date format. 5 | * 6 | * @return string 7 | */ 8 | export function getDefaultFormat() { 9 | return 'yyyy-MM-dd'; 10 | } 11 | 12 | /** 13 | * Get Current Formatted Date. 14 | * 15 | * @param viewFormat string; Default: 'yyyy-MM-dd' 16 | * 17 | * @return {Object} Date 18 | */ 19 | export function getCurrentDate(viewFormat: string = getDefaultFormat()) { 20 | return format(new Date(), viewFormat); 21 | } 22 | 23 | /** 24 | * Get formatted date. 25 | * 26 | * @param date {Date} 27 | * @param formation 28 | * @return string 29 | */ 30 | export function getFormattedDate(date: Date, formation = getDefaultFormat()) { 31 | try { 32 | date = new Date(date); 33 | return format(date, formation); 34 | } catch (error) { 35 | // Fix for any fall-back date format. 36 | if (typeof date === 'object') { 37 | return ''; 38 | } 39 | return date; 40 | } 41 | } 42 | 43 | /** 44 | * Get Subtracted or Added Days Date. 45 | * 46 | * @param type 47 | * @param days 48 | * @param date 49 | * @param viewFormat 50 | * 51 | * @return string 52 | */ 53 | export function getSubOrAddDaysDate( 54 | type: string, 55 | days: number, 56 | date = new Date(), 57 | viewFormat: string = getDefaultFormat() 58 | ): string { 59 | date = date === null ? new Date() : date; 60 | viewFormat = viewFormat === null ? getDefaultFormat() : viewFormat; 61 | 62 | if (type === 'sub') { 63 | return format(subDays(date, days), viewFormat); 64 | } else if (type === 'add') { 65 | return format(addDays(date, days), viewFormat); 66 | } 67 | 68 | return getCurrentDate(); 69 | } 70 | -------------------------------------------------------------------------------- /src/utils/MenuFix.ts: -------------------------------------------------------------------------------- 1 | export const wpReactKitSlug = 'jobplace'; 2 | 3 | /** 4 | * As we are using hash based navigation, hack fix 5 | * to highlight the current selected menu 6 | * 7 | * Requires jQuery 8 | */ 9 | export function menuFix() { 10 | const $ = jQuery; 11 | 12 | const menuRoot = $('#toplevel_page_' + wpReactKitSlug); 13 | const currentUrl = window.location.href; 14 | const currentPath = currentUrl.substr(currentUrl.indexOf('admin.php')); 15 | 16 | $('ul.wp-submenu li', menuRoot).removeClass('current'); 17 | 18 | menuRoot.on('click', 'a', function () { 19 | const self = $(this); 20 | 21 | $('ul.wp-submenu li', menuRoot).removeClass('current'); 22 | 23 | if (self.hasClass('wp-has-submenu')) { 24 | $('li.wp-first-item', menuRoot).addClass('current'); 25 | } else { 26 | self.parents('li').addClass('current'); 27 | } 28 | }); 29 | 30 | const navRoutes = currentPath.split('/'); 31 | $('ul.wp-submenu a', menuRoot).each(function (index: number, el: any) { 32 | const routeName: string = typeof navRoutes[1] !== "undefined" ? navRoutes[1] : ""; // eslint-disable-line 33 | let isActive = false; 34 | // const subRoute = typeof(routeName.split('?')[0] !== 'undefined') ? routeName.split('?')[0] : ""; // eslint-disable-line 35 | 36 | switch ($(el).attr('href')) { 37 | case currentPath: 38 | isActive = true; 39 | break; 40 | 41 | default: 42 | break; 43 | } 44 | 45 | if (isActive) { 46 | $(el).parent().addClass('current'); 47 | } 48 | }); 49 | } 50 | -------------------------------------------------------------------------------- /src/utils/NumberFormat.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies 3 | */ 4 | import { getGlobalData } from './global-data'; 5 | 6 | /** 7 | * Number format implementation. 8 | * 9 | * @param number number to format 10 | * @param noDecimals noDecimals points of decimals 11 | * 12 | * @return {string} Formatted number 13 | */ 14 | export function number_format( 15 | number: number, 16 | noDecimals: number | null = 2 17 | ): string { 18 | const { decimals, decimal_separator, thousand_separator } = 19 | getGlobalData('wc'); 20 | noDecimals = 21 | typeof noDecimals === 'undefined' || noDecimals === null 22 | ? decimals 23 | : noDecimals; 24 | 25 | var number = !isFinite(+number) ? 0 : +number, 26 | precedence = !isFinite(+noDecimals) ? 0 : Math.abs(noDecimals), 27 | sep = 28 | typeof thousand_separator === 'undefined' 29 | ? ',' 30 | : thousand_separator, 31 | dec = 32 | typeof decimal_separator === 'undefined' ? '.' : decimal_separator, 33 | toFixedFix = function (number, precedence: number) { 34 | const k = Math.pow(10, precedence); 35 | return Math.round(number * k) / k; 36 | }, 37 | s = (precedence ? toFixedFix(number, precedence) : Math.round(number)) 38 | .toString() 39 | .split('.'); 40 | 41 | if (s[0].length > 3) { 42 | s[0] = s[0].replace(/\B(?=(?:\d{3})+(?!\d))/g, sep); 43 | } 44 | 45 | if ((s[1] || '').length < precedence) { 46 | s[1] = s[1] || ''; 47 | s[1] += new Array(precedence - s[1].length + 1).join('0'); 48 | } 49 | 50 | return s.join(dec); 51 | } 52 | -------------------------------------------------------------------------------- /src/utils/Select2Helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Internal dependencies. 3 | */ 4 | import { ISelect2Input } from '../components/inputs/Select2Input'; 5 | 6 | /** 7 | * Format array to accept as select2 array. 8 | * 9 | * @param data array 10 | * 11 | * @return array 12 | */ 13 | export function formatSelect2Data(data: Array | object) { 14 | // If data is array and empty, return empty array. 15 | if (Array.isArray(data) && data.length === 0) { 16 | return []; 17 | } 18 | 19 | // If data is an array, format it with label and value 20 | if (Array.isArray(data)) { 21 | return data.map((item, key) => { 22 | // Check if it's already formatted with label and value 23 | if (item.hasOwnProperty('label') && item.hasOwnProperty('value')) { 24 | // if value is empty, return null 25 | if (item.value === '') { 26 | return null; 27 | } 28 | 29 | return item; 30 | } 31 | 32 | // Otherwise, format it with label and value 33 | let itemValue: any = item.id; 34 | if (typeof itemValue === 'undefined' || itemValue === null) { 35 | itemValue = 36 | typeof item.value !== 'undefined' ? item.value : key; 37 | } 38 | 39 | return { 40 | label: item?.name || item, 41 | value: itemValue, 42 | }; 43 | }); 44 | } 45 | 46 | // If data is object, then make an array of object with key value pair. 47 | if (typeof data === 'object') { 48 | return Object.keys(data).map((key) => { 49 | // data is empty, return null 50 | if (data[key] === '') { 51 | return null; 52 | } 53 | 54 | return { 55 | label: data[key], 56 | value: key, 57 | }; 58 | }); 59 | } 60 | } 61 | 62 | export const getSelectedOption = (options: Array, value: string) => { 63 | if ( 64 | typeof options !== 'undefined' && 65 | Array.isArray(options) && 66 | typeof value !== 'undefined' && 67 | value !== null && 68 | value !== '' 69 | ) { 70 | const optionValues = options.filter((option) => option.value == value); 71 | 72 | if (optionValues.length > 0) { 73 | return optionValues[0]; 74 | } 75 | } 76 | 77 | return null; 78 | }; 79 | -------------------------------------------------------------------------------- /src/utils/StringHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Capitalize a string. 3 | * 4 | * @param text string 5 | * @return string 6 | */ 7 | export function capitalize(text: string) { 8 | if (text === undefined) { 9 | return ''; 10 | } 11 | 12 | return text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/global-data.ts: -------------------------------------------------------------------------------- 1 | import { __ } from '@wordpress/i18n'; 2 | 3 | /** 4 | * Get global data. 5 | * 6 | * @param {string} key 7 | * 8 | * @return {Object | string} Global settings related data. 9 | */ 10 | export const getGlobalData = (key: string = '') => { 11 | const data = 12 | typeof window?.cart_pulse === 'undefined' ? {} : window?.cart_pulse; 13 | 14 | if (typeof data === 'undefined') { 15 | return data; 16 | } 17 | 18 | if (!key.length) { 19 | return data; 20 | } 21 | 22 | return data[key]; 23 | }; 24 | 25 | /** 26 | * Check if PRO is enabled or Not. 27 | * 28 | * @return {boolean} 29 | */ 30 | export const hasPro = !!getGlobalData('hasPro'); 31 | 32 | /** 33 | * Get email frequency units. 34 | * 35 | * @return {Array} 36 | */ 37 | export const emailFrequencyUnits = [ 38 | { 39 | label: __('Minute', 'jobplace'), 40 | value: 'minute', 41 | }, 42 | { 43 | label: __('Hour', 'jobplace'), 44 | value: 'hour', 45 | }, 46 | { 47 | label: __('Day', 'jobplace'), 48 | value: 'day', 49 | }, 50 | ]; 51 | 52 | /** 53 | * Default block content. 54 | */ 55 | export const defaultBlockContent = `

Start your Email template

`; 56 | -------------------------------------------------------------------------------- /src/utils/http.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External Dependencies. 3 | */ 4 | import axios from 'axios'; 5 | import { toast } from 'react-toastify'; 6 | import { __ } from '@wordpress/i18n'; 7 | 8 | /** 9 | * Internal Dependencies. 10 | */ 11 | import { getGlobalData } from './global-data'; 12 | 13 | /** 14 | * Http - Axios Wrapper. 15 | */ 16 | const http = axios.create({ 17 | baseURL: getGlobalData('rest').root + getGlobalData('rest').version, 18 | headers: { 19 | 'X-WP-Nonce': getGlobalData('rest').nonce, 20 | }, 21 | }); 22 | 23 | /** 24 | * Global Error Handler. 25 | * 26 | * @param {Object} error 27 | * 28 | * @return {void} 29 | */ 30 | const globalErrorHandler = (error: any) => { 31 | const statusCode = error.response ? error.response.status : null; 32 | 33 | if (statusCode === 400) { 34 | // Handle input response errors 35 | const errors = error.response.data.data.params; 36 | 37 | for (const key in errors) { 38 | if (errors.hasOwnProperty(key)) { 39 | // format the error message to show a better error message 40 | // replace the '_' with a space and capitalize the first letter 41 | const errorMessage = errors[key] 42 | .replace(/_/g, ' ') 43 | .replace(/\b\w/g, (l: any) => l.toUpperCase()); 44 | 45 | toast.error(errorMessage); 46 | break; 47 | } 48 | } 49 | } 50 | 51 | if (statusCode === 404) { 52 | toast.error( 53 | __( 54 | 'The requested resource does not exist or has been deleted.', 55 | 'cp' 56 | ) 57 | ); 58 | } 59 | 60 | if (statusCode === 401) { 61 | toast.error( 62 | __('Unauthorized. Please login to access this resource.', 'cp') 63 | ); 64 | } 65 | 66 | if (statusCode === 500) { 67 | // Handle server error responses. 68 | const errors = error.response; 69 | if (typeof errors.data.message === 'string') { 70 | toast.error(errors.data.message); 71 | } 72 | } 73 | }; 74 | 75 | // Add interceptor to handle errors 76 | http.interceptors.response.use(undefined, function (error) { 77 | globalErrorHandler(error); 78 | return Promise.reject(error); 79 | }); 80 | 81 | export default http; 82 | -------------------------------------------------------------------------------- /src/utils/text-parser.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import parse from 'html-react-parser'; 5 | 6 | /** 7 | * Parse any HTML string into React elements. 8 | * 9 | * @param html html string to parse. 10 | * @return string 11 | */ 12 | export function parseHtml(html: string) { 13 | return parse(html); 14 | } 15 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 4 | media: false, 5 | theme: { 6 | extend: { 7 | minWidth: { 8 | 36: '9rem', 9 | 44: '11rem', 10 | 56: '14rem', 11 | 60: '15rem', 12 | 80: '20rem', 13 | }, 14 | screens: { 15 | xs: '480px', 16 | md: '768px', 17 | lg: '1024px', 18 | xl: '1280px', 19 | '2xl': '1536px', 20 | }, 21 | colors: { 22 | primary: '#1c64f2', 23 | 'primary-dark': '#1a56db', 24 | 'primary-lite': '#1972f5', 25 | 26 | success: '#319F45', 27 | 'success-dark': '#27AE60', 28 | 'success-darker': '#00A88A', 29 | 'success-lite': '#DDFFE7', 30 | 'success-liter': '#C9FEB7', 31 | 32 | error: '#E2808A', 33 | 'error-dark': '#BD081C', 34 | 'error-lite': '#F1CCD7', 35 | 36 | warning: '#F08D07', 37 | 'warning-lite': '#FFF1E5', 38 | 'warning-liter': '#FFE9A8', 39 | 40 | black: '#1D2327', 41 | 42 | gray: '#DDDDDD', 43 | 'gray-lite': '#E0E0E0', 44 | 'gray-liter': '#F5F5F5', 45 | 'gray-dark': '#787878', 46 | }, 47 | }, 48 | variants: { 49 | extend: { 50 | opacity: ['disabled'], 51 | }, 52 | }, 53 | }, 54 | plugins: [], 55 | }; 56 | -------------------------------------------------------------------------------- /templates/app.php: -------------------------------------------------------------------------------- 1 |
2 |

...

3 |
4 | -------------------------------------------------------------------------------- /templates/blocks/header/markup.php: -------------------------------------------------------------------------------- 1 | 15 |
17 | style="background-color: ; padding: ; " 18 | > 19 |

20 | 21 |

22 | 23 |
24 | 25 |
26 |
27 | -------------------------------------------------------------------------------- /tests/e2e/global.setup.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * External dependencies 3 | */ 4 | import { request } from '@playwright/test'; 5 | import type { FullConfig } from '@playwright/test'; 6 | 7 | /** 8 | * WordPress dependencies 9 | */ 10 | import { RequestUtils } from '@wordpress/e2e-test-utils-playwright'; 11 | 12 | async function globalSetup(config: FullConfig) { 13 | const { storageState, baseURL } = config.projects[0].use; 14 | const storageStatePath = 15 | typeof storageState === 'string' ? storageState : undefined; 16 | 17 | const requestContext = await request.newContext({ 18 | baseURL, 19 | }); 20 | 21 | const requestUtils = await RequestUtils.setup({ 22 | storageStatePath 23 | }); 24 | 25 | // Authenticate and save the storageState to disk. 26 | await requestUtils.setupRest(); 27 | 28 | await requestContext.dispose(); 29 | } 30 | 31 | export default globalSetup; 32 | -------------------------------------------------------------------------------- /tests/e2e/gutenberg-test-plugin-disables-the-css-animations/gutenberg-test-plugin-disables-the-css-animations.php: -------------------------------------------------------------------------------- 1 | { 7 | test.beforeEach(async ({ admin }) => { 8 | await admin.createNewPost(); 9 | }); 10 | 11 | test('Should render title and description fields', async ({ editor, page }) => { 12 | await editor.insertBlock({ name: 'wrc/header' }); 13 | 14 | const titleField = page.locator('role=textbox[name="Header title"i]'); 15 | const descriptionField = page.locator('role=textbox[name="Header description"i]'); 16 | 17 | expect(await titleField.isVisible()).toBe(true); 18 | expect(await descriptionField.isVisible()).toBe(true); 19 | }); 20 | 21 | test('Should update title attribute when title field is changed', async ({ editor, page }) => { 22 | await editor.insertBlock({ name: 'wrc/header' }); 23 | 24 | const titleField = page.locator('role=textbox[name="Header title"i]'); 25 | 26 | await titleField.fill('New Title'); 27 | 28 | const postContent = await editor.getEditedPostContent(); 29 | 30 | expect(postContent).toBe(''); 31 | }); 32 | 33 | test('Should update description attribute when description field is changed', async ({ editor, page }) => { 34 | await editor.insertBlock({ name: 'wrc/header' }); 35 | 36 | const descriptionField = page.locator('role=textbox[name="Header description"i]'); 37 | 38 | await descriptionField.fill('New description'); 39 | 40 | const postContent = await editor.getEditedPostContent(); 41 | 42 | expect(postContent).toBe(''); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /tests/e2e/specs/env.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@wordpress/e2e-test-utils-playwright'; 2 | 3 | test.describe('Environment Setup Test', () => { 4 | test('Should load properly', async ({ page }) => { 5 | page.goto('/wp-admin'); 6 | await page.waitForLoadState('networkidle'); 7 | await page.waitForLoadState('domcontentloaded'); 8 | await expect(page.locator('div.wrap > h1')).toHaveText( 9 | 'Dashboard' 10 | ); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /tests/e2e/specs/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { test, expect } from '@playwright/test'; 2 | 3 | test('has title in playwright.dev', async ({ page }) => { 4 | await page.goto('https://playwright.dev/'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page).toHaveTitle(/Playwright/); 8 | }); 9 | 10 | test('get started link works or not', async ({ page }) => { 11 | await page.goto('https://playwright.dev/'); 12 | 13 | // Click the get started link. 14 | await page.getByRole('link', { name: 'Get started' }).click(); 15 | 16 | // Expects the URL to contain intro. 17 | await expect(page).toHaveURL(/.*intro/); 18 | }); 19 | -------------------------------------------------------------------------------- /tests/e2e/unit/example.test.ts: -------------------------------------------------------------------------------- 1 | describe('Example Unit Test-case', () => { 2 | function testSumFunction(a: number, b: number) { 3 | return a + b; 4 | } 5 | 6 | it('should equal 4', () => { 7 | expect(testSumFunction(2, 2)).toBe(4); 8 | }); 9 | 10 | test('also should equal 4', () => { 11 | expect(testSumFunction(2, 2)).toBe(4); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /tests/phpunit/Api/CompanyRestApiTest.php: -------------------------------------------------------------------------------- 1 | server = $wp_rest_server = new \WP_REST_Server; 38 | do_action( 'rest_api_init' ); 39 | } 40 | 41 | /** 42 | * @test 43 | * @group company-rest-api 44 | */ 45 | public function test_company_dropdown_list_endpoint_exists() { 46 | $endpoint = '/' . $this->namespace . '/' . $this->base . '/dropdown'; 47 | 48 | $request = new \WP_REST_Request( 'GET', $endpoint ); 49 | 50 | $response = $this->server->dispatch( $request ); 51 | 52 | $this->assertEquals( 200, $response->get_status() ); 53 | } 54 | 55 | /** 56 | * @test 57 | * @group company-rest-api 58 | */ 59 | public function test_company_dropdown_list_endpoint() { 60 | $endpoint = '/' . $this->namespace . '/' . $this->base . '/dropdown'; 61 | $request = new \WP_REST_Request( 'GET', $endpoint ); 62 | $response = $this->server->dispatch( $request ); 63 | $data = $response->get_data(); 64 | 65 | // It must be an array. 66 | $this->assertTrue( is_array( $data ) ); 67 | $this->assertEquals( count( $data ), 0 ); 68 | 69 | // Set company meta to administrator user. 70 | $user = get_user_by( 'id', 1 ); 71 | if ( $user ) { 72 | update_user_meta( 1, 'user_type', 'company' ); 73 | 74 | $response = $this->server->dispatch( $request ); 75 | $data = $response->get_data(); 76 | 77 | // Length must be 1 78 | $this->assertEquals( count( $data ), 1 ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/phpunit/Install/RunnerTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( true ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /tests/phpunit/Jobs/JobManagerTest.php: -------------------------------------------------------------------------------- 1 | job = new Job(); 30 | $this->job_manager = wp_react_kit()->jobs; 31 | 32 | // Truncate jobs table first before running tests. 33 | $this->job->truncate(); 34 | } 35 | 36 | /** 37 | * @test 38 | * @group jobs 39 | */ 40 | public function test_if_job_count_is_int() { 41 | $jobs_count = $this->job_manager->all( [ 'count' => true ] ); 42 | 43 | // Check if jobs_count is an integer. 44 | $this->assertIsInt( $jobs_count ); 45 | } 46 | 47 | /** 48 | * @test 49 | * @group jobs 50 | */ 51 | public function test_if_job_lists_is_array() { 52 | $jobs = $this->job_manager->all(); 53 | $this->assertIsArray( $jobs ); 54 | } 55 | 56 | /** 57 | * @test 58 | * @group jobs 59 | */ 60 | public function test_can_create_a_job() { 61 | // Get total jobs before creating job. 62 | $jobs_count = $this->job_manager->all( [ 'count' => true ] ); 63 | $this->assertEquals( 0, $jobs_count ); 64 | 65 | $job_id = $this->job_manager->create( [ 66 | 'title' => 'Job Title', 67 | 'description' => 'Job Description', 68 | 'company_id' => 1, 69 | 'job_type_id' => 2, 70 | 'is_active' => 1, 71 | ] ); 72 | 73 | // Check again the total jobs = 1 74 | $jobs_count = $this->job_manager->all( [ 'count' => true ] ); 75 | $this->assertEquals( 1, $jobs_count ); 76 | 77 | // Check if job_id is an integer also. 78 | $this->assertIsInt( $job_id ); 79 | } 80 | 81 | /** 82 | * @test 83 | * @group jobs 84 | */ 85 | public function test_can_find_a_job() { 86 | $job_id = $this->job_manager->create( [ 87 | 'title' => 'Job Title', 88 | 'description' => 'Job Description', 89 | 'company_id' => 1, 90 | 'job_type_id' => 2, 91 | 'is_active' => 1, 92 | ] ); 93 | $this->assertIsInt( $job_id ); 94 | 95 | // Find the job 96 | $job = $this->job_manager->get( [ 'key' => 'id', 'value' => $job_id ] ); 97 | 98 | // Check if job is an object 99 | $this->assertIsObject( $job ); 100 | 101 | // Check if job id is found on $job->id 102 | $this->assertEquals( $job_id, $job->id ); 103 | } 104 | 105 | /** 106 | * @test 107 | * @group jobs 108 | */ 109 | public function test_can_update_a_job() { 110 | $job_id = $this->job_manager->create( [ 111 | 'title' => 'Job Title', 112 | 'description' => 'Job Description', 113 | 'company_id' => 1, 114 | 'job_type_id' => 2, 115 | 'is_active' => 1, 116 | ] ); 117 | $this->assertIsInt( $job_id ); 118 | $this->assertGreaterThan( 0, $job_id ); 119 | $this->assertEquals( 1, $this->job_manager->update([ 120 | 'title' => 'Job Title Updated', 121 | 'description' => 'Job Description Updated', 122 | 'company_id' => 1, 123 | 'job_type_id' => 2, 124 | 'is_active' => 1, 125 | ], $job_id)); 126 | } 127 | 128 | /** 129 | * @test 130 | * @group jobs 131 | */ 132 | public function test_can_delete_a_job() { 133 | $job_id = $this->job_manager->create( [ 134 | 'title' => 'Job Title', 135 | 'description' => 'Job Description', 136 | 'company_id' => 1, 137 | 'job_type_id' => 2, 138 | 'is_active' => 1, 139 | ] ); 140 | 141 | // Check total jobs = 1 142 | $jobs_count = $this->job_manager->all( [ 'count' => true ] ); 143 | $this->assertEquals( 1, $jobs_count ); 144 | 145 | // Delete the job 146 | $this->job_manager->delete( $job_id ); 147 | 148 | // Check total jobs = 0 149 | $jobs_count = $this->job_manager->all( [ 'count' => true ] ); 150 | $this->assertEquals( 0, $jobs_count ); 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /tests/phpunit/bootstrap.php: -------------------------------------------------------------------------------- 1 | run(); 75 | } 76 | 77 | /** 78 | * After setup theme install any needed plugins. 79 | * 80 | * @since 0.3.1 81 | * 82 | * @example for Woocommerce Installation 83 | * 84 | * @return void 85 | */ 86 | function install_necessary_plugins(): void { 87 | // clean existing install first 88 | /* 89 | define( 'WP_UNINSTALL_PLUGIN', true ); 90 | define( 'WC_REMOVE_ALL_DATA', true ); 91 | 92 | include dirname( dirname( dirname( dirname( __FILE__ ) ) ) ) . '/woocommerce/uninstall.php'; 93 | 94 | WC_Install::install(); 95 | 96 | // Reload capabilities after install, see https://core.trac.wordpress.org/ticket/28374. 97 | if ( version_compare( $GLOBALS['wp_version'], '4.7', '<' ) ) { 98 | $GLOBALS['wp_roles']->reinit(); 99 | } else { 100 | $GLOBALS['wp_roles'] = null; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited 101 | wp_roles(); 102 | } 103 | 104 | echo esc_html( 'Installing WooCommerce...' . PHP_EOL ); 105 | */ 106 | } 107 | 108 | tests_add_filter( 'setup_theme', 'install_plugin_databases' ); 109 | tests_add_filter( 'setup_theme', 'install_necessary_plugins' ); 110 | 111 | // Start up the WP testing environment. 112 | require "{$_tests_dir}/includes/bootstrap.php"; -------------------------------------------------------------------------------- /tests/phpunit/wp-config.php: -------------------------------------------------------------------------------- 1 |