diff --git a/.gitignore b/.gitignore index 49976e11..47972c0c 100644 --- a/.gitignore +++ b/.gitignore @@ -478,3 +478,5 @@ $RECYCLE.BIN/ *.lnk /MathGame2 /CodingTracker.TomDonegan/TextFile1.txt + +*.db \ No newline at end of file diff --git a/CodeReviews.Console.CodingTracker.sln b/CodeReviews.Console.CodingTracker.sln new file mode 100644 index 00000000..d53da901 --- /dev/null +++ b/CodeReviews.Console.CodingTracker.sln @@ -0,0 +1,24 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.2.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CodingTracker.Kaylubr", "CodingTracker.Kaylubr\CodingTracker.Kaylubr.csproj", "{6AABB9C0-5449-298C-1EB0-59B25D4A6CAC}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6AABB9C0-5449-298C-1EB0-59B25D4A6CAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6AABB9C0-5449-298C-1EB0-59B25D4A6CAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6AABB9C0-5449-298C-1EB0-59B25D4A6CAC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6AABB9C0-5449-298C-1EB0-59B25D4A6CAC}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {67FA8CC2-65C3-4683-B605-8F131556E3AC} + EndGlobalSection +EndGlobal diff --git a/CodingTracker.Kaylubr/CodingTracker.Kaylubr.csproj b/CodingTracker.Kaylubr/CodingTracker.Kaylubr.csproj new file mode 100644 index 00000000..de3bc337 --- /dev/null +++ b/CodingTracker.Kaylubr/CodingTracker.Kaylubr.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + + + + + PreserveNewest + + + + + + + + + diff --git a/CodingTracker.Kaylubr/Controllers/CodingSessionController.cs b/CodingTracker.Kaylubr/Controllers/CodingSessionController.cs new file mode 100644 index 00000000..66845893 --- /dev/null +++ b/CodingTracker.Kaylubr/Controllers/CodingSessionController.cs @@ -0,0 +1,76 @@ +using CodingTracker.Utils; + +namespace CodingTracker.Controllers; + +internal static class CodingTrackerController +{ + internal static void InsertSession() + { + var (startTime, endTime) = UserInput.GetStartAndEndTime(); + string duration = UserInput.GetDuration(startTime, endTime); + Database.Insert(startTime, endTime, duration); + Helper.Pause("Successful Operation!", success: true); + } + + internal static void LogAllRecords() + { + Helper.RenderCodingSessionInTable(Database.GetAll()); + Helper.Pause(); + } + + internal static void UpdateRecord() + { + bool exists = Helper.RenderCodingSessionInTable(Database.GetAll()); + + if (exists) + { + int id = UserInput.GetID("EDITED"); + + if (!Database.FindOneSession(id)) + { + Helper.Pause("Record not found!", success: false); + return; + } + + var (startTime, endTime) = UserInput.GetStartAndEndTime(); + string duration = UserInput.GetDuration(startTime, endTime); + + Database.Update(id, startTime, endTime, duration); + + Helper.Pause("Successful Operation!", success: true); + } + else + { + Helper.Pause(); + } + } + + internal static void DeleteRecord() + { + bool exists = Helper.RenderCodingSessionInTable(Database.GetAll()); + + if (exists) + { + int id = UserInput.GetID("DELETED"); + + if (!Database.FindOneSession(id)) + { + Helper.Pause("Record not found!", success: false); + return; + } + + if (!Helper.Confirmation("Are you sure?")) + { + return; + } + + Database.DeleteOne(id); + + Helper.Pause("Successful Operation!", success: true); + } + else + { + Helper.Pause(); + } + } +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Enums/MenuChoices.cs b/CodingTracker.Kaylubr/Enums/MenuChoices.cs new file mode 100644 index 00000000..a59f91fa --- /dev/null +++ b/CodingTracker.Kaylubr/Enums/MenuChoices.cs @@ -0,0 +1,10 @@ +namespace CodingTracker.Enums; + +internal enum MenuChoices +{ + View, + Insert, + Update, + Delete, + Exit +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Models/CodingSession.cs b/CodingTracker.Kaylubr/Models/CodingSession.cs new file mode 100644 index 00000000..95f00f66 --- /dev/null +++ b/CodingTracker.Kaylubr/Models/CodingSession.cs @@ -0,0 +1,9 @@ +namespace CodingTracker.Models; + +public class CodingSession +{ + public int Id { get; set; } + public DateTime StartTime { get; set; } + public DateTime EndTime { get; set; } + public string Duration { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Program.cs b/CodingTracker.Kaylubr/Program.cs new file mode 100644 index 00000000..9c42948a --- /dev/null +++ b/CodingTracker.Kaylubr/Program.cs @@ -0,0 +1,5 @@ +using CodingTracker.Views; +using CodingTracker.Utils; + +Database.CreateDatabase(); +UserInterface.Run(); \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Utils/Config.cs b/CodingTracker.Kaylubr/Utils/Config.cs new file mode 100644 index 00000000..46774a10 --- /dev/null +++ b/CodingTracker.Kaylubr/Utils/Config.cs @@ -0,0 +1,16 @@ +using Microsoft.Extensions.Configuration; + +namespace CodingTracker.Utils; + +internal static class Config +{ + internal static string? InitializeConfig() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + return configuration.GetConnectionString("DefaultConnection"); + } +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Utils/Database.cs b/CodingTracker.Kaylubr/Utils/Database.cs new file mode 100644 index 00000000..d17501ea --- /dev/null +++ b/CodingTracker.Kaylubr/Utils/Database.cs @@ -0,0 +1,70 @@ +using CodingTracker.Models; +using Dapper; +using Microsoft.Data.Sqlite; + +namespace CodingTracker.Utils; + +internal static class Database +{ + readonly static string? connectionString = Config.InitializeConfig(); + readonly static SqliteConnection connection = new(connectionString); + + internal static void CreateDatabase() + { + var sql = @"CREATE TABLE IF NOT EXISTS coding_session ( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + StartTime TEXT, + EndTime TEXT, + Duration TEXT + )"; + + connection.Execute(sql); + } + + internal static void Insert(string st, string et, string duration) + { + var sql = @"INSERT INTO coding_session (StartTime, EndTime, Duration) + VALUES (@start, @end, @duration) + "; + + connection.Execute(sql, new { start = st, end = et, duration }); + } + + internal static List GetAll() + { + var sql = "SELECT * FROM coding_session"; + List records = connection.Query(sql).ToList(); + + return records; + } + + internal static void Update(int id, string st, string et, string duration) + { + var sql = "UPDATE coding_session SET StartTime = @st, EndTime = @et, Duration = @duration WHERE Id = @id"; + var obj = new { id, st, et, duration }; + + connection.Execute(sql, obj); + } + + internal static void DeleteOne(int id) + { + var sql = "DELETE FROM coding_session WHERE id = @id"; + connection.Execute(sql, new { id }); + } + + internal static bool FindOneSession(int id) + { + var sql = "SELECT * FROM coding_session WHERE Id = @id"; + var obj = new { id }; + + try + { + connection.QuerySingle(sql, obj); + return true; + } + catch + { + return false; + } + } +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Utils/Helper.cs b/CodingTracker.Kaylubr/Utils/Helper.cs new file mode 100644 index 00000000..bda8863b --- /dev/null +++ b/CodingTracker.Kaylubr/Utils/Helper.cs @@ -0,0 +1,89 @@ +using CodingTracker.Models; +using Spectre.Console; + +namespace CodingTracker.Utils; + +internal static class Helper +{ + internal static bool ValidateTime(DateTime start, DateTime end) + { + if (start > end) + { + AnsiConsole.MarkupLine("[red]Starting time shouldn't be greater than the end time.[/]"); + return false; + } + + return true; + } + + internal static bool RenderCodingSessionInTable(List codingSessions) + { + AnsiConsole.Clear(); + + if (codingSessions.Count >= 1) + { + var table = new Table() + .Title("[green bold]Session Records[/]") + .Border(TableBorder.Heavy); + + table.AddColumn("ID"); + table.AddColumn("Starting time"); + table.AddColumn("End time"); + table.AddColumn("Duration"); + + foreach (var session in codingSessions) + { + table.AddRow(session.Id.ToString(), session.StartTime.ToString(), session.EndTime.ToString(), session.Duration); + } + + AnsiConsole.Write(table); + + return true; + } + else + { + AnsiConsole.MarkupLine("[red]No coding sessions to display. Add a session to see it here.[/]"); + return false; + } + + } + + internal static bool Confirmation(string message) + { + AnsiConsole.WriteLine(); + + string? choice; + do + { + choice = AnsiConsole.Ask($"{message} [bold green]Y[/] or [bold red]N[/]:").Trim().ToUpper(); + } while (choice != "Y" && choice != "N"); + + if (choice == "N") + return false; + + return true; + } + + internal static void Pause() + { + AnsiConsole.Write("\nPress any key to continue.."); + Console.ReadKey(); + } + + internal static void Pause(string message, bool success) + { + if (success) + { + AnsiConsole.MarkupLine($"\n[green]{message}[/]"); + } + else + { + AnsiConsole.MarkupLine($"\n[red]{message}[/]"); + } + + AnsiConsole.Write("\nPress any key to continue.."); + Console.ReadKey(); + + AnsiConsole.Clear(); + } +} diff --git a/CodingTracker.Kaylubr/Utils/UserInput.cs b/CodingTracker.Kaylubr/Utils/UserInput.cs new file mode 100644 index 00000000..20f0eca1 --- /dev/null +++ b/CodingTracker.Kaylubr/Utils/UserInput.cs @@ -0,0 +1,56 @@ +using Spectre.Console; +using System.Globalization; + +namespace CodingTracker.Utils; + +internal static class UserInput +{ + internal static int GetID(string mode) + { + AnsiConsole.WriteLine(); + return AnsiConsole.Ask($"\nEnter the [green]ID[/] of the row to be [bold]{mode}[/]: "); + } + + internal static (string StartTime, string EndTime) GetStartAndEndTime() + { + DateTime start; + DateTime end; + + do + { + AnsiConsole.WriteLine(); + start = GetTime("START"); + end = GetTime("END"); + } while (!Helper.ValidateTime(start, end)); + + string startTime = start.ToString(); + string endTime = end.ToString(); + + return (startTime, endTime); + } + + static DateTime GetTime(string message) + { + while (true) + { + var time = AnsiConsole.Ask($"Enter [bold green]{message}[/] session time in the format (dd-MM-yy HH-mm): "); + + if (DateTime.TryParseExact(time, "dd-MM-yy HH:mm", new CultureInfo("en-US"), DateTimeStyles.None, out _)) + { + return DateTime.ParseExact(time, "dd-MM-yy HH:mm", new CultureInfo("en-US")); + } + + AnsiConsole.MarkupLine("[red]Invalid Format or Invalid Date & Time[/]"); + } + + } + + internal static string GetDuration(string start, string end) + { + DateTime startTime = DateTime.Parse(start); + DateTime endTime = DateTime.Parse(end); + + return (endTime - startTime).ToString(@"hh\:mm\:ss"); + } + +} \ No newline at end of file diff --git a/CodingTracker.Kaylubr/Views/UserInterface.cs b/CodingTracker.Kaylubr/Views/UserInterface.cs new file mode 100644 index 00000000..dfe024b8 --- /dev/null +++ b/CodingTracker.Kaylubr/Views/UserInterface.cs @@ -0,0 +1,67 @@ +using Spectre.Console; +using CodingTracker.Controllers; +using CodingTracker.Enums; +using CodingTracker.Utils; + +namespace CodingTracker.Views; + +internal static class UserInterface +{ + internal static void Run() + { + while (true) + { + RenderTitle(); + + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("\n[Green]Pick operation:[/]") + .HighlightStyle(new Style(Color.Green)) + .AddChoices(Enum.GetValues()) + ); + + switch (choice) + { + case MenuChoices.View: + CodingTrackerController.LogAllRecords(); + break; + + case MenuChoices.Insert: + + if (!Helper.Confirmation("Proceed with this operation? (No will return to the main menu)")) + continue; + + CodingTrackerController.InsertSession(); + break; + + case MenuChoices.Update: + + if (!Helper.Confirmation("Proceed with this operation? (No will return to the main menu)")) + continue; + + CodingTrackerController.UpdateRecord(); + break; + + case MenuChoices.Delete: + + if (!Helper.Confirmation("Proceed with this operation? (No will return to the main menu)")) + continue; + + CodingTrackerController.DeleteRecord(); + break; + case MenuChoices.Exit: + AnsiConsole.WriteLine("\nExiting.."); + Environment.Exit(0); + break; + } + } + } + + static void RenderTitle() + { + Console.Clear(); + var panel = new Panel("Welcome to Coding Session Tracker!"); + panel.Border(BoxBorder.Heavy); + AnsiConsole.Write(panel); + } +} diff --git a/CodingTracker.Kaylubr/appsettings.json b/CodingTracker.Kaylubr/appsettings.json new file mode 100644 index 00000000..be023eb9 --- /dev/null +++ b/CodingTracker.Kaylubr/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Data Source=HabitTracker.db" + } +} \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 00000000..ea30f115 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# Coding Tracker Console App + +A simple console-based coding session tracker built with **.NET**, **Dapper**, **Microsoft.Extensions.Configuration**, **Microsoft.Data.Sqlite**, and **Spectre.Console**. The app allows you to **view, add, edit, and delete** coding sessions in a lightweight SQLite database with a clean console interface. +I like this project because I feel like I had better grasp on OOP and patterns like DRY and KISS + +--- + +## Features + +- Add new coding sessions with start and end times +- View all recorded sessions in a formatted console table +- Edit existing sessions directly from the console +- Delete sessions when needed +- Uses **Spectre.Console** for a colorful and user-friendly terminal UI +- Configuration handled via **Microsoft.Extensions.Configuration** for flexible database and app settings + +--- + +## Challenges + +One thing that really frustrated me was how **Dapper maps values** from the database to C# objects. + +At first, I named my columns `start_time` and `end_time`. When I tried to render them, Dapper didn’t automatically convert them into `DateTime` even though the types were declared in `models/CodingSession.cs`. The issue was that Dapper maps columns to properties **by name**, and I was sticking to C#’s PascalCase naming convention instead of the database’s snake_case convention. + +The fix was simple: rename the columns to `StartTime` and `EndTime` to match the property names in my model. Once that was done, Dapper handled the mapping perfectly. + +--- + +## Lessons Learned + +- **Dapper is incredibly convenient**. You don’t need to manually open or close the database connection, and retrieving rows into a list is simple using Dapper’s built-in methods +- Keeping column names and model property names consistent is crucial for smooth mapping +- **Spectre.Console** makes even simple console apps feel interactive and polished + +--- + +## Resources Used + +- [Dapper Documentation](https://dapper-tutorial.net/) +- [Microsoft.Data.Sqlite](https://learn.microsoft.com/en-us/dotnet/standard/data/sqlite/) +- [Microsoft.Extensions.Configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration) +- [Spectre.Console](https://spectreconsole.net/)