Refactor API structure and update UI components

- Changed namespaces for controllers to better organize API components.
- Updated base class for controllers to `BaseController` with standard routing.
- Simplified method signatures by removing `id` parameters in entity creation methods.
- Introduced new `CreateAsync` methods in service and repository layers.
- Removed dependencies on `font-awesome` and `ionicons` from package files.
- Migrated UI components to Material-UI, enhancing consistency and design.
- Refactored `ForgotPasswordModal` to use Material-UI's `Dialog`.
- Implemented `yup` validation in form components for stricter checks.
- Cleaned up unused styles in CSS files for a cleaner codebase.
- Updated interfaces to require `id` property for stricter type checks.
- Improved error handling and user feedback in the `Login` component.
- Added action buttons for adding and refreshing data in various components.
This commit is contained in:
David Headrick 2025-03-05 12:10:43 -06:00
parent d7b00cf335
commit ef75bdb779
41 changed files with 487 additions and 867 deletions

View File

@ -1,13 +1,12 @@
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.API.Controllers;
using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class AuthenticationController : ControllerBase
public class AuthenticationController : BaseController
{
private readonly IAuthService _authService;

View File

@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Mvc;
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("[controller]")]
[Route("api/[controller]")]
[ApiController]
public class BaseController : ControllerBase
{
}
}

View File

