diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735.slnx b/CodingTracker.Hacker-735/CodingTracker.Hacker-735.slnx new file mode 100644 index 00000000..eaf14085 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735.slnx @@ -0,0 +1,3 @@ + + + diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/AppConfig.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/AppConfig.cs new file mode 100644 index 00000000..1038c2b5 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/AppConfig.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.Configuration; + +namespace CodingTracker.Hacker_735 +{ + internal static class AppConfig + { + private static readonly IConfiguration _configuration; + + static AppConfig() + { + _configuration = new ConfigurationBuilder() + .SetBasePath(AppContext.BaseDirectory) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .Build(); + } + + internal static string ConnectionString => + _configuration.GetConnectionString("Default"); + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingSession.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingSession.cs new file mode 100644 index 00000000..7d9a3266 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingSession.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CodingTracker.Hacker_735 +{ + internal class CodingSession + { + public int Id { get; set; } + public string Date { get; set; } + public string StartTime { get; set; } + public string EndTime { get; set; } + public string Time { get; set; } + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingTracker.Hacker-735.csproj b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingTracker.Hacker-735.csproj new file mode 100644 index 00000000..fb18918d --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/CodingTracker.Hacker-735.csproj @@ -0,0 +1,27 @@ + + + + Exe + net10.0 + CodingTracker.Hacker_735 + enable + enable + + + + + + + + + + + + + + PreserveNewest + + + + + diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Enums.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Enums.cs new file mode 100644 index 00000000..23af586a --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Enums.cs @@ -0,0 +1,18 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace CodingTracker.Hacker_735 +{ + internal static class Enums + { + internal enum MenuAction + { + Insert, + Display, + Delete, + Update, + Close, + } + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Program.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Program.cs new file mode 100644 index 00000000..2d86324c --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Program.cs @@ -0,0 +1,15 @@ +using Microsoft.Data.Sqlite; +using Microsoft.VisualBasic.FileIO; +using System; +using System.Data; +using System.Globalization; +using Spectre.Console; +using CodingTracker.Hacker_735; +using Dapper; +using static CodingTracker.Hacker_735.Enums; + +TableController.CreateTable(); + +UserInterface.ChoicePrompt(); + + diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/README.md b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/README.md new file mode 100644 index 00000000..3ef77685 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/README.md @@ -0,0 +1,55 @@ +# Coding Tracker +Console application for logging daily coding sessions, built in C# with Spectre.Console for visuals, Dapper for interfacing with SQLite, and SQLite for a database. + +## Overview +This app lets you log your coding sessions by inputting a start and end time and then automatically calculating the length from the two inputted times. You can view, update, and delete the sessions using the main menu. + +## Features +* Add a coding session (each session includes date, start time, end time, duration is calculated based of of these) +* View all logged sessions in a table +* Update existing session from table +* Delete session from table +* It automatically creates the SQL table +* Date and time input validations +* A nice looking spectre interface +* you can type "td" to automatically input the date (As this is very annoying to do manually) + +## Thought Process +Now i will preface this with the fact that i had at some point lost most access to my computer for about a month or two so somethings are not fresh in my memory. (I think in future i will make the readme along side with the project to avoid this issue in general, it will also allow me to think things through better) + +#### Getting the Times +I first began with making the Time input system, just so i could think things through since it would be (in my opinion) the most important part of the program since it will be processing the data going on the table. At first it was very simple using two massive while loops with TryParseExact to just manually get the start and end time. + +Now +* GetTimeInput() calls a helper AskForTime() two times one for start and one for end +* User is asked Enter start time (HH:mm, 24-hour format): +* It reads the string +* we use Validation.IsValidTime() to see if it is a valid format and it will loop until it is +* Then we get two values parsedStartTime and parsedEndTime + +* Then we get totalMinutes from the start and end time which we have turned into pure minutes for calculation +* I also added handling for overnight sessions "if (totalMinutes < 0) totalMinutes += 24 * 60;" +* This assumes the session crossed midnight rather than being an error — so `23:00 → 01:00` is treated as a 2-hour session. +* We then formate the result as "string finalTime = $"{totalMinutes / 60:D2}:{totalMinutes % 60:D2}";" +* GetTimeInput() returns a tuple of: (FinalTime, StartTime, EndTime) + + +#### Error Handling + +All error handling is within the Validation class + +IsValidTime was just a method to see if it parsed as a time and if so it would be output + +IsValidDate was mostly the same. + +I wanted to store most info as strings as it made it more simple to produce and format (In my opinion) + +Safe Execute is the main thing here though as i designed it to work with most things in the program. whatever code block gets ran within action will try and if it goes wrong either catch (SqliteException ex) (Incase SQLite messes up) or catch (Exception ex) (incase anything else messes up) will catch it. Each method has it's own errorContext +## A few things i struggled on +Mostly dapper and SQL in general since i didn't really know how to use it until now basically (I think i understand it alot better now though) + +access levels really had me confused on some parts + +Finding the best way to display information in a effective manner (Need some advice on this if possible, but i will look into it anyway) + +Deciding which aspect was important to handle first (Separation of Concerns) \ No newline at end of file diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TableController.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TableController.cs new file mode 100644 index 00000000..0940a05a --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TableController.cs @@ -0,0 +1,198 @@ +using Dapper; +using Microsoft.Data.Sqlite; +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Text; +using System.Timers; + +namespace CodingTracker.Hacker_735 +{ + internal class TableController + { + private static readonly string connectionString = AppConfig.ConnectionString; + static internal void CreateTable() + { + AnsiConsole.Progress() + .Start(ctx => + { + var task = ctx.AddTask("[yellow]Setting up database[/]"); + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + task.Increment(40); + + string createTableSql = + @"CREATE TABLE IF NOT EXISTS coding_sessions ( + id INTEGER PRIMARY KEY, + Date TEXT, + StartTime TEXT, + EndTime TEXT, + Time TEXT + )"; + + connection.Execute(createTableSql); + task.Increment(60); + }); + + AnsiConsole.MarkupLine("[green]Table ready.[/]"); + } + static internal void Insert() + { + string date = TimeInputController.GetDateInput(); + var timeResult = TimeInputController.GetTimeInput(); + + Validation.SafeExecute("Inserting session", () => + { + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + connection.Execute( + "INSERT INTO coding_sessions(Date, StartTime, EndTime, Time) VALUES(@Date, @StartTime, @EndTime, @Time)", + new + { + Date = date, + StartTime = timeResult.StartTime.ToString("HH:mm"), + EndTime = timeResult.EndTime.ToString("HH:mm"), + Time = timeResult.FinalTime + }); + }); + } + + static internal void Display() + { + + Console.Clear(); + + Validation.SafeExecute("Displaying sessions", () => + { + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + var sessions = connection.Query( + "SELECT id AS Id, Date, StartTime, EndTime, Time FROM coding_sessions ORDER BY id" + ).ToList(); + + if (!sessions.Any()) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + return; + } + + var table = new Table(); + + + table.AddColumn("Id"); + table.AddColumn("Date"); + table.AddColumn("Start"); + table.AddColumn("End"); + table.AddColumn("Duration"); + + foreach (var session in sessions) + { + table.AddRow( + session.Id.ToString(), + session.Date, + session.StartTime, + session.EndTime, + session.Time + ); + } + + AnsiConsole.Write(table); + }); + } + + static internal void Delete() + { + string action = "delete"; + + int id = SelectSessionId(action); + + Validation.SafeExecute("deleting session", () => + { + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + int rowsAffected = connection.Execute( + "DELETE FROM coding_sessions WHERE id = @Id", + new { Id = id } + ); + + if (rowsAffected > 0) + AnsiConsole.MarkupLine($"[green]Deleted session {id}.[/]"); + else + AnsiConsole.MarkupLine("[yellow]Nothing was deleted.[/]"); + }); + } + + static private List GetAllSessionIds() + { + return Validation.SafeExecute("fetching session ids", () => + { + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + return connection.Query("SELECT id FROM coding_sessions ORDER BY id").ToList(); + }, new List()); + } + + static internal void Update() + { + string action = "update"; + + int id = SelectSessionId(action); + + string date = TimeInputController.GetDateInput(); + + var timeResult = TimeInputController.GetTimeInput(); + string time = timeResult.FinalTime; + DateTime startingTime = timeResult.StartTime; + DateTime endingTime = timeResult.EndTime; + + Validation.SafeExecute("Displaying sessions", () => + { + + using var connection = new SqliteConnection(connectionString); + connection.Open(); + + int rowsAffected = connection.Execute( + @"UPDATE coding_sessions + SET Date = @Date, StartTime = @StartTime, EndTime = @EndTime, Time = @Time + WHERE id = @Id", + new + { + Date = date, + StartTime = startingTime.ToString("HH:mm"), + EndTime = endingTime.ToString("HH:mm"), + Time = time, + Id = id + }); + + if (rowsAffected > 0) + AnsiConsole.MarkupLine($"[green]Updated session {id}.[/]"); + else + AnsiConsole.MarkupLine("[yellow]Nothing was updated.[/]"); + }); + } + static private int SelectSessionId(string action) + { + Display(); + var sessions = GetAllSessionIds(); + if (sessions.Count == 0) + { + AnsiConsole.MarkupLine("[yellow]No coding sessions found.[/]"); + return -1; + } + + return AnsiConsole.Prompt( + new TextPrompt($"Enter the Id of the session to {action}:") + .Validate(input => sessions.Contains(input) + ? ValidationResult.Success() + : ValidationResult.Error("[red]No session with that Id.[/]"))); + } + + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TimeInputController.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TimeInputController.cs new file mode 100644 index 00000000..0808b7f2 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/TimeInputController.cs @@ -0,0 +1,67 @@ +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Timers; + +namespace CodingTracker.Hacker_735 +{ + internal class TimeInputController + { + static internal (string FinalTime, DateTime StartTime, DateTime EndTime) GetTimeInput() + { + DateTime parsedStartTime = AskForTime("start"); + DateTime parsedEndTime = AskForTime("end"); + + int totalMinutes = ((parsedEndTime.Hour * 60) + parsedEndTime.Minute) - ((parsedStartTime.Hour * 60) + parsedStartTime.Minute); + + if (totalMinutes < 0) + { + totalMinutes += 24 * 60; + } + + + string finalTime = ($"{totalMinutes / 60:D2}:{totalMinutes % 60:D2}"); + + + AnsiConsole.MarkupLine($"Time passed was {finalTime}"); + + return (finalTime, parsedStartTime, parsedEndTime); + } + + + internal static string GetDateInput() + { + AnsiConsole.MarkupLine("Enter Date (dd-mm-yy format):"); + string dateInput = Console.ReadLine(); + + while (true) + { + if (dateInput == "td") + return DateTime.Today.ToString("dd-MM-yy"); + + if (Validation.IsValidDate(dateInput, out string dateOutput)) + return dateOutput; + + AnsiConsole.MarkupLine("Invalid format. Please use dd-mm-yy"); + dateInput = Console.ReadLine(); + } + } + + private static DateTime AskForTime(string timeType) + { + while (true) + { + AnsiConsole.Markup($"Enter {timeType} time (HH:mm, 24-hour format): "); + string? input = Console.ReadLine(); + + if (Validation.IsValidTime(input, out DateTime output)) + return output; + + AnsiConsole.MarkupLine("Invalid format. Please use 24-hour HH:mm (e.g. 14:30)."); + } + } + + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/UserInterface.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/UserInterface.cs new file mode 100644 index 00000000..ce030cf6 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/UserInterface.cs @@ -0,0 +1,51 @@ +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Text; +using static CodingTracker.Hacker_735.Enums; + +namespace CodingTracker.Hacker_735 +{ + internal class UserInterface + { + static internal void ChoicePrompt() + { + bool loop = true; + while (loop == true) + { + Console.Clear(); + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("What would you like to do?") + .AddChoices(Enum.GetValues())); + switch (choice) + { + case MenuAction.Display: + TableController.Display(); + Console.ReadLine(); + break; + + case MenuAction.Insert: + TableController.Insert(); + Console.ReadLine(); + break; + + case MenuAction.Delete: + TableController.Delete(); + Console.ReadLine(); + break; + + case MenuAction.Update: + TableController.Update(); + Console.ReadLine(); + break; + case MenuAction.Close: + loop = false; + AnsiConsole.Markup("[red]Bye bye![/]"); + Console.ReadLine(); + break; + } + } + } + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Validation.cs b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Validation.cs new file mode 100644 index 00000000..e9edfcbc --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/Validation.cs @@ -0,0 +1,68 @@ +using Microsoft.Data.Sqlite; +using Spectre.Console; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Text; +using System.Timers; + +namespace CodingTracker.Hacker_735 +{ + internal class Validation + { + internal static bool IsValidTime(string input, out DateTime output) + { + if (DateTime.TryParseExact(input, "HH:mm", CultureInfo.InvariantCulture, + DateTimeStyles.None, out output)) + { + return true; + } + return false; + } + + internal static bool IsValidDate(string input, out string output) + { + if (DateTime.TryParseExact(input, "dd-MM-yy", new CultureInfo("en-US"), DateTimeStyles.None, out _)) + { + output = input; + return true; + } + output = null; + return false; + } + + static internal void SafeExecute(string errorContext, System.Action action) + { + try + { + action(); + } + catch (SqliteException ex) + { + AnsiConsole.MarkupLine($"[red]Database error while {errorContext}: {ex.Message}[/]"); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Unexpected error while {errorContext}: {ex.Message}[/]"); + } + } + + static internal T SafeExecute(string errorContext, Func action, T fallback) + { + try + { + return action(); + } + catch (SqliteException ex) + { + AnsiConsole.MarkupLine($"[red]Database error while {errorContext}: {ex.Message}[/]"); + return fallback; + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Unexpected error while {errorContext}: {ex.Message}[/]"); + return fallback; + } + } + } +} diff --git a/CodingTracker.Hacker-735/CodingTracker.Hacker-735/appsettings.json b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/appsettings.json new file mode 100644 index 00000000..f4b92985 --- /dev/null +++ b/CodingTracker.Hacker-735/CodingTracker.Hacker-735/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "Default": "Data Source=Coding_Tracker.db" + } +} \ No newline at end of file