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
4 changes: 4 additions & 0 deletions Coding Tracker/Coding Tracker.slnx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<Solution>
<Project Path="Coding Tracker/Coding Tracker.csproj" />
<Project Path="Coding_Tracker.Tests/Coding_Tracker.Tests.csproj" Id="7563a785-b1e0-493d-8dd8-d40e2dc5fc5c" />
</Solution>
27 changes: 27 additions & 0 deletions Coding Tracker/Coding Tracker/Coding Tracker.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">

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

<ItemGroup>
<PackageReference Include="Dapper" Version="2.1.66" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.FileExtensions" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.1" />
<PackageReference Include="Spectre" Version="0.0.1" />
<PackageReference Include="Spectre.Console" Version="0.54.0" />
</ItemGroup>

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

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using Spectre.Console;
using Coding_Tracker.Models;
using Coding_Tracker.Repositories;

namespace Coding_Tracker.Controllers
{
internal class AutoTimerSessionController : ISessionController
{
private readonly ConsoleUI _ui;
private readonly CodingSessionRepository _repo;
public AutoTimerSessionController(CodingSessionRepository repo, ConsoleUI ui)
{
_repo = repo;
_ui = ui;
}

public void StartSession()
{
var start = DateTime.Now;
DisplayStopWatch();
var end = DateTime.Now;

_repo.AddSession(CodingSession.Create(start, end));
}

public void DisplayStopWatch()
{
float timer = 0f;
var stopWatchPanel = new Panel("");
stopWatchPanel.Border = BoxBorder.Rounded;
stopWatchPanel.Header = new PanelHeader("[green]Coding Session In Progress[/]");
while (true)
{
stopWatchPanel = new Panel(
new Markup($"[yellow]{TimeSpan.FromSeconds(timer):hh\\:mm\\:ss}[/]")
);
AnsiConsole.Write(stopWatchPanel);
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[grey]Press [blue]S[/] to stop the session...[/]");
if (Console.KeyAvailable)
{
var key = Console.ReadKey(true);
if (key.Key == ConsoleKey.S)
{
break;
}
}
timer += 1f;
Thread.Sleep(1000);
AnsiConsole.Clear();
}

}
}
}
142 changes: 142 additions & 0 deletions Coding Tracker/Coding Tracker/Controllers/ConsoleUI.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
using Spectre.Console;
using Spectre.Console.Rendering;
using System.Data;

