├── .editorconfig ├── .env.example ├── .gitattributes ├── .gitignore ├── README.md ├── app ├── Http │ ├── Controllers │ │ ├── Auth │ │ │ ├── AuthenticatedSessionController.php │ │ │ ├── ConfirmablePasswordController.php │ │ │ ├── EmailVerificationNotificationController.php │ │ │ ├── EmailVerificationPromptController.php │ │ │ ├── NewPasswordController.php │ │ │ ├── PasswordController.php │ │ │ ├── PasswordResetLinkController.php │ │ │ ├── RegisteredUserController.php │ │ │ └── VerifyEmailController.php │ │ ├── Controller.php │ │ ├── DashboardController.php │ │ ├── ProfileController.php │ │ ├── ProjectController.php │ │ ├── TaskController.php │ │ └── UserController.php │ ├── Middleware │ │ └── HandleInertiaRequests.php │ ├── Requests │ │ ├── Auth │ │ │ └── LoginRequest.php │ │ ├── ProfileUpdateRequest.php │ │ ├── StoreProjectRequest.php │ │ ├── StoreTaskRequest.php │ │ ├── StoreUserRequest.php │ │ ├── UpdateProjectRequest.php │ │ ├── UpdateTaskRequest.php │ │ └── UpdateUserRequest.php │ └── Resources │ │ ├── ProjectResource.php │ │ ├── TaskResource.php │ │ ├── UserCrudResource.php │ │ └── UserResource.php ├── Models │ ├── Project.php │ ├── Task.php │ └── User.php └── Providers │ └── AppServiceProvider.php ├── artisan ├── bootstrap ├── app.php ├── cache │ └── .gitignore └── providers.php ├── composer.json ├── composer.lock ├── config └── .gitkeep ├── database ├── .gitignore ├── factories │ ├── ProjectFactory.php │ ├── TaskFactory.php │ └── UserFactory.php ├── migrations │ ├── 0001_01_01_000000_create_users_table.php │ ├── 0001_01_01_000001_create_cache_table.php │ ├── 0001_01_01_000002_create_jobs_table.php │ ├── 2024_02_03_102720_create_projects_table.php │ └── 2024_02_03_102804_create_tasks_table.php └── seeders │ └── DatabaseSeeder.php ├── jsconfig.json ├── package-lock.json ├── package.json ├── phpunit.xml ├── postcss.config.js ├── public ├── .htaccess ├── favicon.ico ├── index.php └── robots.txt ├── resources ├── css │ └── app.css ├── js │ ├── Components │ │ ├── ApplicationLogo.jsx │ │ ├── Checkbox.jsx │ │ ├── DangerButton.jsx │ │ ├── Dropdown.jsx │ │ ├── InputError.jsx │ │ ├── InputLabel.jsx │ │ ├── Modal.jsx │ │ ├── NavLink.jsx │ │ ├── Pagination.jsx │ │ ├── PrimaryButton.jsx │ │ ├── ResponsiveNavLink.jsx │ │ ├── SecondaryButton.jsx │ │ ├── SelectInput.jsx │ │ ├── TableHeading.jsx │ │ ├── TextAreaInput.jsx │ │ └── TextInput.jsx │ ├── Layouts │ │ ├── AuthenticatedLayout.jsx │ │ └── GuestLayout.jsx │ ├── Pages │ │ ├── Auth │ │ │ ├── ConfirmPassword.jsx │ │ │ ├── ForgotPassword.jsx │ │ │ ├── Login.jsx │ │ │ ├── Register.jsx │ │ │ ├── ResetPassword.jsx │ │ │ └── VerifyEmail.jsx │ │ ├── Dashboard.jsx │ │ ├── Profile │ │ │ ├── Edit.jsx │ │ │ └── Partials │ │ │ │ ├── DeleteUserForm.jsx │ │ │ │ ├── UpdatePasswordForm.jsx │ │ │ │ └── UpdateProfileInformationForm.jsx │ │ ├── Project │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ └── Show.jsx │ │ ├── Task │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ ├── Show.jsx │ │ │ └── TasksTable.jsx │ │ ├── User │ │ │ ├── Create.jsx │ │ │ ├── Edit.jsx │ │ │ ├── Index.jsx │ │ │ └── Show.jsx │ │ └── Welcome.jsx │ ├── app.jsx │ ├── bootstrap.js │ └── constants.jsx └── views │ ├── app.blade.php │ └── welcome.blade.php ├── routes ├── auth.php ├── console.php └── web.php ├── storage ├── app │ ├── .gitignore │ └── public │ │ └── .gitignore ├── framework │ ├── .gitignore │ ├── cache │ │ ├── .gitignore │ │ └── data │ │ │ └── .gitignore │ ├── sessions │ │ └── .gitignore │ ├── testing │ │ └── .gitignore │ └── views │ │ └── .gitignore └── logs │ └── .gitignore ├── tailwind.config.js ├── tests ├── Feature │ ├── Auth │ │ ├── AuthenticationTest.php │ │ ├── EmailVerificationTest.php │ │ ├── PasswordConfirmationTest.php │ │ ├── PasswordResetTest.php │ │ ├── PasswordUpdateTest.php │ │ └── RegistrationTest.php │ ├── ExampleTest.php │ └── ProfileTest.php ├── Pest.php ├── TestCase.php └── Unit │ └── ExampleTest.php └── vite.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 4 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{js,jsx}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [*.{yml,yaml}] 18 | indent_size = 2 19 | 20 | [docker-compose.yml] 21 | indent_size = 4 22 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | APP_NAME=Laravel 2 | APP_ENV=local 3 | APP_KEY= 4 | APP_DEBUG=true 5 | APP_TIMEZONE=UTC 6 | APP_URL=http://localhost 7 | 8 | APP_LOCALE=en 9 | APP_FALLBACK_LOCALE=en 10 | APP_FAKER_LOCALE=en_US 11 | 12 | APP_MAINTENANCE_DRIVER=file 13 | APP_MAINTENANCE_STORE=database 14 | 15 | BCRYPT_ROUNDS=12 16 | 17 | LOG_CHANNEL=stack 18 | LOG_STACK=single 19 | LOG_DEPRECATIONS_CHANNEL=null 20 | LOG_LEVEL=debug 21 | 22 | DB_CONNECTION=sqlite 23 | # DB_HOST=127.0.0.1 24 | # DB_PORT=3306 25 | # DB_DATABASE=laravel 26 | # DB_USERNAME=root 27 | # DB_PASSWORD= 28 | 29 | BROADCAST_CONNECTION=log 30 | CACHE_STORE=database 31 | FILESYSTEM_DISK=local 32 | QUEUE_CONNECTION=database 33 | SESSION_DRIVER=database 34 | SESSION_LIFETIME=120 35 | 36 | MEMCACHED_HOST=127.0.0.1 37 | 38 | REDIS_CLIENT=phpredis 39 | REDIS_HOST=127.0.0.1 40 | REDIS_PASSWORD=null 41 | REDIS_PORT=6379 42 | 43 | MAIL_MAILER=log 44 | MAIL_HOST=127.0.0.1 45 | MAIL_PORT=2525 46 | MAIL_USERNAME=null 47 | MAIL_PASSWORD=null 48 | MAIL_ENCRYPTION=null 49 | MAIL_FROM_ADDRESS="hello@example.com" 50 | MAIL_FROM_NAME="${APP_NAME}" 51 | 52 | AWS_ACCESS_KEY_ID= 53 | AWS_SECRET_ACCESS_KEY= 54 | AWS_DEFAULT_REGION=us-east-1 55 | AWS_BUCKET= 56 | AWS_USE_PATH_STYLE_ENDPOINT=false 57 | 58 | PUSHER_APP_ID= 59 | PUSHER_APP_KEY= 60 | PUSHER_APP_SECRET= 61 | PUSHER_HOST= 62 | PUSHER_PORT=443 63 | PUSHER_SCHEME=https 64 | PUSHER_APP_CLUSTER=mt1 65 | 66 | VITE_APP_NAME="${APP_NAME}" 67 | VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}" 68 | VITE_PUSHER_HOST="${PUSHER_HOST}" 69 | VITE_PUSHER_PORT="${PUSHER_PORT}" 70 | VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" 71 | VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" 72 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | 3 | *.blade.php diff=html 4 | *.css diff=css 5 | *.html diff=html 6 | *.md diff=markdown 7 | *.php diff=php 8 | 9 | /.github export-ignore 10 | CHANGELOG.md export-ignore 11 | .styleci.yml export-ignore 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.phpunit.cache 2 | /node_modules 3 | /public/build 4 | /public/hot 5 | /public/storage 6 | /storage/*.key 7 | /vendor 8 | .env 9 | .env.backup 10 | .env.production 11 | .phpunit.result.cache 12 | Homestead.json 13 | Homestead.yaml 14 | auth.json 15 | npm-debug.log 16 | yarn-error.log 17 | /.fleet 18 | /.idea 19 | /.vscode 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel 11 + React SPA Project 2 | A simple project management application using Laravel 11 and React. 3 | 4 | The project was developed for [the following](https://youtu.be/VrQRa-afCAk) YouTube tutorial. 5 | 6 | ## Features 7 | 1. Registration & Login 8 | 2. Projects CRUD with sorting, filtering and pagination 9 | 3. Tasks CRUD with sorting, filtering and pagination 10 | 4. Create Tasks inside project 11 | 5. Show all tasks or show tasks for a specific project 12 | 6. Assign users to tasks 13 | 7. View Tasks assigned to me 14 | 8. Show dashboard with overview information 15 | 16 | ## Installation 17 | 1. Clone the project 18 | 2. Navigate to the project's root directory using terminal 19 | 3. Create `.env` file - `cp .env.example .env` 20 | 4. Execute `composer install` 21 | 5. Execute `npm install` 22 | 6. Set application key - `php artisan key:generate --ansi` 23 | 7. Execute migrations and seed data - `php artisan migrate --seed` 24 | 8. Start vite server - `npm run dev` 25 | 9. Start Artisan server - `php artisan serve` 26 | 27 | ## Demo 28 | Coming soon... 29 | 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/AuthenticatedSessionController.php: -------------------------------------------------------------------------------- 1 | Route::has('password.request'), 23 | 'status' => session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Handle an incoming authentication request. 29 | */ 30 | public function store(LoginRequest $request): RedirectResponse 31 | { 32 | $request->authenticate(); 33 | 34 | $request->session()->regenerate(); 35 | 36 | return redirect()->intended(route('dashboard', absolute: false)); 37 | } 38 | 39 | /** 40 | * Destroy an authenticated session. 41 | */ 42 | public function destroy(Request $request): RedirectResponse 43 | { 44 | Auth::guard('web')->logout(); 45 | 46 | $request->session()->invalidate(); 47 | 48 | $request->session()->regenerateToken(); 49 | 50 | return redirect('/'); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/ConfirmablePasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 29 | 'email' => $request->user()->email, 30 | 'password' => $request->password, 31 | ])) { 32 | throw ValidationException::withMessages([ 33 | 'password' => __('auth.password'), 34 | ]); 35 | } 36 | 37 | $request->session()->put('auth.password_confirmed_at', time()); 38 | 39 | return redirect()->intended(route('dashboard', absolute: false)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationNotificationController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 17 | return redirect()->intended(route('dashboard', absolute: false)); 18 | } 19 | 20 | $request->user()->sendEmailVerificationNotification(); 21 | 22 | return back()->with('status', 'verification-link-sent'); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/EmailVerificationPromptController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail() 19 | ? redirect()->intended(route('dashboard', absolute: false)) 20 | : Inertia::render('Auth/VerifyEmail', ['status' => session('status')]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/NewPasswordController.php: -------------------------------------------------------------------------------- 1 | $request->email, 26 | 'token' => $request->route('token'), 27 | ]); 28 | } 29 | 30 | /** 31 | * Handle an incoming new password request. 32 | * 33 | * @throws \Illuminate\Validation\ValidationException 34 | */ 35 | public function store(Request $request): RedirectResponse 36 | { 37 | $request->validate([ 38 | 'token' => 'required', 39 | 'email' => 'required|email', 40 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 41 | ]); 42 | 43 | // Here we will attempt to reset the user's password. If it is successful we 44 | // will update the password on an actual user model and persist it to the 45 | // database. Otherwise we will parse the error and return the response. 46 | $status = Password::reset( 47 | $request->only('email', 'password', 'password_confirmation', 'token'), 48 | function ($user) use ($request) { 49 | $user->forceFill([ 50 | 'password' => Hash::make($request->password), 51 | 'remember_token' => Str::random(60), 52 | ])->save(); 53 | 54 | event(new PasswordReset($user)); 55 | } 56 | ); 57 | 58 | // If the password was successfully reset, we will redirect the user back to 59 | // the application's home authenticated view. If there is an error we can 60 | // redirect them back to where they came from with their error message. 61 | if ($status == Password::PASSWORD_RESET) { 62 | return redirect()->route('login')->with('status', __($status)); 63 | } 64 | 65 | throw ValidationException::withMessages([ 66 | 'email' => [trans($status)], 67 | ]); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordController.php: -------------------------------------------------------------------------------- 1 | validate([ 19 | 'current_password' => ['required', 'current_password'], 20 | 'password' => ['required', Password::defaults(), 'confirmed'], 21 | ]); 22 | 23 | $request->user()->update([ 24 | 'password' => Hash::make($validated['password']), 25 | ]); 26 | 27 | return back(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/PasswordResetLinkController.php: -------------------------------------------------------------------------------- 1 | session('status'), 22 | ]); 23 | } 24 | 25 | /** 26 | * Handle an incoming password reset link request. 27 | * 28 | * @throws \Illuminate\Validation\ValidationException 29 | */ 30 | public function store(Request $request): RedirectResponse 31 | { 32 | $request->validate([ 33 | 'email' => 'required|email', 34 | ]); 35 | 36 | // We will send the password reset link to this user. Once we have attempted 37 | // to send the link, we will examine the response then see the message we 38 | // need to show to the user. Finally, we'll send out a proper response. 39 | $status = Password::sendResetLink( 40 | $request->only('email') 41 | ); 42 | 43 | if ($status == Password::RESET_LINK_SENT) { 44 | return back()->with('status', __($status)); 45 | } 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => [trans($status)], 49 | ]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/RegisteredUserController.php: -------------------------------------------------------------------------------- 1 | validate([ 34 | 'name' => 'required|string|max:255', 35 | 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class, 36 | 'password' => ['required', 'confirmed', Rules\Password::defaults()], 37 | ]); 38 | 39 | $user = User::create([ 40 | 'name' => $request->name, 41 | 'email' => $request->email, 42 | 'password' => Hash::make($request->password), 43 | ]); 44 | 45 | event(new Registered($user)); 46 | 47 | Auth::login($user); 48 | 49 | return redirect(route('dashboard', absolute: false)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /app/Http/Controllers/Auth/VerifyEmailController.php: -------------------------------------------------------------------------------- 1 | user()->hasVerifiedEmail()) { 18 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 19 | } 20 | 21 | if ($request->user()->markEmailAsVerified()) { 22 | event(new Verified($request->user())); 23 | } 24 | 25 | return redirect()->intended(route('dashboard', absolute: false).'?verified=1'); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Controllers/Controller.php: -------------------------------------------------------------------------------- 1 | user(); 14 | $totalPendingTasks = Task::query() 15 | ->where('status', 'pending') 16 | ->count(); 17 | $myPendingTasks = Task::query() 18 | ->where('status', 'pending') 19 | ->where('assigned_user_id', $user->id) 20 | ->count(); 21 | 22 | 23 | $totalProgressTasks = Task::query() 24 | ->where('status', 'in_progress') 25 | ->count(); 26 | $myProgressTasks = Task::query() 27 | ->where('status', 'in_progress') 28 | ->where('assigned_user_id', $user->id) 29 | ->count(); 30 | 31 | 32 | $totalCompletedTasks = Task::query() 33 | ->where('status', 'completed') 34 | ->count(); 35 | $myCompletedTasks = Task::query() 36 | ->where('status', 'completed') 37 | ->where('assigned_user_id', $user->id) 38 | ->count(); 39 | 40 | $activeTasks = Task::query() 41 | ->whereIn('status', ['pending', 'in_progress']) 42 | ->where('assigned_user_id', $user->id) 43 | ->limit(10) 44 | ->get(); 45 | $activeTasks = TaskResource::collection($activeTasks); 46 | return inertia( 47 | 'Dashboard', 48 | compact( 49 | 'totalPendingTasks', 50 | 'myPendingTasks', 51 | 'totalProgressTasks', 52 | 'myProgressTasks', 53 | 'totalCompletedTasks', 54 | 'myCompletedTasks', 55 | 'activeTasks' 56 | ) 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProfileController.php: -------------------------------------------------------------------------------- 1 | $request->user() instanceof MustVerifyEmail, 23 | 'status' => session('status'), 24 | ]); 25 | } 26 | 27 | /** 28 | * Update the user's profile information. 29 | */ 30 | public function update(ProfileUpdateRequest $request): RedirectResponse 31 | { 32 | $request->user()->fill($request->validated()); 33 | 34 | if ($request->user()->isDirty('email')) { 35 | $request->user()->email_verified_at = null; 36 | } 37 | 38 | $request->user()->save(); 39 | 40 | return Redirect::route('profile.edit'); 41 | } 42 | 43 | /** 44 | * Delete the user's account. 45 | */ 46 | public function destroy(Request $request): RedirectResponse 47 | { 48 | $request->validate([ 49 | 'password' => ['required', 'current_password'], 50 | ]); 51 | 52 | $user = $request->user(); 53 | 54 | Auth::logout(); 55 | 56 | $user->delete(); 57 | 58 | $request->session()->invalidate(); 59 | $request->session()->regenerateToken(); 60 | 61 | return Redirect::to('/'); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /app/Http/Controllers/ProjectController.php: -------------------------------------------------------------------------------- 1 | where("name", "like", "%" . request("name") . "%"); 28 | } 29 | if (request("status")) { 30 | $query->where("status", request("status")); 31 | } 32 | 33 | $projects = $query->orderBy($sortField, $sortDirection) 34 | ->paginate(10) 35 | ->onEachSide(1); 36 | 37 | return inertia("Project/Index", [ 38 | "projects" => ProjectResource::collection($projects), 39 | 'queryParams' => request()->query() ?: null, 40 | 'success' => session('success'), 41 | ]); 42 | } 43 | 44 | /** 45 | * Show the form for creating a new resource. 46 | */ 47 | public function create() 48 | { 49 | return inertia("Project/Create"); 50 | } 51 | 52 | /** 53 | * Store a newly created resource in storage. 54 | */ 55 | public function store(StoreProjectRequest $request) 56 | { 57 | $data = $request->validated(); 58 | /** @var $image \Illuminate\Http\UploadedFile */ 59 | $image = $data['image'] ?? null; 60 | $data['created_by'] = Auth::id(); 61 | $data['updated_by'] = Auth::id(); 62 | if ($image) { 63 | $data['image_path'] = $image->store('project/' . Str::random(), 'public'); 64 | } 65 | Project::create($data); 66 | 67 | return to_route('project.index') 68 | ->with('success', 'Project was created'); 69 | } 70 | 71 | /** 72 | * Display the specified resource. 73 | */ 74 | public function show(Project $project) 75 | { 76 | $query = $project->tasks(); 77 | 78 | $sortField = request("sort_field", 'created_at'); 79 | $sortDirection = request("sort_direction", "desc"); 80 | 81 | if (request("name")) { 82 | $query->where("name", "like", "%" . request("name") . "%"); 83 | } 84 | if (request("status")) { 85 | $query->where("status", request("status")); 86 | } 87 | 88 | $tasks = $query->orderBy($sortField, $sortDirection) 89 | ->paginate(10) 90 | ->onEachSide(1); 91 | return inertia('Project/Show', [ 92 | 'project' => new ProjectResource($project), 93 | "tasks" => TaskResource::collection($tasks), 94 | 'queryParams' => request()->query() ?: null, 95 | 'success' => session('success'), 96 | ]); 97 | } 98 | 99 | /** 100 | * Show the form for editing the specified resource. 101 | */ 102 | public function edit(Project $project) 103 | { 104 | return inertia('Project/Edit', [ 105 | 'project' => new ProjectResource($project), 106 | ]); 107 | } 108 | 109 | /** 110 | * Update the specified resource in storage. 111 | */ 112 | public function update(UpdateProjectRequest $request, Project $project) 113 | { 114 | $data = $request->validated(); 115 | $image = $data['image'] ?? null; 116 | $data['updated_by'] = Auth::id(); 117 | if ($image) { 118 | if ($project->image_path) { 119 | Storage::disk('public')->deleteDirectory(dirname($project->image_path)); 120 | } 121 | $data['image_path'] = $image->store('project/' . Str::random(), 'public'); 122 | } 123 | $project->update($data); 124 | 125 | return to_route('project.index') 126 | ->with('success', "Project \"$project->name\" was updated"); 127 | } 128 | 129 | /** 130 | * Remove the specified resource from storage. 131 | */ 132 | public function destroy(Project $project) 133 | { 134 | $name = $project->name; 135 | $project->delete(); 136 | if ($project->image_path) { 137 | Storage::disk('public')->deleteDirectory(dirname($project->image_path)); 138 | } 139 | return to_route('project.index') 140 | ->with('success', "Project \"$name\" was deleted"); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /app/Http/Controllers/TaskController.php: -------------------------------------------------------------------------------- 1 | where("name", "like", "%" . request("name") . "%"); 31 | } 32 | if (request("status")) { 33 | $query->where("status", request("status")); 34 | } 35 | 36 | $tasks = $query->orderBy($sortField, $sortDirection) 37 | ->paginate(10) 38 | ->onEachSide(1); 39 | 40 | return inertia("Task/Index", [ 41 | "tasks" => TaskResource::collection($tasks), 42 | 'queryParams' => request()->query() ?: null, 43 | 'success' => session('success'), 44 | ]); 45 | } 46 | 47 | /** 48 | * Show the form for creating a new resource. 49 | */ 50 | public function create() 51 | { 52 | $projects = Project::query()->orderBy('name', 'asc')->get(); 53 | $users = User::query()->orderBy('name', 'asc')->get(); 54 | 55 | return inertia("Task/Create", [ 56 | 'projects' => ProjectResource::collection($projects), 57 | 'users' => UserResource::collection($users), 58 | ]); 59 | } 60 | 61 | /** 62 | * Store a newly created resource in storage. 63 | */ 64 | public function store(StoreTaskRequest $request) 65 | { 66 | $data = $request->validated(); 67 | /** @var $image \Illuminate\Http\UploadedFile */ 68 | $image = $data['image'] ?? null; 69 | $data['created_by'] = Auth::id(); 70 | $data['updated_by'] = Auth::id(); 71 | if ($image) { 72 | $data['image_path'] = $image->store('task/' . Str::random(), 'public'); 73 | } 74 | Task::create($data); 75 | 76 | return to_route('task.index') 77 | ->with('success', 'Task was created'); 78 | } 79 | 80 | /** 81 | * Display the specified resource. 82 | */ 83 | public function show(Task $task) 84 | { 85 | return inertia('Task/Show', [ 86 | 'task' => new TaskResource($task), 87 | ]); 88 | } 89 | 90 | /** 91 | * Show the form for editing the specified resource. 92 | */ 93 | public function edit(Task $task) 94 | { 95 | $projects = Project::query()->orderBy('name', 'asc')->get(); 96 | $users = User::query()->orderBy('name', 'asc')->get(); 97 | 98 | return inertia("Task/Edit", [ 99 | 'task' => new TaskResource($task), 100 | 'projects' => ProjectResource::collection($projects), 101 | 'users' => UserResource::collection($users), 102 | ]); 103 | } 104 | 105 | /** 106 | * Update the specified resource in storage. 107 | */ 108 | public function update(UpdateTaskRequest $request, Task $task) 109 | { 110 | $data = $request->validated(); 111 | $image = $data['image'] ?? null; 112 | $data['updated_by'] = Auth::id(); 113 | if ($image) { 114 | if ($task->image_path) { 115 | Storage::disk('public')->deleteDirectory(dirname($task->image_path)); 116 | } 117 | $data['image_path'] = $image->store('task/' . Str::random(), 'public'); 118 | } 119 | $task->update($data); 120 | 121 | return to_route('task.index') 122 | ->with('success', "Task \"$task->name\" was updated"); 123 | } 124 | 125 | /** 126 | * Remove the specified resource from storage. 127 | */ 128 | public function destroy(Task $task) 129 | { 130 | $name = $task->name; 131 | $task->delete(); 132 | if ($task->image_path) { 133 | Storage::disk('public')->deleteDirectory(dirname($task->image_path)); 134 | } 135 | return to_route('task.index') 136 | ->with('success', "Task \"$name\" was deleted"); 137 | } 138 | 139 | public function myTasks() 140 | { 141 | $user = auth()->user(); 142 | $query = Task::query()->where('assigned_user_id', $user->id); 143 | 144 | $sortField = request("sort_field", 'created_at'); 145 | $sortDirection = request("sort_direction", "desc"); 146 | 147 | if (request("name")) { 148 | $query->where("name", "like", "%" . request("name") . "%"); 149 | } 150 | if (request("status")) { 151 | $query->where("status", request("status")); 152 | } 153 | 154 | $tasks = $query->orderBy($sortField, $sortDirection) 155 | ->paginate(10) 156 | ->onEachSide(1); 157 | 158 | return inertia("Task/Index", [ 159 | "tasks" => TaskResource::collection($tasks), 160 | 'queryParams' => request()->query() ?: null, 161 | 'success' => session('success'), 162 | ]); 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /app/Http/Controllers/UserController.php: -------------------------------------------------------------------------------- 1 | where("name", "like", "%" . request("name") . "%"); 24 | } 25 | if (request("email")) { 26 | $query->where("email", "like", "%" . request("email") . "%"); 27 | } 28 | 29 | $users = $query->orderBy($sortField, $sortDirection) 30 | ->paginate(10) 31 | ->onEachSide(1); 32 | 33 | return inertia("User/Index", [ 34 | "users" => UserCrudResource::collection($users), 35 | 'queryParams' => request()->query() ?: null, 36 | 'success' => session('success'), 37 | ]); 38 | } 39 | 40 | /** 41 | * Show the form for creating a new resource. 42 | */ 43 | public function create() 44 | { 45 | return inertia("User/Create"); 46 | } 47 | 48 | /** 49 | * Store a newly created resource in storage. 50 | */ 51 | public function store(StoreUserRequest $request) 52 | { 53 | $data = $request->validated(); 54 | $data['email_verified_at'] = time(); 55 | $data['password'] = bcrypt($data['password']); 56 | User::create($data); 57 | 58 | return to_route('user.index') 59 | ->with('success', 'User was created'); 60 | } 61 | 62 | /** 63 | * Display the specified resource. 64 | */ 65 | public function show(User $user) 66 | { 67 | // 68 | } 69 | 70 | /** 71 | * Show the form for editing the specified resource. 72 | */ 73 | public function edit(User $user) 74 | { 75 | return inertia('User/Edit', [ 76 | 'user' => new UserCrudResource($user), 77 | ]); 78 | } 79 | 80 | /** 81 | * Update the specified resource in storage. 82 | */ 83 | public function update(UpdateUserRequest $request, User $user) 84 | { 85 | $data = $request->validated(); 86 | $password = $data['password'] ?? null; 87 | if ($password) { 88 | $data['password'] = bcrypt($password); 89 | } else { 90 | unset($data['password']); 91 | } 92 | $user->update($data); 93 | 94 | return to_route('user.index') 95 | ->with('success', "User \"$user->name\" was updated"); 96 | } 97 | 98 | /** 99 | * Remove the specified resource from storage. 100 | */ 101 | public function destroy(User $user) 102 | { 103 | $name = $user->name; 104 | $user->delete(); 105 | return to_route('user.index') 106 | ->with('success', "User \"$name\" was deleted"); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /app/Http/Middleware/HandleInertiaRequests.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | public function share(Request $request): array 31 | { 32 | return [ 33 | ...parent::share($request), 34 | 'auth' => [ 35 | 'user' => $request->user(), 36 | ], 37 | ]; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Http/Requests/Auth/LoginRequest.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public function rules(): array 28 | { 29 | return [ 30 | 'email' => ['required', 'string', 'email'], 31 | 'password' => ['required', 'string'], 32 | ]; 33 | } 34 | 35 | /** 36 | * Attempt to authenticate the request's credentials. 37 | * 38 | * @throws \Illuminate\Validation\ValidationException 39 | */ 40 | public function authenticate(): void 41 | { 42 | $this->ensureIsNotRateLimited(); 43 | 44 | if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) { 45 | RateLimiter::hit($this->throttleKey()); 46 | 47 | throw ValidationException::withMessages([ 48 | 'email' => trans('auth.failed'), 49 | ]); 50 | } 51 | 52 | RateLimiter::clear($this->throttleKey()); 53 | } 54 | 55 | /** 56 | * Ensure the login request is not rate limited. 57 | * 58 | * @throws \Illuminate\Validation\ValidationException 59 | */ 60 | public function ensureIsNotRateLimited(): void 61 | { 62 | if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) { 63 | return; 64 | } 65 | 66 | event(new Lockout($this)); 67 | 68 | $seconds = RateLimiter::availableIn($this->throttleKey()); 69 | 70 | throw ValidationException::withMessages([ 71 | 'email' => trans('auth.throttle', [ 72 | 'seconds' => $seconds, 73 | 'minutes' => ceil($seconds / 60), 74 | ]), 75 | ]); 76 | } 77 | 78 | /** 79 | * Get the rate limiting throttle key for the request. 80 | */ 81 | public function throttleKey(): string 82 | { 83 | return Str::transliterate(Str::lower($this->input('email')).'|'.$this->ip()); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /app/Http/Requests/ProfileUpdateRequest.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function rules(): array 17 | { 18 | return [ 19 | 'name' => ['required', 'string', 'max:255'], 20 | 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', Rule::unique(User::class)->ignore($this->user()->id)], 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreProjectRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | "name" => ['required', 'max:255'], 27 | 'image' => ['nullable', 'image'], 28 | "description" => ['nullable', 'string'], 29 | 'due_date' => ['nullable', 'date'], 30 | 'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])] 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreTaskRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | "name" => ['required', 'max:255'], 27 | 'image' => ['nullable', 'image'], 28 | "description" => ['nullable', 'string'], 29 | 'due_date' => ['nullable', 'date'], 30 | 'project_id' => ['required', 'exists:projects,id'], 31 | 'assigned_user_id' => ['required', 'exists:users,id'], 32 | 'status' => [ 33 | 'required', 34 | Rule::in(['pending', 'in_progress', 'completed']) 35 | ], 36 | 'priority' => [ 37 | 'required', 38 | Rule::in(['low', 'medium', 'high']) 39 | ] 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Requests/StoreUserRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | "name" => ["required", "string", "max:255"], 27 | "email" => ["required", "string", "email", "unique:users,email"], 28 | "password" => [ 29 | "required", 30 | 'confirmed', 31 | Password::min(8)->letters()->symbols(), 32 | ], 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateProjectRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | "name" => ['required', 'max:255'], 27 | 'image' => ['nullable', 'image'], 28 | "description" => ['nullable', 'string'], 29 | 'due_date' => ['nullable', 'date'], 30 | 'status' => ['required', Rule::in(['pending', 'in_progress', 'completed'])] 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateTaskRequest.php: -------------------------------------------------------------------------------- 1 | |string> 22 | */ 23 | public function rules(): array 24 | { 25 | return [ 26 | "name" => ['required', 'max:255'], 27 | 'image' => ['nullable', 'image'], 28 | "description" => ['nullable', 'string'], 29 | 'due_date' => ['nullable', 'date'], 30 | 'project_id' => ['required', 'exists:projects,id'], 31 | 'assigned_user_id' => ['required', 'exists:users,id'], 32 | 'status' => [ 33 | 'required', 34 | Rule::in(['pending', 'in_progress', 'completed']) 35 | ], 36 | 'priority' => [ 37 | 'required', 38 | Rule::in(['low', 'medium', 'high']) 39 | ] 40 | ]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /app/Http/Requests/UpdateUserRequest.php: -------------------------------------------------------------------------------- 1 | |string> 23 | */ 24 | public function rules(): array 25 | { 26 | $user = $this->route("user"); 27 | return [ 28 | "name" => ["required", "string", "max:255"], 29 | "email" => [ 30 | "required", 31 | "email", 32 | Rule::unique('users')->ignore($user->id), 33 | ], 34 | "password" => [ 35 | 'nullable', 36 | 'confirmed', 37 | Password::min(8)->letters()->symbols(), 38 | ], 39 | ]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app/Http/Resources/ProjectResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | return [ 21 | 'id' => $this->id, 22 | 'name' => $this->name, 23 | 'description' => $this->description, 24 | 'created_at' => (new Carbon($this->created_at))->format('Y-m-d'), 25 | 'due_date' => (new Carbon($this->due_date))->format('Y-m-d'), 26 | 'status' => $this->status, 27 | 'image_path' => $this->image_path && !(str_starts_with($this->image_path, 'http')) ? 28 | Storage::url($this->image_path) : $this->image_path, 29 | 'createdBy' => new UserResource($this->createdBy), 30 | 'updatedBy' => new UserResource($this->updatedBy), 31 | ]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/Http/Resources/TaskResource.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | public function toArray(Request $request): array 21 | { 22 | return [ 23 | 'id' => $this->id, 24 | 'name' => $this->name, 25 | 'description' => $this->description, 26 | 'created_at' => (new Carbon($this->created_at))->format('Y-m-d'), 27 | 'due_date' => (new Carbon($this->due_date))->format('Y-m-d'), 28 | 'status' => $this->status, 29 | 'priority' => $this->priority, 30 | 'image_path' => $this->image_path && !(str_starts_with($this->image_path, 'http')) ? 31 | Storage::url($this->image_path) : '', 32 | 'project_id' => $this->project_id, 33 | 'project' => new ProjectResource($this->project), 34 | 'assigned_user_id' => $this->assigned_user_id, 35 | 'assignedUser' => $this->assignedUser ? new UserResource($this->assignedUser) : null, 36 | 'createdBy' => new UserResource($this->createdBy), 37 | 'updatedBy' => new UserResource($this->updatedBy), 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /app/Http/Resources/UserCrudResource.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function toArray(Request $request): array 19 | { 20 | return [ 21 | "id" => $this->id, 22 | "name" => $this->name, 23 | "email" => $this->email, 24 | 'created_at' => (new Carbon($this->created_at))->format('Y-m-d H:i:s'), 25 | ]; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/Http/Resources/UserResource.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function toArray(Request $request): array 17 | { 18 | return [ 19 | "id" => $this->id, 20 | "name" => $this->name, 21 | "email" => $this->email 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /app/Models/Project.php: -------------------------------------------------------------------------------- 1 | hasMany(Task::class); 17 | } 18 | 19 | public function createdBy() 20 | { 21 | return $this->belongsTo(User::class, 'created_by'); 22 | } 23 | 24 | public function updatedBy() 25 | { 26 | return $this->belongsTo(User::class, 'updated_by'); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /app/Models/Task.php: -------------------------------------------------------------------------------- 1 | belongsTo(Project::class); 28 | } 29 | 30 | public function assignedUser() 31 | { 32 | return $this->belongsTo(User::class, 'assigned_user_id'); 33 | } 34 | 35 | public function createdBy() 36 | { 37 | return $this->belongsTo(User::class, 'created_by'); 38 | } 39 | 40 | public function updatedBy() 41 | { 42 | return $this->belongsTo(User::class, 'updated_by'); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/Models/User.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | protected $fillable = [ 21 | 'name', 22 | 'email', 23 | 'password', 24 | 'email_verified_at' 25 | ]; 26 | 27 | /** 28 | * The attributes that should be hidden for serialization. 29 | * 30 | * @var array 31 | */ 32 | protected $hidden = [ 33 | 'password', 34 | 'remember_token', 35 | ]; 36 | 37 | /** 38 | * Get the attributes that should be cast. 39 | * 40 | * @return array 41 | */ 42 | protected function casts(): array 43 | { 44 | return [ 45 | 'email_verified_at' => 'datetime', 46 | 'password' => 'hashed', 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /app/Providers/AppServiceProvider.php: -------------------------------------------------------------------------------- 1 | handleCommand(new ArgvInput); 14 | 15 | exit($status); 16 | -------------------------------------------------------------------------------- /bootstrap/app.php: -------------------------------------------------------------------------------- 1 | withProviders() 9 | ->withRouting( 10 | web: __DIR__.'/../routes/web.php', 11 | // api: __DIR__.'/../routes/api.php', 12 | commands: __DIR__.'/../routes/console.php', 13 | // channels: __DIR__.'/../routes/channels.php', 14 | health: '/up', 15 | ) 16 | ->withMiddleware(function (Middleware $middleware) { 17 | $middleware->web(append: [ 18 | \App\Http\Middleware\HandleInertiaRequests::class, 19 | \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, 20 | ]); 21 | 22 | // 23 | }) 24 | ->withExceptions(function (Exceptions $exceptions) { 25 | // 26 | })->create(); 27 | -------------------------------------------------------------------------------- /bootstrap/cache/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /bootstrap/providers.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class ProjectFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake()->sentence(), 21 | 'description' => fake()->realText(), 22 | 'due_date' => fake()->dateTimeBetween('now', '+1 year'), 23 | 'status' => fake()->randomElement(['pending', 'in_progress', 'completed']), 24 | 'image_path' => fake()->imageUrl(), 25 | 'created_by' => 1, 26 | 'updated_by' => 1, 27 | 'created_at' => time(), 28 | 'updated_at' => time(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /database/factories/TaskFactory.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class TaskFactory extends Factory 11 | { 12 | /** 13 | * Define the model's default state. 14 | * 15 | * @return array 16 | */ 17 | public function definition(): array 18 | { 19 | return [ 20 | 'name' => fake()->sentence(), 21 | 'description' => fake()->realText(), 22 | 'due_date' => fake()->dateTimeBetween('now', '+1 year'), 23 | 'status' => fake() 24 | ->randomElement(['pending', 'in_progress', 'completed']), 25 | 'priority' => fake() 26 | ->randomElement(['low', 'medium', 'high']), 27 | 'image_path' => fake()->imageUrl(), 28 | 'assigned_user_id' => fake()->randomElement([1, 2]), 29 | 'created_by' => 1, 30 | 'updated_by' => 1, 31 | 'created_at' => time(), 32 | 'updated_at' => time(), 33 | ]; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /database/factories/UserFactory.php: -------------------------------------------------------------------------------- 1 | 11 | */ 12 | class UserFactory extends Factory 13 | { 14 | /** 15 | * The current password being used by the factory. 16 | */ 17 | protected static ?string $password; 18 | 19 | /** 20 | * Define the model's default state. 21 | * 22 | * @return array 23 | */ 24 | public function definition(): array 25 | { 26 | return [ 27 | 'name' => fake()->name(), 28 | 'email' => fake()->unique()->safeEmail(), 29 | 'email_verified_at' => now(), 30 | 'password' => static::$password ??= Hash::make('password'), 31 | 'remember_token' => Str::random(10), 32 | ]; 33 | } 34 | 35 | /** 36 | * Indicate that the model's email address should be unverified. 37 | */ 38 | public function unverified(): static 39 | { 40 | return $this->state(fn (array $attributes) => [ 41 | 'email_verified_at' => null, 42 | ]); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000000_create_users_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('name'); 17 | $table->string('email')->unique(); 18 | $table->timestamp('email_verified_at')->nullable(); 19 | $table->string('password'); 20 | $table->rememberToken(); 21 | $table->timestamps(); 22 | }); 23 | 24 | Schema::create('password_reset_tokens', function (Blueprint $table) { 25 | $table->string('email')->primary(); 26 | $table->string('token'); 27 | $table->timestamp('created_at')->nullable(); 28 | }); 29 | 30 | Schema::create('sessions', function (Blueprint $table) { 31 | $table->string('id')->primary(); 32 | $table->foreignId('user_id')->nullable()->index(); 33 | $table->string('ip_address', 45)->nullable(); 34 | $table->text('user_agent')->nullable(); 35 | $table->longText('payload'); 36 | $table->integer('last_activity')->index(); 37 | }); 38 | } 39 | 40 | /** 41 | * Reverse the migrations. 42 | */ 43 | public function down(): void 44 | { 45 | Schema::dropIfExists('users'); 46 | Schema::dropIfExists('password_reset_tokens'); 47 | Schema::dropIfExists('sessions'); 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000001_create_cache_table.php: -------------------------------------------------------------------------------- 1 | string('key')->primary(); 16 | $table->mediumText('value'); 17 | $table->integer('expiration'); 18 | }); 19 | 20 | Schema::create('cache_locks', function (Blueprint $table) { 21 | $table->string('key')->primary(); 22 | $table->string('owner'); 23 | $table->integer('expiration'); 24 | }); 25 | } 26 | 27 | /** 28 | * Reverse the migrations. 29 | */ 30 | public function down(): void 31 | { 32 | Schema::dropIfExists('cache'); 33 | Schema::dropIfExists('cache_locks'); 34 | } 35 | }; 36 | -------------------------------------------------------------------------------- /database/migrations/0001_01_01_000002_create_jobs_table.php: -------------------------------------------------------------------------------- 1 | id(); 16 | $table->string('queue')->index(); 17 | $table->longText('payload'); 18 | $table->unsignedTinyInteger('attempts'); 19 | $table->unsignedInteger('reserved_at')->nullable(); 20 | $table->unsignedInteger('available_at'); 21 | $table->unsignedInteger('created_at'); 22 | }); 23 | 24 | Schema::create('job_batches', function (Blueprint $table) { 25 | $table->string('id')->primary(); 26 | $table->string('name'); 27 | $table->integer('total_jobs'); 28 | $table->integer('pending_jobs'); 29 | $table->integer('failed_jobs'); 30 | $table->longText('failed_job_ids'); 31 | $table->mediumText('options')->nullable(); 32 | $table->integer('cancelled_at')->nullable(); 33 | $table->integer('created_at'); 34 | $table->integer('finished_at')->nullable(); 35 | }); 36 | 37 | Schema::create('failed_jobs', function (Blueprint $table) { 38 | $table->id(); 39 | $table->string('uuid')->unique(); 40 | $table->text('connection'); 41 | $table->text('queue'); 42 | $table->longText('payload'); 43 | $table->longText('exception'); 44 | $table->timestamp('failed_at')->useCurrent(); 45 | }); 46 | } 47 | 48 | /** 49 | * Reverse the migrations. 50 | */ 51 | public function down(): void 52 | { 53 | Schema::dropIfExists('jobs'); 54 | Schema::dropIfExists('job_batches'); 55 | Schema::dropIfExists('failed_jobs'); 56 | } 57 | }; 58 | -------------------------------------------------------------------------------- /database/migrations/2024_02_03_102720_create_projects_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name'); 16 | $table->longText('description')->nullable(); 17 | $table->timestamp('due_date')->nullable(); 18 | $table->string('status'); 19 | $table->string('image_path')->nullable(); 20 | $table->foreignId('created_by')->constrained('users'); 21 | $table->foreignId('updated_by')->constrained('users'); 22 | $table->timestamps(); 23 | }); 24 | } 25 | 26 | /** 27 | * Reverse the migrations. 28 | */ 29 | public function down(): void 30 | { 31 | Schema::dropIfExists('projects'); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /database/migrations/2024_02_03_102804_create_tasks_table.php: -------------------------------------------------------------------------------- 1 | id(); 15 | $table->string('name'); 16 | $table->longText('description')->nullable(); 17 | $table->string('image_path')->nullable(); 18 | $table->string('status'); 19 | $table->string('priority'); 20 | $table->string('due_date')->nullable(); 21 | $table->foreignId('assigned_user_id')->constrained('users'); 22 | $table->foreignId('created_by')->constrained('users'); 23 | $table->foreignId('updated_by')->constrained('users'); 24 | $table->foreignId('project_id')->constrained('projects'); 25 | $table->timestamps(); 26 | }); 27 | } 28 | 29 | /** 30 | * Reverse the migrations. 31 | */ 32 | public function down(): void 33 | { 34 | Schema::dropIfExists('tasks'); 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /database/seeders/DatabaseSeeder.php: -------------------------------------------------------------------------------- 1 | create(); 18 | 19 | User::factory()->create([ 20 | 'id' => 1, 21 | 'name' => 'Zura', 22 | 'email' => 'zura@example.com', 23 | 'password' => bcrypt('123.321A'), 24 | 'email_verified_at' => time() 25 | ]); 26 | User::factory()->create([ 27 | 'id' => 2, 28 | 'name' => 'John Smith', 29 | 'email' => 'john@example.com', 30 | 'password' => bcrypt('123.321A'), 31 | 'email_verified_at' => time() 32 | ]); 33 | 34 | Project::factory() 35 | ->count(30) 36 | ->hasTasks(30) 37 | ->create(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "paths": { 5 | "@/*": ["resources/js/*"], 6 | "ziggy-js": ["./vendor/tightenco/ziggy"] 7 | } 8 | }, 9 | "exclude": ["node_modules", "public"] 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build" 7 | }, 8 | "devDependencies": { 9 | "@headlessui/react": "^1.4.2", 10 | "@inertiajs/react": "^1.0.0", 11 | "@tailwindcss/forms": "^0.5.3", 12 | "@vitejs/plugin-react": "^4.2.0", 13 | "autoprefixer": "^10.4.12", 14 | "axios": "^1.6.4", 15 | "laravel-vite-plugin": "^1.0", 16 | "postcss": "^8.4.31", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "tailwindcss": "^3.2.1", 20 | "vite": "^5.0" 21 | }, 22 | "dependencies": { 23 | "@heroicons/react": "^2.1.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | 33 | 34 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /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/thecodeholic/laravel11-react-spa/b6c1d436140ce32e256b2143f3d44dc7fe6fdeb3/public/favicon.ico -------------------------------------------------------------------------------- /public/index.php: -------------------------------------------------------------------------------- 1 | handleRequest(Request::capture()); 18 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /resources/css/app.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /resources/js/Components/ApplicationLogo.jsx: -------------------------------------------------------------------------------- 1 | export default function ApplicationLogo(props) { 2 | return ( 3 | 4 | 5 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /resources/js/Components/Checkbox.jsx: -------------------------------------------------------------------------------- 1 | export default function Checkbox({ className = '', ...props }) { 2 | return ( 3 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /resources/js/Components/DangerButton.jsx: -------------------------------------------------------------------------------- 1 | export default function DangerButton({ className = '', disabled, children, ...props }) { 2 | return ( 3 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /resources/js/Components/Dropdown.jsx: -------------------------------------------------------------------------------- 1 | import { useState, createContext, useContext, Fragment } from 'react'; 2 | import { Link } from '@inertiajs/react'; 3 | import { Transition } from '@headlessui/react'; 4 | 5 | const DropDownContext = createContext(); 6 | 7 | const Dropdown = ({ children }) => { 8 | const [open, setOpen] = useState(false); 9 | 10 | const toggleOpen = () => { 11 | setOpen((previousState) => !previousState); 12 | }; 13 | 14 | return ( 15 | 16 |
{children}
17 |
18 | ); 19 | }; 20 | 21 | const Trigger = ({ children }) => { 22 | const { open, setOpen, toggleOpen } = useContext(DropDownContext); 23 | 24 | return ( 25 | <> 26 |
{children}
27 | 28 | {open &&
setOpen(false)}>
} 29 | 30 | ); 31 | }; 32 | 33 | const Content = ({ align = 'right', width = '48', contentClasses = 'py-1 bg-white dark:bg-gray-700', children }) => { 34 | const { open, setOpen } = useContext(DropDownContext); 35 | 36 | let alignmentClasses = 'origin-top'; 37 | 38 | if (align === 'left') { 39 | alignmentClasses = 'ltr:origin-top-left rtl:origin-top-right start-0'; 40 | } else if (align === 'right') { 41 | alignmentClasses = 'ltr:origin-top-right rtl:origin-top-left end-0'; 42 | } 43 | 44 | let widthClasses = ''; 45 | 46 | if (width === '48') { 47 | widthClasses = 'w-48'; 48 | } 49 | 50 | return ( 51 | <> 52 | 62 |
setOpen(false)} 65 | > 66 |
{children}
67 |
68 |
69 | 70 | ); 71 | }; 72 | 73 | const DropdownLink = ({ className = '', children, ...props }) => { 74 | return ( 75 | 82 | {children} 83 | 84 | ); 85 | }; 86 | 87 | Dropdown.Trigger = Trigger; 88 | Dropdown.Content = Content; 89 | Dropdown.Link = DropdownLink; 90 | 91 | export default Dropdown; 92 | -------------------------------------------------------------------------------- /resources/js/Components/InputError.jsx: -------------------------------------------------------------------------------- 1 | export default function InputError({ message, className = '', ...props }) { 2 | return message ? ( 3 |

4 | {message} 5 |

6 | ) : null; 7 | } 8 | -------------------------------------------------------------------------------- /resources/js/Components/InputLabel.jsx: -------------------------------------------------------------------------------- 1 | export default function InputLabel({ value, className = '', children, ...props }) { 2 | return ( 3 | 6 | ); 7 | } 8 | -------------------------------------------------------------------------------- /resources/js/Components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Dialog, Transition } from '@headlessui/react'; 3 | 4 | export default function Modal({ children, show = false, maxWidth = '2xl', closeable = true, onClose = () => {} }) { 5 | const close = () => { 6 | if (closeable) { 7 | onClose(); 8 | } 9 | }; 10 | 11 | const maxWidthClass = { 12 | sm: 'sm:max-w-sm', 13 | md: 'sm:max-w-md', 14 | lg: 'sm:max-w-lg', 15 | xl: 'sm:max-w-xl', 16 | '2xl': 'sm:max-w-2xl', 17 | }[maxWidth]; 18 | 19 | return ( 20 | 21 | 27 | 36 |
37 | 38 | 39 | 48 | 51 | {children} 52 | 53 | 54 |
55 |
56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /resources/js/Components/NavLink.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@inertiajs/react'; 2 | 3 | export default function NavLink({ active = false, className = '', children, ...props }) { 4 | return ( 5 | 15 | {children} 16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/Components/Pagination.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from "@inertiajs/react"; 2 | 3 | export default function Pagination({ links }) { 4 | return ( 5 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /resources/js/Components/PrimaryButton.jsx: -------------------------------------------------------------------------------- 1 | export default function PrimaryButton({ className = '', disabled, children, ...props }) { 2 | return ( 3 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /resources/js/Components/ResponsiveNavLink.jsx: -------------------------------------------------------------------------------- 1 | import { Link } from '@inertiajs/react'; 2 | 3 | export default function ResponsiveNavLink({ active = false, className = '', children, ...props }) { 4 | return ( 5 | 13 | {children} 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/Components/SecondaryButton.jsx: -------------------------------------------------------------------------------- 1 | export default function SecondaryButton({ type = 'button', className = '', disabled, children, ...props }) { 2 | return ( 3 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /resources/js/Components/SelectInput.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useRef } from "react"; 2 | 3 | export default forwardRef(function SelectInput( 4 | { className = "", children, ...props }, 5 | ref 6 | ) { 7 | const input = ref ? ref : useRef(); 8 | 9 | return ( 10 | 20 | ); 21 | }); 22 | -------------------------------------------------------------------------------- /resources/js/Components/TableHeading.jsx: -------------------------------------------------------------------------------- 1 | import { ChevronUpIcon, ChevronDownIcon } from "@heroicons/react/16/solid"; 2 | 3 | export default function TableHeading({ 4 | name, 5 | sortable = true, 6 | sort_field = null, 7 | sort_direction = null, 8 | sortChanged = () => {}, 9 | children, 10 | }) { 11 | return ( 12 | sortChanged(name)}> 13 |
14 | {children} 15 | {sortable && ( 16 |
17 | 25 | 33 |
34 | )} 35 |
36 | 37 | ); 38 | } 39 | -------------------------------------------------------------------------------- /resources/js/Components/TextAreaInput.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useEffect, useRef } from "react"; 2 | 3 | export default forwardRef(function TextAreaInput( 4 | { className = "", isFocused = false, children, ...props }, 5 | ref 6 | ) { 7 | const input = ref ? ref : useRef(); 8 | 9 | useEffect(() => { 10 | if (isFocused) { 11 | input.current.focus(); 12 | } 13 | }, []); 14 | 15 | return ( 16 | 26 | ); 27 | }); 28 | -------------------------------------------------------------------------------- /resources/js/Components/TextInput.jsx: -------------------------------------------------------------------------------- 1 | import { forwardRef, useEffect, useRef } from 'react'; 2 | 3 | export default forwardRef(function TextInput({ type = 'text', className = '', isFocused = false, ...props }, ref) { 4 | const input = ref ? ref : useRef(); 5 | 6 | useEffect(() => { 7 | if (isFocused) { 8 | input.current.focus(); 9 | } 10 | }, []); 11 | 12 | return ( 13 | 22 | ); 23 | }); 24 | -------------------------------------------------------------------------------- /resources/js/Layouts/AuthenticatedLayout.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from "react"; 2 | import ApplicationLogo from "@/Components/ApplicationLogo"; 3 | import Dropdown from "@/Components/Dropdown"; 4 | import NavLink from "@/Components/NavLink"; 5 | import ResponsiveNavLink from "@/Components/ResponsiveNavLink"; 6 | import { Link } from "@inertiajs/react"; 7 | 8 | export default function AuthenticatedLayout({ user, header, children }) { 9 | const [showingNavigationDropdown, setShowingNavigationDropdown] = 10 | useState(false); 11 | 12 | return ( 13 |
14 | 179 | 180 | {header && ( 181 |
182 |
183 | {header} 184 |
185 |
186 | )} 187 | 188 |
{children}
189 |
190 | ); 191 | } 192 | -------------------------------------------------------------------------------- /resources/js/Layouts/GuestLayout.jsx: -------------------------------------------------------------------------------- 1 | import ApplicationLogo from '@/Components/ApplicationLogo'; 2 | import { Link } from '@inertiajs/react'; 3 | 4 | export default function Guest({ children }) { 5 | return ( 6 |
7 |
8 | 9 | 10 | 11 |
12 | 13 |
14 | {children} 15 |
16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ConfirmPassword.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import GuestLayout from '@/Layouts/GuestLayout'; 3 | import InputError from '@/Components/InputError'; 4 | import InputLabel from '@/Components/InputLabel'; 5 | import PrimaryButton from '@/Components/PrimaryButton'; 6 | import TextInput from '@/Components/TextInput'; 7 | import { Head, useForm } from '@inertiajs/react'; 8 | 9 | export default function ConfirmPassword() { 10 | const { data, setData, post, processing, errors, reset } = useForm({ 11 | password: '', 12 | }); 13 | 14 | useEffect(() => { 15 | return () => { 16 | reset('password'); 17 | }; 18 | }, []); 19 | 20 | const submit = (e) => { 21 | e.preventDefault(); 22 | 23 | post(route('password.confirm')); 24 | }; 25 | 26 | return ( 27 | 28 | 29 | 30 |
31 | This is a secure area of the application. Please confirm your password before continuing. 32 |
33 | 34 |
35 |
36 | 37 | 38 | setData('password', e.target.value)} 46 | /> 47 | 48 | 49 |
50 | 51 |
52 | 53 | Confirm 54 | 55 |
56 |
57 |
58 | ); 59 | } 60 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ForgotPassword.jsx: -------------------------------------------------------------------------------- 1 | import GuestLayout from '@/Layouts/GuestLayout'; 2 | import InputError from '@/Components/InputError'; 3 | import PrimaryButton from '@/Components/PrimaryButton'; 4 | import TextInput from '@/Components/TextInput'; 5 | import { Head, useForm } from '@inertiajs/react'; 6 | 7 | export default function ForgotPassword({ status }) { 8 | const { data, setData, post, processing, errors } = useForm({ 9 | email: '', 10 | }); 11 | 12 | const submit = (e) => { 13 | e.preventDefault(); 14 | 15 | post(route('password.email')); 16 | }; 17 | 18 | return ( 19 | 20 | 21 | 22 |
23 | Forgot your password? No problem. Just let us know your email address and we will email you a password 24 | reset link that will allow you to choose a new one. 25 |
26 | 27 | {status &&
{status}
} 28 | 29 |
30 | setData('email', e.target.value)} 38 | /> 39 | 40 | 41 | 42 |
43 | 44 | Email Password Reset Link 45 | 46 |
47 | 48 |
49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/Login.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import Checkbox from "@/Components/Checkbox"; 3 | import GuestLayout from "@/Layouts/GuestLayout"; 4 | import InputError from "@/Components/InputError"; 5 | import InputLabel from "@/Components/InputLabel"; 6 | import PrimaryButton from "@/Components/PrimaryButton"; 7 | import TextInput from "@/Components/TextInput"; 8 | import { Head, Link, useForm } from "@inertiajs/react"; 9 | 10 | export default function Login({ status, canResetPassword }) { 11 | const { data, setData, post, processing, errors, reset } = useForm({ 12 | email: "", 13 | password: "", 14 | remember: false, 15 | }); 16 | 17 | useEffect(() => { 18 | return () => { 19 | reset("password"); 20 | }; 21 | }, []); 22 | 23 | const submit = (e) => { 24 | e.preventDefault(); 25 | 26 | post(route("login")); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 | {status && ( 34 |
35 | {status} 36 |
37 | )} 38 | 39 |
40 |
41 | 42 | 43 | setData("email", e.target.value)} 52 | /> 53 | 54 | 55 |
56 | 57 |
58 | 59 | 60 | setData("password", e.target.value)} 69 | /> 70 | 71 | 72 |
73 | 74 |
75 | 87 |
88 | 89 |
90 | {canResetPassword && ( 91 | 95 | Forgot your password? 96 | 97 | )} 98 | 99 | 100 | Log in 101 | 102 |
103 |
104 |
105 | ); 106 | } 107 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/Register.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import GuestLayout from '@/Layouts/GuestLayout'; 3 | import InputError from '@/Components/InputError'; 4 | import InputLabel from '@/Components/InputLabel'; 5 | import PrimaryButton from '@/Components/PrimaryButton'; 6 | import TextInput from '@/Components/TextInput'; 7 | import { Head, Link, useForm } from '@inertiajs/react'; 8 | 9 | export default function Register() { 10 | const { data, setData, post, processing, errors, reset } = useForm({ 11 | name: '', 12 | email: '', 13 | password: '', 14 | password_confirmation: '', 15 | }); 16 | 17 | useEffect(() => { 18 | return () => { 19 | reset('password', 'password_confirmation'); 20 | }; 21 | }, []); 22 | 23 | const submit = (e) => { 24 | e.preventDefault(); 25 | 26 | post(route('register')); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | setData('name', e.target.value)} 45 | required 46 | /> 47 | 48 | 49 |
50 | 51 |
52 | 53 | 54 | setData('email', e.target.value)} 62 | required 63 | /> 64 | 65 | 66 |
67 | 68 |
69 | 70 | 71 | setData('password', e.target.value)} 79 | required 80 | /> 81 | 82 | 83 |
84 | 85 |
86 | 87 | 88 | setData('password_confirmation', e.target.value)} 96 | required 97 | /> 98 | 99 | 100 |
101 | 102 |
103 | 107 | Already registered? 108 | 109 | 110 | 111 | Register 112 | 113 |
114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/ResetPassword.jsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import GuestLayout from '@/Layouts/GuestLayout'; 3 | import InputError from '@/Components/InputError'; 4 | import InputLabel from '@/Components/InputLabel'; 5 | import PrimaryButton from '@/Components/PrimaryButton'; 6 | import TextInput from '@/Components/TextInput'; 7 | import { Head, useForm } from '@inertiajs/react'; 8 | 9 | export default function ResetPassword({ token, email }) { 10 | const { data, setData, post, processing, errors, reset } = useForm({ 11 | token: token, 12 | email: email, 13 | password: '', 14 | password_confirmation: '', 15 | }); 16 | 17 | useEffect(() => { 18 | return () => { 19 | reset('password', 'password_confirmation'); 20 | }; 21 | }, []); 22 | 23 | const submit = (e) => { 24 | e.preventDefault(); 25 | 26 | post(route('password.store')); 27 | }; 28 | 29 | return ( 30 | 31 | 32 | 33 |
34 |
35 | 36 | 37 | setData('email', e.target.value)} 46 | /> 47 | 48 | 49 |
50 | 51 |
52 | 53 | 54 | setData('password', e.target.value)} 64 | /> 65 | 66 | 67 |
68 | 69 |
70 | 71 | 72 | setData('password_confirmation', e.target.value)} 80 | /> 81 | 82 | 83 |
84 | 85 |
86 | 87 | Reset Password 88 | 89 |
90 |
91 |
92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /resources/js/Pages/Auth/VerifyEmail.jsx: -------------------------------------------------------------------------------- 1 | import GuestLayout from '@/Layouts/GuestLayout'; 2 | import PrimaryButton from '@/Components/PrimaryButton'; 3 | import { Head, Link, useForm } from '@inertiajs/react'; 4 | 5 | export default function VerifyEmail({ status }) { 6 | const { post, processing } = useForm({}); 7 | 8 | const submit = (e) => { 9 | e.preventDefault(); 10 | 11 | post(route('verification.send')); 12 | }; 13 | 14 | return ( 15 | 16 | 17 | 18 |
19 | Thanks for signing up! Before getting started, could you verify your email address by clicking on the 20 | link we just emailed to you? If you didn't receive the email, we will gladly send you another. 21 |
22 | 23 | {status === 'verification-link-sent' && ( 24 |
25 | A new verification link has been sent to the email address you provided during registration. 26 |
27 | )} 28 | 29 |
30 |
31 | Resend Verification Email 32 | 33 | 39 | Log Out 40 | 41 |
42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /resources/js/Pages/Dashboard.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 2 | import { TASK_STATUS_CLASS_MAP, TASK_STATUS_TEXT_MAP } from "@/constants"; 3 | import { Head, Link } from "@inertiajs/react"; 4 | 5 | export default function Dashboard({ 6 | auth, 7 | totalPendingTasks, 8 | myPendingTasks, 9 | totalProgressTasks, 10 | myProgressTasks, 11 | totalCompletedTasks, 12 | myCompletedTasks, 13 | activeTasks, 14 | }) { 15 | return ( 16 | 20 | Dashboard 21 | 22 | } 23 | > 24 | 25 | 26 |
27 |
28 |
29 |
30 |

31 | Pending Tasks 32 |

33 |

34 | {myPendingTasks}/ 35 | {totalPendingTasks} 36 |

37 |
38 |
39 |
40 |
41 |

42 | In Progress Tasks 43 |

44 |

45 | {myProgressTasks}/ 46 | {totalProgressTasks} 47 |

48 |
49 |
50 |
51 |
52 |

53 | Completed Tasks 54 |

55 |

56 | {myCompletedTasks}/ 57 | {totalCompletedTasks} 58 |

59 |
60 |
61 |
62 |
63 |
64 |
65 |

66 | My Active Tasks 67 |

68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {activeTasks.data.map((task) => ( 81 | 82 | 83 | 88 | 93 | 103 | 104 | 105 | ))} 106 | 107 |
IDProject NameNameStatusDue Date
{task.id} 84 | 85 | {task.project.name} 86 | 87 | 89 | 90 | {task.name} 91 | 92 | 94 | 100 | {TASK_STATUS_TEXT_MAP[task.status]} 101 | 102 | {task.due_date}
108 |
109 |
110 |
111 |
112 |
113 | ); 114 | } 115 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Edit.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; 2 | import DeleteUserForm from './Partials/DeleteUserForm'; 3 | import UpdatePasswordForm from './Partials/UpdatePasswordForm'; 4 | import UpdateProfileInformationForm from './Partials/UpdateProfileInformationForm'; 5 | import { Head } from '@inertiajs/react'; 6 | 7 | export default function Edit({ auth, mustVerifyEmail, status }) { 8 | return ( 9 | Profile} 12 | > 13 | 14 | 15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 | 27 |
28 | 29 |
30 | 31 |
32 |
33 |
34 |
35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Partials/DeleteUserForm.jsx: -------------------------------------------------------------------------------- 1 | import { useRef, useState } from 'react'; 2 | import DangerButton from '@/Components/DangerButton'; 3 | import InputError from '@/Components/InputError'; 4 | import InputLabel from '@/Components/InputLabel'; 5 | import Modal from '@/Components/Modal'; 6 | import SecondaryButton from '@/Components/SecondaryButton'; 7 | import TextInput from '@/Components/TextInput'; 8 | import { useForm } from '@inertiajs/react'; 9 | 10 | export default function DeleteUserForm({ className = '' }) { 11 | const [confirmingUserDeletion, setConfirmingUserDeletion] = useState(false); 12 | const passwordInput = useRef(); 13 | 14 | const { 15 | data, 16 | setData, 17 | delete: destroy, 18 | processing, 19 | reset, 20 | errors, 21 | } = useForm({ 22 | password: '', 23 | }); 24 | 25 | const confirmUserDeletion = () => { 26 | setConfirmingUserDeletion(true); 27 | }; 28 | 29 | const deleteUser = (e) => { 30 | e.preventDefault(); 31 | 32 | destroy(route('profile.destroy'), { 33 | preserveScroll: true, 34 | onSuccess: () => closeModal(), 35 | onError: () => passwordInput.current.focus(), 36 | onFinish: () => reset(), 37 | }); 38 | }; 39 | 40 | const closeModal = () => { 41 | setConfirmingUserDeletion(false); 42 | 43 | reset(); 44 | }; 45 | 46 | return ( 47 |
48 |
49 |

Delete Account

50 | 51 |

52 | Once your account is deleted, all of its resources and data will be permanently deleted. Before 53 | deleting your account, please download any data or information that you wish to retain. 54 |

55 |
56 | 57 | Delete Account 58 | 59 | 60 |
61 |

62 | Are you sure you want to delete your account? 63 |

64 | 65 |

66 | Once your account is deleted, all of its resources and data will be permanently deleted. Please 67 | enter your password to confirm you would like to permanently delete your account. 68 |

69 | 70 |
71 | 72 | 73 | setData('password', e.target.value)} 80 | className="mt-1 block w-3/4" 81 | isFocused 82 | placeholder="Password" 83 | /> 84 | 85 | 86 |
87 | 88 |
89 | Cancel 90 | 91 | 92 | Delete Account 93 | 94 |
95 |
96 |
97 |
98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Partials/UpdatePasswordForm.jsx: -------------------------------------------------------------------------------- 1 | import { useRef } from 'react'; 2 | import InputError from '@/Components/InputError'; 3 | import InputLabel from '@/Components/InputLabel'; 4 | import PrimaryButton from '@/Components/PrimaryButton'; 5 | import TextInput from '@/Components/TextInput'; 6 | import { useForm } from '@inertiajs/react'; 7 | import { Transition } from '@headlessui/react'; 8 | 9 | export default function UpdatePasswordForm({ className = '' }) { 10 | const passwordInput = useRef(); 11 | const currentPasswordInput = useRef(); 12 | 13 | const { data, setData, errors, put, reset, processing, recentlySuccessful } = useForm({ 14 | current_password: '', 15 | password: '', 16 | password_confirmation: '', 17 | }); 18 | 19 | const updatePassword = (e) => { 20 | e.preventDefault(); 21 | 22 | put(route('password.update'), { 23 | preserveScroll: true, 24 | onSuccess: () => reset(), 25 | onError: (errors) => { 26 | if (errors.password) { 27 | reset('password', 'password_confirmation'); 28 | passwordInput.current.focus(); 29 | } 30 | 31 | if (errors.current_password) { 32 | reset('current_password'); 33 | currentPasswordInput.current.focus(); 34 | } 35 | }, 36 | }); 37 | }; 38 | 39 | return ( 40 |
41 |
42 |

Update Password

43 | 44 |

45 | Ensure your account is using a long, random password to stay secure. 46 |

47 |
48 | 49 |
50 |
51 | 52 | 53 | setData('current_password', e.target.value)} 58 | type="password" 59 | className="mt-1 block w-full" 60 | autoComplete="current-password" 61 | /> 62 | 63 | 64 |
65 | 66 |
67 | 68 | 69 | setData('password', e.target.value)} 74 | type="password" 75 | className="mt-1 block w-full" 76 | autoComplete="new-password" 77 | /> 78 | 79 | 80 |
81 | 82 |
83 | 84 | 85 | setData('password_confirmation', e.target.value)} 89 | type="password" 90 | className="mt-1 block w-full" 91 | autoComplete="new-password" 92 | /> 93 | 94 | 95 |
96 | 97 |
98 | Save 99 | 100 | 107 |

Saved.

108 |
109 |
110 |
111 |
112 | ); 113 | } 114 | -------------------------------------------------------------------------------- /resources/js/Pages/Profile/Partials/UpdateProfileInformationForm.jsx: -------------------------------------------------------------------------------- 1 | import InputError from '@/Components/InputError'; 2 | import InputLabel from '@/Components/InputLabel'; 3 | import PrimaryButton from '@/Components/PrimaryButton'; 4 | import TextInput from '@/Components/TextInput'; 5 | import { Link, useForm, usePage } from '@inertiajs/react'; 6 | import { Transition } from '@headlessui/react'; 7 | 8 | export default function UpdateProfileInformation({ mustVerifyEmail, status, className = '' }) { 9 | const user = usePage().props.auth.user; 10 | 11 | const { data, setData, patch, errors, processing, recentlySuccessful } = useForm({ 12 | name: user.name, 13 | email: user.email, 14 | }); 15 | 16 | const submit = (e) => { 17 | e.preventDefault(); 18 | 19 | patch(route('profile.update')); 20 | }; 21 | 22 | return ( 23 |
24 |
25 |

Profile Information

26 | 27 |

28 | Update your account's profile information and email address. 29 |

30 |
31 | 32 |
33 |
34 | 35 | 36 | setData('name', e.target.value)} 41 | required 42 | isFocused 43 | autoComplete="name" 44 | /> 45 | 46 | 47 |
48 | 49 |
50 | 51 | 52 | setData('email', e.target.value)} 58 | required 59 | autoComplete="username" 60 | /> 61 | 62 | 63 |
64 | 65 | {mustVerifyEmail && user.email_verified_at === null && ( 66 |
67 |

68 | Your email address is unverified. 69 | 75 | Click here to re-send the verification email. 76 | 77 |

78 | 79 | {status === 'verification-link-sent' && ( 80 |
81 | A new verification link has been sent to your email address. 82 |
83 | )} 84 |
85 | )} 86 | 87 |
88 | Save 89 | 90 | 97 |

