Merge pull request #12 from bytegrip/dev
Dev
This commit was merged in pull request #12.
This commit is contained in:
23
Imprink.Integration.Tests/Imprink.Integration.Tests.csproj
Normal file
23
Imprink.Integration.Tests/Imprink.Integration.Tests.csproj
Normal file
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit.assert" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.extensibility.core" Version="2.9.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\src\Imprink.Application\Imprink.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Commands\" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -36,6 +36,7 @@ services:
|
||||
- ACCEPT_EULA=Y
|
||||
- SEQ_CACHE_SYSTEMRAMTARGET=0.9
|
||||
- BASE_URI=${SEQ_BASE_URI}
|
||||
- SEQ_FIRSTRUN_NOAUTHENTICATION=true
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ public class CreateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler<Cre
|
||||
};
|
||||
|
||||
var createdCategory = await unitOfWork.CategoryRepository.AddAsync(category, cancellationToken);
|
||||
await unitOfWork.SaveAsync(cancellationToken);
|
||||
await unitOfWork.CommitTransactionAsync(cancellationToken);
|
||||
|
||||
return new CategoryDto
|
||||
|
||||
@@ -0,0 +1,237 @@
|
||||
using Imprink.Application.Commands.Categories;
|
||||
using Imprink.Application.Dtos;
|
||||
using Imprink.Domain.Entities;
|
||||
using Imprink.Domain.Repositories;
|
||||
using Imprink.Infrastructure;
|
||||
using Imprink.Infrastructure.Database;
|
||||
using Imprink.Infrastructure.Repositories;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Data.Sqlite;
|
||||
|
||||
namespace Imprink.Application.Tests;
|
||||
|
||||
public class CreateCategoryHandlerIntegrationTest : IDisposable
|
||||
{
|
||||
private readonly ApplicationDbContext _context;
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly CreateCategoryHandler _handler;
|
||||
private readonly SqliteConnection _connection;
|
||||
|
||||
public CreateCategoryHandlerIntegrationTest()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
|
||||
_connection = new SqliteConnection("Data Source=:memory:");
|
||||
_connection.Open();
|
||||
|
||||
services.AddDbContext<ApplicationDbContext>(options =>
|
||||
options.UseSqlite(_connection));
|
||||
|
||||
services.AddScoped<ICategoryRepository, CategoryRepository>();
|
||||
services.AddScoped<IProductRepository, ProductRepository>();
|
||||
services.AddScoped<IProductVariantRepository, ProductVariantRepository>();
|
||||
services.AddScoped<IUserRepository, UserRepository>();
|
||||
services.AddScoped<IUserRoleRepository, UserRoleRepository>();
|
||||
services.AddScoped<IRoleRepository, RoleRepository>();
|
||||
services.AddScoped<IOrderRepository, OrderRepository>();
|
||||
services.AddScoped<IOrderItemRepository, OrderItemRepository>();
|
||||
|
||||
services.AddScoped<IUnitOfWork, UnitOfWork>();
|
||||
services.AddScoped<CreateCategoryHandler>();
|
||||
|
||||
_serviceProvider = services.BuildServiceProvider();
|
||||
_context = _serviceProvider.GetRequiredService<ApplicationDbContext>();
|
||||
_handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
|
||||
|
||||
_context.Database.EnsureCreated();
|
||||
}
|
||||
|
||||
private async Task CleanDatabase()
|
||||
{
|
||||
_context.Categories.RemoveRange(_context.Categories);
|
||||
await _context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidCreateCategoryCommand_PersistsToDatabase()
|
||||
{
|
||||
await CleanDatabase();
|
||||
|
||||
// Arrange
|
||||
var command = new CreateCategoryCommand
|
||||
{
|
||||
Name = "Electronics",
|
||||
Description = "Electronic devices and gadgets",
|
||||
ImageUrl = "https://example.com/electronics.jpg",
|
||||
SortOrder = 1,
|
||||
IsActive = true,
|
||||
ParentCategoryId = null
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.NotEqual(Guid.Empty, result.Id);
|
||||
Assert.Equal(command.Name, result.Name);
|
||||
Assert.Equal(command.Description, result.Description);
|
||||
Assert.Equal(command.ImageUrl, result.ImageUrl);
|
||||
Assert.Equal(command.SortOrder, result.SortOrder);
|
||||
Assert.Equal(command.IsActive, result.IsActive);
|
||||
Assert.Equal(command.ParentCategoryId, result.ParentCategoryId);
|
||||
Assert.NotNull(result.CreatedAt);
|
||||
Assert.NotNull(result.ModifiedAt);
|
||||
|
||||
var savedCategory = await _context.Categories
|
||||
.FirstOrDefaultAsync(c => c.Id == result.Id);
|
||||
|
||||
Assert.NotNull(savedCategory);
|
||||
Assert.Equal(command.Name, savedCategory.Name);
|
||||
Assert.Equal(command.Description, savedCategory.Description);
|
||||
Assert.Equal(command.ImageUrl, savedCategory.ImageUrl);
|
||||
Assert.Equal(command.SortOrder, savedCategory.SortOrder);
|
||||
Assert.Equal(command.IsActive, savedCategory.IsActive);
|
||||
Assert.Equal(command.ParentCategoryId, savedCategory.ParentCategoryId);
|
||||
Assert.NotNull(savedCategory.CreatedAt);
|
||||
Assert.NotNull(savedCategory.ModifiedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidCreateCategoryCommandWithParent_PersistsWithCorrectParentRelationship()
|
||||
{
|
||||
await CleanDatabase();
|
||||
|
||||
// Arrange
|
||||
var parentCategory = new Category
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
Name = "Parent Category",
|
||||
Description = "Parent category description",
|
||||
SortOrder = 1,
|
||||
IsActive = true,
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
ModifiedAt = DateTime.UtcNow
|
||||
};
|
||||
|
||||
_context.Categories.Add(parentCategory);
|
||||
await _context.SaveChangesAsync();
|
||||
|
||||
var command = new CreateCategoryCommand
|
||||
{
|
||||
Name = "Child Electronics",
|
||||
Description = "Electronic devices and gadgets under parent",
|
||||
ImageUrl = "https://example.com/child-electronics.jpg",
|
||||
SortOrder = 2,
|
||||
IsActive = true,
|
||||
ParentCategoryId = parentCategory.Id
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(parentCategory.Id, result.ParentCategoryId);
|
||||
|
||||
var savedCategory = await _context.Categories
|
||||
.Include(c => c.ParentCategory)
|
||||
.FirstOrDefaultAsync(c => c.Id == result.Id);
|
||||
|
||||
Assert.NotNull(savedCategory);
|
||||
Assert.NotNull(savedCategory.ParentCategory);
|
||||
Assert.Equal(parentCategory.Id, savedCategory.ParentCategory.Id);
|
||||
Assert.Equal(parentCategory.Name, savedCategory.ParentCategory.Name);
|
||||
|
||||
var parentWithChildren = await _context.Categories
|
||||
.Include(c => c.SubCategories)
|
||||
.FirstOrDefaultAsync(c => c.Id == parentCategory.Id);
|
||||
|
||||
Assert.NotNull(parentWithChildren);
|
||||
Assert.Contains(parentWithChildren.SubCategories, sc => sc.Id == result.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CreateMultipleCategories_AllPersistCorrectly()
|
||||
{
|
||||
await CleanDatabase();
|
||||
|
||||
// Arrange
|
||||
var commands = new[]
|
||||
{
|
||||
new CreateCategoryCommand
|
||||
{
|
||||
Name = "Category 1",
|
||||
Description = "First category",
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
},
|
||||
new CreateCategoryCommand
|
||||
{
|
||||
Name = "Category 2",
|
||||
Description = "Second category",
|
||||
SortOrder = 2,
|
||||
IsActive = false
|
||||
}
|
||||
};
|
||||
|
||||
// Act
|
||||
var results = new List<CategoryDto>();
|
||||
foreach (var command in commands)
|
||||
{
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
results.Add(result);
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(2, results.Count);
|
||||
|
||||
var categoriesInDb = await _context.Categories.ToListAsync();
|
||||
|
||||
Assert.Equal(2, categoriesInDb.Count);
|
||||
|
||||
foreach (var result in results)
|
||||
{
|
||||
var savedCategory = categoriesInDb.First(c => c.Id == result.Id);
|
||||
Assert.Equal(result.Name, savedCategory.Name);
|
||||
Assert.Equal(result.Description, savedCategory.Description);
|
||||
Assert.Equal(result.IsActive, savedCategory.IsActive);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_TransactionFailure_RollsBackCorrectly()
|
||||
{
|
||||
await CleanDatabase();
|
||||
|
||||
// Arrange
|
||||
var command = new CreateCategoryCommand
|
||||
{
|
||||
Name = "Test Category",
|
||||
Description = "Test description",
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
var initialCount = await _context.Categories.CountAsync();
|
||||
|
||||
var handler = _serviceProvider.GetRequiredService<CreateCategoryHandler>();
|
||||
|
||||
var result = await handler.Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
|
||||
var finalCount = await _context.Categories.CountAsync();
|
||||
Assert.Equal(initialCount + 1, finalCount);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
GC.SuppressFinalize(this);
|
||||
_context.Dispose();
|
||||
_connection.Close();
|
||||
_connection.Dispose();
|
||||
_serviceProvider.GetService<IServiceScope>()?.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -8,14 +8,18 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
|
||||
<PackageReference Include="Moq" Version="4.20.70" />
|
||||
<PackageReference Include="xunit" Version="2.6.6" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.6">
|
||||
<PackageReference Include="MediatR" Version="12.5.0" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.0-preview.1.24081.5" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.6" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Moq" Version="4.20.72" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.0">
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
@@ -28,6 +32,7 @@
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Imprink.Application\Imprink.Application.csproj" />
|
||||
<ProjectReference Include="..\..\src\Imprink.Domain\Imprink.Domain.csproj" />
|
||||
<ProjectReference Include="..\..\src\Imprink.Infrastructure\Imprink.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
244
tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs
Normal file
244
tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs
Normal file
@@ -0,0 +1,244 @@
|
||||
using Imprink.Application.Commands.Categories;
|
||||
using Imprink.Application.Exceptions;
|
||||
using Imprink.Domain.Entities;
|
||||
using Imprink.Domain.Repositories;
|
||||
using Moq;
|
||||
|
||||
namespace Imprink.Application.Tests;
|
||||
|
||||
public class UpdateCategoryHandlerTests
|
||||
{
|
||||
private readonly Mock<IUnitOfWork> _unitOfWorkMock;
|
||||
private readonly Mock<ICategoryRepository> _categoryRepositoryMock;
|
||||
private readonly UpdateCategoryHandler _handler;
|
||||
|
||||
public UpdateCategoryHandlerTests()
|
||||
{
|
||||
_unitOfWorkMock = new Mock<IUnitOfWork>();
|
||||
_categoryRepositoryMock = new Mock<ICategoryRepository>();
|
||||
|
||||
_unitOfWorkMock.Setup(x => x.CategoryRepository)
|
||||
.Returns(_categoryRepositoryMock.Object);
|
||||
|
||||
_handler = new UpdateCategoryHandler(_unitOfWorkMock.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_ValidRequest_ShouldUpdateCategoryAndReturnDto()
|
||||
{
|
||||
var categoryId = Guid.NewGuid();
|
||||
var parentCategoryId = Guid.NewGuid();
|
||||
var createdAt = DateTime.UtcNow.AddDays(-10);
|
||||
var modifiedAt = DateTime.UtcNow;
|
||||
|
||||
var existingCategory = new Category
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Old Name",
|
||||
Description = "Old Description",
|
||||
ImageUrl = "old-image.jpg",
|
||||
SortOrder = 1,
|
||||
IsActive = true,
|
||||
ParentCategoryId = null,
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = createdAt
|
||||
};
|
||||
|
||||
var updatedCategory = new Category
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "New Name",
|
||||
Description = "New Description",
|
||||
ImageUrl = "new-image.jpg",
|
||||
SortOrder = 2,
|
||||
IsActive = false,
|
||||
ParentCategoryId = parentCategoryId,
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = modifiedAt
|
||||
};
|
||||
|
||||
var command = new UpdateCategoryCommand
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "New Name",
|
||||
Description = "New Description",
|
||||
ImageUrl = "new-image.jpg",
|
||||
SortOrder = 2,
|
||||
IsActive = false,
|
||||
ParentCategoryId = parentCategoryId
|
||||
};
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.GetByIdAsync(categoryId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(existingCategory);
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<Category>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(updatedCategory);
|
||||
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(categoryId, result.Id);
|
||||
Assert.Equal("New Name", result.Name);
|
||||
Assert.Equal("New Description", result.Description);
|
||||
Assert.Equal("new-image.jpg", result.ImageUrl);
|
||||
Assert.Equal(2, result.SortOrder);
|
||||
Assert.False(result.IsActive);
|
||||
Assert.Equal(parentCategoryId, result.ParentCategoryId);
|
||||
Assert.Equal(createdAt, result.CreatedAt);
|
||||
Assert.Equal(modifiedAt, result.ModifiedAt);
|
||||
|
||||
_unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
|
||||
_categoryRepositoryMock.Verify(x => x.GetByIdAsync(categoryId, It.IsAny<CancellationToken>()), Times.Once);
|
||||
_categoryRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny<Category>(), It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CategoryNotFound_ShouldThrowNotFoundException()
|
||||
{
|
||||
// Arrange
|
||||
var categoryId = Guid.NewGuid();
|
||||
var command = new UpdateCategoryCommand
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Test Name",
|
||||
Description = "Test Description",
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.GetByIdAsync(categoryId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((Category?)null);
|
||||
|
||||
var exception = await Assert.ThrowsAsync<NotFoundException>(
|
||||
() => _handler.Handle(command, CancellationToken.None));
|
||||
|
||||
Assert.Equal($"Category with ID {categoryId} not found.", exception.Message);
|
||||
|
||||
_unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
|
||||
_categoryRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny<Category>(), It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_UpdateThrowsException_ShouldRollbackTransactionAndRethrow()
|
||||
{
|
||||
var categoryId = Guid.NewGuid();
|
||||
var existingCategory = new Category
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Existing Name",
|
||||
Description = "Existing Description",
|
||||
SortOrder = 1,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
var command = new UpdateCategoryCommand
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "New Name",
|
||||
Description = "New Description",
|
||||
SortOrder = 2,
|
||||
IsActive = false
|
||||
};
|
||||
|
||||
var expectedException = new InvalidOperationException("Database error");
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.GetByIdAsync(categoryId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(existingCategory);
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<Category>(), It.IsAny<CancellationToken>()))
|
||||
.ThrowsAsync(expectedException);
|
||||
|
||||
var thrownException = await Assert.ThrowsAsync<InvalidOperationException>(
|
||||
() => _handler.Handle(command, CancellationToken.None));
|
||||
|
||||
Assert.Same(expectedException, thrownException);
|
||||
|
||||
_unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Handle_CategoryWithNullableFields_ShouldHandleNullValues()
|
||||
{
|
||||
// Arrange
|
||||
var categoryId = Guid.NewGuid();
|
||||
var createdAt = DateTime.UtcNow.AddDays(-5);
|
||||
var modifiedAt = DateTime.UtcNow;
|
||||
|
||||
var existingCategory = new Category
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Test Category",
|
||||
Description = "Test Description",
|
||||
ImageUrl = "test-image.jpg",
|
||||
SortOrder = 1,
|
||||
IsActive = true,
|
||||
ParentCategoryId = Guid.NewGuid(),
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = createdAt
|
||||
};
|
||||
|
||||
var updatedCategory = new Category
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Updated Category",
|
||||
Description = "Updated Description",
|
||||
ImageUrl = null,
|
||||
SortOrder = 5,
|
||||
IsActive = false,
|
||||
ParentCategoryId = null,
|
||||
CreatedAt = createdAt,
|
||||
ModifiedAt = modifiedAt
|
||||
};
|
||||
|
||||
var command = new UpdateCategoryCommand
|
||||
{
|
||||
Id = categoryId,
|
||||
Name = "Updated Category",
|
||||
Description = "Updated Description",
|
||||
ImageUrl = null,
|
||||
SortOrder = 5,
|
||||
IsActive = false,
|
||||
ParentCategoryId = null
|
||||
};
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.GetByIdAsync(categoryId, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(existingCategory);
|
||||
|
||||
_categoryRepositoryMock
|
||||
.Setup(x => x.UpdateAsync(It.IsAny<Category>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(updatedCategory);
|
||||
|
||||
// Act
|
||||
var result = await _handler.Handle(command, CancellationToken.None);
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(result);
|
||||
Assert.Equal(categoryId, result.Id);
|
||||
Assert.Equal("Updated Category", result.Name);
|
||||
Assert.Equal("Updated Description", result.Description);
|
||||
Assert.Null(result.ImageUrl);
|
||||
Assert.Equal(5, result.SortOrder);
|
||||
Assert.False(result.IsActive);
|
||||
Assert.Null(result.ParentCategoryId);
|
||||
Assert.Equal(createdAt, result.CreatedAt);
|
||||
Assert.Equal(modifiedAt, result.ModifiedAt);
|
||||
|
||||
_unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny<CancellationToken>()), Times.Once);
|
||||
_unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
}
|
||||
104
webui/package-lock.json
generated
104
webui/package-lock.json
generated
@@ -21,13 +21,15 @@
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"axios": "^1.9.0",
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^25.2.1",
|
||||
"lucide-react": "^0.516.0",
|
||||
"next": "15.3.3",
|
||||
"next-i18next": "^15.4.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.5.3"
|
||||
"react-i18next": "^15.5.3",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
@@ -1954,6 +1956,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deepmerge": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-2.2.1.tgz",
|
||||
"integrity": "sha512-R9hc1Xa/NOBi9WRVUWg19rl1UB7Tt4kuPd+thNJgFZoxXsTz7ncaPaeIm+40oSGuP33DfMb4sZt1QIGiJzC4EA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
@@ -2134,6 +2145,31 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formik": {
|
||||
"version": "2.4.6",
|
||||
"resolved": "https://registry.npmjs.org/formik/-/formik-2.4.6.tgz",
|
||||
"integrity": "sha512-A+2EI7U7aG296q2TLGvNapDNTZp1khVt5Vk0Q/fyfSROss0V/V6+txt2aJnwEos44IxTCW/LYAi/zgWzlevj+g==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://opencollective.com/formik"
|
||||
}
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@types/hoist-non-react-statics": "^3.3.1",
|
||||
"deepmerge": "^2.1.1",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash-es": "^4.17.21",
|
||||
"react-fast-compare": "^2.0.1",
|
||||
"tiny-warning": "^1.0.2",
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -2634,6 +2670,18 @@
|
||||
"integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.21",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
|
||||
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -2991,6 +3039,12 @@
|
||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/property-expr": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/property-expr/-/property-expr-2.0.6.tgz",
|
||||
"integrity": "sha512-SVtmxhRE/CGkn3eZY1T6pC8Nln6Fr/lu1mKSgRud0eC73whjGfoAogbn78LkD8aFL0zz3bAFerKSnOl7NlErBA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
@@ -3018,6 +3072,12 @@
|
||||
"react": "^19.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-fast-compare": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-i18next": {
|
||||
"version": "15.5.3",
|
||||
"resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.3.tgz",
|
||||
@@ -3287,12 +3347,42 @@
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/tiny-case": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-case/-/tiny-case-1.0.3.tgz",
|
||||
"integrity": "sha512-Eet/eeMhkO6TX8mnUteS9zgPbUMQa4I6Kkp5ORiBD5476/m+PIRiumP5tmh5ioJpH7k51Kehawy2UDfsnxxY8Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tiny-warning": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz",
|
||||
"integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/toposort": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz",
|
||||
"integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "2.19.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz",
|
||||
"integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"engines": {
|
||||
"node": ">=12.20"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz",
|
||||
@@ -3329,6 +3419,18 @@
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/yup": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/yup/-/yup-1.6.1.tgz",
|
||||
"integrity": "sha512-JED8pB50qbA4FOkDol0bYF/p60qSEDQqBD0/qeIrUCG1KbPBIQ776fCUNb9ldbPcSTxA69g/47XTo4TqWiuXOA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"property-expr": "^2.0.5",
|
||||
"tiny-case": "^1.0.3",
|
||||
"toposort": "^2.0.2",
|
||||
"type-fest": "^2.19.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@
|
||||
"@stripe/react-stripe-js": "^3.7.0",
|
||||
"@stripe/stripe-js": "^7.3.1",
|
||||
"axios": "^1.9.0",
|
||||
"formik": "^2.4.6",
|
||||
"i18next": "^25.2.1",
|
||||
"lucide-react": "^0.516.0",
|
||||
"next": "15.3.3",
|
||||
"next-i18next": "^15.4.2",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-i18next": "^15.5.3"
|
||||
"react-i18next": "^15.5.3",
|
||||
"yup": "^1.6.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
|
||||
376
webui/src/app/form/page.js
Normal file
376
webui/src/app/form/page.js
Normal file
@@ -0,0 +1,376 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
CardContent,
|
||||
TextField,
|
||||
Button,
|
||||
Typography,
|
||||
Grid,
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
FormHelperText,
|
||||
Chip,
|
||||
Avatar,
|
||||
Container,
|
||||
} from '@mui/material';
|
||||
import { useFormik } from 'formik';
|
||||
import * as yup from 'yup';
|
||||
import PersonIcon from '@mui/icons-material/Person';
|
||||
import EmailIcon from '@mui/icons-material/Email';
|
||||
import PhoneIcon from '@mui/icons-material/Phone';
|
||||
import WorkIcon from '@mui/icons-material/Work';
|
||||
import SendIcon from '@mui/icons-material/Send';
|
||||
|
||||
const validationSchema = yup.object({
|
||||
firstName: yup
|
||||
.string('Enter your first name')
|
||||
.min(2, 'First name should be at least 2 characters')
|
||||
.required('First name is required'),
|
||||
lastName: yup
|
||||
.string('Enter your last name')
|
||||
.min(2, 'Last name should be at least 2 characters')
|
||||
.required('Last name is required'),
|
||||
email: yup
|
||||
.string('Enter your email')
|
||||
.email('Enter a valid email')
|
||||
.required('Email is required'),
|
||||
phone: yup
|
||||
.string('Enter your phone number')
|
||||
.matches(/^[\+]?[1-9][\d]{0,15}$/, 'Enter a valid phone number')
|
||||
.required('Phone number is required'),
|
||||
age: yup
|
||||
.number('Enter your age')
|
||||
.min(18, 'Must be at least 18 years old')
|
||||
.max(120, 'Must be less than 120 years old')
|
||||
.required('Age is required'),
|
||||
department: yup
|
||||
.string('Select a department')
|
||||
.required('Department is required'),
|
||||
skills: yup
|
||||
.array()
|
||||
.min(1, 'Select at least one skill')
|
||||
.required('Skills are required'),
|
||||
bio: yup
|
||||
.string('Enter your bio')
|
||||
.min(10, 'Bio should be at least 10 characters')
|
||||
.max(500, 'Bio should not exceed 500 characters')
|
||||
.required('Bio is required'),
|
||||
});
|
||||
|
||||
const departments = [
|
||||
'Engineering',
|
||||
'Marketing',
|
||||
'Sales',
|
||||
'HR',
|
||||
'Finance',
|
||||
'Operations',
|
||||
'Design',
|
||||
];
|
||||
|
||||
const skillOptions = [
|
||||
'JavaScript',
|
||||
'React',
|
||||
'Node.js',
|
||||
'Python',
|
||||
'UI/UX Design',
|
||||
'Project Management',
|
||||
'Data Analysis',
|
||||
'Marketing',
|
||||
'Sales',
|
||||
'Communication',
|
||||
];
|
||||
|
||||
export default function FormPage() {
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
phone: '',
|
||||
age: '',
|
||||
department: '',
|
||||
skills: [],
|
||||
bio: '',
|
||||
},
|
||||
validationSchema: validationSchema,
|
||||
onSubmit: (values, { setSubmitting, resetForm }) => {
|
||||
setTimeout(() => {
|
||||
console.log('Form submitted:', values);
|
||||
alert('Form submitted successfully!');
|
||||
setSubmitting(false);
|
||||
resetForm();
|
||||
}, 1000);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSkillChange = (event) => {
|
||||
const value = event.target.value;
|
||||
formik.setFieldValue('skills', typeof value === 'string' ? value.split(',') : value);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: '100vh',
|
||||
background: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)'
|
||||
: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
||||
py: 4,
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="sm">
|
||||
<Card
|
||||
elevation={8}
|
||||
sx={{
|
||||
borderRadius: 3,
|
||||
background: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'rgba(26, 26, 46, 0.95)'
|
||||
: 'rgba(255, 255, 255, 0.95)',
|
||||
backdropFilter: 'blur(10px)',
|
||||
border: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? '1px solid rgba(99, 102, 241, 0.2)'
|
||||
: '1px solid rgba(255, 255, 255, 0.3)',
|
||||
}}
|
||||
>
|
||||
<CardContent sx={{ p: 4 }}>
|
||||
<Box
|
||||
display="flex"
|
||||
flexDirection="column"
|
||||
alignItems="center"
|
||||
mb={4}
|
||||
>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: 'primary.main',
|
||||
width: 56,
|
||||
height: 56,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<PersonIcon fontSize="large" />
|
||||
</Avatar>
|
||||
<Typography
|
||||
variant="h4"
|
||||
component="h1"
|
||||
fontWeight="bold"
|
||||
color="primary"
|
||||
textAlign="center"
|
||||
>
|
||||
User Registration
|
||||
</Typography>
|
||||
<Typography variant="subtitle1" color="text.secondary" textAlign="center">
|
||||
Please fill out all required fields
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
<form onSubmit={formik.handleSubmit}>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3 }}>
|
||||
<Typography variant="h6" color="primary" sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
||||
<PersonIcon /> Personal Information
|
||||
</Typography>
|
||||
|
||||
<Grid container spacing={2}>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="firstName"
|
||||
name="firstName"
|
||||
label="First Name"
|
||||
value={formik.values.firstName}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.firstName && Boolean(formik.errors.firstName)}
|
||||
helperText={formik.touched.firstName && formik.errors.firstName}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item xs={6}>
|
||||
<TextField
|
||||
fullWidth
|
||||
id="lastName"
|
||||
name="lastName"
|
||||
label="Last Name"
|
||||
value={formik.values.lastName}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.lastName && Boolean(formik.errors.lastName)}
|
||||
helperText={formik.touched.lastName && formik.errors.lastName}
|
||||
variant="outlined"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="email"
|
||||
name="email"
|
||||
label="Email Address"
|
||||
type="email"
|
||||
value={formik.values.email}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.email && Boolean(formik.errors.email)}
|
||||
helperText={formik.touched.email && formik.errors.email}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: <EmailIcon sx={{ color: 'action.active', mr: 1 }} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="phone"
|
||||
name="phone"
|
||||
label="Phone Number"
|
||||
value={formik.values.phone}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.phone && Boolean(formik.errors.phone)}
|
||||
helperText={formik.touched.phone && formik.errors.phone}
|
||||
variant="outlined"
|
||||
InputProps={{
|
||||
startAdornment: <PhoneIcon sx={{ color: 'action.active', mr: 1 }} />,
|
||||
}}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="age"
|
||||
name="age"
|
||||
label="Age"
|
||||
type="number"
|
||||
value={formik.values.age}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.age && Boolean(formik.errors.age)}
|
||||
helperText={formik.touched.age && formik.errors.age}
|
||||
variant="outlined"
|
||||
/>
|
||||
|
||||
<Typography variant="h6" color="primary" sx={{ display: 'flex', alignItems: 'center', gap: 1, mt: 2 }}>
|
||||
<WorkIcon /> Professional Information
|
||||
</Typography>
|
||||
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={formik.touched.department && Boolean(formik.errors.department)}
|
||||
>
|
||||
<InputLabel id="department-label">Department</InputLabel>
|
||||
<Select
|
||||
labelId="department-label"
|
||||
id="department"
|
||||
name="department"
|
||||
value={formik.values.department}
|
||||
label="Department"
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
>
|
||||
{departments.map((dept) => (
|
||||
<MenuItem key={dept} value={dept}>
|
||||
{dept}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{formik.touched.department && formik.errors.department && (
|
||||
<FormHelperText>{formik.errors.department}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
fullWidth
|
||||
error={formik.touched.skills && Boolean(formik.errors.skills)}
|
||||
>
|
||||
<InputLabel id="skills-label">Skills</InputLabel>
|
||||
<Select
|
||||
labelId="skills-label"
|
||||
id="skills"
|
||||
multiple
|
||||
value={formik.values.skills}
|
||||
onChange={handleSkillChange}
|
||||
onBlur={formik.handleBlur}
|
||||
label="Skills"
|
||||
renderValue={(selected) => (
|
||||
<Box sx={{ display: 'flex', flexWrap: 'wrap', gap: 0.5 }}>
|
||||
{selected.map((value) => (
|
||||
<Chip key={value} label={value} size="small" />
|
||||
))}
|
||||
</Box>
|
||||
)}
|
||||
>
|
||||
{skillOptions.map((skill) => (
|
||||
<MenuItem key={skill} value={skill}>
|
||||
{skill}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
{formik.touched.skills && formik.errors.skills && (
|
||||
<FormHelperText>{formik.errors.skills}</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
id="bio"
|
||||
name="bio"
|
||||
label="Bio"
|
||||
multiline
|
||||
rows={4}
|
||||
value={formik.values.bio}
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
error={formik.touched.bio && Boolean(formik.errors.bio)}
|
||||
helperText={
|
||||
formik.touched.bio && formik.errors.bio
|
||||
? formik.errors.bio
|
||||
: `${formik.values.bio.length}/500 characters`
|
||||
}
|
||||
variant="outlined"
|
||||
placeholder="Tell us about yourself..."
|
||||
/>
|
||||
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
fullWidth
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={formik.isSubmitting}
|
||||
startIcon={<SendIcon />}
|
||||
sx={{
|
||||
mt: 2,
|
||||
py: 1.5,
|
||||
borderRadius: 3,
|
||||
background: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'linear-gradient(45deg, #6366f1 30%, #8b5cf6 90%)'
|
||||
: 'linear-gradient(45deg, #FE6B8B 30%, #FF8E53 90%)',
|
||||
boxShadow: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? '0 3px 5px 2px rgba(99, 102, 241, .3)'
|
||||
: '0 3px 5px 2px rgba(255, 105, 135, .3)',
|
||||
'&:hover': {
|
||||
background: (theme) =>
|
||||
theme.palette.mode === 'dark'
|
||||
? 'linear-gradient(45deg, #5b21b6 60%, #7c3aed 100%)'
|
||||
: 'linear-gradient(45deg, #FE6B8B 60%, #FF8E53 100%)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{formik.isSubmitting ? 'Submitting...' : 'Submit Registration'}
|
||||
</Button>
|
||||
</Box>
|
||||
</form>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Container>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user