diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs
new file mode 100644
index 0000000..3a5a7b8
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/AppTests.cs
@@ -0,0 +1,47 @@
+using Microsoft.VisualStudio.TestTools.UnitTesting;
+using CodingTrackerApp.JJHH17.Models;
+using System;
+
+namespace CodingTrackerApp.JJHH17.Tests;
+
+[TestClass]
+public class AppTests
+{
+ [TestMethod]
+ public void Duration_CalculateDurationCorrectly()
+ {
+ var start = "2024-01-01 10:00:00";
+ var end = "2024-01-01 12:30:45";
+ var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end);
+
+ Assert.AreEqual("0 years, 0 months, 0 days, 2 hours, 30 minutes, 45 seconds", session.GetDuration());
+ }
+
+ [TestMethod]
+ public void Duration_InvalidDurationCalculation_PassesIfTestPasses()
+ {
+ var start = "2024-01-01 10:00:00";
+ var end = "2024-01-01 11:00:00";
+ var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end);
+
+ Assert.AreNotEqual("0 years, 0 months, 0 days, 3 hours, 0 minutes, 0 seconds", session.GetDuration());
+ }
+
+ [TestMethod]
+ public void Duration_ZeroDuration_PassesIfTestPasses()
+ {
+ var start = "2024-01-01 10:00:00";
+ var end = "2024-01-01 10:00:00";
+ var session = new CodingTrackerApp.JJHH17.Models.CodingSession(start, end);
+ Assert.AreEqual("0 years, 0 months, 0 days, 0 hours, 0 minutes, 0 seconds", session.GetDuration());
+ }
+
+ [TestMethod]
+ public void DateInput_ValidDateFormat_PassesIfValidFormat()
+ {
+ var dateString = "2024-06-15 14:30";
+ DateTime parsedDate;
+ var isValid = DateTime.TryParse(dateString, out parsedDate);
+ Assert.IsTrue(isValid);
+ }
+}
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj
new file mode 100644
index 0000000..1bf2f0f
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.Tests/CodingTrackerApp.JJHH17.Tests.csproj
@@ -0,0 +1,20 @@
+
+
+ net8.0
+ false
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj
new file mode 100644
index 0000000..4363f1d
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config
new file mode 100644
index 0000000..3ad7955
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/App.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs
new file mode 100644
index 0000000..9e00ebc
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Database/Database.cs
@@ -0,0 +1,95 @@
+using Dapper;
+using System.Configuration;
+using System.Data.SQLite;
+using CodingTrackerApp.JJHH17.Models;
+
+namespace CodingTrackerApp.JJHH17.Database;
+
+public class Database
+{
+ private static readonly string dbPath = ConfigurationManager.AppSettings["databasePath"];
+ private static readonly string tableName = ConfigurationManager.AppSettings["tableName"];
+ private static readonly string connectionString = $"Data Source={dbPath};";
+
+ public static void CreateDatabase()
+ {
+ if (!File.Exists(dbPath))
+ {
+ SQLiteConnection.CreateFile(dbPath);
+ Console.WriteLine("Database created successfully");
+ }
+ else
+ {
+ Console.WriteLine("Database already exists");
+ }
+
+ CreateTable();
+ }
+
+ private static void CreateTable()
+ {
+ using (var connection = new SQLiteConnection(connectionString))
+ {
+ connection.Open();
+
+ string tableCreation = @"CREATE TABLE IF NOT EXISTS CodeTracker (
+ Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ StartTime TEXT NOT NULL,
+ EndTime TEXT NOT NULL,
+ Duration TEXT);";
+
+ connection.Execute(tableCreation);
+ Console.WriteLine("Table created successfully or already exists");
+ }
+ }
+
+ public static long AddEntry(string startTime, string endTime, string duration)
+ {
+ using (var connection = new SQLiteConnection(connectionString))
+ {
+ connection.Open();
+ var sql = "INSERT INTO CodeTracker (StartTime, EndTime, Duration) VALUES (@StartTime, @EndTime, @Duration);" +
+ $"SELECT last_insert_rowid();";
+
+ var newEntry = new CodingSession(startTime, endTime, duration);
+
+ long newId = connection.ExecuteScalar(sql, newEntry);
+
+ return newId;
+ }
+ }
+
+ public static List GetAllEntries()
+ {
+ using (var connection = new SQLiteConnection(connectionString))
+ {
+ connection.Open();
+ var sql = "SELECT * FROM CodeTracker;";
+ var entries = connection.Query(sql).ToList();
+ return entries;
+ }
+ }
+
+ public static void DeleteAllEntries()
+ {
+ using (var connection = new SQLiteConnection(connectionString))
+ {
+ connection.Open();
+ var sql = $"DELETE FROM {tableName};";
+ connection.Execute(sql);
+ }
+ }
+
+ public static void DeleteEntryById(long id)
+ {
+ using (var connection = new SQLiteConnection(connectionString))
+ {
+ // Get list of existing entries for ID selection
+ GetAllEntries();
+
+ connection.Open();
+ var sql = $"DELETE FROM {tableName} WHERE Id = @Id;";
+ connection.Execute(sql, new { Id = id } );
+ }
+ }
+}
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs
new file mode 100644
index 0000000..21c2034
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Models/CodingSession.cs
@@ -0,0 +1,67 @@
+namespace CodingTrackerApp.JJHH17.Models;
+
+public class CodingSession
+{
+ public long Id { get; set; }
+ public string StartTime { get; set; }
+ public string EndTime { get; set; }
+ public string Duration { get; set; }
+
+ public DateTime? stopwatchStartTime;
+ public DateTime? stopwatchEndTime;
+
+ public CodingSession()
+ {
+ }
+
+ public CodingSession(string startTime, string endTime)
+ {
+ StartTime = startTime;
+ EndTime = endTime;
+ CalculateDuration();
+ }
+
+ public CodingSession(string startTime, string endTime, string duration)
+ {
+ StartTime = startTime;
+ EndTime = endTime;
+ Duration = duration;
+ }
+
+ public void CalculateDuration()
+ {
+ DateTime start = DateTime.Parse(StartTime);
+ DateTime end = DateTime.Parse(EndTime);
+
+ int years = end.Year - start.Year;
+ int months = end.Month - start.Month;
+ int days = end.Day - start.Day;
+
+ if (days < 0)
+ {
+ months--;
+ var previousMonth = end.AddMonths(-1);
+ days += DateTime.DaysInMonth(previousMonth.Year, previousMonth.Month);
+ }
+
+ if (months < 0)
+ {
+ years--;
+ months += 12;
+ }
+
+ TimeSpan timespan = end - start;
+
+ int totalDays = (int)timespan.TotalDays;
+ int totalHours = (int)timespan.TotalHours % 24;
+ int totalMinutes = (int)timespan.TotalMinutes % 60;
+ int totalSeconds = (int)timespan.TotalSeconds % 60;
+
+ Duration = $"{years} years, {months} months, {days} days, {totalHours} hours, {totalMinutes} minutes, {totalSeconds} seconds";
+ }
+
+ public string GetDuration()
+ {
+ return Duration;
+ }
+}
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs
new file mode 100644
index 0000000..0f2aa1d
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/Program.cs
@@ -0,0 +1,12 @@
+using Menu = CodingTrackerApp.JJHH17.UserInterface;
+
+namespace CodingTrackerApp.JJHH17;
+
+class Program
+{
+ public static void Main(string[] args)
+ {
+ Database.Database.CreateDatabase();
+ Menu.Running();
+ }
+}
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs
new file mode 100644
index 0000000..33abf91
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/CodingTrackerApp.JJHH17/UserInterface/UserInterface.cs
@@ -0,0 +1,141 @@
+using Spectre.Console;
+using CodingTrackerApp.JJHH17;
+using CodingTrackerApp.JJHH17.Models;
+
+namespace CodingTrackerApp.JJHH17;
+
+public class UserInterface
+{
+ enum MenuOptions
+ {
+ AddEvent,
+ ViewAllEvents,
+ DeleteAll,
+ DeleteSingleEvent,
+ Exit
+ }
+
+ public static void Running()
+ {
+ bool active = true;
+
+ while (active)
+ {
+ Console.Clear();
+
+ var choice = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select an option:")
+ .AddChoices(Enum.GetValues()));
+
+ switch (choice)
+ {
+ case MenuOptions.AddEvent:
+ AnsiConsole.MarkupLine("[green]Add Event selected.[/]");
+ AddEntry();
+ break;
+
+ case MenuOptions.ViewAllEvents:
+ AnsiConsole.MarkupLine("[green]View All Events selected.[/]");
+ ViewAllEntries();
+ break;
+
+ case MenuOptions.DeleteAll:
+ AnsiConsole.MarkupLine("[green]Delete All selected.[/]");
+ DeleteAllEntries();
+ break;
+
+ case MenuOptions.DeleteSingleEvent:
+ AnsiConsole.MarkupLine("[green]Delete Single Event selected.[/]");
+ DeleteSingleEntry();
+ break;
+
+ case MenuOptions.Exit:
+ active = false;
+ break;
+ }
+ }
+ }
+
+ public static void AddEntry()
+ {
+ string startTime = AnsiConsole.Ask("Enter start time (YYYY-MM-DD HH:MM) (Time is optional):");
+
+ DateTime parsedStartTime;
+ while (!DateTime.TryParse(startTime, out parsedStartTime))
+ {
+ AnsiConsole.MarkupLine("[red]Invalid date format. Please try again.[/]");
+ startTime = AnsiConsole.Ask("Enter start time (YYYY-MM-DD HH:MM):");
+ }
+
+ string endTime = AnsiConsole.Ask("Enter end time (YYYY-MM-DD HH:MM) (Time is optional)");
+
+ DateTime parsedEndTime;
+ while (!DateTime.TryParse(endTime, out parsedEndTime) || parsedEndTime <= parsedStartTime)
+ {
+ AnsiConsole.MarkupLine("[red]Invalid date format or end time is before start time. Please try again.[/]");
+ endTime = AnsiConsole.Ask("Enter end time (YYYY-MM-DD HH:MM):");
+ }
+
+ AnsiConsole.MarkupLine("[green]Entry added successfully! Enter a key to continue[/]");
+ Console.ReadKey();
+ var newEntry = new CodingSession(startTime, endTime);
+ newEntry.CalculateDuration();
+ Database.Database.AddEntry(newEntry.StartTime, newEntry.EndTime, newEntry.Duration);
+ }
+
+ public static void ViewAllEntries()
+ {
+ var table = new Table();
+ table.AddColumn("ID");
+ table.AddColumn("Start Time");
+ table.AddColumn("End Time");
+ table.AddColumn("Duration");
+
+ List entries = Database.Database.GetAllEntries();
+ foreach (var entry in entries)
+ {
+ table.AddRow(entry.Id.ToString(), entry.StartTime, entry.EndTime, entry.Duration);
+ }
+
+ AnsiConsole.Write(table);
+ AnsiConsole.MarkupLine("[green]Press any key to continue...[/]");
+ Console.ReadKey();
+ }
+
+ public static void DeleteAllEntries()
+ {
+ string confirmation = AnsiConsole.Ask("Are you sure you want to delete all entries? (yes/no):");
+ if (confirmation.ToLower() == "yes")
+ {
+ Database.Database.DeleteAllEntries();
+ AnsiConsole.MarkupLine("[green]All entries deleted successfully! Press any key to continue...[/]");
+ }
+ else
+ {
+ AnsiConsole.MarkupLine("[yellow]Deletion cancelled. Press any key to continue...[/]");
+ }
+
+ Console.ReadKey();
+ }
+
+ public static void DeleteSingleEntry()
+ {
+ ViewAllEntries();
+ int idToDelete = AnsiConsole.Ask("Enter the ID of the entry to delete:");
+ AnsiConsole.MarkupLine($"[red]Are you sure you want to delete entry ID {idToDelete}[/]");
+ string confirmation = AnsiConsole.Ask("Type 'yes' to confirm deletion:");
+
+ if (confirmation.ToLower() == "yes")
+ {
+ Database.Database.DeleteEntryById(idToDelete);
+ AnsiConsole.MarkupLine("[green]Entry deleted successfully! Press any key to continue...[/]");
+ }
+ else
+ {
+ AnsiConsole.MarkupLine("[yellow]Deletion cancelled. Press any key to continue...[/]");
+ }
+
+ Console.ReadKey();
+ }
+}
\ No newline at end of file
diff --git a/CodingTrackerApp.JJHH17/README.md b/CodingTrackerApp.JJHH17/README.md
new file mode 100644
index 0000000..2ec71d4
--- /dev/null
+++ b/CodingTrackerApp.JJHH17/README.md
@@ -0,0 +1,44 @@
+### Coding Tracking Application - Unit Tests
+
+A command line based code tracking application - allows the user to enter time studied, which is then stored to a local instance of SQLite.
+This project follows the "Unit Testing" project of the CSharpAcademy, found on: https://www.thecsharpacademy.com/project/21/unit-testing.
+
+## Technologies used and installed
+
+- SQLite database.
+- Dapper (for integrating with SQLite).
+- Spectre console (for terminal styling).
+- Microsoft.VisualStudio.TestTools.UnitTesting - For unit testing.
+
+## Installation and Running Steps:
+- This project uses SQLite, meaning a local instance needs to exist in order to store data.
+- The project (program.cs) file contains a method that creates the database if it doesn't already exist, meaning you don't need to create this manually.
+- Furthermore, it will also create the relevant SQL table needed for storing events.
+
+## Application Details
+
+- The application can be started by cloning the directory and running the program.cs file.
+- You're then presented with a list of options - use the arrow keys to navigate via the terminal options:
+
+
+
+- The user can add entries via the "Add Event" option, which takes in the start and end date and time:
+
+
+
+- View a full list of coding events added.
+
+
+
+- Delete a single event (by the unique event ID)
+
+
+
+- Delete all events on the database.
+
+
+
+## Unit Tests
+- This project contains a suite of unit tests, which can be found under the "CodingTrackerApp.JJHH17.Tests" directory.
+- To run the tests, cd to the test directory via terminal and run ```dotnet test```.
+- This command will execute all tests in the testing class.