Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
483 changes: 483 additions & 0 deletions CodingTracker.paddymcn/.gitignore

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>CodingTracker.Console</RootNamespace>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\CodingTracker.Core\CodingTracker.Core.csproj" />
<ProjectReference Include="..\CodingTracker.Data\CodingTracker.Data.csproj" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Configuration" Version="11.0.0-rc.1.26425.128" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="11.0.0-rc.1.26425.128" />
<PackageReference Include="Spectre.Console" Version="0.57.3-alpha.0.29" />
</ItemGroup>

</Project>
107 changes: 107 additions & 0 deletions CodingTracker.paddymcn/CodingTracker.ConsoleUI/Menu.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
using Spectre.Console;
using CodingTracker.Core;
using CodingTracker.Core.DataModel;
using CodingTracker.Data;
using Microsoft.Extensions.Configuration;

namespace CodingTracker.Console;
public class Menu
{
private readonly UserInput _userInput = new();
static IConfiguration config = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json")
.Build();
static string connectionString = config.GetConnectionString("CodingTracker")
?? throw new InvalidOperationException("Connection string 'CodingTracker' not found in appsettings.json.");
ICodingSessionRepository repo = new SqliteCodingSessionRepository( connectionString);
public void Run()
{
var isAppRunning = true;
while (isAppRunning)
{
var option = AnsiConsole.Prompt(
new SelectionPrompt<MenuOptions>()
.Title("What would you like to do?")
.UseConverter(ToDisplayText)
.AddChoices(Enum.GetValues<MenuOptions>()));

switch (option)
{
case MenuOptions.AddCodingSession:
repo.EnsureCreated();
var sessionNew = new CodingSession
{
StartTime = _userInput.GetDateTime("Enter start time"),
EndTime = _userInput.GetDateTime("Enter end time"),
Description = _userInput.GetDescription("Enter description for the coding session")
};
repo.Add(sessionNew);
break;
case MenuOptions.DeleteCodingSession:
repo.EnsureCreated();
repo.EnsureCreated();
int idDelete = _userInput.GetPositiveInt("Enter session Id");
var sessionDelete = repo.GetById(idDelete);
if (sessionDelete is null)
{
AnsiConsole.MarkupLine($"[red]No session found with Id {idDelete}.[/]");
break;
}
repo.Delete(idDelete);
break;
case MenuOptions.UpdateCodingSession:
{
int idUpdate = _userInput.GetPositiveInt("Enter id of the coding session");
var sessionUpdate = repo.GetById(idUpdate);
if (sessionUpdate is null)
{
AnsiConsole.MarkupLine($"[red]No session found with Id {idUpdate}.[/]");
break;
}
sessionUpdate.StartTime = _userInput.GetDateTime("Enter start time");
sessionUpdate.EndTime = _userInput.GetDateTime("Enter end time");
sessionUpdate.Description = _userInput.GetDescription("Enter description for the coding session");
repo.Update(sessionUpdate);
AnsiConsole.MarkupLine("[green]Session updated.[/]");
break;
}
break;
case MenuOptions.ViewCodingSession:
repo.EnsureCreated();
int idSession = _userInput.GetPositiveInt("Enter session Id");
var sessionById = repo.GetById(idSession);
if (sessionById is null)
{
AnsiConsole.MarkupLine($"[red]No session found with Id {idSession}.[/]");
break;
}
System.Console.WriteLine($"{sessionById.Id} - {sessionById.Duration} - {sessionById.Description}");
break;
case MenuOptions.ViewAllCodingSessions:
repo.EnsureCreated();
List<CodingSession> allSessions= repo.GetAll();
foreach (var codingSessions in allSessions)
{
System.Console.WriteLine($"{codingSessions.Id} - {codingSessions.Duration} - {codingSessions.Description}");
}
break;
case MenuOptions.Quit:
isAppRunning = false;
break;
default:
throw new ArgumentOutOfRangeException(nameof(option), option, "Unhandled menu option.");
}
}
}
private static string ToDisplayText(MenuOptions option) => option switch
{
MenuOptions.AddCodingSession => "Add Coding Session",
MenuOptions.DeleteCodingSession => "Delete Coding Session",
MenuOptions.UpdateCodingSession => "Update Coding Session",
MenuOptions.ViewCodingSession => "View Coding Session",
MenuOptions.ViewAllCodingSessions => "View All Coding Session",
MenuOptions.Quit => "Quit",
_ => option.ToString()
};
}
11 changes: 11 additions & 0 deletions CodingTracker.paddymcn/CodingTracker.ConsoleUI/MenuOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace CodingTracker.Console;

public enum MenuOptions
{
AddCodingSession,
DeleteCodingSession,
UpdateCodingSession,
ViewCodingSession,
ViewAllCodingSessions,
Quit
}
8 changes: 8 additions & 0 deletions CodingTracker.paddymcn/CodingTracker.ConsoleUI/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using CodingTracker.Console;
new Menu().Run();






