From 171c117ccdce507ae23f4ed7a7e25c0488b7ba64 Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:05:19 +0300 Subject: [PATCH 1/3] Add 4 integration tests --- .../Imprink.Integration.Tests.csproj | 23 ++ docker-compose.yml | 1 + .../Categories/CreateCategoryHandler.cs | 1 + .../CreateCategoryHandlerIntegrationTest.cs | 237 ++++++++++++++++++ .../Imprink.Application.Tests.csproj | 15 +- 5 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 Imprink.Integration.Tests/Imprink.Integration.Tests.csproj create mode 100644 tests/Imprink.Application.Tests/CreateCategoryHandlerIntegrationTest.cs diff --git a/Imprink.Integration.Tests/Imprink.Integration.Tests.csproj b/Imprink.Integration.Tests/Imprink.Integration.Tests.csproj new file mode 100644 index 0000000..b2f8cd9 --- /dev/null +++ b/Imprink.Integration.Tests/Imprink.Integration.Tests.csproj @@ -0,0 +1,23 @@ + + + + net9.0 + enable + enable + + + + + + + + + + + + + + + + + diff --git a/docker-compose.yml b/docker-compose.yml index 07c5680..3f2ef07 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/src/Imprink.Application/Commands/Categories/CreateCategoryHandler.cs b/src/Imprink.Application/Commands/Categories/CreateCategoryHandler.cs index 0d088ca..310578a 100644 --- a/src/Imprink.Application/Commands/Categories/CreateCategoryHandler.cs +++ b/src/Imprink.Application/Commands/Categories/CreateCategoryHandler.cs @@ -33,6 +33,7 @@ public class CreateCategoryHandler(IUnitOfWork unitOfWork) : IRequestHandler(options => + options.UseSqlite(_connection)); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + + _serviceProvider = services.BuildServiceProvider(); + _context = _serviceProvider.GetRequiredService(); + _handler = _serviceProvider.GetRequiredService(); + + _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(); + 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(); + + 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()?.Dispose(); + } +} \ No newline at end of file diff --git a/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj b/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj index 643561b..12b9750 100644 --- a/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj +++ b/tests/Imprink.Application.Tests/Imprink.Application.Tests.csproj @@ -8,14 +8,18 @@ - - - - + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all @@ -28,6 +32,7 @@ + From 50adfd8d6b95339e2aaf6c3a40800ff53eb3690f Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:09:42 +0300 Subject: [PATCH 2/3] Add 4 unit tests --- .../UpdateCategoryHandlerUnitTest.cs | 244 ++++++++++++++++++ 1 file changed, 244 insertions(+) create mode 100644 tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs diff --git a/tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs b/tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs new file mode 100644 index 0000000..3336fe8 --- /dev/null +++ b/tests/Imprink.Application.Tests/UpdateCategoryHandlerUnitTest.cs @@ -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 _unitOfWorkMock; + private readonly Mock _categoryRepositoryMock; + private readonly UpdateCategoryHandler _handler; + + public UpdateCategoryHandlerTests() + { + _unitOfWorkMock = new Mock(); + _categoryRepositoryMock = new Mock(); + + _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())) + .ReturnsAsync(existingCategory); + + _categoryRepositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .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()), Times.Once); + _unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny()), Times.Never); + + _categoryRepositoryMock.Verify(x => x.GetByIdAsync(categoryId, It.IsAny()), Times.Once); + _categoryRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), 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())) + .ReturnsAsync((Category?)null); + + var exception = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + + Assert.Equal($"Category with ID {categoryId} not found.", exception.Message); + + _unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny()), Times.Never); + + _categoryRepositoryMock.Verify(x => x.UpdateAsync(It.IsAny(), It.IsAny()), 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())) + .ReturnsAsync(existingCategory); + + _categoryRepositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .ThrowsAsync(expectedException); + + var thrownException = await Assert.ThrowsAsync( + () => _handler.Handle(command, CancellationToken.None)); + + Assert.Same(expectedException, thrownException); + + _unitOfWorkMock.Verify(x => x.BeginTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny()), 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())) + .ReturnsAsync(existingCategory); + + _categoryRepositoryMock + .Setup(x => x.UpdateAsync(It.IsAny(), It.IsAny())) + .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()), Times.Once); + _unitOfWorkMock.Verify(x => x.CommitTransactionAsync(It.IsAny()), Times.Once); + _unitOfWorkMock.Verify(x => x.RollbackTransactionAsync(It.IsAny()), Times.Never); + } +} \ No newline at end of file From 850e8ae852fa68692cf97ec26e1a26e5f7fc011c Mon Sep 17 00:00:00 2001 From: lumijiez <59575049+lumijiez@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:31:24 +0300 Subject: [PATCH 3/3] Add form --- webui/package-lock.json | 104 +++++++++- webui/package.json | 4 +- webui/src/app/form/page.js | 376 +++++++++++++++++++++++++++++++++++++ 3 files changed, 482 insertions(+), 2 deletions(-) create mode 100644 webui/src/app/form/page.js diff --git a/webui/package-lock.json b/webui/package-lock.json index 2270b2a..5956179 100644 --- a/webui/package-lock.json +++ b/webui/package-lock.json @@ -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" + } } } } diff --git a/webui/package.json b/webui/package.json index d361ade..6159669 100644 --- a/webui/package.json +++ b/webui/package.json @@ -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", diff --git a/webui/src/app/form/page.js b/webui/src/app/form/page.js new file mode 100644 index 0000000..b87a321 --- /dev/null +++ b/webui/src/app/form/page.js @@ -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 ( + + theme.palette.mode === 'dark' + ? 'linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%)' + : 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)', + py: 4, + }} + > + + + 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)', + }} + > + + + + + + + User Registration + + + Please fill out all required fields + + + +
+ + + Personal Information + + + + + + + + + + + + , + }} + /> + + , + }} + /> + + + + + Professional Information + + + + Department + + {formik.touched.department && formik.errors.department && ( + {formik.errors.department} + )} + + + + Skills + + {formik.touched.skills && formik.errors.skills && ( + {formik.errors.skills} + )} + + + + + + +
+
+
+
+
+ ); +} \ No newline at end of file