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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
40 |
41 | );
42 | }
43 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/app/Http/Controllers/DashboardController.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 |
--------------------------------------------------------------------------------
/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 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/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 |
43 |
44 | );
45 | }
46 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
57 |
58 | );
59 | }
60 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "laravel/laravel",
3 | "type": "project",
4 | "description": "The skeleton application for the Laravel framework.",
5 | "keywords": ["laravel", "framework"],
6 | "license": "MIT",
7 | "require": {
8 | "php": "^8.2",
9 | "inertiajs/inertia-laravel": "^1.0",
10 | "laravel/framework": "^11.0",
11 | "laravel/sanctum": "^4.0",
12 | "laravel/tinker": "^2.9",
13 | "tightenco/ziggy": "^1.0"
14 | },
15 | "require-dev": {
16 | "fakerphp/faker": "^1.23",
17 | "laravel/breeze": "^2.0@dev",
18 | "laravel/pint": "^1.13",
19 | "laravel/sail": "^1.26",
20 | "mockery/mockery": "^1.6",
21 | "nunomaduro/collision": "^8.0",
22 | "pestphp/pest": "^2.0",
23 | "pestphp/pest-plugin-laravel": "^2.0",
24 | "spatie/laravel-ignition": "^2.4"
25 | },
26 | "autoload": {
27 | "psr-4": {
28 | "App\\": "app/",
29 | "Database\\Factories\\": "database/factories/",
30 | "Database\\Seeders\\": "database/seeders/"
31 | }
32 | },
33 | "autoload-dev": {
34 | "psr-4": {
35 | "Tests\\": "tests/"
36 | }
37 | },
38 | "scripts": {
39 | "post-autoload-dump": [
40 | "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
41 | "@php artisan package:discover --ansi"
42 | ],
43 | "post-update-cmd": [
44 | "@php artisan vendor:publish --tag=laravel-assets --ansi --force"
45 | ],
46 | "post-root-package-install": [
47 | "@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
48 | ],
49 | "post-create-project-cmd": [
50 | "@php artisan key:generate --ansi",
51 | "@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
52 | "@php artisan migrate --ansi"
53 | ]
54 | },
55 | "extra": {
56 | "branch-alias": {
57 | "dev-master": "11.x-dev"
58 | },
59 | "laravel": {
60 | "dont-discover": []
61 | }
62 | },
63 | "config": {
64 | "optimize-autoloader": true,
65 | "preferred-install": "dist",
66 | "sort-packages": true,
67 | "allow-plugins": {
68 | "pestphp/pest-plugin": true,
69 | "php-http/discovery": true
70 | }
71 | },
72 | "minimum-stability": "dev",
73 | "prefer-stable": true
74 | }
75 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/resources/js/Components/ApplicationLogo.jsx:
--------------------------------------------------------------------------------
1 | export default function ApplicationLogo(props) {
2 | return (
3 |
4 |
5 |
6 | );
7 | }
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
91 |
92 | );
93 | }
94 |
--------------------------------------------------------------------------------
/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 |
User ID
31 |
{user.id}
32 |
33 |
34 |
User Name
35 |
{user.name}
36 |
37 |
38 |
39 |
User Status
40 |
41 |
47 | {USER_STATUS_TEXT_MAP[user.status]}
48 |
49 |
50 |
51 |
52 |
Created By
53 |
{user.createdBy.name}
54 |
55 |
56 |
57 |
58 |
Due Date
59 |
{user.due_date}
60 |
61 |
62 |
Create Date
63 |
{user.created_at}
64 |
65 |
66 |
Updated By
67 |
{user.updatedBy.name}
68 |
69 |
70 |
71 |
72 |
73 |
User Description
74 |
{user.description}
75 |
76 |
77 |
78 |
79 |
80 |
81 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/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 |
96 |
97 |
98 | );
99 | }
100 |
--------------------------------------------------------------------------------
/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 |
104 |
105 | );
106 | }
107 |
--------------------------------------------------------------------------------
/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 |
Project ID
42 |
{project.id}
43 |
44 |
45 |
Project Name
46 |
{project.name}
47 |
48 |
49 |
50 |
Project Status
51 |
52 |
58 | {PROJECT_STATUS_TEXT_MAP[project.status]}
59 |
60 |
61 |
62 |
63 |
Created By
64 |
{project.createdBy.name}
65 |
66 |
67 |
68 |
69 |
Due Date
70 |
{project.due_date}
71 |
72 |
73 |
Create Date
74 |
{project.created_at}
75 |
76 |
77 |
Updated By
78 |
{project.updatedBy.name}
79 |
80 |
81 |
82 |
83 |
84 |
Project Description
85 |
{project.description}
86 |
87 |
88 |
89 |
90 |
91 |
92 |
106 |
107 | );
108 | }
109 |
--------------------------------------------------------------------------------
/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 |
102 | );
103 | }
104 |
--------------------------------------------------------------------------------
/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 |
124 |
125 | );
126 | }
127 |
--------------------------------------------------------------------------------
/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 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/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 |
112 | );
113 | }
114 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
126 |
127 | );
128 | }
129 |
--------------------------------------------------------------------------------
/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 | ID
73 | Project Name
74 | Name
75 | Status
76 | Due Date
77 |
78 |
79 |
80 | {activeTasks.data.map((task) => (
81 |
82 | {task.id}
83 |
84 |
85 | {task.project.name}
86 |
87 |
88 |
89 |
90 | {task.name}
91 |
92 |
93 |
94 |
100 | {TASK_STATUS_TEXT_MAP[task.status]}
101 |
102 |
103 | {task.due_date}
104 |
105 | ))}
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 | );
114 | }
115 |
--------------------------------------------------------------------------------
/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 |
Task ID
43 |
{task.id}
44 |
45 |
46 |
Task Name
47 |
{task.name}
48 |
49 |
50 |
51 |
Task Status
52 |
53 |
59 | {TASK_STATUS_TEXT_MAP[task.status]}
60 |
61 |
62 |
63 |
64 |
65 |
Task Priority
66 |
67 |
73 | {TASK_PRIORITY_TEXT_MAP[task.priority]}
74 |
75 |
76 |
77 |
78 |
Created By
79 |
{task.createdBy.name}
80 |
81 |
82 |
83 |
84 |
Due Date
85 |
{task.due_date}
86 |
87 |
88 |
Create Date
89 |
{task.created_at}
90 |
91 |
92 |
Updated By
93 |
{task.updatedBy.name}
94 |
95 |
96 |
Project
97 |
98 |
102 | {task.project.name}
103 |
104 |
105 |
106 |
107 |
Assigned User
108 |
{task.assignedUser.name}
109 |
110 |
111 |
112 |
113 |
114 |
Task Description
115 |
{task.description}
116 |
117 |
118 |
119 |
120 |
121 |
122 | );
123 | }
124 |
--------------------------------------------------------------------------------
/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 |
138 |
139 | );
140 | }
141 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
144 |
145 | );
146 | }
147 |
--------------------------------------------------------------------------------
/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 |
196 |
197 | );
198 | }
199 |
--------------------------------------------------------------------------------
/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 | Actions
114 |
115 |
116 |
117 |
118 |
119 |
120 |
125 | searchFieldChanged("name", e.target.value)
126 | }
127 | onKeyPress={(e) => onKeyPress("name", e)}
128 | />
129 |
130 |
131 |
136 | searchFieldChanged("email", e.target.value)
137 | }
138 | onKeyPress={(e) => onKeyPress("email", e)}
139 | />
140 |
141 |
142 |
143 |
144 |
145 |
146 | {users.data.map((user) => (
147 |
151 | {user.id}
152 |
153 | {user.name}
154 |
155 | {user.email}
156 |
157 | {user.created_at}
158 |
159 |
160 |
164 | Edit
165 |
166 | deleteUser(user)}
168 | className="font-medium text-red-600 dark:text-red-500 hover:underline mx-1"
169 | >
170 | Delete
171 |
172 |
173 |
174 | ))}
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | );
185 | }
186 |
--------------------------------------------------------------------------------
/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 |
15 |
16 |
17 |
18 |
23 |
24 |
25 |
29 | Dashboard
30 |
31 |
35 | Projects
36 |
37 |
41 | All Tasks
42 |
43 |
47 | Users
48 |
49 |
53 | My Tasks
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
67 | {user.name}
68 |
69 |
75 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | Profile
88 |
89 |
94 | Log Out
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
104 | setShowingNavigationDropdown(
105 | (previousState) => !previousState
106 | )
107 | }
108 | className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out"
109 | >
110 |
116 |
125 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
145 |
146 |
150 | Dashboard
151 |
152 |
153 |
154 |
155 |
156 |
157 | {user.name}
158 |
159 |
160 | {user.email}
161 |
162 |
163 |
164 |
165 |
166 | Profile
167 |
168 |
173 | Log Out
174 |
175 |
176 |
177 |
178 |
179 |
180 | {header && (
181 |
182 |
183 | {header}
184 |
185 |
186 | )}
187 |
188 |
{children}
189 |
190 | );
191 | }
192 |
--------------------------------------------------------------------------------