diff --git a/Coding Tracker/Coding Tracker.slnx b/Coding Tracker/Coding Tracker.slnx
new file mode 100644
index 00000000..f4dfc280
--- /dev/null
+++ b/Coding Tracker/Coding Tracker.slnx
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/Coding Tracker/Coding Tracker/Coding Tracker.csproj b/Coding Tracker/Coding Tracker/Coding Tracker.csproj
new file mode 100644
index 00000000..19b303eb
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Coding Tracker.csproj
@@ -0,0 +1,27 @@
+
+
+
+ Exe
+ net10.0
+ Coding_Tracker
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/Coding Tracker/Coding Tracker/Controllers/AutoTimerSessionController.cs b/Coding Tracker/Coding Tracker/Controllers/AutoTimerSessionController.cs
new file mode 100644
index 00000000..b0d2dabf
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Controllers/AutoTimerSessionController.cs
@@ -0,0 +1,55 @@
+using Spectre.Console;
+using Coding_Tracker.Models;
+using Coding_Tracker.Repositories;
+
+namespace Coding_Tracker.Controllers
+{
+ internal class AutoTimerSessionController : ISessionController
+ {
+ private readonly ConsoleUI _ui;
+ private readonly CodingSessionRepository _repo;
+ public AutoTimerSessionController(CodingSessionRepository repo, ConsoleUI ui)
+ {
+ _repo = repo;
+ _ui = ui;
+ }
+
+ public void StartSession()
+ {
+ var start = DateTime.Now;
+ DisplayStopWatch();
+ var end = DateTime.Now;
+
+ _repo.AddSession(CodingSession.Create(start, end));
+ }
+
+ public void DisplayStopWatch()
+ {
+ float timer = 0f;
+ var stopWatchPanel = new Panel("");
+ stopWatchPanel.Border = BoxBorder.Rounded;
+ stopWatchPanel.Header = new PanelHeader("[green]Coding Session In Progress[/]");
+ while (true)
+ {
+ stopWatchPanel = new Panel(
+ new Markup($"[yellow]{TimeSpan.FromSeconds(timer):hh\\:mm\\:ss}[/]")
+ );
+ AnsiConsole.Write(stopWatchPanel);
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine("[grey]Press [blue]S[/] to stop the session...[/]");
+ if (Console.KeyAvailable)
+ {
+ var key = Console.ReadKey(true);
+ if (key.Key == ConsoleKey.S)
+ {
+ break;
+ }
+ }
+ timer += 1f;
+ Thread.Sleep(1000);
+ AnsiConsole.Clear();
+ }
+
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Controllers/ConsoleUI.cs b/Coding Tracker/Coding Tracker/Controllers/ConsoleUI.cs
new file mode 100644
index 00000000..aa4512c1
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Controllers/ConsoleUI.cs
@@ -0,0 +1,142 @@
+using Spectre.Console;
+using Spectre.Console.Rendering;
+using System.Data;
+
+namespace Coding_Tracker.Controllers
+{
+ internal class ConsoleUI
+ {
+
+ private const string DateFormat = "dd-MM-yyyy HH:mm";
+ public void DisplayMessage(string message, bool isError, string colour = "yellow")
+ {
+ var panel = new Panel(message)
+ {
+ Border = BoxBorder.Rounded,
+ Padding = new Padding(1, 1),
+ Header = new PanelHeader(isError ? "[red]Error[/]" : "[green]Info[/]")
+ };
+ AnsiConsole.Write(panel);
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine("[grey]Press any key to continue...[/]");
+ Console.ReadKey(true);
+ }
+
+ public bool ConfirmDeletion(string itemName)
+ {
+ return AnsiConsole.Confirm($"[red]Are you sure you want to delete[/] [yellow]{itemName}[/]? This action cannot be undone.");
+ }
+
+ public DateTime PromptForDateTime(string prompt)
+ {
+ return PromptUntilValid(
+ prompt,
+ DateFormat.ToLower(),
+ input => DateTimeValidator.ValidateDateResponse(input, DateFormat),
+ $"Invalid date format. Please use the format: {DateFormat.ToLower()}");
+ }
+ public DateTime PromptForEndDateTime(string prompt, DateTime startTime)
+ {
+ return PromptUntilValid(
+ prompt,
+ DateFormat.ToLower(),
+ input => DateTimeValidator.ValidateEndDateResponse(startTime, input, DateFormat),
+ $"Invalid end time. It must be after the start time and use the format: {DateFormat.ToLower()}");
+ }
+ private T PromptUntilValid(string prompt, string formatHint, Func parse, string errorMessage)
+ {
+ while (true)
+ {
+ try
+ {
+ var input = AnsiConsole.Ask($"{prompt} format: {formatHint}:");
+ return parse(input);
+ }
+ catch (FormatException)
+ {
+ DisplayMessage(errorMessage, true);
+ }
+ }
+ }
+
+ public void DisplayActivityHeatMap(List sessions)
+ {
+ var today = DateTime.Today;
+ bool isToday = sessions.Any(s => s.StartTime.Date == today);
+ var startDate = today.AddDays(-29);
+
+ // Sum minutes per day for records with multiple entries
+ var minutesByDay = sessions
+ .Where(s => s.StartTime.Date >= startDate && s.StartTime.Date <= today)
+ .GroupBy(s => s.StartTime.Date)
+ .ToDictionary(g => g.Key, g => g.Sum(s => s.Duration.TotalMinutes));
+
+ // Snap grid start back to Monday
+ DateTime gridStart = startDate;
+ while (gridStart.DayOfWeek != DayOfWeek.Monday)
+ gridStart = gridStart.AddDays(-1);
+
+ // Grid end is 'today'. Calculate number of weeks needed
+ int totalGridDays = (today - gridStart).Days + 1;
+ int weekColumns = (int)Math.Ceiling(totalGridDays / 7.0);
+
+ var table = new Table()
+ .Border(TableBorder.None)
+ .HideHeaders();
+
+ table.AddColumn(new TableColumn(""));
+ for (int c = 0; c < weekColumns; c++)
+ table.AddColumn(new TableColumn(""));
+
+ string[] labels = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };
+
+ for (int row = 0; row < 7; row++)
+ {
+ var cells = new List();
+
+ // Adding the day labels
+ cells.Add(new Markup($"[grey]{labels[row]}[/]"));
+
+ for (int col = 0; col < weekColumns; col++)
+ {
+ var day = gridStart.AddDays(col * 7 + row);
+
+ if (day < startDate || day > today)
+ {
+ cells.Add(new Text(" ", new Style(background: Color.Grey19)));
+ continue;
+ }
+
+ var minutes = minutesByDay.TryGetValue(day, out var m) ? m : 0;
+
+ var bg =
+ minutes > 0 && minutes <= 1 ? Color.LightGreen :
+ minutes > 1 && minutes <= 30 ? Color.Green :
+ minutes > 30 ? Color.DarkGreen :
+ Color.Grey;
+
+ if (isToday && day == today) bg = Color.Blue;
+
+ cells.Add(new Text(" ", new Style(background: bg)));
+
+ }
+
+ table.AddRow(cells.ToArray());
+ }
+
+ AnsiConsole.Write(new Spectre.Console.Rule("[green]Last 30 Days Coding Heatmap[/]").Centered());
+ AnsiConsole.Write(table);
+
+ //legend text
+ AnsiConsole.MarkupLine("\nLegend:\n" +
+ "[on grey] [/] No record\n" +
+ "[on lightgreen] [/] Up to 30 mins\n" +
+ "[on green] [/] Between 30 mins and up to 120 mins\n" +
+ "[on darkgreen] [/] Greater than 120 mins\n" +
+ "[on blue] [/] Today\n");
+
+ AnsiConsole.MarkupLine("\nPress any key to return to the main menu...");
+ Console.ReadKey(true);
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Controllers/ISessionController.cs b/Coding Tracker/Coding Tracker/Controllers/ISessionController.cs
new file mode 100644
index 00000000..5f686e14
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Controllers/ISessionController.cs
@@ -0,0 +1,9 @@
+using Spectre.Console;
+
+namespace Coding_Tracker.Controllers
+{
+ internal interface ISessionController
+ {
+ void StartSession();
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Controllers/ManualTimerSessionController.cs b/Coding Tracker/Coding Tracker/Controllers/ManualTimerSessionController.cs
new file mode 100644
index 00000000..8c5ad366
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Controllers/ManualTimerSessionController.cs
@@ -0,0 +1,26 @@
+using Coding_Tracker.Repositories;
+using Spectre.Console;
+
+namespace Coding_Tracker.Controllers
+{
+ internal class ManualTimerSessionController : ISessionController
+ {
+ private readonly CodingSessionRepository _repo;
+ private readonly ConsoleUI _ui;
+ public ManualTimerSessionController(CodingSessionRepository repo, ConsoleUI ui)
+ {
+ _repo = repo;
+ _ui = ui;
+ }
+
+ public void StartSession()
+ {
+ var start = _ui.PromptForDateTime("Enter the [green]start[/] date and time");
+ var end = _ui.PromptForEndDateTime("Enter the [red]end[/] date and time", start);
+
+ _repo.AddSession(CodingSession.Create(start, end));
+ }
+
+
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/DatabaseInitialiser.cs b/Coding Tracker/Coding Tracker/DatabaseInitialiser.cs
new file mode 100644
index 00000000..94aacdc3
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/DatabaseInitialiser.cs
@@ -0,0 +1,23 @@
+using Dapper;
+using Microsoft.Data.Sqlite;
+
+namespace Coding_Tracker.Controllers
+{
+ internal class DatabaseInitialiser
+ {
+ public DatabaseInitialiser(string databasePath)
+ {
+ using var connection = new SqliteConnection($"Data Source={databasePath}");
+ connection.Open();
+
+ const string createTableQuery = @"
+ CREATE TABLE IF NOT EXISTS CodingSessions (
+ Id INTEGER PRIMARY KEY AUTOINCREMENT,
+ StartTime TEXT NOT NULL,
+ EndTime TEXT NOT NULL
+ );";
+
+ connection.Execute(createTableQuery);
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/DateTimeValidator.cs b/Coding Tracker/Coding Tracker/DateTimeValidator.cs
new file mode 100644
index 00000000..7e34b473
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/DateTimeValidator.cs
@@ -0,0 +1,24 @@
+namespace Coding_Tracker
+{
+ public class DateTimeValidator
+ {
+ public static DateTime ValidateDateResponse(string input, string format = "dd-MM-yyyy HH:mm")
+ {
+ if (DateTime.TryParseExact(input, format, null, System.Globalization.DateTimeStyles.None, out var result))
+ {
+ return result;
+ }
+ throw new FormatException($"Input '{input}' is not in the expected format '{format}'.");
+ }
+
+ public static DateTime ValidateEndDateResponse(DateTime startDate, string input, string format = "dd-MM-yyyy HH:mm")
+ {
+ var endDate = ValidateDateResponse(input, format);
+ if (endDate <= startDate)
+ {
+ throw new FormatException($"End date '{endDate}' must be after start date '{startDate}'.");
+ }
+ return endDate;
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Models/CodingSession.cs b/Coding Tracker/Coding Tracker/Models/CodingSession.cs
new file mode 100644
index 00000000..36468c0a
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Models/CodingSession.cs
@@ -0,0 +1,25 @@
+using Coding_Tracker;
+
+internal class CodingSession
+{
+ public int Id { get; set; }
+ public DateTime StartTime { get; private set; }
+ public DateTime EndTime { get; private set; }
+
+ public TimeSpan Duration => EndTime - StartTime;
+
+ public static CodingSession Create(DateTime startTime, DateTime endTime)
+ {
+ if (endTime <= startTime)
+ {
+ throw new ArgumentException("End time must be after start time.");
+ }
+
+
+ return new CodingSession
+ {
+ StartTime = startTime,
+ EndTime = endTime
+ };
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Models/Enums.cs b/Coding Tracker/Coding Tracker/Models/Enums.cs
new file mode 100644
index 00000000..db37ae4d
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Models/Enums.cs
@@ -0,0 +1,28 @@
+namespace Coding_Tracker.Models
+{
+ internal class Enums
+ {
+
+ internal enum MenuAction
+ {
+ Start_Coding_Session = 1,
+ View_Sessions,
+ View_Previous_30_Day_Activity_Heatmap,
+ Exit
+ }
+
+ internal enum SessionMenuAction
+ {
+ Start_Timer = 1,
+ Enter_Time_Manually,
+ Back_To_Main_Menu
+ }
+
+ internal enum ViewSessionsAction
+ {
+ View_All_Sessions = 1,
+ View_Sessions_From_Set_Date_Selection,
+ Back_To_Main_Menu
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/Program.cs b/Coding Tracker/Coding Tracker/Program.cs
new file mode 100644
index 00000000..07ea1238
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Program.cs
@@ -0,0 +1,22 @@
+using Spectre.Console;
+using Microsoft.Extensions.Configuration;
+using Coding_Tracker.Repositories;
+using Coding_Tracker;
+using Coding_Tracker.Controllers;
+
+var configuration = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
+ .Build();
+
+var dbPath = configuration["Database:ConnectionString"];
+
+var databaseInitialiser = new DatabaseInitialiser(dbPath);
+var repo = new CodingSessionRepository(dbPath);
+
+var userInterface = new UserInterface(repo);
+
+userInterface.MainMenu();
+
+
+
diff --git a/Coding Tracker/Coding Tracker/Repositories/CodingSessionRepository.cs b/Coding Tracker/Coding Tracker/Repositories/CodingSessionRepository.cs
new file mode 100644
index 00000000..61e06240
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/Repositories/CodingSessionRepository.cs
@@ -0,0 +1,53 @@
+using Dapper;
+using Microsoft.Data.Sqlite;
+
+namespace Coding_Tracker.Repositories
+{
+ internal class CodingSessionRepository
+ {
+ private readonly string _connectionString;
+
+ public CodingSessionRepository(string databasePath)
+ {
+ _connectionString = $"Data Source={databasePath}";
+ }
+
+ private SqliteConnection OpenConnection() => new SqliteConnection(_connectionString);
+
+ public int AddSession(CodingSession session)
+ {
+ const string sql = @"
+ INSERT INTO CodingSessions (StartTime, EndTime)
+ VALUES (@StartTime, @EndTime);
+ SELECT last_insert_rowid();";
+
+ using var connection = OpenConnection();
+ return connection.ExecuteScalar(sql, session);
+ }
+
+ public List GetAllSessions()
+ {
+ const string sql = @"
+ SELECT Id, StartTime, EndTime
+ FROM CodingSessions
+ ORDER BY StartTime DESC;";
+
+ using var connection = OpenConnection();
+ return connection.Query(sql).ToList();
+ }
+
+ public List GetFilteredSession(DateTime? fromDate, DateTime? toDate)
+ {
+ const string sql = @"
+ SELECT Id, StartTime, EndTime
+ FROM CodingSessions
+ WHERE (@FromDate IS NULL OR StartTime >= @FromDate)
+ AND (@ToDate IS NULL OR EndTime <= @ToDate)
+ ORDER BY StartTime DESC;";
+
+ using var connection = OpenConnection();
+ return connection.Query(sql, new { FromDate = fromDate, ToDate = toDate }).ToList();
+ }
+
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/UserInterface.cs b/Coding Tracker/Coding Tracker/UserInterface.cs
new file mode 100644
index 00000000..7e5a051f
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/UserInterface.cs
@@ -0,0 +1,159 @@
+using Coding_Tracker.Controllers;
+using Coding_Tracker.Repositories;
+using Spectre.Console;
+using static Coding_Tracker.Models.Enums;
+namespace Coding_Tracker
+{
+ internal class UserInterface
+ {
+ private readonly ConsoleUI _ui = new ConsoleUI();
+ private readonly AutoTimerSessionController _autoTimerSessionController;
+ private readonly ManualTimerSessionController _manualTimerSessionController;
+ private readonly CodingSessionRepository _repo;
+
+ public UserInterface(CodingSessionRepository repo)
+ {
+ _repo = repo;
+ _autoTimerSessionController = new AutoTimerSessionController(_repo, _ui);
+ _manualTimerSessionController = new ManualTimerSessionController(_repo, _ui);
+ }
+ internal void MainMenu()
+ {
+ while (true)
+ {
+ Console.Clear();
+
+ var choice = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select an option:")
+ .AddChoices(Enum.GetValues()));
+
+
+
+ switch (choice)
+ {
+
+ case MenuAction.Start_Coding_Session:
+ bool flowControl = StartSession();
+ if (!flowControl)
+ {
+ continue;
+ }
+ break;
+
+ case MenuAction.View_Sessions:
+ ShowSessions();
+
+ break;
+
+ case MenuAction.View_Previous_30_Day_Activity_Heatmap:
+ _ui.DisplayActivityHeatMap(_repo.GetAllSessions());
+ break;
+ case MenuAction.Exit:
+ Environment.Exit(0);
+ return;
+ }
+
+ }
+ }
+
+ private bool StartSession()
+ {
+ var sessionType = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select session type:")
+ .AddChoices(Enum.GetValues()));
+ switch (sessionType)
+ {
+ case SessionMenuAction.Start_Timer:
+ _autoTimerSessionController.StartSession();
+ break;
+ case SessionMenuAction.Enter_Time_Manually:
+ _manualTimerSessionController.StartSession();
+ break;
+ case SessionMenuAction.Back_To_Main_Menu:
+ return false;
+ }
+
+ return true;
+ }
+
+ private void ShowSessions()
+ {
+ switch (AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("View sessions by:")
+ .AddChoices(Enum.GetValues()
+ )))
+ {
+ case ViewSessionsAction.View_All_Sessions:
+ var allSessions = _repo.GetAllSessions();
+ DisplayFilteredSessions(allSessions);
+ break;
+
+ case ViewSessionsAction.View_Sessions_From_Set_Date_Selection:
+ FilteredSessions();
+ break;
+
+ case ViewSessionsAction.Back_To_Main_Menu:
+ Environment.Exit(0);
+ break;
+ }
+ }
+
+ private void FilteredSessions()
+ {
+ switch (AnsiConsole.Prompt(new SelectionPrompt()
+ .Title("Please select an option from the following:")
+ .AddChoices(new[] {
+ "Yesterday",
+ "Last 7 Days",
+ "Last 30 Days",
+ "Custom Date Range",
+ "Back to Main Menu"
+ }))){
+ case "Yesterday":
+ var yesterday = DateTime.Today.AddDays(-1);
+ var sessionsYesterday = _repo.GetFilteredSession(yesterday, null);
+ DisplayFilteredSessions(sessionsYesterday);
+ break;
+
+ case "Last 7 Days":
+ var last7Days = DateTime.Today.AddDays(-7);
+ var sessions7Days = _repo.GetFilteredSession(last7Days, null);
+ DisplayFilteredSessions(sessions7Days);
+ break;
+
+ case "Last 30 Days":
+ var last30Days = DateTime.Today.AddDays(-30);
+ var sessions30Days = _repo.GetFilteredSession(last30Days, null);
+ DisplayFilteredSessions(sessions30Days);
+ break;
+
+ case "Custom Date Range":
+ var fromDate = _ui.PromptForDateTime("Enter the [green]start[/] date");
+ var toDate = _ui.PromptForDateTime("Enter the [red]end[/] date");
+ var customSessions = _repo.GetFilteredSession(fromDate, toDate);
+ DisplayFilteredSessions(customSessions);
+ break;
+ }
+ }
+
+ private static void DisplayFilteredSessions(List sessions)
+ {
+ if (sessions.Count == 0)
+ {
+ AnsiConsole.MarkupLine("[red]No coding sessions found in the specified date range.[/]");
+ }
+ else
+ {
+ foreach (var s in sessions)
+ {
+ AnsiConsole.MarkupLine($"[green]Session ID:[/] {s.Id}, [green]Start Time:[/] {s.StartTime}, [green]End Time:[/] {s.EndTime}, [green]Duration:[/] {s.Duration:hh\\:mm\\:ss}");
+ }
+ }
+ AnsiConsole.MarkupLine("\nPress any key to return to the main menu...");
+ Console.ReadKey();
+ }
+ }
+}
diff --git a/Coding Tracker/Coding Tracker/appsettings.json b/Coding Tracker/Coding Tracker/appsettings.json
new file mode 100644
index 00000000..4f209301
--- /dev/null
+++ b/Coding Tracker/Coding Tracker/appsettings.json
@@ -0,0 +1,5 @@
+{
+ "Database": {
+ "ConnectionString": "coding_Tracker.db"
+ }
+}
\ No newline at end of file
diff --git a/Coding Tracker/Coding_Tracker.Tests/Coding_Tracker.Tests.csproj b/Coding Tracker/Coding_Tracker.Tests/Coding_Tracker.Tests.csproj
new file mode 100644
index 00000000..d20ae352
--- /dev/null
+++ b/Coding Tracker/Coding_Tracker.Tests/Coding_Tracker.Tests.csproj
@@ -0,0 +1,25 @@
+
+
+
+ net10.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Coding Tracker/Coding_Tracker.Tests/DateTimeValidatorTests.cs b/Coding Tracker/Coding_Tracker.Tests/DateTimeValidatorTests.cs
new file mode 100644
index 00000000..aa7d903d
--- /dev/null
+++ b/Coding Tracker/Coding_Tracker.Tests/DateTimeValidatorTests.cs
@@ -0,0 +1,46 @@
+namespace Coding_Tracker.Tests
+{
+ public class DateTimeValidatorTests
+ {
+ [Fact]
+ public void ValidateDateResponse_ValidDate_ReturnsDateTime()
+ {
+ var validDate = "25-12-2023 14:30";
+ var format = "dd-MM-yyyy HH:mm";
+
+ var result = DateTimeValidator.ValidateDateResponse(validDate, format);
+
+ Assert.Equal(new DateTime(2023, 12, 25, 14, 30, 0), result);
+ }
+
+ [Fact]
+ public void ValidateDateResponse_InvalidDate_ThrowsFormatException()
+ {
+ var invalidDate = "41/13/2025 14:30";
+ var format = "dd-MM-yyyy HH:mm";
+
+ Assert.Throws(() => DateTimeValidator.ValidateDateResponse(invalidDate, format));
+ }
+
+ [Fact]
+ public void ValidateDateResponse_EmptyString_ThrowsFormatException()
+ {
+ var emptyDate = "";
+ var format = "dd-MM-yyyy HH:mm";
+ Assert.Throws(() => DateTimeValidator.ValidateDateResponse(emptyDate, format));
+ }
+
+ [Fact]
+ public void ValidateEndDateResponse_EndBeforeStart_ThrowsFormatException()
+ {
+
+ var start = new DateTime(2026, 1, 2, 9, 0, 0);
+ var endInput = "01-01-2026 09:00";
+ var format = "dd-MM-yyyy HH:mm";
+
+
+ Assert.Throws(() => DateTimeValidator.ValidateEndDateResponse(start, endInput, format));
+ }
+
+ }
+}
diff --git a/README.md b/README.md
new file mode 100644
index 00000000..632b139c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,75 @@
+# Coding Tracker
+
+A simple console app for tracking coding sessions. Start a timer, log sessions manually, review history by date ranges, and view a 30-day activity heatmap.
+
+## Features
+
+- **Track sessions in real time** using a built-in stopwatch.
+- **Log sessions manually** with validated start/end times.
+- **Review history** for all sessions or a filtered date range (yesterday, last 7 days, last 30 days, or custom dates).
+- **Visual activity heatmap** for the previous 30 days.
+- **Local SQLite storage** for sessions.
+
+## Tech Stack
+
+- **.NET 10** console application
+- **SQLite** with **Dapper** for data access
+- **Spectre.Console** for rich terminal UI
+
+## Getting Started
+
+### Prerequisites
+
+- .NET SDK 10.0 or later
+
+### Configure the database
+
+The SQLite database path is configured in `appsettings.json`:
+
+```json
+{
+ "Database": {
+ "ConnectionString": "coding_Tracker.db"
+ }
+}
+```
+
+Update the connection string if you want the database stored elsewhere.
+
+### Run the app
+
+From the repo root:
+
+```bash
+dotnet run --project "Coding Tracker/Coding Tracker"
+```
+
+## Usage Overview
+
+- **Start Coding Session**
+ - Choose **Start Timer** to track time automatically.
+ - Choose **Enter Time Manually** to log a past session.
+- **View Sessions**
+ - View all sessions or filter by a specific date range.
+- **View Previous 30 Day Activity Heatmap**
+ - See a quick visual summary of recent activity.
+
+## Running Tests
+
+```bash
+dotnet test "Coding Tracker/Coding_Tracker.Tests"
+```
+
+## Project Structure
+
+```
+Coding Tracker/
+├── Coding Tracker/ # Application source
+├── Coding_Tracker.Tests/ # Test project
+└── Coding Tracker.slnx
+```
+
+## Notes
+
+- Sessions are stored in a local SQLite database file.
+- The database schema is created automatically on first run.