namespace Coding_Tracker.Controllers
{
internal class ConsoleUI
{

private const string DateFormat = "dd-MM-yyyy HH:mm";
public void DisplayMessage(string message, bool isError, string colour = "yellow")
{
var panel = new Panel(message)
{
Border = BoxBorder.Rounded,
Padding = new Padding(1, 1),
Header = new PanelHeader(isError ? "[red]Error[/]" : "[green]Info[/]")
};
AnsiConsole.Write(panel);
AnsiConsole.WriteLine();
AnsiConsole.MarkupLine("[grey]Press any key to continue...[/]");
Console.ReadKey(true);
}

public bool ConfirmDeletion(string itemName)
{
return AnsiConsole.Confirm($"[red]Are you sure you want to delete[/] [yellow]{itemName}[/]? This action cannot be undone.");
}

public DateTime PromptForDateTime(string prompt)
{
return PromptUntilValid(
prompt,
DateFormat.ToLower(),
input => DateTimeValidator.ValidateDateResponse(input, DateFormat),
$"Invalid date format. Please use the format: {DateFormat.ToLower()}");
}
public DateTime PromptForEndDateTime(string prompt, DateTime startTime)
{
return PromptUntilValid(
prompt,
DateFormat.ToLower(),
input => DateTimeValidator.ValidateEndDateResponse(startTime, input, DateFormat),
$"Invalid end time. It must be after the start time and use the format: {DateFormat.ToLower()}");
}
private T PromptUntilValid<T>(string prompt, string formatHint, Func<string, T> parse, string errorMessage)
{
while (true)
{
try
{
var input = AnsiConsole.Ask<string>($"{prompt} format: {formatHint}:");
return parse(input);
}
catch (FormatException)
{
DisplayMessage(errorMessage, true);
}
}
}

public void DisplayActivityHeatMap(List<CodingSession> sessions)
{
var today = DateTime.Today;
bool isToday = sessions.Any(s => s.StartTime.Date == today);
var startDate = today.AddDays(-29);

// Sum minutes per day for records with multiple entries
var minutesByDay = sessions
.Where(s => s.StartTime.Date >= startDate && s.StartTime.Date <= today)
.GroupBy(s => s.StartTime.Date)
.ToDictionary(g => g.Key, g => g.Sum(s => s.Duration.TotalMinutes));

// Snap grid start back to Monday
DateTime gridStart = startDate;
while (gridStart.DayOfWeek != DayOfWeek.Monday)
gridStart = gridStart.AddDays(-1);

// Grid end is 'today'. Calculate number of weeks needed
int totalGridDays = (today - gridStart).Days + 1;
int weekColumns = (int)Math.Ceiling(totalGridDays / 7.0);

var table = new Table()
.Border(TableBorder.None)
.HideHeaders();

table.AddColumn(new TableColumn(""));
for (int c = 0; c < weekColumns; c++)
table.AddColumn(new TableColumn(""));

string[] labels = { "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" };

for (int row = 0; row < 7; row++)
{
var cells = new List<IRenderable>();

// Adding the day labels
cells.Add(new Markup($"[grey]{labels[row]}[/]"));

for (int col = 0; col < weekColumns; col++)
{
var day = gridStart.AddDays(col * 7 + row);

if (day < startDate || day > today)
{
cells.Add(new Text(" ", new Style(background: Color.Grey19)));
continue;
}

var minutes = minutesByDay.TryGetValue(day, out var m) ? m : 0;

var bg =
minutes > 0 && minutes <= 1 ? Color.LightGreen :
minutes > 1 && minutes <= 30 ? Color.Green :
minutes > 30 ? Color.DarkGreen :
Color.Grey;

if (isToday && day == today) bg = Color.Blue;

cells.Add(new Text(" ", new Style(background: bg)));

}

table.AddRow(cells.ToArray());
}

AnsiConsole.Write(new Spectre.Console.Rule("[green]Last 30 Days Coding Heatmap[/]").Centered());
AnsiConsole.Write(table);

//legend text
AnsiConsole.MarkupLine("\nLegend:\n" +
"[on grey] [/] No record\n" +
"[on lightgreen] [/] Up to 30 mins\n" +
"[on green] [/] Between 30 mins and up to 120 mins\n" +
"[on darkgreen] [/] Greater than 120 mins\n" +
"[on blue] [/] Today\n");

AnsiConsole.MarkupLine("\nPress any key to return to the main menu...");
Console.ReadKey(true);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
using Spectre.Console;

namespace Coding_Tracker.Controllers
{
internal interface ISessionController
{
void StartSession();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
using Coding_Tracker.Repositories;
using Spectre.Console;

namespace Coding_Tracker.Controllers
{
internal class ManualTimerSessionController : ISessionController
{
private readonly CodingSessionRepository _repo;
private readonly ConsoleUI _ui;
public ManualTimerSessionController(CodingSessionRepository repo, ConsoleUI ui)
{
_repo = repo;
_ui = ui;
}

public void StartSession()
{
var start = _ui.PromptForDateTime("Enter the [green]start[/] date and time");
var end = _ui.PromptForEndDateTime("Enter the [red]end[/] date and time", start);

_repo.AddSession(CodingSession.Create(start, end));
}


}
}
23 changes: 23 additions & 0 deletions Coding Tracker/Coding Tracker/DatabaseInitialiser.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using Dapper;
using Microsoft.Data.Sqlite;

namespace Coding_Tracker.Controllers
{
internal class DatabaseInitialiser
{
public DatabaseInitialiser(string databasePath)
{
using var connection = new SqliteConnection($"Data Source={databasePath}");
connection.Open();

const string createTableQuery = @"
CREATE TABLE IF NOT EXISTS CodingSessions (
Id INTEGER PRIMARY KEY AUTOINCREMENT,
StartTime TEXT NOT NULL,
EndTime TEXT NOT NULL
);";

connection.Execute(createTableQuery);
}
}
}
24 changes: 24 additions & 0 deletions Coding Tracker/Coding Tracker/DateTimeValidator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace Coding_Tracker
{
public class DateTimeValidator
{
public static DateTime ValidateDateResponse(string input, string format = "dd-MM-yyyy HH:mm")
{
if (DateTime.TryParseExact(input, format, null, System.Globalization.DateTimeStyles.None, out var result))
{
return result;
}
throw new FormatException($"Input '{input}' is not in the expected format '{format}'.");
}

public static DateTime ValidateEndDateResponse(DateTime startDate, string input, string format = "dd-MM-yyyy HH:mm")
{
var endDate = ValidateDateResponse(input, format);
if (endDate <= startDate)
{
throw new FormatException($"End date '{endDate}' must be after start date '{startDate}'.");
}
return endDate;
}
}
}
25 changes: 25 additions & 0 deletions Coding Tracker/Coding Tracker/Models/CodingSession.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
using Coding_Tracker;

internal class CodingSession
{
public int Id { get; set; }
public DateTime StartTime { get; private set; }
public DateTime EndTime { get; private set; }

public TimeSpan Duration => EndTime - StartTime;

public static CodingSession Create(DateTime startTime, DateTime endTime)
{
if (endTime <= startTime)
{
throw new ArgumentException("End time must be after start time.");
}


return new CodingSession
{
StartTime = startTime,
EndTime = endTime
};
}
}
28 changes: 28 additions & 0 deletions Coding Tracker/Coding Tracker/Models/Enums.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace Coding_Tracker.Models
{
internal class Enums
{

internal enum MenuAction
{
Start_Coding_Session = 1,
View_Sessions,
View_Previous_30_Day_Activity_Heatmap,
Exit
}

internal enum SessionMenuAction
{
Start_Timer = 1,
Enter_Time_Manually,
Back_To_Main_Menu
}

internal enum ViewSessionsAction
{
View_All_Sessions = 1,
View_Sessions_From_Set_Date_Selection,
Back_To_Main_Menu
}
}
}
22 changes: 22 additions & 0 deletions Coding Tracker/Coding Tracker/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
using Spectre.Console;
using Microsoft.Extensions.Configuration;
using Coding_Tracker.Repositories;
using Coding_Tracker;
using Coding_Tracker.Controllers;

var configuration = new ConfigurationBuilder()
.SetBasePath(AppContext.BaseDirectory)
.AddJsonFile("appsettings.json", optional: false, reloadOnChange: false)
.Build();

var dbPath = configuration["Database:ConnectionString"];

var databaseInitialiser = new DatabaseInitialiser(dbPath);
var repo = new CodingSessionRepository(dbPath);

var userInterface = new UserInterface(repo);

userInterface.MainMenu();



Loading