From 70ee45771f2eb43706d60ad3cb1e6efb5ff4490c Mon Sep 17 00:00:00 2001 From: 0lcm Date: Thu, 5 Feb 2026 14:49:13 -0500 Subject: [PATCH 1/6] submission commit --- codingTracker.0lcm | 1 + 1 file changed, 1 insertion(+) create mode 160000 codingTracker.0lcm diff --git a/codingTracker.0lcm b/codingTracker.0lcm new file mode 160000 index 00000000..c56a6b36 --- /dev/null +++ b/codingTracker.0lcm @@ -0,0 +1 @@ +Subproject commit c56a6b366641c763c04d6750e93c7ffc2a235ba2 From 5a1e6453a8ab9a7b341d9064ce611367ea9b8a35 Mon Sep 17 00:00:00 2001 From: 0lcm Date: Thu, 5 Feb 2026 14:57:58 -0500 Subject: [PATCH 2/6] removed folders --- codingTracker.0lcm | 1 - 1 file changed, 1 deletion(-) delete mode 160000 codingTracker.0lcm diff --git a/codingTracker.0lcm b/codingTracker.0lcm deleted file mode 160000 index c56a6b36..00000000 --- a/codingTracker.0lcm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c56a6b366641c763c04d6750e93c7ffc2a235ba2 From 76b2bf563d64d88a4a049871eab9545bb3fed5de Mon Sep 17 00:00:00 2001 From: 0lcm Date: Thu, 5 Feb 2026 14:59:50 -0500 Subject: [PATCH 3/6] Re-added project folder Re-added project folder after removing problematic git files --- .../UnitTest1.cs | 54 ++++ .../codingTracker.0lcm.NUnitTesting.csproj | 28 ++ codingTracker.0lcm/codingTracker.0lcm.slnx | 4 + .../CRUD Controller/SqliteController.cs | 145 ++++++++++ .../Extensions/EnumExtensions.cs | 14 + .../codingTracker.0lcm/Logging/Logging.cs | 72 +++++ .../Models/CodingSession.cs | 21 ++ .../Models/DateTimeFormats.cs | 24 ++ .../codingTracker.0lcm/Models/Enums.cs | 31 +++ .../codingTracker.0lcm/Models/Timer.cs | 20 ++ .../codingTracker.0lcm/Program.cs | 20 ++ .../Properties/launchSettings.json | 8 + .../Services/SessionService.cs | 75 ++++++ .../Services/TimeValidationService.cs | 76 ++++++ .../User Input/UserInputHelper.cs | 83 ++++++ .../User Interface/ConsoleUi.cs | 248 ++++++++++++++++++ .../User Interface/DisplayHelper.cs | 128 +++++++++ .../codingTracker.0lcm/appSettings.json | 19 ++ .../codingTracker.0lcm.csproj | 30 +++ .../codingTracker.0lcm/codingTracker.db | Bin 0 -> 16384 bytes codingTracker.0lcm/goals.txt | 0 21 files changed, 1100 insertions(+) create mode 100644 codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/UnitTest1.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/codingTracker.0lcm.NUnitTesting.csproj create mode 100644 codingTracker.0lcm/codingTracker.0lcm.slnx create mode 100644 codingTracker.0lcm/codingTracker.0lcm/CRUD Controller/SqliteController.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Extensions/EnumExtensions.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Logging/Logging.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Models/CodingSession.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Models/DateTimeFormats.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Models/Enums.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Models/Timer.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Program.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Properties/launchSettings.json create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Services/SessionService.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/Services/TimeValidationService.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/User Input/UserInputHelper.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/User Interface/ConsoleUi.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/User Interface/DisplayHelper.cs create mode 100644 codingTracker.0lcm/codingTracker.0lcm/appSettings.json create mode 100644 codingTracker.0lcm/codingTracker.0lcm/codingTracker.0lcm.csproj create mode 100644 codingTracker.0lcm/codingTracker.0lcm/codingTracker.db create mode 100644 codingTracker.0lcm/goals.txt diff --git a/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/UnitTest1.cs b/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/UnitTest1.cs new file mode 100644 index 00000000..aba54936 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/UnitTest1.cs @@ -0,0 +1,54 @@ +using codingTracker._0lcm.Services; + +namespace codingTracker._0lcm.NUnitTesting +{ + public class TimeValidationTests + { + [TestCase("2026-1-1", true)] + [TestCase("2026-01-01", true)] + [TestCase("2026-01-1", true)] + [TestCase("2026-1-01", true)] + [TestCase("2026-31-1", false)] + [TestCase("26-1-1", false)] + [TestCase("2026/1/1", false)] + public void TryValidateDateTime_GivenValidInput_ReturnCorrectBool(string input, bool expectedResult) + { + bool? result; + + try + { + result = TimeValidationService.TryValidateDateTime(input, out DateOnly date, out string? errorMessage); + } + catch (Exception) + { + result = null; + } + + Assert.That(result, Is.EqualTo(expectedResult)); + } + + [TestCase("10:10", "12:20", true)] + [TestCase("10:10", "08:20", true)] + [TestCase("10:10", "12:20", true)] + [TestCase("1:10", "02:20", true)] + [TestCase("01:10", "2:20", true)] + [TestCase("10:01", "12:00", true)] + [TestCase("10:1", "12:20", false)] + public void TryValidateStartAndEndTimes_GivenValidInputs_ReturnCorrectBool(string startTimeInput, string endTimeInput, bool expectedResult) + { + bool? result; + + try + { + result = TimeValidationService.TryValidateStartAndEndTimes(startTimeInput, endTimeInput, + out DateTime startTime, out DateTime endTime, out string? errorMessage); + } + catch (Exception) + { + result = null; + } + + Assert.That(result, Is.EqualTo(expectedResult)); + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/codingTracker.0lcm.NUnitTesting.csproj b/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/codingTracker.0lcm.NUnitTesting.csproj new file mode 100644 index 00000000..2b631d5a --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm.NUnitTesting/codingTracker.0lcm.NUnitTesting.csproj @@ -0,0 +1,28 @@ + + + + net10.0 + codingTracker._0lcm.NUnitTesting + latest + enable + enable + false + + + + + + + + + + + + + + + + + + + diff --git a/codingTracker.0lcm/codingTracker.0lcm.slnx b/codingTracker.0lcm/codingTracker.0lcm.slnx new file mode 100644 index 00000000..a1114285 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm.slnx @@ -0,0 +1,4 @@ + + + + diff --git a/codingTracker.0lcm/codingTracker.0lcm/CRUD Controller/SqliteController.cs b/codingTracker.0lcm/codingTracker.0lcm/CRUD Controller/SqliteController.cs new file mode 100644 index 00000000..2844186f --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/CRUD Controller/SqliteController.cs @@ -0,0 +1,145 @@ +using codingTracker._0lcm.Logging; +using codingTracker._0lcm.Models; +using Dapper; +using Microsoft.Data.Sqlite; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; + +namespace codingTracker._0lcm.CRUD_Controller; + +internal class SqliteController +{ + private static readonly ILogger Logger = AppLogger.CreateLogger(); + + private static readonly string fullDateWithTimeFormat = DateTimeFormats.FullDateWithTimeFormat; + + //------- Connection Factory ------- + private static SqliteConnection CreateOpenConnection() + { + string? connectionString = Program.configuration.GetConnectionString("DefaultConnection"); + if (string.IsNullOrEmpty(connectionString)) + { + throw new InvalidOperationException("Connection string 'DefaultConnection' not found."); + } + + var connection = new SqliteConnection(connectionString); + connection.Open(); + return connection; + } + + //------- Execution Helpers ------- + private static void Execute(string sql, object? parameters = null) + { + using var connection = CreateOpenConnection(); + connection.Execute(sql, parameters); + } + + //------- Initlize Database ------- + internal static void CreateDatabase() + { + const string CreateTableQuery = @" + CREATE TABLE IF NOT EXISTS codingSessions ( + id INTEGER NOT NULL UNIQUE, + startTime TEXT NOT NULL, + endTime TEXT NOT NULL, + duration TEXT NOT NULL, + PRIMARY KEY(id AUTOINCREMENT) + );"; + + try + { + Execute(CreateTableQuery); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error creating database table."); + throw; + } + } + + //------- CRUD Operations ------- + internal static void InsertCodingSession(CodingSession session) + { + const string InsertCommand = @" + INSERT INTO codingSessions (startTime, endTime, duration) + VALUES (@StartTime, @EndTime, @Duration); + SELECT last_insert_rowid(); + "; + + try + { + using var connection = CreateOpenConnection(); + int sessionId = connection.ExecuteScalar(InsertCommand, new + { + StartTime = session.StartTime.ToString(fullDateWithTimeFormat), + EndTime = session.EndTime.ToString(fullDateWithTimeFormat), + Duration = session.Duration.ToString(@"hh\:mm\:ss") + }); + + session.Id = sessionId; + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Error inserting coding session."); + throw; + } + } + + internal static void DeleteCodingSession(CodingSession session) + { + const string DeleteCommand = "DELETE FROM codingSessions WHERE id = @Id"; + + try + { + Execute(DeleteCommand, session); + } + catch (Exception ex) + { + Logger.LogError(ex, "Error while trying to delete a session."); + throw; + } + } + + internal static void UpdateCodingSession(CodingSession session) + { + const string UpdateCommand = @" + UPDATE codingSessions + SET startTime = @StartTime, endTime = @EndTime, duration = @Duration + WHERE id = @Id + "; + + try + { + Execute(UpdateCommand, new + { + Id = session.Id, + StartTime = session.StartTime.ToString(fullDateWithTimeFormat), + EndTime = session.EndTime.ToString(fullDateWithTimeFormat), + Duration = session.Duration.ToString(@"hh\:mm\:ss") + }); + } + catch (Exception ex) + { + Logger.LogWarning(ex, "Could not update session"); + throw; + } + } + + //------- Queries ------- + internal static List GetAllSessions() + { + const string Query = "SELECT * FROM codingSessions"; + + using var connection = CreateOpenConnection(); + var results = connection.Query(Query).ToList(); + + return results.Select(row => new CodingSession + { + Id = (int)row.id, + StartTime = DateTime.Parse(row.startTime), + EndTime = DateTime.Parse(row.endTime), + Duration = TimeSpan.Parse(row.duration), + Date = DateOnly.FromDateTime(DateTime.Parse(row.startTime)), + }).ToList(); + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Extensions/EnumExtensions.cs b/codingTracker.0lcm/codingTracker.0lcm/Extensions/EnumExtensions.cs new file mode 100644 index 00000000..b53a72f7 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Extensions/EnumExtensions.cs @@ -0,0 +1,14 @@ +namespace codingTracker._0lcm.Extensions +{ + internal static class EnumExtensions + { + internal static string ToDisplayString(this Enum value) + { + return System.Text.RegularExpressions.Regex.Replace( + value.ToString(), + "([a-z])([A-Z])", + "$1 $2" + ); + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Logging/Logging.cs b/codingTracker.0lcm/codingTracker.0lcm/Logging/Logging.cs new file mode 100644 index 00000000..05cdf77d --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Logging/Logging.cs @@ -0,0 +1,72 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Logging.Console; + +namespace codingTracker._0lcm.Logging +{ + internal class CustomFormatter : ConsoleFormatter + { + public CustomFormatter() : base("customFormatter") { } + + public override void Write( + in LogEntry logEntry, + IExternalScopeProvider? scopeProvider, + TextWriter textWriter + ) + { + string message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (string.IsNullOrEmpty(message)) return; + + ConsoleColor originalColor = Console.ForegroundColor; + try + { + Console.ForegroundColor = GetLogLevelColor(logEntry.LogLevel); + + textWriter.Write($"[{DateTimeOffset.Now:HH:mm:ss}]"); + + textWriter.Write($"[{logEntry.LogLevel,-12}]"); + + textWriter.Write($"[{logEntry.Category}]"); + + textWriter.Write(message); + + if (logEntry.Exception != null) + { + textWriter.Write(logEntry.Exception.ToString()); + } + } + finally + { + Console.ForegroundColor = originalColor; + } + + } + + private static ConsoleColor GetLogLevelColor(LogLevel logLevel) => logLevel switch + { + LogLevel.Trace => ConsoleColor.Gray, + LogLevel.Debug => ConsoleColor.Gray, + LogLevel.Information => ConsoleColor.Green, + LogLevel.Warning => ConsoleColor.Yellow, + LogLevel.Error => ConsoleColor.Red, + LogLevel.Critical => ConsoleColor.Magenta, + _ => ConsoleColor.White, + }; + } + + internal class AppLogger + { + private static readonly ILoggerFactory AppLoggerFactory = + LoggerFactory.Create(builder => + { + builder + .SetMinimumLevel(LogLevel.Debug) + .AddConsole(options => + { + options.FormatterName = "customFormatter"; + }) + .AddConsoleFormatter(); + }); + internal static ILogger CreateLogger() => AppLoggerFactory.CreateLogger(); + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Models/CodingSession.cs b/codingTracker.0lcm/codingTracker.0lcm/Models/CodingSession.cs new file mode 100644 index 00000000..cf75c336 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Models/CodingSession.cs @@ -0,0 +1,21 @@ +namespace codingTracker._0lcm.Models +{ + internal class CodingSession + { + public int Id { get; set; } + public DateOnly Date { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public TimeSpan Duration { get; set; } + + public CodingSession() { } + + public CodingSession(DateTime startTime, DateTime endTime, TimeSpan duration) + { + StartTime = startTime; + EndTime = endTime; + Duration = TimeSpan.FromSeconds(Math.Floor(duration.TotalSeconds)); + Date = DateOnly.FromDateTime(startTime); + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Models/DateTimeFormats.cs b/codingTracker.0lcm/codingTracker.0lcm/Models/DateTimeFormats.cs new file mode 100644 index 00000000..ee83a4ea --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Models/DateTimeFormats.cs @@ -0,0 +1,24 @@ + +using Microsoft.Extensions.Configuration; + +namespace codingTracker._0lcm.Models +{ + internal static class DateTimeFormats + { + private static readonly IConfiguration _configuration = Program.configuration; + + internal static readonly string[] HourFormats = + _configuration.GetSection("TimeFormats:HourFormat").Get() + ?? ["H:mm", "HH:mm"]; + + internal static readonly string[] DateFormats = + _configuration.GetSection("TimeFormats:DateFormats").Get() + ?? ["yyyy-MM-dd", "yyyy-MM-d", "yyyy-M-dd", "yyyy-M-d"]; + + internal static readonly string DateIso = + _configuration["TimeFormats:DateIso"] ?? "yyyy-MM-dd"; + + internal static readonly string FullDateWithTimeFormat = + _configuration["TimeFormats:FullDateWithTimeFormat"] ?? "yyyy-MM-dd HH:mm:ss"; + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Models/Enums.cs b/codingTracker.0lcm/codingTracker.0lcm/Models/Enums.cs new file mode 100644 index 00000000..28013f76 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Models/Enums.cs @@ -0,0 +1,31 @@ +namespace codingTracker._0lcm.Models +{ + internal class Enums + { + internal enum MainMenuOption + { + NewSession, + ViewSessions, + StartTimer, + Exit + } + internal enum LoadSessionOption + { + Delete, + Update, + Return, + } + internal enum FilterSessionDateOption + { + Today, + Other, + Default, + } + internal enum FilterSessionAscendingOption + { + Ascending, + Descending, + Default + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Models/Timer.cs b/codingTracker.0lcm/codingTracker.0lcm/Models/Timer.cs new file mode 100644 index 00000000..9240725c --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Models/Timer.cs @@ -0,0 +1,20 @@ +namespace codingTracker._0lcm.Models +{ + internal class Timer + { + public DateTime StartTime { get; private set; } + public DateTime EndTime { get; private set; } + public bool IsStopped { get; private set; } + + public Timer() + { + StartTime = DateTime.Now; + } + + public void Stop() + { + EndTime = DateTime.Now; + IsStopped = true; + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Program.cs b/codingTracker.0lcm/codingTracker.0lcm/Program.cs new file mode 100644 index 00000000..602cf66a --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Program.cs @@ -0,0 +1,20 @@ +using codingTracker._0lcm.CRUD_Controller; +using codingTracker._0lcm.User_Interface; +using Microsoft.Extensions.Configuration; + +namespace codingTracker._0lcm +{ + internal class Program + { + internal static readonly IConfiguration configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appSettings.json", optional: false, reloadOnChange: true) + .Build(); + + static async Task Main(string[] args) + { + SqliteController.CreateDatabase(); + await ConsoleUi.MainMenu(); + } + } +} \ No newline at end of file diff --git a/codingTracker.0lcm/codingTracker.0lcm/Properties/launchSettings.json b/codingTracker.0lcm/codingTracker.0lcm/Properties/launchSettings.json new file mode 100644 index 00000000..3ef287ba --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "codingTracker.0lcm": { + "commandName": "Project", + "workingDirectory": "$(ProjectDir)" + } + } +} \ No newline at end of file diff --git a/codingTracker.0lcm/codingTracker.0lcm/Services/SessionService.cs b/codingTracker.0lcm/codingTracker.0lcm/Services/SessionService.cs new file mode 100644 index 00000000..86f97e10 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Services/SessionService.cs @@ -0,0 +1,75 @@ +using codingTracker._0lcm.CRUD_Controller; +using codingTracker._0lcm.Models; + +namespace codingTracker._0lcm.Services +{ + internal class SessionService + { + internal static void CreateNewSession(DateTime startTime, DateTime endTime, TimeSpan duration) + { + CodingSession session = new( + startTime: startTime, + endTime: endTime, + duration: duration + ); + + SqliteController.InsertCodingSession(session); + } + + internal static Task CreateTimerTask(Models.Timer timer, CancellationToken cancellationToken) + { + Task runTimer = Task.Run(async () => + { + try + { + await Task.Delay(Timeout.Infinite, cancellationToken); + } + catch (TaskCanceledException) + { + timer.Stop(); + } + }); + return runTimer; + } + + internal static void SaveTimer(Models.Timer timer) + { + CodingSession session = new( + startTime: timer.StartTime, + endTime: timer.EndTime, + duration: timer.EndTime - timer.StartTime + ); + + SqliteController.InsertCodingSession(session); + } + + internal static List GetFilteredSessions(DateOnly? filterDate = null, bool? ascending = true) + { + var sessions = SqliteController.GetAllSessions(); + + if (filterDate.HasValue) + { + sessions = sessions.Where(s => s.Date == filterDate.Value).ToList(); + } + + if (ascending != null) + { + sessions = (bool)ascending + ? sessions.OrderBy(s => s.Duration).ToList() + : sessions.OrderByDescending(s => s.Duration).ToList(); + } + + return sessions; + } + + internal static bool CheckDateOnlyInSessions(DateOnly date) + { + var sessions = SqliteController.GetAllSessions(); + foreach (var session in sessions) + { + if (date == session.Date) return true; + } + return false; + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/Services/TimeValidationService.cs b/codingTracker.0lcm/codingTracker.0lcm/Services/TimeValidationService.cs new file mode 100644 index 00000000..50285fb6 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/Services/TimeValidationService.cs @@ -0,0 +1,76 @@ +using codingTracker._0lcm.Models; +using System.Globalization; + +namespace codingTracker._0lcm.Services +{ + public class TimeValidationService + { + private static readonly string[] hourFormats = DateTimeFormats.HourFormats; + private static readonly string[] dateFormats = DateTimeFormats.DateFormats; + + /// + /// Takes a startTime string and endTime string and attempts to parse them by 'H:mm' and 'HH:mm'. Also checks that endTime + /// cannot be before starTime. outputs the validated startTime and endTime, as well as an error message if any validation + /// check fails. + /// + public static bool TryValidateStartAndEndTimes( + string startTimeInput, + string endTimeInput, + out DateTime startTime, + out DateTime endTime, + out string? errorMessage) + { + startTime = default; + endTime = default; + errorMessage = null; + + bool isValidStartTime = DateTime.TryParseExact(startTimeInput.Trim(), + hourFormats, + CultureInfo.InvariantCulture, + DateTimeStyles.None, out var parsedStartTime); + bool isValidEndTime = DateTime.TryParseExact(endTimeInput.Trim(), + hourFormats, + CultureInfo.InvariantCulture, + DateTimeStyles.None, out var parsedEndTime); + + if (!isValidStartTime || !isValidEndTime) + { + errorMessage = "Invalid Time Format. Please Use HH:mm (e.g 09:30 or 9:30)"; + return false; + } + + startTime = DateTime.Today + parsedStartTime.TimeOfDay; + endTime = DateTime.Today + parsedEndTime.TimeOfDay; + + if (endTime <= startTime) + { + endTime = endTime.AddDays(1); + } + + return true; + } + + /// + /// Trys to Parse a string to a DateTime following Iso yyyy-MM-dd to be converted to DateOnly and outputted. + /// + public static bool TryValidateDateTime(string dateInput, out DateOnly date, out string? errorMessage) + { + date = default; + errorMessage = null; + + bool isValidDate = DateTime.TryParseExact + (dateInput.Trim(), dateFormats, + CultureInfo.InvariantCulture, + DateTimeStyles.None, out var parsedDate); + + if (!isValidDate) + { + errorMessage = "Invalid Time Format. Please Use yyyy-MM-dd"; + return false; + } + + date = DateOnly.FromDateTime(parsedDate); + return true; + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/User Input/UserInputHelper.cs b/codingTracker.0lcm/codingTracker.0lcm/User Input/UserInputHelper.cs new file mode 100644 index 00000000..a50422ec --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/User Input/UserInputHelper.cs @@ -0,0 +1,83 @@ +using codingTracker._0lcm.Models; +using codingTracker._0lcm.Services; +using codingTracker._0lcm.User_Interface; +using Spectre.Console; + +namespace codingTracker._0lcm.User_Input +{ + internal class UserInputHelper + { + /// + /// Asks user to manually input a valid start and end time and returns a new CodingSession + /// or updates and returns a pre-existing CodingSession if one is passed through. + /// + internal static CodingSession GetSessionFromInput(CodingSession? session = null) + { + while (true) + { + Console.Clear(); + + DisplayHelper.DisplayInfo("Please Follow a 24hr HH:mm Format (00:00-23:59). Press to Submit Input."); + DisplayHelper.DisplayInfo("Please Note That If Your End Time Is Before Your Start Time It Will Automatically Be Counted as a Cross-Midnight Session.\n"); + + string startTimeInput = DisplayHelper.DisplayQuestion("Please Enter a Start Time:"); + string endTimeInput = DisplayHelper.DisplayQuestion("Please Enter an End Time:"); + + if (TimeValidationService.TryValidateStartAndEndTimes(startTimeInput, endTimeInput, + out DateTime startTime, out DateTime endTime, out string? errorMessage)) + { + TimeSpan duration = endTime - startTime; + if (duration.TotalHours >= 10) + { + if (!AnsiConsole.Confirm($"[{DisplayHelper.Red}]This Session Is {duration.TotalHours:F1} Hours Long. Is That Right?[/]")) continue; + } + + if (session != null) + { + session.StartTime = startTime; + session.EndTime = endTime; + session.Duration = duration; + + return session; + } + + return new CodingSession( + startTime: startTime, + endTime: endTime, + duration: endTime - startTime + ); + } + + DisplayHelper.DisplayUrgent(errorMessage ?? "Invalid Input."); + DisplayHelper.DisplayInfo("Press To Re-enter Time Selections."); + Console.ReadLine(); + } + } + + internal static DateOnly GetDateInput() + { + while (true) + { + Console.Clear(); + + DisplayHelper.DisplayInfo("Please Follow YYYY-MM-dd Format"); + + string dateInput = DisplayHelper.DisplayQuestion("Please enter a date:"); + + if (TimeValidationService.TryValidateDateTime(dateInput, out DateOnly date, out string? errorMessage) + && SessionService.CheckDateOnlyInSessions(date)) + { + return date; + } + else if (errorMessage == null && !SessionService.CheckDateOnlyInSessions(date)) + { + errorMessage = "Date Is Not In Recorded Sessions. Please Choose A New Date."; + } + + DisplayHelper.DisplayUrgent(errorMessage ?? "Invalid Input."); + DisplayHelper.DisplayInfo("Press To Re-Enter Date Selection."); + Console.ReadLine(); + } + } + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/User Interface/ConsoleUi.cs b/codingTracker.0lcm/codingTracker.0lcm/User Interface/ConsoleUi.cs new file mode 100644 index 00000000..721661f6 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/User Interface/ConsoleUi.cs @@ -0,0 +1,248 @@ +using codingTracker._0lcm.CRUD_Controller; +using codingTracker._0lcm.Logging; +using codingTracker._0lcm.Models; +using codingTracker._0lcm.Services; +using codingTracker._0lcm.User_Input; +using Microsoft.Extensions.Logging; +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace codingTracker._0lcm.User_Interface +{ + internal class ConsoleUi + { + private static readonly ILogger Logger = AppLogger.CreateLogger(); + + private static readonly string DateIso = DateTimeFormats.DateIso; + + const string ReturnOption = "Return to Menu"; + const string FilterOption = "Filter Sessions"; + const string ClearFilterOption = "Clear Filters"; + + //------- Main Menu ------- + internal static async Task MainMenu() + { + while (true) + { + try + { + Console.Clear(); + await HandleMainMenuChoice(); + } + catch (Exception ex) + { + HandleException(ex); + } + } + } + + private static async Task HandleMainMenuChoice() + { + var mainMenuOption = DisplayHelper.DisplayMenu(); + switch (mainMenuOption) + { + case Enums.MainMenuOption.NewSession: + CreateNewSession(); + break; + + case Enums.MainMenuOption.ViewSessions: + ViewSessions(); + break; + + case Enums.MainMenuOption.StartTimer: + await StartTimer(); + break; + + case Enums.MainMenuOption.Exit: + ExitApplication(); + break; + } + } + + private static void HandleException(Exception ex) + { + DisplayHelper.DisplayError("So Sorry, An Error Has Occured Somewhere:"); + Logger.LogError(ex, "An Error Ocurred In Main Menu, Or Subsequent Methods: "); + + DisplayHelper.DisplayWarning("Please Press Enter to Return to The Main Menu"); + Console.ReadLine(); + } + + private static void ExitApplication() + { + Console.Clear(); + DisplayHelper.DisplaySpinner("Closing Application...", 1500); + Environment.Exit(0); + } + + //------- CRUD Operations ------- + private static void CreateNewSession() + { + SqliteController.InsertCodingSession(UserInputHelper.GetSessionFromInput()); + + DisplayHelper.DisplaySuccess("Succesfully Created Session!"); + DisplayHelper.DisplayInfo("Press To Continue."); + Console.ReadLine(); + } + + private static void UpdateSession(CodingSession session) + { + SqliteController.UpdateCodingSession(UserInputHelper.GetSessionFromInput(session)); + + DisplayHelper.DisplaySuccess("Succesfully Updated Session!"); + DisplayHelper.DisplayInfo("Press To Continue"); + Console.ReadLine(); + } + + private static void DeleteSession(CodingSession session) + { + if (AnsiConsole.Confirm($"[{DisplayHelper.Red}]Are You Sure You Want to Delete This Session?[/]")) + { + SqliteController.DeleteCodingSession(session); + + DisplayHelper.DisplaySuccess("Succesfully Deleted Session."); + DisplayHelper.DisplayInfo("Press To Continue."); + Console.ReadLine(); + } + } + + //------- View Sessions ------- + private static void ViewSessions() + { + DisplayHelper.DisplayInfo("Scroll With And . Press To Choose An Option."); + + List? filteredSessions = null; + bool hasLoadedOnce = false; + + while (true) + { + Console.Clear(); + + if (!hasLoadedOnce) + { + DisplayHelper.DisplaySpinner("Loading Sessions...", 2000); + hasLoadedOnce = true; + } + + var sessionMap = BuildSessionMap(filteredSessions); + + string choice = DisplayHelper.DisplayPrompt( + sessionMap.Keys.ToList(), + title: "| ID \t| Date \t | Duration |"); + + if (choice == ReturnOption) return; + if (choice == FilterOption) + { + filteredSessions = FilterSessions(); + continue; + } + if (choice == ClearFilterOption) + { + filteredSessions = null; + continue; + } + + LoadSpecificSession(sessionMap[choice]!); + } + } + + private static Dictionary BuildSessionMap(List? filteredSessions) + { + List sessions = filteredSessions ?? SqliteController.GetAllSessions(); + var sessionMap = new Dictionary(); + + sessionMap[ReturnOption] = null; + sessionMap[FilterOption] = null; + if (filteredSessions != null) + { + sessionMap[ClearFilterOption] = null; + } + + foreach (var session in sessions) + { + string date = session.Date.ToString(DateIso); + string display = $"ID: {session.Id}. {date} - {session.Duration}"; + sessionMap[display] = session; + } + + return sessionMap; + } + + private static void LoadSpecificSession(CodingSession session) + { + DisplayHelper.DisplaySpinner("Loading Session...", 1000); + DisplayHelper.DisplaySuccess("Succesfully Loaded Session:\n"); + + string date = session.Date.ToString(DateIso); + var properties = new List + { + new Markup($"[{DisplayHelper.White}]Date: [{DisplayHelper.Yellow}]{date}[/][/]"), + new Markup($"[{DisplayHelper.White}]Start Time: [{DisplayHelper.Green}]{session.StartTime}[/][/]"), + new Markup($"[{DisplayHelper.White}]End Time: [{DisplayHelper.Red}]{session.EndTime}[/][/]"), + new Markup($"[{DisplayHelper.White}]Duration: [{DisplayHelper.Yellow}]{session.Duration}[/][/]") + }; + DisplayHelper.DisplayRows(properties); + DisplayHelper.DisplayMessage("\n"); + var choice = DisplayHelper.DisplayMenu(); + + switch (choice) + { + case Enums.LoadSessionOption.Delete: + DeleteSession(session); + break; + case Enums.LoadSessionOption.Update: + UpdateSession(session); + break; + case Enums.LoadSessionOption.Return: + return; + } + } + + //------- Filter Operations ------- + private static List FilterSessions() + { + var filterDateChoice = DisplayHelper.DisplayMenu + (title: "Select 'Today' For Today's Date, 'Other' To Manually Insert a Date, and 'All' To See All Sessions."); + + var filterChoice = DisplayHelper.DisplayMenu + (title: "Select 'Ascending' or 'Descending', or 'Default' For the Default Filtering."); + + DateOnly? filterDate = filterDateChoice switch + { + Enums.FilterSessionDateOption.Today => DateOnly.FromDateTime(DateTime.Today), + Enums.FilterSessionDateOption.Other => UserInputHelper.GetDateInput(), + Enums.FilterSessionDateOption.Default => null, + _ => throw new NotImplementedException() + }; + + bool? ascending = filterChoice switch + { + Enums.FilterSessionAscendingOption.Ascending => true, + Enums.FilterSessionAscendingOption.Descending => false, + Enums.FilterSessionAscendingOption.Default => null, + _ => throw new NotImplementedException() + }; + + return SessionService.GetFilteredSessions(filterDate, ascending); + } + + //------- Timer ------- + private static async Task StartTimer() + { + var timer = new Models.Timer(); + + var cts = new CancellationTokenSource(); + var task = SessionService.CreateTimerTask(timer, cts.Token); + + var spinnerTask = DisplayHelper.DisplayAsyncSpinner("Press 'Q' To Stop Timer", task); + + while (Console.ReadKey(true).Key != ConsoleKey.Q) { } + cts.Cancel(); + + await spinnerTask; + + SessionService.SaveTimer(timer); + DisplayHelper.DisplaySpinner("Saving Coding Session...", 3500); + } + } +} \ No newline at end of file diff --git a/codingTracker.0lcm/codingTracker.0lcm/User Interface/DisplayHelper.cs b/codingTracker.0lcm/codingTracker.0lcm/User Interface/DisplayHelper.cs new file mode 100644 index 00000000..7ee821cf --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/User Interface/DisplayHelper.cs @@ -0,0 +1,128 @@ +using codingTracker._0lcm.Extensions; +using Spectre.Console; +using Spectre.Console.Rendering; + +namespace codingTracker._0lcm.User_Interface; + +internal static class DisplayHelper +{ + //------- Colors ------- + internal const string White = "#f1f1f1"; + internal const string Grey = "#8c8e8f"; + internal const string Green = "#32aa3b"; + internal const string Red = "#cd2d2d"; + internal const string Yellow = "#e2b929"; + internal const string Error = "#870c00"; + + //------- Basic Outputs ------- + internal static void DisplayMessage(string message, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{White}]{message}[/]"); + else + AnsiConsole.Markup($"[{White}]{message}[/]"); + } + internal static void DisplayRows(List rows, bool writeLine = true) + { + var rowsLayout = new Rows(rows); + AnsiConsole.Write(rowsLayout); + } + internal static void DisplayInfo(string info, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{Grey}]{info}[/]"); + else + AnsiConsole.Markup($"[{Grey}]{info}[/]"); + } + internal static void DisplaySuccess(string message, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{Green}]{message}[/]"); + else + AnsiConsole.Markup($"[{Green}]{message}[/]"); + } + internal static void DisplayUrgent(string message, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{Red}]{message}[/]"); + else + AnsiConsole.Markup($"[{Red}]{message}[/]"); + } + internal static void DisplayWarning(string message, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{Yellow}]{message}[/]"); + else + AnsiConsole.Markup($"[{Yellow}]{message}[/]"); + } + internal static void DisplayError(string message, bool writeLine = true) + { + if (writeLine) + AnsiConsole.MarkupLine($"[{Error}]{message}[/]"); + else + AnsiConsole.Markup($"[{Error}]{message}[/]"); + } + + //------- Menus & Prompts ------- + internal static T DisplayMenu(string? title = null) where T : Enum + { + var menuChoice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title(title ?? "Please Select An Option:") + .HighlightStyle(Style.Parse("darkviolet")) + .AddChoices(Enum.GetValues(typeof(T)).Cast()) + .UseConverter(e => EnumExtensions.ToDisplayString(e)) + ); + + + return menuChoice; + } + internal static string DisplayPrompt(List choiceList, string? title = null) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title(title ?? "Please Select An Option:") + .HighlightStyle(Style.Parse("darkviolet")) + .AddChoices(choiceList)); + + return choice; + } + internal static List DisplayMultiPrompt(List choiceList, string? title = null, bool requireChoice = true) + { + var prompt = new MultiSelectionPrompt() + .Title(title ?? "Please Select An Option:") + .HighlightStyle(Style.Parse("darkviolet")) + .InstructionsText($"[{Grey}]Press[/] [{White}][/] to Toggle, and [{White}][/] to Confirm") + .AddChoices(choiceList); + + if (requireChoice) + prompt.Required(); + else + prompt.NotRequired(); + + return AnsiConsole.Prompt(prompt); + } + internal static string DisplayQuestion(string question) + { + var response = AnsiConsole.Ask($"[{White}]{question}[/]"); + return response; + } + internal static void DisplaySpinner(string waitMessage, int waitTimeInMs = 3000) + { + AnsiConsole.Status() + .Spinner(Spinner.Known.Star) + .Start($"[{White}]{waitMessage}[/]", ctx => + { + Thread.Sleep(waitTimeInMs); + }); + } + internal static async Task DisplayAsyncSpinner(string waitMessage, Task task) + { + await AnsiConsole.Status() + .Spinner(Spinner.Known.Star) + .StartAsync($"[{White}]{waitMessage}[/]", async ctx => + { + await task; + }); + } +} diff --git a/codingTracker.0lcm/codingTracker.0lcm/appSettings.json b/codingTracker.0lcm/codingTracker.0lcm/appSettings.json new file mode 100644 index 00000000..bfadf528 --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/appSettings.json @@ -0,0 +1,19 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=codingTracker.db" + }, + "TimeFormats": { + "DateIso": "yyyy-MM-dd", + "DateFormats": [ + "yyyy-MM-dd", + "yyyy-MM-d", + "yyyy-M-dd", + "yyyy-M-d" + ], + "HourFormats": [ + "H:mm", + "HH:mm" + ], + "FullDateWithTimeFormat": "yyyy-MM-dd HH:mm:ss" + } +} \ No newline at end of file diff --git a/codingTracker.0lcm/codingTracker.0lcm/codingTracker.0lcm.csproj b/codingTracker.0lcm/codingTracker.0lcm/codingTracker.0lcm.csproj new file mode 100644 index 00000000..1b2184ac --- /dev/null +++ b/codingTracker.0lcm/codingTracker.0lcm/codingTracker.0lcm.csproj @@ -0,0 +1,30 @@ + + + + Exe + net10.0 + codingTracker._0lcm + enable + enable + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/codingTracker.0lcm/codingTracker.0lcm/codingTracker.db b/codingTracker.0lcm/codingTracker.0lcm/codingTracker.db new file mode 100644 index 0000000000000000000000000000000000000000..e5768a80979dfb5ca5f9188fef1ac6ac3e3ec390 GIT binary patch literal 16384 zcmeI(zfRjg90%|_n*c%zuDWDGx`81UP@QeUkPdA?ClnDJ2#(ZDk&CaDCE|$KsXAAP zN8n|61YV_M2gJnB5UA|5Vyi0j{mFOd`|~^bKJpSpJ~{5hnHFEhlTnxnkG)}>vyVbB z#wt`6s5}Lyn{xkY?r&{{y}Pul)-5wv*V*bf>zWz_1Rwwb2tWV=5P$##AOHaf{HFp( z=3;GQgAcOs^O;W1&*%r^FxBT%ot&1t3wu4)3X}+1yB#G;YwJlk(wmfN|JdbAHQVMF zA0GBR9Y=BU^+cy>JWkSL&nWCzoD=oxiYP@K{T^uhf!bF+;dcY!_d6ZY_uI#PwK<=a zX5l0Y;*qB0Kz*X+vy?xq)k!p4`SRLmItep6RQ^ma?R(T~AGUe}ai9iuIz_7=blX0i z{!saWJ^#q|dtt41hEB0>(yO+dziw43hIPfP8>$cxfB*y_ z009U<00Izz00bZa0SNp<0ne;3&s%e)yR#+TE!hyxc5~ZpHr&#X*OUz@n{+v@XUruz z`HYm$NjpNik3&x5v0b548P+dm-BE>r00bZa0SG_<0uX=z1Rwwb2teS~2vkhYO*5|z qBme)OTR$0H2nav`0uX=z1Rwwb2tWV=5P$##UV*?{ZrZ%`9l$*;bE^RW literal 0 HcmV?d00001 diff --git a/codingTracker.0lcm/goals.txt b/codingTracker.0lcm/goals.txt new file mode 100644 index 00000000..e69de29b From 25b813aeff653a4d1ff1e0cddf7bcc468a973f72 Mon Sep 17 00:00:00 2001 From: 0lcm Date: Thu, 5 Feb 2026 15:06:55 -0500 Subject: [PATCH 4/6] Removed db file Removed database file containing old data from tests --- .../codingTracker.0lcm/codingTracker.db | Bin 16384 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 codingTracker.0lcm/codingTracker.0lcm/codingTracker.db diff --git a/codingTracker.0lcm/codingTracker.0lcm/codingTracker.db b/codingTracker.0lcm/codingTracker.0lcm/codingTracker.db deleted file mode 100644 index e5768a80979dfb5ca5f9188fef1ac6ac3e3ec390..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16384 zcmeI(zfRjg90%|_n*c%zuDWDGx`81UP@QeUkPdA?ClnDJ2#(ZDk&CaDCE|$KsXAAP zN8n|61YV_M2gJnB5UA|5Vyi0j{mFOd`|~^bKJpSpJ~{5hnHFEhlTnxnkG)}>vyVbB z#wt`6s5}Lyn{xkY?r&{{y}Pul)-5wv*V*bf>zWz_1Rwwb2tWV=5P$##AOHaf{HFp( z=3;GQgAcOs^O;W1&*%r^FxBT%ot&1t3wu4)3X}+1yB#G;YwJlk(wmfN|JdbAHQVMF zA0GBR9Y=BU^+cy>JWkSL&nWCzoD=oxiYP@K{T^uhf!bF+;dcY!_d6ZY_uI#PwK<=a zX5l0Y;*qB0Kz*X+vy?xq)k!p4`SRLmItep6RQ^ma?R(T~AGUe}ai9iuIz_7=blX0i z{!saWJ^#q|dtt41hEB0>(yO+dziw43hIPfP8>$cxfB*y_ z009U<00Izz00bZa0SNp<0ne;3&s%e)yR#+TE!hyxc5~ZpHr&#X*OUz@n{+v@XUruz z`HYm$NjpNik3&x5v0b548P+dm-BE>r00bZa0SG_<0uX=z1Rwwb2teS~2vkhYO*5|z qBme)OTR$0H2nav`0uX=z1Rwwb2tWV=5P$##UV*?{ZrZ%`9l$*;bE^RW From 6e27778af512a4bbc680a6924da9d5712f92ed92 Mon Sep 17 00:00:00 2001 From: Andrew Date: Thu, 5 Feb 2026 16:52:59 -0500 Subject: [PATCH 5/6] Create README.md with project overview and features Added detailed project description, features, and resources. --- README.md | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..6516a52d --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# About Project +This is project was created as a learning project from [CSharpAcademy's Coding Tracker project](https://thecsharpacademy.com/project/13/coding-tracker) +and follows the requirments set there. +The main functionality of the project lies in creating and tracking the user's coding sessions, allowing them to create, view, update, and delete their sessions. +This is a basic console project and uses the Spectre.Console library for Ui related issues, and Sqlite and Dapper ORM for the database. + +# Features - Overview +* Most menus and prompts use basic up, down, and enter keys, requiring little user typing, leading to less possibilities for input error, and an easier life for the user. +* Menus use Spectre.Console for a nice look, and easy accesibility. + ![Image displaying the main menu, with several options for input.](https://i.imgur.com/rF9FrGu.png) +* Viewing sessions shows a brief overview of each session, allowing the user to select any session to see more details. + ![Image displaying a menu along with various sessions](https://i.imgur.com/asyxmGM.png) +* The user can easily filter for specific dates, or filter sessions to show in ascending/descending order. + ![Image displaying a menu along with different options, including a 'filter sessions' and 'clear filters' option, along with various sessions arranged by ascending duration.](https://i.imgur.com/3a4DF6j.png) +* Each session can be indivdually selected to see more details, as well as take certain actions like deleting or updating a session. + ![Image displaying a menu that displays a session's details, and provides a 'Delete', 'Update' and 'Return' option.](https://i.imgur.com/xao218V.png) +* Confirmation screens are displayed for destructive actions, and common user mistakes. + ![Image displaying a deletion confirmation screen](https://i.imgur.com/pxlxj5O.png) +* Features a timer to that records a coding session in real time. + ![Image displaying a menu labeled "Press 'Q' To Stop Timer"](https://i.imgur.com/i9et7w9.png) + +# Features - Detailed +## Menus and Ui +For menus with constant values such as main menus, or certain action menus, they are displayed using the DisplayHelper's DisplayMenu method, which uses the AnsiConsole.Prompt +method. This method uses a generic type, and allows Enums to be passed in. The menu is created from the Enum's items, and uses an enum extension to make items more presentable, +for example; the main menu's enum has an item called "NewSession", but using the enum extension, this is displayed as "New Session", leading to much more readable menus. +For menus that use display flexible values instead of constant ones, DisplayHelper's DisplayPrompt method is called. This method is functionally identical to DisplayMenu, +except it takes in a `List` parameter instead of using Enums. +The DisplayHelper.cs file takes care of almost all the calls to Spectre.Console's Ui methods, and uses constant strings of hex codes for different colors, allowing the asthetics +of the application to stay focused on consistency, since almost all needs for displaying Ui is passed through these helper methods. Furthermore, the constant strings use an +`internal` access modifier, meaning any Ui needs outside of DisplayHelper.cs can be displayed with the same consistent colors. Using internal constant strings also means that +if a color needs to be changed within the application, you can simply swap out out the hex code string value for that color, or you can even remove colors or add new colors all +by switching one section of code. + +## Viewing and Filtering Sessions +The main method for viewing sessions is the ViewSessions method within the ConsoleUi.cs class. This method displays a functionally empty loading screen to the user just to +present the user with the idea of loading, although this only shows once per trip to the method, since it gets annoying quickly if you have to wait everytime, especially when +filtering sessions. After the method has finished loading, it displays a menu containing a "Return to Main Menu", "Filter Sessions", and a conditional "Clear Filters" option. +It also displays each current session recorded in the database, along with it's ID, Date of creation, and duration. From there the user can either select the "Filter Sessions" +option to go to the filtering menu, or they can select any individual session to see more details and actions. Going to the filter menu allows the user to select a date to filter +by, either 'Today' for today's date, 'Other' to enter a specific date, or 'Default' to not filter by date. Afterwards theres also an 'Ascending' and 'Descending' option, as well +as another 'Default' option for not filtering by ascending/descending. Loading an invidual session allows the user to see more details of a session, such as the full: date, +start time, end time, and duration. There are also options to delete the specific session, update the session, or return to the previous menu. + +## Timer and Real Time Logging +Using the timer creates an asynchronous task, and then passes the task to DisplayHelper's DisplayAsyncSpinner method, which displays a spinner until the timer is cancelled or +finished. A title is passed to the spinner to let the user know to press 'Q' in order to stop the timer, and the application waits unitl the Q key is pressed before cancelling +the task and ending the spinner. Afterwards, it displays a spinner while saving the session to the database, and returns to the main menu. + +# Resources Used +[.NET (10.0)](https://learn.microsoft.com/en-us/dotnet/) +[Spectre.Console (0.54.0)](https://spectreconsole.net) - Ui +[Dapper (2.1.66)](https://www.learndapper.com) - ORM +[Microsoft.Data.Sqlite (10.0.2)](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/?tabs=net-cli) - Database +[Microsoft.Extensions.Configuration (10.0.2)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration?view=net-10.0-pp) - Configuration +[Microsoft.Extensions.Configuration.EnviromentVariables (10.0.2)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.environmentvariablesextensions?view=net-10.0-pp) +[Microsoft.Extensions.Configuration.Json (10.0.2)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.configuration.json?view=net-8.0-pp) +[Microsoft.Extensions.Logging (10.0.2)](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging/overview?tabs=command-line) - Logging +[Microsoft.Extensions.Logging.Abstractions (10.0.2)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.abstractions?view=net-10.0-pp) +[Microsoft.Extensions.Logging.Console (10.0.2)](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.console?view=net-8.0-pp) + +# Personal Thoughts +When I first started this project I was a little confused on how I should start, but i was able to get an idea after a while. In fact, in comparison to past projects, this one +felt a lot easier, Maybe because I in the [last project I had](https://www.thecsharpacademy.com/project/12/habit-logger), I had to figure out how to use Sqlite's database from +scratch, whereas for this one I already had a little idea of how to use it. +By far, my favorite part of this project was learning and using Spectre.Console. It made it so easy to customize my application, and it amazed me how things that I would have no +idea how to do otherwise, could be done with just a single line of code. This also gave me the ability to turn the basic console of black and white text into real menus with +options, selections, colors, and tons of other things to make my life easier and make the app nicer. +Overall, this project was very fun and I enjoyed it, although I'm a little nervous for the next project since I heard it gets a lot more difficult, but I'm still excited to start. +One thing I'd like to learn more of would be proper error handling, since right now my application doesn't do much other than logging it to the console and keeping it from +crashing. I think I could also learn more about Seperation of Concerns and how to keep everything clean. I tried to seperate the code into a Ui layer for actually Displaying things +to the user, a Service layer for taking care of the real logic, and a Database layer for dealing with the Sqlite commands and operations, but I think I could still improve on +these topics. +I definitly enjoyed this project, and the fact that the gap of knowledge wasn't as big as it felt in previous projects let me relax a little more and not worry about having to +learn a ton of new things at once. I'm excited to start the next project, and hopefully it wont take long before I have to write a second ReadMe. From ef2c06c8a46d3549266ced427dcfd90d0f683623 Mon Sep 17 00:00:00 2001 From: 0lcm Date: Thu, 5 Feb 2026 17:00:37 -0500 Subject: [PATCH 6/6] removed unused parameter --- codingTracker.0lcm/codingTracker.0lcm/Program.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/codingTracker.0lcm/codingTracker.0lcm/Program.cs b/codingTracker.0lcm/codingTracker.0lcm/Program.cs index 602cf66a..ecd8fdd7 100644 --- a/codingTracker.0lcm/codingTracker.0lcm/Program.cs +++ b/codingTracker.0lcm/codingTracker.0lcm/Program.cs @@ -11,7 +11,7 @@ internal class Program .AddJsonFile("appSettings.json", optional: false, reloadOnChange: true) .Build(); - static async Task Main(string[] args) + static async Task Main() { SqliteController.CreateDatabase(); await ConsoleUi.MainMenu();