17 | */
18 | public function definition()
19 | {
20 | return [
21 | 'name' => $this->faker->name(),
22 | 'email' => $this->faker->unique()->safeEmail(),
23 | 'email_verified_at' => now(),
24 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
25 | 'remember_token' => Str::random(10),
26 | ];
27 | }
28 |
29 | /**
30 | * Indicate that the model's email address should be unverified.
31 | *
32 | * @return static
33 | */
34 | public function unverified()
35 | {
36 | return $this->state(function (array $attributes) {
37 | return [
38 | 'email_verified_at' => null,
39 | ];
40 | });
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/database/migrations/2014_10_12_000000_create_users_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->string('email')->unique();
20 | $table->timestamp('email_verified_at')->nullable();
21 | $table->string('password');
22 | $table->rememberToken();
23 | $table->timestamps();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('users');
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/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/2019_08_19_000000_create_failed_jobs_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('uuid')->unique();
19 | $table->text('connection');
20 | $table->text('queue');
21 | $table->longText('payload');
22 | $table->longText('exception');
23 | $table->timestamp('failed_at')->useCurrent();
24 | });
25 | }
26 |
27 | /**
28 | * Reverse the migrations.
29 | *
30 | * @return void
31 | */
32 | public function down()
33 | {
34 | Schema::dropIfExists('failed_jobs');
35 | }
36 | };
37 |
--------------------------------------------------------------------------------
/database/migrations/2019_12_14_000001_create_personal_access_tokens_table.php:
--------------------------------------------------------------------------------
1 | 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/migrations/2022_05_17_064519_create_posts_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('title');
19 | $table->text('content');
20 | $table->timestamps();
21 | });
22 | }
23 |
24 | /**
25 | * Reverse the migrations.
26 | *
27 | * @return void
28 | */
29 | public function down()
30 | {
31 | Schema::dropIfExists('posts');
32 | }
33 | };
34 |
--------------------------------------------------------------------------------
/database/migrations/2022_05_19_064242_create_categories_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('categories');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2022_05_19_064307_add_category_id_to_posts_table.php:
--------------------------------------------------------------------------------
1 | foreignId('category_id')->nullable()->constrained();
18 | });
19 | }
20 |
21 | /**
22 | * Reverse the migrations.
23 | *
24 | * @return void
25 | */
26 | public function down()
27 | {
28 | Schema::table('posts', function (Blueprint $table) {
29 | //
30 | });
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2022_06_09_070903_create_permissions_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('permissions');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2022_06_09_070912_create_roles_table.php:
--------------------------------------------------------------------------------
1 | id();
18 | $table->string('name');
19 | $table->timestamps();
20 | });
21 | }
22 |
23 | /**
24 | * Reverse the migrations.
25 | *
26 | * @return void
27 | */
28 | public function down()
29 | {
30 | Schema::dropIfExists('roles');
31 | }
32 | };
33 |
--------------------------------------------------------------------------------
/database/migrations/2022_06_09_070937_create_permission_role_table.php:
--------------------------------------------------------------------------------
1 | foreignId('permission_id')->constrained();
18 | $table->foreignId('role_id')->constrained();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::dropIfExists('permission_role');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/migrations/2022_06_09_070949_create_role_user_table.php:
--------------------------------------------------------------------------------
1 | foreignId('role_id')->constrained();
18 | $table->foreignId('user_id')->constrained();
19 | });
20 | }
21 |
22 | /**
23 | * Reverse the migrations.
24 | *
25 | * @return void
26 | */
27 | public function down()
28 | {
29 | Schema::dropIfExists('role_user');
30 | }
31 | };
32 |
--------------------------------------------------------------------------------
/database/seeders/CategorySeeder.php:
--------------------------------------------------------------------------------
1 | create();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/database/seeders/DatabaseSeeder.php:
--------------------------------------------------------------------------------
1 | call(CategorySeeder::class);
18 | $this->call(PostSeeder::class);
19 |
20 | $this->call(PermissionSeeder::class);
21 | $this->call(RoleSeeder::class);
22 | $this->call(UserSeeder::class);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/database/seeders/PermissionSeeder.php:
--------------------------------------------------------------------------------
1 | 'post_view']);
19 | Permission::create(['name' => 'post_create']);
20 | Permission::create(['name' => 'post_update']);
21 | Permission::create(['name' => 'post_delete']);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/database/seeders/PostSeeder.php:
--------------------------------------------------------------------------------
1 | create();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/database/seeders/RoleSeeder.php:
--------------------------------------------------------------------------------
1 | 'Administrator']);
20 | $admin->permissions()->attach(Permission::pluck('id'));
21 |
22 | $editor = Role::create(['name' => 'Editor']);
23 | $editor->permissions()->attach(
24 | Permission::where('name', '!=', 'post_delete')->pluck('id')
25 | );
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/database/seeders/UserSeeder.php:
--------------------------------------------------------------------------------
1 | create(['email' => 'admin@admin.com']);
19 | $admin->roles()->attach(1);
20 |
21 | $editor = User::factory()->create();
22 | $editor->roles()->attach(2);
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/lang/en/auth.php:
--------------------------------------------------------------------------------
1 | 'These credentials do not match our records.',
17 | 'password' => 'The provided password is incorrect.',
18 | 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
19 |
20 | ];
21 |
--------------------------------------------------------------------------------
/lang/en/pagination.php:
--------------------------------------------------------------------------------
1 | '« Previous',
17 | 'next' => 'Next »',
18 |
19 | ];
20 |
--------------------------------------------------------------------------------
/lang/en/passwords.php:
--------------------------------------------------------------------------------
1 | 'Your password has been reset!',
17 | 'sent' => 'We have emailed your password reset link!',
18 | 'throttled' => 'Please wait before retrying.',
19 | 'token' => 'This password reset token is invalid.',
20 | 'user' => "We can't find a user with that email address.",
21 |
22 | ];
23 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
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 | "devDependencies": {
13 | "@babel/preset-react": "^7.16.7",
14 | "@tailwindcss/forms": "^0.4.0",
15 | "alpinejs": "^3.4.2",
16 | "autoprefixer": "^10.4.2",
17 | "axios": "^0.25",
18 | "laravel-mix": "^6.0.6",
19 | "lodash": "^4.17.19",
20 | "postcss": "^8.4.6",
21 | "postcss-import": "^14.0.2",
22 | "tailwindcss": "^3.0.18"
23 | },
24 | "dependencies": {
25 | "@casl/ability": "^5.4.4",
26 | "@casl/react": "^3.0.0",
27 | "react": "^18.1.0",
28 | "react-dom": "^18.1.0",
29 | "react-router-dom": "^6.3.0",
30 | "sweetalert2": "^11.4.17"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/phpunit.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 | ./tests/Unit
10 |
11 |
12 | ./tests/Feature
13 |
14 |
15 |
16 |
17 | ./app
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/public/.htaccess:
--------------------------------------------------------------------------------
1 |
2 |
3 | Options -MultiViews -Indexes
4 |
5 |
6 | RewriteEngine On
7 |
8 | # Handle Authorization Header
9 | RewriteCond %{HTTP:Authorization} .
10 | RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
11 |
12 | # Redirect Trailing Slashes If Not A Folder...
13 | RewriteCond %{REQUEST_FILENAME} !-d
14 | RewriteCond %{REQUEST_URI} (.+)/$
15 | RewriteRule ^ %1 [L,R=301]
16 |
17 | # Send Requests To Front Controller...
18 | RewriteCond %{REQUEST_FILENAME} !-d
19 | RewriteCond %{REQUEST_FILENAME} !-f
20 | RewriteRule ^ index.php [L]
21 |
22 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/LaravelDaily/Laravel-React-Course/fc2342fb6c84c61489818b30199be1aeef073ae0/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class);
50 |
51 | $response = $kernel->handle(
52 | $request = Request::capture()
53 | )->send();
54 |
55 | $kernel->terminate($request, $response);
56 |
--------------------------------------------------------------------------------
/public/mix-manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "/js/app.js": "/js/app.js",
3 | "/css/app.css": "/css/app.css"
4 | }
5 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Disallow:
3 |
--------------------------------------------------------------------------------
/resources/css/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 | @import 'tailwindcss/components';
3 | @import 'tailwindcss/utilities';
4 |
5 | table.table {
6 | @apply min-w-full divide-y divide-gray-200 border
7 | }
8 |
9 | table.table thead.table-header > tr > th {
10 | @apply px-6 py-3 bg-gray-50 text-left
11 | }
12 |
13 | table.table thead.table-header > tr > th > div {
14 | @apply flex items-center justify-between text-xs leading-4 font-medium text-gray-500 uppercase tracking-wider
15 | }
16 |
17 | table.table thead.table-header .column-sort {
18 | @apply text-lg
19 | }
20 |
21 | table.table tbody.table-body {
22 | @apply bg-white divide-y divide-gray-200 divide-solid
23 | }
24 |
25 | table.table tbody.table-body > tr > td {
26 | @apply px-6 py-4 text-sm leading-5 text-gray-900
27 | }
28 |
29 | button:disabled {
30 | @apply opacity-60 cursor-not-allowed
31 | }
32 |
--------------------------------------------------------------------------------
/resources/js/Abilities/Ability.js:
--------------------------------------------------------------------------------
1 | import { AbilityBuilder, Ability } from '@casl/ability'
2 |
3 | const { can, cannot, build } = new AbilityBuilder(Ability);
4 |
5 | export default build();
6 |
--------------------------------------------------------------------------------
/resources/js/Abilities/Can.js:
--------------------------------------------------------------------------------
1 | import { createContext } from 'react';
2 | import { createContextualCan } from '@casl/react';
3 |
4 | export const AbilityContext = createContext();
5 | export const Can = createContextualCan(AbilityContext.Consumer);
6 |
--------------------------------------------------------------------------------
/resources/js/Layouts/App.js:
--------------------------------------------------------------------------------
1 | import {Link, NavLink, Outlet, useNavigate} from "react-router-dom";
2 | import {useContext, useEffect} from "react";
3 | import {Ability, AbilityBuilder} from "@casl/ability";
4 | import {AbilityContext} from "../Abilities/Can";
5 |
6 | function App() {
7 | const navigate = useNavigate();
8 | const ability = useContext(AbilityContext)
9 |
10 | const handleLogout = (event) => {
11 | axios.post('/logout')
12 | .then(response => navigate('/login'))
13 | }
14 |
15 | useEffect(() => {
16 | axios.get('/api/user')
17 | .then(response => {
18 | axios.get('/api/abilities')
19 | .then(response => {
20 | const { can, rules } = new AbilityBuilder(Ability);
21 | can(response.data);
22 | ability.update(rules);
23 | })
24 | })
25 | .catch(error => {
26 | if (error.response.status === 401) {
27 | navigate('/login');
28 | }
29 | })
30 | });
31 |
32 | return (
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | React Course
41 |
42 |
43 |
44 |
45 | isActive ? "inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out" : "inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"}>Posts
47 | isActive ? "inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out" : "inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out"}>Add
49 | post
50 |
51 |
52 |
53 |
54 | Log out
55 |
56 |
57 |
58 |
59 |
60 |
61 |
68 |
69 |
70 |
79 |
80 |
81 | )
82 | }
83 |
84 | export default App;
85 |
--------------------------------------------------------------------------------
/resources/js/Layouts/Guest.js:
--------------------------------------------------------------------------------
1 | import {Outlet, Link} from "react-router-dom";
2 |
3 | export default function Guest() {
4 | return (
5 |
6 |
7 |
8 | React Course
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | )
17 | }
18 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/Login.js:
--------------------------------------------------------------------------------
1 | import {useContext, useState} from "react";
2 | import { useNavigate, Link } from "react-router-dom";
3 | import {Ability, AbilityBuilder} from "@casl/ability";
4 | import {AbilityContext} from "../../Abilities/Can";
5 |
6 | export default function Login() {
7 | const [email, setEmail] = useState('');
8 | const [password, setPassword] = useState('');
9 | const [errors, setErrors] = useState([]);
10 | const navigate = useNavigate();
11 | const ability = useContext(AbilityContext)
12 |
13 | const handleSubmit = (event) => {
14 | event.preventDefault();
15 |
16 | axios.post('/login', { email, password })
17 | .then(response => {
18 | axios.get('/api/abilities')
19 | .then(response => {
20 | const { can, rules } = new AbilityBuilder(Ability);
21 | can(response.data);
22 | ability.update(rules);
23 | })
24 |
25 | navigate('/posts');
26 | })
27 | .catch(error => {
28 | setErrors(Object.entries(error.response.data.errors))
29 | })
30 | }
31 |
32 | return (
33 |
34 | { errors.length > 0 &&
35 |
36 | Whoops! Something went wrong.
37 |
38 |
39 |
40 | { errors.map((error, index) => {
41 | return (
42 | {error[1][0]}
43 | )
44 | }) }
45 |
46 |
}
47 |
48 |
94 |
95 | )
96 | }
97 |
--------------------------------------------------------------------------------
/resources/js/Pages/Auth/Register.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { useNavigate } from "react-router-dom";
3 |
4 | export default function Register() {
5 | const [name, setName] = useState('');
6 | const [email, setEmail] = useState('');
7 | const [password, setPassword] = useState('');
8 | const [password_confirmation, setPasswordConfirmation] = useState('');
9 | const [errors, setErrors] = useState([]);
10 | const navigate = useNavigate();
11 |
12 | const handleSubmit = (event) => {
13 | event.preventDefault();
14 |
15 | axios.post('/register', { name, email, password, password_confirmation })
16 | .then(response => navigate('/posts'))
17 | .catch(error => {
18 | setErrors(Object.entries(error.response.data.errors))
19 | })
20 | }
21 |
22 | return (
23 |
24 | { errors.length > 0 &&
25 |
26 | Whoops! Something went wrong.
27 |
28 |
29 |
30 | { errors.map((error, index) => {
31 | return (
32 | {error[1][0]}
33 | )
34 | }) }
35 |
36 |
}
37 |
38 |
109 |
110 | )
111 | }
112 |
--------------------------------------------------------------------------------
/resources/js/Pages/Posts/Create.js:
--------------------------------------------------------------------------------
1 | import {Component} from "react";
2 | import CategoriesService from "../../Services/CategoriesService";
3 | import { useNavigate } from "react-router-dom";
4 | import Swal from 'sweetalert2'
5 |
6 | export const withNavigation = (Component) => {
7 | return props => ;
8 | }
9 |
10 | class PostsCreate extends Component {
11 | constructor(props) {
12 | super(props);
13 |
14 | this.state = {
15 | title: '',
16 | content: '',
17 | category_id: '',
18 | thumbnail: '',
19 | categories: [],
20 | errors: {},
21 | isLoading: false
22 | }
23 |
24 | this.handleTitleChange = this.handleTitleChange.bind(this);
25 | this.handleContentChange = this.handleContentChange.bind(this);
26 | this.handleCategoryChange = this.handleCategoryChange.bind(this);
27 | this.handleThumbnailChange = this.handleThumbnailChange.bind(this);
28 | this.handleSubmit = this.handleSubmit.bind(this);
29 | }
30 |
31 | handleTitleChange(event) {
32 | this.setState({ title: event.target.value });
33 | }
34 |
35 | handleContentChange(event) {
36 | this.setState({ content: event.target.value });
37 | }
38 |
39 | handleCategoryChange(event) {
40 | this.setState({ category_id: event.target.value });
41 | }
42 |
43 | handleThumbnailChange(event) {
44 | this.setState({ thumbnail: event.target.files[0] });
45 | }
46 |
47 | handleSubmit(event) {
48 | event.preventDefault();
49 |
50 | if (this.state.isLoading) return;
51 |
52 | this.setState({
53 | errors: {},
54 | isLoading: true
55 | });
56 |
57 | let postData = new FormData()
58 | postData.append('title', this.state.title);
59 | postData.append('content', this.state.content);
60 | postData.append('category_id', this.state.category_id);
61 | postData.append('thumbnail', this.state.thumbnail);
62 |
63 | axios.post('/api/posts', postData).then(response => {
64 | Swal.fire({
65 | icon: 'success',
66 | title: 'Post added successfully'
67 | });
68 | this.props.navigate('/')
69 | })
70 | .catch(error => {
71 | this.setState({ errors: error.response.data.errors })
72 | Swal.fire({
73 | icon: 'error',
74 | title: error.response.data.message
75 | })
76 | })
77 | .finally(() => this.setState({ isLoading: false }));
78 | }
79 |
80 | componentDidMount() {
81 | CategoriesService.getAll()
82 | .then(response => this.setState({categories: response.data.data}))
83 | }
84 |
85 | errorMessage(field) {
86 | return (
87 |
88 | {
89 | this.state.errors?.[field]?.map((message, index) => {
90 | return (
91 |
{ message }
92 | )
93 | })
94 | }
95 |
96 | )
97 | }
98 |
99 | render() {
100 | return (
101 |
145 | )
146 | }
147 | }
148 |
149 | export default withNavigation(PostsCreate);
150 |
--------------------------------------------------------------------------------
/resources/js/Pages/Posts/Edit.js:
--------------------------------------------------------------------------------
1 | import {Component} from "react";
2 | import CategoriesService from "../../Services/CategoriesService";
3 | import { useNavigate, useParams } from "react-router-dom";
4 |
5 | export const withNavigation = (Component) => {
6 | return props => ;
7 | }
8 |
9 | export const withParams = (Component) => {
10 | return props => ;
11 | }
12 |
13 | class PostsEdit extends Component {
14 | constructor(props) {
15 | super(props);
16 |
17 | this.state = {
18 | id: '',
19 | title: '',
20 | content: '',
21 | category_id: '',
22 | thumbnail: '',
23 | categories: [],
24 | errors: {},
25 | isLoading: false
26 | }
27 |
28 | this.handleTitleChange = this.handleTitleChange.bind(this);
29 | this.handleContentChange = this.handleContentChange.bind(this);
30 | this.handleCategoryChange = this.handleCategoryChange.bind(this);
31 | this.handleSubmit = this.handleSubmit.bind(this);
32 | }
33 |
34 | handleTitleChange(event) {
35 | this.setState({ title: event.target.value });
36 | }
37 |
38 | handleContentChange(event) {
39 | this.setState({ content: event.target.value });
40 | }
41 |
42 | handleCategoryChange(event) {
43 | this.setState({ category_id: event.target.value });
44 | }
45 |
46 | handleSubmit(event) {
47 | event.preventDefault();
48 |
49 | if (this.state.isLoading) return;
50 |
51 | this.setState({
52 | errors: {},
53 | isLoading: true
54 | });
55 |
56 | axios.put('/api/posts/' + this.state.id, {
57 | 'title': this.state.title,
58 | 'content': this.state.content,
59 | 'category_id': this.state.category_id
60 | }).then(response => this.props.navigate('/'))
61 | .catch(error => this.setState({ errors: error.response.data.errors }))
62 | .finally(() => this.setState({ isLoading: false }));
63 | }
64 |
65 | componentDidMount() {
66 | this.setState({ isLoading: true });
67 | axios.get('/api/posts/' + this.props.params.id).then(response => {
68 | this.setState({ id: response.data.data.id } );
69 | this.setState({ title: response.data.data.title } );
70 | this.setState({ content: response.data.data.content } );
71 | this.setState({ category_id: response.data.data.category_id } );
72 | }).finally(() => this.setState({ isLoading: false }));
73 | CategoriesService.getAll()
74 | .then(response => this.setState({categories: response.data.data}))
75 | }
76 |
77 | errorMessage(field) {
78 | return (
79 |
80 | {
81 | this.state.errors?.[field]?.map((message, index) => {
82 | return (
83 |
{ message }
84 | )
85 | })
86 | }
87 |
88 | )
89 | }
90 |
91 | render() {
92 | return (
93 |
94 |
95 |
96 | Title
97 |
98 |
99 | { this.errorMessage('title') }
100 |
101 |
102 |
103 | Content
104 |
105 |
106 | { this.errorMessage('content') }
107 |
108 |
109 |
110 | Category
111 |
112 |
113 | -- Select category --
114 | { this.state.categories.map((category, index) => (
115 | { category.name }
116 | )) }
117 |
118 | { this.errorMessage('category_id') }
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 | Save
127 |
128 |
129 |
130 | )
131 | }
132 | }
133 |
134 | export default withParams(withNavigation(PostsEdit));
135 |
--------------------------------------------------------------------------------
/resources/js/Services/CategoriesService.js:
--------------------------------------------------------------------------------
1 | class CategoriesService {
2 | getAll() {
3 | return axios.get('/api/categories');
4 | }
5 | }
6 |
7 | export default new CategoriesService;
8 |
--------------------------------------------------------------------------------
/resources/js/app.js:
--------------------------------------------------------------------------------
1 | import {Route, Routes, BrowserRouter, Navigate} from "react-router-dom";
2 |
3 | require('./bootstrap');
4 |
5 | import {createRoot} from 'react-dom/client';
6 | import App from "./Layouts/App";
7 | import PostsIndex from "./Pages/Posts";
8 | import PostsCreate from "./Pages/Posts/Create";
9 | import PostsEdit from "./Pages/Posts/Edit";
10 | import Guest from "./Layouts/Guest";
11 | import Login from "./Pages/Auth/Login";
12 | import Register from "./Pages/Auth/Register";
13 |
14 | import {AbilityContext} from './Abilities/Can';
15 | import Ability from './Abilities/Ability';
16 |
17 | const root = createRoot(document.getElementById('app'));
18 | root.render(
19 |
20 |
21 |
22 | }>
23 | }>
24 | }>
25 | }>
26 |
27 | }>
28 | }>
29 |
30 | }>
31 | }>
32 |
33 | }/>
34 |
35 |
36 |
37 | )
38 |
--------------------------------------------------------------------------------
/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 | /**
15 | * Echo exposes an expressive API for subscribing to channels and listening
16 | * for events that are broadcast by Laravel. Echo and event broadcasting
17 | * allows your team to easily build robust real-time web applications.
18 | */
19 |
20 | // import Echo from 'laravel-echo';
21 |
22 | // window.Pusher = require('pusher-js');
23 |
24 | // window.Echo = new Echo({
25 | // broadcaster: 'pusher',
26 | // key: process.env.MIX_PUSHER_APP_KEY,
27 | // cluster: process.env.MIX_PUSHER_APP_CLUSTER,
28 | // forceTLS: true
29 | // });
30 |
--------------------------------------------------------------------------------
/resources/views/auth/confirm-password.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ __('This is a secure area of the application. Please confirm your password before continuing.') }}
11 |
12 |
13 |
14 |
15 |
16 |
17 | @csrf
18 |
19 |
20 |
21 |
22 |
23 |
27 |
28 |
29 |
30 |
31 | {{ __('Confirm') }}
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/resources/views/auth/forgot-password.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ __('Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.') }}
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | @csrf
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {{ __('Email Password Reset Link') }}
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/resources/views/auth/login.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | @csrf
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | {{ __('Remember me') }}
40 |
41 |
42 |
43 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/resources/views/auth/register.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | @csrf
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
46 |
47 |
48 |
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/resources/views/auth/reset-password.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | @csrf
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
39 |
40 |
41 |
42 |
43 | {{ __('Reset Password') }}
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/resources/views/auth/verify-email.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | {{ __('Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn\'t receive the email, we will gladly send you another.') }}
11 |
12 |
13 | @if (session('status') == 'verification-link-sent')
14 |
15 | {{ __('A new verification link has been sent to the email address you provided during registration.') }}
16 |
17 | @endif
18 |
19 |
20 |
21 | @csrf
22 |
23 |
24 |
25 | {{ __('Resend Verification Email') }}
26 |
27 |
28 |
29 |
30 |
31 | @csrf
32 |
33 |
34 | {{ __('Log Out') }}
35 |
36 |
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/resources/views/components/application-logo.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/auth-card.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ $logo }}
4 |
5 |
6 |
7 | {{ $slot }}
8 |
9 |
10 |
--------------------------------------------------------------------------------
/resources/views/components/auth-session-status.blade.php:
--------------------------------------------------------------------------------
1 | @props(['status'])
2 |
3 | @if ($status)
4 | merge(['class' => 'font-medium text-sm text-green-600']) }}>
5 | {{ $status }}
6 |
7 | @endif
8 |
--------------------------------------------------------------------------------
/resources/views/components/auth-validation-errors.blade.php:
--------------------------------------------------------------------------------
1 | @props(['errors'])
2 |
3 | @if ($errors->any())
4 |
5 |
6 | {{ __('Whoops! Something went wrong.') }}
7 |
8 |
9 |
10 | @foreach ($errors->all() as $error)
11 | {{ $error }}
12 | @endforeach
13 |
14 |
15 | @endif
16 |
--------------------------------------------------------------------------------
/resources/views/components/button.blade.php:
--------------------------------------------------------------------------------
1 | merge(['type' => 'submit', 'class' => 'inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring ring-gray-300 disabled:opacity-25 transition ease-in-out duration-150']) }}>
2 | {{ $slot }}
3 |
4 |
--------------------------------------------------------------------------------
/resources/views/components/dropdown-link.blade.php:
--------------------------------------------------------------------------------
1 | merge(['class' => 'block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition duration-150 ease-in-out']) }}>{{ $slot }}
2 |
--------------------------------------------------------------------------------
/resources/views/components/dropdown.blade.php:
--------------------------------------------------------------------------------
1 | @props(['align' => 'right', 'width' => '48', 'contentClasses' => 'py-1 bg-white'])
2 |
3 | @php
4 | switch ($align) {
5 | case 'left':
6 | $alignmentClasses = 'origin-top-left left-0';
7 | break;
8 | case 'top':
9 | $alignmentClasses = 'origin-top';
10 | break;
11 | case 'right':
12 | default:
13 | $alignmentClasses = 'origin-top-right right-0';
14 | break;
15 | }
16 |
17 | switch ($width) {
18 | case '48':
19 | $width = 'w-48';
20 | break;
21 | }
22 | @endphp
23 |
24 |
25 |
26 | {{ $trigger }}
27 |
28 |
29 |
39 |
40 | {{ $content }}
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/resources/views/components/input.blade.php:
--------------------------------------------------------------------------------
1 | @props(['disabled' => false])
2 |
3 | merge(['class' => 'rounded-md shadow-sm border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50']) !!}>
4 |
--------------------------------------------------------------------------------
/resources/views/components/label.blade.php:
--------------------------------------------------------------------------------
1 | @props(['value'])
2 |
3 | merge(['class' => 'block font-medium text-sm text-gray-700']) }}>
4 | {{ $value ?? $slot }}
5 |
6 |
--------------------------------------------------------------------------------
/resources/views/components/nav-link.blade.php:
--------------------------------------------------------------------------------
1 | @props(['active'])
2 |
3 | @php
4 | $classes = ($active ?? false)
5 | ? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition duration-150 ease-in-out'
6 | : 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition duration-150 ease-in-out';
7 | @endphp
8 |
9 | merge(['class' => $classes]) }}>
10 | {{ $slot }}
11 |
12 |
--------------------------------------------------------------------------------
/resources/views/components/responsive-nav-link.blade.php:
--------------------------------------------------------------------------------
1 | @props(['active'])
2 |
3 | @php
4 | $classes = ($active ?? false)
5 | ? 'block pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition duration-150 ease-in-out'
6 | : 'block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition duration-150 ease-in-out';
7 | @endphp
8 |
9 | merge(['class' => $classes]) }}>
10 | {{ $slot }}
11 |
12 |
--------------------------------------------------------------------------------
/resources/views/dashboard.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ __('Dashboard') }}
5 |
6 |
7 |
8 |
16 |
17 |
--------------------------------------------------------------------------------
/resources/views/layouts/app.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ config('app.name', 'Laravel') }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
--------------------------------------------------------------------------------
/resources/views/layouts/guest.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | {{ config('app.name', 'Laravel') }}
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {{ $slot }}
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/resources/views/layouts/navigation.blade.php:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
14 |
15 |
16 | {{ __('Dashboard') }}
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | {{ __('Dashboard') }}
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/routes/api.php:
--------------------------------------------------------------------------------
1 | group(function () {
18 | Route::get('/user', function (Request $request) {
19 | return $request->user();
20 | });
21 |
22 | Route::apiResource('posts', \App\Http\Controllers\Api\PostController::class);
23 | Route::get('categories',
24 | [\App\Http\Controllers\Api\CategoryController::class, 'index'])->name('categories.index');
25 |
26 | Route::get('abilities', function(Request $request) {
27 | return $request->user()->roles()->with('permissions')
28 | ->get()
29 | ->pluck('permissions')
30 | ->flatten()
31 | ->pluck('name')
32 | ->unique()
33 | ->values()
34 | ->toArray();
35 | });
36 | });
37 |
38 |
39 |
40 |
--------------------------------------------------------------------------------
/routes/auth.php:
--------------------------------------------------------------------------------
1 | group(function () {
14 | Route::get('register', [RegisteredUserController::class, 'create'])
15 | ->name('register');
16 |
17 | Route::post('register', [RegisteredUserController::class, 'store']);
18 |
19 | Route::get('login', [AuthenticatedSessionController::class, 'create'])
20 | ->name('login');
21 |
22 | Route::post('login', [AuthenticatedSessionController::class, 'store']);
23 |
24 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create'])
25 | ->name('password.request');
26 |
27 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store'])
28 | ->name('password.email');
29 |
30 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create'])
31 | ->name('password.reset');
32 |
33 | Route::post('reset-password', [NewPasswordController::class, 'store'])
34 | ->name('password.update');
35 | });
36 |
37 | Route::middleware('auth')->group(function () {
38 | Route::get('verify-email', [EmailVerificationPromptController::class, '__invoke'])
39 | ->name('verification.notice');
40 |
41 | Route::get('verify-email/{id}/{hash}', [VerifyEmailController::class, '__invoke'])
42 | ->middleware(['signed', 'throttle:6,1'])
43 | ->name('verification.verify');
44 |
45 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store'])
46 | ->middleware('throttle:6,1')
47 | ->name('verification.send');
48 |
49 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show'])
50 | ->name('password.confirm');
51 |
52 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']);
53 |
54 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy'])
55 | ->name('logout');
56 | });
57 |
--------------------------------------------------------------------------------
/routes/channels.php:
--------------------------------------------------------------------------------
1 | id === (int) $id;
18 | });
19 |
--------------------------------------------------------------------------------
/routes/console.php:
--------------------------------------------------------------------------------
1 | comment(Inspiring::quote());
19 | })->purpose('Display an inspiring quote');
20 |
--------------------------------------------------------------------------------
/routes/web.php:
--------------------------------------------------------------------------------
1 | where('any', '.*');
23 |
--------------------------------------------------------------------------------
/storage/app/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !public/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/app/public/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/storage/framework/.gitignore:
--------------------------------------------------------------------------------
1 | compiled.php
2 | config.php
3 | down
4 | events.scanned.php
5 | maintenance.php
6 | routes.php
7 | routes.scanned.php
8 | schedule-*
9 | services.json
10 |
--------------------------------------------------------------------------------
/storage/framework/cache/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !data/
3 | !.gitignore
4 |
--------------------------------------------------------------------------------
/storage/framework/cache/data/.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 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const defaultTheme = require('tailwindcss/defaultTheme');
2 |
3 | module.exports = {
4 | content: [
5 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php',
6 | './storage/framework/views/*.php',
7 | './resources/views/**/*.blade.php',
8 | './resources/js/**/*.js',
9 | ],
10 |
11 | theme: {
12 | extend: {
13 | fontFamily: {
14 | sans: ['Nunito', ...defaultTheme.fontFamily.sans],
15 | },
16 | },
17 | },
18 |
19 | plugins: [require('@tailwindcss/forms')],
20 | };
21 |
--------------------------------------------------------------------------------
/tests/CreatesApplication.php:
--------------------------------------------------------------------------------
1 | make(Kernel::class)->bootstrap();
19 |
20 | return $app;
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/AuthenticationTest.php:
--------------------------------------------------------------------------------
1 | get('/login');
17 |
18 | $response->assertStatus(200);
19 | }
20 |
21 | public function test_users_can_authenticate_using_the_login_screen()
22 | {
23 | $user = User::factory()->create();
24 |
25 | $response = $this->post('/login', [
26 | 'email' => $user->email,
27 | 'password' => 'password',
28 | ]);
29 |
30 | $this->assertAuthenticated();
31 | $response->assertRedirect(RouteServiceProvider::HOME);
32 | }
33 |
34 | public function test_users_can_not_authenticate_with_invalid_password()
35 | {
36 | $user = User::factory()->create();
37 |
38 | $this->post('/login', [
39 | 'email' => $user->email,
40 | 'password' => 'wrong-password',
41 | ]);
42 |
43 | $this->assertGuest();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/EmailVerificationTest.php:
--------------------------------------------------------------------------------
1 | create([
20 | 'email_verified_at' => null,
21 | ]);
22 |
23 | $response = $this->actingAs($user)->get('/verify-email');
24 |
25 | $response->assertStatus(200);
26 | }
27 |
28 | public function test_email_can_be_verified()
29 | {
30 | $user = User::factory()->create([
31 | 'email_verified_at' => null,
32 | ]);
33 |
34 | Event::fake();
35 |
36 | $verificationUrl = URL::temporarySignedRoute(
37 | 'verification.verify',
38 | now()->addMinutes(60),
39 | ['id' => $user->id, 'hash' => sha1($user->email)]
40 | );
41 |
42 | $response = $this->actingAs($user)->get($verificationUrl);
43 |
44 | Event::assertDispatched(Verified::class);
45 | $this->assertTrue($user->fresh()->hasVerifiedEmail());
46 | $response->assertRedirect(RouteServiceProvider::HOME.'?verified=1');
47 | }
48 |
49 | public function test_email_is_not_verified_with_invalid_hash()
50 | {
51 | $user = User::factory()->create([
52 | 'email_verified_at' => null,
53 | ]);
54 |
55 | $verificationUrl = URL::temporarySignedRoute(
56 | 'verification.verify',
57 | now()->addMinutes(60),
58 | ['id' => $user->id, 'hash' => sha1('wrong-email')]
59 | );
60 |
61 | $this->actingAs($user)->get($verificationUrl);
62 |
63 | $this->assertFalse($user->fresh()->hasVerifiedEmail());
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/PasswordConfirmationTest.php:
--------------------------------------------------------------------------------
1 | create();
16 |
17 | $response = $this->actingAs($user)->get('/confirm-password');
18 |
19 | $response->assertStatus(200);
20 | }
21 |
22 | public function test_password_can_be_confirmed()
23 | {
24 | $user = User::factory()->create();
25 |
26 | $response = $this->actingAs($user)->post('/confirm-password', [
27 | 'password' => 'password',
28 | ]);
29 |
30 | $response->assertRedirect();
31 | $response->assertSessionHasNoErrors();
32 | }
33 |
34 | public function test_password_is_not_confirmed_with_invalid_password()
35 | {
36 | $user = User::factory()->create();
37 |
38 | $response = $this->actingAs($user)->post('/confirm-password', [
39 | 'password' => 'wrong-password',
40 | ]);
41 |
42 | $response->assertSessionHasErrors();
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/PasswordResetTest.php:
--------------------------------------------------------------------------------
1 | get('/forgot-password');
18 |
19 | $response->assertStatus(200);
20 | }
21 |
22 | public function test_reset_password_link_can_be_requested()
23 | {
24 | Notification::fake();
25 |
26 | $user = User::factory()->create();
27 |
28 | $this->post('/forgot-password', ['email' => $user->email]);
29 |
30 | Notification::assertSentTo($user, ResetPassword::class);
31 | }
32 |
33 | public function test_reset_password_screen_can_be_rendered()
34 | {
35 | Notification::fake();
36 |
37 | $user = User::factory()->create();
38 |
39 | $this->post('/forgot-password', ['email' => $user->email]);
40 |
41 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) {
42 | $response = $this->get('/reset-password/'.$notification->token);
43 |
44 | $response->assertStatus(200);
45 |
46 | return true;
47 | });
48 | }
49 |
50 | public function test_password_can_be_reset_with_valid_token()
51 | {
52 | Notification::fake();
53 |
54 | $user = User::factory()->create();
55 |
56 | $this->post('/forgot-password', ['email' => $user->email]);
57 |
58 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
59 | $response = $this->post('/reset-password', [
60 | 'token' => $notification->token,
61 | 'email' => $user->email,
62 | 'password' => 'password',
63 | 'password_confirmation' => 'password',
64 | ]);
65 |
66 | $response->assertSessionHasNoErrors();
67 |
68 | return true;
69 | });
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/Feature/Auth/RegistrationTest.php:
--------------------------------------------------------------------------------
1 | get('/register');
16 |
17 | $response->assertStatus(200);
18 | }
19 |
20 | public function test_new_users_can_register()
21 | {
22 | $response = $this->post('/register', [
23 | 'name' => 'Test User',
24 | 'email' => 'test@example.com',
25 | 'password' => 'password',
26 | 'password_confirmation' => 'password',
27 | ]);
28 |
29 | $this->assertAuthenticated();
30 | $response->assertRedirect(RouteServiceProvider::HOME);
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/tests/Feature/ExampleTest.php:
--------------------------------------------------------------------------------
1 | get('/');
18 |
19 | $response->assertStatus(200);
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/tests/TestCase.php:
--------------------------------------------------------------------------------
1 | assertTrue(true);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/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 applications. By default, we are compiling the CSS
10 | | file for the application as well as bundling up all the JS files.
11 | |
12 | */
13 |
14 | mix.js('resources/js/app.js', 'public/js')
15 | .react()
16 | .postCss('resources/css/app.css', 'public/css', [
17 | require('postcss-import'),
18 | require('tailwindcss'),
19 | require('autoprefixer'),
20 | ]);
21 |
--------------------------------------------------------------------------------