Saved.

98 |
99 |
100 |
101 |
102 | ); 103 | } 104 | -------------------------------------------------------------------------------- /resources/js/Pages/Project/Create.jsx: -------------------------------------------------------------------------------- 1 | import InputError from "@/Components/InputError"; 2 | import InputLabel from "@/Components/InputLabel"; 3 | import SelectInput from "@/Components/SelectInput"; 4 | import TextAreaInput from "@/Components/TextAreaInput"; 5 | import TextInput from "@/Components/TextInput"; 6 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 7 | import { Head, Link, useForm } from "@inertiajs/react"; 8 | 9 | export default function Create({ auth }) { 10 | const { data, setData, post, errors, reset } = useForm({ 11 | image: "", 12 | name: "", 13 | status: "", 14 | description: "", 15 | due_date: "", 16 | }); 17 | 18 | const onSubmit = (e) => { 19 | e.preventDefault(); 20 | 21 | post(route("project.store")); 22 | }; 23 | 24 | return ( 25 | 29 |

30 | Create new Project 31 |

32 | 33 | } 34 | > 35 | 36 | 37 |
38 |
39 |
40 |
44 |
45 | 49 | setData("image", e.target.files[0])} 55 | /> 56 | 57 |
58 |
59 | 60 | 61 | setData("name", e.target.value)} 69 | /> 70 | 71 | 72 |
73 |
74 | 78 | 79 | setData("description", e.target.value)} 85 | /> 86 | 87 | 88 |
89 |
90 | 94 | 95 | setData("due_date", e.target.value)} 102 | /> 103 | 104 | 105 |
106 |
107 | 108 | 109 | setData("status", e.target.value)} 114 | > 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 |
123 |
124 | 128 | Cancel 129 | 130 | 133 |
134 |
135 |
136 |
137 |
138 |
139 | ); 140 | } 141 | -------------------------------------------------------------------------------- /resources/js/Pages/Project/Edit.jsx: -------------------------------------------------------------------------------- 1 | import InputError from "@/Components/InputError"; 2 | import InputLabel from "@/Components/InputLabel"; 3 | import SelectInput from "@/Components/SelectInput"; 4 | import TextAreaInput from "@/Components/TextAreaInput"; 5 | import TextInput from "@/Components/TextInput"; 6 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 7 | import { Head, Link, useForm } from "@inertiajs/react"; 8 | 9 | export default function Create({ auth, project }) { 10 | const { data, setData, post, errors, reset } = useForm({ 11 | image: "", 12 | name: project.name || "", 13 | status: project.status || "", 14 | description: project.description || "", 15 | due_date: project.due_date || "", 16 | _method: "PUT", 17 | }); 18 | 19 | const onSubmit = (e) => { 20 | e.preventDefault(); 21 | 22 | post(route("project.update", project.id)); 23 | }; 24 | 25 | return ( 26 | 30 |

31 | Edit project "{project.name}" 32 |

33 | 34 | } 35 | > 36 | 37 | 38 |
39 |
40 |
41 |
45 | {project.image_path && ( 46 |
47 | 48 |
49 | )} 50 |
51 | 55 | setData("image", e.target.files[0])} 61 | /> 62 | 63 |
64 |
65 | 66 | 67 | setData("name", e.target.value)} 75 | /> 76 | 77 | 78 |
79 |
80 | 84 | 85 | setData("description", e.target.value)} 91 | /> 92 | 93 | 94 |
95 |
96 | 100 | 101 | setData("due_date", e.target.value)} 108 | /> 109 | 110 | 111 |
112 |
113 | 114 | 115 | setData("status", e.target.value)} 120 | > 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 |
129 |
130 | 134 | Cancel 135 | 136 | 139 |
140 |
141 |
142 |
143 |
144 |
145 | ); 146 | } 147 | -------------------------------------------------------------------------------- /resources/js/Pages/Project/Show.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 2 | import { Head, Link } from "@inertiajs/react"; 3 | import { 4 | PROJECT_STATUS_CLASS_MAP, 5 | PROJECT_STATUS_TEXT_MAP, 6 | } from "@/constants.jsx"; 7 | import TasksTable from "../Task/TasksTable"; 8 | export default function Show({ auth, success, project, tasks, queryParams }) { 9 | return ( 10 | 14 |

15 | {`Project "${project.name}"`} 16 |

17 | 21 | Edit 22 | 23 | 24 | } 25 | > 26 | 27 |
28 |
29 |
30 |
31 | 36 |
37 |
38 |
39 |
40 |
41 | 42 |

{project.id}

43 |
44 |
45 | 46 |

{project.name}

47 |
48 | 49 |
50 | 51 |

52 | 58 | {PROJECT_STATUS_TEXT_MAP[project.status]} 59 | 60 |

61 |
62 |
63 | 64 |

{project.createdBy.name}

65 |
66 |
67 |
68 |
69 | 70 |

{project.due_date}

71 |
72 |
73 | 74 |

{project.created_at}

75 |
76 |
77 | 78 |

{project.updatedBy.name}

79 |
80 |
81 |
82 | 83 |
84 | 85 |

{project.description}

86 |
87 |
88 |
89 |
90 |
91 | 92 |
93 |
94 |
95 |
96 | 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /resources/js/Pages/Task/Create.jsx: -------------------------------------------------------------------------------- 1 | import InputError from "@/Components/InputError"; 2 | import InputLabel from "@/Components/InputLabel"; 3 | import SelectInput from "@/Components/SelectInput"; 4 | import TextAreaInput from "@/Components/TextAreaInput"; 5 | import TextInput from "@/Components/TextInput"; 6 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 7 | import { Head, Link, useForm } from "@inertiajs/react"; 8 | 9 | export default function Create({ auth, projects, users }) { 10 | const { data, setData, post, errors, reset } = useForm({ 11 | image: "", 12 | name: "", 13 | status: "", 14 | description: "", 15 | due_date: "", 16 | }); 17 | 18 | const onSubmit = (e) => { 19 | e.preventDefault(); 20 | 21 | post(route("task.store")); 22 | }; 23 | 24 | return ( 25 | 29 |

30 | Create new Task 31 |

32 | 33 | } 34 | > 35 | 36 | 37 |
38 |
39 |
40 |
44 |
45 | 46 | 47 | setData("project_id", e.target.value)} 52 | > 53 | 54 | {projects.data.map((project) => ( 55 | 58 | ))} 59 | 60 | 61 | 62 |
63 |
64 | 65 | setData("image", e.target.files[0])} 71 | /> 72 | 73 |
74 |
75 | 76 | 77 | setData("name", e.target.value)} 85 | /> 86 | 87 | 88 |
89 |
90 | 94 | 95 | setData("description", e.target.value)} 101 | /> 102 | 103 | 104 |
105 |
106 | 107 | 108 | setData("due_date", e.target.value)} 115 | /> 116 | 117 | 118 |
119 |
120 | 121 | 122 | setData("status", e.target.value)} 127 | > 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 |
136 | 137 |
138 | 139 | 140 | setData("priority", e.target.value)} 145 | > 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 |
154 | 155 |
156 | 160 | 161 | setData("assigned_user_id", e.target.value)} 166 | > 167 | 168 | {users.data.map((user) => ( 169 | 172 | ))} 173 | 174 | 175 | 179 |
180 | 181 |
182 | 186 | Cancel 187 | 188 | 191 |
192 |
193 |
194 |
195 |
196 |
197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /resources/js/Pages/Task/Index.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 2 | 3 | import { Head, Link } from "@inertiajs/react"; 4 | 5 | import TasksTable from "./TasksTable"; 6 | 7 | export default function Index({ auth, success, tasks, queryParams = null }) { 8 | return ( 9 | 13 |

14 | Tasks 15 |

16 | 20 | Add new 21 | 22 | 23 | } 24 | > 25 | 26 | 27 |
28 |
29 |
30 |
31 | 36 |
37 |
38 |
39 |
40 |
41 | ); 42 | } 43 | -------------------------------------------------------------------------------- /resources/js/Pages/Task/Show.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 2 | import { Head, Link } from "@inertiajs/react"; 3 | import { 4 | TASK_PRIORITY_CLASS_MAP, 5 | TASK_PRIORITY_TEXT_MAP, 6 | TASK_STATUS_CLASS_MAP, 7 | TASK_STATUS_TEXT_MAP, 8 | } from "@/constants.jsx"; 9 | export default function Show({ auth, task }) { 10 | return ( 11 | 15 |

16 | {`Task "${task.name}"`} 17 |

18 | 22 | Edit 23 | 24 | 25 | } 26 | > 27 | 28 |
29 |
30 |
31 |
32 | 37 |
38 |
39 |
40 |
41 |
42 | 43 |

{task.id}

44 |
45 |
46 | 47 |

{task.name}

48 |
49 | 50 |
51 | 52 |

53 | 59 | {TASK_STATUS_TEXT_MAP[task.status]} 60 | 61 |

62 |
63 | 64 |
65 | 66 |

67 | 73 | {TASK_PRIORITY_TEXT_MAP[task.priority]} 74 | 75 |

76 |
77 |
78 | 79 |

{task.createdBy.name}

80 |
81 |
82 |
83 |
84 | 85 |

{task.due_date}

86 |
87 |
88 | 89 |

{task.created_at}

90 |
91 |
92 | 93 |

{task.updatedBy.name}

94 |
95 |
96 | 97 |

98 | 102 | {task.project.name} 103 | 104 |

105 |
106 |
107 | 108 |

{task.assignedUser.name}

109 |
110 |
111 |
112 | 113 |
114 | 115 |

{task.description}

116 |
117 |
118 |
119 |
120 |
121 |
122 | ); 123 | } 124 | -------------------------------------------------------------------------------- /resources/js/Pages/User/Create.jsx: -------------------------------------------------------------------------------- 1 | import InputError from "@/Components/InputError"; 2 | import InputLabel from "@/Components/InputLabel"; 3 | import TextInput from "@/Components/TextInput"; 4 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 5 | import { Head, Link, useForm } from "@inertiajs/react"; 6 | 7 | export default function Create({ auth }) { 8 | const { data, setData, post, errors, reset } = useForm({ 9 | name: "", 10 | email: "", 11 | password: "", 12 | password_confirmation: "", 13 | }); 14 | 15 | const onSubmit = (e) => { 16 | e.preventDefault(); 17 | 18 | post(route("user.store")); 19 | }; 20 | 21 | return ( 22 | 26 |

27 | Create new User 28 |

29 | 30 | } 31 | > 32 | 33 | 34 |
35 |
36 |
37 |
41 |
42 | 43 | 44 | setData("name", e.target.value)} 52 | /> 53 | 54 | 55 |
56 |
57 | 58 | 59 | setData("email", e.target.value)} 66 | /> 67 | 68 | 69 |
70 | 71 |
72 | 73 | 74 | setData("password", e.target.value)} 81 | /> 82 | 83 | 84 |
85 | 86 |
87 | 91 | 92 | 99 | setData("password_confirmation", e.target.value) 100 | } 101 | /> 102 | 103 | 107 |
108 | 109 |
110 | 114 | Cancel 115 | 116 | 119 |
120 |
121 |
122 |
123 |
124 |
125 | ); 126 | } 127 | -------------------------------------------------------------------------------- /resources/js/Pages/User/Edit.jsx: -------------------------------------------------------------------------------- 1 | import InputError from "@/Components/InputError"; 2 | import InputLabel from "@/Components/InputLabel"; 3 | import SelectInput from "@/Components/SelectInput"; 4 | import TextAreaInput from "@/Components/TextAreaInput"; 5 | import TextInput from "@/Components/TextInput"; 6 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 7 | import { Head, Link, useForm } from "@inertiajs/react"; 8 | 9 | export default function Create({ auth, user }) { 10 | const { data, setData, post, errors, reset } = useForm({ 11 | name: user.name || "", 12 | email: user.email || "", 13 | password: "", 14 | password_confirmation: "", 15 | _method: "PUT", 16 | }); 17 | 18 | const onSubmit = (e) => { 19 | e.preventDefault(); 20 | 21 | post(route("user.update", user.id)); 22 | }; 23 | 24 | return ( 25 | 29 |

30 | Edit user "{user.name}" 31 |

32 | 33 | } 34 | > 35 | 36 | 37 |
38 |
39 |
40 |
44 |
45 | 46 | 47 | setData("name", e.target.value)} 55 | /> 56 | 57 | 58 |
59 |
60 | 61 | 62 | setData("email", e.target.value)} 69 | /> 70 | 71 | 72 |
73 | 74 |
75 | 76 | 77 | setData("password", e.target.value)} 84 | /> 85 | 86 | 87 |
88 | 89 |
90 | 94 | 95 | 102 | setData("password_confirmation", e.target.value) 103 | } 104 | /> 105 | 106 | 110 |
111 |
112 | 116 | Cancel 117 | 118 | 121 |
122 |
123 |
124 |
125 |
126 |
127 | ); 128 | } 129 | -------------------------------------------------------------------------------- /resources/js/Pages/User/Index.jsx: -------------------------------------------------------------------------------- 1 | import Pagination from "@/Components/Pagination"; 2 | import TextInput from "@/Components/TextInput"; 3 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 4 | import { Head, Link, router } from "@inertiajs/react"; 5 | import TableHeading from "@/Components/TableHeading"; 6 | 7 | export default function Index({ auth, users, queryParams = null, success }) { 8 | queryParams = queryParams || {}; 9 | const searchFieldChanged = (name, value) => { 10 | if (value) { 11 | queryParams[name] = value; 12 | } else { 13 | delete queryParams[name]; 14 | } 15 | 16 | router.get(route("user.index"), queryParams); 17 | }; 18 | 19 | const onKeyPress = (name, e) => { 20 | if (e.key !== "Enter") return; 21 | 22 | searchFieldChanged(name, e.target.value); 23 | }; 24 | 25 | const sortChanged = (name) => { 26 | if (name === queryParams.sort_field) { 27 | if (queryParams.sort_direction === "asc") { 28 | queryParams.sort_direction = "desc"; 29 | } else { 30 | queryParams.sort_direction = "asc"; 31 | } 32 | } else { 33 | queryParams.sort_field = name; 34 | queryParams.sort_direction = "asc"; 35 | } 36 | router.get(route("user.index"), queryParams); 37 | }; 38 | 39 | const deleteUser = (user) => { 40 | if (!window.confirm("Are you sure you want to delete the user?")) { 41 | return; 42 | } 43 | router.delete(route("user.destroy", user.id)); 44 | }; 45 | 46 | return ( 47 | 51 |

52 | Users 53 |

54 | 58 | Add new 59 | 60 | 61 | } 62 | > 63 | 64 | 65 |
66 |
67 | {success && ( 68 |
69 | {success} 70 |
71 | )} 72 |
73 |
74 |
75 | 76 | 77 | 78 | 84 | ID 85 | 86 | 92 | Name 93 | 94 | 95 | 101 | Email 102 | 103 | 104 | 110 | Create Date 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 130 | 141 | 142 | 143 | 144 | 145 | 146 | {users.data.map((user) => ( 147 | 151 | 152 | 155 | 156 | 159 | 173 | 174 | ))} 175 | 176 |
Actions
120 | 125 | searchFieldChanged("name", e.target.value) 126 | } 127 | onKeyPress={(e) => onKeyPress("name", e)} 128 | /> 129 | 131 | 136 | searchFieldChanged("email", e.target.value) 137 | } 138 | onKeyPress={(e) => onKeyPress("email", e)} 139 | /> 140 |
{user.id} 153 | {user.name} 154 | {user.email} 157 | {user.created_at} 158 | 160 | 164 | Edit 165 | 166 | 172 |
177 |
178 | 179 |
180 |
181 |
182 |
183 |
184 | ); 185 | } 186 | -------------------------------------------------------------------------------- /resources/js/Pages/User/Show.jsx: -------------------------------------------------------------------------------- 1 | import AuthenticatedLayout from "@/Layouts/AuthenticatedLayout"; 2 | import { Head } from "@inertiajs/react"; 3 | import { USER_STATUS_CLASS_MAP, USER_STATUS_TEXT_MAP } from "@/constants.jsx"; 4 | import TasksTable from "../Task/TasksTable"; 5 | export default function Show({ auth, user, tasks, queryParams }) { 6 | return ( 7 | 11 | {`User "${user.name}"`} 12 | 13 | } 14 | > 15 | 16 |
17 |
18 |
19 |
20 | 25 |
26 |
27 |
28 |
29 |
30 | 31 |

{user.id}

32 |
33 |
34 | 35 |

{user.name}

36 |
37 | 38 |
39 | 40 |

41 | 47 | {USER_STATUS_TEXT_MAP[user.status]} 48 | 49 |

50 |
51 |
52 | 53 |

{user.createdBy.name}

54 |
55 |
56 |
57 |
58 | 59 |

{user.due_date}

60 |
61 |
62 | 63 |

{user.created_at}

64 |
65 |
66 | 67 |

{user.updatedBy.name}

68 |
69 |
70 |
71 | 72 |
73 | 74 |

{user.description}

75 |
76 |
77 |
78 |
79 |
80 | 81 |
82 |
83 |
84 |
85 | 90 |
91 |
92 |
93 |
94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /resources/js/app.jsx: -------------------------------------------------------------------------------- 1 | import './bootstrap'; 2 | import '../css/app.css'; 3 | 4 | import { createRoot } from 'react-dom/client'; 5 | import { createInertiaApp } from '@inertiajs/react'; 6 | import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers'; 7 | 8 | const appName = import.meta.env.VITE_APP_NAME || 'Laravel'; 9 | 10 | createInertiaApp({ 11 | title: (title) => `${title} - ${appName}`, 12 | resolve: (name) => resolvePageComponent(`./Pages/${name}.jsx`, import.meta.glob('./Pages/**/*.jsx')), 13 | setup({ el, App, props }) { 14 | const root = createRoot(el); 15 | 16 | root.render(); 17 | }, 18 | progress: { 19 | color: '#4B5563', 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /resources/js/bootstrap.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | window.axios = axios; 3 | 4 | window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; 5 | -------------------------------------------------------------------------------- /resources/js/constants.jsx: -------------------------------------------------------------------------------- 1 | export const PROJECT_STATUS_CLASS_MAP = { 2 | pending: "bg-amber-500", 3 | in_progress: "bg-blue-500 ", 4 | completed: "bg-green-500", 5 | }; 6 | export const PROJECT_STATUS_TEXT_MAP = { 7 | pending: "Pending", 8 | in_progress: "In Progress", 9 | completed: "Completed", 10 | }; 11 | export const TASK_STATUS_CLASS_MAP = { 12 | pending: "bg-amber-500", 13 | in_progress: "bg-blue-500", 14 | completed: "bg-green-500", 15 | }; 16 | export const TASK_STATUS_TEXT_MAP = { 17 | pending: "Pending", 18 | in_progress: "In Progress", 19 | completed: "Completed", 20 | }; 21 | export const TASK_PRIORITY_CLASS_MAP = { 22 | low: "bg-gray-600", 23 | medium: "bg-amber-600", 24 | high: "bg-red-600", 25 | }; 26 | export const TASK_PRIORITY_TEXT_MAP = { 27 | low: "Low", 28 | medium: "Medium", 29 | high: "High", 30 | }; 31 | -------------------------------------------------------------------------------- /resources/views/app.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | {{ config('app.name', 'Laravel') }} 9 | 10 | 11 | 12 | 13 | 14 | 15 | @routes 16 | @viteReactRefresh 17 | @vite(['resources/js/app.jsx', "resources/js/Pages/{$page['component']}.jsx"]) 18 | @inertiaHead 19 | 20 | 21 | 22 | @inertia 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /routes/auth.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('register', [RegisteredUserController::class, 'create']) 16 | ->name('register'); 17 | 18 | Route::post('register', [RegisteredUserController::class, 'store']); 19 | 20 | Route::get('login', [AuthenticatedSessionController::class, 'create']) 21 | ->name('login'); 22 | 23 | Route::post('login', [AuthenticatedSessionController::class, 'store']); 24 | 25 | Route::get('forgot-password', [PasswordResetLinkController::class, 'create']) 26 | ->name('password.request'); 27 | 28 | Route::post('forgot-password', [PasswordResetLinkController::class, 'store']) 29 | ->name('password.email'); 30 | 31 | Route::get('reset-password/{token}', [NewPasswordController::class, 'create']) 32 | ->name('password.reset'); 33 | 34 | Route::post('reset-password', [NewPasswordController::class, 'store']) 35 | ->name('password.store'); 36 | }); 37 | 38 | Route::middleware('auth')->group(function () { 39 | Route::get('verify-email', EmailVerificationPromptController::class) 40 | ->name('verification.notice'); 41 | 42 | Route::get('verify-email/{id}/{hash}', VerifyEmailController::class) 43 | ->middleware(['signed', 'throttle:6,1']) 44 | ->name('verification.verify'); 45 | 46 | Route::post('email/verification-notification', [EmailVerificationNotificationController::class, 'store']) 47 | ->middleware('throttle:6,1') 48 | ->name('verification.send'); 49 | 50 | Route::get('confirm-password', [ConfirmablePasswordController::class, 'show']) 51 | ->name('password.confirm'); 52 | 53 | Route::post('confirm-password', [ConfirmablePasswordController::class, 'store']); 54 | 55 | Route::put('password', [PasswordController::class, 'update'])->name('password.update'); 56 | 57 | Route::post('logout', [AuthenticatedSessionController::class, 'destroy']) 58 | ->name('logout'); 59 | }); 60 | -------------------------------------------------------------------------------- /routes/console.php: -------------------------------------------------------------------------------- 1 | comment(Inspiring::quote()); 8 | })->purpose('Display an inspiring quote')->hourly(); 9 | -------------------------------------------------------------------------------- /routes/web.php: -------------------------------------------------------------------------------- 1 | group(function () { 15 | Route::get('/dashboard', [DashboardController::class, 'index']) 16 | ->name('dashboard'); 17 | 18 | Route::resource('project', ProjectController::class); 19 | Route::get('/task/my-tasks', [TaskController::class, 'myTasks']) 20 | ->name('task.myTasks'); 21 | Route::resource('task', TaskController::class); 22 | Route::resource('user', UserController::class); 23 | }); 24 | 25 | Route::middleware('auth')->group(function () { 26 | Route::get('/profile', [ProfileController::class, 'edit'])->name('profile.edit'); 27 | Route::patch('/profile', [ProfileController::class, 'update'])->name('profile.update'); 28 | Route::delete('/profile', [ProfileController::class, 'destroy'])->name('profile.destroy'); 29 | }); 30 | 31 | require __DIR__ . '/auth.php'; 32 | -------------------------------------------------------------------------------- /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 | import defaultTheme from 'tailwindcss/defaultTheme'; 2 | import forms from '@tailwindcss/forms'; 3 | 4 | /** @type {import('tailwindcss').Config} */ 5 | export default { 6 | darkMode: 'class', 7 | content: [ 8 | './vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php', 9 | './storage/framework/views/*.php', 10 | './resources/views/**/*.blade.php', 11 | './resources/js/**/*.jsx', 12 | ], 13 | 14 | theme: { 15 | extend: { 16 | fontFamily: { 17 | sans: ['Figtree', ...defaultTheme.fontFamily.sans], 18 | }, 19 | }, 20 | }, 21 | 22 | plugins: [forms], 23 | }; 24 | -------------------------------------------------------------------------------- /tests/Feature/Auth/AuthenticationTest.php: -------------------------------------------------------------------------------- 1 | get('/login'); 7 | 8 | $response->assertStatus(200); 9 | }); 10 | 11 | test('users can authenticate using the login screen', function () { 12 | $user = User::factory()->create(); 13 | 14 | $response = $this->post('/login', [ 15 | 'email' => $user->email, 16 | 'password' => 'password', 17 | ]); 18 | 19 | $this->assertAuthenticated(); 20 | $response->assertRedirect(route('dashboard', absolute: false)); 21 | }); 22 | 23 | test('users can not authenticate with invalid password', function () { 24 | $user = User::factory()->create(); 25 | 26 | $this->post('/login', [ 27 | 'email' => $user->email, 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $this->assertGuest(); 32 | }); 33 | 34 | test('users can logout', function () { 35 | $user = User::factory()->create(); 36 | 37 | $response = $this->actingAs($user)->post('/logout'); 38 | 39 | $this->assertGuest(); 40 | $response->assertRedirect('/'); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/Feature/Auth/EmailVerificationTest.php: -------------------------------------------------------------------------------- 1 | create([ 10 | 'email_verified_at' => null, 11 | ]); 12 | 13 | $response = $this->actingAs($user)->get('/verify-email'); 14 | 15 | $response->assertStatus(200); 16 | }); 17 | 18 | test('email can be verified', function () { 19 | $user = User::factory()->create([ 20 | 'email_verified_at' => null, 21 | ]); 22 | 23 | Event::fake(); 24 | 25 | $verificationUrl = URL::temporarySignedRoute( 26 | 'verification.verify', 27 | now()->addMinutes(60), 28 | ['id' => $user->id, 'hash' => sha1($user->email)] 29 | ); 30 | 31 | $response = $this->actingAs($user)->get($verificationUrl); 32 | 33 | Event::assertDispatched(Verified::class); 34 | expect($user->fresh()->hasVerifiedEmail())->toBeTrue(); 35 | $response->assertRedirect(route('dashboard', absolute: false).'?verified=1'); 36 | }); 37 | 38 | test('email is not verified with invalid hash', function () { 39 | $user = User::factory()->create([ 40 | 'email_verified_at' => null, 41 | ]); 42 | 43 | $verificationUrl = URL::temporarySignedRoute( 44 | 'verification.verify', 45 | now()->addMinutes(60), 46 | ['id' => $user->id, 'hash' => sha1('wrong-email')] 47 | ); 48 | 49 | $this->actingAs($user)->get($verificationUrl); 50 | 51 | expect($user->fresh()->hasVerifiedEmail())->toBeFalse(); 52 | }); 53 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordConfirmationTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this->actingAs($user)->get('/confirm-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('password can be confirmed', function () { 14 | $user = User::factory()->create(); 15 | 16 | $response = $this->actingAs($user)->post('/confirm-password', [ 17 | 'password' => 'password', 18 | ]); 19 | 20 | $response->assertRedirect(); 21 | $response->assertSessionHasNoErrors(); 22 | }); 23 | 24 | test('password is not confirmed with invalid password', function () { 25 | $user = User::factory()->create(); 26 | 27 | $response = $this->actingAs($user)->post('/confirm-password', [ 28 | 'password' => 'wrong-password', 29 | ]); 30 | 31 | $response->assertSessionHasErrors(); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordResetTest.php: -------------------------------------------------------------------------------- 1 | get('/forgot-password'); 9 | 10 | $response->assertStatus(200); 11 | }); 12 | 13 | test('reset password link can be requested', function () { 14 | Notification::fake(); 15 | 16 | $user = User::factory()->create(); 17 | 18 | $this->post('/forgot-password', ['email' => $user->email]); 19 | 20 | Notification::assertSentTo($user, ResetPassword::class); 21 | }); 22 | 23 | test('reset password screen can be rendered', function () { 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, function ($notification) { 31 | $response = $this->get('/reset-password/'.$notification->token); 32 | 33 | $response->assertStatus(200); 34 | 35 | return true; 36 | }); 37 | }); 38 | 39 | test('password can be reset with valid token', function () { 40 | Notification::fake(); 41 | 42 | $user = User::factory()->create(); 43 | 44 | $this->post('/forgot-password', ['email' => $user->email]); 45 | 46 | Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) { 47 | $response = $this->post('/reset-password', [ 48 | 'token' => $notification->token, 49 | 'email' => $user->email, 50 | 'password' => 'password', 51 | 'password_confirmation' => 'password', 52 | ]); 53 | 54 | $response->assertSessionHasNoErrors(); 55 | 56 | return true; 57 | }); 58 | }); 59 | -------------------------------------------------------------------------------- /tests/Feature/Auth/PasswordUpdateTest.php: -------------------------------------------------------------------------------- 1 | create(); 8 | 9 | $response = $this 10 | ->actingAs($user) 11 | ->from('/profile') 12 | ->put('/password', [ 13 | 'current_password' => 'password', 14 | 'password' => 'new-password', 15 | 'password_confirmation' => 'new-password', 16 | ]); 17 | 18 | $response 19 | ->assertSessionHasNoErrors() 20 | ->assertRedirect('/profile'); 21 | 22 | $this->assertTrue(Hash::check('new-password', $user->refresh()->password)); 23 | }); 24 | 25 | test('correct password must be provided to update password', function () { 26 | $user = User::factory()->create(); 27 | 28 | $response = $this 29 | ->actingAs($user) 30 | ->from('/profile') 31 | ->put('/password', [ 32 | 'current_password' => 'wrong-password', 33 | 'password' => 'new-password', 34 | 'password_confirmation' => 'new-password', 35 | ]); 36 | 37 | $response 38 | ->assertSessionHasErrors('current_password') 39 | ->assertRedirect('/profile'); 40 | }); 41 | -------------------------------------------------------------------------------- /tests/Feature/Auth/RegistrationTest.php: -------------------------------------------------------------------------------- 1 | get('/register'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | 9 | test('new users can register', function () { 10 | $response = $this->post('/register', [ 11 | 'name' => 'Test User', 12 | 'email' => 'test@example.com', 13 | 'password' => 'password', 14 | 'password_confirmation' => 'password', 15 | ]); 16 | 17 | $this->assertAuthenticated(); 18 | $response->assertRedirect(route('dashboard', absolute: false)); 19 | }); 20 | -------------------------------------------------------------------------------- /tests/Feature/ExampleTest.php: -------------------------------------------------------------------------------- 1 | get('/'); 5 | 6 | $response->assertStatus(200); 7 | }); 8 | -------------------------------------------------------------------------------- /tests/Feature/ProfileTest.php: -------------------------------------------------------------------------------- 1 | create(); 7 | 8 | $response = $this 9 | ->actingAs($user) 10 | ->get('/profile'); 11 | 12 | $response->assertOk(); 13 | }); 14 | 15 | test('profile information can be updated', function () { 16 | $user = User::factory()->create(); 17 | 18 | $response = $this 19 | ->actingAs($user) 20 | ->patch('/profile', [ 21 | 'name' => 'Test User', 22 | 'email' => 'test@example.com', 23 | ]); 24 | 25 | $response 26 | ->assertSessionHasNoErrors() 27 | ->assertRedirect('/profile'); 28 | 29 | $user->refresh(); 30 | 31 | $this->assertSame('Test User', $user->name); 32 | $this->assertSame('test@example.com', $user->email); 33 | $this->assertNull($user->email_verified_at); 34 | }); 35 | 36 | test('email verification status is unchanged when the email address is unchanged', function () { 37 | $user = User::factory()->create(); 38 | 39 | $response = $this 40 | ->actingAs($user) 41 | ->patch('/profile', [ 42 | 'name' => 'Test User', 43 | 'email' => $user->email, 44 | ]); 45 | 46 | $response 47 | ->assertSessionHasNoErrors() 48 | ->assertRedirect('/profile'); 49 | 50 | $this->assertNotNull($user->refresh()->email_verified_at); 51 | }); 52 | 53 | test('user can delete their account', function () { 54 | $user = User::factory()->create(); 55 | 56 | $response = $this 57 | ->actingAs($user) 58 | ->delete('/profile', [ 59 | 'password' => 'password', 60 | ]); 61 | 62 | $response 63 | ->assertSessionHasNoErrors() 64 | ->assertRedirect('/'); 65 | 66 | $this->assertGuest(); 67 | $this->assertNull($user->fresh()); 68 | }); 69 | 70 | test('correct password must be provided to delete account', function () { 71 | $user = User::factory()->create(); 72 | 73 | $response = $this 74 | ->actingAs($user) 75 | ->from('/profile') 76 | ->delete('/profile', [ 77 | 'password' => 'wrong-password', 78 | ]); 79 | 80 | $response 81 | ->assertSessionHasErrors('password') 82 | ->assertRedirect('/profile'); 83 | 84 | $this->assertNotNull($user->fresh()); 85 | }); 86 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Expectations 22 | |-------------------------------------------------------------------------- 23 | | 24 | | When you're writing tests, you often need to check that values meet certain conditions. The 25 | | "expect()" function gives you access to a set of "expectations" methods that you can use 26 | | to assert different things. Of course, you may extend the Expectation API at any time. 27 | | 28 | */ 29 | 30 | expect()->extend('toBeOne', function () { 31 | return $this->toBe(1); 32 | }); 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Functions 37 | |-------------------------------------------------------------------------- 38 | | 39 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 40 | | project that you don't want to repeat in every file. Here you can also expose helpers as 41 | | global functions to help you to reduce the number of lines of code in your test files. 42 | | 43 | */ 44 | 45 | function something() 46 | { 47 | // .. 48 | } 49 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | toBeTrue(); 5 | }); 6 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import laravel from 'laravel-vite-plugin'; 3 | import react from '@vitejs/plugin-react'; 4 | 5 | export default defineConfig({ 6 | plugins: [ 7 | laravel({ 8 | input: 'resources/js/app.jsx', 9 | refresh: true, 10 | }), 11 | react(), 12 | ], 13 | }); 14 | --------------------------------------------------------------------------------