@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System.Net.Mail;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class BouncedEmailsController : ControllerBase
public class BouncedEmailsController : BaseController
{
private readonly IBouncedEmailService _bouncedEmailService;

View File

@ -3,11 +3,9 @@ using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using System.Threading.Tasks;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class EmailDomainsController : ControllerBase
public class EmailDomainsController : BaseController
{
private readonly IEmailDomainService _emailDomainService;
@ -34,7 +32,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
}
[HttpPost()]
public async Task<IActionResult> CreateTarget(int id, [FromBody] EmailDomainUpdateDto emailDomainUpdateDto)
public async Task<IActionResult> CreateTarget([FromBody] EmailDomainUpdateDto emailDomainUpdateDto)
{
if (emailDomainUpdateDto.Id != null && emailDomainUpdateDto.Id > 0)
return BadRequest("Id must be null or 0");

View File

@ -4,12 +4,11 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
using Surge365.MassEmailReact.Infrastructure.Services;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class ServersController : ControllerBase
public class ServersController : BaseController
{
private readonly IServerService _serverService;
@ -19,6 +18,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
}
[HttpGet()]
[HttpGet("GetAll")]
public async Task<IActionResult> GetAll([FromQuery] bool? activeOnly, bool? returnPassword = null)
{
@ -35,6 +35,21 @@ namespace Surge365.MassEmailReact.Server.Controllers
var server = await _serverService.GetByIdAsync(id, returnPasswordValue);
return server is not null ? Ok(server) : NotFound($"Server with key '{id}' not found.");
}
[HttpPost()]
public async Task<IActionResult> CreateServer([FromBody] ServerUpdateDto serverUpdateDto)
{
if (serverUpdateDto.Id != null && serverUpdateDto.Id > 0)
return BadRequest("Id must be null or 0");
var serverId = await _serverService.CreateAsync(serverUpdateDto);
if (serverId == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create server.");
var createdServer = await _serverService.GetByIdAsync(serverId.Value);
return Ok(createdServer);
}
[HttpPut("{id}")]
public async Task<IActionResult> UpdateServer(int id, [FromBody] ServerUpdateDto serverUpdateDto)
{

View File

@ -5,11 +5,9 @@ using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TargetsController : ControllerBase
public class TargetsController : BaseController
{
private readonly ITargetService _targetService;
@ -33,14 +31,14 @@ namespace Surge365.MassEmailReact.Server.Controllers
return target is not null ? Ok(target) : NotFound($"Target with key '{id}' not found.");
}
[HttpPost()]
public async Task<IActionResult> CreateTarget(int id, [FromBody] TargetUpdateDto targetUpdateDto)
public async Task<IActionResult> CreateTarget([FromBody] TargetUpdateDto targetUpdateDto)
{
if (targetUpdateDto.Id != null && targetUpdateDto.Id > 0)
return BadRequest("Id must be null or 0");
var targetId = await _targetService.CreateAsync(targetUpdateDto);
if (targetId == null)
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to craete target.");
return StatusCode(StatusCodes.Status500InternalServerError, "Failed to create target.");
var createdTarget = await _targetService.GetByIdAsync(targetId.Value);

View File

@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TemplatesController : ControllerBase
public class TemplatesController : BaseController
{
private readonly ITemplateService _templateService;

View File

@ -3,11 +3,9 @@ using Microsoft.AspNetCore.Mvc;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class TestEmailListsController : ControllerBase
public class TestEmailListsController : BaseController
{
private readonly ITestEmailListService _testEmailListService;

View File

@ -4,11 +4,9 @@ using Surge365.MassEmailReact.Application.DTOs;
using Surge365.MassEmailReact.Application.Interfaces;
using Surge365.MassEmailReact.Domain.Entities;
namespace Surge365.MassEmailReact.Server.Controllers
namespace Surge365.MassEmailReact.API.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class UnsubscribeUrlsController : ControllerBase
public class UnsubscribeUrlsController : BaseController
{
private readonly IUnsubscribeUrlService _unsubscribeUrlService;
@ -31,7 +29,7 @@ namespace Surge365.MassEmailReact.Server.Controllers
return unsubscribeUrl is not null ? Ok(unsubscribeUrl) : NotFound($"UnsubscribeUrl with key '{id}' not found.");
}
[HttpPost()]
public async Task<IActionResult> Create(int id, [FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto)
public async Task<IActionResult> Create([FromBody] UnsubscribeUrlUpdateDto unsubscribeUrlUpdateDto)
{
if (unsubscribeUrlUpdateDto.Id != null && unsubscribeUrlUpdateDto.Id > 0)
return BadRequest("Id must be null or 0");

View File

@ -46,7 +46,6 @@ app.UseAuthorization();
app.MapControllers();
app.MapFallbackToFile("/index.html");
DapperConfiguration.ConfigureMappings();

View File

@ -1,6 +1,11 @@
@Surge365.MassEmailReact.Server_HostAddress = http://localhost:5065
@Surge365.MassEmailReact.API_HostAddress = http://localhost:5065/api
@Surge365.MassEmailReact.UATServer_HostAddress = https://uat.massemail2.surge365.com/api
GET {{Surge365.MassEmailReact.Server_HostAddress}}/weatherforecast/
GET {{Surge365.MassEmailReact.API_HostAddress}}/servers/
Accept: application/json
###
GET {{Surge365.MassEmailReact.UATServer_HostAddress}}/servers/get
Accept: application/json
###

View File

@ -11,6 +11,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
{
Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
Task<int?> CreateAsync(Server server);
Task<bool> UpdateAsync(Server server);
}
}

View File

@ -6,6 +6,7 @@ namespace Surge365.MassEmailReact.Application.Interfaces
{
Task<Server?> GetByIdAsync(int id, bool returnPassword = false);
Task<List<Server>> GetAllAsync(bool activeOnly = true, bool returnPassword = false);
Task<int?> CreateAsync(ServerUpdateDto targetDto);
Task<bool> UpdateAsync(ServerUpdateDto serverDto);
}
}

View File

@ -66,6 +66,35 @@ namespace Surge365.MassEmailReact.Infrastructure.Repositories
return servers;
}
public async Task<int?> CreateAsync(Server server)
{
ArgumentNullException.ThrowIfNull(server);
if (server.Id != null && server.Id > 0)
throw new Exception("ID must be null");
using SqlConnection conn = new SqlConnection(_config.GetConnectionString(_connectionStringName));
var parameters = new DynamicParameters();
parameters.Add("@server_key", dbType: DbType.Int32, direction: ParameterDirection.Output);
parameters.Add("@name", server.Name, DbType.String);
parameters.Add("@server_name", server.ServerName, DbType.String);
parameters.Add("@port", server.Port, DbType.Int16);
parameters.Add("@username", server.Username, DbType.String);
parameters.Add("@password", server.Password, DbType.String);
// Output parameter
parameters.Add("@success", dbType: DbType.Boolean, direction: ParameterDirection.Output);
await conn.ExecuteAsync("mem_save_server", parameters, commandType: CommandType.StoredProcedure);
// Retrieve the output parameter value
bool success = parameters.Get<bool>("@success");
if (success)
return parameters.Get<int>("@server_key");
return null;
}
public async Task<bool> UpdateAsync(Server server)
{
ArgumentNullException.ThrowIfNull(server);

View File

@ -10,6 +10,7 @@ using Microsoft.IdentityModel.Tokens;
using Microsoft.Extensions.Configuration;
using Surge365.MassEmailReact.Domain.Entities;
using System.Security.Cryptography;
using Surge365.MassEmailReact.Infrastructure.Repositories;
namespace Surge365.MassEmailReact.Infrastructure.Services
@ -33,6 +34,22 @@ namespace Surge365.MassEmailReact.Infrastructure.Services
{
return await _serverRepository.GetAllAsync(activeOnly, returnPassword);
}
public async Task<int?> CreateAsync(ServerUpdateDto serverDto)
{
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));
if (serverDto.Id != null && serverDto.Id > 0)
throw new Exception("ID must be null");
var server = new Server();
server.Name = serverDto.Name;
server.ServerName = serverDto.ServerName;
server.Port = serverDto.Port;
server.Username = serverDto.Username;
server.Password = serverDto.Password;
return await _serverRepository.CreateAsync(server);
}
public async Task<bool> UpdateAsync(ServerUpdateDto serverDto)
{
ArgumentNullException.ThrowIfNull(serverDto, nameof(serverDto));

View File

@ -9,7 +9,9 @@
<BuildOutputFolder>$(MSBuildProjectDirectory)\dist</BuildOutputFolder>
</PropertyGroup>
<ItemGroup>
<None Remove="dist\**" />
<None Remove="public\content\lib\**" />
<TypeScriptConfiguration Remove="dist\**" />
<TypeScriptConfiguration Remove="public\content\lib\**" />
</ItemGroup>
<ItemGroup>

View File

@ -20,8 +20,6 @@
"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-bootstrap": "^2.10.9",
"react-dom": "^19.0.0",
@ -2035,19 +2033,6 @@
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
"license": "MIT"
},
"node_modules/@stencil/core": {
"version": "4.26.0",
"resolved": "https://registry.npmjs.org/@stencil/core/-/core-4.26.0.tgz",
"integrity": "sha512-+0Inu+dJ9/LgWSskcZwx7v17v4GILcwIYxNgD+OuK0U+D5z61WsxWw7yHkYG5OqGPBijsJMVssYRx/Tn+e7F9A==",
"license": "MIT",
"bin": {
"stencil": "bin/stencil"
},
"engines": {
"node": ">=16.0.0",
"npm": ">=7.10.0"
}
},
"node_modules/@swc/helpers": {
"version": "0.5.15",
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
@ -3345,15 +3330,6 @@
"dev": true,
"license": "ISC"
},
"node_modules/font-awesome": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/font-awesome/-/font-awesome-4.7.0.tgz",
"integrity": "sha512-U6kGnykA/6bFmg1M/oT9EkFeIYv7JlX3bozwQJWiiLz6L0w3F5vBVPxHlwyX/vtNq1ckcpRKOB9f2Qal/VtFpg==",
"license": "(OFL-1.1 AND MIT)",
"engines": {
"node": ">=0.10.3"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@ -3506,15 +3482,6 @@
"loose-envify": "^1.0.0"
}
},
"node_modules/ionicons": {
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/ionicons/-/ionicons-7.4.0.tgz",
"integrity": "sha512-ZK94MMqgzMCPPMhmk8Ouu6goyVHFIlw/ACP6oe3FrikcI0N7CX0xcwVaEbUc0G/v3W0shI93vo+9ve/KpvcNhQ==",
"license": "MIT",
"dependencies": {
"@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",

View File

@ -23,8 +23,6 @@
"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-bootstrap": "^2.10.9",
"react-dom": "^19.0.0",

View File

@ -228,7 +228,23 @@ const Layout = ({ children }: LayoutProps) => {
{ text: 'Completed Mailings', icon: <CheckCircleIcon />, path: '/completedMailings' },
].map((item) => (
<ListItem key={item.text} disablePadding>
<ListItemButton component={RouterLink} to={item.path} onClick={() => isMobile && handleDrawerClose()}
<ListItemButton
component={RouterLink}
to={item.path}
selected={location.pathname === item.path}
onClick={() => isMobile && handleDrawerClose()}
sx={{
'&.Mui-selected': {
backgroundColor: 'primary.main',
color: 'primary.contrastText',
'& .MuiListItemIcon-root': {
color: 'primary.contrastText',
},
},
'&.Mui-selected:hover': {
backgroundColor: 'primary.dark',
},
}}
>
<ListItemIcon>{item.icon}</ListItemIcon>
<ListItemText primary={item.text} />

View File

@ -1,29 +1,13 @@
import { ReactNode } from 'react';
//import { useEffect } 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 '@/css/adminlte-custom.css';
import '@/css/surge365.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import 'admin-lte/dist/js/adminlte.min.js';
import 'admin-lte/dist/js/adminlte.min.js';
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
return children;
}
};
LayoutLogin.propTypes = {
children: PropTypes.any
children: PropTypes.any,
};
export default LayoutLogin;

View File

@ -1,37 +0,0 @@
import { ReactNode } from 'react';
//import { useEffect } from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async';
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 '@/css/adminlte-custom.css';
import '@/css/surge365.css';
import 'bootstrap/dist/js/bootstrap.bundle.min.js'; // Bootstrap JS
import 'admin-lte/dist/js/adminlte.min.js';
import 'admin-lte/dist/js/adminlte.min.js';
const LayoutLogin = function LayoutLogin({ children }: { children: ReactNode }) {
return (
<HelmetProvider>
<Helmet>
</Helmet>
{children}
</HelmetProvider>
);
}
LayoutLogin.propTypes = {
children: PropTypes.any
};
export default LayoutLogin;

View File

@ -1,120 +0,0 @@
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 (
<div className="wrapper" style={{ overflow: 'initial' }}>
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
<script src="/content/js/jquery.surge365.global.js?36"></script>
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
<script src="/content/js/Main-1.0.js?39"></script>
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
<header className="main-header">
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
</span>
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
</span>
</a>
<nav className="navbar navbar-static-top">
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
<span className="sr-only">Toggle navigation</span>
</a>
<div className="navbar-custom-menu">
<ul className="nav navbar-nav">
<li className="dropdown user user-menu">
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
<span id="spanProfileName"></span>
</a>
<ul className="dropdown-menu">
<li className="user-header">
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
<p>
<span id="spanProfileNameTitle"></span>
</p>
</li>
<li className="user-footer">
<div className="pull-left">
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
</div>
<div className="pull-right">
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
</div>
</li>
</ul>
</li>
</ul>
</div>
</nav>
</header>
<aside className="main-sidebar">
<section className="sidebar">
<div className="user-panel">
<div className="pull-left image">
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
</div>
<div className="pull-left info" style={{ lineHeight: 3 }}>
<p><span id="spanMenuName"></span></p>
</div>
</div>
<ul className="sidebar-menu">
<li className="header">MAIN NAVIGATION</li>
<li id="liDashboard" className="active treeview">
<a href="Dashboard">
<i className="fa fa-home"></i><span>Dashboard</span>
</a>
</li>
<li className="treeview">
<a href="#" id="aSignOut">
<i className="fa fa-times-circle"></i>
<span>Logout</span>
</a>
</li>
</ul>
</section>
</aside>
{children}
<footer className="main-footer" style={{ padding: '2px' }}>
<div className="pull-right hidden-xs">
<b>Version</b> 1.0.0
</div>
<strong>Copyright &copy; 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
</footer>
</div>
);
}
Layout.propTypes = {
children: PropTypes.any
};
export default Layout;

View File

@ -1,125 +0,0 @@
import { ReactNode } from 'react';
//import React from 'react';
import { Helmet, HelmetProvider } from 'react-helmet-async';
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 (
<HelmetProvider>
<div className="wrapper" style={{ overflow: 'initial' }}>
<Helmet>
<link rel="stylesheet" href="/content/dist/css/skins/_all-skins.min.css" />
<link href="/content/plugins/datepicker/v4/bootstrap-datetimepicker.css" rel="stylesheet" />
<link rel="stylesheet" href="/content/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css" />
<link rel="stylesheet" href="/content/plugins/datatables/dataTables.bootstrap.css" />
{/*<script src="/content/dist/js/app.min.js"></script>AdminLTE?*/}
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.2/moment.min.js"></script>
{/*<script src="/content/plugins/datatables/jquery.dataTables.min.js"></script>*/}
{/*<script src="/content/plugins/validator/validator.js"></script>*/}
<script src="/content/js/jquery.surge365.utilities-1.0.js" />
<script src="/content/js/jquery.surge365.global.js?36"></script>
<script src="/content/js/jquery.surge365.webmethods-1.0.js?15"></script>
<script src="/content/js/Main-1.0.js?39"></script>
{/*<script src="/content/plugins/growl/jquery.bootstrap-growl.min.js"></script>*/}
{/*<script src="/content/plugins/input-mask-5/jquery.inputmask.js"></script>*/}
{/*<script src="/content/plugins/printThis/printThis.js"></script>*/}
{/*<script src="/content/plugins/datatables/dataTables.bootstrap.min.js"></script>*/}
</Helmet>
<header className="main-header">
<a href="Dashboard" className="logo" style={{ backgroundColor: '#333' }}>
<span className="logo-mini" style={{ backgroundColor: '#333' }}>
<img id="imgLogo-sm" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
</span>
<span className="logo-lg" style={{ textAlign: 'center', backgroundColor: '#333' }}>
<img id="imgLogo-lg" src="/content/img/imove_mini_logo.png" style={{ height: '35px', marginBottom: '3px' }} alt="Logo" className="hide" />
<span style={{ marginLeft: '5px', verticalAlign: 'middle' }}><b>USA</b>Haulers</span>
</span>
</a>
<nav className="navbar navbar-static-top">
<a href="#" className="fa5 sidebar-toggle" data-toggle="offcanvas" role="button">
<span className="sr-only">Toggle navigation</span>
</a>
<div className="navbar-custom-menu">
<ul className="nav navbar-nav">
<li className="dropdown user user-menu">
<a href="#" className="dropdown-toggle" data-toggle="dropdown">
<span id="spanSpinner" className="fa fa-sync-alt pull-left" style={{ lineHeight: 'unset', display: 'none', fontSize: '16pt', fontWeight: 'bold', marginRight: '10px' }}></span>
<img id="imgRightProfile" src="/content/img/generic_avatar.jpg" className="user-image" alt="User Image" />
<span id="spanProfileName"></span>
</a>
<ul className="dropdown-menu">
<li className="user-header">
<img id="imgMainProfile" src="" className="img-circle" alt="User Image" />
<p>
<span id="spanProfileNameTitle"></span>
</p>
</li>
<li className="user-footer">
<div className="pull-left">
<a href="Profile" className="btn btn-default btn-flat">Profile</a>
</div>
<div className="pull-right">
<input type="button" id="btnSignOut" className="btn btn-primary" value="Sign Out" />
</div>
</li>
</ul>
</li>
</ul>
</div>
</nav>
</header>
<aside className="main-sidebar">
<section className="sidebar">
<div className="user-panel">
<div className="pull-left image">
<img id="imgLeftProfile" src="/content/img/generic_avatar.jpg" className="img-circle" alt="User Image" />
</div>
<div className="pull-left info" style={{ lineHeight: 3 }}>
<p><span id="spanMenuName"></span></p>
</div>
</div>
<ul className="sidebar-menu">
<li className="header">MAIN NAVIGATION</li>
<li id="liDashboard" className="active treeview">
<a href="Dashboard">
<i className="fa fa-home"></i><span>Dashboard</span>
</a>
</li>
<li className="treeview">
<a href="#" id="aSignOut">
<i className="fa fa-times-circle"></i>
<span>Logout</span>
</a>
</li>
</ul>
</section>
</aside>
{children}
<footer className="main-footer" style={{ padding: '2px' }}>
<div className="pull-right hidden-xs">
<b>Version</b> 1.0.0
</div>
<strong>Copyright &copy; 2024 <a href="https://www.surge365.com">Surge365</a>.</strong> All rights reserved.
</footer>
</div>
</HelmetProvider>
);
}
Layout.propTypes = {
children: PropTypes.any
};
export default Layout;

View File

@ -23,7 +23,7 @@ type EmailDomainEditProps = {
};
const schema = yup.object().shape({
id: yup.number(),
id: yup.number().default(0),
name: yup
.string()
.required("Name is required")
@ -37,7 +37,7 @@ const schema = yup.object().shape({
emailAddress: yup.string().email("Invalid email").required("Email address is required"),
username: yup.string().required("Username is required"),
password: yup.string().default("")
.test("required-if-new", "NamePassword is required", function (value) {
.test("required-if-new", "Password is required", function (value) {
if (this.parent.id > 0) return true;
else return value.length > 0;
}),
@ -60,7 +60,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
const setupData: SetupData = useSetupData();
const originalEmailDomain: EmailDomain | null = emailDomain ? { ...emailDomain } : null;
const { register, trigger, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({
const { register, control, handleSubmit, reset, formState: { errors } } = useForm<EmailDomain>({
mode: "onBlur",
defaultValues: emailDomain || defaultEmailDomain,
resolver: yupResolver(schema) as Resolver<EmailDomain>,
@ -89,7 +89,7 @@ const EmailDomainEdit = ({ open, emailDomain, onClose, onSave }: EmailDomainEdit
if (!response.ok) throw new Error(isNew ? "Failed to create" : "Failed to update");
const updatedEmailDomain = await response.json();
onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password);
onSave(updatedEmailDomain, isNew || formData.password ? formData.password : originalEmailDomain?.password ?? "");
onClose();
} catch (error) {
console.error("Update error:", error);

View File

@ -1,6 +1,6 @@
import { useState, FormEvent } from 'react';
import { Modal, Button, Form } from 'react-bootstrap';
import { FaExclamationCircle } from 'react-icons/fa'; // For optional font icon
import { Dialog, DialogTitle, DialogContent, DialogActions, Button, TextField, Typography, Box, IconButton } from '@mui/material';
import { Close as CloseIcon } from '@mui/icons-material';
import utils from '@/ts/utils';
type FormErrors = Record<string, string>;
@ -25,9 +25,8 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
if (Object.keys(errors).length > 0) {
setFormErrors(errors);
return false;
} else {
return true;
}
return true;
};
const handleStartPasswordRecovery = async (e: FormEvent) => {
@ -52,48 +51,51 @@ const ForgotPasswordModal: React.FC<ForgotPasswordModalProps> = ({ show, onClose
};
return (
<Modal show={show} onHide={onClose} backdrop="static" keyboard={false} centered animation={false}>
<Modal.Header closeButton>
<Modal.Title>Forgot your password?</Modal.Title>
</Modal.Header>
<Modal.Body>
<Dialog open={show} onClose={() => { }}>
<DialogTitle>
Forgot your password?
<IconButton onClick={onClose} sx={{ position: 'absolute', right: 8, top: 8 }}>
<CloseIcon />
</IconButton>
</DialogTitle>
<DialogContent>
{usernameNotFound && (
<span>An email has been sent to the address you provided. Please follow the instructions to reset your password.</span>
<Typography color="error" sx={{ mb: 2 }}>
An email has been sent to the address you provided. Please follow the instructions to reset your password.
</Typography>
)}
{!recoveryStarted && (
<Form onSubmit={handleStartPasswordRecovery}>
<Form.Group controlId="formForgotEmail" className="position-relative mb-3">
<Form.Label className="mb-4 text-center">Enter your email address below and we'll send you instructions on how to reset your password...</Form.Label>
<Form.Label className="visually-hidden">Email</Form.Label>
<Form.Control
type="username"
placeholder="Username"
<form onSubmit={handleStartPasswordRecovery}>
<Box sx={{ mb: 2 }}>
<Typography variant="body1" sx={{ mb: 2, textAlign: 'center' }}>
Enter your username below and we'll send you instructions on how to reset your password...
</Typography>
<TextField
label="Username"
variant="outlined"
fullWidth
value={username}
isInvalid={!!formErrors.username}
onChange={(e) => setUsername(e.target.value)}
error={!!formErrors.username}
helperText={formErrors.username}
required
autoFocus
size="lg"
size="small"
/>
{formErrors.username && (
<FaExclamationCircle className="validation-icon text-danger" title={formErrors.username} />
)}
<Form.Control.Feedback type="invalid">{formErrors.username}</Form.Control.Feedback>
</Form.Group>
<Button variant="primary" className="bg-orange btn-flat w-100" type="submit">
</Box>
<Button type="submit" variant="contained" color="primary" fullWidth>
Submit
</Button>
</Form>
</form>
)}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={onClose}>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="secondary">
Close
</Button>
</Modal.Footer>
</Modal>
</DialogActions>
</Dialog>
);
};
export default ForgotPasswordModal;

View File

@ -9,35 +9,72 @@ import {
} from "@mui/material";
import Server from "@/types/server";
import { useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
type ServerEditProps = {
open: boolean;
server: Server;
server: Server | null;
onClose: () => void;
onSave: (updatedServer: Server) => void;
onSave: (updatedServer: Server, password: string) => void;
};
const schema = yup.object().shape({
id: yup.number().default(0),
name: yup
.string()
.required("Name is required")
.test("unique-name", "Name must be unique", function (value) {
const setupData = this.options.context?.setupData as { servers: Server[] };
if (!setupData) return true;
return !setupData.servers.some(
(d) => d.name.toLowerCase() === value?.toLowerCase() && (d.id === 0 || d.id !== this.parent.id)
);
}),
serverName: yup.string().required("Server name is required"),
port: yup.number().default(1433),
username: yup.string().required("Username is required"),
password: yup.string().default("")
.test("required-if-new", "Password is required", function (value) {
if (this.parent.id > 0) return true;
else return value.length > 0;
})
});
const defaultServer: Server = {
id: 0,
name: "",
serverName: "",
port: 1433,
username: "",
password: "",
};
const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
const [formData, setFormData] = useState<Server>({ ...server });
//const [serverError, setServerError] = useState(false); // Track validation
const isNew = !server || server.id === 0;
const originalServer: Server | null = server ? { ...server } : null;
//const [formData, setFormData] = useState<Server>({ ...server });
const { register, handleSubmit, reset, formState: { errors } } = useForm<Server>({
mode: "onBlur",
defaultValues: server || defaultServer,
resolver: yupResolver(schema)
});
const [loading, setLoading] = useState(false);
useEffect(() => { //Reset form to unedited state on open or server change
useEffect(() => {
if (open) {
setFormData(server);
reset(server || defaultServer, { keepDefaultValues: true });
}
}, [open, server]);
}, [open, server, reset]);
const handleChange = (field: string, value: any) => {
setFormData((prev) => ({ ...prev, [field]: value || "" }));
//if (field === "serverId" && value) setServerError(false);
};
const handleSave = async () => {
const handleSave = async (formData: Server) => {
const apiUrl = isNew ? "/api/servers" : `/api/servers/${formData.id}`;
const method = isNew ? "POST" : "PUT";
setLoading(true);
try {
const response = await fetch(`/api/servers/${formData.id}`, {
method: "PUT",
const response = await fetch(apiUrl, {
method: method,
headers: { "Content-Type": "application/json" },
body: JSON.stringify(formData),
});
@ -45,7 +82,7 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
if (!response.ok) throw new Error("Failed to update");
const updatedServer = await response.json();
onSave(updatedServer); // Update UI optimistically
onSave(updatedServer, isNew || formData.password ? formData.password : originalServer?.password ?? "");
onClose();
} catch (error) {
console.error("Update error:", error);
@ -56,47 +93,52 @@ const ServerEdit = ({ open, server, onClose, onSave }: ServerEditProps) => {
return (
<Dialog open={open} onClose={onClose} maxWidth="sm" fullWidth>
<DialogTitle>Edit Server - id={formData.id}</DialogTitle>
<DialogTitle>{isNew ? "Add Server" : "Edit Server id=" + server?.id}</DialogTitle>
<DialogContent>
<TextField
{...register("name")}
label="Name"
fullWidth
value={formData.name}
onChange={(e) => handleChange("name", e.target.value)}
margin="dense"
error={!!errors.name}
helperText={errors.name?.message}
/>
<TextField
{...register("serverName")}
label="Server Name"
fullWidth
value={formData.serverName}
onChange={(e) => handleChange("serverName", e.target.value)}
margin="dense"
error={!!errors.serverName}
helperText={errors.serverName?.message}
/>
<TextField
{...register("port")}
label="Port"
fullWidth
value={formData.port}
onChange={(e) => handleChange("port", e.target.value)}
margin="dense"
error={!!errors.port}
helperText={errors.port?.message}
/>
<TextField
{...register("username")}
label="Username"
fullWidth
value={formData.username}
onChange={(e) => handleChange("username", e.target.value)}
margin="dense"
error={!!errors.username}
helperText={errors.username?.message}
/>
<TextField
{...register("password")}
label="Password"
fullWidth
value={formData.password}
onChange={(e) => handleChange("password", e.target.value)}
margin="dense"
error={!!errors.password}
helperText={errors.password?.message}
/>
</DialogContent>
<DialogActions>
<Button onClick={onClose} disabled={loading}>Cancel</Button>
<Button onClick={handleSave} color="primary" disabled={loading}>
<Button onClick={handleSubmit(handleSave)} color="primary" disabled={loading}>
{loading ? "Saving..." : "Save"}
</Button>
</DialogActions>

View File

@ -94,8 +94,8 @@ const TemplateEdit = ({ open, template, onClose, onSave }: TemplateEditProps) =>
const handleSave = async (formData: Template) => {
const domain = setupData.emailDomains.find(el => el.id === formData.domainId);
formData.fromEmail = domain.emailAddress;
formData.replyToEmail = domain.emailAddress;
formData.fromEmail = domain?.emailAddress ?? "";
formData.replyToEmail = domain?.emailAddress ?? "";
const apiUrl = isNew ? "/api/templates" : `/api/templates/${formData.id}`;
const method = isNew ? "POST" : "PUT";
setLoading(true);

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton, GridDeleteIcon } from '@mui/x-data-grid';
import BouncedEmail from '@/types/bouncedEmail';
import BouncedEmailEdit from "@/components/modals/BouncedEmailEdit";
@ -22,10 +24,10 @@ function BouncedEmails() {
sortable: false,
renderCell: (params: GridRenderCellParams<BouncedEmail>) => (
<div>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
<IconButton onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleDelete(params.row); }}>
<GridDeleteIcon />
</IconButton>
</div>
@ -111,22 +113,12 @@ function BouncedEmails() {
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => handleNew()}
sx={{ marginRight: 2 }}
>
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadBouncedEmails()}
sx={{ marginRight: 2 }}
>
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadBouncedEmails()} sx={{ marginLeft: 1 }}>
{setupData.bouncedEmailsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />

View File

@ -1,6 +1,8 @@
import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { Lock, LockOpen } from "@mui/icons-material";
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
@ -21,12 +23,13 @@ function EmailDomains() {
const columns: GridColDef<EmailDomain>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<EmailDomain>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
Edit
</Button>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
),
},
{ field: "id", headerName: "ID", width: 60 },
@ -40,7 +43,7 @@ function EmailDomains() {
renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}>
Password
<IconButton size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}>
<IconButton color="primary" size="small" onClick={(e) => { e.stopPropagation(); togglePasswordVisibility(); }} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton>
</div>
@ -100,7 +103,7 @@ function EmailDomains() {
: [...prev, updatedEmailDomain];
});
};
const displayRows = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains;
const displayRows: EmailDomain[] = isPasswordVisible ? (emailDomainsWithPasswords ?? setupData.emailDomains) : setupData.emailDomains;
return (
<Box ref={gridContainerRef} sx={{
@ -133,7 +136,7 @@ function EmailDomains() {
<Typography variant="body2">Display Order: {row.displayOrder}</Typography>
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
</CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon />
</IconButton>
</Box>
@ -150,26 +153,12 @@ function EmailDomains() {
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => handleNew()}
sx={{ marginRight: 2 }}
>
Add New
</Button>
<Button
variant="contained"
color="primary"
onClick={() => {
if (isPasswordVisible)
loadEmailDomainsWithPasswords();
setupData.reloadEmailDomains();
}}
sx={{ marginRight: 2 }}
>
{setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadEmailDomains()} sx={{ marginLeft: 1 }}>
{setupData.emailDomainsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />

View File

@ -1,10 +1,16 @@
import { useState } from 'react';
import { Button, Form, Spinner } from 'react-bootstrap';
import {
Button,
TextField,
CircularProgress,
Container,
Typography,
Box,
Alert,
} from '@mui/material';
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';
//import ForgotPasswordModal from '@/components/modals/ForgotPasswordModal';
type SpinnerState = Record<string, boolean>;
type FormErrors = Record<string, string>;
@ -15,17 +21,10 @@ function Login() {
const [formErrors, setFormErrors] = useState<FormErrors>({});
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
//const [user, setUser] = useState<User | null>(null);
//const [showForgotPasswordModal, setShowForgotPasswordModal] = useState(false);
const [loginError, setLoginError] = useState<boolean>(false);
const [loginErrorMessage, setLoginErrorMessage] = useState<string>('');
//const setSpinners = (newValues: Partial<SpinnerState>) => {
// setSpinnersState((prevSpinners) => ({
// ...prevSpinners,
// ...newValues,
// }));
//};
const setSpinners = (newValues: Partial<SpinnerState>) => {
setSpinnersState((prevSpinners) => {
const updatedSpinners: SpinnerState = { ...prevSpinners };
@ -38,23 +37,19 @@ function Login() {
});
};
const handleCloseForgotPasswordModal = () => {
setShowForgotPasswordModal(false);
};
//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);
}
@ -63,14 +58,15 @@ function Login() {
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setIsLoading(true);
spinners.Login = true;
setSpinners(spinners);
setSpinners({ Login: true });
validateLoginForm();
if (Object.keys(formErrors).length > 0) {
setIsLoading(false);
setSpinners({ Login: false });
return;
}
if (Object.keys(formErrors).length > 0) return;
//setUser(null);
setLoginError(false);
setLoginErrorMessage('');
let loggedInUser: User | null = null;
@ -83,45 +79,39 @@ function Login() {
success: (json: AuthResponse) => {
try {
loggedInUser = json.user;
//setUser(loggedInUser);
}
catch {
const errorMsg: string = "Unexpected Error";
} catch {
const errorMsg: string = 'Unexpected Error';
hadLoginError = true;
hadLoginErrorMessage = errorMsg;
}
},
error: (err: unknown) => {
let errorMsg: string = "Unexpected Error";
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);
const errorResponse = err as AuthErrorResponse;
if (errorResponse.data?.message) {
errorMsg = errorResponse.data.message;
}
console.error(errorMsg);
setLoginErrorMessage(errorMsg);
}
hadLoginError = true;
hadLoginErrorMessage = errorMsg;
}
},
});
if (hadLoginError) {
setLoginErrorMessage(hadLoginErrorMessage);
setLoginError(true);
setIsLoading(false);
spinners.Login = false;
setSpinners(spinners);
setSpinners({ Login: false });
return;
}
if (loggedInUser == null) {
setLoginError(true);
setIsLoading(false);
spinners.Login = false;
setSpinners(spinners);
setSpinners({ Login: false });
} else {
await finishUserLogin(loggedInUser);
}
@ -129,15 +119,13 @@ function Login() {
const finishUserLogin = async (loggedInUser: User) => {
setIsLoading(false);
spinners.Login = false;
spinners.LoginWithPasskey = false;
setSpinners(spinners);
setSpinners({ Login: false, LoginWithPasskey: false });
utils.localStorage("session_currentUser", loggedInUser);
utils.localStorage('session_currentUser', loggedInUser);
const redirectUrl = utils.sessionStorage("redirect_url");
const redirectUrl = utils.sessionStorage('redirect_url');
if (redirectUrl) {
utils.sessionStorage("redirect_url", null);
utils.sessionStorage('redirect_url', null);
document.location.href = redirectUrl;
} else {
document.location.href = '/home';
@ -145,54 +133,88 @@ function Login() {
};
return (
<div className="container">
<div className="row text-center mt-5">
<h1>surge365 - React</h1>
</div>
<div className="row text-center" style={{ maxWidth: '400px', margin: 'auto' }}>
<h3 className="form-signin-heading mt-3 mb-1">Please sign in</h3>
<Form id="frmLogin" onSubmit={handleLogin}>
<Container maxWidth="sm">
{/* Main heading */}
<Box sx={{ textAlign: 'center', mt: 5 }}>
<Typography variant="h4">Surge 365 Mass Email 2</Typography>
</Box>
{/* Login form */}
<Box sx={{ maxWidth: 400, margin: 'auto', mt: 3 }}>
<Typography variant="h5" align="center">
Please sign in
</Typography>
<form onSubmit={handleLogin}>
{/* Login error message */}
{loginError && (
<Form.Label style={{ color: 'red' }}>{loginErrorMessage ?? "Login error"}</Form.Label>
<Alert severity="error" sx={{ mb: 2 }}>
{loginErrorMessage || 'Login error'}
</Alert>
)}
<Form.Group className="mb-3" controlId="txtUsernamel">
<Form.Label className="visually-hidden">Username</Form.Label>
<Form.Control
type="username"
placeholder="Username"
value={username}
onChange={(e) => setUsername(e.target.value)}
required
autoFocus
size="sm"
/>
{spinners.Username && <Spinner animation="border" size="sm" />}
</Form.Group>
<Form.Group className="mb-3" controlId="txtPassword">
<Form.Label className="visually-hidden">Password</Form.Label>
<Form.Control
type="password"
placeholder="Password"
value={password}
onChange={(e) => setPassword(e.target.value)}
required
size="sm"
/>
</Form.Group>
{/* Username field */}
<TextField
label="Username"
variant="outlined"
fullWidth
margin="normal"
value={username}
onChange={(e) => setUsername(e.target.value)}
error={!!formErrors.username}
helperText={formErrors.username}
required
autoFocus
size="small"
/>
<Button className="bg-orange w-100" type="submit" disabled={isLoading}>
{spinners.Login && <Spinner animation="border" size="sm" className="me-2" />}
{/* Password field */}
<TextField
label="Password"
type="password"
variant="outlined"
fullWidth
margin="normal"
value={password}
onChange={(e) => setPassword(e.target.value)}
error={!!formErrors.password}
helperText={formErrors.password}
required
size="small"
/>
{/* Sign in button */}
<Button
type="submit"
variant="contained"
color="primary"
fullWidth
disabled={isLoading}
startIcon={
isLoading && spinners.Login ? <CircularProgress size={24} color="inherit" /> : null
}
sx={{ mt: 2 }}
>
{isLoading && spinners.Login ? 'Signing in...' : 'Sign in'}
</Button>
<Button variant="secondary" className="w-100 mt-2" onClick={() => setShowForgotPasswordModal(true)}>
Forgot Password
</Button>
</Form>
</div>
<ForgotPasswordModal show={showForgotPasswordModal} onClose={handleCloseForgotPasswordModal} />
</div>
{/* Forgot password button */}
{/*<Button*/}
{/* variant="outlined"*/}
{/* fullWidth*/}
{/* onClick={() => setShowForgotPasswordModal(true)}*/}
{/* sx={{ mt: 1 }}*/}
{/*>*/}
{/* Forgot Password*/}
{/*</Button>*/}
</form>
</Box>
{/* Forgot password modal */}
{/*<ForgotPasswordModal*/}
{/* show={showForgotPasswordModal}*/}
{/* onClose={handleCloseForgotPasswordModal}*/}
{/*/>*/}
</Container>
);
}

View File

@ -2,7 +2,9 @@
import { useSetupData, SetupData } from "@/context/SetupDataContext";
//import Typography from '@mui/material/Typography';
import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import { Lock, LockOpen } from "@mui/icons-material";
//import utils from '@/ts/utils';
@ -15,7 +17,7 @@ function Servers() {
const setupData: SetupData = useSetupData();
const isMobile = useMediaQuery(theme.breakpoints.down("sm"));
const [servers, setServers] = useState<Server[] | null>(null);
const [serversWithPasswords, setServersWithPasswords] = useState<Server[] | null>(null);
const gridContainerRef = useRef<HTMLDivElement | null>(null);
const [selectedRow, setSelectedRow] = useState<Server | null>(null);
const [open, setOpen] = useState<boolean>(false);
@ -24,14 +26,14 @@ function Servers() {
const togglePasswordVisibility = async () => {
if (isPasswordVisible) {
setIsPasswordVisible(false);
setServers(setupData.servers);
setServersWithPasswords(setupData.servers);
}
else {
try {
setIsPasswordVisible(true);
const serversResponse = await fetch("/api/servers/GetAll?activeOnly=false&returnPassword=true");
const serversData = await serversResponse.json();
setServers(serversData);
setServersWithPasswords(serversData);
}
catch (error) {
console.error("Error fetching servers:", error);
@ -42,12 +44,13 @@ function Servers() {
const columns: GridColDef<Server>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<Server>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
Edit
</Button>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
),
},
{ field: "id", headerName: "ID", width: 60 },
@ -62,7 +65,7 @@ function Servers() {
renderHeader: () => (
<div style={{ display: "flex", alignItems: "center" }}>
Password
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
<IconButton size="small" color="primary" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton>
</div>
@ -73,21 +76,49 @@ function Servers() {
];
const handleNew = () => {
setSelectedRow(null);
setOpen(true);
};
const handleEdit = (row: GridRowModel<Server>) => {
setSelectedRow(row);
setOpen(true);
};
const handleUpdateRow = (updatedRow: Server) => {
setupData.setServers(updatedRow);
updateServers(updatedRow);
//const handleSaveRow = (updatedRow: Server, password: string) => {
// setupData.setServersWithPasswords(updatedRow);
// if (isPasswordVisible) {
// updateServers
// }
// updateServers(updatedRow);
//};
//const updateServers = (updatedServer: Server) => {
// setServersWithPasswords((prevServers) => {
// if (prevServers == null) return null;
// return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
// });
//};
const handleSaveRow = (savedRow: Server, password: string) => {
setupData.setServers(savedRow);
if (isPasswordVisible) {
if (password)
updateServers({ ...savedRow, password: password }); // Update local state
else
updateServers({ ...savedRow });
}
};
const updateServers = (updatedServer: Server) => {
setServers((prevServers) => {
if (prevServers == null) return null;
return prevServers.map((server) => (server.id === updatedServer.id ? updatedServer : server))
setServersWithPasswords((prev) => {
if (prev == null) return null;
const exists = prev.some((e) => e.id === updatedServer.id);
return exists
? prev.map((server) => (server.id === updatedServer.id ? updatedServer : server))
: [...prev, updatedServer];
});
};
const displayRows: Server[] = isPasswordVisible ? (serversWithPasswords ?? setupData.servers) : setupData.servers;
return (
<Box ref={gridContainerRef} sx={{
position: 'relative', left: 0, right: 0, height: "calc(100vh - 124px)", overflow: "hidden",
@ -99,7 +130,7 @@ function Servers() {
<Box sx={{ position: 'absolute', inset: 0 }}>
{isMobile ? (
<List>
{(!isPasswordVisible ? setupData.servers : servers)?.map((row) => (
{displayRows.map((row) => (
<Card key={row.id} sx={{ marginBottom: 2 }}>
<Box sx={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<CardContent>
@ -109,11 +140,11 @@ function Servers() {
<Typography variant="body2">Port: {row.port}</Typography>
<Typography variant="body2">Username: {row.username}</Typography>
<Typography variant="body2">Password
<IconButton size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
<IconButton color="primary" size="small" onClick={togglePasswordVisibility} sx={{ marginLeft: 1 }}>
{isPasswordVisible ? <LockOpen /> : <Lock />}
</IconButton>: {row.password}</Typography>
</CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon />
</IconButton>
</Box>
@ -122,7 +153,7 @@ function Servers() {
</List>
) : (
<DataGrid
rows={(!isPasswordVisible ? setupData.servers : servers)!}
rows={displayRows}
columns={columns}
autoPageSize
sx={{ minWidth: "600px" }}
@ -130,14 +161,12 @@ function Servers() {
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadServers()} // Refresh only active servers
sx={{ marginRight: 2 }}
>
{setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadServers()} sx={{ marginLeft: 1 }}>
{setupData.serversLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />
@ -164,12 +193,12 @@ function Servers() {
</Box>
{/* Server Edit Modal */}
{selectedRow && (
{open && (
<ServerEdit
open={open}
server={selectedRow}
onClose={() => setOpen(false)}
onSave={handleUpdateRow}
onSave={handleSaveRow}
/>
)}
</Box>

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Target from '@/types/target';
import TargetEdit from "@/components/modals/TargetEdit";
@ -21,12 +23,13 @@ function Targets() {
const columns: GridColDef<Target>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<Target>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
Edit
</Button>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
),
},
{ field: "id", headerName: "ID", width: 60 },
@ -119,22 +122,12 @@ function Targets() {
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => handleNew()}
sx={{ marginRight: 2 }}
>
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTargets()}
sx={{ marginRight: 2 }}
>
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadTargets()} sx={{ marginLeft: 1 }}>
{setupData.targetsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import Template from '@/types/template';
import TemplateEdit from "@/components/modals/TemplateEdit";
@ -18,12 +20,13 @@ function Templates() {
const columns: GridColDef<Template>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
width:60,
renderCell: (params: GridRenderCellParams<Template>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
Edit
</Button>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
),
},
{ field: "id", headerName: "ID", width: 60 },
@ -75,7 +78,7 @@ function Templates() {
<Typography variant="body2">Description: {row.description}</Typography>
<Typography variant="body2">Active: {row.isActive ? "Yes" : "No"}</Typography>
</CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon />
</IconButton>
</Box>
@ -91,23 +94,13 @@ function Templates() {
minWidth: "600px", maxWidth: getMaxWidth() }}
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => handleNew()}
sx={{ marginRight: 2 }}
>
{"Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTemplates()}
sx={{ marginRight: 2 }}
>
{setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<GridToolbarContainer sx={{ display: "flex", alignItems: "center", }}>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadTemplates()} sx={{ marginLeft: 1 }}>
{setupData.templatesLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />

View File

@ -1,7 +1,9 @@
import { useState, useRef } from 'react';
import { useSetupData, SetupData } from "@/context/SetupDataContext";
import EditIcon from '@mui/icons-material/Edit';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, Button, CircularProgress, IconButton } from '@mui/material';
import AddIcon from '@mui/icons-material/Add';
import RefreshIcon from '@mui/icons-material/Refresh';
import { List, Card, CardContent, Typography, Box, useTheme, useMediaQuery, CircularProgress, IconButton } from '@mui/material';
import { DataGrid, GridColDef, GridRenderCellParams, GridRowModel, GridToolbarContainer, GridToolbarQuickFilter, GridToolbarExport, GridToolbarDensitySelector, GridToolbarColumnsButton } from '@mui/x-data-grid';
import TestEmailList from '@/types/testEmailList';
import TestEmailListEdit from "@/components/modals/TestEmailListEdit";
@ -18,12 +20,13 @@ function TestEmailLists() {
const columns: GridColDef<TestEmailList>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
width: 60,
renderCell: (params: GridRenderCellParams<TestEmailList>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>
Edit
</Button>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(params.row); }}>
<EditIcon />
</IconButton>
),
},
{ field: "id", headerName: "ID", width: 60 },
@ -70,7 +73,7 @@ function TestEmailLists() {
<Typography variant="body2">ID: {row.id}</Typography>
<Typography variant="body2">Email List: {row.emails}</Typography> {/*TODO: Format properly*/}
</CardContent>
<IconButton onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<IconButton color="primary" onClick={(e) => { e.stopPropagation(); handleEdit(row); }}>
<EditIcon />
</IconButton>
</Box>
@ -86,22 +89,12 @@ function TestEmailLists() {
slots={{
toolbar: () => (
<GridToolbarContainer sx={{ display: "flex", alignItems: "center" }}>
<Button
variant="contained"
color="primary"
onClick={() => handleNew()}
sx={{ marginRight: 2 }}
>
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Add New"}
</Button>
<Button
variant="contained"
color="primary"
onClick={() => setupData.reloadTestEmailLists()}
sx={{ marginRight: 2 }}
>
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : "Refresh"}
</Button>
<IconButton size="small" color="primary" onClick={handleNew} sx={{ marginLeft: 1 }}>
<AddIcon />
</IconButton>
<IconButton size="small" color="primary" onClick={() => setupData.reloadTestEmailLists()} sx={{ marginLeft: 1 }}>
{setupData.testEmailListsLoading ? <CircularProgress size={24} color="inherit" /> : <RefreshIcon />}
</IconButton>
<GridToolbarColumnsButton />
<GridToolbarDensitySelector />
<GridToolbarExport />

View File

@ -18,7 +18,7 @@ function UnsubscribeUrls() {
const columns: GridColDef<UnsubscribeUrl>[] = [
{
field: "actions",
headerName: "Actions",
headerName: "",
sortable: false,
renderCell: (params: GridRenderCellParams<UnsubscribeUrl>) => (
<Button variant="contained" color="primary" size="small" onClick={() => handleEdit(params.row)}>

View File

@ -1,78 +0,0 @@
.has-feedback .form-control {
padding-right: 30.5px;
}
.notesStyle {
text-overflow: ellipsis;
overflow: hidden;
max-width: 200px;
min-width: 200px;
white-space: nowrap;
}
.table > thead > tr > th, .table > tbody > tr > th, .table > tfoot > tr > th, .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td {
padding: 3px;
line-height: 1.42857143;
vertical-align: top;
border-top: 1px solid #ddd;
}
.dataTables_wrapper {
padding: 20px;
}
.dataTables_wrapper .sorting_icon {
width: 0px;
}
.no-sort {
padding-right: 3px !important;
}
.drop-zone {
width: 300px;
height: 200px;
border: 2px dashed #ccc;
border-radius: 10px;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
margin: 20px auto;
cursor: pointer;
}
.drop-zone.dragover {
background-color: #f0f0f0;
}
.preview {
display: flex;
flex-wrap: wrap;
margin-top: 20px;
}
.preview div {
position: relative;
margin: 10px;
}
.preview img {
max-width: 100px;
display: block;
}
.preview button {
position: absolute;
top: 0;
right: 0;
background: red;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
}
.form-group {
margin-bottom: 5px;
}

View File

@ -1,107 +0,0 @@
/* Custom CSS to restore old AdminLTE color classes in AdminLTE 4.x */
/* Orange Background */
.bg-orange {
background-color: #ff851b !important;
color: #ffffff;
}
/* Blue Background */
.bg-blue {
background-color: #0073b7 !important;
color: #ffffff;
}
/* Green Background */
.bg-green {
background-color: #00a65a !important;
color: #ffffff;
}
/* Red Background */
.bg-red {
background-color: #dd4b39 !important;
color: #ffffff;
}
/* Yellow Background */
.bg-yellow {
background-color: #f39c12 !important;
color: #ffffff;
}
/* Purple Background */
.bg-purple {
background-color: #605ca8 !important;
color: #ffffff;
}
/* Light Blue Background */
.bg-light-blue {
background-color: #3c8dbc !important;
color: #ffffff;
}
/* Navy Background */
.bg-navy {
background-color: #001f3f !important;
color: #ffffff;
}
/* Teal Background */
.bg-teal {
background-color: #39cccc !important;
color: #ffffff;
}
/* Maroon Background */
.bg-maroon {
background-color: #d81b60 !important;
color: #ffffff;
}
/* Black Background */
.bg-black {
background-color: #111 !important;
color: #ffffff;
}
/* Olive Background */
.bg-olive {
background-color: #3d9970 !important;
color: #ffffff;
}
/* Lime Background */
.bg-lime {
background-color: #01ff70 !important;
color: #ffffff;
}
/* Fuchsia Background */
.bg-fuchsia {
background-color: #f012be !important;
color: #ffffff;
}
/* Aqua Background */
.bg-aqua {
background-color: #00c0ef !important;
color: #ffffff;
}
/* Gray Background */
.bg-gray {
background-color: #d2d6de !important;
color: #ffffff;
}
/* Flat Button */
.btn-flat {
box-shadow: none;
border-radius: 0;
border: none;
padding: 10px 15px;
font-size: 14px;
transition: background-color 0.3s ease;
}

View File

@ -1,5 +1,5 @@
export interface EmailDomain {
id?: number;
id: number;
name: string;
emailAddress: string;
username: string;

View File

@ -1,5 +1,5 @@
export interface Server {
id?: number;
id: number;
name: string;
serverName: string;
port: number;

View File

@ -1,5 +1,5 @@
export interface Target {
id?: number;
id: number;
serverId: number;
name: string;
databaseName: string;

View File

@ -1,5 +1,5 @@
export interface UnsubscribeUrl {
id?: number;
id: number;
name: string;
url: string;
}