├── .babelrc
├── .docker
├── db
│ └── dump.sql
├── install-php.sh
├── nginx.conf
└── php.ini
├── .env.docker
├── .env.example
├── .env.travis
├── .eslintignore
├── .eslintrc.js
├── .gitattributes
├── .gitignore
├── .phpunit.result.cache
├── .travis.yml
├── Dockerfile
├── README.md
├── app
├── Console
│ ├── Commands
│ │ └── RefreshDatabase.php
│ └── Kernel.php
├── Exceptions
│ └── Handler.php
├── Http
│ ├── Controllers
│ │ ├── Api
│ │ │ ├── ArticleController.php
│ │ │ ├── Auth
│ │ │ │ ├── LoginController.php
│ │ │ │ └── RegisterController.php
│ │ │ └── UserController.php
│ │ ├── Auth
│ │ │ ├── ConfirmPasswordController.php
│ │ │ ├── ForgotPasswordController.php
│ │ │ ├── LoginController.php
│ │ │ ├── RegisterController.php
│ │ │ ├── ResetPasswordController.php
│ │ │ └── VerificationController.php
│ │ └── Controller.php
│ ├── Kernel.php
│ ├── Middleware
│ │ ├── EncryptCookies.php
│ │ ├── RedirectIfAuthenticated.php
│ │ ├── TrimStrings.php
│ │ └── VerifyCsrfToken.php
│ └── Requests
│ │ ├── ArticleRequest.php
│ │ └── UserRequest.php
├── Models
│ ├── Article.php
│ └── User.php
└── Providers
│ ├── AppServiceProvider.php
│ ├── AuthServiceProvider.php
│ ├── BroadcastServiceProvider.php
│ ├── EventServiceProvider.php
│ └── RouteServiceProvider.php
├── artisan
├── bootstrap
├── app.php
├── autoload.php
└── cache
│ └── .gitignore
├── composer.json
├── composer.lock
├── config
├── api.php
├── app.php
├── auth.php
├── broadcasting.php
├── cache.php
├── cors.php
├── database.php
├── filesystems.php
├── mail.php
├── queue.php
├── sanctum.php
├── services.php
├── session.php
└── view.php
├── database
├── .gitignore
├── factories
│ ├── ArticleFactory.php
│ └── UserFactory.php
├── migrations
│ ├── 2014_10_12_000000_create_users_table.php
│ ├── 2014_10_12_100000_create_password_resets_table.php
│ ├── 2017_03_24_122715_create_article_table.php
│ └── 2019_12_14_000001_create_personal_access_tokens_table.php
└── seeders
│ ├── DatabaseSeeder.php
│ └── UsersTableSeeder.php
├── docker-compose.yml
├── license.md
├── package-lock.json
├── package.json
├── phpunit.xml
├── public
├── .htaccess
├── favicon.ico
├── index.php
├── robots.txt
└── web.config
├── resources
├── js
│ ├── app.js
│ ├── bootstrap.js
│ ├── common
│ │ ├── articles
│ │ │ └── listing
│ │ │ │ ├── components
│ │ │ │ ├── Article.js
│ │ │ │ └── Articles.js
│ │ │ │ └── index.js
│ │ ├── footer
│ │ │ └── index.js
│ │ ├── loader
│ │ │ └── index.js
│ │ ├── navigation
│ │ │ ├── NavItem.js
│ │ │ ├── PrivateHeader.js
│ │ │ ├── PublicHeader.js
│ │ │ └── index.js
│ │ └── scroll-top
│ │ │ ├── ScrollTop.js
│ │ │ └── index.js
│ ├── layout
│ │ ├── Private.js
│ │ ├── Public.js
│ │ └── index.js
│ ├── modules
│ │ ├── article
│ │ │ ├── Article.js
│ │ │ ├── pages
│ │ │ │ ├── add
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── edit
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ │ └── list
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ ├── ArticleRow.js
│ │ │ │ │ └── Pagination.js
│ │ │ │ │ └── index.js
│ │ │ ├── routes.js
│ │ │ ├── service.js
│ │ │ └── store
│ │ │ │ ├── action-types.js
│ │ │ │ ├── actions.js
│ │ │ │ └── reducer.js
│ │ ├── auth
│ │ │ ├── pages
│ │ │ │ ├── login
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── password
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ │ ├── register
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ │ └── reset-password
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ ├── routes.js
│ │ │ ├── service.js
│ │ │ └── store
│ │ │ │ ├── action-types.js
│ │ │ │ ├── actions.js
│ │ │ │ └── reduer.js
│ │ ├── user
│ │ │ ├── User.js
│ │ │ ├── pages
│ │ │ │ └── edit
│ │ │ │ │ ├── Page.js
│ │ │ │ │ ├── components
│ │ │ │ │ └── Form.js
│ │ │ │ │ └── index.js
│ │ │ ├── routes.js
│ │ │ ├── service.js
│ │ │ └── store
│ │ │ │ ├── action-types.js
│ │ │ │ ├── actions.js
│ │ │ │ └── reducer.js
│ │ └── web
│ │ │ ├── pages
│ │ │ ├── blog
│ │ │ │ ├── details
│ │ │ │ │ ├── Page.js
│ │ │ │ │ └── index.js
│ │ │ │ └── list
│ │ │ │ │ ├── Page.js
│ │ │ │ │ └── index.js
│ │ │ └── home
│ │ │ │ ├── Page.js
│ │ │ │ ├── components
│ │ │ │ └── Header.js
│ │ │ │ └── index.js
│ │ │ └── routes.js
│ ├── routes
│ │ ├── Private.js
│ │ ├── Public.js
│ │ ├── index.js
│ │ └── routes.js
│ ├── store
│ │ ├── config.js
│ │ ├── index.js
│ │ └── reducers.js
│ ├── utils
│ │ ├── Http.js
│ │ ├── Model.js
│ │ └── Transformer.js
│ └── values
│ │ └── index.js
├── lang
│ └── en
│ │ ├── auth.php
│ │ ├── pagination.php
│ │ ├── passwords.php
│ │ └── validation.php
├── sass
│ ├── _variables.scss
│ └── app.scss
└── views
│ └── index.blade.php
├── routes
├── api.php
├── api
│ ├── articles.php
│ ├── auth.php
│ └── users.php
├── channels.php
├── console.php
└── web.php
├── server.php
├── storage
├── app
│ ├── .gitignore
│ └── public
│ │ └── .gitignore
├── framework
│ ├── .gitignore
│ ├── cache
│ │ └── .gitignore
│ ├── sessions
│ │ └── .gitignore
│ ├── testing
│ │ └── .gitignore
│ └── views
│ │ └── .gitignore
└── logs
│ └── .gitignore
├── tests
├── CreatesApplication.php
├── Feature
│ ├── ArticleTest.php
│ └── LoginTest.php
├── TestCase.php
└── Unit
│ └── ExampleTest.php
└── webpack.mix.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": [
3 | "@babel/preset-env"
4 | ],
5 | "plugins": [
6 | "@babel/plugin-syntax-dynamic-import",
7 | "@babel/plugin-proposal-class-properties"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/.docker/install-php.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | apk add bzip2 file re2c freetds freetype icu libintl libldap libjpeg libmcrypt libpng libpq libwebp libzip nodejs npm
4 |
5 | TMP="autoconf \
6 | bzip2-dev \
7 | freetds-dev \
8 | freetype-dev \
9 | g++ \
10 | gcc \
11 | gettext-dev \
12 | icu-dev \
13 | jpeg-dev \
14 | libmcrypt-dev \
15 | libpng-dev \
16 | libwebp-dev \
17 | libxml2-dev \
18 | libzip-dev \
19 | make \
20 | openldap-dev \
21 | postgresql-dev"
22 |
23 | apk add $TMP
24 |
25 | # Configure extensions
26 | docker-php-ext-configure gd --with-jpeg-dir=usr/ --with-freetype-dir=usr/ --with-webp-dir=usr/
27 | docker-php-ext-configure ldap --with-libdir=lib/
28 | docker-php-ext-configure pdo_dblib --with-libdir=lib/
29 |
30 | docker-php-ext-install \
31 | bz2 \
32 | exif \
33 | gd \
34 | gettext \
35 | intl \
36 | ldap \
37 | pdo_dblib \
38 | pdo_pgsql \
39 | xmlrpc \
40 | zip \
41 | mysqli \
42 | pdo_mysql
43 |
44 | # Install Xdebug
45 | pecl install xdebug \
46 | && docker-php-ext-enable xdebug \
47 | && echo "remote_host=docker.for.mac.localhost" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
48 | && echo "remote_port=9001" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
49 | && echo "remote_enable=1" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
50 | && echo "idekey=IDE_DEBUG" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
51 | && echo "error_reporting=E_ALL" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
52 | && echo "display_startup_errors=On" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
53 | && echo "display_errors=On" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini
54 |
55 | # Install composer
56 | cd /tmp && php -r "readfile('https://getcomposer.org/installer');" | php && \
57 | mv composer.phar /usr/bin/composer && \
58 | chmod +x /usr/bin/composer
59 |
60 | apk del $TMP
61 |
62 | # Install PHPUnit
63 | curl -sSL -o /usr/bin/phpunit https://phar.phpunit.de/phpunit.phar && chmod +x /usr/bin/phpunit
64 |
65 | # Set timezone
66 | #RUN echo Asia/Karachi > /etc/timezone && dpkg-reconfigure -f noninteractive tzdata
67 |
--------------------------------------------------------------------------------
/.docker/nginx.conf:
--------------------------------------------------------------------------------
1 | server {
2 | listen 8080;
3 | index index.php index.html;
4 | error_log /var/log/nginx/api-error.log;
5 | access_log /var/log/nginx/api-access.log;
6 | root /var/www/app/public;
7 | location ~ \.php$ {
8 | try_files $uri =404;
9 | fastcgi_split_path_info ^(.+\.php)(/.+)$;
10 | fastcgi_pass lr_app:9000;
11 | fastcgi_index index.php;
12 | include fastcgi_params;
13 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
14 | fastcgi_param PATH_INFO $fastcgi_path_info;
15 | }
16 | location / {
17 | try_files $uri $uri/ /index.php?$query_string;
18 | gzip_static on;
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.docker/php.ini:
--------------------------------------------------------------------------------
1 | upload_max_filesize=40M
2 | post_max_size=40M
3 | memory_limit = -1
4 |
--------------------------------------------------------------------------------
/.env.docker:
--------------------------------------------------------------------------------
1 | APP_ENV=test
2 | APP_KEY=key
3 | APP_DEBUG=true
4 | APP_LOG_LEVEL=debug
5 | APP_URL=http://localhost:8100
6 |
7 | DB_CONNECTION=mysql
8 | DB_HOST=lr_database
9 | DB_PORT=3306
10 | DB_DATABASE=laravel_react
11 | DB_USERNAME=moeen
12 | DB_PASSWORD=basra
13 |
14 | BROADCAST_DRIVER=log
15 | CACHE_DRIVER=file
16 | SESSION_DRIVER=file
17 | QUEUE_DRIVER=sync
18 |
19 | REDIS_HOST=127.0.0.1
20 | REDIS_PASSWORD=null
21 | REDIS_PORT=6379
22 |
23 | MAIL_DRIVER=smtp
24 | MAIL_HOST=mailtrap.io
25 | MAIL_PORT=2525
26 | MAIL_USERNAME=null
27 | MAIL_PASSWORD=null
28 | MAIL_ENCRYPTION=null
29 |
30 | PUSHER_APP_ID=
31 | PUSHER_APP_KEY=
32 | PUSHER_APP_SECRET=
33 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | APP_ENV=local
2 | APP_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
3 | APP_DEBUG=true
4 | APP_LOG_LEVEL=debug
5 | APP_URL=http://localhost:3000
6 |
7 | DB_CONNECTION=mysql
8 | DB_HOST=127.0.0.1
9 | DB_PORT=3306
10 | DB_DATABASE=database
11 | DB_USERNAME=username
12 | DB_PASSWORD=password
13 |
14 | BROADCAST_DRIVER=log
15 | CACHE_DRIVER=file
16 | SESSION_DRIVER=file
17 | QUEUE_DRIVER=sync
18 |
19 | REDIS_HOST=127.0.0.1
20 | REDIS_PASSWORD=null
21 | REDIS_PORT=6379
22 |
23 | MAIL_DRIVER=smtp
24 | MAIL_HOST=mailtrap.io
25 | MAIL_PORT=2525
26 | MAIL_USERNAME=null
27 | MAIL_PASSWORD=null
28 | MAIL_ENCRYPTION=null
29 |
30 | PUSHER_APP_ID=
31 | PUSHER_APP_KEY=
32 | PUSHER_APP_SECRET=
33 |
34 | PERSONAL_CLIENT_ID=1
35 | PERSONAL_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
36 | PASSWORD_CLIENT_ID=2
37 | PASSWORD_CLIENT_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
--------------------------------------------------------------------------------
/.env.travis:
--------------------------------------------------------------------------------
1 | APP_ENV=testing
2 | APP_KEY=SomeRandomString
3 |
4 | DB_CONNECTION=testing
5 | DB_TEST_USERNAME=root
6 | DB_TEST_PASSWORD=
7 |
8 | CACHE_DRIVER=array
9 | SESSION_DRIVER=array
10 | QUEUE_DRIVER=sync
11 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /public
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | // http://eslint.org/docs/user-guide/configuring
2 |
3 | module.exports = {
4 | root: true,
5 | parser: 'babel-eslint',
6 | env: {
7 | browser: true,
8 | node: true,
9 | es6: true,
10 | },
11 | globals: {
12 | React: true
13 | },
14 | parserOptions: {
15 | ecmaFeatures: {
16 | jsx: true
17 | }
18 | },
19 | // https://github.com/feross/standard/blob/master/RULES.md#javascript-standard-style
20 | extends: ["eslint:recommended", "plugin:react/recommended"],
21 | // required to lint *.vue files
22 | plugins: [
23 | 'html', 'react'
24 | ],
25 | // add your custom rules here
26 | 'rules': {
27 | // allow paren-less arrow functions
28 | 'arrow-parens': 0,
29 | // allow async-await
30 | 'generator-star-spacing': 0,
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 | *.css linguist-vendored
3 | *.scss linguist-vendored
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /storage/*.key
3 | /vendor
4 | /.idea
5 | Homestead.json
6 | Homestead.yaml
7 | .env
8 | _ide_helper.php
9 |
10 | /public/storage
11 | /public/hot
12 | /public/js/
13 | /public/css/
14 | /public/fonts/
15 | /public/mix-manifest.json
16 | /*.log
17 | /public/*.js
18 | /public/*.js.map
19 |
--------------------------------------------------------------------------------
/.phpunit.result.cache:
--------------------------------------------------------------------------------
1 | C:37:"PHPUnit\Runner\DefaultTestResultCache":1256:{a:2:{s:7:"defects";a:7:{s:74:"Tests\Feature\ArticleTest::that_only_loading_articles_for_provided_user_id";i:4;s:49:"Tests\Feature\ArticleTest::that_load_all_articles";i:4;s:62:"Tests\Feature\ArticleTest::that_loaded_only_published_articles";i:4;s:59:"Tests\Feature\ArticleTest::that_load_only_published_article";i:4;s:95:"Tests\Feature\ArticleTest::that_article_get_published_and_total_number_of_published_get_changed";i:4;s:99:"Tests\Feature\ArticleTest::that_article_get_unpublished_and_total_number_of_unpublished_get_changed";i:4;s:44:"Tests\Feature\LoginTest::test_user_can_login";i:3;}s:5:"times";a:8:{s:74:"Tests\Feature\ArticleTest::that_only_loading_articles_for_provided_user_id";d:1.668;s:49:"Tests\Feature\ArticleTest::that_load_all_articles";d:0.541;s:62:"Tests\Feature\ArticleTest::that_loaded_only_published_articles";d:0.376;s:59:"Tests\Feature\ArticleTest::that_load_only_published_article";d:0.556;s:95:"Tests\Feature\ArticleTest::that_article_get_published_and_total_number_of_published_get_changed";d:0.438;s:99:"Tests\Feature\ArticleTest::that_article_get_unpublished_and_total_number_of_unpublished_get_changed";d:0.419;s:44:"Tests\Feature\LoginTest::test_user_can_login";d:1.037;s:37:"Tests\Unit\ExampleTest::testBasicTest";d:0.21;}}}
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: php
2 |
3 | php:
4 | - 7.3
5 |
6 | before_script:
7 | - cp .env.travis .env
8 | - composer self-update
9 | - composer install --no-interaction
10 | - php artisan key:generate
11 | - php artisan migrate --no-interaction --verbose
12 | - php artisan passport:configure
13 |
14 | before_install:
15 | - mysql -e 'CREATE DATABASE testing;'
16 |
17 | script:
18 | - ./vendor/bin/phpunit
19 |
20 | services:
21 | - mysql
22 |
23 | cache:
24 | directories:
25 | - vendor
26 |
27 | branches:
28 | only:
29 | - master
30 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM php:7.4-fpm-alpine
2 |
3 | # Comment this to improve stability on "auto deploy" environments
4 | RUN apk update && apk upgrade
5 |
6 | # Install basic dependencies
7 | RUN apk -u add bash git
8 |
9 | # Install PHP extensions
10 | ADD ./.docker/install-php.sh /usr/sbin/install-php.sh
11 | RUN chmod +x /usr/sbin/install-php.sh
12 | RUN /usr/sbin/install-php.sh
13 |
14 | # Copy existing application directory contents
15 | COPY ./.docker/*.ini /usr/local/etc/php/conf.d/
16 | COPY . .
17 |
18 | # Change current user to www-data
19 | USER www-data
20 |
21 | # Expose ports and start php-fpm server
22 | EXPOSE 9000
23 | CMD ["php-fpm"]
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 | ## Laravel 8 and React 17 boilerplate
4 | There are two different ways to run this demo
5 |
6 | Please follow the guide.
7 |
8 | ## Prerequisite
9 |
10 | 1. Make sure you have [composer](https://getcomposer.org/download/) installed.
11 | 2. Make sure you have latest stable version of [node](https://nodejs.org/en/download/) installed.
12 |
13 | ### Option 1
14 |
15 | 1. `git clone`
16 | 2. `create a .env file copy content from .env.example and update the values`
17 | 3. `composer install && composer update`
18 | 4. `php artisan cron:refresh-database`
19 | 5. if npm version < 7 `npm install && npm run dev` else `npm install --legacy-peer-deps && npm run dev`
20 | 6. `php artisan key:gen`
21 | 7. `php artisan serve`
22 |
23 | ### Option 2
24 |
25 | ## Prerequisite
26 | Make sure you have [docker](https://docs.docker.com/install/) and [docker-compose](https://docs.docker.com/compose/install/) installed on you machine.
27 |
28 | 1. `git clone`
29 | 2. `create a .env file copy content from .env.docker and do not make any change`
30 |
31 | run following command in terminal / power shell
32 | ```
33 | docker-compose up -d
34 | ```
35 |
36 | when docker will finish building the containers, access the "laravel-react-app" container using following command
37 |
38 | `docker exec -it lr_app sh`
39 |
40 | now you will be inside container
41 |
42 | run following commands
43 | 1. `composer install && composer update`
44 | 2. `php artisan cron:refresh-database`
45 | 3. `php artisan key:gen`
46 | 4. if npm version < 7 `npm install && npm run dev` else `npm install --legacy-peer-deps && npm run dev`
47 |
48 | open browser and check the following address
49 |
50 | `http://localhost:8100`
51 |
52 | TODO:
53 |
54 | - [x] Add Redux
55 | - [x] Add Laravel Sanctum for authentication
56 | - [x] User Login
57 | - [x] User Register
58 | - [x] Users Crud
59 | - [x] Articles Crud
60 | - [x] Form validation Client and Server
61 | - [x] Reset Password
62 | - [x] Tests
63 | - [x] Upgrade to Laravel 7
64 | - [x] Upgrade to React 16.13
65 | - [x] docker
66 |
67 |
68 |
69 |
--------------------------------------------------------------------------------
/app/Console/Commands/RefreshDatabase.php:
--------------------------------------------------------------------------------
1 | call('migrate:refresh');
31 | $this->call('db:seed');
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/app/Console/Kernel.php:
--------------------------------------------------------------------------------
1 | command('inspire')
29 | // ->hourly();
30 | $schedule->command(RefreshDatabase::class)->hourly();
31 | }
32 |
33 | /**
34 | * Register the Closure based commands for the application.
35 | *
36 | * @return void
37 | */
38 | protected function commands()
39 | {
40 | $this->load(__DIR__.'/Commands');
41 | require base_path('routes/console.php');
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/Exceptions/Handler.php:
--------------------------------------------------------------------------------
1 | expectsJson()) {
67 | return response()->json(['error' => 'Unauthenticated.'], 401);
68 | }
69 |
70 | return redirect()->guest(route('login'));
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/ArticleController.php:
--------------------------------------------------------------------------------
1 | user()->is_admin) {
23 | return Article::loadAll();
24 | }
25 | return Article::loadAllMine($request->user()->id);
26 | }
27 |
28 | /**
29 | * get all published articles
30 | *
31 | * @return mixed
32 | */
33 | public function publishedArticles()
34 | {
35 | return Article::loadAllPublished();
36 | }
37 |
38 | /**
39 | * Get single published article
40 | *
41 | * @param $slug
42 | * @return mixed
43 | */
44 | public function publishedArticle($slug)
45 | {
46 | return Article::loadPublished($slug);
47 | }
48 |
49 | /**
50 | * Show the form for creating a new resource.
51 | *
52 | * @return \Illuminate\Http\Response
53 | */
54 | public function create()
55 | {
56 | //
57 | }
58 |
59 | /**
60 | * Store a newly created resource in storage.
61 | *
62 | * @param ArticleRequest $request
63 | * @return \Illuminate\Http\Response
64 | */
65 | public function store(ArticleRequest $request)
66 | {
67 | $user = $request->user();
68 |
69 | $article = new Article($request->validated());
70 | $article->slug = Str::slug($request->get('title'));
71 |
72 | $user->articles()->save($article);
73 |
74 | return response()->json($article, 201);
75 | }
76 |
77 | /**
78 | * Display the specified resource.
79 | *
80 | * @param \Illuminate\Http\Request $request
81 | * @param int $id
82 | * @return \Illuminate\Http\Response
83 | */
84 | public function show(Request $request, $id)
85 | {
86 | if (!$request->user()->is_admin) {
87 | return Article::mine($request->user()->id)->findOrFail($id);
88 | }
89 |
90 | return Article::findOrFail($id);
91 | }
92 |
93 | /**
94 | * Show the form for editing the specified resource.
95 | *
96 | * @param int $id
97 | * @return \Illuminate\Http\Response
98 | */
99 | public function edit($id)
100 | {
101 | //
102 | }
103 |
104 | /**
105 | * Update the specified resource in storage.
106 | *
107 | * @param ArticleRequest $request
108 | * @param int $id
109 | * @return \Illuminate\Http\Response
110 | */
111 | public function update(ArticleRequest $request, $id)
112 | {
113 | $article = Article::findOrFail($id);
114 |
115 | $data = $request->validated();
116 | $data['slug'] = Str::slug($data['title']);
117 | $article->update($data);
118 |
119 | return response()->json($article, 200);
120 | }
121 |
122 | /**
123 | * Remove the specified resource from storage.
124 | *
125 | * @param int $id
126 | * @return \Illuminate\Http\Response
127 | */
128 | public function delete($id)
129 | {
130 | $article = Article::findOrFail($id);
131 |
132 | $article->delete();
133 |
134 | return response([], 200);
135 | }
136 | }
137 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/Auth/LoginController.php:
--------------------------------------------------------------------------------
1 | validate($request, [
15 | 'email' => 'required|email|exists:users,email',
16 | 'password' => 'required|min:6',
17 | ], [
18 | 'email.exists' => 'The user credentials were incorrect.',
19 | ]);
20 |
21 | request()->request->add([
22 | 'grant_type' => 'password',
23 | 'client_id' => env('PASSWORD_CLIENT_ID'),
24 | 'client_secret' => env('PASSWORD_CLIENT_SECRET'),
25 | 'username' => $input['email'],
26 | 'password' => $input['password'],
27 | ]);
28 |
29 | $response = Route::dispatch(Request::create('/oauth/token', 'POST'));
30 |
31 | $data = json_decode($response->getContent(), true);
32 |
33 | if (!$response->isOk()) {
34 | return response()->json($data, 401);
35 | }
36 |
37 | return $data;
38 | }
39 |
40 | public function logout(Request $request)
41 | {
42 | $accessToken = $request->user()->token();
43 |
44 | DB::table('oauth_refresh_tokens')
45 | ->where('access_token_id', $accessToken->id)
46 | ->update([
47 | 'revoked' => true,
48 | ]);
49 |
50 | $accessToken->revoke();
51 |
52 | return response()->json([], 201);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/Auth/RegisterController.php:
--------------------------------------------------------------------------------
1 | validate($request, [
16 | 'name' => 'required|min:3',
17 | 'email' => 'required|email|unique:users,email',
18 | 'password' => 'required|min:6|confirmed',
19 | 'password_confirmation' => 'required|min:6'
20 | ], [
21 | 'password.confirmed' => 'The password does not match.'
22 | ]);
23 |
24 | try {
25 | event(new Registered($this->create($request->all())));
26 |
27 | $http = new Client;
28 |
29 | $response = $http->post(env('APP_URL') . '/oauth/token', [
30 | 'form_params' => [
31 | 'grant_type' => 'password',
32 | 'client_id' => env('PASSWORD_CLIENT_ID'),
33 | 'client_secret' => env('PASSWORD_CLIENT_SECRET'),
34 | 'username' => $request->get('email'),
35 | 'password' => $request->get('password'),
36 | 'remember' => false,
37 | 'scope' => '',
38 | ],
39 | ]);
40 |
41 | return json_decode((string)$response->getBody(), true);
42 | } catch (\Exception $e) {
43 | dd($e->getMessage(), $e->getCode(), $e->getTrace());
44 | return response()->json([
45 | "error" => "invalid_credentials",
46 | "message" => "The user credentials were incorrect."
47 | ], 401);
48 | }
49 | }
50 |
51 | /**
52 | * Create a new user instance after a valid registration.
53 | *
54 | * @param array $data
55 | * @return User
56 | */
57 | protected function create(array $data)
58 | {
59 | return User::create([
60 | 'name' => $data['name'],
61 | 'email' => $data['email'],
62 | 'password' => bcrypt($data['password']),
63 | ]);
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Api/UserController.php:
--------------------------------------------------------------------------------
1 | update($request->validated());
15 |
16 | return response()->json([
17 | 'user' => $user
18 | ], 201);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ConfirmPasswordController.php:
--------------------------------------------------------------------------------
1 | middleware('auth');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ForgotPasswordController.php:
--------------------------------------------------------------------------------
1 | middleware('guest')->except('logout');
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/RegisterController.php:
--------------------------------------------------------------------------------
1 | middleware('guest');
42 | }
43 |
44 | /**
45 | * Get a validator for an incoming registration request.
46 | *
47 | * @param array $data
48 | * @return \Illuminate\Contracts\Validation\Validator
49 | */
50 | protected function validator(array $data)
51 | {
52 | return Validator::make($data, [
53 | 'name' => ['required', 'string', 'max:255'],
54 | 'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
55 | 'password' => ['required', 'string', 'min:8', 'confirmed'],
56 | ]);
57 | }
58 |
59 | /**
60 | * Create a new user instance after a valid registration.
61 | *
62 | * @param array $data
63 | * @return \App\Models\User
64 | */
65 | protected function create(array $data)
66 | {
67 | return User::create([
68 | 'name' => $data['name'],
69 | 'email' => $data['email'],
70 | 'password' => Hash::make($data['password']),
71 | ]);
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Auth/ResetPasswordController.php:
--------------------------------------------------------------------------------
1 | middleware('auth');
39 | $this->middleware('signed')->only('verify');
40 | $this->middleware('throttle:6,1')->only('verify', 'resend');
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/app/Http/Controllers/Controller.php:
--------------------------------------------------------------------------------
1 | [
30 | \App\Http\Middleware\EncryptCookies::class,
31 | \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
32 | \Illuminate\Session\Middleware\StartSession::class,
33 | // \Illuminate\Session\Middleware\AuthenticateSession::class,
34 | \Illuminate\View\Middleware\ShareErrorsFromSession::class,
35 | \App\Http\Middleware\VerifyCsrfToken::class,
36 | \Illuminate\Routing\Middleware\SubstituteBindings::class,
37 | ],
38 |
39 | 'api' => [
40 | \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
41 | 'throttle:api',
42 | 'bindings',
43 | ],
44 | ];
45 |
46 | /**
47 | * The application's route middleware.
48 | *
49 | * These middleware may be assigned to groups or used individually.
50 | *
51 | * @var array
52 | */
53 | protected $routeMiddleware = [
54 | 'auth' => \Illuminate\Auth\Middleware\Authenticate::class,
55 | 'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
56 | 'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
57 | 'can' => \Illuminate\Auth\Middleware\Authorize::class,
58 | 'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
59 | 'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
60 | ];
61 | }
62 |
--------------------------------------------------------------------------------
/app/Http/Middleware/EncryptCookies.php:
--------------------------------------------------------------------------------
1 | check()) {
21 | return redirect('/home');
22 | }
23 |
24 | return $next($request);
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/app/Http/Middleware/TrimStrings.php:
--------------------------------------------------------------------------------
1 | '',
28 | 'title' => 'required|min:3',
29 | 'description' => 'required|min:10',
30 | 'content' => 'required|min:10',
31 | 'published' => 'nullable|boolean',
32 | 'published_at' => 'nullable|date',
33 | ];
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/Http/Requests/UserRequest.php:
--------------------------------------------------------------------------------
1 | 'required|min:3',
30 | 'email' => [
31 | 'required',
32 | Rule::unique('users')->ignore(Auth::user()->id)
33 | ],
34 | 'phone' => 'nullable|min:8|numeric',
35 | 'about' => 'nullable|min:10|max:1024'
36 | ];
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/app/Models/Article.php:
--------------------------------------------------------------------------------
1 | 'boolean',
42 | ];
43 |
44 | protected static function newFactory(): ArticleFactory
45 | {
46 | return ArticleFactory::new();
47 | }
48 |
49 | /**
50 | * Load all for admin and paginate
51 | *
52 | * @return Paginator
53 | */
54 | public static function loadAll(): Paginator
55 | {
56 | return static::latest()
57 | ->paginate();
58 | }
59 |
60 | /**
61 | * Load all for logged in user and paginate
62 | *
63 | * @param $user_id
64 | *
65 | * @return Paginator
66 | */
67 | public static function loadAllMine(int $user_id): Paginator
68 | {
69 | return static::latest()
70 | ->mine($user_id)
71 | ->paginate();
72 | }
73 |
74 | /**
75 | * load all published with pagination
76 | *
77 | * @return Paginator
78 | */
79 | public static function loadAllPublished(): Paginator
80 | {
81 | return static::with([
82 | 'user' => function (BelongsTo $query) {
83 | $query->select('id', 'name');
84 | },
85 | ])
86 | ->latest()
87 | ->published()
88 | ->paginate();
89 | }
90 |
91 | /**
92 | * load one published
93 | *
94 | * @param string $slug
95 | *
96 | * @return Article
97 | */
98 | public static function loadPublished(string $slug): Article
99 | {
100 | return static::with([
101 | 'user' => function (BelongsTo $query) {
102 | $query->select('id', 'name');
103 | },
104 | ])
105 | ->published()
106 | ->where('slug', $slug)
107 | ->firstOrFail();
108 | }
109 |
110 | /**
111 | * Add query scope to get only published articles
112 | *
113 | * @param Builder $query
114 | *
115 | * @return Builder
116 | */
117 | public function scopePublished(Builder $query): Builder
118 | {
119 | return $query->where([
120 | 'published' => true,
121 | ]);
122 | }
123 |
124 | /**
125 | * Load only articles related with the user id
126 | *
127 | * @param Builder $query
128 | * @param int $user_id
129 | *
130 | * @return Builder
131 | */
132 | public function scopeMine(Builder $query, int $user_id): Builder
133 | {
134 | return $query->where('user_id', $user_id);
135 | }
136 |
137 | /**
138 | * Relationship between articles and user
139 | *
140 | * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
141 | */
142 | public function user(): BelongsTo
143 | {
144 | return $this->belongsTo(User::class);
145 | }
146 | }
147 |
--------------------------------------------------------------------------------
/app/Models/User.php:
--------------------------------------------------------------------------------
1 | 'boolean',
44 | ];
45 |
46 | protected static function newFactory(): UserFactory
47 | {
48 | return UserFactory::new();
49 | }
50 |
51 | /**
52 | * @return HasMany
53 | */
54 | public function articles(): HasMany
55 | {
56 | return $this->hasMany(Article::class);
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/Providers/AppServiceProvider.php:
--------------------------------------------------------------------------------
1 | 'App\Policies\ModelPolicy',
16 | ];
17 |
18 | /**
19 | * Register any authentication / authorization services.
20 | *
21 | * @return void
22 | */
23 | public function boot()
24 | {
25 | $this->registerPolicies();
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/app/Providers/BroadcastServiceProvider.php:
--------------------------------------------------------------------------------
1 | [
17 | 'App\Listeners\EventListener',
18 | ],
19 | ];
20 |
21 | /**
22 | * Register any events for your application.
23 | *
24 | * @return void
25 | */
26 | public function boot()
27 | {
28 | parent::boot();
29 |
30 | //
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/Providers/RouteServiceProvider.php:
--------------------------------------------------------------------------------
1 | mapApiRoutes();
48 |
49 | $this->mapWebRoutes();
50 |
51 | //
52 | }
53 |
54 | /**
55 | * Define the "web" routes for the application.
56 | *
57 | * These routes all receive session state, CSRF protection, etc.
58 | *
59 | * @return void
60 | */
61 | protected function mapWebRoutes()
62 | {
63 | Route::middleware('web')
64 | ->namespace($this->namespace)
65 | ->group(base_path('routes/web.php'));
66 | }
67 |
68 | /**
69 | * Define the "api" routes for the application.
70 | *
71 | * These routes are typically stateless.
72 | *
73 | * @return void
74 | */
75 | protected function mapApiRoutes()
76 | {
77 | Route::prefix('api')
78 | ->middleware('api')
79 | ->namespace($this->namespace . '\\Api')
80 | ->group(base_path('routes/api.php'));
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/artisan:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env php
2 | make(Illuminate\Contracts\Console\Kernel::class);
32 |
33 | $status = $kernel->handle(
34 | $input = new Symfony\Component\Console\Input\ArgvInput,
35 | new Symfony\Component\Console\Output\ConsoleOutput
36 | );
37 |
38 | /*
39 | |--------------------------------------------------------------------------
40 | | Shutdown The Application
41 | |--------------------------------------------------------------------------
42 | |
43 | | Once Artisan has finished running. We will fire off the shutdown events
44 | | so that any final work may be done by the application before we shut
45 | | down the process. This is the last thing to happen to the request.
46 | |
47 | */
48 |
49 | $kernel->terminate($input, $status);
50 |
51 | exit($status);
52 |
--------------------------------------------------------------------------------
/bootstrap/app.php:
--------------------------------------------------------------------------------
1 | singleton(
30 | Illuminate\Contracts\Http\Kernel::class,
31 | App\Http\Kernel::class
32 | );
33 |
34 | $app->singleton(
35 | Illuminate\Contracts\Console\Kernel::class,
36 | App\Console\Kernel::class
37 | );
38 |
39 | $app->singleton(
40 | Illuminate\Contracts\Debug\ExceptionHandler::class,
41 | App\Exceptions\Handler::class
42 | );
43 |
44 | /*
45 | |--------------------------------------------------------------------------
46 | | Return The Application
47 | |--------------------------------------------------------------------------
48 | |
49 | | This script returns the application instance. The instance is given to
50 | | the calling script so we can separate the building of the instances
51 | | from the actual running of the application and sending responses.
52 | |
53 | */
54 |
55 | return $app;
56 |
--------------------------------------------------------------------------------
/bootstrap/autoload.php:
--------------------------------------------------------------------------------
1 | env('API_VERSION', 'v1'),
6 | ];
7 |
--------------------------------------------------------------------------------
/config/auth.php:
--------------------------------------------------------------------------------
1 | [
17 | 'guard' => 'web',
18 | 'passwords' => 'users',
19 | ],
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Authentication Guards
24 | |--------------------------------------------------------------------------
25 | |
26 | | Next, you may define every authentication guard for your application.
27 | | Of course, a great default configuration has been defined for you
28 | | here which uses session storage and the Eloquent user provider.
29 | |
30 | | All authentication drivers have a user provider. This defines how the
31 | | users are actually retrieved out of your database or other storage
32 | | mechanisms used by this application to persist your user's data.
33 | |
34 | | Supported: "session", "token"
35 | |
36 | */
37 |
38 | 'guards' => [
39 | 'web' => [
40 | 'driver' => 'session',
41 | 'provider' => 'users',
42 | ],
43 |
44 | 'api' => [
45 | 'driver' => 'token',
46 | 'provider' => 'users',
47 | 'hash' => false,
48 | ],
49 | ],
50 |
51 | /*
52 | |--------------------------------------------------------------------------
53 | | User Providers
54 | |--------------------------------------------------------------------------
55 | |
56 | | All authentication drivers have a user provider. This defines how the
57 | | users are actually retrieved out of your database or other storage
58 | | mechanisms used by this application to persist your user's data.
59 | |
60 | | If you have multiple user tables or models you may configure multiple
61 | | sources which represent each model / table. These sources may then
62 | | be assigned to any extra authentication guards you have defined.
63 | |
64 | | Supported: "database", "eloquent"
65 | |
66 | */
67 |
68 | 'providers' => [
69 | 'users' => [
70 | 'driver' => 'eloquent',
71 | 'model' => App\Models\User::class,
72 | ],
73 |
74 | // 'users' => [
75 | // 'driver' => 'database',
76 | // 'table' => 'users',
77 | // ],
78 | ],
79 |
80 | /*
81 | |--------------------------------------------------------------------------
82 | | Resetting Passwords
83 | |--------------------------------------------------------------------------
84 | |
85 | | You may specify multiple password reset configurations if you have more
86 | | than one user table or model in the application and you want to have
87 | | separate password reset settings based on the specific user types.
88 | |
89 | | The expire time is the number of minutes that the reset token should be
90 | | considered valid. This security feature keeps tokens short-lived so
91 | | they have less time to be guessed. You may change this as needed.
92 | |
93 | */
94 |
95 | 'passwords' => [
96 | 'users' => [
97 | 'provider' => 'users',
98 | 'table' => 'password_resets',
99 | 'expire' => 60,
100 | 'throttle' => 60,
101 | ],
102 | ],
103 |
104 | /*
105 | |--------------------------------------------------------------------------
106 | | Password Confirmation Timeout
107 | |--------------------------------------------------------------------------
108 | |
109 | | Here you may define the amount of seconds before a password confirmation
110 | | times out and the user is prompted to re-enter their password via the
111 | | confirmation screen. By default, the timeout lasts for three hours.
112 | |
113 | */
114 |
115 | 'password_timeout' => 10800,
116 |
117 | ];
118 |
--------------------------------------------------------------------------------
/config/broadcasting.php:
--------------------------------------------------------------------------------
1 | env('BROADCAST_DRIVER', 'null'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Broadcast Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the broadcast connections that will be used
26 | | to broadcast events to other systems or over websockets. Samples of
27 | | each available type of connection are provided inside this array.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'pusher' => [
34 | 'driver' => 'pusher',
35 | 'key' => env('PUSHER_APP_KEY'),
36 | 'secret' => env('PUSHER_APP_SECRET'),
37 | 'app_id' => env('PUSHER_APP_ID'),
38 | 'options' => [
39 | 'cluster' => env('PUSHER_APP_CLUSTER'),
40 | 'useTLS' => true,
41 | ],
42 | ],
43 |
44 | 'ably' => [
45 | 'driver' => 'ably',
46 | 'key' => env('ABLY_KEY'),
47 | ],
48 |
49 | 'redis' => [
50 | 'driver' => 'redis',
51 | 'connection' => 'default',
52 | ],
53 |
54 | 'log' => [
55 | 'driver' => 'log',
56 | ],
57 |
58 | 'null' => [
59 | 'driver' => 'null',
60 | ],
61 |
62 | ],
63 |
64 | ];
65 |
--------------------------------------------------------------------------------
/config/cache.php:
--------------------------------------------------------------------------------
1 | env('CACHE_DRIVER', 'file'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Cache Stores
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may define all of the cache "stores" for your application as
26 | | well as their drivers. You may even define multiple stores for the
27 | | same cache driver to group types of items stored in your caches.
28 | |
29 | | Supported drivers: "apc", "array", "database", "file",
30 | | "memcached", "redis", "dynamodb", "null"
31 | |
32 | */
33 |
34 | 'stores' => [
35 |
36 | 'apc' => [
37 | 'driver' => 'apc',
38 | ],
39 |
40 | 'array' => [
41 | 'driver' => 'array',
42 | 'serialize' => false,
43 | ],
44 |
45 | 'database' => [
46 | 'driver' => 'database',
47 | 'table' => 'cache',
48 | 'connection' => null,
49 | ],
50 |
51 | 'file' => [
52 | 'driver' => 'file',
53 | 'path' => storage_path('framework/cache/data'),
54 | ],
55 |
56 | 'memcached' => [
57 | 'driver' => 'memcached',
58 | 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
59 | 'sasl' => [
60 | env('MEMCACHED_USERNAME'),
61 | env('MEMCACHED_PASSWORD'),
62 | ],
63 | 'options' => [
64 | // Memcached::OPT_CONNECT_TIMEOUT => 2000,
65 | ],
66 | 'servers' => [
67 | [
68 | 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
69 | 'port' => env('MEMCACHED_PORT', 11211),
70 | 'weight' => 100,
71 | ],
72 | ],
73 | ],
74 |
75 | 'redis' => [
76 | 'driver' => 'redis',
77 | 'connection' => 'cache',
78 | ],
79 |
80 | 'dynamodb' => [
81 | 'driver' => 'dynamodb',
82 | 'key' => env('AWS_ACCESS_KEY_ID'),
83 | 'secret' => env('AWS_SECRET_ACCESS_KEY'),
84 | 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
85 | 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
86 | 'endpoint' => env('DYNAMODB_ENDPOINT'),
87 | ],
88 |
89 | ],
90 |
91 | /*
92 | |--------------------------------------------------------------------------
93 | | Cache Key Prefix
94 | |--------------------------------------------------------------------------
95 | |
96 | | When utilizing a RAM based store such as APC or Memcached, there might
97 | | be other applications utilizing the same cache. So, we'll specify a
98 | | value to get prefixed to all our keys so we can avoid collisions.
99 | |
100 | */
101 |
102 | 'prefix' => env('CACHE_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_cache'),
103 |
104 | ];
105 |
--------------------------------------------------------------------------------
/config/cors.php:
--------------------------------------------------------------------------------
1 | ['api/*', 'sanctum/csrf-cookie'],
19 |
20 | 'allowed_methods' => ['*'],
21 |
22 | 'allowed_origins' => ['*'],
23 |
24 | 'allowed_origins_patterns' => [],
25 |
26 | 'allowed_headers' => ['*'],
27 |
28 | 'exposed_headers' => [],
29 |
30 | 'max_age' => 0,
31 |
32 | 'supports_credentials' => false,
33 |
34 | ];
35 |
--------------------------------------------------------------------------------
/config/filesystems.php:
--------------------------------------------------------------------------------
1 | 'local',
17 |
18 | /*
19 | |--------------------------------------------------------------------------
20 | | Default Cloud Filesystem Disk
21 | |--------------------------------------------------------------------------
22 | |
23 | | Many applications store files both locally and in the cloud. For this
24 | | reason, you may specify a default "cloud" driver here. This driver
25 | | will be bound as the Cloud disk implementation in the container.
26 | |
27 | */
28 |
29 | 'cloud' => 's3',
30 |
31 | /*
32 | |--------------------------------------------------------------------------
33 | | Filesystem Disks
34 | |--------------------------------------------------------------------------
35 | |
36 | | Here you may configure as many filesystem "disks" as you wish, and you
37 | | may even configure multiple disks of the same driver. Defaults have
38 | | been setup for each driver as an example of the required options.
39 | |
40 | | Supported Drivers: "local", "ftp", "s3", "rackspace"
41 | |
42 | */
43 |
44 | 'disks' => [
45 |
46 | 'local' => [
47 | 'driver' => 'local',
48 | 'root' => storage_path('app'),
49 | ],
50 |
51 | 'public' => [
52 | 'driver' => 'local',
53 | 'root' => storage_path('app/public'),
54 | 'url' => env('APP_URL').'/storage',
55 | 'visibility' => 'public',
56 | ],
57 |
58 | 's3' => [
59 | 'driver' => 's3',
60 | 'key' => env('AWS_KEY'),
61 | 'secret' => env('AWS_SECRET'),
62 | 'region' => env('AWS_REGION'),
63 | 'bucket' => env('AWS_BUCKET'),
64 | ],
65 |
66 | ],
67 |
68 | ];
69 |
--------------------------------------------------------------------------------
/config/queue.php:
--------------------------------------------------------------------------------
1 | env('QUEUE_DRIVER', 'sync'),
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Queue Connections
23 | |--------------------------------------------------------------------------
24 | |
25 | | Here you may configure the connection information for each server that
26 | | is used by your application. A default configuration has been added
27 | | for each back-end shipped with Laravel. You are free to add more.
28 | |
29 | */
30 |
31 | 'connections' => [
32 |
33 | 'sync' => [
34 | 'driver' => 'sync',
35 | ],
36 |
37 | 'database' => [
38 | 'driver' => 'database',
39 | 'table' => 'jobs',
40 | 'queue' => 'default',
41 | 'retry_after' => 90,
42 | ],
43 |
44 | 'beanstalkd' => [
45 | 'driver' => 'beanstalkd',
46 | 'host' => 'localhost',
47 | 'queue' => 'default',
48 | 'retry_after' => 90,
49 | ],
50 |
51 | 'sqs' => [
52 | 'driver' => 'sqs',
53 | 'key' => 'your-public-key',
54 | 'secret' => 'your-secret-key',
55 | 'prefix' => 'https://sqs.us-east-1.amazonaws.com/your-account-id',
56 | 'queue' => 'your-queue-name',
57 | 'region' => 'us-east-1',
58 | ],
59 |
60 | 'redis' => [
61 | 'driver' => 'redis',
62 | 'connection' => 'default',
63 | 'queue' => 'default',
64 | 'retry_after' => 90,
65 | ],
66 |
67 | ],
68 |
69 | /*
70 | |--------------------------------------------------------------------------
71 | | Failed Queue Jobs
72 | |--------------------------------------------------------------------------
73 | |
74 | | These options configure the behavior of failed queue job logging so you
75 | | can control which database and table are used to store the jobs that
76 | | have failed. You may change them to any database / table you wish.
77 | |
78 | */
79 |
80 | 'failed' => [
81 | 'database' => env('DB_CONNECTION', 'mysql'),
82 | 'table' => 'failed_jobs',
83 | ],
84 |
85 | ];
86 |
--------------------------------------------------------------------------------
/config/sanctum.php:
--------------------------------------------------------------------------------
1 | explode(',', env(
17 | 'SANCTUM_STATEFUL_DOMAINS',
18 | 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1'
19 | )),
20 |
21 | /*
22 | |--------------------------------------------------------------------------
23 | | Expiration Minutes
24 | |--------------------------------------------------------------------------
25 | |
26 | | This value controls the number of minutes until an issued token will be
27 | | considered expired. If this value is null, personal access tokens do
28 | | not expire. This won't tweak the lifetime of first-party sessions.
29 | |
30 | */
31 |
32 | 'expiration' => null,
33 |
34 | /*
35 | |--------------------------------------------------------------------------
36 | | Sanctum Middleware
37 | |--------------------------------------------------------------------------
38 | |
39 | | When authenticating your first-party SPA with Sanctum you may need to
40 | | customize some of the middleware Sanctum uses while processing the
41 | | request. You may change the middleware listed below as required.
42 | |
43 | */
44 |
45 | 'middleware' => [
46 | 'verify_csrf_token' => App\Http\Middleware\VerifyCsrfToken::class,
47 | 'encrypt_cookies' => App\Http\Middleware\EncryptCookies::class,
48 | ],
49 |
50 | ];
51 |
--------------------------------------------------------------------------------
/config/services.php:
--------------------------------------------------------------------------------
1 | [
18 | 'domain' => env('MAILGUN_DOMAIN'),
19 | 'secret' => env('MAILGUN_SECRET'),
20 | ],
21 |
22 | 'ses' => [
23 | 'key' => env('SES_KEY'),
24 | 'secret' => env('SES_SECRET'),
25 | 'region' => 'us-east-1',
26 | ],
27 |
28 | 'sparkpost' => [
29 | 'secret' => env('SPARKPOST_SECRET'),
30 | ],
31 |
32 | 'stripe' => [
33 | 'model' => App\Models\User::class,
34 | 'key' => env('STRIPE_KEY'),
35 | 'secret' => env('STRIPE_SECRET'),
36 | ],
37 |
38 | ];
39 |
--------------------------------------------------------------------------------
/config/view.php:
--------------------------------------------------------------------------------
1 | [
17 | realpath(base_path('resources/views')),
18 | ],
19 |
20 | /*
21 | |--------------------------------------------------------------------------
22 | | Compiled View Path
23 | |--------------------------------------------------------------------------
24 | |
25 | | This option determines where all the compiled Blade templates will be
26 | | stored for your application. Typically, this is within the storage
27 | | directory. However, as usual, you are free to change this value.
28 | |
29 | */
30 |
31 | 'compiled' => realpath(storage_path('framework/views')),
32 |
33 | ];
34 |
--------------------------------------------------------------------------------
/database/.gitignore:
--------------------------------------------------------------------------------
1 | *.sqlite
2 |
--------------------------------------------------------------------------------
/database/factories/ArticleFactory.php:
--------------------------------------------------------------------------------
1 | faker->sentence;
20 |
21 | return [
22 | 'user_id' => User::factory(),
23 | 'title' => $title,
24 | 'slug' => Str::slug($title),
25 | 'description' => $this->faker->sentence(15),
26 | 'content' => implode(' ', $this->faker->paragraphs(2)),
27 | 'published' => true,
28 | 'published_at' => Carbon::now(),
29 | ];
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/database/factories/UserFactory.php:
--------------------------------------------------------------------------------
1 | $this->faker->name,
19 | 'email' => $this->faker->unique()->safeEmail,
20 | 'phone' => $this->faker->phoneNumber,
21 | 'about' => $this->faker->sentence(10),
22 | 'password' => $password = bcrypt('secret'),
23 | 'remember_token' => Str::random(10),
24 | ];
25 | }
26 |
27 | public function isAdmin()
28 | {
29 | return $this->state(function() {
30 | return [
31 | 'is_admin' => true
32 | ];
33 | });
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->string('name');
19 | $table->string('email')->unique();
20 | $table->string('password');
21 | $table->string('phone')->nullable();
22 | $table->string('about')->nullable();
23 | $table->boolean('is_admin')->default(0);
24 | $table->timestamp('email_verified_at')->nullable();
25 | $table->rememberToken();
26 | $table->timestamps();
27 | });
28 | }
29 |
30 | /**
31 | * Reverse the migrations.
32 | *
33 | * @return void
34 | */
35 | public function down()
36 | {
37 | Schema::dropIfExists('users');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_100000_create_password_resets_table.php:
--------------------------------------------------------------------------------
1 | string('email')->index();
18 | $table->string('token');
19 | $table->timestamp('created_at')->nullable();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('password_resets');
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/database/migrations/2017_03_24_122715_create_article_table.php:
--------------------------------------------------------------------------------
1 | increments('id');
18 | $table->unsignedInteger('user_id');
19 | $table->string('title');
20 | $table->string('slug');
21 | $table->text('description');
22 | $table->text('content');
23 | $table->boolean('published')->default(false);
24 | $table->timestamp('published_at')->nullable();
25 | $table->softDeletes();
26 | $table->timestamps();
27 | });
28 | }
29 |
30 | /**
31 | * Reverse the migrations.
32 | *
33 | * @return void
34 | */
35 | public function down()
36 | {
37 | Schema::dropIfExists('articles');
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | bigIncrements('id');
18 | $table->morphs('tokenable');
19 | $table->string('name');
20 | $table->string('token', 64)->unique();
21 | $table->text('abilities')->nullable();
22 | $table->timestamp('last_used_at')->nullable();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('personal_access_tokens');
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call(UsersTableSeeder::class);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/database/seeders/UsersTableSeeder.php:
--------------------------------------------------------------------------------
1 | has(Article::factory(250))
21 | ->create([
22 | 'name' => 'Moeen Basra',
23 | 'email' => 'm.basra@live.com',
24 | 'password' => bcrypt('secret'),
25 | 'is_admin' => true,
26 | 'remember_token' => Str::random(10),
27 | ]);
28 |
29 | User::factory(50)->create();
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: "3.8"
2 | services:
3 | #Laravel Service
4 | lr_app:
5 | container_name: lr_app
6 | restart: always
7 | tty: true
8 | working_dir: /var/www/app
9 | build:
10 | context: .
11 | dockerfile: Dockerfile
12 | environment:
13 | XDEBUG_CONFIG: "idekey=IDE_DEBUG"
14 | PHP_IDE_CONFIG: "serverName=laravel_react_app"
15 | volumes:
16 | - .:/var/www/app
17 | depends_on:
18 | - lr_database
19 | links:
20 | - lr_database
21 | networks:
22 | - lr_network
23 |
24 | #DB Service
25 | lr_database:
26 | image: mariadb:latest
27 | container_name: lr_database
28 | restart: always
29 | working_dir: /etc/mysql
30 | tty: true
31 | environment:
32 | MYSQL_DATABASE: laravel_react
33 | MYSQL_USER: moeen
34 | MYSQL_ROOT_PASSWORD: basra
35 | MYSQL_PASSWORD: basra
36 | ports:
37 | - 3333:3306
38 | volumes:
39 | - ./.docker/db/dump.sql:/docker-entrypoint-initdb.d/dump.sql
40 | command: --default-authentication-plugin=mysql_native_password
41 | networks:
42 | - lr_network
43 |
44 | #Nginx Service
45 | lr_server:
46 | image: nginx:alpine
47 | container_name: lr_server
48 | restart: always
49 | tty: true
50 | ports:
51 | - 8100:8080
52 | volumes:
53 | - .:/var/www/app
54 | - ./.docker/nginx.conf:/etc/nginx/conf.d/default.conf
55 | working_dir: /var/www
56 | depends_on:
57 | - lr_app
58 | networks:
59 | - lr_network
60 |
61 | #Docker Networks
62 | networks:
63 | lr_network:
64 | driver: bridge
65 | volumes:
66 | dbdata:
67 | driver: local
68 |
--------------------------------------------------------------------------------
/license.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Moeen Farooq
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel-react",
3 | "scripts": {
4 | "dev": "npm run development",
5 | "development": "mix",
6 | "watch": "mix watch",
7 | "watch-poll": "mix watch -- --watch-options-poll=1000",
8 | "hot": "mix watch --hot",
9 | "prod": "npm run production",
10 | "production": "mix --production"
11 | },
12 | "author": "Moeen Basra",
13 | "license": "ISC",
14 | "browserslist": [
15 | ">0.2%",
16 | "not dead",
17 | "not ie <= 11",
18 | "not op_mini all"
19 | ],
20 | "dependencies": {
21 | "@popperjs/core": "^2.6.0",
22 | "axios": "^0.21.1",
23 | "bootstrap": "^4.6.0",
24 | "clsx": "^1.1.1",
25 | "font-awesome": "^4.7.0",
26 | "history": "^5.0.0",
27 | "jquery": "^3.5.1",
28 | "lodash": "^4.17.20",
29 | "moment": "^2.29.1",
30 | "prop-types": "^15.7.2",
31 | "react": "^17.0.1",
32 | "react-document-title": "^2.0.3",
33 | "react-dom": "^17.0.1",
34 | "react-loadable": "^5.5.0",
35 | "react-redux": "^7.2.2",
36 | "react-router-dom": "^5.2.0",
37 | "reactstrap": "^8.9.0",
38 | "redux": "^4.0.5",
39 | "redux-thunk": "^2.3.0",
40 | "ree-validate": "^3.3.2"
41 | },
42 | "devDependencies": {
43 | "@babel/plugin-proposal-class-properties": "^7.12.1",
44 | "@babel/preset-env": "^7.12.11",
45 | "@babel/preset-react": "^7.12.10",
46 | "babel-eslint": "^10.1.0",
47 | "babel-plugin-syntax-dynamic-import": "^6.18.0",
48 | "browser-sync": "^2.26.14",
49 | "browser-sync-webpack-plugin": "^2.3.0",
50 | "cross-env": "^7.0.3",
51 | "eslint": "^7.19.0",
52 | "eslint-config-standard": "^16.0.2",
53 | "eslint-friendly-formatter": "^4.0.1",
54 | "eslint-plugin-html": "^6.1.1",
55 | "eslint-plugin-import": "^2.22.1",
56 | "eslint-plugin-node": "^11.1.0",
57 | "eslint-plugin-promise": "^4.2.1",
58 | "eslint-plugin-react": "^7.22.0",
59 | "eslint-webpack-plugin": "^2.4.3",
60 | "laravel-mix": "^6.0.11",
61 | "postcss": "^8.2.4",
62 | "redux-logger": "^3.0.6",
63 | "remove-files-webpack-plugin": "^1.4.4",
64 | "resolve-url-loader": "^3.1.2",
65 | "sass": "^1.32.5",
66 | "sass-loader": "^10.1.1"
67 | }
68 | }
69 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
11 |
12 |
13 | ./tests/Feature
14 |
15 |
16 |
17 | ./tests/Unit
18 |
19 |
20 |
21 |
22 | ./app
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Redirect Trailing Slashes If Not A Folder...
9 | RewriteCond %{REQUEST_FILENAME} !-d
10 | RewriteRule ^(.*)/$ /$1 [L,R=301]
11 |
12 | # Handle Front Controller...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_FILENAME} !-f
15 | RewriteRule ^ index.php [L]
16 |
17 | # Handle Authorization Header
18 | RewriteCond %{HTTP:Authorization} .
19 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
20 |
21 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/moeen-basra/laravel-react/a6c3284cc301c47e68990730b2509f7aa7a3f06d/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 |
10 | /*
11 | |--------------------------------------------------------------------------
12 | | Register The Auto Loader
13 | |--------------------------------------------------------------------------
14 | |
15 | | Composer provides a convenient, automatically generated class loader for
16 | | our application. We just need to utilize it! We'll simply require it
17 | | into the script here so that we don't have to worry about manual
18 | | loading any of our classes later on. It feels nice to relax.
19 | |
20 | */
21 |
22 | require __DIR__.'/../bootstrap/autoload.php';
23 |
24 | /*
25 | |--------------------------------------------------------------------------
26 | | Turn On The Lights
27 | |--------------------------------------------------------------------------
28 | |
29 | | We need to illuminate PHP development, so let us turn on the lights.
30 | | This bootstraps the framework and gets it ready for use, then it
31 | | will load up this application so that we can run it and send
32 | | the responses back to the browser and delight our users.
33 | |
34 | */
35 |
36 | $app = require_once __DIR__.'/../bootstrap/app.php';
37 |
38 | /*
39 | |--------------------------------------------------------------------------
40 | | Run The Application
41 | |--------------------------------------------------------------------------
42 | |
43 | | Once we have the application, we can handle the incoming request
44 | | through the kernel, and send the associated response back to
45 | | the client's browser allowing them to enjoy the creative
46 | | and wonderful application we have prepared for them.
47 | |
48 | */
49 |
50 | $kernel = $app->make(Illuminate\Contracts\Http\Kernel::class);
51 |
52 | $response = $kernel->handle(
53 | $request = Illuminate\Http\Request::capture()
54 | );
55 |
56 | $response->send();
57 |
58 | $kernel->terminate($request, $response);
59 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/public/web.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | /**
2 | * First we will load all of this project's JavaScript dependencies which
3 | * includes React and other helpers. It's a great starting point while
4 | * building robust, powerful web applications using React + Laravel.
5 | */
6 |
7 | require('./bootstrap');
8 |
9 | /**
10 | * Next, we will create a fresh React component instance and attach it to
11 | * the page. Then, you may begin adding components to this application
12 | * or customize the JavaScript scaffolding to fit your unique needs.
13 | */
14 | import React from 'react'
15 | import { render } from 'react-dom'
16 | import { Provider } from 'react-redux'
17 | import store from './store'
18 | import Routes from './routes'
19 |
20 | import { authCheck } from './modules/auth/store/actions'
21 |
22 | store.dispatch(authCheck())
23 |
24 | render((
25 |
26 | ),
27 | document.getElementById('app'),
28 | )
29 |
--------------------------------------------------------------------------------
/resources/js/bootstrap.js:
--------------------------------------------------------------------------------
1 | window._ = require('lodash');
2 |
3 | /**
4 | * We'll load the axios HTTP library which allows us to easily issue requests
5 | * to our Laravel back-end. This library automatically handles sending the
6 | * CSRF token as a header based on the value of the "XSRF" token cookie.
7 | */
8 |
9 | window.axios = require('axios');
10 |
11 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
12 | window.axios.defaults.withCredentials = true;
13 |
14 |
--------------------------------------------------------------------------------
/resources/js/common/articles/listing/components/Article.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | // import components
6 | import { Link } from 'react-router-dom'
7 |
8 | const displayName = 'ArticleComponent'
9 | const propTypes = {
10 | index: PropTypes.number.isRequired,
11 | article: PropTypes.object.isRequired,
12 | }
13 |
14 | // const renderAuthor = (article) => {
15 | // return article.user && `By ${ article.user.name }`
16 | // }
17 |
18 | const renderPublishedAt = (article) => {
19 | return article.publishedAt && `at ${article.publishedAt.format('MMMM D, YYYY')}`
20 | }
21 |
22 | function render ({ article }) {
23 | return
24 |
25 |
26 |
{article.title}
27 |
{renderPublishedAt(article)}
28 |
{ article.description }
29 |
Read More
30 |
31 |
32 |
33 | }
34 |
35 | render.displayName = displayName
36 | render.propTypes = propTypes
37 |
38 | export default render
39 |
--------------------------------------------------------------------------------
/resources/js/common/articles/listing/components/Articles.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from 'react'
2 | import PropTypes from 'prop-types'
3 | import Article from './Article'
4 |
5 | class Articles extends Component {
6 | static displayName = 'Articles'
7 | static propTypes = {
8 | articles: PropTypes.array.isRequired,
9 | dispatch: PropTypes.func.isRequired,
10 | }
11 |
12 | constructor(props) {
13 | super(props)
14 |
15 | this.state = {
16 | //
17 | }
18 | }
19 |
20 | renderArticles() {
21 | return this.props.articles.map((article, index) => {
22 | return
25 | })
26 | }
27 |
28 | render() {
29 | return (
30 |
31 |
32 | { this.props.articles && this.renderArticles() }
33 |
34 |
35 | )
36 | }
37 | }
38 |
39 | export default Articles
40 |
--------------------------------------------------------------------------------
/resources/js/common/articles/listing/index.js:
--------------------------------------------------------------------------------
1 | // libs
2 | import { connect } from 'react-redux'
3 | import Article from '../../../modules/article/Article'
4 |
5 | // components
6 | import Articles from './components/Articles'
7 |
8 | const mapStateToProps = state => {
9 | const {data, ...meta} = state.articles
10 |
11 | return {
12 | articles: data?.map((article) => new Article(article)),
13 | meta: Object.assign({}, meta)
14 | }
15 | }
16 |
17 | export default connect(mapStateToProps)(Articles)
18 |
--------------------------------------------------------------------------------
/resources/js/common/footer/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import moment from "moment";
3 |
4 | const Footer = () => ()
9 |
10 | export default Footer
11 |
--------------------------------------------------------------------------------
/resources/js/common/loader/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | // set display name for component
5 | const displayName = 'CommonLoader'
6 |
7 | // validate component properties
8 | const propTypes = {
9 | isLoading: PropTypes.bool,
10 | error: PropTypes.object,
11 | }
12 |
13 | const LoadingComponent = ({isLoading, error}) => {
14 | // Handle the loading state
15 | if (isLoading) {
16 | return Loading...
17 | }
18 | // Handle the error state
19 | else if (error) {
20 |
21 | // This resolves an issue that newly named code-splitted js files make
22 | if(error['name'] && error['name'] == "ChunkLoadError"){
23 | window.location.reload();
24 | }
25 |
26 | return Sorry, there was a problem loading the page.
27 | }
28 | else {
29 | return null
30 | }
31 | }
32 |
33 | LoadingComponent.displayName = displayName
34 | LoadingComponent.propTypes = propTypes
35 |
36 | export default LoadingComponent
--------------------------------------------------------------------------------
/resources/js/common/navigation/NavItem.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { NavLink } from 'react-router-dom'
4 | import { NavItem } from 'reactstrap'
5 |
6 | const propTypes = {
7 | path: PropTypes.string.isRequired,
8 | children: PropTypes.any,
9 | }
10 |
11 | const contextTypes = {
12 | router: PropTypes.object,
13 | }
14 |
15 | const Link = ({ path, children }) => {
16 | return
17 |
18 | {children}
19 |
20 |
21 | }
22 |
23 | Link.propTypes = propTypes
24 | Link.contextTypes = contextTypes
25 |
26 | export default Link
27 |
--------------------------------------------------------------------------------
/resources/js/common/navigation/PrivateHeader.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 | import { Link } from 'react-router-dom'
5 | // import components
6 | import { Collapse, Dropdown, DropdownItem, DropdownMenu, DropdownToggle } from 'reactstrap'
7 | import NavItem from './NavItem'
8 |
9 | // initiate Component
10 | export default function PrivateHeader({user, showNavigation, showDropdown, toggleDropdown, logout}) {
11 | return (
12 |
13 |
14 | Home
15 | Articles
16 |
17 |
18 |
19 |
20 |
21 | { user.name }
22 |
23 |
24 |
25 | Profile
26 |
27 |
28 | logout(e) }>
29 | Logout
30 |
31 |
32 |
33 |
34 |
35 | );
36 | }
37 |
38 | // bind properties
39 | PrivateHeader.displayName = 'PrivateHeader'
40 | PrivateHeader.propTypes = {
41 | user: PropTypes.object.isRequired,
42 | showNavigation: PropTypes.bool.isRequired,
43 | showDropdown: PropTypes.bool.isRequired,
44 | toggleDropdown: PropTypes.func.isRequired,
45 | logout: PropTypes.func.isRequired,
46 | }
47 |
--------------------------------------------------------------------------------
/resources/js/common/navigation/PublicHeader.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | // import components
6 | import { Collapse } from 'reactstrap'
7 | import NavItem from './NavItem'
8 |
9 | // define component name
10 | const displayName = 'PublicHeader'
11 |
12 | // validate properties
13 | const propTypes = {
14 | showNavigation: PropTypes.bool.isRequired,
15 | }
16 |
17 | // initiate comppnent
18 | const PublicHeader = ({ showNavigation }) => (
19 |
20 |
23 |
24 | Login
25 | Register
26 |
27 | )
28 |
29 | // bind properties
30 | PublicHeader.displayName = displayName
31 | PublicHeader.propTypes = propTypes
32 |
33 | // export component
34 | export default PublicHeader
35 |
--------------------------------------------------------------------------------
/resources/js/common/navigation/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import { connect } from 'react-redux'
5 | import { logout } from '../../modules/auth/service'
6 |
7 | // import components
8 | import { Link } from 'react-router-dom'
9 | import { Navbar, NavbarToggler } from 'reactstrap';
10 | import PrivateHeader from './PrivateHeader'
11 | import PublicHeader from './PublicHeader'
12 |
13 | class Navigation extends Component {
14 | static propTypes = {
15 | isAuthenticated: PropTypes.bool.isRequired,
16 | user: PropTypes.object.isRequired,
17 | dispatch: PropTypes.func.isRequired,
18 | }
19 |
20 | constructor(props) {
21 | super(props)
22 |
23 | this.state = {
24 | showNavigation: false,
25 | showDropdown: false,
26 | }
27 | }
28 |
29 | toggleNavbar = () => {
30 | this.setState({
31 | showNavigation: !this.state.showNavigation,
32 | });
33 | }
34 |
35 | toggleDropdown = () => {
36 | this.setState({
37 | showDropdown: !this.state.showDropdown,
38 | })
39 | }
40 |
41 | logout = e => {
42 | e.preventDefault()
43 |
44 | this.props.dispatch(logout())
45 | }
46 |
47 | render() {
48 | return (
49 |
50 | MOEEN.ME
51 |
52 | {
53 | this.props.isAuthenticated
54 | ?
59 | :
60 | }
61 |
62 | )
63 | }
64 | }
65 |
66 | const mapStateToProps = state => {
67 | return {
68 | isAuthenticated: state.auth.isAuthenticated,
69 | user: state.user
70 | }
71 | }
72 |
73 | export default connect(mapStateToProps)(Navigation)
74 |
--------------------------------------------------------------------------------
/resources/js/common/scroll-top/ScrollTop.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 |
3 | const style = {
4 | float: 'right',
5 | position: 'fixed',
6 | bottom: '1rem',
7 | right: '1rem',
8 | }
9 |
10 | class ScrollTop extends Component {
11 | constructor() {
12 | super()
13 |
14 | this.delayInMs = '16'
15 | this.scrollStepInPx = 50
16 |
17 | this.state = {
18 | intervalId: 0,
19 | showScoller: false,
20 | }
21 |
22 | this.toggleScroll = this.toggleScroll.bind(this)
23 | this.scrollStep = this.scrollStep.bind(this)
24 | }
25 |
26 | componentDidMount() {
27 | window.addEventListener("scroll", this.toggleScroll)
28 | }
29 |
30 | componentWillUnmount() {
31 | window.removeEventListener("scroll", this.toggleScroll)
32 | }
33 |
34 | toggleScroll() {
35 | if (window.pageYOffset > 200) {
36 | this.setState({ showScoller: true })
37 | } else {
38 | this.setState({ showScoller: false })
39 | }
40 | }
41 |
42 | scrollStep() {
43 | if (window.pageYOffset === 0) {
44 | clearInterval(this.state.intervalId)
45 | }
46 | window.scroll(0, window.pageYOffset - this.scrollStepInPx)
47 | }
48 |
49 | scrollToTop(e) {
50 | e.preventDefault()
51 |
52 | let intervalId = setInterval(this.scrollStep, this.delayInMs)
53 | this.setState({ intervalId: intervalId })
54 | }
55 |
56 | render() {
57 | if (this.state.showScoller) {
58 | return ( { this.scrollToTop(e) }}>
61 |
64 | )
65 | }
66 | return null
67 | }
68 | }
69 |
70 | export default ScrollTop
71 |
--------------------------------------------------------------------------------
/resources/js/common/scroll-top/index.js:
--------------------------------------------------------------------------------
1 | import ScrollTop from './ScrollTop'
2 |
3 | export default ScrollTop
--------------------------------------------------------------------------------
/resources/js/layout/Private.js:
--------------------------------------------------------------------------------
1 | //import libs
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | // import components
6 | import Navigation from '../common/navigation/index'
7 | import ScrollTop from '../common/scroll-top/index'
8 | import Footer from '../common/footer/index'
9 |
10 | const containerStyle = {
11 | paddingTop: '3.5rem',
12 | }
13 |
14 | const displayName = 'Private Layout'
15 | const propTypes = {
16 | children: PropTypes.node.isRequired,
17 | }
18 |
19 | function PrivateLayout({ children }) {
20 | return
21 |
22 |
23 | { children }
24 |
25 |
26 |
27 |
28 | }
29 |
30 | PrivateLayout.dispatch = displayName
31 | PrivateLayout.propTypes = propTypes
32 |
33 | export default PrivateLayout
34 |
--------------------------------------------------------------------------------
/resources/js/layout/Public.js:
--------------------------------------------------------------------------------
1 | //import libs
2 | import React from 'react'
3 | import PropTypes from 'prop-types'
4 |
5 | // import components
6 | import Navigation from '../common/navigation/index'
7 | import ScrollTop from '../common/scroll-top/index'
8 | import Footer from '../common/footer/index'
9 |
10 | const containerStyle = {
11 | paddingTop: '3.5rem',
12 | }
13 |
14 | const displayName = 'Public Layout'
15 | const propTypes = {
16 | children: PropTypes.node.isRequired,
17 | }
18 |
19 | function PublicLayout({ children }) {
20 | return
21 |
22 |
23 | { children }
24 |
25 |
26 |
27 |
28 | }
29 |
30 | PublicLayout.dispatch = displayName
31 | PublicLayout.propTypes = propTypes
32 |
33 | export default PublicLayout
34 |
--------------------------------------------------------------------------------
/resources/js/layout/index.js:
--------------------------------------------------------------------------------
1 | //import libs
2 | import React, { useEffect } from 'react'
3 | import PropTypes from 'prop-types'
4 | import { connect } from 'react-redux'
5 | import { withRouter } from 'react-router-dom'
6 |
7 | // import services actions
8 | import { fetchUser } from '../modules/auth/service'
9 |
10 | // import components
11 | import PrivateLayout from './Private'
12 | import PublicLayout from './Public'
13 |
14 | function Layout(props) {
15 |
16 | const { isAuthenticated, user, children, dispatch } = props
17 |
18 | useEffect(() => {
19 | if (isAuthenticated && !user.id) {
20 | dispatch(fetchUser())
21 | }
22 | }, [isAuthenticated])
23 |
24 | if (isAuthenticated) {
25 | return {children}
26 | }
27 | return {children}
28 | }
29 |
30 | Layout.displayName = 'Layout';
31 |
32 | Layout.propTypes = {
33 | isAuthenticated: PropTypes.bool.isRequired,
34 | user: PropTypes.object.isRequired,
35 | children: PropTypes.node.isRequired,
36 | dispatch: PropTypes.func.isRequired,
37 | }
38 |
39 | const mapStateToProps = state => {
40 | return {
41 | isAuthenticated: state.auth.isAuthenticated,
42 | user: state.user,
43 | }
44 | }
45 |
46 | export default withRouter(connect(mapStateToProps)(Layout))
47 |
--------------------------------------------------------------------------------
/resources/js/modules/article/Article.js:
--------------------------------------------------------------------------------
1 | import moment from 'moment'
2 | import Model from '../../utils/Model'
3 | import User from '../user/User'
4 |
5 | class Article extends Model {
6 | constructor(props) {
7 | super(props)
8 |
9 | this.initialize(props)
10 | }
11 |
12 | initialize(props) {
13 | super.initialize(props)
14 |
15 | this.slug = props.slug || ''
16 | this.title = props.title || ''
17 | this.description = props.description || ''
18 | this.content = props.content || ''
19 | this.published = props.published || false
20 | this.publishedAt = props.publishedAt ? moment(props.publishedAt) : null
21 |
22 | // relate user model
23 | this.user = props.user ? new User(props.user) : null
24 | }
25 | }
26 |
27 | export default Article
28 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/add/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 | import { articleAddRequest } from '../../service'
6 | import { Validator } from 'ree-validate'
7 |
8 | // import components
9 | import Form from './components/Form'
10 |
11 | class Page extends Component {
12 | static displayName = 'AddArticle'
13 | static propTypes = {
14 | article: PropTypes.object.isRequired,
15 | dispatch: PropTypes.func.isRequired,
16 | }
17 |
18 | constructor(props) {
19 | super(props)
20 |
21 | this.validator = new Validator({
22 | title: 'required|min:3',
23 | content: 'required|min:10',
24 | description: 'required|min:10',
25 | })
26 |
27 | const article = this.props.article.toJson()
28 |
29 | this.state = {
30 | article,
31 | errors: this.validator.errors
32 | }
33 |
34 | this.handleSubmit = this.handleSubmit.bind(this)
35 | this.handleChange = this.handleChange.bind(this)
36 | }
37 |
38 | UNSAFE_componentWillReceiveProps(nextProps) {
39 | const article = nextProps.article.toJson()
40 |
41 | if (!_.isEqual(this.state.article, article)) {
42 | this.setState({ article })
43 | }
44 |
45 | }
46 |
47 | handleChange(name, value) {
48 | const { errors } = this.validator
49 |
50 | this.setState({ article: { ...this.state.article, [name]: value} })
51 |
52 | errors.remove(name)
53 |
54 | this.validator.validate(name, value)
55 | .then(() => {
56 | this.setState({ errors })
57 | })
58 | }
59 |
60 | handleSubmit(e) {
61 | e.preventDefault()
62 | const article = this.state.article
63 | const { errors } = this.validator
64 |
65 | this.validator.validateAll(article)
66 | .then((success) => {
67 | if (success) {
68 | this.submit(article)
69 | } else {
70 | this.setState({ errors })
71 | }
72 | })
73 | }
74 |
75 | submit(article) {
76 | this.props.dispatch(articleAddRequest(article))
77 | .catch(({ error, statusCode }) => {
78 | const { errors } = this.validator
79 |
80 | if (statusCode === 422) {
81 | _.forOwn(error, (message, field) => {
82 | errors.add(field, message);
83 | });
84 | }
85 |
86 | this.setState({ errors })
87 | })
88 | }
89 |
90 | render() {
91 | return
92 |
Create
93 |
96 |
97 | }
98 | }
99 |
100 | export default Page
101 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/add/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const displayName = 'ArticleFrom'
5 | const propTypes = {
6 | article: PropTypes.object.isRequired,
7 | errors: PropTypes.object.isRequired,
8 | onChange: PropTypes.func.isRequired,
9 | onSubmit: PropTypes.func.isRequired,
10 | }
11 |
12 | const Form = ({ article, errors, onChange, onSubmit }) => {
13 |
14 | function handleChange(name, value) {
15 | if (value !== article[name]) {
16 | onChange(name, value)
17 | }
18 | }
19 |
20 | return
66 | }
67 |
68 | Form.displayName = displayName
69 | Form.propTypes = propTypes
70 |
71 | export default Form
72 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/add/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import Article from '../../Article'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = () => {
8 | const article = new Article({})
9 | return {
10 | article
11 | }
12 | }
13 |
14 | export default connect(mapStateToProps)(Page)
15 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/edit/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 | import { articleEditRequest, articleUpdateRequest } from '../../service'
6 | import { Validator } from 'ree-validate'
7 |
8 | // import components
9 | import Form from './components/Form'
10 |
11 | class Page extends Component {
12 | static displayName = 'EditArticle'
13 | static propTypes = {
14 | match: PropTypes.object.isRequired,
15 | article: PropTypes.object,
16 | dispatch: PropTypes.func.isRequired,
17 | }
18 |
19 | constructor(props) {
20 | super(props)
21 |
22 | this.validator = new Validator({
23 | title: 'required|min:3',
24 | content: 'required|min:10',
25 | description: 'required|min:10',
26 | })
27 |
28 | const article = this.props.article.toJson()
29 |
30 | this.state = {
31 | article,
32 | errors: this.validator.errors
33 | }
34 |
35 | this.handleSubmit = this.handleSubmit.bind(this)
36 | this.handleChange = this.handleChange.bind(this)
37 | }
38 |
39 | UNSAFE_componentWillMount() {
40 | this.loadArticle()
41 | }
42 |
43 | UNSAFE_componentWillReceiveProps(nextProps) {
44 | const article = nextProps.article.toJson()
45 |
46 | if (!_.isEqual(this.state.article, article)) {
47 | this.setState({ article })
48 | }
49 |
50 | }
51 |
52 | loadArticle() {
53 | const { match, article, dispatch } = this.props
54 |
55 | if (!article.id) {
56 | dispatch(articleEditRequest(match.params.id))
57 | }
58 | }
59 |
60 | handleChange(name, value) {
61 | const { errors } = this.validator
62 |
63 | this.setState({ article: { ...this.state.article, [name]: value} })
64 |
65 | errors.remove(name)
66 |
67 | this.validator.validate(name, value)
68 | .then(() => {
69 | this.setState({ errors })
70 | })
71 | }
72 |
73 | handleSubmit(e) {
74 | e.preventDefault()
75 | const article = this.state.article
76 | const { errors } = this.validator
77 |
78 | this.validator.validateAll(article)
79 | .then((success) => {
80 | if (success) {
81 | this.submit(article)
82 | } else {
83 | this.setState({ errors })
84 | }
85 | })
86 | }
87 |
88 | submit(article) {
89 | this.props.dispatch(articleUpdateRequest(article))
90 | .catch(({ error, statusCode }) => {
91 | const { errors } = this.validator
92 |
93 | if (statusCode === 422) {
94 | _.forOwn(error, (message, field) => {
95 | errors.add(field, message);
96 | });
97 | }
98 |
99 | this.setState({ errors })
100 | })
101 | }
102 |
103 | renderForm() {
104 | const { article } = this.props
105 |
106 | if (article.id) {
107 | return
110 | }
111 | }
112 |
113 | render() {
114 | return
115 | Edit
116 | { this.renderForm() }
117 |
118 | }
119 | }
120 |
121 | export default Page
122 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/edit/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const displayName = 'ArticleFrom'
5 | const propTypes = {
6 | article: PropTypes.object.isRequired,
7 | errors: PropTypes.object.isRequired,
8 | onChange: PropTypes.func.isRequired,
9 | onSubmit: PropTypes.func.isRequired,
10 | }
11 |
12 | const Form = ({ article, errors, onChange, onSubmit }) => {
13 |
14 | function handleChange(name, value) {
15 | if (value !== article[name]) {
16 | onChange(name, value)
17 | }
18 | }
19 |
20 | return
66 | }
67 |
68 | Form.displayName = displayName
69 | Form.propTypes = propTypes
70 |
71 | export default Form
72 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/edit/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import Article from '../../Article'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = (state, router) => {
8 | const { params } = router.match
9 | const article = state.articles.data.find(article => article.id === Number(params.id))
10 | return {
11 | article: article ? new Article(article) : new Article({})
12 | }
13 | }
14 |
15 | export default connect(mapStateToProps)(Page)
16 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/list/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import moment from 'moment'
5 | import { articleListRequest, articleUpdateRequest, articleRemoveRequest } from '../../service'
6 |
7 | // import components
8 | import ArticleRow from './components/ArticleRow'
9 | import Pagination from './components/Pagination'
10 | import { Link } from 'react-router-dom'
11 |
12 | class Page extends Component {
13 | static displayName = 'ArticlesPage'
14 | static propTypes = {
15 | meta: PropTypes.object.isRequired,
16 | articles: PropTypes.array.isRequired,
17 | dispatch: PropTypes.func.isRequired,
18 | }
19 |
20 | constructor(props) {
21 | super(props)
22 | }
23 |
24 | componentDidMount() {
25 | const { dispatch } = this.props
26 |
27 | dispatch(articleListRequest({}))
28 | }
29 |
30 | pageChange = (pageNumber) => {
31 | this.props.dispatch(articleListRequest({ pageNumber }))
32 | }
33 |
34 | togglePublish = (id) => {
35 | const article = this.props.articles.find(article => (article.id === id))
36 |
37 | if (!article)
38 | return
39 |
40 | article.published = !article.published
41 | if (article.published) {
42 | article.publishedAt = moment()
43 | } else {
44 | article.publishedAt = null
45 | }
46 |
47 | this.props.dispatch(articleUpdateRequest(article.toJson()))
48 | }
49 |
50 | handleRemove = (id) => {
51 | this.props.dispatch(articleRemoveRequest(id))
52 | }
53 |
54 | renderArticles() {
55 | return this.props.articles.map((article, index) => {
56 | return
61 | })
62 | }
63 |
64 | render() {
65 | return
66 | Articles
67 |
68 |
69 |
70 | # |
71 | Title |
72 | Description |
73 | Created At |
74 | Updated At |
75 | Published At |
76 | Add |
77 |
78 |
79 |
80 | { this.renderArticles() }
81 |
82 |
83 |
84 |
85 | }
86 | }
87 |
88 | export default Page
89 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/list/components/ArticleRow.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const displayName = 'ArticleRow'
6 | const propTypes = {
7 | index: PropTypes.number.isRequired,
8 | article: PropTypes.object.isRequired,
9 | togglePublish: PropTypes.func.isRequired,
10 | handleRemove: PropTypes.func.isRequired,
11 | }
12 |
13 | const ArticleRow = ({ index, article, togglePublish, handleRemove }) => {
14 | return (
15 | {index+1} |
16 | {article.title} |
17 | {article.description} |
18 | {article.createdAt && article.createdAt.format('MMMM, DD YYYY')} |
19 | {article.updatedAt && article.updatedAt.format('MMMM, DD YYYY')} |
20 | {article.publishedAt && article.publishedAt.format('MMMM, DD YYYY')} |
21 |
22 |
23 | {
24 | article.published
25 | ?
26 | :
27 | }
28 | Edit
29 |
30 |
31 | |
32 |
)
33 | }
34 |
35 | ArticleRow.displayName = displayName
36 | ArticleRow.propTypes = propTypes
37 |
38 | export default ArticleRow
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/list/components/Pagination.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | class Pagination extends Component {
5 | static displayName = 'Pagination'
6 | static propTypes = {
7 | meta: PropTypes.object.isRequired,
8 | onChange: PropTypes.func.isRequired,
9 | }
10 |
11 | constructor(props) {
12 | super(props)
13 |
14 | this.state = {
15 | //
16 | }
17 | }
18 |
19 | renderLinks() {
20 | const { meta } = this.props
21 | const range = [...Array(meta.lastPage).keys()]
22 |
23 | return range.map(n => {
24 | const className = meta.currentPage === (n+1) ? 'primary' : 'light'
25 |
26 | return
30 | })
31 | }
32 |
33 | render() {
34 | return
35 |
36 | {this.renderLinks()}
37 |
38 |
39 | }
40 | }
41 |
42 | export default Pagination
43 |
--------------------------------------------------------------------------------
/resources/js/modules/article/pages/list/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from 'react-redux'
3 | import Article from '../../Article'
4 |
5 | // import components
6 | import Page from './Page'
7 |
8 | const mapStateToProps = state => {
9 | const {data, ...meta} = state.articles
10 |
11 | return {
12 | articles: data?.map((article) => new Article(article)),
13 | meta: Object.assign({}, meta)
14 | }
15 | }
16 |
17 | export default connect(mapStateToProps)(Page)
18 |
--------------------------------------------------------------------------------
/resources/js/modules/article/routes.js:
--------------------------------------------------------------------------------
1 | // import lib
2 | import { lazy } from 'react'
3 |
4 | export default [
5 | {
6 | path: '/articles',
7 | exact: true,
8 | auth: true,
9 | component: lazy(() => import('./pages/list')),
10 | },
11 | {
12 | path: '/articles/create',
13 | exact: true,
14 | auth: true,
15 | component: lazy(() => import('./pages/add')),
16 | },
17 | {
18 | path: '/articles/:id/edit',
19 | exact: true,
20 | auth: true,
21 | component: lazy(() => import('./pages/edit')),
22 | },
23 | ]
24 |
--------------------------------------------------------------------------------
/resources/js/modules/article/service.js:
--------------------------------------------------------------------------------
1 | import Http from '../../utils/Http'
2 | import Transformer from '../../utils/Transformer'
3 | import * as articleActions from './store/actions'
4 |
5 | function transformRequest(parms) {
6 | return Transformer.send(parms)
7 | }
8 |
9 | function transformResponse(params) {
10 | return Transformer.fetch(params)
11 | }
12 |
13 | export function articleAddRequest(params) {
14 | return dispatch => (
15 | new Promise((resolve, reject) => {
16 | Http.post('api/v1/articles', transformRequest(params))
17 | .then(res => {
18 | dispatch(articleActions.add(transformResponse(res.data)))
19 | return resolve()
20 | })
21 | .catch((err) => {
22 | const statusCode = err.response.status;
23 | const data = {
24 | error: null,
25 | statusCode,
26 | };
27 |
28 | if (statusCode === 422) {
29 | const resetErrors = {
30 | errors: err.response.data,
31 | replace: false,
32 | searchStr: '',
33 | replaceStr: '',
34 | };
35 | data.error = Transformer.resetValidationFields(resetErrors);
36 | } else if (statusCode === 401) {
37 | data.error = err.response.data.message;
38 | }
39 | return reject(data);
40 | })
41 | })
42 | )
43 | }
44 |
45 | export function articleUpdateRequest(params) {
46 | return dispatch => (
47 | new Promise((resolve, reject) => {
48 | Http.patch(`api/v1/articles/${params.id}`, transformRequest(params))
49 | .then(res => {
50 | dispatch(articleActions.add(transformResponse(res.data)))
51 | return resolve()
52 | })
53 | .catch((err) => {
54 | const statusCode = err.response.status;
55 | const data = {
56 | error: null,
57 | statusCode,
58 | };
59 |
60 | if (statusCode === 422) {
61 | const resetErrors = {
62 | errors: err.response.data,
63 | replace: false,
64 | searchStr: '',
65 | replaceStr: '',
66 | };
67 | data.error = Transformer.resetValidationFields(resetErrors);
68 | } else if (statusCode === 401) {
69 | data.error = err.response.data.message;
70 | }
71 | return reject(data);
72 | })
73 | })
74 | )
75 | }
76 |
77 | export function articleRemoveRequest(id) {
78 | return dispatch => {
79 | Http.delete(`api/v1/articles/${id}`)
80 | .then(() => {
81 | dispatch(articleActions.remove(id))
82 | })
83 | .catch((err) => {
84 | // TODO: handle err
85 | console.error(err.response)
86 | })
87 | }
88 | }
89 |
90 | export function articleListRequest(params) {
91 |
92 | let { pageNumber = 1, url = 'api/v1/articles' } = params
93 |
94 | return dispatch => {
95 | if (pageNumber > 1) {
96 | url = url + `?page=${pageNumber}`
97 | }
98 |
99 | Http.get(url)
100 | .then((res) => {
101 | dispatch(articleActions.list(transformResponse(res.data)))
102 | })
103 | .catch((err) => {
104 | // TODO: handle err
105 | console.error(err.response)
106 | })
107 | }
108 | }
109 |
110 | export function articleEditRequest(id) {
111 | return dispatch => {
112 | Http.get(`api/v1/articles/${id}`)
113 | .then((res) => {
114 | dispatch(articleActions.add(transformResponse(res.data)))
115 | })
116 | .catch((err) => {
117 | // TODO: handle err
118 | console.error(err.response)
119 | })
120 | }
121 | }
122 |
123 | export function articleFetchRequest(slug) {
124 | return dispatch => {
125 | Http.get(`api/v1/articles/published/${slug}`)
126 | .then((res) => {
127 | dispatch(articleActions.add(transformResponse(res.data)))
128 | })
129 | .catch((err) => {
130 | // TODO: handle err
131 | console.error(err.response)
132 | })
133 | }
134 | }
135 |
--------------------------------------------------------------------------------
/resources/js/modules/article/store/action-types.js:
--------------------------------------------------------------------------------
1 | export const ARTICLE_LIST = 'ARTICLE_LIST'
2 | export const ARTICLE_ADD = 'ARTICLE_ADD'
3 | export const ARTICLE_UPDATE = 'ARTICLE_UPDATE'
4 | export const ARTICLE_REMOVE = 'ARTICLE_REMOVE'
5 |
6 | export default {
7 | ARTICLE_LIST,
8 | ARTICLE_ADD,
9 | ARTICLE_UPDATE,
10 | ARTICLE_REMOVE,
11 | }
--------------------------------------------------------------------------------
/resources/js/modules/article/store/actions.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Actions for the article module
3 | * ============
4 | *
5 | * The actions that are available on the
6 | * article module.
7 | */
8 |
9 | import {
10 | ARTICLE_ADD,
11 | ARTICLE_UPDATE,
12 | ARTICLE_REMOVE,
13 | ARTICLE_LIST,
14 | } from './action-types';
15 |
16 | export function add(payload) {
17 | return {
18 | type: ARTICLE_ADD,
19 | payload
20 | }
21 | }
22 |
23 | export function update(payload) {
24 | return {
25 | type: ARTICLE_UPDATE,
26 | payload
27 | }
28 | }
29 |
30 | export function remove(payload) {
31 | return {
32 | type: ARTICLE_REMOVE,
33 | payload
34 | }
35 | }
36 |
37 | export function list(payload) {
38 | return {
39 | type: ARTICLE_LIST,
40 | payload
41 | }
42 | }
--------------------------------------------------------------------------------
/resources/js/modules/article/store/reducer.js:
--------------------------------------------------------------------------------
1 | import {
2 | ARTICLE_ADD,
3 | ARTICLE_UPDATE,
4 | ARTICLE_REMOVE,
5 | ARTICLE_LIST,
6 | } from './action-types'
7 |
8 | const initialState = {
9 | currentPage: 0,
10 | data: [],
11 | from: 0,
12 | lastPage: 0,
13 | nextPageUrl: '',
14 | path: '',
15 | perPage: 0,
16 | prevPageUrl: null,
17 | to: 0,
18 | total: 0,
19 | }
20 |
21 | const reducer = (state = initialState, { type, payload = null }) => {
22 | switch(type) {
23 | case ARTICLE_ADD:
24 | return add(state, payload)
25 | case ARTICLE_UPDATE:
26 | return update(state, payload)
27 | case ARTICLE_REMOVE:
28 | return remove(state, payload)
29 | case ARTICLE_LIST:
30 | return list(state, payload)
31 | default:
32 | return state
33 | }
34 | }
35 |
36 | function add(state, payload) {
37 | const article = state.data.find((article) => (article.id === payload.id))
38 |
39 | if (!article) {
40 | const data = [...state.data, payload]
41 |
42 | return Object.assign({}, state, { data })
43 | }
44 |
45 | return update(state, payload)
46 | }
47 |
48 | function update(state, payload) {
49 | const data = state.data.map(obj => {
50 | if (obj.id === payload.id) {
51 | return { ...obj, ...payload }
52 | }
53 | return obj
54 | })
55 |
56 | return Object.assign({}, state, { data })
57 | }
58 |
59 | function remove(state, id) {
60 | const data = state.data.filter(obj => obj.id !== id)
61 |
62 | return Object.assign({}, state, { data })
63 | }
64 |
65 | function list(state, payload) {
66 | state = Object.assign({}, payload)
67 |
68 | return state
69 | }
70 |
71 | export default reducer
72 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/login/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import $ from 'jquery'
5 | import _ from 'lodash'
6 | import { Redirect } from 'react-router-dom'
7 | import { login } from '../../service'
8 | import { Validator } from 'ree-validate'
9 |
10 | // import components
11 | import Form from './components/Form'
12 |
13 | // initialize component
14 | class Page extends Component {
15 | // set name of the component
16 | static displayName = 'LoginPage'
17 |
18 | // validate props
19 | static propTypes = {
20 | isAuthenticated: PropTypes.bool.isRequired,
21 | dispatch: PropTypes.func.isRequired
22 | }
23 |
24 | constructor(props) {
25 | super(props)
26 |
27 | this.validator = new Validator({
28 | email: 'required|email',
29 | password: 'required|min:6'
30 | })
31 |
32 | // set the state of the app
33 | this.state = {
34 | credentials: {
35 | email: '',
36 | password: '',
37 | remember: false,
38 | },
39 | errors: this.validator.errors
40 | }
41 | }
42 |
43 | // after mounting the component add a style to the body
44 | componentDidMount() {
45 | $('body').attr('style', 'background-color: #eee')
46 | }
47 |
48 | // remove body style before component leaves dom
49 | componentWillUnmount() {
50 | $('body').removeAttr('style')
51 | }
52 |
53 | // event to handle input change
54 | handleChange = (name, value) => {
55 | const { errors } = this.validator
56 |
57 | this.setState({ credentials: { ...this.state.credentials, [name]: value } })
58 |
59 | errors.remove(name)
60 |
61 | this.validator.validate(name, value)
62 | .then(() => {
63 | this.setState({ errors })
64 | })
65 | }
66 |
67 | // event to handle form submit
68 | handleSubmit = e => {
69 | e.preventDefault()
70 | const { credentials } = this.state
71 | const { errors } = this.validator
72 |
73 | this.validator.validateAll(credentials)
74 | .then((success) => {
75 | if (success) {
76 | this.submit(credentials)
77 | } else {
78 | this.setState({ errors })
79 | }
80 | })
81 | }
82 |
83 | submit(credentials) {
84 | this.props.dispatch(login(credentials))
85 | .catch(({ error, statusCode }) => {
86 | const { errors } = this.validator
87 |
88 | if (statusCode === 422) {
89 | _.forOwn(error, (message, field) => {
90 | errors.add(field, message);
91 | });
92 | } else if (statusCode === 401) {
93 | errors.add('password', error);
94 | }
95 |
96 | this.setState({ errors })
97 | })
98 | }
99 |
100 | // render component
101 | render() {
102 |
103 | // check if user is authenticated then redirect him to home page
104 | if (this.props.isAuthenticated) {
105 | return
106 | }
107 | const props = {
108 | email: this.state.credentials.email,
109 | password: this.state.credentials.password,
110 | remember: this.state.credentials.remember,
111 | errors: this.state.errors,
112 | handleChange: this.handleChange,
113 | handleSubmit: this.handleSubmit,
114 | }
115 |
116 | return ()
132 | }
133 | }
134 |
135 | export default Page
136 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/login/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const displayName = 'LoginForm'
6 | const propTypes = {
7 | email: PropTypes.string,
8 | password: PropTypes.string,
9 | remember: PropTypes.bool,
10 | errors: PropTypes.object.isRequired,
11 | handleSubmit: PropTypes.func.isRequired,
12 | handleChange: PropTypes.func.isRequired,
13 | }
14 |
15 | const Form = ({ email, password, remember, errors, handleChange, handleSubmit }) => (
16 |
59 | )
60 |
61 | Form.displayName = displayName
62 | Form.propTypes = propTypes
63 |
64 | export default Form
65 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/login/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from 'react-redux'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = state => {
8 | return {
9 | isAuthenticated: state.auth.isAuthenticated,
10 | }
11 | }
12 |
13 | export default connect(mapStateToProps)(Page)
14 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/password/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import $ from 'jquery'
5 | import { Redirect } from 'react-router-dom'
6 | import { requestPasswordLink } from '../../service'
7 | import { Validator } from 'ree-validate'
8 |
9 | // import components
10 | import Form from './components/Form'
11 |
12 | // initialize component
13 | class Page extends Component {
14 | // set name of the component
15 | static displayName = 'ForgetPasswordPage'
16 |
17 | // validate props
18 | static propTypes = {
19 | isAuthenticated: PropTypes.bool.isRequired,
20 | dispatch: PropTypes.func.isRequired
21 | }
22 |
23 | constructor(props) {
24 | super(props)
25 |
26 | this.validator = new Validator({
27 | email: 'required|email',
28 | })
29 |
30 | // set the state of the app
31 | this.state = {
32 | credentials: {
33 | email: '',
34 | },
35 | message: '',
36 | errors: this.validator.errors
37 | }
38 | }
39 |
40 | // after mounting the component add a style to the body
41 | componentDidMount() {
42 | $('body').attr('style', 'background-color: #eee')
43 | }
44 |
45 | // remove body style before component leaves dom
46 | componentWillUnmount() {
47 | $('body').removeAttr('style')
48 | }
49 |
50 | // event to handle input change
51 | handleChange = (name, value) => {
52 | const { errors } = this.validator
53 |
54 | this.setState({ credentials: { ...this.state.credentials, [name]: value } })
55 |
56 | errors.remove(name)
57 |
58 | this.validator.validate(name, value)
59 | .then(() => {
60 | this.setState({ errors })
61 | })
62 | }
63 |
64 | // event to handle form submit
65 | handleSubmit = e => {
66 | e.preventDefault()
67 | const { credentials } = this.state
68 | const { errors } = this.validator
69 |
70 | this.validator.validateAll(credentials)
71 | .then((success) => {
72 | if (success) {
73 | this.submit(credentials)
74 | } else {
75 | this.setState({ errors })
76 | }
77 | })
78 | }
79 |
80 |
81 | submit(data) {
82 | this.setState({message: ''});
83 | requestPasswordLink(data).then(res => {
84 | this.setState({message: res.data.message})
85 | }).catch(err => {
86 | console.error('Reset Error', err);
87 | })
88 | }
89 |
90 | // render component
91 | render() {
92 |
93 | // check if user is authenticated then redirect him to home page
94 | if (this.props.isAuthenticated) {
95 | return
96 | }
97 | const props = {
98 | email: this.state.credentials.email,
99 | errors: this.state.errors,
100 | handleChange: this.handleChange,
101 | handleSubmit: this.handleSubmit,
102 | }
103 |
104 | return (
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | { this.state.message ?
113 | (
{this.state.message}
)
114 | : null
115 | }
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
)
125 | }
126 | }
127 |
128 | export default Page
129 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/password/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const displayName = 'ForgetPasswordForm'
5 | const propTypes = {
6 | email: PropTypes.string,
7 | password: PropTypes.string,
8 | remember: PropTypes.bool,
9 | errors: PropTypes.object.isRequired,
10 | handleSubmit: PropTypes.func.isRequired,
11 | handleChange: PropTypes.func.isRequired,
12 | }
13 |
14 | const Form = ({ email, errors, handleChange, handleSubmit }) => (
15 |
34 | )
35 |
36 | Form.displayName = displayName
37 | Form.propTypes = propTypes
38 |
39 | export default Form
40 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/password/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from 'react-redux'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = state => {
8 | return {
9 | isAuthenticated: state.auth.isAuthenticated,
10 | }
11 | }
12 |
13 | export default connect(mapStateToProps)(Page)
14 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/register/Page.js:
--------------------------------------------------------------------------------
1 | //import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import $ from 'jquery'
5 | import _ from 'lodash'
6 | import { Redirect } from 'react-router-dom'
7 | import { register } from '../../service'
8 | import { Validator } from 'ree-validate'
9 |
10 | // import components
11 | import Form from './components/Form'
12 |
13 | // initialize component
14 | class Page extends Component {
15 | static displayName = 'RegisterPage'
16 | static propTypes = {
17 | isAuthenticated: PropTypes.bool.isRequired,
18 | dispatch: PropTypes.func.isRequired,
19 | }
20 |
21 | constructor(props) {
22 | super(props)
23 |
24 | this.validator = new Validator({
25 | name: 'required|min:6',
26 | email: 'required|email',
27 | password: 'required|min:6',
28 | passwordConfirmation: 'required|min:6'
29 | })
30 |
31 | this.state = {
32 | credentials: {
33 | name: '',
34 | email: '',
35 | password: '',
36 | passwordConfirmation: '',
37 | },
38 | errors: this.validator.errors,
39 | fields: this.validator.fields
40 | }
41 |
42 | this.handleChange = this.handleChange.bind(this)
43 | this.handleSubmit = this.handleSubmit.bind(this)
44 | }
45 |
46 | componentDidMount() {
47 | $('body').attr('style', 'background-color: #eee')
48 | }
49 |
50 | componentWillUnmount() {
51 | $('body').removeAttr('style')
52 | }
53 |
54 | // event to handle input change
55 | handleChange(name, value) {
56 | const { errors } = this.validator
57 |
58 | this.setState({credentials: { ...this.state.credentials, [name]: value }})
59 | errors.remove(name)
60 |
61 | this.validator.validate(name, value)
62 | .then(() => {
63 | this.setState({ errors })
64 | })
65 | }
66 |
67 | handleSubmit(e) {
68 | e.preventDefault()
69 | const { credentials } = this.state
70 | const { errors } = this.validator
71 |
72 | this.validator.validateAll(credentials)
73 | .then((success) => {
74 | if (success) {
75 | this.submit(credentials)
76 | } else {
77 | this.setState({ errors })
78 | }
79 | })
80 | }
81 |
82 | submit(credentials) {
83 | this.props.dispatch(register(credentials))
84 | .catch(({ error, statusCode }) => {
85 | const { errors } = this.validator
86 |
87 | if (statusCode === 422) {
88 | _.forOwn(error, (message, field) => {
89 | errors.add(field, message);
90 | });
91 | } else if (statusCode === 401) {
92 | errors.add('password', error);
93 | }
94 |
95 | this.setState({ errors })
96 | })
97 | }
98 |
99 | render() {
100 | // check if user is authenticated then redirect him to home page
101 | if (this.props.isAuthenticated) {
102 | return
103 | }
104 |
105 | const { name, email, password, passwordConfirmation } = this.state.credentials
106 | const props = {
107 | name,
108 | email,
109 | password,
110 | passwordConfirmation,
111 | errors: this.state.errors,
112 | handleChange: this.handleChange,
113 | handleSubmit: this.handleSubmit,
114 | }
115 |
116 | return ()
132 | }
133 | }
134 |
135 | export default Page
136 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/register/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Link } from 'react-router-dom'
4 |
5 | const displayName = 'RegisterFrom'
6 |
7 | const propTypes = {
8 | name: PropTypes.string.isRequired,
9 | email: PropTypes.string.isRequired,
10 | password: PropTypes.string.isRequired,
11 | passwordConfirmation: PropTypes.string.isRequired,
12 | errors: PropTypes.object.isRequired,
13 | handleChange: PropTypes.func.isRequired,
14 | handleSubmit: PropTypes.func.isRequired,
15 | }
16 |
17 | const Form = ({ name, email, password, passwordConfirmation, errors, handleChange, handleSubmit }) => {
18 | return ()
75 | }
76 |
77 | Form.displayName = displayName
78 | Form.propTypes = propTypes
79 |
80 | export default Form
81 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/register/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from 'react-redux'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = state => {
8 | return {
9 | isAuthenticated: state.auth.isAuthenticated,
10 | }
11 | }
12 |
13 | export default connect(mapStateToProps)(Page)
14 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/reset-password/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const displayName = 'ResetPasswordForm'
5 | const propTypes = {
6 | password: PropTypes.string,
7 | password_confirmation: PropTypes.string,
8 | errors: PropTypes.object.isRequired,
9 | handleSubmit: PropTypes.func.isRequired,
10 | handleChange: PropTypes.func.isRequired,
11 | }
12 |
13 | const Form = ({ password, password_confirmation, errors, handleChange, handleSubmit }) => (
14 |
44 | )
45 |
46 | Form.displayName = displayName
47 | Form.propTypes = propTypes
48 |
49 | export default Form
50 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/pages/reset-password/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from 'react-redux'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = state => {
8 | return {
9 | isAuthenticated: state.auth.isAuthenticated,
10 | }
11 | }
12 |
13 | export default connect(mapStateToProps)(Page)
14 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/routes.js:
--------------------------------------------------------------------------------
1 | // import lib
2 | import { lazy } from 'react'
3 |
4 | export default [
5 | {
6 | path: '/login',
7 | exact: true,
8 | component: lazy(() => import('./pages/login')),
9 | },
10 | {
11 | path: '/register',
12 | exact: true,
13 | component: lazy(() => import('./pages/register')),
14 | },
15 | {
16 | path: '/forget-password',
17 | exact: true,
18 | component: lazy(() => import('./pages/password')),
19 | },
20 | {
21 | path: '/password/reset',
22 | exact: true,
23 | component: lazy(() => import('./pages/reset-password')),
24 | },
25 | ]
26 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/service.js:
--------------------------------------------------------------------------------
1 | import Http from '../../utils/Http'
2 | import * as authActions from './store/actions'
3 | import Transformer from '../../utils/Transformer'
4 |
5 | /**
6 | * fetch the current logged in user
7 | *
8 | * @returns {function(*)}
9 | */
10 | export function fetchUser() {
11 | return dispatch => {
12 | return Http.get('api/v1/auth/me')
13 | .then(res => {
14 | const data = Transformer.fetch(res.data)
15 | dispatch(authActions.authUser(data))
16 | })
17 | .catch(err => {
18 | console.log(err)
19 | })
20 | }
21 | }
22 |
23 | /**
24 | * login user
25 | *
26 | * @param credentials
27 | * @returns {function(*)}
28 | */
29 | export function login(credentials) {
30 | return dispatch => (
31 | new Promise((resolve, reject) => {
32 | Http.get('sanctum/csrf-cookie')
33 | .then(() => {
34 | Http.post('login', credentials)
35 | .then(res => {
36 | const data = Transformer.fetch(res.data)
37 | dispatch(authActions.authLogin(data.accessToken))
38 | return resolve()
39 | })
40 | .catch((err) => {
41 | const statusCode = err.response.status;
42 | const data = {
43 | error: null,
44 | statusCode,
45 | };
46 |
47 | if (statusCode === 422) {
48 | const resetErrors = {
49 | errors: err.response.data.errors,
50 | replace: false,
51 | searchStr: '',
52 | replaceStr: '',
53 | };
54 | data.error = Transformer.resetValidationFields(resetErrors);
55 | } else if (statusCode === 401) {
56 | data.error = err.response.data.message;
57 | }
58 | return reject(data);
59 | })
60 | })
61 | })
62 | )
63 | }
64 |
65 | export function register(credentials) {
66 | return dispatch => (
67 | new Promise((resolve, reject) => {
68 | Http.post('register', Transformer.send(credentials))
69 | .then(res => {
70 | const data = Transformer.fetch(res.data)
71 | dispatch(authActions.authLogin(data.accessToken))
72 | return resolve()
73 | })
74 | .catch((err) => {
75 | const statusCode = err.response.status;
76 | const data = {
77 | error: null,
78 | statusCode,
79 | };
80 |
81 | if (statusCode === 422) {
82 | const resetErrors = {
83 | errors: err.response.data.errors,
84 | replace: false,
85 | searchStr: '',
86 | replaceStr: '',
87 | };
88 | data.error = Transformer.resetValidationFields(resetErrors);
89 | } else if (statusCode === 401) {
90 | data.error = err.response.data.message;
91 | }
92 | console.log(data)
93 | return reject(data);
94 | })
95 | })
96 | )
97 | }
98 |
99 | /**
100 | * logout user
101 | *
102 | * @returns {function(*)}
103 | */
104 | export function logout() {
105 | return dispatch => {
106 | return Http.post('logout')
107 | .then(() => {
108 | dispatch(authActions.authLogout())
109 | })
110 | .catch(err => {
111 | console.log(err)
112 | })
113 | }
114 | }
115 |
116 |
117 | /**
118 | * Request reset password link
119 | */
120 | export function requestPasswordLink(data){
121 | return Http.post('password/email', data);
122 | }
123 |
124 |
125 | /**
126 | * Request reset password link
127 | */
128 | export function resetPassword(data){
129 | return Http.post('password/reset', data);
130 | }
--------------------------------------------------------------------------------
/resources/js/modules/auth/store/action-types.js:
--------------------------------------------------------------------------------
1 | // auth action types
2 | export const AUTH_CHECK = 'AUTH_CHECK'
3 | export const AUTH_LOGIN = 'AUTH_LOGIN'
4 | export const AUTH_LOGOUT = 'AUTH_LOGOUT'
5 | export const AUTH_REFRESH_TOKEN = 'AUTH_REFRESH_TOKEN'
6 | export const AUTH_RESET_PASSWORD = 'AUTH_RESET_PASSWORD'
7 | export const AUTH_USER = 'AUTH_USER'
8 |
9 | export default {
10 | AUTH_CHECK,
11 | AUTH_LOGIN,
12 | AUTH_LOGOUT,
13 | AUTH_REFRESH_TOKEN,
14 | AUTH_RESET_PASSWORD,
15 | AUTH_USER,
16 | }
--------------------------------------------------------------------------------
/resources/js/modules/auth/store/actions.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Actions for the auth module
3 | * ============
4 | *
5 | * The actions that are available on the
6 | * auth module.
7 | */
8 |
9 | import {
10 | AUTH_CHECK,
11 | AUTH_LOGIN,
12 | AUTH_LOGOUT,
13 | AUTH_REFRESH_TOKEN,
14 | AUTH_RESET_PASSWORD,
15 | AUTH_USER,
16 | } from './action-types';
17 |
18 |
19 | export function authCheck() {
20 | return {
21 | type: AUTH_CHECK,
22 | }
23 | }
24 |
25 | export function authLogin(payload) {
26 | return {
27 | type: AUTH_LOGIN,
28 | payload,
29 | };
30 | }
31 |
32 | export function authLogout() {
33 | return {
34 | type: AUTH_LOGOUT,
35 | }
36 | }
37 |
38 | export function authRefreshToken(payload) {
39 | return {
40 | type: AUTH_REFRESH_TOKEN,
41 | payload
42 | }
43 | }
44 |
45 | export function authResetPassword() {
46 | return {
47 | type: AUTH_RESET_PASSWORD,
48 | }
49 | }
50 |
51 | export function authUser(payload) {
52 | return {
53 | type: AUTH_USER,
54 | payload
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/resources/js/modules/auth/store/reduer.js:
--------------------------------------------------------------------------------
1 | import HTTP from '../../../utils/Http';
2 | import {
3 | AUTH_CHECK,
4 | AUTH_LOGIN,
5 | AUTH_LOGOUT,
6 | AUTH_REFRESH_TOKEN,
7 | AUTH_RESET_PASSWORD,
8 | } from './action-types';
9 |
10 | const initialState = {
11 | isAuthenticated: false,
12 | };
13 |
14 | const reducer = (state = initialState, { type, payload = null }) => {
15 | switch(type) {
16 | case AUTH_REFRESH_TOKEN:
17 | case AUTH_LOGIN:
18 | return login(state, payload);
19 | case AUTH_CHECK:
20 | return checkAuth(state);
21 | case AUTH_LOGOUT:
22 | return logout(state);
23 | case AUTH_RESET_PASSWORD:
24 | return resetPassword(state);
25 | default:
26 | return state;
27 | }
28 | };
29 |
30 | function login(state, payload) {
31 | localStorage.setItem('access_token', payload);
32 | HTTP.defaults.headers.common['Authorization'] = `Bearer ${payload}`;
33 |
34 | return {
35 | ...state, isAuthenticated: true,
36 | }
37 | }
38 |
39 | function checkAuth(state) {
40 | state = Object.assign({}, state, {
41 | isAuthenticated: !!localStorage.getItem('access_token')
42 | })
43 |
44 | if (state.isAuthenticated) {
45 | HTTP.defaults.headers.common['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
46 | }
47 |
48 | return state;
49 | }
50 |
51 | function logout(state) {
52 | localStorage.removeItem('access_token')
53 |
54 | return {
55 | ...state, isAuthenticated: false
56 | }
57 | }
58 |
59 | function resetPassword(state) {
60 | return {
61 | ...state, resetPassword: true,
62 | }
63 | }
64 |
65 | export const getAuth = state => state.auth.isAuthenticated;
66 |
67 | export default reducer;
68 |
--------------------------------------------------------------------------------
/resources/js/modules/user/User.js:
--------------------------------------------------------------------------------
1 | import Model from '../../utils/Model'
2 |
3 | class User extends Model {
4 | constructor(props) {
5 | super(props)
6 |
7 | this.initialize(props)
8 | }
9 |
10 | initialize(props) {
11 | super.initialize(props)
12 |
13 | this.name = props.name || ''
14 | this.email = props.email || ''
15 | this.phone = props.phone || ''
16 | this.about = props.about || ''
17 | }
18 | }
19 |
20 | export default User
21 |
--------------------------------------------------------------------------------
/resources/js/modules/user/pages/edit/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, { Component } from 'react'
3 | import PropTypes from 'prop-types'
4 | import _ from 'lodash'
5 | import { userUpdateRequest } from '../../service'
6 | import { Validator } from 'ree-validate'
7 |
8 | // import components
9 | import Form from './components/Form'
10 |
11 | class Page extends Component {
12 | static displayName = 'UserPage'
13 | static propTypes = {
14 | user: PropTypes.object.isRequired,
15 | dispatch: PropTypes.func.isRequired,
16 | }
17 |
18 | constructor(props) {
19 | super(props)
20 |
21 | this.validator = new Validator({
22 | 'name': 'required|min:3',
23 | 'email': 'required|email',
24 | 'phone': 'min:8|numeric',
25 | 'about': 'min:10|max:1024',
26 | })
27 |
28 | const user = this.props.user.toJson()
29 |
30 | this.state = {
31 | user,
32 | errors: this.validator.errors
33 | }
34 |
35 | this.handleChange = this.handleChange.bind(this)
36 | this.handleSubmit = this.handleSubmit.bind(this)
37 | }
38 |
39 | UNSAFE_componentWillReceiveProps(nextProps) {
40 | const user = nextProps.user.toJson()
41 |
42 | if (!_.isEqual(this.state.user, user)) {
43 | this.setState({ user })
44 | }
45 |
46 | }
47 |
48 | handleChange(name, value) {
49 | const { errors } = this.validator
50 |
51 | this.setState({ user: { ...this.props.user, [name]: value} })
52 |
53 | errors.remove(name)
54 |
55 | this.validator.validate(name, value)
56 | .then(() => {
57 | this.setState({ errors })
58 | })
59 | }
60 |
61 | handleSubmit(e) {
62 | e.preventDefault()
63 | const user = this.state.user
64 | const { errors } = this.validator
65 |
66 | this.validator.validateAll(user)
67 | .then((success) => {
68 | if (success) {
69 | this.submit(user)
70 | } else {
71 | this.setState({ errors })
72 | }
73 | })
74 | }
75 |
76 | submit(user) {
77 | this.props.dispatch(userUpdateRequest(user))
78 | .catch(({ error, statusCode }) => {
79 | const { errors } = this.validator
80 |
81 | if (statusCode === 422) {
82 | _.forOwn(error, (message, field) => {
83 | errors.add(field, message);
84 | });
85 | }
86 |
87 | this.setState({ errors })
88 | })
89 | }
90 |
91 | render() {
92 | return
93 | Profile
94 |
95 |
102 |
103 | }
104 | }
105 |
106 | export default Page
107 |
--------------------------------------------------------------------------------
/resources/js/modules/user/pages/edit/components/Form.js:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import PropTypes from 'prop-types'
3 |
4 | const displayName = 'UserFrom'
5 | const propTypes = {
6 | user: PropTypes.object.isRequired,
7 | errors: PropTypes.object.isRequired,
8 | onChange: PropTypes.func.isRequired,
9 | onSubmit: PropTypes.func.isRequired,
10 | }
11 |
12 | const Form = ({ user, errors, onChange, onSubmit }) => {
13 | return
72 | }
73 |
74 | Form.displayName = displayName
75 | Form.propTypes = propTypes
76 |
77 | export default Form
78 |
--------------------------------------------------------------------------------
/resources/js/modules/user/pages/edit/index.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Container
3 | * ============.
4 | *
5 | * Containers are used fetch the data from state
6 | * and disperse to the components.
7 | */
8 |
9 | // import libs
10 | import { connect } from 'react-redux'
11 | import User from '../../User'
12 |
13 | // import components
14 | import Page from './Page'
15 |
16 | // map store state as properties of the component
17 | const mapStateToProps = state => {
18 | return {
19 | user: new User(state.user)
20 | }
21 | }
22 |
23 | // binding store with component
24 | export default connect(mapStateToProps)(Page)
25 |
--------------------------------------------------------------------------------
/resources/js/modules/user/routes.js:
--------------------------------------------------------------------------------
1 | // import lib
2 | import { lazy } from 'react'
3 |
4 | export default [
5 | {
6 | path: '/users/:id/edit',
7 | exact: true,
8 | auth: true,
9 | component: lazy(() => import('./pages/edit')),
10 | },
11 | ]
12 |
--------------------------------------------------------------------------------
/resources/js/modules/user/service.js:
--------------------------------------------------------------------------------
1 | import Http from '../../utils/Http'
2 | import Transformer from '../../utils/Transformer'
3 | import * as userActions from './store/actions'
4 |
5 | export function userUpdateRequest(params) {
6 | return dispatch => (
7 | new Promise((resolve, reject) => {
8 | Http.patch(`/users/${params.id}`, Transformer.send(params))
9 | .then(res => {
10 | dispatch(userActions.userUpdate(Transformer.fetch(res.data.user)))
11 | return resolve()
12 | })
13 | .catch((err) => {
14 | const statusCode = err.response.status;
15 | const data = {
16 | error: null,
17 | statusCode,
18 | };
19 |
20 | if (statusCode === 422) {
21 | const resetErrors = {
22 | errors: err.response.data,
23 | replace: false,
24 | searchStr: '',
25 | replaceStr: '',
26 | };
27 | data.error = Transformer.resetValidationFields(resetErrors);
28 | } else if (statusCode === 401) {
29 | data.error = err.response.data.message;
30 | }
31 | return reject(data);
32 | })
33 | })
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/resources/js/modules/user/store/action-types.js:
--------------------------------------------------------------------------------
1 | // user action types
2 | export const USER_UPDATE = 'USER_UPDATE'
3 | export const USER_UNSET = 'USER_UNSET'
4 |
5 | export default {
6 | USER_UPDATE,
7 | USER_UNSET,
8 | }
--------------------------------------------------------------------------------
/resources/js/modules/user/store/actions.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Actions for the user module
3 | * ============
4 | *
5 | * The actions that are available on the
6 | * user module.
7 | */
8 |
9 | import {
10 | USER_UPDATE,
11 | USER_UNSET,
12 | } from './action-types';
13 |
14 | export function userUpdate(payload) {
15 | return {
16 | type: USER_UPDATE,
17 | payload,
18 | };
19 | }
20 |
21 | export function unsetUser() {
22 | return {
23 | type: USER_UNSET,
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/resources/js/modules/user/store/reducer.js:
--------------------------------------------------------------------------------
1 | import User from '../User'
2 | import { USER_UPDATE , USER_UNSET } from './action-types'
3 | import { AUTH_LOGOUT, AUTH_USER } from '../../auth/store/action-types'
4 |
5 | const initialState = Object.assign({}, new User({}))
6 |
7 | const reducer = (state = initialState, { type, payload = null }) => {
8 | switch (type) {
9 | case AUTH_USER:
10 | return authUser(state, payload)
11 | case USER_UPDATE:
12 | return updateUser(state, payload);
13 | case AUTH_LOGOUT:
14 | case USER_UNSET:
15 | return unsetUser(state);
16 | default:
17 | return state
18 | }
19 | }
20 |
21 | function updateUser(state, payload) {
22 | return {
23 | ...state, ...payload
24 | }
25 | }
26 |
27 | function unsetUser(state) {
28 | return {
29 | ...state, initialState
30 | }
31 | }
32 |
33 | function authUser(state, user) {
34 | return {
35 | ...state, ...user
36 | }
37 | }
38 |
39 | export default reducer
40 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/blog/details/Page.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React, {useEffect} from 'react'
3 | import PropTypes from 'prop-types'
4 | import DocumentTitle from 'react-document-title';
5 | import {articleFetchRequest} from '../../../../article/service'
6 | import {APP_TITLE} from '../../../../../values'
7 |
8 | export default function Page({match, article, dispatch}) {
9 |
10 | const loadArticle = () => {
11 | if (!article.slug) {
12 | dispatch(articleFetchRequest(match.params.slug))
13 | }
14 | }
15 |
16 | useEffect(() => {
17 | loadArticle()
18 | })
19 |
20 | const renderPublishedDate = () => {
21 | const {publishedAt} = article
22 |
23 | if (publishedAt) {
24 | return `at ${publishedAt.format('MMMM d, YYYY')}`
25 | }
26 | }
27 |
28 | const renderAuthor = () => {
29 | const {user} = article
30 |
31 | if (user) {
32 | return `by ${user.name}`
33 | }
34 | }
35 |
36 | const renderArticle = () => {
37 | return (
38 |
{article.title}
39 |
{renderPublishedDate()} {renderAuthor()}
40 |
{article.description}
41 |
{article.content}
42 |
)
43 | }
44 |
45 | return (
46 |
47 |
48 |
49 |
50 | {renderArticle()}
51 |
52 |
53 |
54 |
55 | )
56 |
57 | }
58 |
59 | Page.displayName = 'ArticleShowPage'
60 | Page.propTypes = {
61 | match: PropTypes.object.isRequired,
62 | article: PropTypes.object.isRequired,
63 | dispatch: PropTypes.func.isRequired
64 | }
65 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/blog/details/index.js:
--------------------------------------------------------------------------------
1 | import { connect } from 'react-redux'
2 | import Article from '../../../../article/Article'
3 |
4 | // import components
5 | import Page from './Page'
6 |
7 | const mapStateToProps = (state, router) => {
8 | const { params } = router.match
9 | const article = state.articles.data.find(article => article.slug === params.slug)
10 | return {
11 | article: article ? new Article(article) : new Article({})
12 | }
13 | }
14 |
15 | export default connect(mapStateToProps)(Page)
16 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/blog/list/Page.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | // import components
5 | import Articles from "../../../../../common/articles/listing"
6 |
7 | // import services
8 | import { articleListRequest } from "../../../../article/service"
9 |
10 | class Page extends Component {
11 | static displayName = "HomePage"
12 | static propTypes = {
13 | dispatch: PropTypes.func.isRequired,
14 | }
15 |
16 | componentDidMount() {
17 | this.props.dispatch(articleListRequest({ url: 'api/v1/articles/published' }))
18 | }
19 |
20 | render() {
21 | return
24 | }
25 | }
26 |
27 | export default Page
28 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/blog/list/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import { connect } from "react-redux"
3 |
4 | // import components
5 | import Page from "./Page"
6 |
7 | export default connect()(Page)
8 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/home/Page.js:
--------------------------------------------------------------------------------
1 | import React, { useLayoutEffect } from "react"
2 | import PropTypes from "prop-types"
3 |
4 | // import components
5 | import Header from "./components/Header"
6 | import Articles from "../../../../common/articles/listing"
7 |
8 | // import services
9 | import { articleListRequest } from '../../../article/service'
10 |
11 | export default function Page({ dispatch }) {
12 | useLayoutEffect(() => {
13 | dispatch(articleListRequest({ url: 'api/v1/articles/published' }))
14 | }, [])
15 |
16 | return
20 | }
21 |
22 | Page.displayName = 'HomePage'
23 | Page.propTypes = {
24 | dispatch: PropTypes.func.isRequired,
25 | }
26 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/home/components/Header.js:
--------------------------------------------------------------------------------
1 | import React from "react"
2 |
3 | export default function Header() {
4 | return
12 | }
13 |
14 | Header.displayName = 'HomePageHeader'
15 |
--------------------------------------------------------------------------------
/resources/js/modules/web/pages/home/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import {connect} from "react-redux"
3 |
4 | // import components
5 | import Page from "./Page"
6 |
7 | export default connect()(Page)
8 |
--------------------------------------------------------------------------------
/resources/js/modules/web/routes.js:
--------------------------------------------------------------------------------
1 | // import lib
2 | import { lazy } from 'react'
3 |
4 | const routes = [
5 | {
6 | path: '/',
7 | exact: true,
8 | component: lazy(() => import('./pages/home')),
9 | },
10 | {
11 | path: '/blog',
12 | exact: true,
13 | component: lazy(() => import('./pages/blog/list')),
14 | },
15 | {
16 | path: '/blog/:slug',
17 | exact: true,
18 | component: lazy(() => import('./pages/blog/details')),
19 | },
20 | ]
21 |
22 | export default routes
23 |
--------------------------------------------------------------------------------
/resources/js/routes/Private.js:
--------------------------------------------------------------------------------
1 | import React, {Suspense} from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Route, Redirect } from 'react-router-dom'
4 | import { connect } from 'react-redux'
5 |
6 | const PrivateRoute = ({ component: Component, isAuthenticated, ...rest }) => {
7 | return {
8 | return Loading...}>
9 | {
10 | isAuthenticated
11 | ?
12 | :
16 | }
17 |
18 | }}/>
19 | }
20 |
21 | PrivateRoute.propTypes = {
22 | component: PropTypes.object.isRequired,
23 | location: PropTypes.object,
24 | isAuthenticated: PropTypes.bool.isRequired,
25 | }
26 |
27 | // Retrieve data from store as props
28 | function mapStateToProps(store) {
29 | return {
30 | isAuthenticated: store.auth.isAuthenticated,
31 | }
32 | }
33 |
34 | export default connect(mapStateToProps)(PrivateRoute)
35 |
--------------------------------------------------------------------------------
/resources/js/routes/Public.js:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from 'react'
2 | import PropTypes from 'prop-types'
3 | import { Route } from 'react-router-dom'
4 |
5 | const PublicRoutes = ({ component: Component, ...rest }) => {
6 | return {
7 | return Loading...}>
8 |
9 |
10 | }}/>
11 | }
12 |
13 | PublicRoutes.propTypes = {
14 | component: PropTypes.object.isRequired,
15 | location: PropTypes.object,
16 | };
17 |
18 | export default PublicRoutes
19 |
--------------------------------------------------------------------------------
/resources/js/routes/index.js:
--------------------------------------------------------------------------------
1 | // import libs
2 | import React from 'react'
3 | import { BrowserRouter as Router, Switch } from 'react-router-dom'
4 |
5 | // import components
6 | import routes from './routes'
7 | import PrivateRoute from './Private'
8 | import PublicRoute from './Public'
9 |
10 | import Layout from '../layout'
11 |
12 | const Routes = () => (
13 |
14 |
15 |
16 | {routes.map((route, i) => {
17 | if (route.auth) {
18 | return
19 | }
20 | return
21 | })}
22 |
23 |
24 |
25 | )
26 |
27 | export default Routes
28 |
--------------------------------------------------------------------------------
/resources/js/routes/routes.js:
--------------------------------------------------------------------------------
1 | // import modular routes
2 | import webRoutes from "../modules/web/routes"
3 | import authRoutes from "../modules/auth/routes"
4 | import userRoutes from "../modules/user/routes"
5 | import articleRoutes from "../modules/article/routes"
6 |
7 | export default [...webRoutes, ...authRoutes, ...userRoutes, ...articleRoutes]
8 |
--------------------------------------------------------------------------------
/resources/js/store/config.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main store function
3 | */
4 | import { createStore, applyMiddleware, compose } from 'redux'
5 | import thunk from 'redux-thunk'
6 | // import { createLogger } from 'redux-logger'
7 | import rootReducer from './reducers'
8 |
9 | export default function (initialState = {}) {
10 | // Middleware and store enhancers
11 | const enhancers = [
12 | applyMiddleware(thunk),
13 | ]
14 |
15 | if (process.env.NODE_ENV !== 'production') {
16 | // enhancers.push(applyMiddleware(createLogger()))
17 | window.__REDUX_DEVTOOLS_EXTENSION__ && enhancers.push(window.__REDUX_DEVTOOLS_EXTENSION__())
18 | }
19 |
20 | const store = createStore(rootReducer, initialState, compose(...enhancers))
21 |
22 | // For hot reloading reducers
23 | if (module.hot) {
24 | // Enable Webpack hot module replacement for reducers
25 | module.hot.accept('./reducers', () => {
26 | const nextReducer = require('./reducers').default // eslint-disable-line global-require
27 | store.replaceReducer(nextReducer)
28 | })
29 | }
30 |
31 | return store
32 | }
33 |
--------------------------------------------------------------------------------
/resources/js/store/index.js:
--------------------------------------------------------------------------------
1 | import storeConfig from './config'
2 |
3 | export default storeConfig()
4 |
--------------------------------------------------------------------------------
/resources/js/store/reducers.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux'
2 |
3 | import auth from '../modules/auth/store/reduer'
4 | import user from '../modules/user/store/reducer'
5 | import articles from '../modules/article/store/reducer'
6 |
7 | export default combineReducers({ auth, user, articles })
8 |
--------------------------------------------------------------------------------
/resources/js/utils/Http.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 | import axios from 'axios'
3 | import store from '../store/index'
4 | import { authLogout } from '../modules/auth/store/actions'
5 |
6 | const API_URL = (process.env.NODE_ENV === 'test') ? process.env.BASE_URL || (`http://localhost:${process.env.PORT}/`) : `/`;
7 |
8 | axios.defaults.baseURL = API_URL;
9 | axios.defaults.headers.common.Accept = 'application/json';
10 | axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
11 |
12 | axios.interceptors.response.use(
13 | response => response,
14 | (error) => {
15 | if (error.response.status === 401) {
16 | store.dispatch(authLogout())
17 | }
18 | return Promise.reject(error);
19 | });
20 |
21 | export default axios
22 |
--------------------------------------------------------------------------------
/resources/js/utils/Model.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Model
3 | * ============
4 | *
5 | * The base model.
6 | *
7 | * Model are used to map the data
8 | * and help in avoiding code repetition
9 | * For instance,
10 | * if we need to get user full name by joining first and last name
11 | * or if we want to manipulate user dates
12 | * we can write a function
13 | */
14 | import moment from 'moment'
15 | import _ from 'lodash'
16 |
17 | class Model {
18 | constructor(props) {
19 | this.initialize(props)
20 | }
21 |
22 | initialize(props) {
23 | this.id = props.id && Number(props.id) || null
24 | this.createdAt = props.createdAt && moment(props.createdAt) || null
25 | this.updatedAt = props.updatedAt && moment(props.updatedAt) || null
26 | this.deletedAt = props.deletedAt && moment(props.deletedAt) || null
27 | }
28 |
29 | toJson() {
30 | const props = Object.assign({}, this)
31 |
32 | _.forOwn(props, (value, key) => {
33 | if (value instanceof moment) {
34 | props[key] = value.format('YYYY-MM-DD HH:mm:ss')
35 | }
36 | })
37 | return props
38 | }
39 | }
40 |
41 | export default Model
42 |
--------------------------------------------------------------------------------
/resources/js/utils/Transformer.js:
--------------------------------------------------------------------------------
1 | /* ============
2 | * Transformer
3 | * ============
4 | *
5 | * The base transformer.
6 | *
7 | * Transformers are used to transform the fetched data
8 | * to a more suitable format.
9 | * For instance, when the fetched data contains snake_cased values,
10 | * they will be camelCased.
11 | */
12 |
13 | import _ from 'lodash';
14 |
15 | export default class Transformer {
16 | /**
17 | * Method used to transform a fetched data
18 | *
19 | * @param param
20 | * @return {*}
21 | */
22 | static fetch(param) {
23 | if (param && Array.isArray(param)) {
24 | return Transformer.fetchCollection(param);
25 | } else if (param && typeof param === 'object') {
26 | return Transformer.fetchObject(param);
27 | }
28 | return param
29 | }
30 |
31 | /**
32 | * Method used to transform a fetched collection
33 | *
34 | * @param param
35 | * @return [Array]
36 | */
37 | static fetchCollection(param) {
38 | return param.map(item => Transformer.fetch(item));
39 | }
40 |
41 | /**
42 | * Method used to transform a fetched object
43 | *
44 | * @param param
45 | * @return {{}}
46 | */
47 | static fetchObject(param) {
48 | const data = {};
49 |
50 | _.forOwn(param, (value, key) => {
51 | data[_.camelCase(key)] = Transformer.fetch(value);
52 | });
53 | return data;
54 | }
55 |
56 | /**
57 | * Method used to transform a send data
58 | *
59 | * @param param
60 | * @return {*}
61 | */
62 | static send(param) {
63 | if (param && Array.isArray(param)) {
64 | return Transformer.sendCollection(param);
65 | } else if (param && typeof param === 'object') {
66 | return Transformer.sendObject(param);
67 | }
68 | return param
69 | }
70 |
71 | /**
72 | * Method used to transform a collection to be send
73 | *
74 | * @param param
75 | * @return [Array]
76 | */
77 | static sendCollection(param) {
78 | return param.map(item => Transformer.send(item));
79 | }
80 |
81 | /**
82 | * Method used to transform a object to be send
83 | *
84 | * @param param
85 | * @returns {{}}
86 | */
87 | static sendObject(param) {
88 | const data = {};
89 |
90 | _.forOwn(param, (value, key) => {
91 | data[_.snakeCase(key)] = Transformer.send(value);
92 | });
93 | return data;
94 | }
95 |
96 | /**
97 | * Method used to transform a form errors
98 | *
99 | * @param errors The fetched data
100 | * @param replace Boolean
101 | * @param searchStr String
102 | * @param replaceStr String
103 | * @returns {{}}
104 | */
105 | static resetValidationFields({ errors, replace = false, searchStr = '', replaceStr = '' }) {
106 | const data = {};
107 | _.forOwn(errors, (value, key) => {
108 | let index = '';
109 | if (replace) {
110 | index = _.camelCase(key.replace(searchStr, replaceStr));
111 | } else {
112 | index = _.camelCase(key);
113 | }
114 | data[index] = _.head(value);
115 | });
116 | return data;
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/resources/js/values/index.js:
--------------------------------------------------------------------------------
1 | const APP_TITLE = 'Laravel React Boiler Plate'
2 |
3 | export {
4 | APP_TITLE,
5 | }
6 |
--------------------------------------------------------------------------------
/resources/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/resources/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/resources/lang/en/passwords.php:
--------------------------------------------------------------------------------
1 | 'Passwords must be at least six characters and match the confirmation.',
17 | 'reset' => 'Your password has been reset!',
18 | 'sent' => 'We have e-mailed your password reset link!',
19 | 'token' => 'This password reset token is invalid.',
20 | 'user' => "We can't find a user with that e-mail address.",
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/resources/sass/_variables.scss:
--------------------------------------------------------------------------------
1 | // Body
2 | $body-bg: #f8fafc;
3 |
4 | // Typography
5 | $font-family-sans-serif: 'Nunito', sans-serif;
6 | $font-size-base: 0.9rem;
7 | $line-height-base: 1.6;
8 |
9 | // Colors
10 | $blue: #3490dc;
11 | $indigo: #6574cd;
12 | $purple: #9561e2;
13 | $pink: #f66d9b;
14 | $red: #e3342f;
15 | $orange: #f6993f;
16 | $yellow: #ffed4a;
17 | $green: #38c172;
18 | $teal: #4dc0b5;
19 | $cyan: #6cb2eb;
20 |
--------------------------------------------------------------------------------
/resources/sass/app.scss:
--------------------------------------------------------------------------------
1 | // Fonts
2 | @import url('https://fonts.googleapis.com/css?family=Nunito');
3 |
4 | // Variables
5 | @import 'variables';
6 |
7 | // Bootstrap
8 | @import '~bootstrap/scss/bootstrap';
9 | @import "~font-awesome/scss/font-awesome";
10 |
--------------------------------------------------------------------------------
/resources/views/index.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{'Laravel 8 with React 17 Boilerplate'}}
8 |
9 |
10 |
11 |
12 |
13 |
14 | @yield('content')
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | "{$api_version}"], function() {
20 | // register auth routes
21 | Route::prefix('auth')
22 | ->group(base_path('routes/api/auth.php'));
23 | // register users routes
24 | Route::prefix('users')
25 | ->group(base_path('routes/api/users.php'));
26 | // register articles routes
27 | Route::prefix('articles')
28 | ->group(base_path('routes/api/articles.php'));
29 | });
30 |
--------------------------------------------------------------------------------
/routes/api/articles.php:
--------------------------------------------------------------------------------
1 | name('articles.published.index');
6 | Route::get('published/{id}', 'ArticleController@publishedArticle')->name('articles.published.show');
7 |
8 | Route::group(['middleware' => 'auth:sanctum'], function() {
9 | Route::post('/', 'ArticleController@store')->name('articles.store');
10 | Route::get('/', 'ArticleController@index')->name('articles.index');
11 | Route::get('/{id}', 'ArticleController@show')->name('articles.show');
12 | Route::match(['put', 'patch'], '/{id}', 'ArticleController@update')->name('articles.update');
13 | Route::delete('/{id}', 'ArticleController@delete')->name('articles.delete');
14 | });
15 |
--------------------------------------------------------------------------------
/routes/api/auth.php:
--------------------------------------------------------------------------------
1 | 'auth:sanctum'], function() {
8 | Route::get('/me', function (Request $request) {
9 | return $request->user();
10 | });
11 | });
12 |
--------------------------------------------------------------------------------
/routes/api/users.php:
--------------------------------------------------------------------------------
1 | 'auth:api'], function() {
6 | Route::match(['put', 'patch'], '/{id}', 'UserController@update')->name('users.update');
7 | });
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
16 | });
17 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
18 | })->describe('Display an inspiring quote');
19 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | name('logout');
19 |
20 | // Registration Routes...
21 | Route::post('register', 'Auth\RegisterController@register');
22 |
23 | // Password Reset Routes...
24 | Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail');
25 | Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.reset');
26 |
27 | Route::get('/{any}', function () {
28 | return view('index');
29 | })->where('any', '.*');
30 |
--------------------------------------------------------------------------------
/server.php:
--------------------------------------------------------------------------------
1 |
8 | */
9 |
10 | $uri = urldecode(
11 | parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
12 | );
13 |
14 | // This file allows us to emulate Apache's "mod_rewrite" functionality from the
15 | // built-in PHP web server. This provides a convenient way to test a Laravel
16 | // application without having installed a "real" web server software here.
17 | if ($uri !== '/' && file_exists(__DIR__.'/public'.$uri)) {
18 | return false;
19 | }
20 |
21 | require_once __DIR__.'/public/index.php';
22 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | config.php
2 | routes.php
3 | schedule-*
4 | compiled.php
5 | services.json
6 | events.scanned.php
7 | routes.scanned.php
8 | down
9 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/sessions/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/testing/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/views/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/logs/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Feature/ArticleTest.php:
--------------------------------------------------------------------------------
1 | user = $this->createAdminUser();
23 | }
24 |
25 | private function createAdminUser()
26 | {
27 | return User::create([
28 | 'name' => 'Moeen Basra',
29 | 'email' => 'm.basra@live.com',
30 | 'password' => bcrypt('secret'),
31 | 'is_admin' => true,
32 | 'remember_token' => Str::random(10),
33 | ]);
34 | }
35 |
36 | /** @test */
37 | public function that_only_loading_articles_for_provided_user_id()
38 | {
39 | $this->seedUnpublishedArticles();
40 |
41 | $articles = Article::loadAllMine($this->user->id);
42 |
43 | $this->assertCount(15, $articles);
44 |
45 | }
46 |
47 | private function seedUnpublishedArticles($num = 15)
48 | {
49 | factory(Article::class, $num)->create([
50 | 'user_id' => $this->user->id,
51 | 'published' => false,
52 | ]);
53 | }
54 |
55 | /** @test */
56 | public function that_load_all_articles()
57 | {
58 | $this->seedUnpublishedArticles();
59 |
60 | $articles = Article::loadAll();
61 |
62 | $this->assertCount(15, $articles);
63 | }
64 |
65 | /** @test */
66 | public function that_loaded_only_published_articles()
67 | {
68 | $this->seedPublishedArticles();
69 |
70 | $articles = Article::published()->get();
71 |
72 | $this->assertCount(5, $articles);
73 | }
74 |
75 | private function seedPublishedArticles($num = 5)
76 | {
77 | factory(Article::class, $num)->create([
78 | 'user_id' => $this->user->id,
79 | 'published' => true,
80 | ]);
81 | }
82 |
83 | /** @test */
84 | public function that_load_only_published_article()
85 | {
86 | $this->seedUnpublishedArticles();
87 |
88 | factory(Article::class, 1)->create([
89 | 'user_id' => $this->user->id,
90 | 'published' => true,
91 | ]);
92 |
93 | $this->assertEquals(1, Article::published()->count());
94 | }
95 |
96 | /** @test */
97 | public function that_article_get_published_and_total_number_of_published_get_changed()
98 | {
99 | $this->seedPublishedArticles(2);
100 | $this->seedUnpublishedArticles(5);
101 |
102 | $date = Carbon::now()->format('Y-m-d');
103 |
104 | $article = Article::where('published', false)->first();
105 | $article->published = true;
106 | $article->published_at = $date;
107 |
108 | $article->save();
109 |
110 | $this->assertEquals($article->published, true);
111 | $this->assertEquals($article->published_at->format('Y-m-d'), $date);
112 |
113 | $articles = Article::where('published', true)->get();
114 |
115 | $this->assertEquals($articles->count(), 3);
116 | }
117 |
118 | /** @test */
119 | public function that_article_get_unpublished_and_total_number_of_unpublished_get_changed()
120 | {
121 | $this->seedPublishedArticles(2);
122 | $this->seedUnpublishedArticles(5);
123 |
124 | $article = Article::where('published', true)->first();
125 | $article->published = false;
126 | $article->published_at = null;
127 |
128 | $article->save();
129 |
130 | $this->assertEquals($article->published, false);
131 | $this->assertEquals($article->published_at, null);
132 |
133 | $articles = Article::where('published', false)->get();
134 |
135 | $this->assertEquals($articles->count(), 6);
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/tests/Feature/LoginTest.php:
--------------------------------------------------------------------------------
1 | create();
21 |
22 | $response = $this->post('/api/v1/auth/login', [
23 | 'email' => $user->email,
24 | 'password' => 'secret',
25 | ]);
26 |
27 | $response->assertStatus(200);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/webpack.mix.js:
--------------------------------------------------------------------------------
1 | const mix = require('laravel-mix');
2 |
3 | /*
4 | |--------------------------------------------------------------------------
5 | | Mix Asset Management
6 | |--------------------------------------------------------------------------
7 | |
8 | | Mix provides a clean, fluent API for defining some Webpack build steps
9 | | for your Laravel application. By default, we are compiling the Sass
10 | | file for the application as well as bundling up all the JS files.
11 | |
12 | */
13 |
14 |
15 | const RemovePlugin = require('remove-files-webpack-plugin');
16 | const ESLintPlugin = require('eslint-webpack-plugin');
17 |
18 | const removePlugin = new RemovePlugin({
19 |
20 | before: {
21 | test: [
22 | {
23 | folder: 'public',
24 | method: (filePath) => {
25 | return new RegExp(/(?:.*\.js|.*\.map|mix-manifest\.json)$/, 'm').test(filePath);
26 | }
27 | },
28 | {
29 | folder: 'public/js',
30 | method: (filePath) => {
31 | return new RegExp(/(?:.*\.js|.*\.map)$/, 'm').test(filePath);
32 | },
33 | recursive: true
34 | },
35 | {
36 | folder: 'public/css',
37 | method: (filePath) => {
38 | return new RegExp(/(?:.*\.css|.*\.map)$/, 'm').test(filePath);
39 | }
40 | }
41 | ]
42 | },
43 |
44 | after: {}
45 | })
46 |
47 | mix.webpackConfig({
48 | plugins: [removePlugin, new ESLintPlugin()],
49 | });
50 |
51 | mix.js('resources/js/app.js', 'public/js').react()
52 | .extract(['@popperjs/core', 'axios', 'bootstrap', 'clsx', 'font-awesome', 'history', 'jquery', 'lodash', 'moment', 'prop-types', 'react', 'react-document-title', 'react-dom', 'react-loadable', 'react-redux', 'react-router-dom', 'reactstrap', 'redux', 'redux-thunk', 'ree-validate'])
53 | .sass('resources/sass/app.scss', 'public/css')
54 | .sourceMaps(false, 'source-map')
55 |
56 | if (mix.inProduction()) {
57 | mix.version();
58 | }
59 |
60 | if (!mix.inProduction()) {
61 | mix.browserSync('http://localhost:8100')
62 | }
63 |
--------------------------------------------------------------------------------