diff --git a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs index ea2ac6c..cfa204a 100644 --- a/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs +++ b/Surge365.MassEmailReact.API/Controllers/AuthenticationController.cs @@ -22,10 +22,11 @@ namespace Surge365.MassEmailReact.Server.Controllers var authResponse = await _authService.Authenticate(request.Username, request.Password); if (!authResponse.authenticated) return Unauthorized(new { message = authResponse.errorMessage }); - else if(authResponse.token == null) - return Unauthorized(new { message = "Invalid credentials" }); + else if(authResponse.data == null) + return Unauthorized(new { message = "Invalid credentials" }); - return Ok(new { success = true, authResponse.token.Value.accessToken }); + //TODO: Store user in session + return Ok(new { success = true, authResponse.data.Value.accessToken, authResponse.data.Value.user }); //TODO: Send refresh token in http only cookie. } [HttpPost("refreshtoken")] public IActionResult RefreshToken([FromBody] RefreshTokenRequest request) diff --git a/Surge365.MassEmailReact.API/Controllers/TargetsController.cs b/Surge365.MassEmailReact.API/Controllers/TargetsController.cs new file mode 100644 index 0000000..d5d2061 --- /dev/null +++ b/Surge365.MassEmailReact.API/Controllers/TargetsController.cs @@ -0,0 +1,54 @@ +using Microsoft.AspNetCore.DataProtection.KeyManagement; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Surge365.MassEmailReact.Application.DTOs; +using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Server.Controllers +{ + [Route("api/[controller]")] + [ApiController] + public class TargetsController : ControllerBase + { + private readonly ITargetService _targetService; + + public TargetsController(ITargetService targetService) + { + _targetService = targetService; + } + + + [HttpGet("GetAll")] + public async Task GetAll([FromQuery] bool? activeOnly) + { + var targets = await _targetService.GetAllAsync(activeOnly == null || activeOnly.Value ? true : false); + return Ok(targets); + } + + [HttpGet("{key}")] + public async Task GetByKey(int id) + { + var target = await _targetService.GetByIdAsync(id); + return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found."); + } + [HttpPut("{id}")] + public async Task UpdateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto) + { + if (id != targetUpdateDto.Id) + return BadRequest("ID in URL does not match ID in request body"); + + var existingTarget = await _targetService.GetByIdAsync(id); + if (existingTarget == null) + return NotFound($"Target with ID {id} not found"); + + var success = await _targetService.UpdateAsync(targetUpdateDto); + if (!success) + return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update target."); + + var updatedTarget = await _targetService.GetByIdAsync(id); + + return Ok(updatedTarget); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.API/Program.cs b/Surge365.MassEmailReact.API/Program.cs index bfec0e0..5f9df2c 100644 --- a/Surge365.MassEmailReact.API/Program.cs +++ b/Surge365.MassEmailReact.API/Program.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Identity; using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Infrastructure.DapperMaps; using Surge365.MassEmailReact.Infrastructure.Repositories; using Surge365.MassEmailReact.Infrastructure.Services; @@ -12,6 +14,8 @@ builder.Services.AddControllers(); builder.Services.AddOpenApi(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); var app = builder.Build(); app.UseDefaultFiles(); @@ -31,4 +35,6 @@ app.MapControllers(); app.MapFallbackToFile("/index.html"); +DapperConfiguration.ConfigureMappings(); + app.Run(); diff --git a/Surge365.MassEmailReact.API/appsettings.json b/Surge365.MassEmailReact.API/appsettings.json index c0fe1b5..ca15a90 100644 --- a/Surge365.MassEmailReact.API/appsettings.json +++ b/Surge365.MassEmailReact.API/appsettings.json @@ -12,6 +12,7 @@ "AppCode": "MassEmailReactApi", "EnvironmentCode": "UAT", "ConnectionStrings": { - "Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Application Name=##application_name##" + "Marketing.ConnectionString": "data source=uat.surge365.com;initial catalog=Marketing;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;", //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT + "MassEmail.ConnectionString": "data source=uat.surge365.com;initial catalog=MassEmail;User ID=ytb;Password=YTB()nl!n3;Encrypt=yes;TrustServerCertificate=yes;Connection Timeout=3;Application Name=##application_name##;" //TODO: Move this to development.json, on server should go somewhere secure. GET IT OUT OF GIT } } diff --git a/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs b/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs new file mode 100644 index 0000000..5839a6e --- /dev/null +++ b/Surge365.MassEmailReact.Application/DTOs/TargetUpdateDto.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class TargetUpdateDto + { + public int? Id { get; set; } + public int ServerId { get; set; } + public string Name { get; set; } = ""; + public string DatabaseName { get; set; } = ""; + public string ViewName { get; set; } = ""; + public string FilterQuery { get; set; } = ""; + public bool AllowWriteBack { get; set; } = false; + public bool IsActive { get; set; } = true; + } +} diff --git a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs index a47f7d0..1ef1557 100644 --- a/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs +++ b/Surge365.MassEmailReact.Application/Interfaces/IAuthService.cs @@ -1,8 +1,10 @@ -namespace Surge365.MassEmailReact.Application.Interfaces +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Application.Interfaces { public interface IAuthService { - Task<(bool authenticated, (string accessToken, string refreshToken)? token, string errorMessage)> Authenticate(string username, string password); + Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password); (string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken); } } diff --git a/Surge365.MassEmailReact.Application/Interfaces/ITargetRepository.cs b/Surge365.MassEmailReact.Application/Interfaces/ITargetRepository.cs new file mode 100644 index 0000000..746e317 --- /dev/null +++ b/Surge365.MassEmailReact.Application/Interfaces/ITargetRepository.cs @@ -0,0 +1,16 @@ +using Surge365.MassEmailReact.Domain.Entities; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Application.Interfaces +{ + public interface ITargetRepository + { + Task GetByIdAsync(int id); + Task> GetAllAsync(bool activeOnly = true); + Task UpdateAsync(Target target); + } +} diff --git a/Surge365.MassEmailReact.Application/Interfaces/ITargetService.cs b/Surge365.MassEmailReact.Application/Interfaces/ITargetService.cs new file mode 100644 index 0000000..2ba7c62 --- /dev/null +++ b/Surge365.MassEmailReact.Application/Interfaces/ITargetService.cs @@ -0,0 +1,11 @@ +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Application.Interfaces +{ + public interface ITargetService + { + Task GetByIdAsync(int id); + Task> GetAllAsync(bool activeOnly = true); + Task UpdateAsync(TargetUpdateDto targetDto); + } +} diff --git a/Surge365.MassEmailReact.Domain/Entities/Target.cs b/Surge365.MassEmailReact.Domain/Entities/Target.cs new file mode 100644 index 0000000..7780d19 --- /dev/null +++ b/Surge365.MassEmailReact.Domain/Entities/Target.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Domain.Entities +{ + public class Target + { + public int? Id { get; private set; } + public int ServerId { get; set; } + public string Name { get; set; } + public string DatabaseName { get; set; } + public string ViewName { get; set; } + public string FilterQuery { get; set; } + public bool AllowWriteBack { get; set; } + public bool IsActive { get; set; } + + public Target() { } + private Target(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive) + { + Id = id; + ServerId = ServerId; + Name = name; + DatabaseName = databaseName; + ViewName = viewName; + FilterQuery = filterQuery; + AllowWriteBack = allowWriteBack; + IsActive = isActive; + } + public static Target Create(int id, int serverId, string name, string databaseName, string viewName, string filterQuery, bool allowWriteBack, bool isActive) + { + return new Target(id, serverId, name, databaseName, viewName, filterQuery, allowWriteBack, isActive); + } + } +} diff --git a/Surge365.MassEmailReact.Domain/Entities/User.cs b/Surge365.MassEmailReact.Domain/Entities/User.cs index e5dc397..7e7b7fe 100644 --- a/Surge365.MassEmailReact.Domain/Entities/User.cs +++ b/Surge365.MassEmailReact.Domain/Entities/User.cs @@ -21,7 +21,7 @@ namespace Surge365.MassEmailReact.Domain.Entities UserKey = userKey; UserId = userId; Username = username; - Username = firstName ?? ""; + FirstName = firstName ?? ""; MiddleInitial = middleInitial ?? ""; LastName = lastName ?? ""; IsActive = isActive; diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs new file mode 100644 index 0000000..30efc4e --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/DapperConfiguration.cs @@ -0,0 +1,20 @@ +using Dapper.FluentMap; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Infrastructure.DapperMaps +{ + public class DapperConfiguration + { + public static void ConfigureMappings() + { + FluentMapper.Initialize(config => + { + config.AddMap(new TargetMap()); + }); + } + } +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Infrastructure/DapperMaps/TargetMap.cs b/Surge365.MassEmailReact.Infrastructure/DapperMaps/TargetMap.cs new file mode 100644 index 0000000..ccd36bb --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/DapperMaps/TargetMap.cs @@ -0,0 +1,25 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dapper.FluentMap.Mapping; +using Surge365.MassEmailReact.Domain.Entities; + +namespace Surge365.MassEmailReact.Infrastructure.DapperMaps +{ + public class TargetMap : EntityMap + { + public TargetMap() + { + Map(t => t.Id).ToColumn("target_key"); + Map(t => t.ServerId).ToColumn("server_key"); + Map(t => t.Name).ToColumn("name"); + Map(t => t.DatabaseName).ToColumn("database_name"); + Map(t => t.ViewName).ToColumn("view_name"); + Map(t => t.FilterQuery).ToColumn("filter_query"); + Map(t => t.AllowWriteBack).ToColumn("allow_write_back"); + Map(t => t.IsActive).ToColumn("is_active"); + } + } +} diff --git a/Surge365.MassEmailReact.Infrastructure/DataAccess.cs b/Surge365.MassEmailReact.Infrastructure/DataAccess.cs index 19f89e7..f79b5ae 100644 --- a/Surge365.MassEmailReact.Infrastructure/DataAccess.cs +++ b/Surge365.MassEmailReact.Infrastructure/DataAccess.cs @@ -233,8 +233,37 @@ namespace Surge365.MassEmailReact.Infrastructure #region Async internal async Task OpenConnectionAsync() { - _connection = new SqlConnection(_connectionString); - await _connection.OpenAsync(); + //_connection = new SqlConnection(_connectionString); + //await _connection.OpenAsync(); + await Task.Run(() => OpenConnectionWithRetry()); + } + internal void OpenConnectionWithRetry(short maxRetries = 3, int totalTimeoutMs = 1000) + { + int attempt = 0; + while (attempt < maxRetries) + { + using (var cts = new CancellationTokenSource(totalTimeoutMs)) // Total cap, e.g., 3 seconds + { + try + { + Console.WriteLine($"Attempt {attempt + 1}..."); + _connection = new SqlConnection(_connectionString); + _connection.OpenAsync(cts.Token).Wait(); // Use async with cancellation + return; + } + catch (Exception ex) + { + attempt++; + if (attempt == maxRetries) + { + Console.WriteLine($"Failed after {attempt} attempts: {ex.Message}"); + throw; + } + Console.WriteLine($"Retrying after failure: {ex.Message}"); + Thread.Sleep(1000); // Delay between retries + } + } + } } internal async Task CloseConnectionAsync() { diff --git a/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs b/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs new file mode 100644 index 0000000..79bc2d3 --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/Repositories/TargetRepository.cs @@ -0,0 +1,92 @@ +using Dapper; +using Dapper.FluentMap; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; +using Surge365.MassEmailReact.Application.Interfaces; +using Surge365.MassEmailReact.Domain.Entities; +using Surge365.MassEmailReact.Domain.Enums; +using Surge365.MassEmailReact.Domain.Enums.Extensions; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Surge365.MassEmailReact.Infrastructure.Repositories +{ + public class TargetRepository : ITargetRepository + { + private IConfiguration _config; + private const string _connectionStringName = "MassEmail.ConnectionString"; + private string? ConnectionString + { + get + { + return _config.GetConnectionString(_connectionStringName); + } + } + public TargetRepository(IConfiguration config) + { + _config = config; +#if DEBUG + if (!FluentMapper.EntityMaps.ContainsKey(typeof(Target))) + { + throw new InvalidOperationException("Target dapper mapping is missing. Make sure ConfigureMappings() is called inside program.cs (program startup)."); + } +#endif + } + public async Task GetByIdAsync(int targetKey) + { + ArgumentNullException.ThrowIfNull(_config); + ArgumentNullException.ThrowIfNull(_connectionStringName); + + using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); + + return (await conn.QueryAsync("SELECT * FROM mem_target WHERE target_key = @target_key", new { target_key = targetKey })).ToList().FirstOrDefault(); + } + public async Task> GetAllAsync(bool activeOnly = true) + { + ArgumentNullException.ThrowIfNull(_config); + ArgumentNullException.ThrowIfNull(_connectionStringName); + + using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); + + return (await conn.QueryAsync("mem_get_target_all", new { active_only = activeOnly }, commandType: CommandType.StoredProcedure)).ToList(); + //return conn.Query("SELECT * FROM mem_target WHERE is_active = @active_only", new { active_only = activeOnly }).ToList(); + } + + public async Task UpdateAsync(Target target) + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(target.Id); + using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName)); + + var parameters = new DynamicParameters(); + parameters.Add("@target_key", target.Id, DbType.Int32); + parameters.Add("@server_key", target.ServerId, DbType.Int32); + parameters.Add("@name", target.Name, DbType.String); + parameters.Add("@database_name", target.DatabaseName, DbType.String); + parameters.Add("@view_name", target.ViewName, DbType.String); + parameters.Add("@filter_query", target.FilterQuery, DbType.String); + parameters.Add("@allow_write_back", target.AllowWriteBack, DbType.Boolean); + parameters.Add("@is_active", target.IsActive, DbType.Boolean); + + // Output parameter + parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output); + + await conn.ExecuteAsync("mem_save_target", parameters, commandType: CommandType.StoredProcedure); + + // Retrieve the output parameter value + bool success = parameters.Get("@success"); + + return success; + } + + //public void Add(Target target) + //{ + // Targets.Add(target); + //} + + } +} diff --git a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs index 605665c..2f01f6e 100644 --- a/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs +++ b/Surge365.MassEmailReact.Infrastructure/Services/AuthService.cs @@ -26,7 +26,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services _config = config; } - public async Task<(bool authenticated, (string accessToken, string refreshToken)? token, string errorMessage)> Authenticate(string username, string password) + public async Task<(bool authenticated, (User user, string accessToken, string refreshToken)? data, string errorMessage)> Authenticate(string username, string password) { var authResponse = await _userRepository.Authenticate(username, password); if (authResponse.user == null) @@ -56,7 +56,7 @@ namespace Surge365.MassEmailReact.Infrastructure.Services var refreshToken = Convert.ToBase64String(Guid.NewGuid().ToByteArray()); //_userRepository.SaveRefreshToken(userId.Value, refreshToken); // TODO: Store refresh token in DB - return (true, (accessToken, refreshToken), ""); + return (true, (authResponse.user, accessToken, refreshToken), ""); } public (string accessToken, string refreshToken)? GenerateTokens(Guid userId, string refreshToken) { diff --git a/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs b/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs new file mode 100644 index 0000000..4d8671e --- /dev/null +++ b/Surge365.MassEmailReact.Infrastructure/Services/TargetService.cs @@ -0,0 +1,55 @@ +using Surge365.MassEmailReact.Application.Interfaces; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Security.Claims; +using System.Text; +using System.Threading.Tasks; +using System.IdentityModel.Tokens.Jwt; +using Microsoft.IdentityModel.Tokens; +using Microsoft.Extensions.Configuration; +using Surge365.MassEmailReact.Domain.Entities; +using System.Security.Cryptography; + + +namespace Surge365.MassEmailReact.Infrastructure.Services +{ + public class TargetService : ITargetService + { + private readonly ITargetRepository _targetRepository; + private readonly IConfiguration _config; + + public TargetService(ITargetRepository targetRepository, IConfiguration config) + { + _targetRepository = targetRepository; + _config = config; + } + + public async Task GetByIdAsync(int id) + { + return await _targetRepository.GetByIdAsync(id); + } + public async Task> GetAllAsync(bool activeOnly = true) + { + return await _targetRepository.GetAllAsync(activeOnly); + } + public async Task UpdateAsync(TargetUpdateDto targetDto) + { + ArgumentNullException.ThrowIfNull(targetDto, nameof(targetDto)); + ArgumentNullException.ThrowIfNull(targetDto.Id, nameof(targetDto.Id)); + + var target = await _targetRepository.GetByIdAsync(targetDto.Id.Value); + if (target == null || target.Id == null) return false; + + target.ServerId = targetDto.ServerId; + target.Name = targetDto.Name; + target.DatabaseName = targetDto.DatabaseName; + target.ViewName = targetDto.ViewName; + target.FilterQuery = targetDto.FilterQuery; + target.AllowWriteBack = targetDto.AllowWriteBack; + target.IsActive = targetDto.IsActive; + + return await _targetRepository.UpdateAsync(target); + } + } +} diff --git a/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj b/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj index 9499a02..2dc1328 100644 --- a/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj +++ b/Surge365.MassEmailReact.Infrastructure/Surge365.MassEmailReact.Infrastructure.csproj @@ -12,6 +12,7 @@ + diff --git a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj index ec8ac4c..d4db629 100644 --- a/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj +++ b/Surge365.MassEmailReact.Web/Surge365.MassEmailReact.Web.esproj @@ -16,4 +16,7 @@ + + + \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/index.html b/Surge365.MassEmailReact.Web/index.html index f93a2b1..6c8070d 100644 --- a/Surge365.MassEmailReact.Web/index.html +++ b/Surge365.MassEmailReact.Web/index.html @@ -8,6 +8,6 @@
- + diff --git a/Surge365.MassEmailReact.Web/package-lock.json b/Surge365.MassEmailReact.Web/package-lock.json index 83be51e..74a1df1 100644 --- a/Surge365.MassEmailReact.Web/package-lock.json +++ b/Surge365.MassEmailReact.Web/package-lock.json @@ -8,8 +8,16 @@ "name": "surge365.massemailreact.client", "version": "0.0.0", "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fontsource/roboto": "^5.1.1", + "@mui/icons-material": "^6.4.5", + "@mui/material": "^6.4.5", + "@mui/x-charts": "^7.27.1", + "@mui/x-data-grid": "^7.27.1", "admin-lte": "4.0.0-beta3", "bootstrap": "^5.3.3", + "dayjs": "^1.11.13", "font-awesome": "^4.7.0", "ionicons": "^7.4.0", "react": "^19.0.0", @@ -51,7 +59,6 @@ "version": "7.26.2", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.25.9", @@ -107,7 +114,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.9.tgz", "integrity": "sha512-kEWdzjOAUMW4hAyrzJ0ZaTOu9OmpyDIQicIh0zg0EEcEkYXZb2TjtBhnHi2ViX7PKwZqF4xwqfAm299/QMP3lg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/parser": "^7.26.9", @@ -141,7 +147,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/traverse": "^7.25.9", @@ -183,7 +188,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -193,7 +197,6 @@ "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.9.0" @@ -227,7 +230,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.9.tgz", "integrity": "sha512-81NWa1njQblgZbQHxWHpxxCzNsa3ZwvFqpUg7P+NNUU6f3UU2jBEg4OlF/J6rl8+PQGh1q6/zWScd001YwcA5A==", - "dev": true, "license": "MIT", "dependencies": { "@babel/types": "^7.26.9" @@ -287,7 +289,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.26.9.tgz", "integrity": "sha512-qyRplbeIpNZhmzOysF/wFMuP9sctmh2cFzRAZOn1YapxBsE1i9bJIY586R/WBLfLcmcBlM8ROBiQURnnNy+zfA==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -302,7 +303,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.9.tgz", "integrity": "sha512-ZYW7L+pL8ahU5fXmNbPF+iZFHCv5scFak7MZ9bwaRPLUhHh7QQEMjZUg0HevihoqCM5iSYHN61EyCoZvqC+bxg==", - "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.26.2", @@ -321,7 +321,6 @@ "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -331,7 +330,6 @@ "version": "7.26.9", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.9.tgz", "integrity": "sha512-Y3IR1cRnOxOCDvMmNiym7XpXQ93iGDDPHx+Zj+NM+rg0fBaShfQLkg+hKPaZCEvg5N/LeCo4+Rj/i3FuJsIQaw==", - "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -341,6 +339,158 @@ "node": ">=6.9.0" } }, + "node_modules/@emotion/babel-plugin": { + "version": "11.13.5", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", + "integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/serialize": "^1.3.3", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/babel-plugin/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "license": "MIT" + }, + "node_modules/@emotion/cache": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz", + "integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0", + "@emotion/sheet": "^1.4.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "stylis": "4.2.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz", + "integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==", + "license": "MIT" + }, + "node_modules/@emotion/is-prop-valid": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", + "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "license": "MIT", + "dependencies": { + "@emotion/memoize": "^0.9.0" + } + }, + "node_modules/@emotion/memoize": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz", + "integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==", + "license": "MIT" + }, + "node_modules/@emotion/react": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", + "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/cache": "^11.14.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2", + "@emotion/weak-memoize": "^0.4.0", + "hoist-non-react-statics": "^3.3.1" + }, + "peerDependencies": { + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/serialize": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz", + "integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==", + "license": "MIT", + "dependencies": { + "@emotion/hash": "^0.9.2", + "@emotion/memoize": "^0.9.0", + "@emotion/unitless": "^0.10.0", + "@emotion/utils": "^1.4.2", + "csstype": "^3.0.2" + } + }, + "node_modules/@emotion/sheet": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz", + "integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==", + "license": "MIT" + }, + "node_modules/@emotion/styled": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", + "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "@emotion/babel-plugin": "^11.13.5", + "@emotion/is-prop-valid": "^1.3.0", + "@emotion/serialize": "^1.3.3", + "@emotion/use-insertion-effect-with-fallbacks": "^1.2.0", + "@emotion/utils": "^1.4.2" + }, + "peerDependencies": { + "@emotion/react": "^11.0.0-rc.0", + "react": ">=16.8.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@emotion/unitless": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz", + "integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==", + "license": "MIT" + }, + "node_modules/@emotion/use-insertion-effect-with-fallbacks": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz", + "integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==", + "license": "MIT", + "peerDependencies": { + "react": ">=16.8.0" + } + }, + "node_modules/@emotion/utils": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz", + "integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==", + "license": "MIT" + }, + "node_modules/@emotion/weak-memoize": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz", + "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", @@ -907,6 +1057,12 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@fontsource/roboto": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.1.1.tgz", + "integrity": "sha512-XwVVXtERDQIM7HPUIbyDe0FP4SRovpjF7zMI8M7pbqFp3ahLJsJTd18h+E6pkar6UbV3btbwkKjYARr5M+SQow==", + "license": "Apache-2.0" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -977,7 +1133,6 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -992,7 +1147,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1002,7 +1156,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, "license": "MIT", "engines": { "node": ">=6.0.0" @@ -1012,20 +1165,436 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", - "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", - "dev": true, "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@mui/core-downloads-tracker": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.5.tgz", + "integrity": "sha512-zoXvHU1YuoodgMlPS+epP084Pqv9V+Vg+5IGv9n/7IIFVQ2nkTngYHYxElCq8pdTTbDcgji+nNh0lxri2abWgA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + } + }, + "node_modules/@mui/icons-material": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.5.tgz", + "integrity": "sha512-4A//t8Nrc+4u4pbVhGarIFU98zpuB5AV9hTNzgXx1ySZJ1tWtx+i/1SbQ8PtGJxWeXlljhwimZJNPQ3x0CiIFw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@mui/material": "^6.4.5", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material": { + "version": "6.4.5", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.5.tgz", + "integrity": "sha512-5eyEgSXocIeV1JkXs8mYyJXU0aFyXZIWI5kq2g/mCnIgJe594lkOBNAKnCIaGVfQTu2T6TTEHF8/hHIqpiIRGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/core-downloads-tracker": "^6.4.5", + "@mui/system": "^6.4.3", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.3", + "@popperjs/core": "^2.11.8", + "@types/react-transition-group": "^4.4.12", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1", + "react-is": "^19.0.0", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@mui/material-pigment-css": "^6.4.3", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@mui/material-pigment-css": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/material/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", + "license": "MIT" + }, + "node_modules/@mui/private-theming": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-6.4.3.tgz", + "integrity": "sha512-7x9HaNwDCeoERc4BoEWLieuzKzXu5ZrhRnEM6AUcRXUScQLvF1NFkTlP59+IJfTbEMgcGg1wWHApyoqcksrBpQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/utils": "^6.4.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/styled-engine": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.3.tgz", + "integrity": "sha512-OC402VfK+ra2+f12Gef8maY7Y9n7B6CZcoQ9u7mIkh/7PKwW/xH81xwX+yW+Ak1zBT3HYcVjh2X82k5cKMFGoQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@emotion/cache": "^11.13.5", + "@emotion/serialize": "^1.3.3", + "@emotion/sheet": "^1.4.0", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.4.1", + "@emotion/styled": "^11.3.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/system": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.3.tgz", + "integrity": "sha512-Q0iDwnH3+xoxQ0pqVbt8hFdzhq1g2XzzR4Y5pVcICTNtoCLJmpJS3vI4y/OIM1FHFmpfmiEC2IRIq7YcZ8nsmg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/private-theming": "^6.4.3", + "@mui/styled-engine": "^6.4.3", + "@mui/types": "^7.2.21", + "@mui/utils": "^6.4.3", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.5.0", + "@emotion/styled": "^11.3.0", + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/types": { + "version": "7.2.21", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.21.tgz", + "integrity": "sha512-6HstngiUxNqLU+/DPqlUJDIPbzUBxIVHb1MmXP0eTWDIROiCR2viugXpEif0PPe2mLqqakPzzRClWAnK+8UJww==", + "license": "MIT", + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils": { + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-6.4.3.tgz", + "integrity": "sha512-jxHRHh3BqVXE9ABxDm+Tc3wlBooYz/4XPa0+4AI+iF38rV1/+btJmSUgG4shDtSWVs/I97aDn5jBCt6SF2Uq2A==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.26.0", + "@mui/types": "^7.2.21", + "@types/prop-types": "^15.7.14", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "react-is": "^19.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@mui/utils/node_modules/react-is": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.0.0.tgz", + "integrity": "sha512-H91OHcwjZsbq3ClIDHMzBShc1rotbfACdWENsmEf0IFvZ3FgGPtdHMcsv45bQ1hAbgdfiA8SnxTKfDS+x/8m2g==", + "license": "MIT" + }, + "node_modules/@mui/x-charts": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@mui/x-charts/-/x-charts-7.27.1.tgz", + "integrity": "sha512-9z7fopitKjazY+p+sI2Z0zpip5zq3GYBC0hDuzxFUMvH582/FX1ZP6g1Wub0oetQReIMciL+rqU4agmRucvanw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-charts-vendor": "7.20.0", + "@mui/x-internals": "7.26.0", + "@react-spring/rafz": "^9.7.5", + "@react-spring/web": "^9.7.5", + "clsx": "^2.1.1", + "prop-types": "^15.8.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-charts-vendor": { + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@mui/x-charts-vendor/-/x-charts-vendor-7.20.0.tgz", + "integrity": "sha512-pzlh7z/7KKs5o0Kk0oPcB+sY0+Dg7Q7RzqQowDQjpy5Slz6qqGsgOB5YUzn0L+2yRmvASc4Pe0914Ao3tMBogg==", + "license": "MIT AND ISC", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@types/d3-color": "^3.1.3", + "@types/d3-delaunay": "^6.0.4", + "@types/d3-interpolate": "^3.0.4", + "@types/d3-scale": "^4.0.8", + "@types/d3-shape": "^3.1.6", + "@types/d3-time": "^3.0.3", + "d3-color": "^3.1.0", + "d3-delaunay": "^6.0.4", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.2.0", + "d3-time": "^3.1.0", + "delaunator": "^5.0.1", + "robust-predicates": "^3.0.2" + } + }, + "node_modules/@mui/x-charts/node_modules/@react-spring/web": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/web/-/web-9.7.5.tgz", + "integrity": "sha512-lmvqGwpe+CSttsWNZVr+Dg62adtKhauGwLyGE/RRyZ8AAMLgb9x3NDMA5RMElXo+IMyTkPp7nxTB8ZQlmhb6JQ==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/core": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-charts/node_modules/@react-spring/web/node_modules/@react-spring/animated": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/animated/-/animated-9.7.5.tgz", + "integrity": "sha512-Tqrwz7pIlsSDITzxoLS3n/v/YCUHQdOIKtOJf4yL6kYVSDTSmVK1LI1Q3M/uu2Sx4X3pIWF3xLUhlsA6SPNTNg==", + "license": "MIT", + "dependencies": { + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-charts/node_modules/@react-spring/web/node_modules/@react-spring/core": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/core/-/core-9.7.5.tgz", + "integrity": "sha512-rmEqcxRcu7dWh7MnCcMXLvrf6/SDlSokLaLTxiPlAYi11nN3B5oiCUAblO72o+9z/87j2uzxa2Inm8UbLjXA+w==", + "license": "MIT", + "dependencies": { + "@react-spring/animated": "~9.7.5", + "@react-spring/shared": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-spring/donate" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-charts/node_modules/@react-spring/web/node_modules/@react-spring/shared": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/shared/-/shared-9.7.5.tgz", + "integrity": "sha512-wdtoJrhUeeyD/PP/zo+np2s1Z820Ohr/BbuVYv+3dVLW7WctoiN7std8rISoYoHpUXtbkpesSKuPIw/6U1w1Pw==", + "license": "MIT", + "dependencies": { + "@react-spring/rafz": "~9.7.5", + "@react-spring/types": "~9.7.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/@mui/x-data-grid": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@mui/x-data-grid/-/x-data-grid-7.27.1.tgz", + "integrity": "sha512-dDgrCIiIcb3XPxUVxXQsk9yjXFSAXBunqW1m1jjcr0D3jj3qHibBtGslSKnBmmcoT6XLAXdbZOu1fFC+xYhyGA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0", + "@mui/x-internals": "7.26.0", + "clsx": "^2.1.1", + "prop-types": "^15.8.1", + "reselect": "^5.1.1", + "use-sync-external-store": "^1.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.15.14 || ^6.0.0", + "@mui/system": "^5.15.14 || ^6.0.0", + "react": "^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + } + } + }, + "node_modules/@mui/x-internals": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@mui/x-internals/-/x-internals-7.26.0.tgz", + "integrity": "sha512-VxTCYQcZ02d3190pdvys2TDg9pgbvewAVakEopiOgReKAUhLdRlgGJHcOA/eAuGLyK1YIo26A6Ow6ZKlSRLwMg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.25.7", + "@mui/utils": "^5.16.6 || ^6.0.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui-org" + }, + "peerDependencies": { + "react": "^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1089,6 +1658,18 @@ "react": "^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0-rc.1" } }, + "node_modules/@react-spring/rafz": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/rafz/-/rafz-9.7.5.tgz", + "integrity": "sha512-5ZenDQMC48wjUzPAm1EtwQ5Ot3bLIAwwqP2w2owG5KoNdNHpEJV263nGhCeKKmuA3vG2zLLOdu3or6kuDjA6Aw==", + "license": "MIT" + }, + "node_modules/@react-spring/types": { + "version": "9.7.5", + "resolved": "https://registry.npmjs.org/@react-spring/types/-/types-9.7.5.tgz", + "integrity": "sha512-HVj7LrZ4ReHWBimBvu2SKND3cDVUPWKLqRTmWe/fNY6o1owGOX0cAHbdPDTMelgBlVbrTKrre6lFkhqGZErK/g==", + "license": "MIT" + }, "node_modules/@restart/hooks": { "version": "0.4.16", "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", @@ -1482,6 +2063,57 @@ "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", "license": "MIT" }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", @@ -1505,6 +2137,12 @@ "undici-types": "~6.20.0" } }, + "node_modules/@types/parse-json": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", + "integrity": "sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==", + "license": "MIT" + }, "node_modules/@types/prop-types": { "version": "15.7.14", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", @@ -1853,6 +2491,21 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10", + "npm": ">=6" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1940,7 +2593,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -1990,6 +2642,15 @@ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==", "license": "MIT" }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2033,6 +2694,31 @@ "node": ">=18" } }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "license": "MIT", + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cosmiconfig/node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -2054,11 +2740,131 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "license": "MIT" }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -2079,6 +2885,15 @@ "dev": true, "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz", + "integrity": "sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2105,6 +2920,15 @@ "dev": true, "license": "ISC" }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/esbuild": { "version": "0.24.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", @@ -2160,7 +2984,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -2433,6 +3256,12 @@ "node": ">=8" } }, + "node_modules/find-root": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", + "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==", + "license": "MIT" + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2495,6 +3324,15 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2548,6 +3386,27 @@ "node": ">=8" } }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hoist-non-react-statics": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", + "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", + "license": "BSD-3-Clause", + "dependencies": { + "react-is": "^16.7.0" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2562,7 +3421,6 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, "license": "MIT", "dependencies": { "parent-module": "^1.0.0", @@ -2585,6 +3443,15 @@ "node": ">=0.8.19" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/invariant": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", @@ -2603,6 +3470,27 @@ "@stencil/core": "^4.0.3" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -2666,7 +3554,6 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "dev": true, "license": "MIT", "bin": { "jsesc": "bin/jsesc" @@ -2682,6 +3569,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "license": "MIT" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2733,6 +3626,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "license": "MIT" + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2819,7 +3718,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/nanoid": { @@ -2918,7 +3816,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, "license": "MIT", "dependencies": { "callsites": "^3.0.0" @@ -2927,6 +3824,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2947,11 +3862,25 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, "license": "ISC" }, "node_modules/picomatch": { @@ -3206,11 +4135,36 @@ "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", + "license": "MIT" + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, "license": "MIT", "engines": { "node": ">=4" @@ -3227,6 +4181,12 @@ "node": ">=0.10.0" } }, + "node_modules/robust-predicates": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz", + "integrity": "sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.34.8", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", @@ -3335,6 +4295,15 @@ "node": ">=8" } }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -3358,6 +4327,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==", + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -3371,6 +4346,18 @@ "node": ">=8" } }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3522,6 +4509,15 @@ "punycode": "^2.1.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", + "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/vite": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", @@ -3636,6 +4632,21 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "dev": true, + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/Surge365.MassEmailReact.Web/package.json b/Surge365.MassEmailReact.Web/package.json index 59ff22d..2e7dbbc 100644 --- a/Surge365.MassEmailReact.Web/package.json +++ b/Surge365.MassEmailReact.Web/package.json @@ -11,13 +11,21 @@ "preview": "vite preview" }, "dependencies": { + "@emotion/react": "^11.14.0", + "@emotion/styled": "^11.14.0", + "@fontsource/roboto": "^5.1.1", + "@mui/icons-material": "^6.4.5", + "@mui/material": "^6.4.5", + "@mui/x-charts": "^7.27.1", + "@mui/x-data-grid": "^7.27.1", "admin-lte": "4.0.0-beta3", "bootstrap": "^5.3.3", + "dayjs": "^1.11.13", "font-awesome": "^4.7.0", "ionicons": "^7.4.0", "react": "^19.0.0", - "react-dom": "^19.0.0", "react-bootstrap": "^2.10.9", + "react-dom": "^19.0.0", "react-icons": "^5.3.0", "react-router-dom": "^7.0.1" }, diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx index 386e4b1..8a878ff 100644 --- a/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx +++ b/Surge365.MassEmailReact.Web/src/components/layouts/Layout.tsx @@ -1,120 +1,212 @@ -import { ReactNode } from 'react'; -//import React from 'react'; -import PropTypes from 'prop-types'; +// src/components/layouts/Layout.tsx +import React, { ReactNode } from 'react'; +import { styled, useColorScheme } from '@mui/material/styles'; +import Box from '@mui/material/Box'; +import Drawer from '@mui/material/Drawer'; +import AppBar from '@mui/material/AppBar'; +import Toolbar from '@mui/material/Toolbar'; +import List from '@mui/material/List'; +import Typography from '@mui/material/Typography'; +import Divider from '@mui/material/Divider'; +import IconButton from '@mui/material/IconButton'; +import MenuIcon from '@mui/icons-material/Menu'; +import ChevronLeftIcon from '@mui/icons-material/ChevronLeft'; +import ListItem from '@mui/material/ListItem'; +import ListItemButton from '@mui/material/ListItemButton'; +import ListItemIcon from '@mui/material/ListItemIcon'; +import ListItemText from '@mui/material/ListItemText'; +import DashboardIcon from '@mui/icons-material/Dashboard'; +import DirectionsCarIcon from '@mui/icons-material/DirectionsCar'; +import PeopleIcon from '@mui/icons-material/People'; +import { Link as RouterLink } from 'react-router-dom'; +import Select, { SelectChangeEvent } from '@mui/material/Select'; +import MenuItem from '@mui/material/MenuItem'; +import FormControl from '@mui/material/FormControl'; +import InputLabel from '@mui/material/InputLabel'; -import 'bootstrap/dist/css/bootstrap.min.css'; -import 'admin-lte/dist/css/adminlte.min.css'; -import 'font-awesome/css/font-awesome.min.css'; -/*import 'ionicons/dist/css/ionicons.min.css';*/ +// Constants +const drawerWidth = 240; -import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS -import 'admin-lte/dist/js/adminlte.min.js'; +// Styled components +const DrawerHeader = styled('div')(({ theme }) => ({ + display: 'flex', + alignItems: 'center', + padding: theme.spacing(0, 1), + ...theme.mixins.toolbar, + justifyContent: 'flex-end', +})); -const Layout = function Layout({ children }: { children: ReactNode }) { - return ( -
- - - - +const Main = styled('main', { shouldForwardProp: (prop) => prop !== 'open' })<{ + open?: boolean; +}>(({ theme, open }) => ({ + flexGrow: 1, + padding: theme.spacing(3), + transition: theme.transitions.create(['margin', 'width', 'padding'], { + easing: theme.transitions.easing.sharp, + duration: theme.transitions.duration.enteringScreen, + }), + marginLeft: "0px !important", // Force remove any margin on the left + marginRight: "0px !important", // Force remove any margin on the left + ...(open && {/*Opened specific types go here*/}), + ...(!open && {/*closed specific styles go here*/}) +})); - {/*AdminLTE?*/} - - {/**/} - {/**/} - - - - {/**/} - {/**/} - {/**/} - {/**/} -
- - - Logo - - - Logo - USAHaulers - - - -
- - - - {children} - -
-
- Version 1.0.0 -
- Copyright © 2024 Surge365. All rights reserved. -
-
- ); +interface LayoutProps { + children: ReactNode; } -Layout.propTypes = { - children: PropTypes.any +const Layout = ({ children }: LayoutProps) => { + const [open, setOpen] = React.useState(false); + const { mode, setMode } = useColorScheme(); // MUI v6 hook for theme switching + const iconButtonRef = React.useRef(null) + + const handleDrawerOpen = () => { + setOpen(true); + sendResize(); + }; + + const handleDrawerClose = () => { + setOpen(false); + sendResize(); + }; + + const sendResize = () => { + //// Force window resize event after drawer state changes + //setTimeout(() => { + // window.dispatchEvent(new Event("resize")); + //}); // Delay slightly to ensure UI updates + }; + const handleThemeChange = (event: SelectChangeEvent) => { + setMode(event.target.value as 'light' | 'dark'); + if (iconButtonRef.current) { + const selectElement = iconButtonRef.current; + if (selectElement) { + if (selectElement instanceof HTMLElement) { + setTimeout(() => { + selectElement.focus(); // Blur the focusable input + }, 0); + } + } + } + }; + + return ( + + {/* App Bar */} + ({ + zIndex: theme.zIndex.drawer + 1, + transition: theme.transitions.create(['width', 'margin','padding'], { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.leavingScreen, + }), + ...(open && { + width: `calc(100% - ${drawerWidth}px)`, + ml: `${drawerWidth}px`, + transition: theme.transitions.create(['width', 'margin', 'padding'], { + easing: theme.transitions.easing.easeInOut, + duration: theme.transitions.duration.enteringScreen, + }), + }), + })} + > + + + + + + Surge365 Dashboard + + + + Theme + + + + + + + {/* Sidebar */} + + + + + + + + + {[ + { text: 'Home', icon: , path: '/home' }, + { text: 'Targets', icon: , path: '/targets' }, + { text: 'Templates', icon: , path: '/templates' }, + ].map((item) => ( + + + {item.icon} + + + + ))} + + + + {/* Main Content */} +
+ + {children} +
+
+ ); }; -export default Layout; +export default Layout; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutAdminLTE.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutAdminLTE.tsx new file mode 100644 index 0000000..dbf8572 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutAdminLTE.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import AdminLTELogo from 'admin-lte/dist/assets/img/AdminLTELogo.png'; +interface LayoutProps { + children: React.ReactNode; +} + +const Layout: React.FC = ({ children }) => { + return ( +
+ {/* Header */} + + + {/* Sidebar */} + + + {/* Main Content Area */} +
+ {children} +
+ + {/* Footer */} +
+
Version 0.0.1
+ + Copyright © 2025  + Surge365. + + All rights reserved. +
+
+ ); +}; + +export default Layout; diff --git a/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx new file mode 100644 index 0000000..386e4b1 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/layouts/LayoutOld.tsx @@ -0,0 +1,120 @@ +import { ReactNode } from 'react'; +//import React from 'react'; +import PropTypes from 'prop-types'; + +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'admin-lte/dist/css/adminlte.min.css'; +import 'font-awesome/css/font-awesome.min.css'; +/*import 'ionicons/dist/css/ionicons.min.css';*/ + +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS +import 'admin-lte/dist/js/adminlte.min.js'; + +const Layout = function Layout({ children }: { children: ReactNode }) { + return ( +
+ + + + + + {/*AdminLTE?*/} + + {/**/} + {/**/} + + + + {/**/} + {/**/} + {/**/} + {/**/} +
+ + + Logo + + + Logo + USAHaulers + + + +
+ + + + {children} + +
+
+ Version 1.0.0 +
+ Copyright © 2024 Surge365. All rights reserved. +
+
+ ); +} + +Layout.propTypes = { + children: PropTypes.any +}; + +export default Layout; diff --git a/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx b/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx new file mode 100644 index 0000000..1a0530a --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/modals/TargetEdit.tsx @@ -0,0 +1,127 @@ +import { useState } from "react"; +import { + Dialog, + DialogTitle, + DialogContent, + TextField, + DialogActions, + Button, + Switch, + FormControlLabel, +} from "@mui/material"; +import { Target } from "@/types/target"; + +type TargetEditProps = { + open: boolean; + target: Target; + onClose: () => void; + onSave: (updatedTarget: Target) => void; +}; + +const TargetEdit = ({ open, target, onClose, onSave }: TargetEditProps) => { + const [formData, setFormData] = useState({ ...target }); + const [loading, setLoading] = useState(false); + + const handleChange = (field: keyof Target, value: string | boolean) => { + setFormData((prev) => ({ ...prev, [field]: value })); + }; + + const handleSave = async () => { + setLoading(true); + try { + const response = await fetch(`/api/targets/${formData.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(formData), + }); + + if (!response.ok) throw new Error("Failed to update"); + + const updatedTarget = await response.json(); + onSave(updatedTarget); // Update UI optimistically + onClose(); + } catch (error) { + console.error("Update error:", error); + } finally { + setLoading(false); + } + }; + + return ( + + Edit Target + + handleChange("id", e.target.value)} + margin="dense" + /> + handleChange("serverId", e.target.value)} + margin="dense" + /> + handleChange("name", e.target.value)} + margin="dense" + /> + handleChange("databaseName", e.target.value)} + margin="dense" + /> + handleChange("viewName", e.target.value)} + margin="dense" + /> + handleChange("filterQuery", e.target.value)} + margin="dense" + /> + + handleChange("allowWriteBack", e.target.checked)} + /> + } + label="Allow Write Back" + /> + + handleChange("isActive", e.target.checked)} + /> + } + label="Active" + /> + + + + + + + ); +}; + +export default TargetEdit; diff --git a/Surge365.MassEmailReact.Web/src/components/pages/App.tsx b/Surge365.MassEmailReact.Web/src/components/pages/App.tsx index b3948c0..053fc71 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/App.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/App.tsx @@ -1,34 +1,77 @@ // App.tsx or main routing component -//import React from 'react'; +import React from 'react'; import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import Layout from '@/components/layouts/Layout'; import LayoutLogin from '@/components/layouts/LayoutLogin'; -import Vehicles from '@/components/pages/Vehicles'; +import Home from '@/components/pages/Home'; import Login from '@/components/pages/Login'; +import Targets from '@/components/pages/Targets'; +import Templates from '@/components/pages/Templates'; +import { ColorModeContext } from '@/theme/theme'; +import { SetupDataProvider } from '@/context/SetupDataContext'; + +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import CssBaseline from '@mui/material/CssBaseline'; + const App = () => { + const [mode, setMode] = React.useState<'light' | 'dark'>('light'); + + const colorMode = React.useMemo( + () => ({ + toggleColorMode: () => { + setMode((prevMode) => (prevMode === 'light' ? 'dark' : 'light')); + }, + }), + [] + ); + + const theme = React.useMemo(() => createTheme({ palette: { mode } }), [mode]); return ( - - - } /> - - - - } - /> - - - - } - /> - - + + + + + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + + ); }; diff --git a/Surge365.MassEmailReact.Web/src/components/pages/AppMain.tsx b/Surge365.MassEmailReact.Web/src/components/pages/AppMain.tsx new file mode 100644 index 0000000..bc99c55 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages/AppMain.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import { createRoot } from 'react-dom/client' +import { createTheme, ThemeProvider } from '@mui/material/styles'; +import '@/css/main.css' +import App from '@/components/pages/App' +import '@/config/constants'; + + +//DEFAULT THEMES +const theme = createTheme({ + cssVariables: { + colorSchemeSelector: 'class' + }, + colorSchemes: { + light: true, // Default light scheme + dark: true, // Default dark scheme + }, + components: { + MuiAppBar: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.vars.palette.primary.main, + color: theme.vars.palette.text.primary, + }), + }, + }, + MuiOutlinedInput: { + styleOverrides: { + notchedOutline: ({ theme }) => ({ + borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[500] : theme.vars.palette.text.primary, + '&:hover': { + borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[700] : theme.vars.palette.text.primary, + }, + }), + root: ({ theme }) => ({ + '&.Mui-focused .MuiOutlinedInput-notchedOutline': { + borderColor: theme.palette.mode === 'light' ? theme.vars.palette.grey[900] : theme.vars.palette.text.primary, + borderWidth: 2, // Match MUI default focus width + }, + }), + }, + }, + }, +}); + + +//CUSTOM THEMES +//const theme = createTheme({ +// cssVariables: { +// colorSchemeSelector: 'class' +// }, +// colorSchemes: { +// light: { +// palette: { +// primary: { main: '#1976d2' }, +// background: { default: '#fff', paper: '#f5f5f5' }, +// }, +// }, +// dark: { +// palette: { +// primary: { main: '#90caf9' }, +// background: { default: '#121212', paper: '#424242' }, +// }, +// }, +// }, +//}); + +const rootElement = document.getElementById('root'); +if (rootElement) { + createRoot(rootElement).render( + + + + + + ); +} else { + throw new Error('Root element not found'); +} diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx new file mode 100644 index 0000000..42911f6 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages/Home.tsx @@ -0,0 +1,36 @@ +// src/components/pages/Home.tsx +//import React from 'react'; +import Typography from '@mui/material/Typography'; +import Box from '@mui/material/Box'; +import Grid2 from '@mui/material/Grid2'; // v6 Grid2 +//import Card from '@mui/material/Card'; +//import CardContent from '@mui/material/CardContent'; +//import { CardActionArea } from '@mui/material'; +import { BarChart } from '@mui/x-charts/BarChart'; + +import LineChartSample from '@/components/widgets/LineChartSample'; + +const Home = () => { + return ( + + + Welcome to Surge365 + + + + + + + + + + + ); +}; + +export default Home; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx index f9c6f06..b3b3a3a 100644 --- a/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx +++ b/Surge365.MassEmailReact.Web/src/components/pages/Login.tsx @@ -1,5 +1,6 @@ import { useState } from 'react'; import { Button, Form, Spinner } from 'react-bootstrap'; +import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth'; //import { Helmet, HelmetProvider } from 'react-helmet-async'; import utils from '@/ts/utils.ts'; @@ -15,8 +16,9 @@ function Login() { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); - const [user, setUser] = useState(null); + //const [user, setUser] = useState(null); const [loginError, setLoginError] = useState(false); + const [loginErrorMessage, setLoginErrorMessage] = useState(''); //const setSpinners = (newValues: Partial) => { // setSpinnersState((prevSpinners) => ({ @@ -68,26 +70,50 @@ function Login() { if (Object.keys(formErrors).length > 0) return; + //setUser(null); setLoginError(false); - let loggedInUser: any = null; - - await utils.webMethod({ + setLoginErrorMessage(''); + let loggedInUser: User | null = null; + let hadLoginError: boolean = false; + let hadLoginErrorMessage: string = ''; + await utils.webMethod({ methodPage: 'authentication', methodName: 'authenticate', - parameters: { username: username, password: password }, - success: (json: any) => { - if (utils.getBoolean(json.success)) { - loggedInUser = json.data; - setUser(loggedInUser); - } else { - setLoginError(true); - setIsLoading(false); - spinners.Login = false; + parameters: { username, password }, + success: (json: AuthResponse) => { + try { + loggedInUser = json.user; + //setUser(loggedInUser); } + catch { + const errorMsg: string = "Unexpected Error"; + hadLoginError = true; + hadLoginErrorMessage = errorMsg; + } + }, + error: (err: unknown) => { + let errorMsg: string = "Unexpected Error"; + if (isAuthErrorResponse(err)) { + if (err && err as AuthErrorResponse) { + if (err.data) { + if (err.data.message) + errorMsg = err.data.message; + } + console.error(errorMsg); + setLoginErrorMessage(errorMsg); + } + } + hadLoginError = true; + hadLoginErrorMessage = errorMsg; } }); - if (loginError) { + if (hadLoginError) { + setLoginErrorMessage(hadLoginErrorMessage); + setLoginError(true); + setIsLoading(false); + spinners.Login = false; + setSpinners(spinners); return; } @@ -101,20 +127,20 @@ function Login() { } }; - const finishUserLogin = async (user: any) => { + const finishUserLogin = async (loggedInUser: User) => { setIsLoading(false); spinners.Login = false; spinners.LoginWithPasskey = false; setSpinners(spinners); - utils.localStorage("session_currentUser", user); + utils.localStorage("session_currentUser", loggedInUser); const redirectUrl = utils.sessionStorage("redirect_url"); if (redirectUrl) { utils.sessionStorage("redirect_url", null); document.location.href = redirectUrl; } else { - document.location.href = '/vehicles'; + document.location.href = '/home'; } }; @@ -127,7 +153,7 @@ function Login() {

Please sign in

{loginError && ( - Login error + {loginErrorMessage ?? "Login error"} )} Username diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx new file mode 100644 index 0000000..a39fb08 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages/Targets.tsx @@ -0,0 +1,160 @@ +import { useState, useRef } from 'react'; +import { useSetupData, SetupData } from "@/context/SetupDataContext"; +//import Typography from '@mui/material/Typography'; +import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress } from '@mui/material'; +import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid'; +//import utils from '@/ts/utils'; +import { Target } from '@/types/target'; +import TargetEdit from "@/components/modals/TargetEdit"; + + +function Targets() { + const theme = useTheme(); + const setupData: SetupData = useSetupData(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + + //const [targets, setTargets] = useState([]); + const gridContainerRef = useRef(null); + const [selectedRow, setSelectedRow] = useState(null); + const [open, setOpen] = useState(false); + + //TODO: Update columns to target format + const columns: GridColDef[] = [ + { + field: "actions", + headerName: "Actions", + sortable: false, + renderCell: (params: GridRenderCellParams) => ( + + ), + }, + { field: "id", headerName: "ID", width: 60 }, + //{ field: "serverKey", headerName: "Server Key", flex: 1, minWidth: 140 }, + { field: "name", headerName: "Name", flex: 1, minWidth: 160 }, + { field: "databaseName", headerName: "Database", flex: 1, minWidth: 100 }, + { field: "viewName", headerName: "View", flex: 1, minWidth: 300 }, + { field: "filterQuery", headerName: "Filter Query", flex: 1, minWidth: 100 }, + { field: "allowWriteBack", headerName: "WriteBack?", width: 100 }, + { field: "isActive", headerName: "Active", width: 75 }, + ]; + + + //async function loadTargets(activeOnly: boolean) { + // let hadError = false; + // let hadErrorMessage = ""; + // await utils.webMethod<[Target]>({ + // httpMethod: 'GET', + // methodPage: 'targets', + // methodName: 'GetAll?activeOnly=' + activeOnly, + // success: (targets: [Target]) => { + // try { + // setTargets(targets); + // } + // catch { + // const errorMsg: string = "Unexpected Error"; + // hadError = true; + // hadErrorMessage = errorMsg; + // } + // }, + // error: () => { + // const errorMsg: string = "Unexpected Error"; + // hadError = true; + // hadErrorMessage = errorMsg; + // } + // }); + // if (hadError) + // alert(hadErrorMessage); //TODO: Make this look better. MUI Alert popup? + //} + const handleEdit = (row: GridRowModel) => { + setSelectedRow(row); + setOpen(true); + }; + + const handleUpdateRow = (updatedRow: Target) => { + setupData.setTargets(updatedRow); + }; + return ( + + + {isMobile ? ( + + {setupData.targets.map((row) => ( + + + {row.name} + Target Key: {row.id} + {/*Server Key: {row.serverKey}*/} + Database: {row.databaseName} + View: {row.viewName} + Filter: {row.filterQuery} + Writeback: {row.allowWriteBack ? "Yes" : "No"} + Active: {row.isActive ? "Yes" : "No"} + + + ))} + + ) : ( + row.id!}//Set this if object doesn't have id as unique id (like targetKey etc) + slots={{ + toolbar: () => ( + + + + + + {/* Keeps the search filter in the toolbar */} + + ), + }} + slotProps={{ + toolbar: { + showQuickFilter: true, + quickFilterProps: { /*debounceMs: 500/*Optional: Adds debounce to search*/ }, + }, + }} + initialState={{ + pagination: { + paginationModel: { + pageSize: 20, + }, + }, + }} + pageSizeOptions={[10, 20, 50, 100]} + /> + )} + + + {/* Target Edit Modal */} + {selectedRow && ( + setOpen(false)} + onSave={handleUpdateRow} + /> + )} + + ); +} + +export default Targets; \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx new file mode 100644 index 0000000..1a6e480 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages/Templates.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +const Templates: React.FC = () => { + return ( +
+ {/* Content Header */} +
+
+
+
+

Templates

+
+
+
    +
  1. + Home +
  2. +
  3. + Templates +
  4. +
+
+
+
+
+ + {/* Page-specific Content */} +
+
+
+ {/* Example: Small Box Widget 1 */} +
+
+
+

150

+

Templates

+
+ + + + + More info + +
+
+ {/* Additional small boxes, charts, or other widgets can be included here */} +
+ {/* You can also add more rows for charts, direct chat, etc. */} +
+
+
+ ); +}; + +export default Templates; diff --git a/Surge365.MassEmailReact.Web/src/components/pages/Vehicles.tsx b/Surge365.MassEmailReact.Web/src/components/pages/Vehicles.tsx deleted file mode 100644 index fa1e683..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages/Vehicles.tsx +++ /dev/null @@ -1,370 +0,0 @@ -//import { useState } from 'react'; - -function Vehicles() { - return (
- - - - - -
- - {/* Content Header (Page header) */} -
- -

Vehicles - - - -

- -
-
- -
-
-
-
- - - - -
-
-
- -
- - -
- -
- -
- -
- - -
- -
- -
- -
- - -
- -
- -
- -
- - -
- -
-
- -
- - - -
- - -
-
- -
- - - -
-
-
-
- - - - - - - -
-
- -
-
- -
-
- - {/* Info boxes */} -
-
- {/* Box Comment */} -
- - {/* /.box-header */} -
- - - - - - - - - - - - - - - - - - - - - - - - - -
IDVINNamePriceStock #YearMakeModelExt. ColorInt. ColorSeatingTransmissionTitleFeatured
- Image
ImagesViewEdit
-
- {/* /.box-body */} -
- {/* /.box */} -
- {/* /.col */} -
- {/* /.row */} -
-
- -
-
- -
-
- -
- -

Edit Patient

-
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - -
-
-
-
-
- - -
- - -
-
-
-
-
- - -
- - -
-
-
-
-
- - -
- - -
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - - -
-
-
-
-
- - -
- - - -
-
-
-
-
- -
- - -
-
-
-
-
- -
- Drag and drop images here or click to select - -
- -
-
-
- - - -
- - - - -
-
- -
- - - - -
-
-
-
- - {/* /.modal-content */} -
- {/* /.modal-dialog */} -
- - - -
-
-
-
-
- -

Upload new document?

-
-
-
-
-
- -
-
-
-
-
- - - - -
-
-
- {/* /.modal-content */} -
- {/* /.modal-dialog */} -
- ); - //return content; -} - -export default Vehicles; diff --git a/Surge365.MassEmailReact.Web/src/components/pages/main.tsx b/Surge365.MassEmailReact.Web/src/components/pages/main.tsx deleted file mode 100644 index f1e1809..0000000 --- a/Surge365.MassEmailReact.Web/src/components/pages/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { StrictMode } from 'react' -import { createRoot } from 'react-dom/client' -import '@/css/main.css' -import App from '@/components/pages/App.tsx' -import '@/config/constants'; - -createRoot(document.getElementById('root')).render( - - - , -) diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx new file mode 100644 index 0000000..3726cec --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppMain.tsx @@ -0,0 +1,24 @@ +import { StrictMode } from 'react' +import { createRoot } from 'react-dom/client' +import '@/css/main.css' +import App from '@/components/pages/App' +import '@/config/constants'; + +import 'bootstrap/dist/css/bootstrap.min.css'; +import 'admin-lte/dist/css/adminlte.min.css'; +import 'font-awesome/css/font-awesome.min.css'; +/*import 'ionicons/dist/css/ionicons.min.css';*/ + +import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS +import 'admin-lte/dist/js/adminlte.min.js'; + +const rootElement = document.getElementById('root'); +if (rootElement) { + createRoot(rootElement).render( + + + + ); +} else { + throw new Error('Root element not found'); +} diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx new file mode 100644 index 0000000..b58ce4e --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/AppRouter.tsx @@ -0,0 +1,53 @@ +// App.tsx or main routing component +//import React from 'react'; +import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; +import Layout from '@/components/layouts/Layout'; +import LayoutLogin from '@/components/layouts/LayoutLogin'; +import Home from '@/components/pages/Home'; +import Login from '@/components/pages/Login'; +import Targets from '@/components/pages/Targets'; +import Templates from '@/components/pages/Templates'; + +const App = () => { + return ( + + + } /> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + + + ); +}; + +export default App; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx new file mode 100644 index 0000000..8f2e6d6 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Home.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +const Home: React.FC = () => { + return ( +
+ {/* Content Header */} +
+
+
+
+

Dashboard

+
+
+
    +
  1. + Home +
  2. +
  3. + Dashboard +
  4. +
+
+
+
+
+ + {/* Page-specific Content */} +
+
+
+ {/* Example: Small Box Widget 1 */} +
+
+
+

150

+

New Orders

+
+ + + + + More info + +
+
+ {/* Additional small boxes, charts, or other widgets can be included here */} +
+ {/* You can also add more rows for charts, direct chat, etc. */} +
+
+
+ ); +}; + +export default Home; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx new file mode 100644 index 0000000..b3b3a3a --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Login.tsx @@ -0,0 +1,199 @@ +import { useState } from 'react'; +import { Button, Form, Spinner } from 'react-bootstrap'; +import { AuthResponse, AuthErrorResponse, User, isAuthErrorResponse } from '@/types/auth'; +//import { Helmet, HelmetProvider } from 'react-helmet-async'; + +import utils from '@/ts/utils.ts'; +import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal'; + +type SpinnerState = Record; +type FormErrors = Record; + +function Login() { + const [isLoading, setIsLoading] = useState(false); + const [spinners, setSpinnersState] = useState({}); + const [formErrors, setFormErrors] = useState({}); + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false); + //const [user, setUser] = useState(null); + const [loginError, setLoginError] = useState(false); + const [loginErrorMessage, setLoginErrorMessage] = useState(''); + + //const setSpinners = (newValues: Partial) => { + // setSpinnersState((prevSpinners) => ({ + // ...prevSpinners, + // ...newValues, + // })); + //}; + const setSpinners = (newValues: Partial) => { + setSpinnersState((prevSpinners) => { + const updatedSpinners: SpinnerState = { ...prevSpinners }; + for (const key in newValues) { + if (newValues[key] !== undefined) { + updatedSpinners[key] = newValues[key] as boolean; + } + } + return updatedSpinners; + }); + }; + + const handleCloseForgotPasswordModal = () => { + setShowForgotPasswordModal(false); + }; + + const validateLoginForm = () => { + setFormErrors({}); + + const errors: FormErrors = {}; + if (!username.trim()) { + errors.username = 'Username is required'; + //} else if (!/\S+@\S+\.\S+/.test(email)) { + // errors.email = 'Invalid email address'; + } + if (!password.trim()) { + errors.password = 'Password is required'; + } + + if (Object.keys(errors).length > 0) { + setFormErrors(errors); + } + }; + + const handleLogin = async (e: React.FormEvent) => { + e.preventDefault(); + setIsLoading(true); + spinners.Login = true; + setSpinners(spinners); + + validateLoginForm(); + + if (Object.keys(formErrors).length > 0) return; + + //setUser(null); + setLoginError(false); + setLoginErrorMessage(''); + let loggedInUser: User | null = null; + let hadLoginError: boolean = false; + let hadLoginErrorMessage: string = ''; + await utils.webMethod({ + methodPage: 'authentication', + methodName: 'authenticate', + parameters: { username, password }, + success: (json: AuthResponse) => { + try { + loggedInUser = json.user; + //setUser(loggedInUser); + } + catch { + const errorMsg: string = "Unexpected Error"; + hadLoginError = true; + hadLoginErrorMessage = errorMsg; + } + }, + error: (err: unknown) => { + let errorMsg: string = "Unexpected Error"; + if (isAuthErrorResponse(err)) { + if (err && err as AuthErrorResponse) { + if (err.data) { + if (err.data.message) + errorMsg = err.data.message; + } + console.error(errorMsg); + setLoginErrorMessage(errorMsg); + } + } + hadLoginError = true; + hadLoginErrorMessage = errorMsg; + } + }); + + if (hadLoginError) { + setLoginErrorMessage(hadLoginErrorMessage); + setLoginError(true); + setIsLoading(false); + spinners.Login = false; + setSpinners(spinners); + return; + } + + if (loggedInUser == null) { + setLoginError(true); + setIsLoading(false); + spinners.Login = false; + setSpinners(spinners); + } else { + await finishUserLogin(loggedInUser); + } + }; + + const finishUserLogin = async (loggedInUser: User) => { + setIsLoading(false); + spinners.Login = false; + spinners.LoginWithPasskey = false; + setSpinners(spinners); + + utils.localStorage("session_currentUser", loggedInUser); + + const redirectUrl = utils.sessionStorage("redirect_url"); + if (redirectUrl) { + utils.sessionStorage("redirect_url", null); + document.location.href = redirectUrl; + } else { + document.location.href = '/home'; + } + }; + + return ( +
+
+

surge365 - React

+
+
+

Please sign in

+
+ {loginError && ( + {loginErrorMessage ?? "Login error"} + )} + + Username + setUsername(e.target.value)} + required + autoFocus + size="sm" + /> + {spinners.Username && } + + + + Password + setPassword(e.target.value)} + required + size="sm" + /> + + + + +
+
+ + +
+ ); +} + +export default Login; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx new file mode 100644 index 0000000..11355fd --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Targets.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +const Targets: React.FC = () => { + return ( +
+ {/* Content Header */} +
+
+
+
+

Targets

+
+
+
    +
  1. + Home +
  2. +
  3. + Targets +
  4. +
+
+
+
+
+ + {/* Page-specific Content */} +
+
+
+ {/* Example: Small Box Widget 1 */} +
+
+
+

150

+

Targets

+
+ + + + + More info + +
+
+ {/* Additional small boxes, charts, or other widgets can be included here */} +
+ {/* You can also add more rows for charts, direct chat, etc. */} +
+
+
+ ); +}; + +export default Targets; diff --git a/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx new file mode 100644 index 0000000..1a6e480 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/pages_adminlte/Templates.tsx @@ -0,0 +1,63 @@ +import React from 'react'; + +const Templates: React.FC = () => { + return ( +
+ {/* Content Header */} +
+
+
+
+

Templates

+
+
+
    +
  1. + Home +
  2. +
  3. + Templates +
  4. +
+
+
+
+
+ + {/* Page-specific Content */} +
+
+
+ {/* Example: Small Box Widget 1 */} +
+
+
+

150

+

Templates

+
+ + + + + More info + +
+
+ {/* Additional small boxes, charts, or other widgets can be included here */} +
+ {/* You can also add more rows for charts, direct chat, etc. */} +
+
+
+ ); +}; + +export default Templates; diff --git a/Surge365.MassEmailReact.Web/src/components/widgets/LineChartSample.tsx b/Surge365.MassEmailReact.Web/src/components/widgets/LineChartSample.tsx new file mode 100644 index 0000000..ebe8b83 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/components/widgets/LineChartSample.tsx @@ -0,0 +1,49 @@ +import { LineChart, lineElementClasses } from '@mui/x-charts/LineChart'; +import dayjs from "dayjs"; + +const uData = [4000, 3000, 2000, 2780, 1890, 2390, 3490]; //This is # errors +const pData = [2400, 1398, 9800, 0, 4800, 3800, 4300]; //This is # delivered above errors (delivered - errors) +const amtData = [2400, 2210, 0, 2000, 2181, 2500, 2100]; /// This is # sent above errors and delivered (sent - delivered - sent) +const time = [ + new Date(2024, 7, 0), + new Date(2024, 8, 0), + new Date(2024, 9, 0), + new Date(2024, 10, 0), + new Date(2024, 11, 0), + new Date(2024, 12, 0), + new Date(2025, 1, 0) +]; + +export default function LineChartSample() { + return ( + dayjs(value).format("MMM YY") + }, + ]} + sx={{ + [`& .${lineElementClasses.root}`]: { + display: 'none', + }, + }} + /> + ); +} diff --git a/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx b/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx new file mode 100644 index 0000000..26e59d9 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/context/SetupDataContext.tsx @@ -0,0 +1,86 @@ +import { createContext, useState, useEffect, useContext } from "react"; +import { Target } from "@/types/target"; +import { Server } from "@/types/server"; + +export type SetupData = { + targets: Target[]; + reloadTargets: () => void; + setTargets: (updatedTarget: Target) => void; + servers: Server[]; + reloadServers: () => void; + reloadSetupData: () => void; + targetsLoading: boolean; + serversLoading: boolean; + dataLoading: boolean; +}; + +const SetupDataContext = createContext(undefined); + +export const SetupDataProvider = ({ children }: { children: React.ReactNode }) => { + const [servers, setServers] = useState([]); + const [targets, setTargets] = useState([]); + const [targetsLoading, setTargetsLoading] = useState(false); + const [serversLoading, setServersLoading] = useState(false); + const [dataLoading, setDataLoading] = useState(false); + + const reloadSetupData = async () => { + sessionStorage.removeItem("setupData"); + await fetchSetupData(); + } + const fetchSetupData = async () => { + try { + setDataLoading(true); + setTargetsLoading(true); + setServersLoading(true); + const cachedData = sessionStorage.getItem("setupData"); + if (cachedData) { //TODO: check if data is stale + const parsedData = JSON.parse(cachedData); + setTargets(parsedData.targets); + setServers(parsedData.servers); + setDataLoading(false); + setTargetsLoading(false); + setServersLoading(false); + return; + } + const targetsResponse = await fetch("/api/targets/GetAll?activeOnly=false"); + const targetsData = await targetsResponse.json(); + setTargets(targetsData); + setTargetsLoading(false); + //const serversResponse = await fetch("/api/setup/servers"); //TODO: call once setup + //const serversData = await serversResponse.json(); + + const serversData: Server[] = []; + setServers(serversData); + setServersLoading(false); + setDataLoading(false); + sessionStorage.setItem("setupData", JSON.stringify({ targets: targetsData, servers: serversData })); + } catch (error) { + setDataLoading(false); + setTargetsLoading(false); + setServersLoading(false); + console.error("Failed to fetch setup data:", error); + } + }; + + const updateTargetCache = (updatedTarget: Target) => { + setTargets((prevTargets) => + prevTargets.map((target) => (target.id === updatedTarget.id ? updatedTarget : target)) + ); + sessionStorage.setItem("setupData", JSON.stringify({ servers, targets })); + }; + useEffect(() => { + fetchSetupData(); + }, []); + + return ( + + {children} + + ); +}; + +export const useSetupData = () => { + const context = useContext(SetupDataContext); + if (!context) throw new Error("useSetupData must be used within a SetupDataProvider"); + return context; +}; diff --git a/Surge365.MassEmailReact.Web/src/css/main.css b/Surge365.MassEmailReact.Web/src/css/main.css index e69de29..48c59f8 100644 --- a/Surge365.MassEmailReact.Web/src/css/main.css +++ b/Surge365.MassEmailReact.Web/src/css/main.css @@ -0,0 +1,2 @@ +@import url('https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap'); +@import url('https://fonts.googleapis.com/icon?family=Material+Icons'); \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/theme/theme.tsx b/Surge365.MassEmailReact.Web/src/theme/theme.tsx new file mode 100644 index 0000000..8c7cd07 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/theme/theme.tsx @@ -0,0 +1,3 @@ +import React from 'react'; + +export const ColorModeContext = React.createContext({ toggleColorMode: () => { } }); diff --git a/Surge365.MassEmailReact.Web/src/ts/utils.ts b/Surge365.MassEmailReact.Web/src/ts/utils.ts index 6edb5b2..ee63220 100644 --- a/Surge365.MassEmailReact.Web/src/ts/utils.ts +++ b/Surge365.MassEmailReact.Web/src/ts/utils.ts @@ -1,3 +1,15 @@ +export class ApiError extends Error { + public status: number; + public data: unknown; + + constructor(status: number, data: unknown) { + super(`HTTP error! status: ${status}`); + this.status = status; + this.data = data; + Object.setPrototypeOf(this, ApiError.prototype); + } +} + const utils = { getCookie: (name: string): string | null => { const value = `; ${document.cookie}`; @@ -6,7 +18,7 @@ const utils = { return null; }, getParameterByName: (name: string): string | null => { - const regex = new RegExp(`[\?&]${name}=([^&#]*)`); + const regex = new RegExp(`[?&]${name}=([^&#]*)`); const results = regex.exec(window.location.search); return results ? decodeURIComponent(results[1].replace(/\+/g, ' ')) : null; }, @@ -29,26 +41,26 @@ const utils = { return headers; }, - webMethod: async ({ + webMethod: async ({ httpMethod = 'POST', baseMethodPath = 'api/', methodPage = '', methodName = '', - parameters = {}, + parameters = {} as Record, contentType = 'application/json;', timeout = 300000, - success = () => { }, - error = () => { }, + success = (_data: T) => { }, + error = (_err: unknown) => { }, }: { httpMethod?: string; baseMethodPath?: string; methodPage?: string; methodName?: string; contentType?: string; - parameters?: Record; + parameters?: Record; timeout?: number; - success?: (data: any) => void; - error?: (err: any) => void; + success?: (_data: T) => void; + error?: (_err: unknown) => void; }): Promise => { try { const baseUrl = window.API_BASE_URL || ''; @@ -59,19 +71,24 @@ const utils = { }); const controller = new AbortController(); - const timeoutId = setTimeout(() => controller.abort(), timeout); + //const timeoutId = setTimeout(() => controller.abort(), timeout); + setTimeout(() => controller.abort(), timeout); const response = await fetch(url, { method: httpMethod, headers, - body: JSON.stringify(parameters), + body: (httpMethod.toUpperCase() == "GET" ? null : JSON.stringify(parameters)), signal: controller.signal, }); - clearTimeout(timeoutId); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + let data = null; + try { + data = await response.json(); + } catch { + // Intentionally empty, if not json ignore + } + throw new ApiError(response.status, data); } const authToken = response.headers.get('Auth-Token'); diff --git a/Surge365.MassEmailReact.Web/src/types/auth.ts b/Surge365.MassEmailReact.Web/src/types/auth.ts new file mode 100644 index 0000000..b8373ae --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/auth.ts @@ -0,0 +1,33 @@ +// src/types/auth.ts + +import { UUID } from "crypto"; + +export interface User { + userKey: number, + userId: UUID; + username: string; + firstName: string; + middleInitial: string; + lastName: string; + isActive: boolean; + // Add any other properties returned by your API +} + +export interface AuthResponse { + accessToken: string; + user: User; +} + +export interface AuthErrorResponse { + + message: string; + data: { message: string }; +} +export function isAuthErrorResponse(err: unknown): err is AuthErrorResponse { + return ( + typeof err === 'object' && + err !== null && + 'message' in err && + typeof (err as any).message === 'string' + ); +} \ No newline at end of file diff --git a/Surge365.MassEmailReact.Web/src/types/server.ts b/Surge365.MassEmailReact.Web/src/types/server.ts new file mode 100644 index 0000000..202d698 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/server.ts @@ -0,0 +1,5 @@ +export interface Server { + id?: number; + name: string; +} + diff --git a/Surge365.MassEmailReact.Web/src/types/target.ts b/Surge365.MassEmailReact.Web/src/types/target.ts new file mode 100644 index 0000000..32c6096 --- /dev/null +++ b/Surge365.MassEmailReact.Web/src/types/target.ts @@ -0,0 +1,11 @@ +export interface Target { + id?: number; + serverId: number; + name: string; + databaseName: string; + viewName: string; + filterQuery: string; + allowWriteBack: boolean; + isActive: boolean; +} + diff --git a/Surge365.MassEmailReact.Web/tsconfig.app.json b/Surge365.MassEmailReact.Web/tsconfig.app.json index 358ca9b..cebec91 100644 --- a/Surge365.MassEmailReact.Web/tsconfig.app.json +++ b/Surge365.MassEmailReact.Web/tsconfig.app.json @@ -1,9 +1,10 @@ { + "extends": "./tsconfig.json", "compilerOptions": { "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", "target": "ES2020", "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], + "lib": [ "ES2020", "DOM", "DOM.Iterable" ], "module": "ESNext", "skipLibCheck": true, @@ -22,5 +23,5 @@ "noFallthroughCasesInSwitch": true, "noUncheckedSideEffectImports": true }, - "include": ["src"] + "include": [ "src" ] }