diff --git a/.dockerignore b/.dockerignore
index b2e52e5e3..6cd921ac2 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,4 @@
node_modules
-docs
Dockerfile
.vscode
.git
diff --git a/.gitignore b/.gitignore
index c2f41d22d..d52604f06 100644
--- a/.gitignore
+++ b/.gitignore
@@ -235,4 +235,7 @@ BundleArtifacts/
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
-!*.[Cc]ache/
\ No newline at end of file
+!*.[Cc]ache/
+
+#specific files
+docker-token.json
\ No newline at end of file
diff --git a/README.md b/README.md
index b8afd376e..a06a845a6 100644
--- a/README.md
+++ b/README.md
@@ -1,36 +1,41 @@
# **Fabio Sereno** - Software Developer
-Highly Experienced Full Stack Web Developer of 10+ years (6+ in the FinTech sector). Highly self-motivated, enthusiastic, professional and a team player. Possesses strong analytical and problem solving skills, code proficiency, and an ability to follow through with projects from initiation to completion with innovation and creativity.
+Highly experienced Full Stack Software Engineer with over 15 years of experience (6+ years in the FinTech sector). Self-motivated, enthusiastic, and professional team player with strong analytical and problem-solving skills. Demonstrates proficiency in coding and a proven ability to successfully complete projects with innovation and creativity.
-Very keen on learning and using the latest technologies, with a real passion for software development. Areas of keen interest: Fin Tech, Health Tech, Commerce, ML/AI, Blockchain, XR(VR/AR), IoT.
+Passionate about software development and constantly eager to learn and adopt the latest technologies. Keen interest in FinTech, HealthTech, Commerce, ML/AI, Blockchain, XR (VR/AR), and IoT.
## My Portfolio Repository
The purpose of this repository is to demonstrate skills in various technologies, coding proficiency and knowledge.
-To see a fully deployed version of this project go to:
+To view a static version of this project with limited functionality, please visit:
https://fsereno.github.io/portfolio/
+To see a fully containerised version of this project deployed to AWS (Amazon Web Services) EC2, please either raise an issue on this GitHub repository or contact me via LinkedIn.
+
+- Raise an issue on Github (https://github.com/fsereno/portfolio/issues)
+- Contact me on LinkedIn (https://www.linkedin.com/in/fabio-sereno-6a97b986/)
+
This project is both built and deployed with continuous integration and deployment (CI/CD).
Some of the technology used in this project and related projects:
-- Azure Functions (https://azure.microsoft.com/en-gb/services/functions/)
-- AWS Lambda (https://aws.amazon.com/lambda/)
+- .NET (https://dotnet.microsoft.com)
- Docker (https://www.docker.com/)
-- Node JS (https://nodejs.org/en/)
-- Gulp (https://gulpjs.com)
+- Docker Hub (https://hub.docker.com/)
+- NodeJS (https://nodejs.org/en/)
- Webpack (https://webpack.js.org/)
- Pug (https://pugjs.org/api/getting-started.html)
- TypeScript (https://www.typescriptlang.org)
+- A-Frame (https://aframe.io/)
- Sass (https://sass-lang.com/)
+- Three.js (https://threejs.org/)
+- React (https://reactjs.org/)
+- Vue (https://vuejs.org/)
+- Taiko (https://www.npmjs.com/package/taiko)
- Mocha (https://mochajs.org/)
- Chai (https://www.chaijs.com/)
-- A-Frame (https://aframe.io/)
-- .NET (https://dotnet.microsoft.com)
+- Enzyme (https://enzymejs.github.io/enzyme/)
- NUnit (https://nunit.org/)
-- ThreeJS (https://threejs.org/)
-- React (https://reactjs.org/)
-- Vue (https://vuejs.org/)
- SOLID principles
- TDD - Test driven development
- DDD - Domain driven development
@@ -45,10 +50,8 @@ Some of the technology used in this project and related projects:
---
## Prerequisites
+
- Docker (https://www.docker.com/)
-- Node JS v ^14.17.5 (https://nodejs.org/en/)
-- NPM (https://www.npmjs.com/)
-> Tip - Node and NPM are not needed locally if running within a container.
---
## Installation
@@ -62,75 +65,57 @@ Some of the technology used in this project and related projects:
---
### Run inside a Container using Docker
+
- Please ensure you have Docker installed and running.
- Open your preferred command line:
-> Launch the container
+Launch the production environment
```shell
$ docker compose up
```
-
->Attach a Bash command line interface.
-From here you will be able to run all subsequent NPM commands
-
-```shell
-$ docker exec -it node bash
-```
-> You should now have a bash cmd connected to the container
----
-
-### Run outside a Container
-- Please ensure you have Node JS and NPM installed.
-- Open your preferred command line:
-
-> install NPM packages
-
-```shell
-$ npm install
-$ npm install --global gulp-cli
-```
----
-
-## Usage
-
-#### Run the initial build
-> Run this first to ensure all resources build successfully
+##### This will:
+- Pull all images from Docker Hub.
+- Spin up all services in containers.
+- The application will be available at: http://localhost/
+
+To run one of the following specific Docker tasks
+- analysis
+- create
+- dev
+- rel
+- test
+- test-e2e
```shell
-$ npm run build
+$ sh start
```
-#### Build a specific application
+To stop one of the above tasks, excluding those which destroy themselves (create, test)
```shell
-$ npm run build dir=
+$ sh stop
```
-
-##### This will:
-- Build all initial development resources (pug, sass, ts, js).
---
-#### Run the development server
+### Tasks
-```shell
-$ npm run dev
-```
-#### Serve a specific application
+#### Serve a specific application via the development server
```shell
-$ npm run dev dir=
+$ sh start dev
```
##### This will:
- Start the development server.
- Watch for any changes on development resources.
-- Live Reload any changes straight to the browser.
+- Hot-reload any changes straight to the browser.
+- The default application is the root application - home
- Open your browser and navigate to http://localhost:8080.
---
#### Run analysis on a specific application
```shell
-$ npm run analysis dir=
+$ sh start analysis
```
##### This will:
- Start the analysis server.
@@ -141,35 +126,35 @@ $ npm run analysis dir=
#### Build for release
```shell
-$ npm run release
+$ sh start rel
```
##### This will:
-- Build the production directory.
+- Build the production static assets directory.
---
#### Run all unit tests
```shell
-$ npm test
+$ sh start test
```
##### This will:
- Run all application specific and global unit tests.
---
-#### Run all functional tests (using a headless browser)
+#### Run all functional end-to-end tests
```shell
-$ npm run test-func
+$ sh start test-e2e
```
##### This will:
-- Please ensure the development server is running
- Run all functional tests from the ./app/tests/functional directory
+- Currently this feature is a work in progress (WIP) and will not work on ARM architecture
---
#### Create a new application
```shell
-$ npm run create
+$ sh create
```
##### This will:
- Build applications based on the config.json file.
diff --git a/app/app_AzureDotNetCoreDataStructuresApi/tests/e2e/taiko.test.js b/app/app_AzureDotNetCoreDataStructuresApi/tests/e2e/taiko.test.js
deleted file mode 100644
index 542da24ee..000000000
--- a/app/app_AzureDotNetCoreDataStructuresApi/tests/e2e/taiko.test.js
+++ /dev/null
@@ -1,95 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
- import { openBrowser, goto, write, click, closeBrowser, $, into, textBox, button, waitFor, evaluate, text } from 'taiko';
-
- const APPLICATION = "app_AzureDotNetCoreDataStructuresApi";
- const URL = `http://localhost:8080/${APPLICATION}/index.html`;
-
- beforeAll(async () => {
- await openBrowser({
- headless: true,
- slowMo: 250,
- args: ['--no-sandbox']
- });
- });
-
- describe(APPLICATION, () => {
- test('Should not add an item to the queue', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await click(button({id:'queueInput_submit'}));
- const error = await $('.was-validated .form-control:invalid').exists();
- const result = await evaluate($('#queueList'), (element) => element.innerText);
- expect(error).toBeTruthy();
- expect(result).toBe("");
- }, 100000);
- test('Should not add an item to the stack', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await click(button({id:'stackInput_submit'}));
- const error = await $('.was-validated .form-control:invalid').exists();
- const result = await evaluate($('#stackList'), (element) => element.innerText);
- expect(error).toBeTruthy();
- expect(result).toBe("");
- }, 100000);
- test('Should add an item to the queue', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('Item 1', into(textBox({id:'queueInput'})));
- await click(button({id:'queueInput_submit'}));
- const result = await evaluate($('#queueList'), (element) => element.innerText);
- expect(result).toBe('Item 1');
- }, 100000);
- test('Should remove an item to the queue', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('Item 1', into(textBox({id:'queueInput'})));
- await click(button({id:'queueInput_submit'}));
- await waitFor(2000);
- await write('Item 2', into(textBox({id:'queueInput'})));
- await click(button({id:'queueInput_submit'}));
- await waitFor(2000);
- await click(button({id:'queueInput_remove'}));
- const result = await evaluate($('#queueList'), (element) => element.innerText);
- expect(result).toBe('Item 2');
- }, 100000);
- test('Should add an item to the stack', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('Item 1', into(textBox({id:'stackInput'})));
- await click(button({id:'stackInput_submit'}));
- const result = await evaluate($('#stackList'), (element) => element.innerText);
- expect(result).toBe('Item 1');
- }, 100000);
- test('Should remove an item to the queue', async () => {
- await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('Item 1', into(textBox({id:'stackInput'})));
- await click(button({id:'stackInput_submit'}));
- await waitFor(2000);
- await write('Item 2', into(textBox({id:'stackInput'})));
- await click(button({id:'stackInput_submit'}));
- await waitFor(2000);
- await click(button({id:'stackInput_remove'}));
- const result = await evaluate($('#stackList'), (element) => element.innerText);
- expect(result).toBe('Item 1');
- }, 100000);
- });
-
- afterAll(() => {
- closeBrowser();
- });
\ No newline at end of file
diff --git a/app/app_AzureDotNetCoreUniqueDataEntryApi/tests/e2e/taiko.test.js b/app/app_AzureDotNetCoreUniqueDataEntryApi/tests/e2e/taiko.test.js
deleted file mode 100644
index c9ee636b3..000000000
--- a/app/app_AzureDotNetCoreUniqueDataEntryApi/tests/e2e/taiko.test.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
- import { openBrowser, goto, write, click, closeBrowser, $, into, textBox, button, waitFor, evaluate, text } from 'taiko';
-
- const APPLICATION = "app_AzureDotNetCoreUniqueDataEntryApi";
- const URL = `http://localhost:8080/${APPLICATION}/index.html`;
-
- beforeAll(async () => {
- await openBrowser({
- headless: true,
- slowMo: 250,
- args: ['--no-sandbox']
- });
- });
-
- describe(APPLICATION, () => {
- test('Should add an item to the table', async () => {
- await goto(URL);
- await write('14', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('James', into(textBox({id:'firstNameInput'})));
- await write('Brown', into(textBox({id:'secondNameInput'})));
- await write('(000) 111 222', into(textBox({id:'contactInput'})));
- await write('AB10 0CD', into(textBox({id:'postCodeInput'})));
- await click(button({id:'submit'}));
- await waitFor(2000);
- const result = await evaluate($('#itemTable'), (element) => element.innerText);
- expect(result).toContain('James\tBrown\t(000) 111 222\tAB10 0CD\tDelete');
- }, 100000);
- test('Should not add a duplicate item to the table', async () => {
- await goto(URL);
- await write('14', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('John', into(textBox({id:'firstNameInput'})));
- await write('Doe', into(textBox({id:'secondNameInput'})));
- await write('000000000', into(textBox({id:'contactInput'})));
- await write('AB101CD', into(textBox({id:'postCodeInput'})));
- await click(button({id:'submit'}));
- await waitFor(2000);
- await click($('#duplicateEntryErrorModule button'));
- await waitFor(2000);
- const exists = await $('#duplicateEntryErrorModule').exists();
- const result = await evaluate($('#itemTable'), (element) => element.innerText);
- expect(exists).toBeFalsy();
- expect(result).toBe('First name\tSecond name\tContact\tPostcode\tAction\nJohn\tDoe\t000000000\tAB101CD\tDelete');
- }, 100000);
- test('Should remove an item to the table', async () => {
- await goto(URL);
- await write('14', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await click($('a.delete[data-index="0"]'));
- const result = await evaluate($('#itemTable'), (element) => element.innerText);
- expect(result).toBe('First name\tSecond name\tContact\tPostcode\tAction');
- }, 100000);
- });
-
- afterAll(() => {
- closeBrowser();
- });
\ No newline at end of file
diff --git a/app/app_AzureDotNetCoreUniqueDataEntryApi/Readme.md b/app/app_UniqueDataEntry/Readme.md
similarity index 56%
rename from app/app_AzureDotNetCoreUniqueDataEntryApi/Readme.md
rename to app/app_UniqueDataEntry/Readme.md
index 2783d943d..60af5df89 100644
--- a/app/app_AzureDotNetCoreUniqueDataEntryApi/Readme.md
+++ b/app/app_UniqueDataEntry/Readme.md
@@ -1,9 +1,7 @@
-# Azure Functions, .NET Core, Unique Data Entry Api
+# Unique Data Entry Application
-With this application I have built a unique data entry form. A simple React frontend handles the UI and data entry logic is dealt with by .NET Core and IEqualityComparer.
+With this application I have built a unique data entry form. A simple React frontend handles the UI and data entry logic is dealt with by .NET and IEqualityComparer.
-Whilst this functionality could be achieved with JavaScript alone, I wanted to demonstrate knowledge of IEqualityComparer and the .NET Core framework. I also wanted to explore Azure Functions and the power of serverless compute.
-
-- Azure Functions Repository (https://github.com/fsereno/app_AzureDotNetCoreUniqueDataEntryApi)
+Whilst this functionality could be achieved with JavaScript alone, I wanted to demonstrate knowledge of IEqualityComparer and the .NET framework.
- IEqualityComparer (https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.iequalitycomparer-1?view=net-5.0)
\ No newline at end of file
diff --git a/app/app_UniqueDataEntry/backend/api/Controllers/ApiController.cs b/app/app_UniqueDataEntry/backend/api/Controllers/ApiController.cs
new file mode 100644
index 000000000..40bf8b468
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Controllers/ApiController.cs
@@ -0,0 +1,54 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Portfolio.Core.Types;
+using Portfolio.Core.Services;
+using Portfolio.UniqueDataEntry.Interfaces;
+using Portfolio.UniqueDataEntry.Utils;
+using Portfolio.UniqueDataEntry.Models;
+
+namespace Portfolio.UniqueDataEntry.Controllers;
+
+///
+/// API controller for unique data entry operations.
+///
+[ApiController]
+[Route("[controller]")]
+public class ApiController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private IUniqueDataEntryUtil _uniqueDataEntryUtil;
+
+ ///
+ /// Initializes a new instance of the ApiController class with the specified logger and unique data entry utility.
+ ///
+ /// The logger to use for logging messages.
+ /// The utility for unique data entry operations.
+ public ApiController(ILogger logger, IUniqueDataEntryUtil uniqueDataEntryUtil)
+ {
+ _logger = logger;
+ _uniqueDataEntryUtil = uniqueDataEntryUtil;
+ }
+
+ ///
+ /// Checks if an item can be added based on uniqueness criteria.
+ ///
+ /// The request body containing the items and the item to be added.
+ /// A boolean indicating whether the item can be added.
+ [HttpPost("CanItemBeAddedAsync")]
+ public IActionResult CanItemBeAddedAsync([FromBody] RequestBody data)
+ {
+ _logger.LogInformation("CanItemBeAddedAsync endpoint hit.");
+
+ var equalityComparer = new Item.ItemEqualityComparer();
+
+ var dictionary = data.Items.ToDictionary(x => x, x => x.FirstName, equalityComparer);
+ var result = _uniqueDataEntryUtil.CanItemBeAdded(dictionary, data.Item);
+
+ _logger.LogInformation($"Result is: {result}");
+
+ return Ok(result);
+ }
+}
diff --git a/app/app_UniqueDataEntry/backend/api/Interfaces/IUniqueDataEntryUtil.cs b/app/app_UniqueDataEntry/backend/api/Interfaces/IUniqueDataEntryUtil.cs
new file mode 100644
index 000000000..31173dfe9
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Interfaces/IUniqueDataEntryUtil.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+using Portfolio.UniqueDataEntry.Models;
+
+namespace Portfolio.UniqueDataEntry.Interfaces
+{
+ public interface IUniqueDataEntryUtil
+ {
+ ///
+ /// Testing to see if an item can be added by attempting to add the passed item to the dictionary
+ ///
+ /// A dictionary of exsiting collection
+ /// The item to be added
+ /// A bool, can the item be added or not ?
+ bool CanItemBeAdded(Dictionary dictionary, Item item);
+ }
+}
\ No newline at end of file
diff --git a/app/app_UniqueDataEntry/backend/api/Models/Item.cs b/app/app_UniqueDataEntry/backend/api/Models/Item.cs
new file mode 100644
index 000000000..2765fe116
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Models/Item.cs
@@ -0,0 +1,91 @@
+using System.Collections.Generic;
+
+namespace Portfolio.UniqueDataEntry.Models
+{
+ ///
+ /// Represents an item for unique data entry.
+ ///
+ public class Item
+ {
+ ///
+ /// Initializes a new instance of the Item class with default values.
+ ///
+ public Item()
+ {
+ this.FirstName = string.Empty;
+ this.SecondName = string.Empty;
+ this.Contact = string.Empty;
+ this.PostCode = string.Empty;
+ }
+
+ ///
+ /// Gets or sets the first name of the item.
+ ///
+ public string FirstName { get; set; }
+
+ ///
+ /// Gets or sets the second name of the item.
+ ///
+ public string SecondName { get; set; }
+
+ ///
+ /// Gets or sets the contact information of the item.
+ ///
+ public string Contact { get; set; }
+
+ ///
+ /// Gets or sets the post code of the item.
+ ///
+ public string PostCode { get; set; }
+
+ ///
+ /// Equality comparer for comparing Item objects.
+ ///
+ public class ItemEqualityComparer : IEqualityComparer
+ {
+ ///
+ /// Determines whether two Item objects are equal.
+ ///
+ /// The first Item to compare.
+ /// The second Item to compare.
+ /// true if the Item objects are equal; otherwise, false.
+ public bool Equals(Item item1, Item item2)
+ {
+ if (item2 == null && item1 == null)
+ {
+ return true;
+ }
+ else if (item1 == null || item2 == null)
+ {
+ return false;
+ }
+ else if (Normalise(item1.SecondName) == Normalise(item2.SecondName)
+ && Normalise(item1.Contact) == Normalise(item2.Contact)
+ && Normalise(item1.PostCode) == Normalise(item2.PostCode))
+ {
+ return true;
+ }
+ else
+ {
+ return false;
+ }
+ }
+
+ ///
+ /// Gets the hash code for the Item object.
+ ///
+ /// The Item for which to get the hash code.
+ /// The hash code for the Item object.
+ public int GetHashCode(Item item)
+ {
+ var toHash = Normalise(item.SecondName) + Normalise(item.Contact) + Normalise(item.PostCode);
+ return toHash.GetHashCode();
+ }
+
+ private string Normalise(string value)
+ {
+ return value.Trim().ToUpper();
+ }
+ }
+ }
+}
diff --git a/app/app_UniqueDataEntry/backend/api/Models/RequestBody.cs b/app/app_UniqueDataEntry/backend/api/Models/RequestBody.cs
new file mode 100644
index 000000000..97abb12a0
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Models/RequestBody.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+
+namespace Portfolio.UniqueDataEntry.Models
+{
+ ///
+ /// Represents the request body for unique data entry operations.
+ ///
+ public class RequestBody
+ {
+ ///
+ /// Initializes a new instance of the RequestBody class with default values.
+ ///
+ public RequestBody()
+ {
+ this.Items = new List();
+ this.Item = new Item();
+ }
+
+ ///
+ /// Gets or sets the list of items.
+ ///
+ public List Items { get; set; }
+
+ ///
+ /// Gets or sets the item to be added.
+ ///
+ public Item Item { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/app/app_UniqueDataEntry/backend/api/Program.cs b/app/app_UniqueDataEntry/backend/api/Program.cs
new file mode 100644
index 000000000..9744c4c1d
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Program.cs
@@ -0,0 +1,38 @@
+using Portfolio.Core.Services;
+using Portfolio.UniqueDataEntry.Interfaces;
+using Portfolio.UniqueDataEntry.Utils;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://*:3003");
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+// Services
+builder.Services.AddScoped();
+
+builder.Services.AddHealthChecks();
+
+var app = builder.Build();
+
+app.MapHealthChecks("/healthcheck");
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/app/app_UniqueDataEntry/backend/api/Properties/launchSettings.json b/app/app_UniqueDataEntry/backend/api/Properties/launchSettings.json
new file mode 100644
index 000000000..d58642981
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:4279",
+ "sslPort": 44379
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7114;http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/app/app_UniqueDataEntry/backend/api/Utils/UniqueDataEntryUtil.cs b/app/app_UniqueDataEntry/backend/api/Utils/UniqueDataEntryUtil.cs
new file mode 100644
index 000000000..d8e28bd46
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/Utils/UniqueDataEntryUtil.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Portfolio.UniqueDataEntry.Interfaces;
+using Portfolio.UniqueDataEntry.Models;
+
+namespace Portfolio.UniqueDataEntry.Utils
+{
+ ///
+ /// Utility class for unique data entry operations.
+ ///
+ public class UniqueDataEntryUtil : IUniqueDataEntryUtil
+ {
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the UniqueDataEntryUtil class with the specified logger.
+ ///
+ /// The logger to use for logging messages.
+ public UniqueDataEntryUtil(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ public bool CanItemBeAdded(Dictionary dict, Item item)
+ {
+ var result = false;
+ var currentCount = dict.Count;
+
+ try
+ {
+ dict.Add(item, item.SecondName);
+ result = dict.Count == currentCount + 1;
+ }
+ catch (Exception exception)
+ {
+ _logger.LogWarning("You cannot add duplicate items.");
+ _logger.LogWarning(exception.Message);
+ }
+
+ return result;
+ }
+ }
+}
diff --git a/app/app_UniqueDataEntry/backend/api/api.csproj b/app/app_UniqueDataEntry/backend/api/api.csproj
new file mode 100644
index 000000000..19bfaa242
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/api.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/app_UniqueDataEntry/backend/api/appsettings.Development.json b/app/app_UniqueDataEntry/backend/api/appsettings.Development.json
new file mode 100644
index 000000000..ff66ba6b2
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/app/app_UniqueDataEntry/backend/api/appsettings.json b/app/app_UniqueDataEntry/backend/api/appsettings.json
new file mode 100644
index 000000000..4d566948d
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/app/app_UniqueDataEntry/backend/test/UniqueDataEntryUtilTests.cs b/app/app_UniqueDataEntry/backend/test/UniqueDataEntryUtilTests.cs
new file mode 100644
index 000000000..495ebb2de
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/test/UniqueDataEntryUtilTests.cs
@@ -0,0 +1,96 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+using Portfolio.UniqueDataEntry.Interfaces;
+using Portfolio.UniqueDataEntry.Utils;
+using Portfolio.UniqueDataEntry.Models;
+
+namespace Portfolio.UniqueDataEntry.Test
+{
+ public class UniqueDataEntryUtilTests
+ {
+ private IUniqueDataEntryUtil _sut;
+ private Mock> _logger;
+ private Dictionary _dictionary;
+
+ public UniqueDataEntryUtilTests()
+ {
+ _logger = new Mock>();
+ _sut = new UniqueDataEntryUtil(_logger.Object);
+
+ var initialCollection = new List()
+ {
+ new Item(){ FirstName = "James", SecondName = "Bond", Contact = "000 000 000", PostCode = "AB00 1AB" },
+ new Item(){ FirstName = "Tom", SecondName = "Jones", Contact = "000 000 000", PostCode = "AB00 2AB"}
+ };
+ _dictionary = initialCollection.ToDictionary(x => x, x => x.FirstName, new Item.ItemEqualityComparer());
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedNotMatching()
+ {
+ var item = new Item(){ FirstName = "Tom", SecondName = "Smith", Contact = "000 000 000", PostCode = "AB00 3AB" };
+ var result = _sut.CanItemBeAdded(_dictionary, item);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedPartialMatch()
+ {
+ var item = new Item(){ FirstName = "Tommy", SecondName = "Jones", Contact = "000 000 000", PostCode = "AB00 2AB" };
+ var result = _sut.CanItemBeAdded(_dictionary, item);
+ VerifyLogger(LogLevel.Warning, "You cannot add duplicate items.");
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedMatchingSecondNameDifferentElse()
+ {
+ var item = new Item(){ FirstName = "Tommy", SecondName = "Jones", Contact = "111 111 111", PostCode = "AB00 4AB" };
+ var result = _sut.CanItemBeAdded(_dictionary, item);
+
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedToEmptyDictionary()
+ {
+ var initialCollection = new List();
+ var item = new Item(){ FirstName = "Karen", SecondName = "Jones", Contact = "000 000 000", PostCode = "AB00 1AB" };
+ var dictionary = initialCollection.ToDictionary(x => x, x => x.FirstName, new Item.ItemEqualityComparer());
+ var result = _sut.CanItemBeAdded(dictionary, item);
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedCaseSensitive()
+ {
+ var item = new Item(){ FirstName = "JAMES", SecondName = "BOND", Contact = "000 000 000", PostCode = "AB00 1AB" };
+ var result = _sut.CanItemBeAdded(_dictionary, item);
+ Assert.False(result);
+ }
+
+ [Fact]
+ public void TestCanItemBeAddedSpaceSensitive()
+ {
+ var item = new Item(){ FirstName = "James", SecondName = "Bond ", Contact = " 000 000 000 ", PostCode = "AB00 1AB" };
+ var result = _sut.CanItemBeAdded(_dictionary, item);
+ Assert.False(result);
+ }
+
+ private void VerifyLogger(LogLevel expectedLogLevel, string expectedMessage = "")
+ {
+ _logger.Verify(
+ x => x.Log(
+ It.Is(l => l == expectedLogLevel),
+ It.IsAny(),
+ It.Is((v, t) => String.IsNullOrEmpty(expectedMessage) ? true : v.ToString() == expectedMessage),
+ It.IsAny(),
+ It.Is>((v, t) => true)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_UniqueDataEntry/backend/test/Usings.cs b/app/app_UniqueDataEntry/backend/test/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/test/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/app/app_UniqueDataEntry/backend/test/test.csproj b/app/app_UniqueDataEntry/backend/test/test.csproj
new file mode 100644
index 000000000..e485514e2
--- /dev/null
+++ b/app/app_UniqueDataEntry/backend/test/test.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/app/app_AzureDotNetCoreDataStructuresApi/pug/index.pug b/app/app_UniqueDataEntry/pug/index.pug
similarity index 100%
rename from app/app_AzureDotNetCoreDataStructuresApi/pug/index.pug
rename to app/app_UniqueDataEntry/pug/index.pug
diff --git a/app/app_AzureDotNetCoreDataStructuresApi/sass/styles.scss b/app/app_UniqueDataEntry/sass/styles.scss
similarity index 100%
rename from app/app_AzureDotNetCoreDataStructuresApi/sass/styles.scss
rename to app/app_UniqueDataEntry/sass/styles.scss
diff --git a/app/app_AzureDotNetCoreUniqueDataEntryApi/src/app.js b/app/app_UniqueDataEntry/src/app.js
similarity index 82%
rename from app/app_AzureDotNetCoreUniqueDataEntryApi/src/app.js
rename to app/app_UniqueDataEntry/src/app.js
index 450e29983..849d57098 100644
--- a/app/app_AzureDotNetCoreUniqueDataEntryApi/src/app.js
+++ b/app/app_UniqueDataEntry/src/app.js
@@ -5,16 +5,17 @@ import '../sass/styles.scss';
import React from 'react';
import ReactDOM from 'react-dom';
import { KeyGeneratorUtil } from '../../typeScript/Utils/keyGeneratorUtil/dist/index';
-import { PuzzleModalComponent } from '../../js/modules/react/puzzleModalComponent.js';
import { SpinnerComponent } from '../../js/modules/react/spinnerComponent.js'
import { ErrorModalComponent } from '../../js/modules/react/errorModalComponent.js';
import { ConfigUtil } from '../../js/modules/utils/configUtil';
import { FormComponent } from './formComponent';
import { jQueryAjaxUtil } from '../../js/modules/utils/jQueryAjaxUtil';
+import { DeploymentModalComponent } from '../../js/modules/react/deploymentModalComponent.js';
+import { DeploymentUtil } from '../../js/modules/utils/deploymentUtil';
-const PUZZLE = "4 x 4 - 2 =";
-const APP_CONFIG = ConfigUtil.get("AzureDotNetCoreUniqueDataEntryApi");
-const CAN_IT_BE_ADDED_ASYNC_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.canItemBeAddedAsync}`;
+const CONFIG = ConfigUtil.get();
+const APP_CONFIG = ConfigUtil.get("uniqueDataEntry");
+const CAN_IT_BE_ADDED_ASYNC_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.canItemBeAddedAsync}`;
const FIRST_NAME_INPUT = "firstNameInput";
const SECOND_NAME_INPUT = "secondNameInput";
const CONTACT_INPUT = "contactInput";
@@ -33,20 +34,17 @@ class UniqueDataEntryApp extends React.Component {
counterLimit: 10,
counter: 1,
showSpinner: false,
- showPuzzleModal: true,
- showErrorModal: false,
showDuplicateErrorModal: false,
- isPuzzleValid: false
+ showDeploymentModal: DeploymentUtil.isNotCloud()
};
this.handleSubmit = this.handleSubmit.bind(this);
this.handleDelete = this.handleDelete.bind(this);
- this.handleIsPuzzleValid = this.handleIsPuzzleValid.bind(this);
- this.handlePuzzleModalClose = this.handlePuzzleModalClose.bind(this);
- this.handlePuzzleModalShow = this.handlePuzzleModalShow.bind(this);
this.handleErrorModalClose = this.handleErrorModalClose.bind(this);
this.handleDuplicateErrorModalClose = this.handleDuplicateErrorModalClose.bind(this);
this.handleBeforeAjax = this.handleBeforeAjax.bind(this);
this.handleFailedAjax = this.handleFailedAjax.bind(this);
+ this.handleDeploymentModalClose = this.handleDeploymentModalClose.bind(this);
+ this.handleDeploymentModalShow = this.handleDeploymentModalShow.bind(this);
}
handleBeforeAjax() {
@@ -64,7 +62,7 @@ class UniqueDataEntryApp extends React.Component {
}
handleAjax(request) {
- jQueryAjaxUtil.handleAjax(request, this.state.isPuzzleValid, this.handleBeforeAjax, this.handleFailedAjax, this.handlePuzzleModalShow);
+ jQueryAjaxUtil.handleAjax(request, DeploymentUtil.isCloud(), this.handleBeforeAjax, this.handleFailedAjax, this.handleDeploymentModalShow);
}
handleSubmit(event) {
@@ -90,6 +88,7 @@ class UniqueDataEntryApp extends React.Component {
url: CAN_IT_BE_ADDED_ASYNC_ENDPOINT,
data: JSON.stringify(data),
type: "POST",
+ contentType: "application/json",
success: (response) => {
if (response === true) {
let items = [...this.state.items];
@@ -139,35 +138,24 @@ class UniqueDataEntryApp extends React.Component {
})
}
- handleIsPuzzleValid() {
+ handleDeploymentModalClose() {
this.setState({
- isPuzzleValid: true,
- showPuzzleModal: false
- })
- }
-
- handlePuzzleModalClose() {
- this.setState({
- showPuzzleModal: false
+ showDeploymentModal: false
})
}
- handlePuzzleModalShow() {
+ handleDeploymentModalShow() {
this.setState({
- showPuzzleModal: true
+ showDeploymentModal: true
})
}
render() {
return (
- {
+ await openBrowser({
+ headless: true,
+ slowMo: 250,
+ args: ['--no-sandbox']
+ });
+});
+
+describe(APPLICATION, () => {
+ test('Should add an item to the table', async () => {
+ await goto(URL);
+ await write('14', into(textBox({ id: 'answerInput' }), { force: true }));
+ await click(button({ id: 'submitPuzzle' }));
+ await waitFor(2000);
+ await write('James', into(textBox({ id: 'firstNameInput' })));
+ await write('Brown', into(textBox({ id: 'secondNameInput' })));
+ await write('(000) 111 222', into(textBox({ id: 'contactInput' })));
+ await write('AB10 0CD', into(textBox({ id: 'postCodeInput' })));
+ await click(button({ id: 'submit' }));
+ await waitFor(2000);
+ const result = await evaluate($('#itemTable'), (element) => element.innerText);
+ expect(result).toContain('James\tBrown\t(000) 111 222\tAB10 0CD\tDelete');
+ }, 100000);
+ test('Should not add a duplicate item to the table', async () => {
+ await goto(URL);
+ await write('14', into(textBox({ id: 'answerInput' }), { force: true }));
+ await click(button({ id: 'submitPuzzle' }));
+ await waitFor(2000);
+ await write('John', into(textBox({ id: 'firstNameInput' })));
+ await write('Doe', into(textBox({ id: 'secondNameInput' })));
+ await write('000000000', into(textBox({ id: 'contactInput' })));
+ await write('AB101CD', into(textBox({ id: 'postCodeInput' })));
+ await click(button({ id: 'submit' }));
+ await waitFor(2000);
+ await click($('#duplicateEntryErrorModule button'));
+ await waitFor(2000);
+ const exists = await $('#duplicateEntryErrorModule').exists();
+ const result = await evaluate($('#itemTable'), (element) => element.innerText);
+ expect(exists).toBeFalsy();
+ expect(result).toBe('First name\tSecond name\tContact\tPostcode\tAction\nJohn\tDoe\t000000000\tAB101CD\tDelete');
+ }, 100000);
+ test('Should remove an item to the table', async () => {
+ await goto(URL);
+ await write('14', into(textBox({ id: 'answerInput' }), { force: true }));
+ await click(button({ id: 'submitPuzzle' }));
+ await waitFor(2000);
+ await click($('a.delete[data-index="0"]'));
+ const result = await evaluate($('#itemTable'), (element) => element.innerText);
+ expect(result).toBe('First name\tSecond name\tContact\tPostcode\tAction');
+ }, 100000);
+});
+
+afterAll(() => {
+ closeBrowser();
+});
\ No newline at end of file
diff --git a/app/app_aframe/Readme.md b/app/app_aframe/Readme.md
deleted file mode 100644
index f04533d5a..000000000
--- a/app/app_aframe/Readme.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# AFrame React (Basic)
-
-With this application I have built a simple 'Hello World' AFrame application, coupled with React as a means of managing state.
-
-### Explanation ###
-
-I have used Webpack to bundle JS dependancies and Babel to transpile to the correct ES version.
-
-The interest in this application is to explore WebXR prototyping using AFrame, with the aim of allowing user input via React in a follow up application.
-
-This application is intended for use in any modern WebXR enabled web browser or headset.
-
-This is one in a series of applications experimenting with WebXR. Currently only supporting 3 degrees of freedom and no controller input.
\ No newline at end of file
diff --git a/app/app_aframe/pug/index.pug b/app/app_aframe/pug/index.pug
deleted file mode 100644
index 35e40a083..000000000
--- a/app/app_aframe/pug/index.pug
+++ /dev/null
@@ -1,6 +0,0 @@
-extends ../../pug/layouts/main.pug
-
-block content
- p Compatible with any WebXR enabled browser and any WebXR enabled VR headset.
- p Click below to launch the AFrame scene.
- a(class="btn btn-dark", href="scene.html") Launch scene
\ No newline at end of file
diff --git a/app/app_aframe/pug/scene.pug b/app/app_aframe/pug/scene.pug
deleted file mode 100644
index d10a49f6f..000000000
--- a/app/app_aframe/pug/scene.pug
+++ /dev/null
@@ -1,4 +0,0 @@
-extends ../../pug/layouts/raw.pug
-
-block content
- div#result
\ No newline at end of file
diff --git a/app/app_aframe/src/app.js b/app/app_aframe/src/app.js
deleted file mode 100644
index c132d9e72..000000000
--- a/app/app_aframe/src/app.js
+++ /dev/null
@@ -1,30 +0,0 @@
-"use strict;"
-
-import "../sass/styles.scss";
-
-import 'aframe';
-import React from 'react';
-import ReactDOM from 'react-dom';
-class Scene extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- message: "Hello World!"
- };
- }
-
- render() {
- let text = `value: ${this.state.message}; color: #ee1111; height: 13.53; lineHeight: 0.54; width: 2.71; wrapCount: 36.5; xOffset: 1.02; zOffset: 0.51`;
- return (
-
-
-
-
-
-
-
- );
- }
-}
-
-ReactDOM.render(, document.getElementById('result'));
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreAsyncCoffeeMachine/pug/index.pug b/app/app_awsDotNetCoreAsyncCoffeeMachine/pug/index.pug
deleted file mode 100644
index b7a4cb2c3..000000000
--- a/app/app_awsDotNetCoreAsyncCoffeeMachine/pug/index.pug
+++ /dev/null
@@ -1,20 +0,0 @@
-extends ../../pug/layouts/main.pug
-
-block content
- div
- p
- | Think about how a cup of coffee is made.
- | Typically tasks are carried out while the kettle is boiling.
- | Why wait for a process to complete, when it could be run as a background task ?
- h2 How ?
- p
- | Technically, when calling the Sync method, a series of actions are run in order of
- | execution, synchronously. With each action completing before the next is run.
- | When calling the Async method, however async / await are being used, spawning
- | a State Machine object, with Task acting as the interface between code and state.
- | The threadpool is free to use new threads if needed and return execution instead
- | of blocking execution as with a synchronous program.
- p
- | Run the process of making a cup of coffee both synchronously and asynchronously.
- | Notice the difference in the order of tasks ?
- div#result.mt-2
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreEntitySortApi/Readme.md b/app/app_awsDotNetCoreEntitySortApi/Readme.md
deleted file mode 100644
index 6ac76f5b9..000000000
--- a/app/app_awsDotNetCoreEntitySortApi/Readme.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# ASP.NET Core Entity Sorting API. A Serverless Application with React Interface
-
-With this application I have built a complex type sorting interface, using .NET Core served via an AWS Serverless Application (SAM). A basic React user interface handles user input and application state.
-
-Whilst this functionality could be achieved with JavaScript alone, I wanted to demonstrate knowledge of two key .NET Core interfaces: ICompareable and IComparer.
-
-- AWS Lambda Repository (https://github.com/fsereno/app_awsDotNetCoreEntitySortApi)
-
-- ICompareable (https://docs.microsoft.com/en-us/dotnet/api/system.icomparable?view=netcore-3.1)
-
-- IComparer (https://docs.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=netcore-3.1)
-
-### Explanation ###
-
-This project shows how to run an ASP.NET Core Web API project as an AWS Lambda exposed through Amazon API Gateway. The NuGet package [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer) contains a Lambda function that is used to translate requests from API Gateway into the ASP.NET Core framework and then the responses from ASP.NET Core back to API Gateway.
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreShoppingCart/Readme.md b/app/app_awsDotNetCoreShoppingCart/Readme.md
deleted file mode 100644
index 707dd83f2..000000000
--- a/app/app_awsDotNetCoreShoppingCart/Readme.md
+++ /dev/null
@@ -1,13 +0,0 @@
-# ASP.NET Core Shopping Basket API. A Serverless Application with React Interface
-
-With this application I have built a simple shopping basket, each item consisting only of a primitive string value.
-
-The basket can be modified using the individual request inputs. The user interface and application state are React driven with data modification being processed via .NET Core running on AWS Lambda.
-
-Whilst this functionality could be achieved with JavaScript alone, I wanted to explore AWS Lambda and .NET Core and the use of List collections. So the decision was made to go serverless compute.
-
-- AWS Lambda Repository (https://github.com/fsereno/app_awsDotNetCoreShoppingCart)
-
-### Explanation ###
-
-This project shows how to run an ASP.NET Core Web API project as an AWS Lambda exposed through Amazon API Gateway. The NuGet package [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer) contains a Lambda function that is used to translate requests from API Gateway into the ASP.NET Core framework and then the responses from ASP.NET Core back to API Gateway.
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreShoppingCart/sass/styles.scss b/app/app_awsDotNetCoreShoppingCart/sass/styles.scss
deleted file mode 100644
index 52c550e33..000000000
--- a/app/app_awsDotNetCoreShoppingCart/sass/styles.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-@import "../../sass/includes/colours.scss";
-
-.api-submit {
- width: 5rem;
-}
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreShoppingCart/src/app.js b/app/app_awsDotNetCoreShoppingCart/src/app.js
deleted file mode 100644
index 7b17707f4..000000000
--- a/app/app_awsDotNetCoreShoppingCart/src/app.js
+++ /dev/null
@@ -1,284 +0,0 @@
-"use strict;"
-
-import '../sass/styles.scss';
-
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { PuzzleModalComponent } from '../../js/modules/react/puzzleModalComponent.js';
-import { SpinnerComponent } from '../../js/modules/react/spinnerComponent.js'
-import { ErrorModalComponent } from '../../js/modules/react/errorModalComponent.js';
-import { ConfigUtil } from '../../js/modules/utils/configUtil';
-import { FormComponent } from './formComponent';
-import { jQueryAjaxUtil } from '../../js/modules/utils/jQueryAjaxUtil';
-
-const APP_CONFIG = ConfigUtil.get("awsDotNetCoreShoppingCart");
-const GET_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.get}`;
-const ADD_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.add}`;
-const DELETE_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.delete}`;
-const UPDATE_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.update}`;
-const PUZZLE = "4 x 4 - 1 =";
-const DEFAULT_COLLECTION = [
- { name: "Apple" },
- { name: "Banana" }
-]
-class ShoppingListApp extends React.Component {
- constructor(props) {
- super(props);
- this.state = {
- items: DEFAULT_COLLECTION,
- resultSet: DEFAULT_COLLECTION,
- isValid: false,
- showSpinner: false,
- showPuzzleModal: true,
- showErrorModal: false,
- isPuzzleValid: false
- };
- this.handleGetSubmit = this.handleGetSubmit.bind(this);
- this.handleAddSubmit = this.handleAddSubmit.bind(this);
- this.handleUpdateSubmit = this.handleUpdateSubmit.bind(this);
- this.handleDeleteSubmit = this.handleDeleteSubmit.bind(this);
- this.handleIsPuzzleValid = this.handleIsPuzzleValid.bind(this);
- this.handlePuzzleModalClose = this.handlePuzzleModalClose.bind(this);
- this.handlePuzzleModalShow = this.handlePuzzleModalShow.bind(this);
- this.handleErrorModalClose = this.handleErrorModalClose.bind(this);
- this.handleBeforeAjax = this.handleBeforeAjax.bind(this);
- this.handleFailedAjax = this.handleFailedAjax.bind(this);
- }
-
- handleBeforeAjax() {
- this.setState({
- showSpinner: true,
- showPuzzleModal: false
- });
- }
-
- handleFailedAjax() {
- this.setState({
- showErrorModal: true,
- showSpinner: false
- });
- }
-
- handleAjax(request) {
- jQueryAjaxUtil.handleAjax(request, this.state.isPuzzleValid, this.handleBeforeAjax, this.handleFailedAjax, this.handlePuzzleModalShow);
- }
-
- handleGetSubmit(event) {
- event.preventDefault();
- let input = event.target.elements[0].value;
- let index = Number(input);
- let isValid = response => typeof response !== "undefined" && response.length > 0;
- let request = {
- url: GET_ENDPOINT,
- type: "POST",
- contentType: 'application/json;',
- data: JSON.stringify({
- "index": index,
- "items":this.state.items
- }),
- success: (response) => {
- if (isValid(response)) {
- this.setState({
- resultSet: response,
- showSpinner: false
- });
- }
- }
- }
- this.handleAjax(request);
- }
-
- handleAddSubmit(event) {
- event.preventDefault();
- let input = event.target.elements[0].value;
- let request = {
- url: ADD_ENDPOINT,
- type: "POST",
- contentType: 'application/json;',
- data: JSON.stringify({
- "item":{
- "name":input
- },
- "items":this.state.items
- }),
- success: (response) => {
- this.setState({
- resultSet: response,
- items: response,
- showSpinner: false
- });
- }
- }
- this.handleAjax(request);
- }
-
- handleUpdateSubmit(event) {
- event.preventDefault();
- let index = Number(event.target.elements[0].value);
- let value = event.target.elements[1].value;
- let request = {
- url: UPDATE_ENDPOINT,
- type: "POST",
- contentType: 'application/json;',
- data: JSON.stringify({
- "index":index,
- "item":{
- "name":value
- },
- "items":this.state.items
- }),
- success: (response) => {
- this.setState({
- resultSet: response,
- items: response,
- showSpinner: false
- });
- }
- }
- this.handleAjax(request);
- }
-
- handleDeleteSubmit(event) {
- event.preventDefault();
- let index = Number(event.target.elements[0].value);
- let request = {
- url: DELETE_ENDPOINT,
- type: "POST",
- contentType: 'application/json;',
- data: JSON.stringify({
- "index":index,
- "items":this.state.items
- }),
- success: (response) => {
- this.setState({
- resultSet: response,
- items: response,
- showSpinner: false
- });
- }
- }
- this.handleAjax(request);
- }
-
- handleIsPuzzleValid() {
- this.setState({
- isPuzzleValid: true,
- showPuzzleModal: false
- })
- }
-
- handlePuzzleModalClose() {
- this.setState({
- showPuzzleModal: false
- })
- }
-
- handlePuzzleModalShow() {
- this.setState({
- showPuzzleModal: true
- })
- }
-
- handleErrorModalClose() {
- this.setState({
- showErrorModal: false
- })
- }
-
- render() {
- return (
-
-
-
-
-
-
-
- Basket:
-
-
- {this.state.resultSet.map((item) => {
- return
{item.name}
- })}
-
-
- Use the below interface to alter the basket's contents:
-
-
-
-
-
-
-
-
-
-
-
-
- );
- }
-}
-
-ReactDOM.render(
- ,
- document.getElementById('result')
-);
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreShoppingCart/src/formComponent.js b/app/app_awsDotNetCoreShoppingCart/src/formComponent.js
deleted file mode 100644
index 18300a8a3..000000000
--- a/app/app_awsDotNetCoreShoppingCart/src/formComponent.js
+++ /dev/null
@@ -1,64 +0,0 @@
-"use strict;"
-
-import React, { useState } from 'react';
-import Button from 'react-bootstrap/Button';
-import Form from 'react-bootstrap/Form';
-import Row from 'react-bootstrap/Row';
-import Col from 'react-bootstrap/Col';
-import InputGroup from 'react-bootstrap/InputGroup';
-
-export function FormComponent(props) {
-
- const [validated, setValidated] = useState(false);
-
- const handleSubmit = (event) => {
- event.preventDefault();
- const form = event.currentTarget;
-
- if (form.checkValidity() === false) {
-
- event.stopPropagation();
-
- } else {
-
- props.handleSubmit(event);
-
- }
-
- setValidated(true);
- };
-
- return (
- <>
-
-
-
-
- {props.label}
-
-
- {props.children.map(item => {
- return ()
- })}
-
-
-
-
- {props.error || "Please enter a valid value."}
-
-
-
-
-
-
- >
- )
-}
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreShoppingCart/tests/e2e/taiko.test.js b/app/app_awsDotNetCoreShoppingCart/tests/e2e/taiko.test.js
deleted file mode 100644
index 2855435d5..000000000
--- a/app/app_awsDotNetCoreShoppingCart/tests/e2e/taiko.test.js
+++ /dev/null
@@ -1,77 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
-import { openBrowser, goto, write, click, closeBrowser, $, into, textBox, button, waitFor, evaluate } from 'taiko';
-
-const APPLICATION = "app_awsDotNetCoreShoppingCart";
-const URL = `http://localhost:8080/${APPLICATION}/index.html`;
-
-beforeAll(async () => {
- await openBrowser({
- headless: true,
- slowMo: 250,
- args: ['--no-sandbox']
- });
-});
-
-describe(APPLICATION, () => {
- test('Should get all items when no value is passed', async () => {
- await goto(URL);
- await write('15', into(textBox({ id: 'answerInput' }), { force: true }));
- await click(button({ id: 'submitPuzzle' }));
- await waitFor(2000);
- await click(button({ id: 'get_submit' }));
- await waitFor(2000);
- const result = await evaluate($('#basketItems'), (element) => element.innerText);
- expect(result).toBe("Apple\nBanana");
- }, 100000);
- test('Should get the correct single item when a position is passed', async () => {
- await goto(URL);
- await write('15', into(textBox({ id: 'answerInput' }), { force: true }));
- await click(button({ id: 'submitPuzzle' }));
- await waitFor(2000);
- await write('2', into(textBox({id:'get'})));
- await click(button({ id: 'get_submit' }));
- await waitFor(2000);
- const result = await evaluate($('#basketItems'), (element) => element.innerText);
- expect(result).toBe("Banana");
- }, 100000);
- test('Should add a single item to the existing collection', async () => {
- await goto(URL);
- await write('15', into(textBox({ id: 'answerInput' }), { force: true }));
- await click(button({ id: 'submitPuzzle' }));
- await waitFor(2000);
- await write('Bread', into(textBox({id:'add'})));
- await click(button({ id: 'add_submit' }));
- await waitFor(2000);
- const result = await evaluate($('#basketItems'), (element) => element.innerText);
- expect(result).toBe("Apple\nBanana\nBread");
- }, 100000);
- test('Should delete the correct item when a position is passed', async () => {
- await goto(URL);
- await write('15', into(textBox({ id: 'answerInput' }), { force: true }));
- await click(button({ id: 'submitPuzzle' }));
- await waitFor(2000);
- await write('2', into(textBox({id:'delete'})));
- await click(button({ id: 'delete_submit' }));
- await waitFor(2000);
- const result = await evaluate($('#basketItems'), (element) => element.innerText);
- expect(result).toBe("Apple");
- }, 100000);
- test('Should update a single item in the existing collection', async () => {
- await goto(URL);
- await write('15', into(textBox({ id: 'answerInput' }), { force: true }));
- await click(button({ id: 'submitPuzzle' }));
- await waitFor(2000);
- await write('2', into(textBox({id:'update_position'})));
- await write('Wine', into(textBox({id:'update_value'})));
- await click(button({ id: 'update_submit' }));
- await waitFor(2000);
- const result = await evaluate($('#basketItems'), (element) => element.innerText);
- expect(result).toBe("Apple\nWine");
- }, 100000);
-});
-afterAll(() => {
- closeBrowser();
-});
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreStringSortApi/Readme.md b/app/app_awsDotNetCoreStringSortApi/Readme.md
deleted file mode 100644
index 5b9dc024f..000000000
--- a/app/app_awsDotNetCoreStringSortApi/Readme.md
+++ /dev/null
@@ -1,17 +0,0 @@
-# ASP.NET Core Natural String Sorting API. A Serverless Application with React Interface
-
-With this application I have built a natural string sorting algorithm, using .NET Core served via an AWS Serverless Application (SAM). A basic React user interface handles user input and application state.
-
-With this application I have implemented IComparer to handle the sorting logic, comparing chunks of alpha and numeric values in order to achieve a natural sort order.
-
-Whilst this functionality could be achieved with JavaScript alone, I wanted to demonstrate knowledge of IComparer and the .NET Core framework along with AWS Lambda.
-
-A lot of research was undertaken for this application, with many permutations in order to overcome this simple, yet complex task. Made for a very enjoyable project!
-
-- AWS Lambda Repository (https://github.com/fsereno/app_awsDotNetCoreStringSortApi)
-
-- IComparer (https://docs.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=netcore-3.1)
-
-### Explanation ###
-
-This project shows how to run an ASP.NET Core Web API project as an AWS Lambda exposed through Amazon API Gateway. The NuGet package [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer) contains a Lambda function that is used to translate requests from API Gateway into the ASP.NET Core framework and then the responses from ASP.NET Core back to API Gateway.
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreStringSortApi/sass/styles.scss b/app/app_awsDotNetCoreStringSortApi/sass/styles.scss
deleted file mode 100644
index bfe962e64..000000000
--- a/app/app_awsDotNetCoreStringSortApi/sass/styles.scss
+++ /dev/null
@@ -1,11 +0,0 @@
-@import "../../sass/includes/colours.scss";
-
-body {
-
- h1 {
-
- color: $black;
-
- }
-
-}
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreStringSortApi/tests/e2e/taiko.test.js b/app/app_awsDotNetCoreStringSortApi/tests/e2e/taiko.test.js
deleted file mode 100644
index ffa58fb7f..000000000
--- a/app/app_awsDotNetCoreStringSortApi/tests/e2e/taiko.test.js
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * @jest-environment jsdom
- */
-
- import { openBrowser, goto, write, click, closeBrowser, $, into, textBox, button, waitFor, evaluate } from 'taiko';
-
- const APPLICATION = "app_awsDotNetCoreStringSortApi";
- const URL = `http://localhost:8080/${APPLICATION}/index.html`;
-
- beforeAll(async () => {
- await openBrowser({
- headless: true,
- slowMo: 250,
- args: ['--no-sandbox']
- });
- });
-
- describe(APPLICATION, () => {
- test('Should add an item', async () => {
- await goto(URL);
- await write('11', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
- await write('C,B,A,10,1', into(textBox({id:'valuesInput'})));
- await click(button({id:'sort_submit'}));
- const result = await evaluate($('#resultOutput'), (element) => element.innerText);
- expect(result).toBe('1,10,A,B,C');
- }, 100000);
- });
-
- afterAll(() => {
- closeBrowser();
- });
\ No newline at end of file
diff --git a/app/app_awsNodeToDoApi/Readme.md b/app/app_awsNodeToDoApi/Readme.md
deleted file mode 100644
index ff7c8b67e..000000000
--- a/app/app_awsNodeToDoApi/Readme.md
+++ /dev/null
@@ -1,21 +0,0 @@
-# AWS driven, B2C API, To-Do List SPA
-
-With this application I have extended previous To-Do concepts. This application is driven by AWS with Node.js and is authenticated by Cognito. Data is persisted with Dynamo DB and an AWS HTTP API Gateway serves all client requests, protected by an "authorizor".
-
-I have used the Serverless Framework architecture to build this application. This has allowed for a great, zero friction, workflow.
-
-The frontend is using React with React Router, state is manage by a combination of React Context and the Context Provider Pattern.
-
-- AWS Repository (https://github.com/fsereno/app_awsNodeToDoApi)
-- Serverless (https://www.serverless.com/)
-- AWS Cognito (https://aws.amazon.com/cognito/)
-- AWS Dynamo (https://aws.amazon.com/dynamodb)
-- AWS API Gateway (https://aws.amazon.com/api-gateway/)
-- Node.js (https://nodejs.org/en/)
-- React Hooks (https://reactjs.org/docs/hooks-reference.html)
-- React Router (https://reactrouter.com/web/guides/quick-start)
-- React Context (https://reactjs.org/docs/context.html)
-
-### Explanation ###
-
-The interest in this application has come from a great deal of research and the need to demonstrate a full, end to end application, covering frontend, authentication, backend business logic and data store.
\ No newline at end of file
diff --git a/app/app_awsNodeToDoApi/pug/index.pug b/app/app_awsNodeToDoApi/pug/index.pug
deleted file mode 100644
index da50e2869..000000000
--- a/app/app_awsNodeToDoApi/pug/index.pug
+++ /dev/null
@@ -1,4 +0,0 @@
-extends ../../pug/layouts/main.pug
-
-block content
- div#result
\ No newline at end of file
diff --git a/app/app_awsNodeToDoApi/src/components/contextProviders/loginContextProvider.js b/app/app_awsNodeToDoApi/src/components/contextProviders/loginContextProvider.js
deleted file mode 100644
index 8d9b38e29..000000000
--- a/app/app_awsNodeToDoApi/src/components/contextProviders/loginContextProvider.js
+++ /dev/null
@@ -1,115 +0,0 @@
-"use strict;"
-
-import React, { useState, useLayoutEffect, useRef } from 'react';
-import { CognitoUser, AuthenticationDetails, CognitoUserPool } from 'amazon-cognito-identity-js';
-import { LoginContext } from '../../contexts';
-import { SUCCESS } from "../../constants";
-
-export const LoginContextProvider = ({ children, poolData }) => {
-
- const [authenticated, setAuthenticated] = useState(false);
-
- const userPool = new CognitoUserPool(poolData);
-
- const token = useRef();
- const username = useRef();
-
- const getCurrentUser = () => new Promise((resolve, reject) => {
-
- const currentUser = userPool.getCurrentUser();
-
- if (currentUser != null) {
-
- currentUser.getSession(err => {
-
- if (err != null && currentUser.signInUserSession != null) {
- reject(undefined);
- console.error(err.message);
- } else {
- resolve(currentUser);
- }
- });
- } else {
- reject(undefined);
- }
- });
-
- const logoutUser = () => new Promise((resolve, reject) => {
- getCurrentUser().then(currentUser => {
- if (currentUser) {
- currentUser.globalSignOut({
- onSuccess: function (result) {
- if (result === SUCCESS) {
- setAuthenticated(false);
- resolve({ success: true });
- }
- },
- onFailure: function (error) {
- reject({ success: false, error });
- },
- });
- }
-
- }).catch((error) => {
- reject({ success: false, error });
- });
- });
-
- const loginUser = (username, password) => new Promise((resolve, reject) => {
-
- const authenticationData = {
- Username: username,
- Password: password,
- };
-
- const authenticationDetails = new AuthenticationDetails(authenticationData);
-
- const userData = {
- Username: username,
- Pool: new CognitoUserPool(poolData),
- };
-
- const cognitoUser = new CognitoUser(userData);
-
- cognitoUser.authenticateUser(authenticationDetails, {
- onSuccess: function (result) {
- setAuthenticated(true);
- resolve({ success: true });
- },
- onFailure: function (error) {
- reject({ success: false, error });
- },
- });
- });
-
- useLayoutEffect(() => {
- getCurrentUser()
- .then(currentUser => {
- if (currentUser) {
-
- token.current = currentUser.signInUserSession.idToken.jwtToken;
- username.current = currentUser.username;
-
- if (!authenticated) {
- setAuthenticated(true);
- }
- }
- })
- .catch(() => setAuthenticated(false));
- }, [authenticated]);
-
- const context = {
- authenticated,
- setAuthenticated,
- loginUser,
- logoutUser,
- token,
- username
- };
-
- return (
-
- {children}
-
- )
-}
\ No newline at end of file
diff --git a/app/app_awsNodeToDoApi/src/components/protectedRoute.js b/app/app_awsNodeToDoApi/src/components/protectedRoute.js
deleted file mode 100644
index e331108a9..000000000
--- a/app/app_awsNodeToDoApi/src/components/protectedRoute.js
+++ /dev/null
@@ -1,21 +0,0 @@
-"use strict;"
-
-import React from 'react';
-import { LoginContext } from '../contexts';
-import { Unauthorised } from './pages/unauthorised';
-
-export const ProtectedRoute = ({Component}) => {
-
- const loginContext = React.useContext(LoginContext);
-
- return (
- <>
- {loginContext.authenticated &&
-
- }
- {!loginContext.authenticated &&
-
- }
- >
- );
-}
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreAsyncCoffeeMachine/Readme.md b/app/app_coffeeMachine/Readme.md
similarity index 59%
rename from app/app_awsDotNetCoreAsyncCoffeeMachine/Readme.md
rename to app/app_coffeeMachine/Readme.md
index 941ea7c9b..7334511bf 100644
--- a/app/app_awsDotNetCoreAsyncCoffeeMachine/Readme.md
+++ b/app/app_coffeeMachine/Readme.md
@@ -1,15 +1,9 @@
-# AWS .NET Core Asynchronous Coffee Maker. A Serverless Application with React Interface
+# .NET Asynchronous Coffee Maker
This application demonstrates knowledge of multithreading with use of the async, await, Task and the State Machine. The process of making a cup of coffee can be either run synchronously or asynchronously. The order in which the steps are carried out depends on available threads. Synchronously, with only a single thread, every task must finish before the next. Asynchronously, we are able to run background tasks, such as boiling the kettle while we get cups out of a cupboard using multiple threads.
Each step is entered into a Log, which is updated by reference during the process. I have also implemented IEnumerable and IEnumorator to demonstrate the iterator pattern. Keeping the Log structure hidden and allowing indirect, readonly iteration of the Log collection.
-- AWS Lambda Repository (https://github.com/fsereno/app_awsDotNetCoreAsyncCoffeeMachine)
-
- IEnumerable (https://docs.microsoft.com/en-us/dotnet/api/system.collections.ienumerable?view=netcore-3.1)
-- IEnumorator (https://docs.microsoft.com/en-us/dotnet/api/system.collections.ienumerator?view=netcore-3.1)
-
-### Explanation ###
-
-This project shows how to run an ASP.NET Core Web API project as an AWS Lambda exposed through Amazon API Gateway. The NuGet package [Amazon.Lambda.AspNetCoreServer](https://www.nuget.org/packages/Amazon.Lambda.AspNetCoreServer) contains a Lambda function that is used to translate requests from API Gateway into the ASP.NET Core framework and then the responses from ASP.NET Core back to API Gateway.
\ No newline at end of file
+- IEnumorator (https://docs.microsoft.com/en-us/dotnet/api/system.collections.ienumerator?view=netcore-3.1)
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/api/Controllers/ApiController.cs b/app/app_coffeeMachine/backend/api/Controllers/ApiController.cs
new file mode 100644
index 000000000..4fd761d45
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Controllers/ApiController.cs
@@ -0,0 +1,70 @@
+using Microsoft.AspNetCore.Mvc;
+using Portfolio.Core.Types;
+using Portfolio.Core.Services;
+using Portfolio.CoffeeMachine.Interfaces;
+using Portfolio.CoffeeMachine.Utils;
+using Portfolio.CoffeeMachine.Models;
+
+namespace Portfolio.CoffeeMachine.Controllers;
+
+///
+/// API controller for Coffee Machine operations.
+///
+[ApiController]
+[Route("[controller]")]
+public class ApiController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly TestService _testService;
+ private readonly ITaskRunner _coffeeMakerUtil;
+
+ public ApiController(
+ ILogger logger,
+ TestService testService,
+ ITaskRunner coffeeMakerUtil)
+ {
+ _logger = logger;
+ _testService = testService;
+ _coffeeMakerUtil = coffeeMakerUtil;
+ }
+
+ ///
+ /// Executes the Coffee Maker process asynchronously and returns the log items.
+ ///
+ /// A list of log items generated during the process.
+ [HttpGet("RunAsync")]
+ public async Task> RunAsync()
+ {
+ Log log;
+
+ try
+ {
+ log = await _coffeeMakerUtil.RunAsync();
+ }
+ catch (Exception exception)
+ {
+ throw new Exception("Unable to run process: " + exception.Message);
+ }
+ return log?.Get();
+ }
+
+ ///
+ /// Executes the Coffee Maker process synchronously and returns the log items.
+ ///
+ /// A list of log items generated during the process.
+ [HttpGet("Run")]
+ public List Run()
+ {
+ Log log;
+
+ try
+ {
+ log = _coffeeMakerUtil.Run();
+ }
+ catch (Exception exception)
+ {
+ throw new Exception("Unable to run process: " + exception.Message);
+ }
+ return log?.Get();
+ }
+}
diff --git a/app/app_coffeeMachine/backend/api/Interfaces/ITaskRunner.cs b/app/app_coffeeMachine/backend/api/Interfaces/ITaskRunner.cs
new file mode 100644
index 000000000..c47d8999d
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Interfaces/ITaskRunner.cs
@@ -0,0 +1,41 @@
+using System.Threading.Tasks;
+using Portfolio.CoffeeMachine.Models;
+
+namespace Portfolio.CoffeeMachine.Interfaces
+{
+ public interface ITaskRunner
+ {
+ ///
+ /// Run tasks synchronously
+ ///
+ /// A Log of taks run
+ Log Run();
+
+ ///
+ /// Run tasks Asynchronously
+ ///
+ /// A Log of taks run
+ Task RunAsync();
+
+ ///
+ /// Carry out the singular instruction immediately with no waiting
+ ///
+ /// An instruction as string
+ void Do(string instruction);
+
+ ///
+ /// Carry out the specified instruction synchronously waiting for a specified delay
+ ///
+ /// An instruction as string
+ /// A delay in seconds
+ void Start(string instruction, int seconds);
+
+ ///
+ /// Carry out the specified instruction Asynchronously awaiting for a specified delay
+ ///
+ /// An instruction as string
+ /// A delay in seconds
+ /// A Task boolean when the task is complete
+ Task StartAsync(string instruction, int seconds);
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/api/Models/Log.cs b/app/app_coffeeMachine/backend/api/Models/Log.cs
new file mode 100644
index 000000000..7f046301b
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Models/Log.cs
@@ -0,0 +1,134 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Portfolio.CoffeeMachine.Models
+{
+ ///
+ /// Represents a log of coffee machine activities.
+ ///
+ public class Log : IEnumerable
+ {
+ private List _log;
+
+ ///
+ /// Initializes a new instance of the Log class.
+ ///
+ public Log()
+ {
+ _log = new List();
+ }
+
+ ///
+ /// Gets the log items in the log.
+ ///
+ /// A list of log items.
+ public List Get()
+ {
+ var log = new List();
+ foreach(var item in _log)
+ {
+ log.Add(item);
+ }
+ return log;
+ }
+
+ ///
+ /// Gets the number of log items in the log.
+ ///
+ /// The number of log items.
+ public int Count()
+ {
+ return _log.Count;
+ }
+
+ ///
+ /// Adds a log item to the log.
+ ///
+ /// The log item to add.
+ public void Add(LogItem logItem)
+ {
+ _log.Add(logItem);
+ }
+
+ ///
+ /// Returns an enumerator that iterates through the log.
+ ///
+ /// An enumerator for the log.
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return (IEnumerator) GetEnumerator();
+ }
+
+ ///
+ /// Returns an enumerator that iterates through the log.
+ ///
+ /// An enumerator for the log.
+ public LogEnumerator GetEnumerator()
+ {
+ return new LogEnumerator(_log);
+ }
+
+ ///
+ /// Represents an enumerator for the Log class.
+ ///
+ public class LogEnumerator : IEnumerator
+ {
+ private int _position = -1;
+ private List _log;
+
+ ///
+ /// Initializes a new instance of the LogEnumerator class with the specified log.
+ ///
+ /// The log to enumerate.
+ public LogEnumerator(List log)
+ {
+ _log = log;
+ }
+
+ ///
+ /// Advances the enumerator to the next log item.
+ ///
+ /// true if the enumerator was successfully advanced to the next log item; false if the enumerator has reached the end of the log.
+ public bool MoveNext()
+ {
+ _position++;
+ return (_position < _log.Count);
+ }
+
+ ///
+ /// Resets the enumerator to its initial position, before the first log item.
+ ///
+ public void Reset()
+ {
+ _position = -1;
+ }
+
+ object IEnumerator.Current
+ {
+ get
+ {
+ return Current;
+ }
+ }
+
+ ///
+ /// Gets the current log item in the log.
+ ///
+ public LogItem Current
+ {
+ get
+ {
+ try
+ {
+ return _log[_position];
+ }
+ catch (IndexOutOfRangeException)
+ {
+ throw new InvalidOperationException();
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/api/Models/LogItem.cs b/app/app_coffeeMachine/backend/api/Models/LogItem.cs
new file mode 100644
index 000000000..69e86565c
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Models/LogItem.cs
@@ -0,0 +1,29 @@
+namespace Portfolio.CoffeeMachine.Models
+{
+ ///
+ /// Represents a single log item in the coffee machine log.
+ ///
+ public class LogItem
+ {
+ ///
+ /// Initializes a new instance of the LogItem class with the specified detail and thread.
+ ///
+ /// The detail of the log item.
+ /// The thread associated with the log item.
+ public LogItem(string detail, int thread)
+ {
+ this.Detail = detail;
+ this.Thread = thread;
+ }
+
+ ///
+ /// Gets or sets the detail of the log item.
+ ///
+ public string Detail { get; set; }
+
+ ///
+ /// Gets or sets the thread associated with the log item.
+ ///
+ public int Thread { get; set; }
+ }
+}
diff --git a/app/app_coffeeMachine/backend/api/Program.cs b/app/app_coffeeMachine/backend/api/Program.cs
new file mode 100644
index 000000000..c5e1fc218
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Program.cs
@@ -0,0 +1,40 @@
+using Portfolio.Core.Services;
+using Portfolio.CoffeeMachine.Interfaces;
+using Portfolio.CoffeeMachine.Utils;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://*:3001");
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Services.AddTransient();
+
+// Services
+builder.Services.AddScoped();
+
+builder.Services.AddHealthChecks();
+
+var app = builder.Build();
+
+app.MapHealthChecks("/healthcheck");
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/app/app_coffeeMachine/backend/api/Properties/launchSettings.json b/app/app_coffeeMachine/backend/api/Properties/launchSettings.json
new file mode 100644
index 000000000..d58642981
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Properties/launchSettings.json
@@ -0,0 +1,41 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:4279",
+ "sslPort": 44379
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7114;http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/app/app_coffeeMachine/backend/api/Utils/CoffeeMakerUtil.cs b/app/app_coffeeMachine/backend/api/Utils/CoffeeMakerUtil.cs
new file mode 100644
index 000000000..7b39f0072
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/Utils/CoffeeMakerUtil.cs
@@ -0,0 +1,106 @@
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Portfolio.CoffeeMachine.Interfaces;
+using Portfolio.CoffeeMachine.Models;
+
+namespace Portfolio.CoffeeMachine.Utils
+{
+ ///
+ /// Utility class for running coffee machine tasks.
+ ///
+ public class CoffeeMakerUtil : ITaskRunner
+ {
+ private Log _log;
+ private readonly ILogger _logger;
+
+ ///
+ /// Initializes a new instance of the CoffeeMakerUtil class with the specified logger.
+ ///
+ /// The logger to use for logging messages.
+ public CoffeeMakerUtil(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ public Log Run()
+ {
+ _log = new Log();
+ _logger.LogInformation("Starting synchronous process");
+
+ this.Start("boiling the kettle", 3000);
+ this.Do("get coffee from cupboard");
+ this.Do("pack coffee into cafetiere");
+ this.Do("get cup from cupboard");
+ this.Do("get milk from fridge");
+ this.Do("pour milk into cup");
+ this.Do("put cup in microwave");
+ this.Start("microwaving cup", 3500);
+ this.Do("pour boiling water into cafetiere");
+ this.Do("brew the coffee");
+ this.Do("get cup from microwave");
+ this.Do("plunge cafetiere");
+ this.Do("pour coffee into cup");
+ this.Do("stir coffee");
+ this.Do("drink coffee");
+
+ _logger.LogInformation("Ending synchronous process");
+
+ return _log;
+ }
+
+ ///
+ public async Task RunAsync()
+ {
+ _log = new Log();
+ _logger.LogInformation("Starting asynchronous process");
+
+ var boilingWaterTask = this.StartAsync("boiling the kettle", 3000);
+ this.Do("get coffee from cupboard");
+ this.Do("pack coffee into cafetiere");
+ this.Do("get cup from cupboard");
+ this.Do("get milk from fridge");
+ this.Do("pour milk into cup");
+ this.Do("put cup in microwave");
+ var microwavingCupTask = this.StartAsync("microwaving cup", 3500);
+ var waterBoild = await boilingWaterTask;
+ this.Do("pour boiling water into cafetiere");
+ this.Do("brew the coffee");
+ var cupMicrowaved = await microwavingCupTask;
+ this.Do("get cup from microwave");
+ this.Do("plunge cafetiere");
+ this.Do("pour coffee into cup");
+ this.Do("stir coffee");
+ this.Do("drink coffee");
+
+ _logger.LogInformation("Ending asynchronous process");
+
+ return _log;
+ }
+
+ ///
+ public async Task StartAsync(string instruction, int seconds)
+ {
+ _log.Add(new LogItem($"Start {instruction.ToLower()}", Thread.CurrentThread.ManagedThreadId));
+ await Task.Delay(seconds);
+ _log.Add(new LogItem($"Finished {instruction.ToLower()}", Thread.CurrentThread.ManagedThreadId));
+ return true;
+ }
+
+ ///
+ public void Start(string instruction, int seconds)
+ {
+ _log.Add(new LogItem($"Start {instruction.ToLower()}", Thread.CurrentThread.ManagedThreadId));
+ Thread.Sleep(seconds);
+ _log.Add(new LogItem($"Finished {instruction.ToLower()}", Thread.CurrentThread.ManagedThreadId));
+ }
+
+ ///
+ public void Do(string instruction)
+ {
+ var detail = char.ToUpper(instruction[0])+instruction.Substring(1);
+ _log.Add(new LogItem(detail, Thread.CurrentThread.ManagedThreadId));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/api/api.csproj b/app/app_coffeeMachine/backend/api/api.csproj
new file mode 100644
index 000000000..19bfaa242
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/api.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/app_coffeeMachine/backend/api/appsettings.Development.json b/app/app_coffeeMachine/backend/api/appsettings.Development.json
new file mode 100644
index 000000000..ff66ba6b2
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/app/app_coffeeMachine/backend/api/appsettings.json b/app/app_coffeeMachine/backend/api/appsettings.json
new file mode 100644
index 000000000..4d566948d
--- /dev/null
+++ b/app/app_coffeeMachine/backend/api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/app/app_coffeeMachine/backend/test/CoffeeMakerUtilTests.cs b/app/app_coffeeMachine/backend/test/CoffeeMakerUtilTests.cs
new file mode 100644
index 000000000..76a7b142a
--- /dev/null
+++ b/app/app_coffeeMachine/backend/test/CoffeeMakerUtilTests.cs
@@ -0,0 +1,115 @@
+using System;
+using System.Linq;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Portfolio.CoffeeMachine.Utils;
+using Portfolio.CoffeeMachine.Models;
+using Xunit;
+
+namespace Portfolio.CoffeeMachine.Tests
+{
+ public class CoffeeMakerUtilTests
+ {
+ private readonly CoffeeMakerUtil _sut;
+
+ private readonly Mock> _logger;
+ public CoffeeMakerUtilTests()
+ {
+ _logger = new Mock>();
+ _sut = new CoffeeMakerUtil(_logger.Object);
+ }
+
+ [Fact]
+ public void Test_Run_Has_Correct_Numer_Of_Steps()
+ {
+ var log = _sut.Run();
+ var result = log.Get().Count == 17;
+ Assert.True(result);
+ }
+
+ [Fact]
+ public void Test_Run_All_Steps_On_Same_Thread()
+ {
+ var log = _sut.Run();
+ var threads = log.Get().Select(x => x.Thread);
+ var result = threads.Distinct().Count() == 1;
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task Test_RunAsync_Has_Correct_Numer_Of_Steps()
+ {
+ var log = await _sut.RunAsync();
+ var result = log.Get().Count == 17;
+ Assert.True(result);
+ }
+
+ [Fact]
+ public async Task Test_RunAsync_Has_The_Correct_Order_For_Kettle()
+ {
+ var log = await _sut.RunAsync();
+ var indexes = GetIndexOfOrderedTasks(log, "finished boiling the kettle", "pour boiling water into cafetiere");
+ var result = indexes.IndexOfFirstTask < indexes.IndexOfSecondTask;
+ Assert.True(true);
+ }
+
+ [Fact]
+ public async Task Test_RunAsync_Has_The_Correct_Order_For_Microwave()
+ {
+ var log = await _sut.RunAsync();
+ var indexes = GetIndexOfOrderedTasks(log, "finished microwaving cup", "get cup from microwave");
+ var result = indexes.IndexOfFirstTask < indexes.IndexOfSecondTask;
+ Assert.True(true);
+ }
+
+ [Fact]
+ public void Test_Run_Verify_Logging()
+ {
+ var log = _sut.Run();
+ VerifyLogger(LogLevel.Information, "Starting synchronous process");
+ VerifyLogger(LogLevel.Information, "Ending synchronous process");
+ }
+
+ [Fact]
+ public async Task Test_Run_Async_Verify_Logging()
+ {
+ var log = await _sut.RunAsync();
+ VerifyLogger(LogLevel.Information, "Starting asynchronous process");
+ VerifyLogger(LogLevel.Information, "Ending asynchronous process");
+ }
+
+ private (int IndexOfFirstTask, int IndexOfSecondTask) GetIndexOfOrderedTasks(Log log, string detailOfFirstItem, string detailOfSecondItem)
+ {
+ var indexOfFirstTask = 0;
+ var indexOfSecondTask = 0;
+
+ var i = 0;
+ foreach (var item in log)
+ {
+ if (item.Detail.ToLower() == detailOfFirstItem.ToLower())
+ {
+ indexOfFirstTask = i;
+ }
+
+ if (item.Detail.ToLower() == detailOfSecondItem.ToLower())
+ {
+ indexOfSecondTask = i;
+ }
+ i++;
+ }
+ return (indexOfFirstTask, indexOfSecondTask);
+ }
+
+ private void VerifyLogger(LogLevel expectedLogLevel, string expectedMessage = "")
+ {
+ _logger.Verify(
+ x => x.Log(
+ It.Is(l => l == expectedLogLevel),
+ It.IsAny(),
+ It.Is((v, t) => String.IsNullOrEmpty(expectedMessage) ? true : v.ToString() == expectedMessage),
+ It.IsAny(),
+ It.Is>((v, t) => true)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/test/LogTests.cs b/app/app_coffeeMachine/backend/test/LogTests.cs
new file mode 100644
index 000000000..273a457c1
--- /dev/null
+++ b/app/app_coffeeMachine/backend/test/LogTests.cs
@@ -0,0 +1,32 @@
+using System.Collections.Generic;
+using Portfolio.CoffeeMachine.Models;
+using Xunit;
+
+namespace Portfolio.CoffeeMachine.Tests
+{
+ public class LogTests
+ {
+ private readonly Log _sut;
+ public LogTests()
+ {
+ _sut = new Log();
+ }
+
+ [Fact]
+ public void Test_Log()
+ {
+ _sut.Add(new LogItem("Instruct 1", 10));
+ _sut.Add(new LogItem("Instruct 2", 20));
+ _sut.Add(new LogItem("Instruct 3", 30));
+
+ var result = new List();
+
+ foreach(var item in _sut)
+ {
+ result.Add(item.Detail);
+ }
+
+ Assert.True(result.Count == 3);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/test/Usings.cs b/app/app_coffeeMachine/backend/test/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/app/app_coffeeMachine/backend/test/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/test/appsettings.json b/app/app_coffeeMachine/backend/test/appsettings.json
new file mode 100644
index 000000000..6ba3a30b2
--- /dev/null
+++ b/app/app_coffeeMachine/backend/test/appsettings.json
@@ -0,0 +1,14 @@
+{
+ "Lambda.Logging": {
+ "IncludeCategory": false,
+ "IncludeLogLevel": false,
+ "IncludeNewline": true,
+ "LogLevel": {
+ "Default": "Debug",
+ "Microsoft": "Information"
+ }
+ },
+ "AWS": {
+ "Region": ""
+ }
+}
\ No newline at end of file
diff --git a/app/app_coffeeMachine/backend/test/test.csproj b/app/app_coffeeMachine/backend/test/test.csproj
new file mode 100644
index 000000000..a3869e69e
--- /dev/null
+++ b/app/app_coffeeMachine/backend/test/test.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/app/app_coffeeMachine/pug/index.pug b/app/app_coffeeMachine/pug/index.pug
new file mode 100644
index 000000000..ad0fb8610
--- /dev/null
+++ b/app/app_coffeeMachine/pug/index.pug
@@ -0,0 +1,20 @@
+extends ../../pug/layouts/main.pug
+
+block content
+ div
+ p
+ | Think about how a cup of coffee is made.
+ | Typically tasks are carried out while the kettle is boiling.
+ | Why wait for a process to complete, when it could be run as a background task ?
+ h2 How ?
+ p
+ | Technically, when calling the 'Sync' method in .NET, a series of actions is executed
+ | synchronously in the order they are called. Each action completes before the next one is
+ | executed. On the other hand, when calling the 'Async' method, the async/await
+ | keywords are used, which create a state machine object. The 'Task' type serves as the
+ | interface between the code and the state machine. The thread pool in .NET is free to use new
+ | threads if necessary and allows execution to continue without blocking, unlike in a synchronous program.
+ p
+ | Run the process of making a cup of coffee both synchronously and asynchronously.
+ | Notice the difference in the order of tasks ?
+ div#result.mt-2
\ No newline at end of file
diff --git a/app/app_awsDotNetCoreAsyncCoffeeMachine/sass/styles.scss b/app/app_coffeeMachine/sass/styles.scss
similarity index 100%
rename from app/app_awsDotNetCoreAsyncCoffeeMachine/sass/styles.scss
rename to app/app_coffeeMachine/sass/styles.scss
diff --git a/app/app_awsDotNetCoreAsyncCoffeeMachine/src/app.js b/app/app_coffeeMachine/src/app.js
similarity index 100%
rename from app/app_awsDotNetCoreAsyncCoffeeMachine/src/app.js
rename to app/app_coffeeMachine/src/app.js
diff --git a/app/app_awsDotNetCoreAsyncCoffeeMachine/src/coffeeMakerApp.js b/app/app_coffeeMachine/src/coffeeMakerApp.js
similarity index 64%
rename from app/app_awsDotNetCoreAsyncCoffeeMachine/src/coffeeMakerApp.js
rename to app/app_coffeeMachine/src/coffeeMakerApp.js
index 0301d9bc7..517cf72fa 100644
--- a/app/app_awsDotNetCoreAsyncCoffeeMachine/src/coffeeMakerApp.js
+++ b/app/app_coffeeMachine/src/coffeeMakerApp.js
@@ -1,30 +1,28 @@
"use strict;"
import React from 'react';
-import { PuzzleModalComponent } from '../../js/modules/react/puzzleModalComponent.js';
import { SpinnerComponent } from '../../js/modules/react/spinnerComponent.js'
import { ErrorModalComponent } from '../../js/modules/react/errorModalComponent.js';
import CoffeeMakerComponent from './coffeeMakerComponent';
import { ConfigUtil } from "../../js/modules/utils/configUtil";
import { jQueryAjaxUtil } from '../../js/modules/utils/jQueryAjaxUtil';
+import { DeploymentModalComponent } from '../../js/modules/react/deploymentModalComponent.js';
+import { DeploymentUtil } from '../../js/modules/utils/deploymentUtil';
+
+const CONFIG = ConfigUtil.get();
+const APP_CONFIG = ConfigUtil.get("coffeeMachine");
+const RUN_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.run}`;
+const RUN_ASYNC_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.runAsync}`;
-const PUZZLE = "3 + 1 + 1 =";
-const APP_CONFIG = ConfigUtil.get("awsDotNetCoreAsyncCoffeeMachine");
-const RUN_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.run}`;
-const RUN_ASYNC_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.runAsync}`;
export default class CoffeeMakerApp extends React.Component {
constructor(props) {
super(props);
this.state = {
log: [],
showSpinner: false,
- showPuzzleModal: true,
- showErrorModal: false,
- isPuzzleValid: false
+ showDeploymentModal: DeploymentUtil.isNotCloud(),
+ showErrorModal: false
};
- this.handleIsPuzzleValid = this.handleIsPuzzleValid.bind(this);
- this.handlePuzzleModalClose = this.handlePuzzleModalClose.bind(this);
- this.handlePuzzleModalShow = this.handlePuzzleModalShow.bind(this);
this.handleErrorModalClose = this.handleErrorModalClose.bind(this);
this.handleBeforeAjax = this.handleBeforeAjax.bind(this);
this.handleFailedAjax = this.handleFailedAjax.bind(this);
@@ -32,6 +30,8 @@ export default class CoffeeMakerApp extends React.Component {
this.handleRunAsync = this.handleRunAsync.bind(this);
this.handleAjax = this.handleAjax.bind(this);
this.handleRequest = this.handleRequest.bind(this);
+ this.handleDeploymentModalClose = this.handleDeploymentModalClose.bind(this);
+ this.handleDeploymentModalShow = this.handleDeploymentModalShow.bind(this);
}
handleRun() {
@@ -60,16 +60,15 @@ export default class CoffeeMakerApp extends React.Component {
handleAjax(request) {
jQueryAjaxUtil.handleAjax(
request,
- this.state.isPuzzleValid,
+ DeploymentUtil.isCloud(),
this.handleBeforeAjax,
this.handleFailedAjax,
- this.handlePuzzleModalShow);
+ this.handleDeploymentModalShow);
}
handleBeforeAjax() {
this.setState({
- showSpinner: true,
- showPuzzleModal: false
+ showSpinner: true
});
}
@@ -80,28 +79,21 @@ export default class CoffeeMakerApp extends React.Component {
});
}
- handleIsPuzzleValid() {
- this.setState({
- isPuzzleValid: true,
- showPuzzleModal: false
- })
- }
-
- handlePuzzleModalClose() {
+ handleErrorModalClose() {
this.setState({
- showPuzzleModal: false
+ showErrorModal: false
})
}
- handlePuzzleModalShow() {
+ handleDeploymentModalClose() {
this.setState({
- showPuzzleModal: true
+ showDeploymentModal: false
})
}
- handleErrorModalClose() {
+ handleDeploymentModalShow() {
this.setState({
- showErrorModal: false
+ showDeploymentModal: true
})
}
@@ -116,13 +108,9 @@ export default class CoffeeMakerApp extends React.Component {
- {
await openBrowser({
@@ -18,9 +20,6 @@ beforeAll(async () => {
describe(APPLICATION, () => {
test('Should run the process asynchronously', async () => {
await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await click(button({id:'runAsync'}));
const result = await text("Log of tasks carried out").exists();
const item = await text("Start boiling the kettle").exists();
@@ -28,9 +27,6 @@ describe(APPLICATION, () => {
}, 100000)
test('Should run the process synchronously', async () => {
await goto(URL);
- await write('5', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await click(button({id:'runSync'}));
const result = await text("Log of tasks carried out").exists();
const item = await text("Start boiling the kettle").exists();
diff --git a/app/app_AzureDotNetCoreDataStructuresApi/Readme.md b/app/app_dataStructures/Readme.md
similarity index 81%
rename from app/app_AzureDotNetCoreDataStructuresApi/Readme.md
rename to app/app_dataStructures/Readme.md
index dbf42b89e..0bacafc70 100644
--- a/app/app_AzureDotNetCoreDataStructuresApi/Readme.md
+++ b/app/app_dataStructures/Readme.md
@@ -1,4 +1,4 @@
-# Azure Functions, .NET Core, Data Structures Api
+# .NET Data Structures
With this application I am demonstrating knowledge of data the structures, Queue and Stack. With Queue being first in, first out and Stack being last in, first out.
@@ -6,8 +6,6 @@ With this application I am demonstrating knowledge of data the structures, Queue
Using Generics in .NET I have implemented an interface, which acts as the contract using polymorphism when instructing the Queue and Stack structures to add and remove items.
-- Azure Functions Repository (https://github.com/fsereno/app_AzureDotNetCoreDataStructuresApi)
-
- Queue in .NET (https://docs.microsoft.com/en-us/dotnet/api/system.collections.queue?view=net-5.0)
- Stack in .NET (https://docs.microsoft.com/en-us/dotnet/api/system.collections.stack?view=net-5.0)
diff --git a/app/app_dataStructures/backend/api/Controllers/ApiController.cs b/app/app_dataStructures/backend/api/Controllers/ApiController.cs
new file mode 100644
index 000000000..b98b0ad10
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Controllers/ApiController.cs
@@ -0,0 +1,125 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Threading.Tasks;
+using Microsoft.AspNetCore.Mvc;
+using Portfolio.Core.Types;
+using Portfolio.Core.Services;
+using Portfolio.DataStructures.Interfaces;
+using Portfolio.DataStructures.Utils;
+using Portfolio.DataStructures.Models;
+
+namespace Portfolio.DataStructures.Controllers;
+
+///
+/// API controller for data structure operations.
+///
+[ApiController]
+[Route("[controller]")]
+public class ApiController : ControllerBase
+{
+ private readonly ILogger _logger;
+ private readonly ICollectionUtil> _queueUtil;
+ private readonly ICollectionUtil> _stackUtil;
+
+ ///
+ /// Initializes a new instance of the ApiController class with the specified logger, queue utility, and stack utility.
+ ///
+ /// The logger to use for logging messages.
+ /// The utility for operating on queues.
+ /// The utility for operating on stacks.
+ public ApiController(ILogger logger, ICollectionUtil> queueUtil, ICollectionUtil> stackUtil)
+ {
+ _logger = logger;
+ _queueUtil = queueUtil;
+ _stackUtil = stackUtil;
+ }
+
+ ///
+ /// Adds an item to the queue asynchronously.
+ ///
+ /// The request body containing the collection name and item to add.
+ /// An IActionResult representing the result of the operation.
+ [HttpPost("AddQueueItemAsync")]
+ public IActionResult AddQueueItemAsync([FromBody] AddRequestBody data)
+ {
+ _logger.LogInformation("AddQueueItemAsync endpoint hit");
+
+ string result = string.Empty;
+
+ _logger.LogInformation("Adding item to the queue");
+
+ var queue = _queueUtil.Create(data.Collection);
+ _queueUtil.Add(queue, data.Item);
+
+ _logger.LogInformation("Added item to the queue");
+
+ return Ok(queue);
+ }
+
+ ///
+ /// Removes an item from the queue asynchronously.
+ ///
+ /// The request body containing the collection name.
+ /// An IActionResult representing the result of the operation.
+ [HttpPost("RemoveQueueItemAsync")]
+ public IActionResult RemoveQueueItemAsync([FromBody] RemoveRequestBody data)
+ {
+ _logger.LogInformation("RemoveQueueItemAsync endpoint hit");
+
+ string result = string.Empty;
+
+ _logger.LogInformation("Removing item from the queue");
+
+ var queue = _queueUtil.Create(data.Collection);
+ _queueUtil.Remove(queue);
+
+ _logger.LogInformation("Removed item from the queue");
+
+ return Ok(queue);
+ }
+
+ ///
+ /// Adds an item to the stack asynchronously.
+ ///
+ /// The request body containing the collection name and item to add.
+ /// An IActionResult representing the result of the operation.
+ [HttpPost("AddStackItemAsync")]
+ public IActionResult AddStackItemAsync([FromBody] AddRequestBody data)
+ {
+ _logger.LogInformation("AddStackItemAsync endpoint hit");
+
+ string result = string.Empty;
+
+ _logger.LogInformation("Adding item to the stack");
+
+ var stack = _stackUtil.Create(data.Collection);
+ _stackUtil.Add(stack, data.Item);
+
+ _logger.LogInformation("Added item to the stack");
+
+ return Ok(stack);
+ }
+
+ ///
+ /// Removes an item from the stack asynchronously.
+ ///
+ /// The request body containing the collection name.
+ /// An IActionResult representing the result of the operation.
+ [HttpPost("RemoveStackItemAsync")]
+ public IActionResult RemoveStackItemAsync([FromBody] RemoveRequestBody data)
+ {
+ _logger.LogInformation("RemoveStackItemAsync endpoint hit");
+
+ string result = string.Empty;
+
+ _logger.LogInformation("Removing item from the stack");
+
+ var stack = _stackUtil.Create(data.Collection);
+ _stackUtil.Remove(stack);
+
+ _logger.LogInformation("Removed item from the stack");
+
+ return Ok(stack);
+ }
+}
diff --git a/app/app_dataStructures/backend/api/Interfaces/ICollectionUtil.cs b/app/app_dataStructures/backend/api/Interfaces/ICollectionUtil.cs
new file mode 100644
index 000000000..64f0054b1
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Interfaces/ICollectionUtil.cs
@@ -0,0 +1,25 @@
+namespace Portfolio.DataStructures.Interfaces
+{
+ public interface ICollectionUtil
+ {
+ ///
+ /// Adds an item to the collection
+ ///
+ ///
+ /// Takes a collection of type T
+ void Add(T collection, string value);
+
+ ///
+ /// Removes an item to the collection
+ ///
+ /// Takes a collection of type T
+ void Remove(T collection);
+
+ ///
+ /// Creates a collection of type T
+ ///
+ /// The primitive array to create the collection from if not null
+ /// Returns the collection of type T
+ T Create(string[] array = null);
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/Models/AddRequestBody.cs b/app/app_dataStructures/backend/api/Models/AddRequestBody.cs
new file mode 100644
index 000000000..03265ee77
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Models/AddRequestBody.cs
@@ -0,0 +1,18 @@
+namespace Portfolio.DataStructures.Models
+{
+ ///
+ /// Represents the request body for adding an item to a data structure.
+ ///
+ public class AddRequestBody
+ {
+ ///
+ /// Gets or sets the collection name or identifier.
+ ///
+ public string[] Collection { get; set; }
+
+ ///
+ /// Gets or sets the item to add.
+ ///
+ public string Item { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/Models/RemoveRequestBody.cs b/app/app_dataStructures/backend/api/Models/RemoveRequestBody.cs
new file mode 100644
index 000000000..9f1f31c0b
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Models/RemoveRequestBody.cs
@@ -0,0 +1,13 @@
+namespace Portfolio.DataStructures.Models
+{
+ ///
+ /// Represents the request body for removing an item from a data structure.
+ ///
+ public class RemoveRequestBody
+ {
+ ///
+ /// Gets or sets the collection name or identifier.
+ ///
+ public string[] Collection { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/Program.cs b/app/app_dataStructures/backend/api/Program.cs
new file mode 100644
index 000000000..c3d9092de
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Program.cs
@@ -0,0 +1,39 @@
+using Portfolio.Core.Services;
+using Portfolio.DataStructures.Interfaces;
+using Portfolio.DataStructures.Utils;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://*:3002");
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+// Services
+builder.Services.AddScoped>, QueueUtil>();
+builder.Services.AddScoped>, StackUtil>();
+
+builder.Services.AddHealthChecks();
+
+var app = builder.Build();
+
+app.MapHealthChecks("/healthcheck");
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/app/app_dataStructures/backend/api/Properties/launchSettings.json b/app/app_dataStructures/backend/api/Properties/launchSettings.json
new file mode 100644
index 000000000..81cd75977
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Properties/launchSettings.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:4279",
+ "sslPort": 44379
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7114;http://localhost:5065",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/Utils/QueueUtil.cs b/app/app_dataStructures/backend/api/Utils/QueueUtil.cs
new file mode 100644
index 000000000..b500b1ea1
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Utils/QueueUtil.cs
@@ -0,0 +1,28 @@
+using System.Collections;
+using Portfolio.DataStructures.Interfaces;
+
+namespace Portfolio.DataStructures.Utils
+{
+ public class QueueUtil : ICollectionUtil>
+ {
+ ///
+ public void Add(Queue collection, string value)
+ {
+ collection?.Enqueue(value);
+ }
+
+ ///
+ public void Remove(Queue collection)
+ {
+ collection?.Dequeue();
+ }
+
+ ///
+ public Queue Create(string[] array = null)
+ {
+ var collection = array != null ? new Queue(array) : new Queue();
+
+ return collection;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/Utils/StackUtil.cs b/app/app_dataStructures/backend/api/Utils/StackUtil.cs
new file mode 100644
index 000000000..8300642d5
--- /dev/null
+++ b/app/app_dataStructures/backend/api/Utils/StackUtil.cs
@@ -0,0 +1,36 @@
+using System.Collections;
+using Portfolio.DataStructures.Interfaces;
+
+namespace Portfolio.DataStructures.Utils
+{
+ public class StackUtil : ICollectionUtil>
+ {
+ ///
+ public void Add(Stack collection, string value)
+ {
+ collection?.Push(value);
+ }
+
+ ///
+ public void Remove(Stack collection)
+ {
+ collection?.Pop();
+ }
+
+ ///
+ public Stack Create(string[] array = null)
+ {
+ var collection = new Stack();
+
+ if (array != null)
+ {
+ for (var i = array.Length - 1; i >= 0; i--)
+ {
+ Add(collection, array[i]);
+ }
+ }
+
+ return collection;
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/api/api.csproj b/app/app_dataStructures/backend/api/api.csproj
new file mode 100644
index 000000000..552ccdcdf
--- /dev/null
+++ b/app/app_dataStructures/backend/api/api.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/app_dataStructures/backend/api/api.sln b/app/app_dataStructures/backend/api/api.sln
new file mode 100644
index 000000000..8652f95ce
--- /dev/null
+++ b/app/app_dataStructures/backend/api/api.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 25.0.1704.4
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api.csproj", "{760951DE-23EF-4490-9F08-BAFE9D4CDF29}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {760951DE-23EF-4490-9F08-BAFE9D4CDF29}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {760951DE-23EF-4490-9F08-BAFE9D4CDF29}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {760951DE-23EF-4490-9F08-BAFE9D4CDF29}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {760951DE-23EF-4490-9F08-BAFE9D4CDF29}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {2D6EF111-100D-42C9-B9FD-BD3C5E8D14A3}
+ EndGlobalSection
+EndGlobal
diff --git a/app/app_dataStructures/backend/api/appsettings.Development.json b/app/app_dataStructures/backend/api/appsettings.Development.json
new file mode 100644
index 000000000..ff66ba6b2
--- /dev/null
+++ b/app/app_dataStructures/backend/api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/app/app_dataStructures/backend/api/appsettings.json b/app/app_dataStructures/backend/api/appsettings.json
new file mode 100644
index 000000000..4d566948d
--- /dev/null
+++ b/app/app_dataStructures/backend/api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/app/app_dataStructures/backend/test/QueueUtilTest.cs b/app/app_dataStructures/backend/test/QueueUtilTest.cs
new file mode 100644
index 000000000..0d0df2214
--- /dev/null
+++ b/app/app_dataStructures/backend/test/QueueUtilTest.cs
@@ -0,0 +1,75 @@
+using System.Collections;
+using Portfolio.DataStructures.Interfaces;
+using Portfolio.DataStructures.Utils;
+using Xunit;
+
+namespace Portfolio.DataStructures.Test
+{
+ public class QueueUtilTest
+ {
+ private readonly Queue _queue;
+ private readonly ICollectionUtil> _queueUtil;
+
+ public QueueUtilTest()
+ {
+ _queueUtil = new QueueUtil();
+ _queue = _queueUtil.Create();
+ }
+
+ [Fact]
+ public void Add_ShouldAddItemToQueue()
+ {
+ _queueUtil.Add(_queue, "Item 1");
+
+ var result = _queue.Count;
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void Add_ShouldNotErrorWhenNullIsPassedForQueue()
+ {
+ _queueUtil.Add(null, "Item 1");
+
+ var result = _queue.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Remove_ShouldRemoveItemFromQueue()
+ {
+ _queueUtil.Add(_queue, "Item 1");
+ _queueUtil.Remove(_queue);
+
+ var result = _queue.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Remove_ShouldNotRemoveIfQueueIsNull()
+ {
+ _queueUtil.Remove(null);
+
+ var result = _queue.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Create_ShouldCreateQueueWhenCollectionPassedIsNull()
+ {
+ Assert.IsType>(_queue);
+ }
+
+ [Fact]
+ public void Create_ShouldCreateAQueueAndRemoveInTheCorrectOrder_FIFO()
+ {
+ var array = new string[] { "1", "2" };
+ var queue = _queueUtil.Create(array);
+
+ _queueUtil.Add(queue, "3");
+ _queueUtil.Remove(queue);
+
+ var result = queue.Peek();
+ Assert.Equal("2", result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/test/StackUtilTest.cs b/app/app_dataStructures/backend/test/StackUtilTest.cs
new file mode 100644
index 000000000..b6dfbb129
--- /dev/null
+++ b/app/app_dataStructures/backend/test/StackUtilTest.cs
@@ -0,0 +1,107 @@
+using System.Collections;
+using Portfolio.DataStructures.Interfaces;
+using Portfolio.DataStructures.Utils;
+using Xunit;
+
+namespace Portfolio.DataStructures.Test
+{
+ public class StackUtilTest
+ {
+ private readonly Stack _stack;
+ private readonly ICollectionUtil> _stackUtil;
+
+ public StackUtilTest()
+ {
+ _stackUtil = new StackUtil();
+ _stack = _stackUtil.Create();
+ }
+
+ [Fact]
+ public void Add_ShouldAddItemToStack()
+ {
+ _stackUtil.Add(_stack, "Item 1");
+
+ var result = _stack.Count;
+ Assert.Equal(1, result);
+ }
+
+ [Fact]
+ public void Add_ShouldNotErrorWhenNullIsPassedForStack()
+ {
+ _stackUtil.Add(null, "Item 1");
+
+ var result = _stack.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Add_ShouldAddItemToStackInCorrectOrder()
+ {
+ var stack = new Stack(new string[] { "1", "2" });
+
+ _stackUtil.Add(stack, "3"); // this results in 321
+
+ var result = stack.Peek();
+ Assert.Equal("3", result);
+ }
+
+ [Fact]
+ public void Add_ShouldAddItemToStackInCorrectOrderWhenOrderIsReversedAlready()
+ {
+ var stack = new Stack(new string[] { "2", "1" });
+
+ _stackUtil.Add(stack, "3");
+
+ var result = stack.Peek();
+ Assert.Equal("3", result);
+ }
+
+ [Fact]
+ public void Remove_ShouldRemoveItemFromStack()
+ {
+ _stackUtil.Add(_stack, "Item 1");
+ _stackUtil.Remove(_stack);
+
+ var result = _stack.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Remove_ShouldNotRemoveIfStackIsNull()
+ {
+ _stackUtil.Remove(null);
+
+ var result = _stack.Count;
+ Assert.Equal(0, result);
+ }
+
+ [Fact]
+ public void Remove_ShouldRemoveItemFromStackInCorrectOrder()
+ {
+ var stack = new Stack(new string[] { "1", "2", "3" });
+ _stackUtil.Remove(stack);
+
+ var result = stack.Peek();
+ Assert.Equal("2", result);
+ }
+
+ [Fact]
+ public void Create_ShouldCreateStackWhenCollectionPassedIsNull()
+ {
+ Assert.IsType>(_stack);
+ }
+
+ [Fact]
+ public void Create_ShouldCreateAStackAndRemoveInCorrectOrderIfInitialOrderIsReversed_LIFO()
+ {
+ var array = new string[] { "2", "1" };
+ var stack = _stackUtil.Create(array);
+
+ _stackUtil.Add(stack, "3");
+ _stackUtil.Remove(stack);
+
+ var result = stack.Peek();
+ Assert.Equal("2", result);
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/test/Usings.cs b/app/app_dataStructures/backend/test/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/app/app_dataStructures/backend/test/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/app/app_dataStructures/backend/test/test.csproj b/app/app_dataStructures/backend/test/test.csproj
new file mode 100644
index 000000000..e2ca563c1
--- /dev/null
+++ b/app/app_dataStructures/backend/test/test.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/app/app_AzureDotNetCoreUniqueDataEntryApi/pug/index.pug b/app/app_dataStructures/pug/index.pug
similarity index 100%
rename from app/app_AzureDotNetCoreUniqueDataEntryApi/pug/index.pug
rename to app/app_dataStructures/pug/index.pug
diff --git a/app/app_AzureDotNetCoreUniqueDataEntryApi/sass/styles.scss b/app/app_dataStructures/sass/styles.scss
similarity index 100%
rename from app/app_AzureDotNetCoreUniqueDataEntryApi/sass/styles.scss
rename to app/app_dataStructures/sass/styles.scss
diff --git a/app/app_AzureDotNetCoreDataStructuresApi/src/app.js b/app/app_dataStructures/src/app.js
similarity index 73%
rename from app/app_AzureDotNetCoreDataStructuresApi/src/app.js
rename to app/app_dataStructures/src/app.js
index b102c4c39..9020b4011 100644
--- a/app/app_AzureDotNetCoreDataStructuresApi/src/app.js
+++ b/app/app_dataStructures/src/app.js
@@ -6,19 +6,23 @@ import React from 'react';
import ReactDOM from 'react-dom';
import Col from 'react-bootstrap/Col';
import Row from 'react-bootstrap/Row';
-import { PuzzleModalComponent } from '../../js/modules/react/puzzleModalComponent.js';
import { SpinnerComponent } from '../../js/modules/react/spinnerComponent.js'
import { ErrorModalComponent } from '../../js/modules/react/errorModalComponent.js';
import { ConfigUtil } from "../../js/modules/utils/configUtil";
import { FormComponent } from './formComponent';
import { jQueryAjaxUtil } from '../../js/modules/utils/jQueryAjaxUtil';
+import { DeploymentModalComponent } from '../../js/modules/react/deploymentModalComponent.js';
+import { DeploymentUtil } from '../../js/modules/utils/deploymentUtil';
+
+const CONFIG = ConfigUtil.get();
+const APP_CONFIG = ConfigUtil.get("dataStructures");
+const ADD_QUEUE_ITEM_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.addQueueItem}`;
+const REMOVE_QUEUE_ITEM_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.removeQueueItem}`;
+const ADD_STACK_ITEM_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.addStackItem}`;
+const REMOVE_STACK_ITEM_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.removeStackItem}`;
+
+console.log(DeploymentUtil.isNotCloud());
-const PUZZLE = "3 x 2 - 1 =";
-const APP_CONFIG = ConfigUtil.get("azureDotNetCoreDataStructuresApi");
-const ADD_QUEUE_ITEM_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.addQueueItem}`;
-const REMOVE_QUEUE_ITEM_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.removeQueueItem}`;
-const ADD_STACK_ITEM_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.addStackItem}`;
-const REMOVE_STACK_ITEM_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.removeStackItem}`;
class DataStructuresApp extends React.Component {
constructor(props) {
super(props);
@@ -26,18 +30,16 @@ class DataStructuresApp extends React.Component {
queue: [],
stack: [],
showSpinner: false,
- showPuzzleModal: true,
- showErrorModal: false,
- isPuzzleValid: false
+ showDeploymentModal: DeploymentUtil.isNotCloud(),
+ showErrorModal: false
};
this.handleQueueAdd = this.handleQueueAdd.bind(this);
this.handleQueueRemove= this.handleQueueRemove.bind(this);
this.handleStackAdd = this.handleStackAdd.bind(this);
this.handleStackRemove = this.handleStackRemove.bind(this);
- this.handleIsPuzzleValid = this.handleIsPuzzleValid.bind(this);
- this.handlePuzzleModalClose = this.handlePuzzleModalClose.bind(this);
- this.handlePuzzleModalShow = this.handlePuzzleModalShow.bind(this);
+ this.handleDeploymentModalClose = this.handleDeploymentModalClose.bind(this);
+ this.handleDeploymentModalShow = this.handleDeploymentModalShow.bind(this);
this.handleErrorModalClose = this.handleErrorModalClose.bind(this);
this.handleBeforeAjax = this.handleBeforeAjax.bind(this);
this.handleFailedAjax = this.handleFailedAjax.bind(this);
@@ -45,8 +47,7 @@ class DataStructuresApp extends React.Component {
handleBeforeAjax() {
this.setState({
- showSpinner: true,
- showPuzzleModal: false
+ showSpinner: true
});
}
@@ -58,7 +59,7 @@ class DataStructuresApp extends React.Component {
}
handleAjax(request) {
- jQueryAjaxUtil.handleAjax(request, this.state.isPuzzleValid, this.handleBeforeAjax, this.handleFailedAjax, this.handlePuzzleModalShow);
+ jQueryAjaxUtil.handleAjax(request, DeploymentUtil.isCloud(), this.handleBeforeAjax, this.handleFailedAjax, this.handleDeploymentModalShow);
}
handleQueueAdd(event, item) {
@@ -71,10 +72,11 @@ class DataStructuresApp extends React.Component {
url: ADD_QUEUE_ITEM_ENDPOINT,
data: JSON.stringify(data),
type: "POST",
+ contentType: "application/json",
success: (response) => {
if (response) {
this.setState({
- queue: JSON.parse(response),
+ queue: response,
showSpinner: false
});
} else {
@@ -98,10 +100,11 @@ class DataStructuresApp extends React.Component {
url: ADD_STACK_ITEM_ENDPOINT,
data: JSON.stringify(data),
type: "POST",
+ contentType: "application/json",
success: (response) => {
if (response) {
this.setState({
- stack: JSON.parse(response),
+ stack: response,
showSpinner: false
});
} else {
@@ -124,10 +127,11 @@ class DataStructuresApp extends React.Component {
url: REMOVE_QUEUE_ITEM_ENDPOINT,
data: JSON.stringify(data),
type: "POST",
+ contentType: "application/json",
success: (response) => {
if (response) {
this.setState({
- queue: JSON.parse(response),
+ queue: response,
showSpinner: false
});
} else {
@@ -150,10 +154,11 @@ class DataStructuresApp extends React.Component {
url: REMOVE_STACK_ITEM_ENDPOINT,
data: JSON.stringify(data),
type: "POST",
+ contentType: "application/json",
success: (response) => {
if (response) {
this.setState({
- stack: JSON.parse(response),
+ stack: response,
showSpinner: false
});
} else {
@@ -173,35 +178,24 @@ class DataStructuresApp extends React.Component {
})
}
- handleIsPuzzleValid() {
- this.setState({
- isPuzzleValid: true,
- showPuzzleModal: false
- })
- }
-
- handlePuzzleModalClose() {
+ handleDeploymentModalClose() {
this.setState({
- showPuzzleModal: false
+ showDeploymentModal: false
})
}
- handlePuzzleModalShow() {
+ handleDeploymentModalShow() {
this.setState({
- showPuzzleModal: true
+ showDeploymentModal: true
})
}
render() {
return (
<>
- {
+ await openBrowser({
+ headless: true,
+ slowMo: 250,
+ args: ['--no-sandbox']
+ });
+});
+
+describe(APPLICATION, () => {
+ test('Should not add an item to the queue', async () => {
+ await goto(URL);
+ await click(button({ id: 'queueInput_submit' }));
+ const error = await $('.was-validated .form-control:invalid').exists();
+ const result = await evaluate($('#queueList'), (element) => element.innerText);
+ expect(error).toBeTruthy();
+ expect(result).toBe("");
+ }, 100000);
+ test('Should not add an item to the stack', async () => {
+ await goto(URL);
+ await click(button({ id: 'stackInput_submit' }));
+ const error = await $('.was-validated .form-control:invalid').exists();
+ const result = await evaluate($('#stackList'), (element) => element.innerText);
+ expect(error).toBeTruthy();
+ expect(result).toBe("");
+ }, 100000);
+ test('Should add an item to the queue', async () => {
+ await goto(URL);
+ await write('Item 1', into(textBox({ id: 'queueInput' })));
+ await click(button({ id: 'queueInput_submit' }));
+ const result = await evaluate($('#queueList'), (element) => element.innerText);
+ expect(result).toBe('Item 1');
+ }, 100000);
+ test('Should remove an item to the queue', async () => {
+ await goto(URL);
+ await write('Item 1', into(textBox({ id: 'queueInput' })));
+ await click(button({ id: 'queueInput_submit' }));
+ await waitFor(2000);
+ await write('Item 2', into(textBox({ id: 'queueInput' })));
+ await click(button({ id: 'queueInput_submit' }));
+ await waitFor(2000);
+ await click(button({ id: 'queueInput_remove' }));
+ const result = await evaluate($('#queueList'), (element) => element.innerText);
+ expect(result).toBe('Item 2');
+ }, 100000);
+ test('Should add an item to the stack', async () => {
+ await goto(URL);
+ await write('Item 1', into(textBox({ id: 'stackInput' })));
+ await click(button({ id: 'stackInput_submit' }));
+ const result = await evaluate($('#stackList'), (element) => element.innerText);
+ expect(result).toBe('Item 1');
+ }, 100000);
+ test('Should remove an item to the queue', async () => {
+ await goto(URL);
+ await write('Item 1', into(textBox({ id: 'stackInput' })));
+ await click(button({ id: 'stackInput_submit' }));
+ await waitFor(2000);
+ await write('Item 2', into(textBox({ id: 'stackInput' })));
+ await click(button({ id: 'stackInput_submit' }));
+ await waitFor(2000);
+ await click(button({ id: 'stackInput_remove' }));
+ const result = await evaluate($('#stackList'), (element) => element.innerText);
+ expect(result).toBe('Item 1');
+ }, 100000);
+});
+
+afterAll(() => {
+ closeBrowser();
+});
\ No newline at end of file
diff --git a/app/app_entitySort/Readme.md b/app/app_entitySort/Readme.md
new file mode 100644
index 000000000..4d69d0638
--- /dev/null
+++ b/app/app_entitySort/Readme.md
@@ -0,0 +1,9 @@
+# .NET, Complex Entity Sorting Algorithm
+
+With this application I have built a complex type sorting interface, using .NET. A basic React user interface handles user input and application state.
+
+Whilst this functionality could be achieved with JavaScript alone, I wanted to demonstrate knowledge of two key .NET interfaces: ICompareable and IComparer.
+
+- ICompareable (https://docs.microsoft.com/en-us/dotnet/api/system.icomparable?view=netcore-3.1)
+
+- IComparer (https://docs.microsoft.com/en-us/dotnet/api/system.collections.icomparer?view=netcore-3.1)
\ No newline at end of file
diff --git a/app/app_entitySort/backend/api/Controllers/ApiController.cs b/app/app_entitySort/backend/api/Controllers/ApiController.cs
new file mode 100644
index 000000000..0586c1312
--- /dev/null
+++ b/app/app_entitySort/backend/api/Controllers/ApiController.cs
@@ -0,0 +1,48 @@
+using Microsoft.AspNetCore.Mvc;
+using Portfolio.EntitySort.Interfaces;
+using Portfolio.EntitySort.Models;
+
+namespace Portfolio.EntitySort.Controllers
+{
+ ///
+ /// API controller for sorting employee entities.
+ ///
+ [Route("[controller]")]
+ public class ApiController : ControllerBase
+ {
+ private readonly IEmployeeSortUtil _employeeSortUtil;
+
+ ///
+ /// Initializes a new instance of the ApiController class with the specified employee sort utility.
+ ///
+ /// The utility for sorting employee entities.
+ public ApiController(IEmployeeSortUtil employeeSortUtil)
+ {
+ _employeeSortUtil = employeeSortUtil;
+ }
+
+ ///
+ /// Sorts the employee entities by salary in descending order.
+ ///
+ /// The request containing the employees to sort.
+ /// A list of employee entities sorted by salary in descending order.
+ [HttpPost("sort/salary/desc")]
+ public IList SortBySalaryDesc([FromBody]GetRequest request)
+ {
+ var employees = _employeeSortUtil.SortBySalaryDesc(request?.Employees);
+ return employees;
+ }
+
+ ///
+ /// Sorts the employee entities by salary in ascending order.
+ ///
+ /// The request containing the employees to sort.
+ /// A list of employee entities sorted by salary in ascending order.
+ [HttpPost("sort/salary/asc")]
+ public IList SortBySalaryAsc([FromBody]GetRequest request)
+ {
+ var employees = _employeeSortUtil.SortBySalaryAsc(request?.Employees);
+ return employees;
+ }
+ }
+}
diff --git a/app/app_entitySort/backend/api/Interfaces/IEmployeeSortUtil.cs b/app/app_entitySort/backend/api/Interfaces/IEmployeeSortUtil.cs
new file mode 100644
index 000000000..11cf8fdb8
--- /dev/null
+++ b/app/app_entitySort/backend/api/Interfaces/IEmployeeSortUtil.cs
@@ -0,0 +1,29 @@
+using System.Collections.Generic;
+using Portfolio.EntitySort.Models;
+
+namespace Portfolio.EntitySort.Interfaces
+{
+ public interface IEmployeeSortUtil
+ {
+ ///
+ /// Default entity sort method, relies on the Employee's IComparable implementation
+ ///
+ /// A List of Employee types to sort
+ /// Returns the sorted List of Employees
+ List SortBySalaryDefault(List employees);
+
+ ///
+ /// Sorts by Employee Salary descending, relies an IComparer implementation being passed in to the Sort method
+ ///
+ /// A List of Employee types to sort
+ /// Returns the sorted List of Employees
+ List SortBySalaryDesc(List employees);
+
+ ///
+ /// Sorts by Employee Salary ascending, relies an IComparer implementation being passed in to the Sort method
+ ///
+ /// A List of Employee types to sort
+ /// Returns the sorted List of Employees
+ List SortBySalaryAsc(List employees);
+ }
+}
diff --git a/app/app_entitySort/backend/api/Models/Employee.cs b/app/app_entitySort/backend/api/Models/Employee.cs
new file mode 100644
index 000000000..9b1b1554d
--- /dev/null
+++ b/app/app_entitySort/backend/api/Models/Employee.cs
@@ -0,0 +1,88 @@
+using System;
+using System.Collections.Generic;
+
+namespace Portfolio.EntitySort.Models
+{
+ ///
+ /// Represents an employee entity.
+ ///
+ public class Employee : IComparable
+ {
+ ///
+ /// Initializes a new instance of the Employee class.
+ ///
+ public Employee()
+ {
+ this.Name = string.Empty;
+ this.Salary = 0;
+ }
+
+ ///
+ /// Gets or sets the name of the employee.
+ ///
+ public string Name { get; set; }
+
+ ///
+ /// Gets or sets the salary of the employee.
+ ///
+ public decimal Salary { get; set; }
+
+ ///
+ /// Gets or sets the display salary of the employee.
+ ///
+ public string DisplaySalary { get; set; }
+
+ ///
+ /// Compares the current employee with another employee based on salary.
+ ///
+ /// The other employee to compare with.
+ /// An integer indicating the relative order of the employees based on salary.
+ public int CompareTo(Employee otherEmployee)
+ {
+ var result = 0;
+ if (this.Salary < otherEmployee.Salary)
+ {
+ result = 1;
+
+ } else if (this.Salary > otherEmployee.Salary)
+ {
+ result = -1;
+ }
+ return result;
+ }
+
+ ///
+ /// Comparer for sorting employees by salary in descending order.
+ ///
+ public class SortBySalaryDesc : IComparer
+ {
+ ///
+ /// Compares two employees based on salary in descending order.
+ ///
+ /// The first employee to compare.
+ /// The second employee to compare.
+ /// An integer indicating the relative order of the employees based on salary.
+ public int Compare(Employee current, Employee next)
+ {
+ return Decimal.Compare(next.Salary, current.Salary);
+ }
+ }
+
+ ///
+ /// Comparer for sorting employees by salary in ascending order.
+ ///
+ public class SortBySalaryAsc : IComparer
+ {
+ ///
+ /// Compares two employees based on salary in ascending order.
+ ///
+ /// The first employee to compare.
+ /// The second employee to compare.
+ /// An integer indicating the relative order of the employees based on salary.
+ public int Compare(Employee current, Employee next)
+ {
+ return Decimal.Compare(current.Salary, next.Salary);
+ }
+ }
+ }
+}
diff --git a/app/app_entitySort/backend/api/Models/GetRequest.cs b/app/app_entitySort/backend/api/Models/GetRequest.cs
new file mode 100644
index 000000000..d3bd07ae0
--- /dev/null
+++ b/app/app_entitySort/backend/api/Models/GetRequest.cs
@@ -0,0 +1,15 @@
+using System.Collections.Generic;
+
+namespace Portfolio.EntitySort.Models
+{
+ ///
+ /// Represents the request body for sorting employees.
+ ///
+ public class GetRequest
+ {
+ ///
+ /// Gets or sets the list of employees to sort.
+ ///
+ public List Employees { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/app/app_entitySort/backend/api/Program.cs b/app/app_entitySort/backend/api/Program.cs
new file mode 100644
index 000000000..8eabc4895
--- /dev/null
+++ b/app/app_entitySort/backend/api/Program.cs
@@ -0,0 +1,40 @@
+using Portfolio.Core.Services;
+using Portfolio.EntitySort.Interfaces;
+using Portfolio.EntitySort.Utils;
+
+var builder = WebApplication.CreateBuilder(args);
+
+builder.WebHost.UseUrls("http://*:3005");
+
+// Add services to the container.
+
+builder.Services.AddControllers();
+// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
+builder.Services.AddEndpointsApiExplorer();
+builder.Services.AddSwaggerGen();
+
+builder.Services.AddTransient();
+
+// Services
+builder.Services.AddScoped();
+
+builder.Services.AddHealthChecks();
+
+var app = builder.Build();
+
+app.MapHealthChecks("/healthcheck");
+
+// Configure the HTTP request pipeline.
+if (app.Environment.IsDevelopment())
+{
+ app.UseSwagger();
+ app.UseSwaggerUI();
+}
+
+app.UseHttpsRedirection();
+
+app.UseAuthorization();
+
+app.MapControllers();
+
+app.Run();
diff --git a/app/app_entitySort/backend/api/Properties/launchSettings.json b/app/app_entitySort/backend/api/Properties/launchSettings.json
new file mode 100644
index 000000000..4bb0ac730
--- /dev/null
+++ b/app/app_entitySort/backend/api/Properties/launchSettings.json
@@ -0,0 +1,39 @@
+{
+ "$schema": "https://json.schemastore.org/launchsettings.json",
+ "iisSettings": {
+ "windowsAuthentication": false,
+ "anonymousAuthentication": true,
+ "iisExpress": {
+ "applicationUrl": "http://localhost:22985",
+ "sslPort": 44310
+ }
+ },
+ "profiles": {
+ "http": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "http://localhost:5186",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "https": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:7280;http://localhost:5186",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ },
+ "IIS Express": {
+ "commandName": "IISExpress",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_entitySort/backend/api/Utils/EmployeeSortUtil.cs b/app/app_entitySort/backend/api/Utils/EmployeeSortUtil.cs
new file mode 100644
index 000000000..241f80b61
--- /dev/null
+++ b/app/app_entitySort/backend/api/Utils/EmployeeSortUtil.cs
@@ -0,0 +1,57 @@
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Portfolio.EntitySort.Interfaces;
+using Portfolio.EntitySort.Models;
+
+namespace Portfolio.EntitySort.Utils
+{
+ public class EmployeeSortUtil : IEmployeeSortUtil
+ {
+ private readonly ILogger _logger;
+
+ //
+ /// Initializes a new instance of the EmployeeSortUtil class with the specified logger.
+ ///
+ /// The logger to use for logging messages.
+ public EmployeeSortUtil(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ ///
+ public List SortBySalaryDefault(List employees)
+ {
+ _logger.LogInformation("Sort by defaut started");
+
+ employees?.Sort();
+
+ _logger.LogInformation("Sort by defaut finished");
+
+ return employees ?? new List();
+ }
+
+ ///
+ public List SortBySalaryDesc(List employees)
+ {
+ _logger.LogInformation("Sort by salary descending started");
+
+ employees?.Sort(new Employee.SortBySalaryDesc());
+
+ _logger.LogInformation("Sort by salary descending finished");
+
+ return employees ?? new List();
+ }
+
+ ///
+ public List SortBySalaryAsc(List employees)
+ {
+ _logger.LogInformation("Sort by salary ascending started");
+
+ employees?.Sort(new Employee.SortBySalaryAsc());
+
+ _logger.LogInformation("Sort by salary ascending finished");
+
+ return employees ?? new List();
+ }
+ }
+}
diff --git a/app/app_entitySort/backend/api/api.csproj b/app/app_entitySort/backend/api/api.csproj
new file mode 100644
index 000000000..40910a9f8
--- /dev/null
+++ b/app/app_entitySort/backend/api/api.csproj
@@ -0,0 +1,20 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/app_entitySort/backend/api/api.sln b/app/app_entitySort/backend/api/api.sln
new file mode 100644
index 000000000..10b393040
--- /dev/null
+++ b/app/app_entitySort/backend/api/api.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 25.0.1704.4
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "api", "api.csproj", "{2A5C2652-B3B9-4423-BD9F-81EBA9F9AA11}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {2A5C2652-B3B9-4423-BD9F-81EBA9F9AA11}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {2A5C2652-B3B9-4423-BD9F-81EBA9F9AA11}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {2A5C2652-B3B9-4423-BD9F-81EBA9F9AA11}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {2A5C2652-B3B9-4423-BD9F-81EBA9F9AA11}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {977E6672-F301-41FC-8D11-429E088E5C4D}
+ EndGlobalSection
+EndGlobal
diff --git a/app/app_entitySort/backend/api/appsettings.Development.json b/app/app_entitySort/backend/api/appsettings.Development.json
new file mode 100644
index 000000000..ff66ba6b2
--- /dev/null
+++ b/app/app_entitySort/backend/api/appsettings.Development.json
@@ -0,0 +1,8 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ }
+}
diff --git a/app/app_entitySort/backend/api/appsettings.json b/app/app_entitySort/backend/api/appsettings.json
new file mode 100644
index 000000000..4d566948d
--- /dev/null
+++ b/app/app_entitySort/backend/api/appsettings.json
@@ -0,0 +1,9 @@
+{
+ "Logging": {
+ "LogLevel": {
+ "Default": "Information",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/app/app_entitySort/backend/test/EmployeeSortUtilTests.cs b/app/app_entitySort/backend/test/EmployeeSortUtilTests.cs
new file mode 100644
index 000000000..bf66800c7
--- /dev/null
+++ b/app/app_entitySort/backend/test/EmployeeSortUtilTests.cs
@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Moq;
+using Xunit;
+using Portfolio.EntitySort.Utils;
+using Portfolio.EntitySort.Models;
+using Portfolio.EntitySort.Interfaces;
+
+namespace Portfolio.EntitySort.Tests
+{
+ public class EmployeeSortUtilTests
+ {
+ private readonly IEmployeeSortUtil _sut;
+ private readonly List _employees;
+ private readonly Mock> _logger;
+ public EmployeeSortUtilTests()
+ {
+ _logger = new Mock>();
+
+ _sut = new EmployeeSortUtil(_logger.Object);
+
+ _employees = new List() {
+ new Employee()
+ {
+ Name = "John Doe",
+ Salary = 5000,
+ DisplaySalary = "£5,000.00"
+ },
+ new Employee()
+ {
+ Name = "Joe Bloggs",
+ Salary = 10000,
+ DisplaySalary = "£10,000.00"
+ }
+ };
+ }
+
+ [Fact]
+ public void Test_Sort_Salary_Default()
+ {
+ var result = _sut.SortBySalaryDefault(_employees);
+
+ VerifyLogger(LogLevel.Information, "Sort by defaut started");
+ VerifyLogger(LogLevel.Information, "Sort by defaut finished");
+
+ Assert.Equal("Joe Bloggs", result[0].Name);
+ }
+
+ [Fact]
+ public void Test_Sort_Salary_Default_Null()
+ {
+ var result = _sut.SortBySalaryDefault(null);
+
+ VerifyLogger(LogLevel.Information, "Sort by defaut started");
+ VerifyLogger(LogLevel.Information, "Sort by defaut finished");
+
+ Assert.Empty(result);
+ }
+ [Fact]
+ public void Test_Sort_Salary_Desc()
+ {
+ var result = _sut.SortBySalaryDesc(_employees);
+
+ VerifyLogger(LogLevel.Information, "Sort by salary descending started");
+ VerifyLogger(LogLevel.Information, "Sort by salary descending finished");
+
+ Assert.Equal("Joe Bloggs", result[0].Name);
+ }
+ [Fact]
+ public void Test_Sort_Salary_Asc()
+ {
+ var result = _sut.SortBySalaryAsc(_employees);
+
+ VerifyLogger(LogLevel.Information, "Sort by salary ascending started");
+ VerifyLogger(LogLevel.Information, "Sort by salary ascending finished");
+
+ Assert.Equal("John Doe", result[0].Name);
+ }
+
+ private void VerifyLogger(LogLevel expectedLogLevel, string expectedMessage = "")
+ {
+ _logger.Verify(
+ x => x.Log(
+ It.Is(l => l == expectedLogLevel),
+ It.IsAny(),
+ It.Is((v, t) => String.IsNullOrEmpty(expectedMessage) ? true : v.ToString() == expectedMessage),
+ It.IsAny(),
+ It.Is>((v, t) => true)));
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/app_entitySort/backend/test/Usings.cs b/app/app_entitySort/backend/test/Usings.cs
new file mode 100644
index 000000000..8c927eb74
--- /dev/null
+++ b/app/app_entitySort/backend/test/Usings.cs
@@ -0,0 +1 @@
+global using Xunit;
\ No newline at end of file
diff --git a/app/app_entitySort/backend/test/test.csproj b/app/app_entitySort/backend/test/test.csproj
new file mode 100644
index 000000000..c17ceb13c
--- /dev/null
+++ b/app/app_entitySort/backend/test/test.csproj
@@ -0,0 +1,29 @@
+
+
+
+ net7.0
+ enable
+ enable
+
+ false
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/app/app_awsDotNetCoreEntitySortApi/pug/index.pug b/app/app_entitySort/pug/index.pug
similarity index 100%
rename from app/app_awsDotNetCoreEntitySortApi/pug/index.pug
rename to app/app_entitySort/pug/index.pug
diff --git a/app/app_awsDotNetCoreEntitySortApi/sass/styles.scss b/app/app_entitySort/sass/styles.scss
similarity index 100%
rename from app/app_awsDotNetCoreEntitySortApi/sass/styles.scss
rename to app/app_entitySort/sass/styles.scss
diff --git a/app/app_awsDotNetCoreEntitySortApi/src/app.js b/app/app_entitySort/src/app.js
similarity index 80%
rename from app/app_awsDotNetCoreEntitySortApi/src/app.js
rename to app/app_entitySort/src/app.js
index 1be33ed5b..4784fc036 100644
--- a/app/app_awsDotNetCoreEntitySortApi/src/app.js
+++ b/app/app_entitySort/src/app.js
@@ -5,7 +5,6 @@ import '../sass/styles.scss';
import React from 'react';
import ReactDOM from 'react-dom';
import { KeyGeneratorUtil } from '../../typeScript/Utils/keyGeneratorUtil/dist/index';
-import { PuzzleModalComponent } from '../../js/modules/react/puzzleModalComponent.js';
import { SpinnerComponent } from '../../js/modules/react/spinnerComponent.js'
import { ErrorModalComponent } from '../../js/modules/react/errorModalComponent.js';
import { ConfigUtil } from "../../js/modules/utils/configUtil";
@@ -13,11 +12,13 @@ import { FormComponent } from "./formComponent";
import { jQueryAjaxUtil } from '../../js/modules/utils/jQueryAjaxUtil';
import { FilterUtil } from '../../typeScript/Utils/filterUtil/dist/index';
import { EmployeeTableComponent } from './employeeTableComponent';
+import { DeploymentModalComponent } from '../../js/modules/react/deploymentModalComponent.js';
+import { DeploymentUtil } from '../../js/modules/utils/deploymentUtil';
-const PUZZLE = "7 x 2 + 1 =";
-const APP_CONFIG = ConfigUtil.get("awsDotNetCoreEntitySortApi");
-const SORT_SALARY_ASC_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.sortSalaryAsc}`;
-const SORT_SALARY_DESC_ENDPOINT = `${APP_CONFIG.endpoints.api}/${APP_CONFIG.endpoints.sortSalaryDesc}`;
+const CONFIG = ConfigUtil.get();
+const APP_CONFIG = ConfigUtil.get("entitySort");
+const SORT_SALARY_ASC_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.sortSalaryAsc}`;
+const SORT_SALARY_DESC_ENDPOINT = `${CONFIG.apiRoot}${APP_CONFIG.endpoints.sortSalaryDesc}`;
class EntitySort extends React.Component {
constructor(props) {
@@ -29,9 +30,8 @@ class EntitySort extends React.Component {
counterLimit: 10,
counter: 1,
showSpinner: false,
- showPuzzleModal: true,
showErrorModal: false,
- isPuzzleValid: false
+ showDeploymentModal: DeploymentUtil.isNotCloud()
};
this.handleNameChange = this.handleNameChange.bind(this);
@@ -40,12 +40,11 @@ class EntitySort extends React.Component {
this.handleDelete= this.handleDelete.bind(this);
this.handleSortSalaryAsc = this.handleSortSalaryAsc.bind(this);
this.handleSortSalaryDesc = this.handleSortSalaryDesc.bind(this);
- this.handleIsPuzzleValid = this.handleIsPuzzleValid.bind(this);
- this.handlePuzzleModalClose = this.handlePuzzleModalClose.bind(this);
- this.handlePuzzleModalShow = this.handlePuzzleModalShow.bind(this);
this.handleErrorModalClose = this.handleErrorModalClose.bind(this);
this.handleBeforeAjax = this.handleBeforeAjax.bind(this);
this.handleFailedAjax = this.handleFailedAjax.bind(this);
+ this.handleDeploymentModalClose = this.handleDeploymentModalClose.bind(this);
+ this.handleDeploymentModalShow = this.handleDeploymentModalShow.bind(this);
}
formatCurrency(value) {
@@ -62,8 +61,7 @@ class EntitySort extends React.Component {
handleBeforeAjax() {
this.setState({
- showSpinner: true,
- showPuzzleModal: false
+ showSpinner: true
});
}
@@ -75,7 +73,7 @@ class EntitySort extends React.Component {
}
handleAjax(request) {
- jQueryAjaxUtil.handleAjax(request, this.state.isPuzzleValid, this.handleBeforeAjax, this.handleFailedAjax, this.handlePuzzleModalShow);
+ jQueryAjaxUtil.handleAjax(request, DeploymentUtil.isCloud(), this.handleBeforeAjax, this.handleFailedAjax, this.handleDeploymentModalShow);
}
handleSortSalaryAsc() {
@@ -154,35 +152,28 @@ class EntitySort extends React.Component {
});
}
- handleIsPuzzleValid() {
+ handleErrorModalClose() {
this.setState({
- isPuzzleValid: true,
- showPuzzleModal: false
+ showErrorModal: false
})
}
- handlePuzzleModalClose() {
- this.setState({
- showPuzzleModal: false
- })
+ handleGenerateKey(employee) {
+ return KeyGeneratorUtil.generate(`${employee.name}_${employee.salary}`);
}
- handlePuzzleModalShow() {
+ handleDeploymentModalClose() {
this.setState({
- showPuzzleModal: true
+ showDeploymentModal: false
})
}
- handleErrorModalClose() {
+ handleDeploymentModalShow() {
this.setState({
- showErrorModal: false
+ showDeploymentModal: true
})
}
- handleGenerateKey(employee) {
- return KeyGeneratorUtil.generate(`${employee.name}_${employee.salary}`);
- }
-
componentDidMount() {
let employee = {
@@ -210,17 +201,13 @@ class EntitySort extends React.Component {
show={this.state.showErrorModal}
handleClose={this.handleErrorModalClose}
/>
+
- {
await openBrowser({
@@ -18,9 +20,6 @@ beforeAll(async () => {
describe(APPLICATION, () => {
test('Should add an item', async () => {
await goto(URL);
- await write('15', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await write('James Bond', into(textBox({id:'nameInput'})));
await write('95000', into(textBox({id:'salaryInput'})));
await click(button({id:'addEmployee_submit'}));
@@ -35,18 +34,12 @@ describe(APPLICATION, () => {
}, 100000);
test('Should remove an item', async () => {
await goto(URL);
- await write('15', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await click(link('delete'));
const result = await tableCell({row:1, col:1}).exists();
expect(result).toBeFalsy();
}, 100000);
test('Shold sort table ascending', async () => {
await goto(URL);
- await write('15', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await write('James Bond', into(textBox({id:'nameInput'})));
await write('95000', into(textBox({id:'salaryInput'})));
await click(button({id:'addEmployee_submit'}));
@@ -62,9 +55,6 @@ describe(APPLICATION, () => {
}, 100000);
test('Shold sort table ascending', async () => {
await goto(URL);
- await write('15', into(textBox({id:'answerInput'}),{force:true}));
- await click(button({id:'submitPuzzle'}));
- await waitFor(2000);
await write('James Bond', into(textBox({id:'nameInput'})));
await write('95000', into(textBox({id:'salaryInput'})));
await click(button({id:'addEmployee_submit'}));
diff --git a/app/app_home/Readme.md b/app/app_home/Readme.md
index cbea019b1..949520e20 100644
--- a/app/app_home/Readme.md
+++ b/app/app_home/Readme.md
@@ -1,22 +1,15 @@
-# To-Do React
+# Welcome
-With this application I have built a simple To-Do list using React to handle user input and manage state.
+## The 3D Introduction
-### Explanation ###
+The Home application serves as the entry point to my Portfolio. It showcases an interactive 3D scene utilising Three.js and demonstrates my understanding of programming complex 3D applications.
-I have used Webpack to bundle JS dependancies and Babel to transpile to the correct ES version.
+A physics engine, Connon.js, has been incorporated to enable collision detection and simulate forces like gravity.
-The interest in this application is to explore how React can be used for 2 way data binding between the DOM and application state. With a particular interest in Reacts use of a virtual DOM.
+I've added texture mapping and written a number of 'randomness' generators to ensure a unique experience with each load. The particles constantly evolve in the background.
-### Project commands ###
+## The Applications
-Build JavaScript by running the default Gulp task from within this directory
-```
- gulp
-```
+Below the introduction, you will find a collection of applications available for demonstration within the Portfolio. The search bar allows you to filter through the applications. Additionally, there is a search bar in the top navigation available on every page.
-Run the application from within the route directory, using the master Gulp file and the default Gulp task
-```
- cd ../../
- gulp
-```
+I hope you enjoy exploring and feel free to view the code on GitHub or contact me with any queries.
diff --git a/app/app_home/src/components/applicationCard.js b/app/app_home/src/components/applicationCard.js
index 0acc3e3b8..21fdd2c44 100644
--- a/app/app_home/src/components/applicationCard.js
+++ b/app/app_home/src/components/applicationCard.js
@@ -26,9 +26,9 @@ export const ApplicationCard = React.memo(({application, condition}) => {
{this.renderSquare(6)}
{this.renderSquare(7)}
{this.renderSquare(8)}
@@ -90,7 +90,7 @@ class Game extends React.Component {
"Go to game start";
return (
);
});
@@ -103,17 +103,20 @@ class Game extends React.Component {
}
return (
-
-
- this.handleClick(i)}
- />
-
-
-
{status}
- {moves}
-
+
+
+
+ this.handleClick(i)}
+ />
+
+
+
{status}
+ {moves}
+
+
+
);
}
diff --git a/app/app_toDoReact/Readme.md b/app/app_toDoReact/Readme.md
index cbea019b1..090fd303f 100644
--- a/app/app_toDoReact/Readme.md
+++ b/app/app_toDoReact/Readme.md
@@ -6,17 +6,4 @@ With this application I have built a simple To-Do list using React to handle use
I have used Webpack to bundle JS dependancies and Babel to transpile to the correct ES version.
-The interest in this application is to explore how React can be used for 2 way data binding between the DOM and application state. With a particular interest in Reacts use of a virtual DOM.
-
-### Project commands ###
-
-Build JavaScript by running the default Gulp task from within this directory
-```
- gulp
-```
-
-Run the application from within the route directory, using the master Gulp file and the default Gulp task
-```
- cd ../../
- gulp
-```
+The interest in this application is to explore how React can be used for 2 way data binding between the DOM and application state. With a particular interest in Reacts use of a virtual DOM.
\ No newline at end of file
diff --git a/app/js/modules/react/cookieBannerComponent/app.js b/app/js/modules/react/cookieBannerComponent/app.js
new file mode 100644
index 000000000..738c4fdf3
--- /dev/null
+++ b/app/js/modules/react/cookieBannerComponent/app.js
@@ -0,0 +1,16 @@
+
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { CookieBannerComponent } from './cookieBannerComponent';
+
+function App() {
+ const isHomeActive = document.head.querySelector('[name="isRoot"]') !== null;
+ return(
+
+ )
+}
+
+ReactDOM.render(
+ ,
+ document.getElementById('cookieBanner')
+);
\ No newline at end of file
diff --git a/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.js b/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.js
new file mode 100644
index 000000000..e7b6b5222
--- /dev/null
+++ b/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.js
@@ -0,0 +1,58 @@
+"use strict;"
+
+import React, { useState, useEffect } from 'react';
+import { getElementFadeClass } from "../../utils/getElementFadeClass";
+import { DeploymentUtil } from '../../utils/deploymentUtil';
+import './cookieBannerComponent.scss';
+
+const SHOW_COOKIE_BANNER_THRESHOLD = 270;
+const COOKIE_BANNER_ACCEPTED_KEY = "Cookie_banner_message_accepted";
+const STORAGE = sessionStorage;
+
+export function CookieBannerComponent({isHomeActive}) {
+
+ const [ fadeClass, setFadeClass ] = useState(getElementFadeClass(false));
+
+ useEffect(() => {
+ if (isHomeActive) {
+ window.addEventListener('scroll', handleFade);
+ } else {
+ handleFade();
+ }
+ return () => window.removeEventListener('scroll', handleFade);
+ },[fadeClass]);
+
+ const handleOnClick = () => {
+ STORAGE.setItem(COOKIE_BANNER_ACCEPTED_KEY, true);
+ setFadeClass(getElementFadeClass(false));
+ }
+
+ const handleFade = () => {
+ const scrollPosition = document.documentElement.scrollTop;
+ const shouldShow = DeploymentUtil.isCloud();
+ const alreadyAccepted = STORAGE.getItem(COOKIE_BANNER_ACCEPTED_KEY);
+
+ if (alreadyAccepted || !shouldShow) {
+ return;
+ }
+
+ if ((isHomeActive && scrollPosition >= SHOW_COOKIE_BANNER_THRESHOLD)) {
+ setFadeClass(getElementFadeClass(true));
+ } else {
+ setFadeClass(getElementFadeClass(false));
+ }
+
+ if (!isHomeActive) {
+ setFadeClass(getElementFadeClass(true));
+ }
+ }
+
+ return (
+ <>
+
+
This application uses cookies to determine which services are deployed and do not store any personal data.
+
+
+ >
+ );
+}
\ No newline at end of file
diff --git a/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.scss b/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.scss
new file mode 100644
index 000000000..f91ae69ef
--- /dev/null
+++ b/app/js/modules/react/cookieBannerComponent/cookieBannerComponent.scss
@@ -0,0 +1,20 @@
+@import "../../../../sass/includes/colours.scss";
+@import "../../../../sass//includes/constants.scss";
+
+.cookie-banner-container {
+ backdrop-filter: blur(10px);
+ background: transparent;
+ padding: .5rem;
+ position: fixed;
+ bottom: 0;
+ left: 0;
+ right: 0;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ z-index: 9999;
+ border-top: $lightGrey solid;
+ p {
+ margin-bottom: 0;
+ }
+ }
diff --git a/app/js/modules/react/cookieBannerComponent/tests/cookieBannerComponent.test.js b/app/js/modules/react/cookieBannerComponent/tests/cookieBannerComponent.test.js
new file mode 100644
index 000000000..8fc55d95c
--- /dev/null
+++ b/app/js/modules/react/cookieBannerComponent/tests/cookieBannerComponent.test.js
@@ -0,0 +1,33 @@
+/**
+ * @jest-environment jsdom
+ */
+
+import React from 'react';
+import { mount } from 'enzyme';
+import { CookieBannerComponent } from '../cookieBannerComponent';
+
+jest.mock('../../cookieBannerComponent/cookieBannerComponent.scss', () => '');
+
+jest.mock('../../../../../../config.json', () => {
+ return {
+ "deploymentTargetCookie": "fs_portfolio_deployment_target",
+ "deploymentTargets": {
+ "cloud": "cloud",
+ "static": "static"
+ }
+ }
+});
+
+beforeEach(() => { });
+afterEach(() => { });
+
+const App = () => {
+ return (
+
+ )
+}
+
+it('can render', () => {
+ const wrapper = mount();
+ expect(wrapper.find('#cookieBannerComponent')).toBeTruthy();
+});
\ No newline at end of file
diff --git a/app/js/modules/react/deploymentModalComponent.js b/app/js/modules/react/deploymentModalComponent.js
new file mode 100644
index 000000000..0deedd16c
--- /dev/null
+++ b/app/js/modules/react/deploymentModalComponent.js
@@ -0,0 +1,45 @@
+import React from 'react';
+import Modal from 'react-bootstrap/Modal';
+import { ConfigUtil } from '../utils/configUtil';
+import Button from 'react-bootstrap/Button';
+
+/**
+ * DeploymentModalComponent displays a modal detailing how to request a deployment.
+ * If the endpoint is not available, the modal is shown to the user.
+ *
+ * @param {string} id - The ID of the modal element.
+ * @param {string} title - The title of the modal.
+ * @param {string} endpoint - The endpoint URL to check.
+ */
+export function DeploymentModalComponent({id, title, show, handleClose}) {
+
+ const config = ConfigUtil.get();
+ const linkedInUrl = config.linkedInUrl;
+ const gitHubIssueUrl = config.gitHubIssuesUrl;
+
+ return (
+ <>
+
+
+ {title || "Request Deployment"}
+
+
+
+
To conserve resources, services are not always available. To proceed, please request a full deployment of the portfolio. This will create a fully containerised environment in the cloud. Once you request the deployment, you will receive a unique URL to access the complete portfolio.