diff --git a/.codacy.yml b/.codacy.yml
deleted file mode 100644
index 44c0e92eb..000000000
--- a/.codacy.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-exclude_paths:
- - "README.md"
- - "**/README.md"
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 49976e115..5c1f7dab3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -10,6 +10,11 @@
*.userosscache
*.sln.docstates
+
+# Vs Code debug config files
+launch.json
+tasks.json
+
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
@@ -399,7 +404,6 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
-.idea/
##
## Visual studio for Mac
@@ -476,5 +480,3 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk
-/MathGame2
-/CodingTracker.TomDonegan/TextFile1.txt
diff --git a/.vscode/launch.json b/.vscode/launch.json
new file mode 100644
index 000000000..24e29b1b1
--- /dev/null
+++ b/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "build",
+ "program": "${workspaceFolder}/bin/Debug/net10.0/CodingTracker.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "externalTerminal"
+ }
+
+ ]
+}
\ No newline at end of file
diff --git a/.vscode/tasks.json b/.vscode/tasks.json
new file mode 100644
index 000000000..28f9a1994
--- /dev/null
+++ b/.vscode/tasks.json
@@ -0,0 +1,12 @@
+{
+ "version": "2.0.0",
+ "tasks": [
+ {
+ "type": "dotnet",
+ "task": "build",
+ "group": "build",
+ "problemMatcher": [],
+ "label": "build"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/CodingTracker/.vscode/launch.json b/CodingTracker/.vscode/launch.json
new file mode 100644
index 000000000..14a0adc9a
--- /dev/null
+++ b/CodingTracker/.vscode/launch.json
@@ -0,0 +1,20 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": ".NET Core Launch (console)",
+ "type": "coreclr",
+ "request": "launch",
+ "preLaunchTask": "dotnet: build",
+ "program": "${workspaceFolder}/bin/Debug/net10.0/CodingTracker.dll",
+ "args": [],
+ "cwd": "${workspaceFolder}",
+ "stopAtEntry": false,
+ "console": "externalTerminal"
+ }
+
+ ]
+}
\ No newline at end of file
diff --git a/CodingTracker/CodingTracker.csproj b/CodingTracker/CodingTracker.csproj
new file mode 100644
index 000000000..5d8e1568a
--- /dev/null
+++ b/CodingTracker/CodingTracker.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/CodingTracker/Data/Database.cs b/CodingTracker/Data/Database.cs
new file mode 100644
index 000000000..3e510621e
--- /dev/null
+++ b/CodingTracker/Data/Database.cs
@@ -0,0 +1,106 @@
+using CodingSession;
+using Dapper;
+using Microsoft.Data.Sqlite;
+
+namespace CodingTracker;
+
+internal class Database
+{
+ private static string ConnectionString;
+
+ internal static void Configure(string connectionString)
+ {
+ ConnectionString = connectionString;
+ }
+
+ internal static T Execute(Func action)
+ {
+ try
+ {
+ using var connection = new SqliteConnection(ConnectionString);
+ return action(connection);
+ }
+ catch (SqliteException ex)
+ {
+ throw new InvalidOperationException("Database access failed.", ex);
+ }
+ }
+
+ internal static void CreateDB()
+ {
+ bool tableExisted;
+
+ using (var connection = new SqliteConnection(ConnectionString))
+ {
+ const string existsSql =
+ $@"SELECT EXISTS (SELECT name
+ FROM sqlite_master
+ WHERE type = 'table'
+ AND name = 'coding_tracker')";
+
+ tableExisted = connection.ExecuteScalar(existsSql);
+
+ string createSql =
+ $@"CREATE TABLE IF NOT EXISTS coding_tracker(
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ activity TEXT,
+ startTime TEXT,
+ endTime TEXT,
+ duration INTEGER)";
+
+ connection.Execute(createSql);
+ }
+
+ if (!tableExisted)
+ DebugTool.SeedData();
+ }
+
+ internal static void Add(string activity, string startTime, string endTime, int duration)
+ {
+ string sql = @$"INSERT INTO coding_tracker
+ (activity, startTime, endTime, duration)
+ VALUES (@activity, @startTime, @endTime, @duration)";
+
+ Execute(connection => connection.Execute(sql, new
+ {
+ activity = activity,
+ startTime = startTime,
+ endTime = endTime,
+ duration = duration
+ }));
+ }
+
+ internal static void ConvertAndAdd(string activity, DateTime startTime, DateTime endTime, TimeSpan duration)
+ {
+ string startTimeDB = TimeFormatter.ConvertDateForDb(startTime);
+ string endTimeDB = TimeFormatter.ConvertDateForDb(endTime);
+ int durationDB = TimeFormatter.MinutesToInt(duration);
+
+ Add(activity, startTimeDB, endTimeDB, durationDB);
+ }
+
+ internal static void Delete(CodingSession entry)
+ {
+ string sql = @$"DELETE FROM coding_tracker WHERE id = @id";
+ Execute(connection => connection.Execute(sql, new { id = entry.Id }));
+ }
+
+ internal static void UpdateRow(CodingSession entry)
+ {
+ string sql = @$"UPDATE coding_tracker
+ SET activity = @activity,
+ startTime = @startTime,
+ endTime = @endTime,
+ duration = @duration
+ WHERE id = @id";
+
+ Execute(connection => connection.Execute(sql, new
+ {
+ activity = entry.Activity,
+ startTime = entry.StartTime,
+ endTime = entry.EndTime,
+ duration = entry.Duration,
+ id = entry.Id
+ }));
+ }
+}
diff --git a/CodingTracker/Data/Query.cs b/CodingTracker/Data/Query.cs
new file mode 100644
index 000000000..97f3436af
--- /dev/null
+++ b/CodingTracker/Data/Query.cs
@@ -0,0 +1,48 @@
+using Dapper;
+using Microsoft.Data.Sqlite;
+using static CodingTracker.Database;
+
+namespace CodingTracker;
+
+internal class Query
+{
+ internal static List ViewAll()
+ {
+ string sql = @$"SELECT * FROM coding_tracker ORDER BY startTime DESC";
+ return Execute(connection => connection.Query(sql).ToList());
+ }
+
+ internal static int CountRows()
+ {
+ string sql = @$"SELECT COUNT(*) FROM coding_tracker";
+ return Execute(connection => Convert.ToInt32(connection.ExecuteScalar(sql)));
+ }
+
+ internal static int RowFromID(int id)
+ {
+ string sql = @$"SELECT * FROM coding_tracker WHERE id = @id";
+ return Execute(connection => Convert.ToInt32(connection.ExecuteScalar(sql, new { id })));
+ }
+
+ internal static List FilterByActivity(string filter)
+ {
+ string sql = @$"SELECT * FROM coding_tracker WHERE activity LIKE @filter ORDER BY startTime DESC";
+ return Execute(connection => connection.Query(sql, new { filter = $"%{filter}%" }).ToList());
+ }
+
+ internal static List FilterByDate(string startDate, string endDate)
+ {
+ string sql = @$"SELECT * FROM coding_tracker
+ WHERE startTime BETWEEN @startDate AND @endDate
+ ORDER BY startTime DESC";
+ return Execute(connection => connection.Query(sql, new { startDate = startDate, endDate = endDate }).ToList());
+ }
+
+ internal static string LastActivity()
+ {
+ string sql = @$"SELECT activity FROM coding_tracker
+ ORDER BY id DESC
+ LIMIT 1";
+ return Execute(connection => Convert.ToString(connection.ExecuteScalar(sql)));
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/Enums/EnumExtensions.cs b/CodingTracker/Enums/EnumExtensions.cs
new file mode 100644
index 000000000..1109b9306
--- /dev/null
+++ b/CodingTracker/Enums/EnumExtensions.cs
@@ -0,0 +1,30 @@
+using System.Reflection;
+using System.ComponentModel.DataAnnotations;
+using static CodingTracker.Enums;
+
+namespace CodingTracker;
+
+public static class EnumExtensions
+{
+ public static string GetDisplayName(this Enum value)
+ {
+ var member = value.GetType().GetMember(value.ToString())[0];
+
+ return member
+ .GetCustomAttribute()?
+ .GetName()
+ ?? value.ToString();
+ }
+
+ internal static List GetEditableColumn()
+ {
+ List choices = new();
+
+ foreach (string columnName in Enum.GetNames())
+ {
+ if (columnName != Column.id.ToString() && columnName != Column.duration.ToString())
+ choices.Add(((Column)Enum.Parse(typeof(Column), columnName)).GetDisplayName());
+ }
+ return choices;
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/Enums/Enums.cs b/CodingTracker/Enums/Enums.cs
new file mode 100644
index 000000000..f562dfb30
--- /dev/null
+++ b/CodingTracker/Enums/Enums.cs
@@ -0,0 +1,45 @@
+using System.ComponentModel.DataAnnotations;
+
+namespace CodingTracker;
+
+internal class Enums
+{
+ internal enum Column
+ {
+ [Display(Name = "ID")]
+ id,
+
+ [Display(Name = "Activity")]
+ activity,
+
+ [Display(Name = "Start time")]
+ startTime,
+
+ [Display(Name = "End time")]
+ endTime,
+
+ [Display(Name = "Duration")]
+ duration
+ }
+
+ internal enum MenuChoice
+ {
+ [Display(Name = "View entries")]
+ viewItems,
+
+ [Display(Name = "[green]Start[/] a session [green]now[/]")]
+ addItemStartNow,
+
+ [Display(Name = "[green]Add[/] a previous session")]
+ addItem,
+
+ [Display(Name = "[blue]Edit[/] an entry")]
+ editItem,
+
+ [Display(Name = "[red]Delete[/] an entry")]
+ deleteItem,
+
+ [Display(Name = "Exit")]
+ exit,
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/LiveSession.cs b/CodingTracker/LiveSession.cs
new file mode 100644
index 000000000..a7fb0fb8e
--- /dev/null
+++ b/CodingTracker/LiveSession.cs
@@ -0,0 +1,110 @@
+using CodingSession;
+using Spectre.Console;
+using System.Diagnostics;
+using static CodingTracker.TimeFormatter;
+
+namespace CodingTracker;
+
+internal class LiveSession
+{
+
+ internal static void Launch()
+ {
+ string action = "Add";
+ string colorHighlight = "green";
+
+ DateTime startDate = DateTime.Now;
+ Stopwatch stopwatch = Stopwatch.StartNew();
+
+ ConsoleKeyInfo input = new();
+
+ while (input.Key != ConsoleKey.Enter)
+ {
+ SessionActiveStatus(startDate, stopwatch);
+ LiveTimer(stopwatch);
+
+ if (Console.KeyAvailable)
+ {
+ input = Console.ReadKey();
+
+ if (stopwatch.IsRunning && input.Key == ConsoleKey.Spacebar)
+ stopwatch.Stop();
+
+ else if (!stopwatch.IsRunning && input.Key == ConsoleKey.Spacebar)
+ stopwatch.Start();
+ }
+ }
+
+ DateTime endDate = DateTime.Now;
+ SessionOverStatus(startDate, stopwatch);
+
+ string activity = ChoicesPrompt.AskForActivity(action, colorHighlight);
+
+ //add autoFill feature, query the DB for Last activity, catch empty DB
+
+ Database.ConvertAndAdd(activity, startDate, endDate, stopwatch.Elapsed);
+
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($"[{colorHighlight}]The session has been added to the DB.[/]");
+ AnsiConsole.WriteLine();
+ }
+
+ private static void SessionActiveStatus(DateTime date, Stopwatch stopwatch)
+ {
+ string colorHighlight;
+ string state;
+ string action;
+
+ if (stopwatch.IsRunning)
+ {
+ colorHighlight = "orange1";
+ state = "active";
+ action = "pause";
+ }
+
+ else
+ {
+ colorHighlight = "blue";
+ state = "paused";
+ action = "resume";
+ }
+
+ Console.Clear();
+ Displayer.PrintTitle();
+
+ AnsiConsole.MarkupLine($"Coding session [{colorHighlight}]{state}.[/]");
+ AnsiConsole.MarkupLine($"[Grey]Toggle 'Spacebar' to {action} session, 'Enter' to terminate.[/]");
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(DisplayDate(date));
+ }
+
+ private static void SessionOverStatus(DateTime date, Stopwatch stopwatch)
+ {
+ Console.Clear();
+ Displayer.PrintTitle();
+
+ AnsiConsole.MarkupLine($"Coding session [green]done.[/]");
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine(DisplayDate(date));
+ AnsiConsole.MarkupLine(DisplayDuration(stopwatch));
+ }
+
+ private static void LiveTimer(Stopwatch stopwatch)
+ {
+ int clockRow = Console.CursorTop;
+
+ bool cursorVisible = Console.CursorVisible;
+ Console.CursorVisible = false;
+
+ while (!Console.KeyAvailable)
+ {
+ Console.SetCursorPosition(0, clockRow);
+ Console.Write(DisplayDuration(stopwatch));
+
+ Thread.Sleep(100);
+ }
+ AnsiConsole.WriteLine("\n");
+
+ Console.CursorVisible = cursorVisible;
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/Model/CodingSession.cs b/CodingTracker/Model/CodingSession.cs
new file mode 100644
index 000000000..1e98f98b3
--- /dev/null
+++ b/CodingTracker/Model/CodingSession.cs
@@ -0,0 +1,50 @@
+namespace CodingTracker;
+
+class CodingSession
+{
+ internal int Id { get; private set; }
+ internal string Activity { get; private set; }
+ internal string StartTime { get; private set; }
+ internal string EndTime { get; private set; }
+ internal int Duration { get; private set; }
+
+ internal CodingSession(Int64 id, string activity, string startTime, string endTime, Int64 duration)
+ {
+ Id = (int)id;
+ Activity = activity;
+ StartTime = startTime;
+ EndTime = endTime;
+ Duration = (int)duration;
+ }
+
+ internal void EditStartTime(string startTime)
+ {
+ StartTime = startTime;
+ }
+
+ internal void EditEndTime(string endTime)
+ {
+ EndTime = endTime;
+ }
+
+ internal void EditDuration(int duration)
+ {
+ Duration = duration;
+ }
+
+ internal void EditActivity(string activity)
+ {
+ Activity = activity;
+ }
+
+ internal string DisplayData()
+ {
+ string activity = Activity;
+ string startTime = TimeFormatter.ConvertDateOutDb(StartTime);
+ string endTime = TimeFormatter.ConvertHourOutDb(EndTime);
+ string duration = TimeFormatter.IntToHHmm(Duration);
+
+ string result = activity.PadRight(20, ' ') + startTime.PadRight(25, ' ') + endTime.PadRight(10, ' ') + duration;
+ return Spectre.Console.Markup.Escape(result);
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/Program.cs b/CodingTracker/Program.cs
new file mode 100644
index 000000000..00105c2c3
--- /dev/null
+++ b/CodingTracker/Program.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Configuration;
+
+namespace CodingTracker;
+
+class Program
+{
+ static void Main(string[] args)
+ {
+ var configuration = new ConfigurationBuilder()
+ .SetBasePath(AppContext.BaseDirectory)
+ .AddJsonFile("appsettings.json", optional: false)
+ .Build();
+
+ var connectionString = configuration.GetConnectionString("CodingTracker")
+ ?? throw new InvalidOperationException(
+ "Connection string 'CodingTracker' is missing.");
+
+ Database.Configure(connectionString);
+ Database.CreateDB();
+
+ Displayer.PrintTitle();
+ UserInterface.MainMenu();
+ }
+}
diff --git a/CodingTracker/Test/DebugTool.cs b/CodingTracker/Test/DebugTool.cs
new file mode 100644
index 000000000..39b56dddb
--- /dev/null
+++ b/CodingTracker/Test/DebugTool.cs
@@ -0,0 +1,40 @@
+using System.Diagnostics;
+using System.Reflection.Metadata;
+using CodingTracker;
+
+namespace CodingSession;
+
+internal class DebugTool
+{
+ internal static void SeedData()
+ {
+ string[] activities = ["Coding Tracker", "Habit Logger", "Website"];
+ DateTime startDate = RandomDateTime();
+ Random rand = new();
+
+ for (int i = 0; i < 30; i++)
+ {
+ int randDay = rand.Next(0, 4);
+ int randMinutes = rand.Next(0, 580);
+ startDate = startDate.AddDays(randDay).AddMinutes(randMinutes);
+
+ string activity = activities[rand.Next(0, activities.Length)];
+
+ TimeSpan duration = TimeSpan.FromMinutes(rand.Next(5, 440));
+ DateTime endDate = startDate.AddMinutes((int)duration.TotalMinutes);
+
+ Database.ConvertAndAdd(activity, startDate, endDate, duration);
+ }
+ }
+
+ private static DateTime RandomDateTime()
+ {
+ Random rand = new();
+
+ DateTime startDay = new DateTime(2026, 8, 1);
+ int maxRange = (DateTime.Today - startDay).Days + 1;
+ DateTime date = startDay.AddDays(rand.Next(0, maxRange));
+
+ return date;
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/UI/ChoicesPrompt.cs b/CodingTracker/UI/ChoicesPrompt.cs
new file mode 100644
index 000000000..989a41585
--- /dev/null
+++ b/CodingTracker/UI/ChoicesPrompt.cs
@@ -0,0 +1,97 @@
+using System.Runtime.CompilerServices;
+using Spectre.Console;
+using static CodingTracker.TimeFormatter;
+
+namespace CodingTracker;
+
+internal class ChoicesPrompt
+{
+ internal static string AskForActivity(string action, string colorHighlight)
+ {
+ AnsiConsole.MarkupLine("[grey]Press 'Enter' to autofill with the last activity (highest id registered).[/]");
+ var prompt = new TextPrompt($"[{colorHighlight}]{action}[/] activity:").AllowEmpty();
+ string result = AnsiConsole.Prompt(prompt);
+
+ if (result == "")
+ return Query.LastActivity();
+ else
+ return result;
+ }
+
+ public static List AskForFilter(string action, string colorHighlight)
+ {
+ List tableData = new();
+ string[] choices = [
+ $"{action} from all",
+ $"{action} from specific {Enums.Column.activity.GetDisplayName()}",
+ $"{action} from specific Time Range"];
+
+ var selection = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title($"What do you want to [bold {colorHighlight}]{action}[/]:")
+ .HighlightStyle(colorHighlight)
+ .PageSize(100)
+ .Mode(SelectionMode.Leaf)
+ .AddChoices(choices));
+
+ if (selection == choices[0])
+ {
+ tableData = Query.ViewAll();
+ }
+ else if (selection == choices[1])
+ {
+ AnsiConsole.MarkupLine($"[{colorHighlight}]{action}[/] - search specific activity");
+ string activity = AskForActivity(action, colorHighlight);
+ tableData = Query.FilterByActivity(activity);
+ }
+ else if (selection == choices[2])
+ {
+ AnsiConsole.MarkupLine($"[{colorHighlight}]{action}[/] - search specific date");
+ string startDate = ConvertDateForDb(GetTimeFromUser(DaTeStyleShort, "From", "00:00"));
+ string endDate = ConvertDateForDb(GetTimeFromUser(DaTeStyleShort, "To", "23:59"));
+ tableData = Query.FilterByDate(startDate, endDate);
+ }
+
+ return tableData;
+ }
+
+ public static CodingSession SelectEntry(List tableData, string action, string colorHighlight)
+ {
+ var selection = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title($"[BOLD {colorHighlight}]Select[/] the entry you want to [BOLD {colorHighlight}]{action}[/]:")
+ .HighlightStyle(colorHighlight)
+ .UseConverter(entry => entry.DisplayData())
+ .PageSize(100)
+ .AddChoices(tableData));
+
+ return selection;
+ }
+
+ public static List SelectMultiString(string action, string colorHighlight, string[] choices)
+ {
+ var selection = AnsiConsole.Prompt(
+ new MultiSelectionPrompt()
+ .Title($"What do you want to [BOLD {colorHighlight}]{action}[/]")
+ .HighlightStyle(colorHighlight)
+ .InstructionsText($"[grey](Press [{colorHighlight}][/] to toggle, [green][/] to confirm)[/]")
+ .PageSize(100)
+ .AddChoices(choices));
+
+ return selection;
+ }
+
+ public static List SelectMultiSessions(string action, string colorHighlight, List tableData)
+ {
+ var selection = AnsiConsole.Prompt(
+ new MultiSelectionPrompt()
+ .Title($"[BOLD {colorHighlight}]Select[/] the entry you want to [BOLD {colorHighlight}]{action}[/]:")
+ .HighlightStyle(colorHighlight)
+ .InstructionsText($"[grey](Press [{colorHighlight}][/] to toggle, [green][/] to confirm)[/]")
+ .UseConverter(entry => entry.DisplayData())
+ .PageSize(100)
+ .AddChoices(tableData));
+
+ return selection;
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/UI/Displayer.cs b/CodingTracker/UI/Displayer.cs
new file mode 100644
index 000000000..f4b8c4a2a
--- /dev/null
+++ b/CodingTracker/UI/Displayer.cs
@@ -0,0 +1,104 @@
+using Spectre.Console;
+using static CodingTracker.Enums;
+using static CodingTracker.TimeFormatter;
+using System.Linq;
+
+namespace CodingTracker;
+
+internal class Displayer
+{
+ internal static void PreviewRow(string activity, string startTime, string endTime, int duration)
+ {
+ var table = new Table()
+ .RoundedBorder();
+
+ table.AddColumn(Markup.Escape(Enums.Column.activity.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.startTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.endTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.duration.GetDisplayName()));
+
+ table.AddRow(
+ Markup.Escape(activity),
+ Markup.Escape(ConvertDateOutDb(startTime)),
+ Markup.Escape(ConvertHourOutDb(endTime)),
+ Markup.Escape(IntToHHmm(duration)));
+
+ AnsiConsole.Write(table);
+ }
+
+ internal static void PreviewRow(CodingSession session)
+ {
+ var table = new Table()
+ .RoundedBorder();
+
+
+ table.AddColumn(Markup.Escape(Enums.Column.activity.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.startTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.endTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.duration.GetDisplayName()));
+
+ table.AddRow(
+ Markup.Escape(session.Activity),
+ Markup.Escape(ConvertDateOutDb(session.StartTime)),
+ Markup.Escape(ConvertHourOutDb(session.EndTime)),
+ Markup.Escape(IntToHHmm(session.Duration)));
+
+ AnsiConsole.Write(table);
+ }
+
+ internal static void PreviewRows(List sessions)
+ {
+ var table = new Table()
+ .RoundedBorder();
+
+ table.AddColumn(Markup.Escape(Enums.Column.activity.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.startTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.endTime.GetDisplayName()));
+ table.AddColumn(Markup.Escape(Enums.Column.duration.GetDisplayName()));
+
+ foreach (var session in sessions)
+ {
+ table.AddRow(
+ Markup.Escape(session.Activity),
+ Markup.Escape(ConvertDateOutDb(session.StartTime)),
+ Markup.Escape(ConvertHourOutDb(session.EndTime)),
+ Markup.Escape(IntToHHmm(session.Duration)));
+ }
+
+ AnsiConsole.Write(table);
+ }
+
+ internal static void PrintTable(List tableData, string title)
+ {
+ var table = new Table()
+ .RoundedBorder()
+ .Title(title);
+
+ foreach (string column in Enum.GetNames())
+ table.AddColumn(column);
+
+ foreach (var entry in tableData)
+ table.AddRow(
+ Markup.Escape(entry.Id.ToString()),
+ Markup.Escape(entry.Activity),
+ Markup.Escape(ConvertDateOutDb(entry.StartTime)),
+ Markup.Escape(ConvertHourOutDb(entry.EndTime)),
+ Markup.Escape(IntToHHmm(entry.Duration)));
+
+ AnsiConsole.Write(table);
+ }
+
+ internal static void PrintTitle()
+ {
+ Console.Clear();
+ AnsiConsole.Write(new Panel(" Coding Tracker ")
+ .AsciiBorder());
+ AnsiConsole.WriteLine();
+ }
+
+ internal static void NoEntriesFound()
+ {
+ AnsiConsole.MarkupLine($"[red]No entries have been found.[/]");
+ AnsiConsole.WriteLine();
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/UI/UserInterface.cs b/CodingTracker/UI/UserInterface.cs
new file mode 100644
index 000000000..89a2b0ea5
--- /dev/null
+++ b/CodingTracker/UI/UserInterface.cs
@@ -0,0 +1,197 @@
+using Spectre.Console;
+using static CodingTracker.Enums;
+using static CodingTracker.TimeFormatter;
+
+namespace CodingTracker;
+
+internal class UserInterface
+{
+ internal static void MainMenu()
+ {
+ bool exitApp = false;
+
+ while (!exitApp)
+ {
+ var selection = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("What do you want to do?")
+ .HighlightStyle(Color.Grey)
+ .UseConverter(choice => choice.GetDisplayName())
+ .AddChoices(Enum.GetValues()));
+
+ switch (selection)
+ {
+ case MenuChoice.viewItems:
+ ViewAll();
+ break;
+
+ case MenuChoice.addItemStartNow:
+ LiveSession.Launch();
+ break;
+
+ case MenuChoice.addItem:
+ AddItem();
+ break;
+
+ case MenuChoice.editItem:
+ EditItem();
+ break;
+
+ case MenuChoice.deleteItem:
+ DeleteItem();
+ break;
+
+ case MenuChoice.exit:
+ exitApp = true;
+ break;
+ }
+ }
+ }
+
+ internal static void AddItem()
+ {
+ string colorHighlight = "green";
+
+ AnsiConsole.MarkupLine($"[{colorHighlight}]Add Session[/]");
+
+ string startDate = GetTimeFromUser(DaTeStyleShort, "Day").ToString(DaTeStyleShort);
+ string startHour = GetTimeFromUser(DateStyleHour, "start hour").ToString(DateStyleHour);
+ string endHour = GetTimeFromUser(DateStyleHour, "end hour").ToString(DateStyleHour);
+ string activity = ChoicesPrompt.AskForActivity("Add", colorHighlight);
+
+ string[] timeInfos = FormatTimeInfo(startDate, startHour, endHour);
+
+ Database.Add(activity, timeInfos[0], timeInfos[1], Convert.ToInt32(timeInfos[2]));
+
+ AnsiConsole.WriteLine();
+ Displayer.PreviewRow(activity, timeInfos[0], timeInfos[1], Convert.ToInt32(timeInfos[2]));
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($"[{colorHighlight}]The session has been added to the DB.[/]");
+ AnsiConsole.WriteLine();
+ }
+
+ internal static void ViewAll()
+ {
+ List tableData = Query.ViewAll();
+ Displayer.PrintTable(tableData, "[bold] all Sessions[/]");
+ }
+
+ internal static void EditItem()
+ {
+ Displayer.PrintTitle();
+
+ if (Query.CountRows() == 0)
+ {
+ Displayer.NoEntriesFound();
+ return;
+ }
+
+ string action = "Edit";
+ string colorHighlight = "Blue";
+ string[] timeInfos;
+
+ List tableData = ChoicesPrompt.AskForFilter(action, colorHighlight);
+ AnsiConsole.WriteLine();
+
+ if (tableData.Count == 0)
+ {
+ Displayer.NoEntriesFound();
+ return;
+ }
+
+ CodingSession selection = ChoicesPrompt.SelectEntry(tableData, action, colorHighlight);
+
+ AnsiConsole.MarkupLine($"[{colorHighlight}]Selection (id {selection.Id})[/]");
+ Displayer.PreviewRow(selection);
+ AnsiConsole.WriteLine();
+
+ string[] choices = EnumExtensions.GetEditableColumn().ToArray();
+ List toEdit = ChoicesPrompt.SelectMultiString(action, colorHighlight, choices);
+
+ if (toEdit.Contains($"{Column.activity.GetDisplayName()}"))
+ selection.EditActivity(ChoicesPrompt.AskForActivity(action, colorHighlight));
+
+ if (toEdit.Contains($"{Column.startTime.GetDisplayName()}") && toEdit.Contains($"{Column.endTime.GetDisplayName()}"))
+ {
+ string startDate = GetTimeFromUser(DaTeStyleShort, $"[{colorHighlight}]{action}[/] start date").ToString(DaTeStyleShort);
+ string startHour = GetTimeFromUser(DateStyleHour, $"[{colorHighlight}]{action}[/] start hour").ToString(DateStyleHour);
+ string endHour = GetTimeFromUser(DateStyleHour, $"[{colorHighlight}]{action}[/] end hour").ToString(DateStyleHour);
+ timeInfos = FormatTimeInfo(startDate, startHour, endHour);
+
+ selection.EditStartTime(timeInfos[0]);
+ selection.EditEndTime(timeInfos[1]);
+ selection.EditDuration(Convert.ToInt32(timeInfos[2]));
+ }
+
+ else if (toEdit.Contains($"{Column.startTime.GetDisplayName()}"))
+ {
+ string startDate = GetTimeFromUser(DaTeStyleShort, $"[{colorHighlight}]{action}[/] start date").ToString(DaTeStyleShort);
+ string startHour = GetTimeFromUser(DateStyleHour, $"[{colorHighlight}]{action}[/] start hour").ToString(DateStyleHour);
+ string endHour = selection.EndTime;
+ timeInfos = FormatTimeInfo(startDate, startHour, endHour);
+
+ selection.EditStartTime(timeInfos[0]);
+ selection.EditDuration(Convert.ToInt32(timeInfos[2]));
+ }
+
+ else if (toEdit.Contains($"{Column.endTime.GetDisplayName()}"))
+ {
+ string startDate = TrimHour(selection.StartTime);
+ string startHour = TrimDate(selection.StartTime);
+ string endHour = GetTimeFromUser(DateStyleHour, $"[{colorHighlight}]{action}[/] end hour").ToString(DateStyleHour);
+ timeInfos = FormatTimeInfo(startDate, startHour, endHour);
+
+ selection.EditEndTime(timeInfos[1]);
+ selection.EditDuration(Convert.ToInt32(timeInfos[2]));
+ }
+
+ Database.UpdateRow(selection);
+
+ AnsiConsole.WriteLine();
+ Displayer.PreviewRow(selection);
+ AnsiConsole.WriteLine();
+ AnsiConsole.MarkupLine($"[green]Entry succesfully [{colorHighlight}]edited.[/][/]");
+ AnsiConsole.WriteLine();
+ }
+
+ internal static void DeleteItem()
+ {
+ Displayer.PrintTitle();
+
+ if (Query.CountRows() == 0)
+ {
+ Displayer.NoEntriesFound();
+ return;
+ }
+
+ string action = "Delete";
+ string colorHighlight = "Red";
+
+ List tableData = ChoicesPrompt.AskForFilter(action, colorHighlight);
+ AnsiConsole.WriteLine();
+
+ if (tableData.Count == 0)
+ {
+ Displayer.NoEntriesFound();
+ return;
+ }
+
+ List selection = ChoicesPrompt.SelectMultiSessions(action, colorHighlight, tableData);
+ Displayer.PreviewRows(selection);
+
+ if (selection.Count == 0)
+ AnsiConsole.MarkupLine($"[yellow]No entry has been deleted[/]");
+ else
+ {
+ foreach (var entry in selection)
+ Database.Delete(entry);
+
+ if (selection.Count == 1)
+ AnsiConsole.MarkupLine($"[green]{selection.Count} entry has been [red]deleted[/].[/]");
+
+ else
+ AnsiConsole.MarkupLine($"[green]{selection.Count} entries have been [red]deleted[/].[/]");
+ }
+ AnsiConsole.WriteLine();
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/Utility/TimeFormater.cs b/CodingTracker/Utility/TimeFormater.cs
new file mode 100644
index 000000000..da1fecf3e
--- /dev/null
+++ b/CodingTracker/Utility/TimeFormater.cs
@@ -0,0 +1,136 @@
+using System.Diagnostics;
+using Spectre.Console;
+using System.Globalization;
+
+namespace CodingTracker;
+
+internal class TimeFormatter
+{
+ public const string DaTeStyleShort = @"yyyy/MM/dd";
+ public const string DaTeStyleDB = @"yyyy/MM/dd HH:mm";
+ public const string HourStyleShort = @"HH.mm";
+ public const string DateStyleFull = "ddd dd MMM yy - HH:mm";
+ public const string DateStyleHour = "HH:mm";
+ public const string StopWatchStyle = @"hh\:mm\:ss";
+
+ internal static string DisplayDate(DateTime date)
+ {
+ return date.ToString(DateStyleFull);
+ }
+
+ internal static string DisplayDuration(Stopwatch stopwatch)
+ {
+ return stopwatch.Elapsed.ToString(StopWatchStyle);
+ }
+
+ internal static string ConvertDateForDb(DateTime date)
+ {
+ return date.ToString(DaTeStyleDB, CultureInfo.InvariantCulture);
+ }
+
+ internal static string ConvertDateOutDb(string dateString)
+ {
+ return DateTime.Parse(dateString).ToString(DateStyleFull);
+ }
+
+ internal static string ConvertHourOutDb(string dateString)
+ {
+ return DateTime.Parse(dateString).ToString(DateStyleHour);
+ }
+
+ internal static string IntToHHmm(int duration)
+ {
+ int hours = duration / 60;
+ int minutes = duration % 60;
+
+ return $"{hours:D2}:{minutes:D2}";
+ }
+
+ internal static int MinutesToInt(TimeSpan t)
+ {
+ return Convert.ToInt32(t.TotalMinutes);
+ }
+
+ /// return [startDate, endDate, durationString]
+ internal static string[] FormatTimeInfo(string startDate, string startHour, string endHour)
+ {
+ string endDate = MergeDateHour(startDate, endHour);
+ startDate = MergeDateHour(startDate, startHour);
+
+ DateTime eh = DateTime.Parse(endHour);
+ DateTime sh = DateTime.Parse(startHour);
+ TimeSpan duration;
+
+ if (eh < sh) // (add 1 day to the date)
+ {
+ duration = TimeSpan.FromHours(24) + (eh - sh);
+ endDate = (DateTime.Parse(endDate) + TimeSpan.FromDays(1)).ToString(DaTeStyleDB);
+ }
+ else
+ duration = eh - sh;
+
+ string durationString = Convert.ToString(MinutesToInt(duration));
+
+ return [startDate, endDate, durationString];
+ }
+
+ internal static string TrimHour(string fullDate)
+ {
+ return fullDate.Substring(0, DaTeStyleShort.Length + 1);
+ }
+
+ internal static string TrimDate(string fullDate)
+ {
+ return fullDate.Substring(DaTeStyleShort.Length + 1);
+ }
+
+ internal static string MergeDateHour(string startDate, string hour)
+ {
+ return startDate + " " + hour;
+ }
+
+ internal static DateTime GetTimeFromUser(string expectedFormat, string topic)
+ {
+ string errorMessage = $"[red]Format mismatch. Use {expectedFormat}[/]";
+
+ string input = AnsiConsole.Prompt(
+ new TextPrompt($"{topic} ({expectedFormat}): ")
+ .ValidationErrorMessage(errorMessage)
+ .Validate(value =>
+ {
+ return DateTime.TryParseExact(
+ value,
+ expectedFormat,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out _)
+ ? ValidationResult.Success()
+ : ValidationResult.Error(errorMessage);
+ }));
+
+ return DateTime.ParseExact(input, expectedFormat, CultureInfo.InvariantCulture);
+ }
+
+ internal static DateTime GetTimeFromUser(string expectedFormat, string topic, string hour)
+ {
+ string errorMessage = $"[red]Format mismatch. Use {expectedFormat}[/]";
+
+ string input = AnsiConsole.Prompt(
+ new TextPrompt($"{topic} ({expectedFormat}): ")
+ .ValidationErrorMessage(errorMessage)
+ .Validate(value =>
+ {
+ return DateTime.TryParseExact(
+ value,
+ expectedFormat,
+ CultureInfo.InvariantCulture,
+ DateTimeStyles.None,
+ out _)
+ ? ValidationResult.Success()
+ : ValidationResult.Error(errorMessage);
+ }));
+
+ string day = $"{input} {hour}";
+ return DateTime.ParseExact(day, DaTeStyleDB, CultureInfo.InvariantCulture);
+ }
+}
\ No newline at end of file
diff --git a/CodingTracker/appsettings.json b/CodingTracker/appsettings.json
new file mode 100644
index 000000000..c4798fec2
--- /dev/null
+++ b/CodingTracker/appsettings.json
@@ -0,0 +1,8 @@
+{
+ "ConnectionStrings": {
+ "CodingTracker": "Data Source=CodingTracker.db"
+ },
+ "Application": {
+ "Name": "Coding Tracker"
+ }
+}
\ No newline at end of file
diff --git a/Readme.md b/Readme.md
new file mode 100644
index 000000000..715970f6b
--- /dev/null
+++ b/Readme.md
@@ -0,0 +1,44 @@
+# Console Coding Tracker
+Console-based CRUD application to track time spent coding. Developed using C#, SQLite, Dapper, and Spectre.Console.
+
+## Given Requirements
+- Show the data in the console using the Spectre.Console library.
+- Have separate classes in different files (i.e., UserInput.cs, Validation.cs, CodingController.cs).
+- Tell the user the specific date and time format to use and not allow any other format.
+- Create a configuration file called appsettings.json, which will contain the database path, connection strings, and any other configurations you might need.
+- Create a CodingSession class in a separate file with the following properties: Id, StartTime, EndTime, and Duration.
+- The user shouldn't input the duration of the session. It should be calculated based on the start and end times.
+- The user should be able to input the start and end times manually.
+- Use the Dapper ORM for data access instead of ADO.NET.
+
+## Features
+- Users can create a real-time coding session and track its duration.
+- Users can add, edit, or remove a previous session.
+- Users can view sessions using a basic search (time period of session activity).
+- Users can autofill their activity using the latest entry in the database.
+- Date and time inputs are checked to ensure they are in the correct and realistic format.
+
+## Challenges
+I struggled a lot with the Elapsed.Time and DateTime paradigms. As I wanted to let the user input dates in a user-friendly way, I had to do a lot more conversions than I expected.
+As I developed the application, I had to do a lot of helpers conversion methods. Way more that I expected. Sometimes I had the hour without the date, sometime the hour and the date etc. It felt complicated. This made naming and code readability challenging.
+I made early the poor decision of letting the Database class be responsible for adding data in the correct format (Database.ConvertAndAdd()). I'm not redoing that again in the future.
+Otherwise I found it hard to remember some really specific solution I used in previous projects. When facing the same need again, I tend to directly reread what I did.
+It's hard to find the balance between learning by heart and being pragmatic and efficient. I don't think it makes a lot of sense to learn every coding scenario or library.
+
+## Lessons Learned
+- I think I have a good basic understanding of DateTime and Elapsed.Time now.
+- I should plan the project beforehand. I started with what was challenging (the stopwatch) and ended up working in a chaotic way. Knowing which features I want and writing user stories will help me stay focused.
+- Having a side project to test features quickly would be helpful.
+- Getting used to do micro-commit. Till here I only used Github to send my work once done. Now I am trying to get used a habbit of commiting new progress increment.
+
+## Areas to improve
+- I really need to learn how delegates, generics and lambda work. I always struggle to understand code that uses them, and they seem really practical.
+- the process of making an appsettings.json is still a bit mysterious to me
+- Error handling is a vast topic that I will have to dig into.
+
+## Resources Used
+- LearnDapper.com
+- My previous habit tracker project
+- Spectre documentation
+- Various Stack Overflow and Medium posts
+- Copilot to get some syntax quickly and explain how it works, as well as for review when I know I'm going the wrong way