37 changes: 37 additions & 0 deletions CodingTracker.paddymcn/CodingTracker.ConsoleUI/UserInput.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Spectre.Console;
namespace CodingTracker.Console;
public class UserInput
{
public DateTime GetDateTime(string message)
{
var input = AnsiConsole.Prompt(
new TextPrompt<string>($"{Markup.Escape(message)} [grey](format: {Validator.DateTimeFormat})[/]")
.Validate(text => Validator.TryParseDateTime(text, out _)
? ValidationResult.Success()
: ValidationResult.Error($"[red]Invalid date and time. Use {Validator.DateTimeFormat}.[/]")));

Validator.TryParseDateTime(input, out var dateTime);
return dateTime;
}
public int GetPositiveInt(string message)
{
var input = AnsiConsole.Prompt(
new TextPrompt<string>(Markup.Escape(message))
.Validate(text => Validator.IsValidPositiveInt(text, out _)
? ValidationResult.Success()
: ValidationResult.Error("[red]Please enter a whole number greater than 0.[/]")));

Validator.IsValidPositiveInt(input, out var number);
return number;
}
public string GetDescription(string message, int maxLength = 250)
{
var input = AnsiConsole.Prompt(
new TextPrompt<string>(Markup.Escape(message))
.Validate(text => Validator.IsValidDescription(text, maxLength)
? ValidationResult.Success()
: ValidationResult.Error($"[red]Name can't be empty or longer than {maxLength} characters.[/]")));

return input.Trim();
}
}
50 changes: 50 additions & 0 deletions CodingTracker.paddymcn/CodingTracker.ConsoleUI/Validator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
using System.Globalization;
namespace CodingTracker.Console;

public class Validator
{
public const string DateFormat = "dd-MM-yy";
public const string TimeFormat = "HH:mm";
public const string DateTimeFormat = "dd/MM/yyyy HH:mm";

internal static bool IsValidTime(string? input, string format, out TimeOnly time)
{
return TimeOnly.TryParseExact(
input?.Trim(),
format,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out time);
}

public static bool TryParseDateTime(string? input, out DateTime result)
{
return DateTime.TryParseExact(
input,
DateTimeFormat,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out result);
}
public static bool IsValidDate(string? input, string format, out DateTime date)
{
return DateTime.TryParseExact(
input?.Trim(),
format,
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out date);
}

public static bool IsValidPositiveInt(string? input, out int number)
{
return int.TryParse(input, NumberStyles.Integer, CultureInfo.InvariantCulture, out number)
&& number > 0;
}

public static bool IsValidDescription(string? input, int maxLength = 50)
{
return !string.IsNullOrWhiteSpace(input)
&& input.Trim().Length <= maxLength;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"ConnectionStrings": {
"CodingTracker": "Data Source=codingtracker.db"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
namespace CodingTracker.Core.DataModel;

public class CodingSession
{
public int Id { get; set; }
public string Description { get; set; } = "";
public DateTime StartTime { get; set; }
public DateTime EndTime { get; set; }
public TimeSpan Duration => EndTime - StartTime;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using CodingTracker.Core.DataModel;

namespace CodingTracker.Core;

public interface ICodingSessionRepository
{
bool Delete(int id);
void EnsureCreated();
List<CodingSession> GetAll();
CodingSession? GetById(int id);
CodingSession Add(CodingSession codingSession);
bool Update(CodingSession codingSession);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="..\CodingTracker.Core\CodingTracker.Core.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.89" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="11.0.0-rc.1.26425.128" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using CodingTracker.Core;
using Dapper;
using Microsoft.Data.Sqlite;
using CodingTracker.Core.DataModel;

namespace CodingTracker.Data;

public class SqliteCodingSessionRepository(string stringIn) : ICodingSessionRepository
{
private readonly string connectionString = stringIn;
public void EnsureCreated()
{
using var connection = new SqliteConnection(connectionString);
const string sql = @"
CREATE TABLE IF NOT EXISTS CodingSessions (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
Description TEXT NOT NULL,
StartTime TEXT NOT NULL,
EndTime TEXT NOT NULL
);";
connection.Execute(sql);
}
public CodingSession Add(CodingSession codingSession)
{
using var connection = new SqliteConnection(connectionString);

const string sql = @"
INSERT INTO CodingSessions (Description, StartTime, EndTime)
VALUES (@Description, @StartTime, @EndTime);
SELECT last_insert_rowid();";

codingSession.Id = connection.ExecuteScalar<int>(sql, codingSession);
return codingSession;
}
public bool Delete(int id)
{
using var connection = new SqliteConnection(connectionString);
const string sql = @"
DELETE FROM CodingSessions WHERE Id = @Id;";
return connection.Execute(sql, new { Id = id }) > 0;
}
public List<CodingSession> GetAll()
{
using var connection = new SqliteConnection(connectionString);

const string sql = @"
SELECT Id, Description, StartTime, EndTime
FROM CodingSessions
ORDER BY StartTime;";

List<CodingSession> sessions = connection.Query<CodingSession>(sql).ToList();
return sessions;
}
public CodingSession? GetById(int id)
{
using var connection = new SqliteConnection(connectionString);
const string sql = @"
SELECT Id, Description, StartTime, EndTime
FROM CodingSessions WHERE Id = @Id;";
CodingSession? session = connection.QuerySingleOrDefault<CodingSession>(sql, new { Id = id });
return session;
}
public bool Update(CodingSession codingSession)
{
using var connection = new SqliteConnection(connectionString);
const string sql = @"
UPDATE CodingSessions
SET Description = @Description, StartTime = @StartTime, EndTime=@EndTime WHERE Id = @Id";
return connection.Execute(sql, codingSession) > 0;
}
}
Loading
Loading