From 451b635d374068da2ea40602c18b4ff2cb1fc870 Mon Sep 17 00:00:00 2001 From: Marcel Menk Date: Sat, 26 Jul 2025 17:12:09 +0200 Subject: [PATCH 1/2] feat: onboarding tutorial --- app/Http/Controllers/ProjectController.php | 4 + app/Http/Controllers/UserController.php | 15 +++ .../Middleware/IdentifyOnboardingStatus.php | 51 ++++++++ app/Models/User.php | 9 +- .../2025_07_26_000001_update_user_tables.php | 29 +++++ resources/sass/app.scss | 20 +++ resources/views/project/index.blade.php | 123 ++++++++++++++++++ routes/web.php | 5 + tests/Unit/Models/UserTest.php | 8 +- 9 files changed, 258 insertions(+), 6 deletions(-) create mode 100644 app/Http/Middleware/IdentifyOnboardingStatus.php create mode 100644 database/migrations/2025_07_26_000001_update_user_tables.php diff --git a/app/Http/Controllers/ProjectController.php b/app/Http/Controllers/ProjectController.php index 6ec8405..06e9e90 100644 --- a/app/Http/Controllers/ProjectController.php +++ b/app/Http/Controllers/ProjectController.php @@ -53,6 +53,10 @@ public function page_index(Request $request) return view('project.index', [ 'projects' => Project::paginate(10), 'statistics' => Project::allStatistics(), + ...(!$request->project_id ? [ + 'onboarding_status' => $request->get('onboarding_status'), + 'onboarding_dismissed' => !empty(Auth::user()->onboarding_dismissed_at), + ] : []), ]); } diff --git a/app/Http/Controllers/UserController.php b/app/Http/Controllers/UserController.php index e19a872..ae89a61 100644 --- a/app/Http/Controllers/UserController.php +++ b/app/Http/Controllers/UserController.php @@ -6,6 +6,7 @@ use App\Helpers\PermissionSet; use App\Models\User; +use Carbon\Carbon; use Illuminate\Http\Request; use Illuminate\Support\Facades\Artisan; use Illuminate\Support\Facades\Auth; @@ -161,4 +162,18 @@ public function action_delete(string $user_id) return redirect()->back()->with('warning', 'Ooops, something went wrong.'); } + + /** + * Dismiss the onboarding. + * + * @return \Illuminate\Http\RedirectResponse + */ + public function action_dismiss_onboarding() + { + Auth::user()->update([ + 'onboarding_dismissed_at' => Carbon::now(), + ]); + + return redirect()->route('project.index')->with('success', 'Onboarding dismissed successfully.'); + } } diff --git a/app/Http/Middleware/IdentifyOnboardingStatus.php b/app/Http/Middleware/IdentifyOnboardingStatus.php new file mode 100644 index 0000000..c0f1ffc --- /dev/null +++ b/app/Http/Middleware/IdentifyOnboardingStatus.php @@ -0,0 +1,51 @@ + + */ +class IdentifyOnboardingStatus +{ + /** + * Handle an incoming request. + * + * @param Request $request + * @param Closure $next + * + * @return mixed + */ + public function handle(Request $request, Closure $next) + { + $firstProject = Project::first(); + $hasProjects = !empty($firstProject); + $hasClusterProvisionerTemplates = Template::where('type', '=', 'cluster')->count() > 0; + $hasClusters = Cluster::query()->count() > 0; + $hasApplicationTemplates = Template::where('type', '=', 'application')->count() > 0; + $hasApplications = Deployment::query()->count() > 0; + + $request->attributes->add(['onboarding_status' => (object) [ + 'projects' => $hasProjects, + 'cluster_provisioner_templates' => $hasClusterProvisionerTemplates, + 'clusters' => $hasClusters, + 'application_templates' => $hasApplicationTemplates, + 'applications' => $hasApplications, + 'first_project' => $firstProject, + ]]); + + return $next($request); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index b6bb00d..4b82ad6 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -31,6 +31,7 @@ * @OA\Property(property="email_verified_at", type="string", format="date-time", example="2021-01-01T00:00:00Z", nullable=true), * @OA\Property(property="password", type="string", example="password123"), * @OA\Property(property="remember_token", type="string", example="remember_token123", nullable=true), + * @OA\Property(property="onboarding_dismissed_at", type="string", format="date-time", example="2021-01-01 00:00:00", nullable=true), * @OA\Property(property="created_at", type="string", format="date-time", example="2021-01-01 00:00:00", nullable=true), * @OA\Property(property="updated_at", type="string", format="date-time", example="2021-01-01 00:00:00", nullable=true), * ) @@ -51,7 +52,7 @@ * @property string $email * @property string $password * @property string $remember_token - * @property string $email_verified_at + * @property string $onboarding_dismissed_at * @property string $created_at * @property string $updated_at * @@ -85,6 +86,7 @@ class User extends Authenticatable implements JWTSubject 'name', 'email', 'password', + 'onboarding_dismissed_at', ]; /** @@ -105,8 +107,9 @@ class User extends Authenticatable implements JWTSubject protected function casts(): array { return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'onboarding_dismissed_at' => 'datetime', ]; } diff --git a/database/migrations/2025_07_26_000001_update_user_tables.php b/database/migrations/2025_07_26_000001_update_user_tables.php new file mode 100644 index 0000000..ca2ca84 --- /dev/null +++ b/database/migrations/2025_07_26_000001_update_user_tables.php @@ -0,0 +1,29 @@ +timestamp('onboarding_dismissed_at')->nullable()->after('remember_token'); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('onboarding_dismissed_at'); + }); + } +}; diff --git a/resources/sass/app.scss b/resources/sass/app.scss index 578e444..dbae914 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -553,3 +553,23 @@ main { font-weight: 600; color: var(--bs-white); } + +.indicator-success { + position: absolute; + top: 0; + right: -0.5rem; + width: 1rem; + height: 1rem; + border-radius: 50%; + background-color: var(--bs-success); +} + +.indicator-warning { + position: absolute; + top: 0; + right: -0.5rem; + width: 1rem; + height: 1rem; + border-radius: 50%; + background-color: var(--bs-warning); +} \ No newline at end of file diff --git a/resources/views/project/index.blade.php b/resources/views/project/index.blade.php index e94f34f..01b873e 100644 --- a/resources/views/project/index.blade.php +++ b/resources/views/project/index.blade.php @@ -14,6 +14,129 @@
@if (empty(request()->get('project'))) + @if (!$onboarding_dismissed) +
+
+ {{ __('Getting started') }} + + + +
+
+
+
+
+
+
+ + @if ($onboarding_status->projects) + + @else + + @endif + +
{{ __('Create a project') }}
+
+ @if (!$onboarding_status->projects) + {{ __('Create') }} + @else + {{ __('Done') }} + @endif +
+
+
+
+
+
+
+ + @if ($onboarding_status->cluster_provisioner_templates) + + @else + + @endif + +
{{ __('Create a cluster provisioner template') }}
+
+ @if (!$onboarding_status->cluster_provisioner_templates) + {{ __('Create') }} + @else + {{ __('Done') }} + @endif +
+
+
+
+
+
+
+ + @if ($onboarding_status->clusters) + + @else + + @endif + +
{{ __('Create a cluster') }}
+
+ @if (empty($onboarding_status->first_project)) + {{ __('Create a project') }} + @elseif (!$onboarding_status->clusters) + {{ __('Create') }} + @else + {{ __('Done') }} + @endif +
+
+
+
+
+
+
+ + @if ($onboarding_status->application_templates) + + @else + + @endif + +
{{ __('Create an application template') }}
+
+ @if (!$onboarding_status->application_templates) + {{ __('Create') }} + @else + {{ __('Done') }} + @endif +
+
+
+
+
+
+
+ + @if ($onboarding_status->applications) + + @else + + @endif + +
{{ __('Create an application') }}
+
+ @if (empty($onboarding_status->first_project)) + {{ __('Create a project') }} + @elseif (!$onboarding_status->applications) + {{ __('Create') }} + @else + {{ __('Done') }} + @endif +
+
+
+
+
+
+ @endif
{{ __('Infrastructure') }} diff --git a/routes/web.php b/routes/web.php index 2d6ef02..616916d 100644 --- a/routes/web.php +++ b/routes/web.php @@ -3,7 +3,9 @@ declare(strict_types=1); use App\Http\Middleware\CheckVersion; +use App\Http\Middleware\IdentifyOnboardingStatus; use App\Http\Middleware\IdentifyProject; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Route; Route::get('/', function () { @@ -24,7 +26,10 @@ 'auth', IdentifyProject::class, CheckVersion::class, + IdentifyOnboardingStatus::class, ])->group(function () { + Route::get('/onboarding/dismiss', [App\Http\Controllers\UserController::class, 'action_dismiss_onboarding'])->name('onboarding.dismiss'); + Route::get('/activities', [App\Http\Controllers\ActivityController::class, 'page_index'])->name('activity.index')->middleware('ui.permission.guard:activities.view'); Route::get('/projects', [App\Http\Controllers\ProjectController::class, 'page_index'])->name('project.index')->middleware('ui.permission.guard:projects.view'); diff --git a/tests/Unit/Models/UserTest.php b/tests/Unit/Models/UserTest.php index 0ee0137..49d23d5 100644 --- a/tests/Unit/Models/UserTest.php +++ b/tests/Unit/Models/UserTest.php @@ -58,6 +58,7 @@ public function itHasCorrectFillableAttributes(): void 'name', 'email', 'password', + 'onboarding_dismissed_at', ]; $this->assertEquals($fillable, $this->user->getFillable()); @@ -82,9 +83,10 @@ public function itHasCorrectHiddenAttributes(): void public function itHasCorrectCasts(): void { $casts = [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - 'id' => 'int', + 'email_verified_at' => 'datetime', + 'password' => 'hashed', + 'onboarding_dismissed_at' => 'datetime', + 'id' => 'int', ]; $this->assertEquals($casts, $this->user->getCasts()); From ae4f19876e5bda328fa0a607ddf5b1cf08bccffb Mon Sep 17 00:00:00 2001 From: Marcel Menk Date: Sat, 26 Jul 2025 18:35:13 +0200 Subject: [PATCH 2/2] chore: add guide background --- resources/images/path.svg | 12 +++++++++++ resources/sass/app.scss | 27 ++++++++++++++++++++++++- resources/views/project/index.blade.php | 4 ++-- 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 resources/images/path.svg diff --git a/resources/images/path.svg b/resources/images/path.svg new file mode 100644 index 0000000..5f9f413 --- /dev/null +++ b/resources/images/path.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/resources/sass/app.scss b/resources/sass/app.scss index dbae914..1b62015 100644 --- a/resources/sass/app.scss +++ b/resources/sass/app.scss @@ -572,4 +572,29 @@ main { height: 1rem; border-radius: 50%; background-color: var(--bs-warning); -} \ No newline at end of file +} + +.get-started { + > .card-body { + position: relative; + + &:after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + width: 100%; + height: 100%; + background: url('../images/path.svg') no-repeat right center; + background-size: contain; + z-index: 1; + opacity: 0.25; + transform: scale(1.25); + } + + > * { + position: relative; + z-index: 2; + } + } +} diff --git a/resources/views/project/index.blade.php b/resources/views/project/index.blade.php index 01b873e..69d1a38 100644 --- a/resources/views/project/index.blade.php +++ b/resources/views/project/index.blade.php @@ -15,10 +15,10 @@
@if (empty(request()->get('project'))) @if (!$onboarding_dismissed) -
+
{{ __('Getting started') }} - +