Hey Q! Make Me A Cool Web App
Welcome to the Hey Q! blog series where we build fun apps using Amazon Q Developer and learn about the code along the way!
The scope of the sample apps created in this series are small 1-2 page applications that are fun, well designed and properly architected following best practices.
Here's a quick link to the other blog posts in the series:
And, the best part, the code is all open-source allowing you to grab it and use it for your apps! Here's the link to the completed open-source project: https://github.com/TheCodeTraveler/TaskQuest
Note: Responses from LLMs are non-deterministic which can result in different outputs despite asking the same question. You may receive slightly different responses from Q Developer when trying to replicate the same steps below.
This week we are building a Web App using Blazor Server. For this app, I have a few goals:
1. The app must compile and run on in a browser
2. The app must have a nice, highly polished, professional User Interface (UI)
3. The app must have a fun name that relates to its use
4. The code must be created by first using the latest version of the Blazor Server App template in the latest version of Visual Studio
5. The code must use the MVVM Architecture
6. The code must follow best practices
7. The code must use the most-recent versions for all dependencies
The Completed App
Note: If you're interested in learning more about prompt engineering and want to see the steps I used to guide Q Developer CLI, scroll down to the Appendix.
This app looks great! It runs on Edge, Chrome and Safari, as requested. And it certainly looks as if it were designed with a professional User Interface, as requested. As a developer who struggles with design, this also provides me a fantastic starting point where I can now take the code provided and make minor edits to adjust the color palate, adjust padding, adjust margins etc.
The app is also responsive for viewing on mobile and web; in other words, the app's UI adjusts automatically when I change the size of my browser window.
The gamification of completing tasks is a very lovely touch and encourages me to keep using the app. This will be good for improving its Monthly Active User (MAU) metrics and should keep users coming back.
I also appreciate that everything works! All of the buttons work, all of the text box inputs work; it just works. With previous AI coding companions, asking it to create an app from scratch required us developers to first edit the generated code to actually implement its functionality, but not anymore.
Finally, I greatly appreciate it using Dependency Injection and using Interfaces that we can leverage when mocking out our Unit Tests later. Testability is a big component for properly architecting any .NET application and with the MVVM Architecture scaffolded by Q Developer CLI, I will be able to easily create a comprehensive Unit Test suite.
Things to Watch Out For
After building this app using Q Developer CLI, there was only one compiler error. The bug was found in MainLayout.razor
. Specifically, MainLayout.razor
was inheriting from the incorrect base library. After comparing he qpp generated by Q Developer CLI with the Blazor Web App template, I found it to be an easy fix: change @inherits LayoutView
to @inherits LayoutComponentBase
.
Appendix
For anyone out there who is interested in learning more about prompt engineering, this section contains the steps I followed to create this app.
I've already installed Q Developer CLI on my Mac, so let's begin by opening the macOS Terminal and begin chatting with Q using the following command:
> q chat
Next, I will help Q out a bit by giving Q a profile. This reduces the scope of the datasets that Q will reference helps Q better understand the problem we are trying to solve together:
Prompt
Hey Q! You are a senior Blazor Developer who is an expert in C# and always follows C# and Web best practices. You always reference the official C# documentation on https://learn.microsoft.com/dotnet/csharp/ when writing your code and you always reference the official Blazor documentation when writing your code https://learn.microsoft.com/aspnet/core/blazor/. You always write code that uses the recommended libraries, like .NET Aspire (https://www.nuget.org/profiles/aspire) and CommunityToolkit.Mvvm (https://www.nuget.org/packages/CommunityToolkit.Mvvm). The User Interface for your apps are always highly polished and professional, giving the appearance that it was created by a Senior Designer. In addition to the reference domains above, you are also allowed to reference the open-source NET runtime code on GitHub, https://github.com/dotnet/runtime, andthe open-source .NET MAUI code on GitHub https://github.com/dotnet/maui.
Done! Now let's ask it to start building us an app and see what happens!
Prompt
Starting first using the latest version of the Blazor Web App template in the latest version of Visual Studio, create a fun one-page web app that adheres to the following rules: 1. The app must compile and run in all popular browsers, 2. The app must have a nice, highly polished, professional User Interface (UI), 3. The app must have a fun name that relates to its use, 4. The code must created by first using the latest version of the Blazor Web App template in the latest version of Visual Studio, 5. The code must use the MVVM Architecture, 6. The code must follow best practices, 7. The code must use the most-recent versions for all dependencies
Response
I'll help you create a fun Blazor Web App following all your requirements. Let me create a polished "Task Quest" - a gamified todolist application that turns productivity into an adventure!
+ 1: <Project Sdk="Microsoft.NET.Sdk.Web">
+ 2:
+ 3: <PropertyGroup>
+ 4: <TargetFramework>net8.0</TargetFramework>
+ 5: <Nullable>enable</Nullable>
+ 6: <ImplicitUsings>enable</ImplicitUsings>
+ 7: </PropertyGroup>
+ 8:
+ 9: <ItemGroup>
+ 10: <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="8.0.0" />
+ 11: <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0" />
+ 12: <PackageReference Include="CommunityToolkit.Mvvm" Version="8.4.0" />
+ 13: </ItemGroup>
+ 14:
+ 15: </Project>
Let's ask it to use .NET 9 and to use the corresponding latest versions of Microsoft.AspNetCore.Components.QuickGrid
and Microsoft.AspNetCore.Components.Web
Prompt
Please use .NET 9. Please use the latest version of `Microsoft.AspNetCore.Components.Web`, 9.0.7. Please use the latest version of `Microsoft.AspNetCore.Components.QuickGrid`, 9.0.7.
Response
I'll create a fun Blazor Web App called "Task Quest" using .NET 9 and the latest package versions you specified. This will be a gamified todo list that turns productivity into an adventure!
+ 1: <Project Sdk="Microsoft.NET.Sdk.Web">
+ 2:
+ 3: <PropertyGroup>
+ 4: <TargetFramework>net9.0</TargetFramework>
+ 5: <Nullable>enable</Nullable>
+ 6: <ImplicitUsings>enable</ImplicitUsings>
+ 7: </PropertyGroup>
+ 8:
+ 9: <ItemGroup>
+ 10: <PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" Version="9.0.7" />
+ 11: <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="9.0.7" />
+ 12: </ItemGroup>
+ 13:
+ 14: </Project>
Much better! Approved ✅
Next, it has created the Program.cs file for our app:
+ 1: using TaskQuest.Components;
+ 2: using TaskQuest.Services;
+ 3: using TaskQuest.ViewModels;
+ 4:
+ 5: var builder = WebApplication.CreateBuilder(args);
+ 6:
+ 7: // Add services to the container.
+ 8: builder.Services.AddRazorComponents()
+ 9: .AddInteractiveServerComponents();
+ 10:
+ 11: // Register MVVM services
+ 12: builder.Services.AddScoped<ITaskService, TaskService>();
+ 13: builder.Services.AddScoped<TaskQuestViewModel>();
+ 14:
+ 15: var app = builder.Build();
+ 16:
+ 17: // Configure the HTTP request pipeline.
+ 18: if (!app.Environment.IsDevelopment())
+ 19: {
+ 20: app.UseExceptionHandler("/Error", createScopeForErrors: true);
+ 21: app.UseHsts();
+ 22: }
+ 23:
+ 24: app.UseHttpsRedirection();
+ 25: app.UseStaticFiles();
+ 26: app.UseAntiforgery();
+ 27:
+ 28: app.MapRazorComponents<App>()
+ 29: .AddInteractiveServerRenderMode();
+ 30:
+ 31: app.Run();
Approved ✅
Next, it created our App.razor file:
+ 1: <!DOCTYPE html>
+ 2: <html lang="en">
+ 3:
+ 4: <head>
+ 5: <meta charset="utf-8" />
+ 6: <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ 7: <base href="/" />
+ 8: <link rel="icon" type="image/png" href="favicon.png" />
+ 9: <title>Task Quest - Gamify Your Productivity</title>
+ 10: <HeadOutlet />
+ 11: <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
+ 12: <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
+ 13: <link href="app.css" rel="stylesheet" />
+ 14: <link href="TaskQuest.styles.css" rel="stylesheet" />
+ 15: </head>
+ 16:
+ 17: <body>
+ 18: <Routes />
+ 19:
+ 20: <div id="blazor-error-ui">
+ 21: <div class="alert alert-danger" role="alert">
+ 22: <i class="fas fa-exclamation-triangle"></i>
+ 23: An unhandled error has occurred.
+ 24: <a href="" class="reload">Reload</a>
+ 25: <a class="dismiss">🗙</a>
+ 26: </div>
+ 27: </div>
+ 28:
+ 29: <script src="_framework/blazor.web.js"></script>
+ 30: <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
+ 31: </body>
+ 32:
+ 33: </html>
Approved ✅
Next, it has created the /Components/Routes.razor file:
+ 1: <Router AppAssembly="@typeof(App).Assembly">
+ 2: <Found Context="routeData">
+ 3: <RouteView RouteData="@routeData" DefaultLayout="@typeof(Components.Layout.MainLayout)" />
+ 4: <FocusOnNavigate RouteData="@routeData" Selector="h1" />
+ 5: </Found>
+ 6: </Router>
Approved ✅
Next, it has created the /Components/Routes.razor file:
+ 1: @inherits LayoutView
+ 2:
+ 3: <div class="page">
+ 4: <main>
+ 5: @Body
+ 6: </main>
+ 7: </div>
+ 8:
+ 9: <div id="blazor-error-ui">
+ 10: An unhandled error has occurred.
+ 11: <a href="" class="reload">Reload</a>
+ 12: <a class="dismiss">🗙</a>
+ 13: </div>
Approved ✅
Next, it has created our Model, Quest
, along with some seed data:
+ 1: namespace TaskQuest.Models;
+ 2:
+ 3: public class QuestTask
+ 4: {
+ 5: public int Id { get; set; }
+ 6: public string Title { get; set; } = string.Empty;
+ 7: public string Description { get; set; } = string.Empty;
+ 8: public TaskPriority Priority { get; set; } = TaskPriority.Medium;
+ 9: public bool IsCompleted { get; set; }
+ 10: public DateTime CreatedAt { get; set; } = DateTime.Now;
+ 11: public DateTime? CompletedAt { get; set; }
+ 12: public int ExperiencePoints { get; set; }
+ 13: public string Icon { get; set; } = "fas fa-tasks";
+ 14: }
+ 15:
+ 16: public enum TaskPriority
+ 17: {
+ 18: Low = 1,
+ 19: Medium = 2,
+ 20: High = 3,
+ 21: Critical = 4
+ 22: }
+ 23:
+ 24: public static class TaskPriorityExtensions
+ 25: {
+ 26: public static string GetDisplayName(this TaskPriority priority) => priority switch
+ 27: {
+ 28: TaskPriority.Low => "Low Priority",
+ 29: TaskPriority.Medium => "Medium Priority",
+ 30: TaskPriority.High => "High Priority",
+ 31: TaskPriority.Critical => "Critical",
+ 32: _ => "Unknown"
+ 33: };
+ 34:
+ 35: public static string GetCssClass(this TaskPriority priority) => priority switch
+ 36: {
+ 37: TaskPriority.Low => "priority-low",
+ 38: TaskPriority.Medium => "priority-medium",
+ 39: TaskPriority.High => "priority-high",
+ 40: TaskPriority.Critical => "priority-critical",
+ 41: _ => "priority-medium"
+ 42: };
+ 43:
+ 44: public static int GetExperiencePoints(this TaskPriority priority) => priority switch
+ 45: {
+ 46: TaskPriority.Low => 10,
+ 47: TaskPriority.Medium => 25,
+ 48: TaskPriority.High => 50,
+ 49: TaskPriority.Critical => 100,
+ 50: _ => 25
+ 51: };
+ 52: }
Nice! It even created an enum
and included some extension methods. Approved ✅
Next, it created the ITaskService
interface:
+ 1: using TaskQuest.Models;
+ 2:
+ 3: namespace TaskQuest.Services;
+ 4:
+ 5: public interface ITaskService
+ 6: {
+ 7: Task<List<QuestTask>> GetAllTasksAsync();
+ 8: Task<QuestTask?> GetTaskByIdAsync(int id);
+ 9: Task<QuestTask> CreateTaskAsync(QuestTask task);
+ 10: Task<QuestTask> UpdateTaskAsync(QuestTask task);
+ 11: Task<bool> DeleteTaskAsync(int id);
+ 12: Task<QuestTask> CompleteTaskAsync(int id);
+ 13: Task<int> GetTotalExperiencePointsAsync();
+ 14: Task<int> GetCompletedTasksCountAsync();
+ 15: }
I assume that we will be leveraging this interface for Dependency Injection later. Approved ✅
Next, it created our TaskService
:
+ 1: using TaskQuest.Models;
+ 2:
+ 3: namespace TaskQuest.Services;
+ 4:
+ 5: public class TaskService : ITaskService
+ 6: {
+ 7: private readonly List<QuestTask> _tasks = [];
+ 8: private int _nextId = 1;
+ 9:
+ 10: public TaskService()
+ 11: {
+ 12: // Initialize with some sample tasks
+ 13: SeedData();
+ 14: }
+ 15:
+ 16: public Task<List<QuestTask>> GetAllTasksAsync()
+ 17: {
+ 18: return Task.FromResult([.. _tasks]);
+ 19: }
+ 20:
+ 21: public Task<QuestTask?> GetTaskByIdAsync(int id)
+ 22: {
+ 23: var task = _tasks.FirstOrDefault(t => t.Id == id);
+ 24: return Task.FromResult(task);
+ 25: }
+ 26:
+ 27: public Task<QuestTask> CreateTaskAsync(QuestTask task)
+ 28: {
+ 29: task.Id = _nextId++;
+ 30: task.ExperiencePoints = task.Priority.GetExperiencePoints();
+ 31: _tasks.Add(task);
+ 32: return Task.FromResult(task);
+ 33: }
+ 34:
+ 35: public Task<QuestTask> UpdateTaskAsync(QuestTask task)
+ 36: {
+ 37: var existingTask = _tasks.FirstOrDefault(t => t.Id == task.Id);
+ 38: if (existingTask != null)
+ 39: {
+ 40: var index = _tasks.IndexOf(existingTask);
+ 41: task.ExperiencePoints = task.Priority.GetExperiencePoints();
+ 42: _tasks[index] = task;
+ 43: }
+ 44: return Task.FromResult(task);
+ 45: }
+ 46:
+ 47: public Task<bool> DeleteTaskAsync(int id)
+ 48: {
+ 49: var task = _tasks.FirstOrDefault(t => t.Id == id);
+ 50: if (task != null)
+ 51: {
+ 52: _tasks.Remove(task);
+ 53: return Task.FromResult(true);
+ 54: }
+ 55: return Task.FromResult(false);
+ 56: }
+ 57:
+ 58: public Task<QuestTask> CompleteTaskAsync(int id)
+ 59: {
+ 60: var task = _tasks.FirstOrDefault(t => t.Id == id);
+ 61: if (task != null)
+ 62: {
+ 63: task.IsCompleted = true;
+ 64: task.CompletedAt = DateTime.Now;
+ 65: }
+ 66: return Task.FromResult(task!);
+ 67: }
+ 68:
+ 69: public Task<int> GetTotalExperiencePointsAsync()
+ 70: {
+ 71: var totalXp = _tasks.Where(t => t.IsCompleted).Sum(t => t.ExperiencePoints);
+ 72: return Task.FromResult(totalXp);
+ 73: }
+ 74:
+ 75: public Task<int> GetCompletedTasksCountAsync()
+ 76: {
+ 77: var completedCount = _tasks.Count(t => t.IsCompleted);
+ 78: return Task.FromResult(completedCount);
+ 79: }
+ 80:
+ 81: private void SeedData()
+ 82: {
+ 83: List<QuestTask> sampleTasks = [
+ 84: new()
+ 85: {
+ 86: Id = _nextId++,
+ 87: Title = "Welcome to Task Quest!",
+ 88: Description = "Complete this task to earn your first experience points",
+ 89: Priority = TaskPriority.Low,
+ 90: Icon = "fas fa-star",
+ 91: ExperiencePoints = TaskPriority.Low.GetExperiencePoints()
+ 92: },
+ 93: new()
+ 94: {
+ 95: Id = _nextId++,
+ 96: Title = "Review Project Documentation",
+ 97: Description = "Go through the project requirements and technical specifications",
+ 98: Priority = TaskPriority.Medium,
+ 99: Icon = "fas fa-book",
+ 100: ExperiencePoints = TaskPriority.Medium.GetExperiencePoints()
+ 101: },
+ 102: new()
+ 103: {
+ 104: Id = _nextId++,
+ 105: Title = "Deploy to Production",
+ 106: Description = "Deploy the latest version to the production environment",
+ 107: Priority = TaskPriority.Critical,
+ 108: Icon = "fas fa-rocket",
+ 109: ExperiencePoints = TaskPriority.Critical.GetExperiencePoints()
+ 110: }
+ 111: ];
+ 112:
+ 113: _tasks.AddRange(sampleTasks);
+ 114: }
+ 115: }
Approved ✅
Next, it has generated our View Model:
+ 1: using CommunityToolkit.Mvvm.ComponentModel;
+ 2: using CommunityToolkit.Mvvm.Input;
+ 3: using TaskQuest.Models;
+ 4: using TaskQuest.Services;
+ 5:
+ 6: namespace TaskQuest.ViewModels;
+ 7:
+ 8: public partial class TaskQuestViewModel : ObservableObject
+ 9: {
+ 10: private readonly ITaskService _taskService;
+ 11:
+ 12: [ObservableProperty]
+ 13: [NotifyPropertyChangedFor(nameof(FilteredTasks))]
+ 14: private List<QuestTask> _tasks = [];
+ 15:
+ 16: [ObservableProperty]
+ 17: private QuestTask _currentTask = new();
+ 18:
+ 19: [ObservableProperty]
+ 20: private bool _isEditing;
+ 21:
+ 22: [ObservableProperty]
+ 23: [NotifyPropertyChangedFor(nameof(PlayerLevel), nameof(ExperienceToNextLevel), nameof(ExperienceProgress))]
+ 24: private int _totalExperience;
+ 25:
+ 26: [ObservableProperty]
+ 27: private int _completedTasksCount;
+ 28:
+ 29: [ObservableProperty]
+ 30: [NotifyPropertyChangedFor(nameof(FilteredTasks))]
+ 31: private string _filterStatus = "All";
+ 32:
+ 33: public TaskQuestViewModel(ITaskService taskService)
+ 34: {
+ 35: _taskService = taskService;
+ 36: _ = LoadDataAsync();
+ 37: }
+ 38:
+ 39: public List<QuestTask> FilteredTasks
+ 40: {
+ 41: get
+ 42: {
+ 43: return FilterStatus switch
+ 44: {
+ 45: "Completed" => [.. Tasks.Where(t => t.IsCompleted)],
+ 46: "Pending" => [.. Tasks.Where(t => !t.IsCompleted)],
+ 47: _ => [.. Tasks]
+ 48: };
+ 49: }
+ 50: }
+ 51:
+ 52: public int PlayerLevel => (TotalExperience / 100) + 1;
+ 53:
+ 54: public int ExperienceToNextLevel => 100 - (TotalExperience % 100);
+ 55:
+ 56: public double ExperienceProgress => (TotalExperience % 100) / 100.0 * 100;
+ 57:
+ 58: public List<string> FilterOptions => ["All", "Pending", "Completed"];
+ 59:
+ 60: public List<TaskPriority> PriorityOptions => [TaskPriority.Low, TaskPriority.Medium, TaskPriority.High, TaskPriority.Critical];
+ 61:
+ 62: public List<string> IconOptions => [
+ 63: "fas fa-tasks", "fas fa-star", "fas fa-rocket", "fas fa-book",
+ 64: "fas fa-code", "fas fa-bug", "fas fa-lightbulb", "fas fa-cog",
+ 65: "fas fa-chart-line", "fas fa-users", "fas fa-envelope", "fas fa-phone"
+ 66: ];
+ 67:
+ 68: [RelayCommand]
+ 69: public async Task LoadDataAsync()
+ 70: {
+ 71: Tasks = await _taskService.GetAllTasksAsync();
+ 72: await UpdateStatsAsync();
+ 73: }
+ 74:
+ 75: [RelayCommand]
+ 76: public async Task CreateTaskAsync()
+ 77: {
+ 78: if (string.IsNullOrWhiteSpace(CurrentTask.Title)) return;
+ 79:
+ 80: await _taskService.CreateTaskAsync(CurrentTask);
+ 81: await LoadDataAsync();
+ 82: ResetCurrentTask();
+ 83: }
+ 84:
+ 85: [RelayCommand]
+ 86: public async Task UpdateTaskAsync()
+ 87: {
+ 88: await _taskService.UpdateTaskAsync(CurrentTask);
+ 89: await LoadDataAsync();
+ 90: ResetCurrentTask();
+ 91: IsEditing = false;
+ 92: }
+ 93:
+ 94: [RelayCommand]
+ 95: public async Task DeleteTaskAsync(int id)
+ 96: {
+ 97: await _taskService.DeleteTaskAsync(id);
+ 98: await LoadDataAsync();
+ 99: }
+ 100:
+ 101: [RelayCommand]
+ 102: public async Task CompleteTaskAsync(int id)
+ 103: {
+ 104: await _taskService.CompleteTaskAsync(id);
+ 105: await LoadDataAsync();
+ 106: }
+ 107:
+ 108: [RelayCommand]
+ 109: public void EditTask(QuestTask task)
+ 110: {
+ 111: CurrentTask = new QuestTask
+ 112: {
+ 113: Id = task.Id,
+ 114: Title = task.Title,
+ 115: Description = task.Description,
+ 116: Priority = task.Priority,
+ 117: Icon = task.Icon,
+ 118: IsCompleted = task.IsCompleted,
+ 119: CreatedAt = task.CreatedAt,
+ 120: CompletedAt = task.CompletedAt,
+ 121: ExperiencePoints = task.ExperiencePoints
+ 122: };
+ 123: IsEditing = true;
+ 124: }
+ 125:
+ 126: [RelayCommand]
+ 127: public void ResetCurrentTask()
+ 128: {
+ 129: CurrentTask = new QuestTask();
+ 130: IsEditing = false;
+ 131: }
+ 132:
+ 133: private async Task UpdateStatsAsync()
+ 134: {
+ 135: TotalExperience = await _taskService.GetTotalExperiencePointsAsync();
+ 136: CompletedTasksCount = await _taskService.GetCompletedTasksCountAsync();
+ 137: }
+ 138: }
Approved ✅
Next, it created the main Page of our app:
+ 1: @page "/"
+ 2: @using TaskQuest.ViewModels
+ 3: @using TaskQuest.Models
+ 4: @inject TaskQuestViewModel ViewModel
+ 5: @rendermode InteractiveServer
+ 6:
+ 7: <PageTitle>Task Quest - Gamify Your Productivity</PageTitle>
+ 8:
+ 9: <div class="container-fluid task-quest-app">
+ 10: <!-- Header Section -->
+ 11: <div class="row mb-4">
+ 12: <div class="col-12">
+ 13: <div class="hero-section text-center py-4">
+ 14: <h1 class="display-4 text-gradient">
+ 15: <i class="fas fa-dragon me-3"></i>Task Quest
+ 16: </h1>
+ 17: <p class="lead">Transform your productivity into an epic adventure!</p>
+ 18: </div>
+ 19: </div>
+ 20: </div>
+ 21:
+ 22: <!-- Stats Dashboard -->
+ 23: <div class="row mb-4">
+ 24: <div class="col-md-3 col-sm-6 mb-3">
+ 25: <div class="stat-card level-card">
+ 26: <div class="stat-icon">
+ 27: <i class="fas fa-crown"></i>
+ 28: </div>
+ 29: <div class="stat-content">
+ 30: <h3>Level @ViewModel.PlayerLevel</h3>
+ 31: <p>Productivity Hero</p>
+ 32: </div>
+ 33: </div>
+ 34: </div>
+ 35: <div class="col-md-3 col-sm-6 mb-3">
+ 36: <div class="stat-card xp-card">
+ 37: <div class="stat-icon">
+ 38: <i class="fas fa-star"></i>
+ 39: </div>
+ 40: <div class="stat-content">
+ 41: <h3>@ViewModel.TotalExperience XP</h3>
+ 42: <p>Total Experience</p>
+ 43: <div class="progress mt-2">
+ 44: <div class="progress-bar progress-bar-animated"
+ 45: style="width: @(ViewModel.ExperienceProgress)%"></div>
+ 46: </div>
+ 47: <small>@ViewModel.ExperienceToNextLevel XP to next level</small>
+ 48: </div>
+ 49: </div>
+ 50: </div>
+ 51: <div class="col-md-3 col-sm-6 mb-3">
+ 52: <div class="stat-card completed-card">
+ 53: <div class="stat-icon">
+ 54: <i class="fas fa-trophy"></i>
+ 55: </div>
+ 56: <div class="stat-content">
+ 57: <h3>@ViewModel.CompletedTasksCount</h3>
+ 58: <p>Quests Completed</p>
+ 59: </div>
+ 60: </div>
+ 61: </div>
+ 62: <div class="col-md-3 col-sm-6 mb-3">
+ 63: <div class="stat-card pending-card">
+ 64: <div class="stat-icon">
+ 65: <i class="fas fa-hourglass-half"></i>
+ 66: </div>
+ 67: <div class="stat-content">
+ 68: <h3>@(ViewModel.Tasks.Count - ViewModel.CompletedTasksCount)</h3>
+ 69: <p>Active Quests</p>
+ 70: </div>
+ 71: </div>
+ 72: </div>
+ 73: </div>
+ 74:
+ 75: <!-- Task Management Section -->
+ 76: <div class="row">
+ 77: <div class="col-lg-4 mb-4">
+ 78: <div class="card task-form-card">
+ 79: <div class="card-header">
+ 80: <h5 class="mb-0">
+ 81: <i class="fas @(ViewModel.IsEditing ? "fa-edit" : "fa-plus") me-2"></i>
+ 82: @(ViewModel.IsEditing ? "Edit Quest" : "Create New Quest")
+ 83: </h5>
+ 84: </div>
+ 85: <div class="card-body">
+ 86: <EditForm Model="ViewModel.CurrentTask" OnValidSubmit="@(ViewModel.IsEditing ? ViewModel.UpdateTaskCommand.ExecuteAsync : ViewModel.CreateTaskCommand.ExecuteAsync)">
+ 87: <div class="mb-3">
+ 88: <label class="form-label">Quest Title</label>
+ 89: <InputText @bind-Value="ViewModel.CurrentTask.Title" class="form-control" placeholder="Enter quest title..." />
+ 90: </div>
+ 91:
+ 92: <div class="mb-3">
+ 93: <label class="form-label">Description</label>
+ 94: <InputTextArea @bind-Value="ViewModel.CurrentTask.Description" class="form-control" rows="3" placeholder="Describe your quest..." />
+ 95: </div>
+ 96:
+ 97: <div class="row mb-3">
+ 98: <div class="col-6">
+ 99: <label class="form-label">Priority</label>
+ 100: <InputSelect @bind-Value="ViewModel.CurrentTask.Priority" class="form-select">
+ 101: @foreach (var priority in ViewModel.PriorityOptions)
+ 102: {
+ 103: <option value="@priority">@priority.GetDisplayName()</option>
+ 104: }
+ 105: </InputSelect>
+ 106: </div>
+ 107: <div class="col-6">
+ 108: <label class="form-label">Icon</label>
+ 109: <InputSelect @bind-Value="ViewModel.CurrentTask.Icon" class="form-select">
+ 110: @foreach (var icon in ViewModel.IconOptions)
+ 111: {
+ 112: <option value="@icon">@icon.Replace("fas fa-", "").Replace("-", " ")</option>
+ 113: }
+ 114: </InputSelect>
+ 115: </div>
+ 116: </div>
+ 117:
+ 118: <div class="d-grid gap-2">
+ 119: <button type="submit" class="btn btn-primary">
+ 120: <i class="fas @(ViewModel.IsEditing ? "fa-save" : "fa-plus") me-2"></i>
+ 121: @(ViewModel.IsEditing ? "Update Quest" : "Create Quest")
+ 122: </button>
+ 123: @if (ViewModel.IsEditing)
+ 124: {
+ 125: <button type="button" class="btn btn-secondary" @onclick="ViewModel.ResetCurrentTaskCommand.Execute">
+ 126: <i class="fas fa-times me-2"></i>Cancel
+ 127: </button>
+ 128: }
+ 129: </div>
+ 130: </EditForm>
+ 131: </div>
+ 132: </div>
+ 133: </div>
+ 134:
+ 135: <div class="col-lg-8">
+ 136: <div class="card task-list-card">
+ 137: <div class="card-header d-flex justify-content-between align-items-center">
+ 138: <h5 class="mb-0">
+ 139: <i class="fas fa-list me-2"></i>Your Quests
+ 140: </h5>
+ 141: <div class="filter-controls">
+ 142: <select @bind="ViewModel.FilterStatus" class="form-select form-select-sm">
+ 143: @foreach (var filter in ViewModel.FilterOptions)
+ 144: {
+ 145: <option value="@filter">@filter</option>
+ 146: }
+ 147: </select>
+ 148: </div>
+ 149: </div>
+ 150: <div class="card-body p-0">
+ 151: @if (ViewModel.FilteredTasks.Any())
+ 152: {
+ 153: <div class="task-list">
+ 154: @foreach (var task in ViewModel.FilteredTasks.OrderByDescending(t => t.CreatedAt))
+ 155: {
+ 156: <div class="task-item @(task.IsCompleted ? "completed" : "") @task.Priority.GetCssClass()">
+ 157: <div class="task-content">
+ 158: <div class="task-header">
+ 159: <div class="task-icon">
+ 160: <i class="@task.Icon"></i>
+ 161: </div>
+ 162: <div class="task-info">
+ 163: <h6 class="task-title @(task.IsCompleted ? "text-decoration-line-through" : "")">
+ 164: @task.Title
+ 165: </h6>
+ 166: <p class="task-description">@task.Description</p>
+ 167: <div class="task-meta">
+ 168: <span class="badge bg-secondary me-2">@task.Priority.GetDisplayName()</span>
+ 169: <span class="badge bg-info me-2">@task.ExperiencePoints XP</span>
+ 170: <small class="text-muted">Created: @task.CreatedAt.ToString("MMM dd, yyyy")</small>
+ 171: @if (task.IsCompleted && task.CompletedAt.HasValue)
+ 172: {
+ 173: <small class="text-success ms-2">Completed: @task.CompletedAt.Value.ToString("MMM dd, yyyy")</small>
+ 174: }
+ 175: </div>
+ 176: </div>
+ 177: </div>
+ 178: </div>
+ 179: <div class="task-actions">
+ 180: @if (!task.IsCompleted)
+ 181: {
+ 182: <button class="btn btn-success btn-sm me-2" @onclick="() => ViewModel.CompleteTaskCommand.ExecuteAsync(task.Id)">
+ 183: <i class="fas fa-check"></i>
+ 184: </button>
+ 185: <button class="btn btn-warning btn-sm me-2" @onclick="() => ViewModel.EditTaskCommand.Execute(task)">
+ 186: <i class="fas fa-edit"></i>
+ 187: </button>
+ 188: }
+ 189: <button class="btn btn-danger btn-sm" @onclick="() => ViewModel.DeleteTaskCommand.ExecuteAsync(task.Id)">
+ 190: <i class="fas fa-trash"></i>
+ 191: </button>
+ 192: </div>
+ 193: </div>
+ 194: }
+ 195: </div>
+ 196: }
+ 197: else
+ 198: {
+ 199: <div class="empty-state text-center py-5">
+ 200: <i class="fas fa-dragon fa-3x text-muted mb-3"></i>
+ 201: <h5>No quests found</h5>
+ 202: <p class="text-muted">
+ 203: @if (ViewModel.FilterStatus == "All")
+ 204: {
+ 205: <text>Create your first quest to begin your adventure!</text>
+ 206: }
+ 207: else
+ 208: {
+ 209: <text>No @ViewModel.FilterStatus.ToLower() quests available.</text>
+ 210: }
+ 211: </p>
+ 212: </div>
+ 213: }
+ 214: </div>
+ 215: </div>
+ 216: </div>
+ 217: </div>
+ 218: </div>
Nice! I see it is using dependency injection. Approved ✅
Q next generated the remaining CSS files. I won't post the output here since it is hundreds of lines long, but you can find all the files in on GitHub: https://github.com/TheCodeTraveler/TaskQuest
Finally, after that, it generated our Error Page:
+ 1: @page "/Error"
+ 2: @using System.Diagnostics
+ 3:
+ 4: <PageTitle>Error - Task Quest</PageTitle>
+ 5:
+ 6: <div class="container-fluid task-quest-app">
+ 7: <div class="row justify-content-center">
+ 8: <div class="col-md-6">
+ 9: <div class="card">
+ 10: <div class="card-header text-center">
+ 11: <h1 class="text-danger">
+ 12: <i class="fas fa-exclamation-triangle me-2"></i>
+ 13: Quest Failed!
+ 14: </h1>
+ 15: </div>
+ 16: <div class="card-body text-center">
+ 17: <p class="lead">An unexpected error occurred during your adventure.</p>
+ 18: <p>Don't worry, even the greatest heroes face setbacks!</p>
+ 19:
+ 20: @if (ShowRequestId)
+ 21: {
+ 22: <p class="text-muted">
+ 23: <strong>Request ID:</strong> <code>@RequestId</code>
+ 24: </p>
+ 25: }
+ 26:
+ 27: <div class="mt-4">
+ 28: <a href="/" class="btn btn-primary">
+ 29: <i class="fas fa-home me-2"></i>
+ 30: Return to Quest Hub
+ 31: </a>
+ 32: </div>
+ 33: </div>
+ 34: </div>
+ 35: </div>
+ 36: </div>
+ 37: </div>
+ 38:
+ 39: @code {
+ 40: [CascadingParameter]
+ 41: private HttpContext? HttpContext { get; set; }
+ 42:
+ 43: private string? RequestId { get; set; }
+ 44:
+ 45: private bool ShowRequestId => !string.IsNullOrEmpty(RequestId);
+ 46:
+ 47: protected override void OnInitialized() =>
+ 48: RequestId = Activity.Current?.Id ?? HttpContext?.TraceIdentifier;
+ 49: }
Approved ✅