diff --git a/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.gitignore b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.gitignore
new file mode 100644
index 00000000..9d13e61a
--- /dev/null
+++ b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.gitignore
@@ -0,0 +1,13 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Rider ignored files
+/contentModel.xml
+/modules.xml
+/projectSettingsUpdater.xml
+/.idea.exerciseTracker.doc415.iml
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
+# Editor-based HTTP Client requests
+/httpRequests/
diff --git a/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.name b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.name
new file mode 100644
index 00000000..29f3b51d
--- /dev/null
+++ b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/.name
@@ -0,0 +1 @@
+exerciseTracker.doc415
\ No newline at end of file
diff --git a/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/indexLayout.xml b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/indexLayout.xml
new file mode 100644
index 00000000..7b08163c
--- /dev/null
+++ b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/indexLayout.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/vcs.xml b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/vcs.xml
new file mode 100644
index 00000000..6c0b8635
--- /dev/null
+++ b/ExerciseTracker.Doc415-r/.idea/.idea.exerciseTracker.doc415/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ExerciseTracker.Doc415-r/Repository/ExerciseRepositoryDapper.cs b/ExerciseTracker.Doc415-r/Repository/ExerciseRepositoryDapper.cs
index f8ef8403..9b998cd8 100644
--- a/ExerciseTracker.Doc415-r/Repository/ExerciseRepositoryDapper.cs
+++ b/ExerciseTracker.Doc415-r/Repository/ExerciseRepositoryDapper.cs
@@ -77,7 +77,7 @@ private async Task TInsert(Exercise exercise)
Type = exercise.Type
});
Console.Error.WriteLine(affectedRows);
- }
+ }
public void Update(Exercise exercise)
{
diff --git a/ExerciseTracker.TwilightSaw/Controller/ExerciseController.cs b/ExerciseTracker.TwilightSaw/Controller/ExerciseController.cs
new file mode 100644
index 00000000..51a5b0b4
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Controller/ExerciseController.cs
@@ -0,0 +1,118 @@
+using ExerciseTracker.TwilightSaw.Helper;
+using ExerciseTracker.TwilightSaw.Model;
+using ExerciseTracker.TwilightSaw.Service;
+using Spectre.Console;
+
+namespace ExerciseTracker.TwilightSaw.Controller;
+
+public class ExerciseController(ExerciseService service)
+{
+ public void AddExercise(string type)
+ {
+ Console.Clear();
+ AnsiConsole.Write(new Rule("[cyan]Format - hh:mm[/]"));
+ var addStartInput = UserInput.CreateRegex("^(?:([0-1][0-9]|2[0-3]):([0-5][0-9])|0)$",
+ "Insert the start of your Exercise", "Wrong format, try again.");
+ if (addStartInput == "0") return;
+ AnsiConsole.Write(new Rule("[cyan]Format - hh:mm[/]"));
+ var addEndInput = UserInput.CreateRegex("^(?:([0-1][0-9]|2[0-3]):([0-5][0-9])|0|(N|n))$",
+ "Insert the end of your Exercise, N for time at this moment", "Wrong format.");
+ addEndInput = addEndInput is "N" or "n"
+ ? DateTime.Now.AddSeconds(-DateTime.Now.Second).ToShortTimeString()
+ : addEndInput;
+ if (addEndInput == "0") return;
+ var addComments = UserInput.Create("Add the comments, leave this field empty");
+ if (addComments == "0") return;
+
+ DateTime.TryParse(addStartInput, out var startTime);
+ DateTime.TryParse(addEndInput, out var endTime);
+ var exercise = new Exercise(type, startTime, endTime, addComments == "" ? null : addComments);
+ service.AddExercise(exercise);
+ Validation.EndMessage("Exercise added successfully.");
+ }
+
+ public Exercise GetExercise(string type)
+ {
+ Console.Clear();
+ var input = UserInput.CreateExerciseChoosingList(service.GetExerciseByType(type), "Return");
+ return input;
+ }
+
+ public void DeleteExercise(Exercise exercise)
+ {
+ Console.Clear();
+ service.DeleteExercise(exercise.Id);
+ Validation.EndMessage("Exercise deleted successfully.");
+ }
+
+ public void ChangeExercise(Exercise exercise)
+ {
+ Console.Clear();
+ var type = exercise.Type;
+ var stringDate = exercise.StartTime.ToShortDateString();
+ var stringStartTime = exercise.StartTime.TimeOfDay.ToString();
+ var stringEndTime = exercise.EndTime.TimeOfDay.ToString();
+ var comment = exercise.Comments;
+
+ var changeInput = UserInput.CreateUpdateChoosingList([
+ $"Type: {type}",
+ $"Date: {stringDate}", $"Start Time: {stringStartTime}",
+ $"End Time: {stringEndTime}", $"Comment: {comment}"
+ ],
+ exercise, "Return");
+ switch (changeInput)
+ {
+ case "1":
+ var typeInput = UserInput.CreateChoosingList(["Cardio", "Weights"], "Return");
+ if (typeInput == "Return") return;
+ exercise.Type = typeInput;
+ break;
+ case "2":
+ AnsiConsole.Write(new Rule("[olive]Format: dd.mm.yyyy[/]"));
+ var newDateInput = UserInput.CreateRegex(@"^(?:([0-2][0-9]|3[01])\.(0[1-9]|1[0-2])\.(\d{4})|(T|t)|0)$",
+ "Insert your new date, T for today's date", "Wrong format, try again.");
+ switch (newDateInput)
+ {
+ case "0":
+ return;
+ case "T" or "t":
+ newDateInput = DateTime.Now.ToShortDateString();
+ break;
+ }
+
+ DateTime.TryParse(newDateInput, out var newDate);
+ exercise.StartTime = newDate + exercise.StartTime.TimeOfDay;
+ exercise.EndTime = newDate + exercise.EndTime.TimeOfDay;
+ break;
+ case "3":
+ AnsiConsole.Write(new Rule("[olive]Format: hh:mm[/]"));
+ var newStartTimeInput = UserInput.CreateRegex("^(?:([0-1][0-9]|2[0-3]):([0-5][0-9])|0)$",
+ "Insert the start of your Exercise", "Wrong format, try again.");
+ if (newStartTimeInput == "0") return;
+
+ DateTime.TryParse(newStartTimeInput, out var newStartTime);
+ exercise.StartTime = exercise.StartTime.Date + newStartTime.TimeOfDay;
+ break;
+ case "4":
+ AnsiConsole.Write(new Rule("[olive]Format: hh:mm[/]"));
+ var newEndTimeInput = UserInput.CreateRegex("^(?:([0-1][0-9]|2[0-3]):([0-5][0-9])|0|(N|n))$",
+ "Insert the end of your Exercise, N for time at this moment", "Wrong format.");
+ if (newEndTimeInput == "0") return;
+ newEndTimeInput = newEndTimeInput is "N" or "n"
+ ? DateTime.Now.AddSeconds(-DateTime.Now.Second).ToShortTimeString()
+ : newEndTimeInput;
+
+ DateTime.TryParse(newEndTimeInput, out var newEndTime);
+ exercise.EndTime = exercise.EndTime.Date + newEndTime.TimeOfDay;
+ break;
+ case "5":
+ var newComment = UserInput.Create("Change your comment, leave this field empty");
+ if (newComment == "0") return;
+ exercise.Comments = newComment;
+ break;
+ }
+
+ service.UpdateExercise(exercise);
+ Validation.EndMessage("Changed successfully.");
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Data/AppDbContext.cs b/ExerciseTracker.TwilightSaw/Data/AppDbContext.cs
new file mode 100644
index 00000000..2f82aac0
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Data/AppDbContext.cs
@@ -0,0 +1,22 @@
+using ExerciseTracker.TwilightSaw.Model;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+
+namespace ExerciseTracker.TwilightSaw.Data;
+
+public class AppDbContext : DbContext
+{
+ public DbSet Exercises { get; set; }
+
+ private readonly IConfiguration _configuration;
+ public AppDbContext(DbContextOptions options)
+ : base(options)
+ {
+
+ }
+
+ protected override void OnModelCreating(ModelBuilder modelBuilder)
+ {
+ modelBuilder.Entity();
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.csproj b/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.csproj
new file mode 100644
index 00000000..2d809805
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.csproj
@@ -0,0 +1,37 @@
+
+
+
+ Exe
+ net8.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
+
+
+
+
+
+
+
+ PreserveNewest
+
+
+
+
diff --git a/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.sln b/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.sln
new file mode 100644
index 00000000..9f832439
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/ExerciseTracker.TwilightSaw.sln
@@ -0,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 17
+VisualStudioVersion = 17.11.35312.102
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ExerciseTracker.TwilightSaw", "ExerciseTracker.TwilightSaw.csproj", "{792D3445-2C8A-43F8-A586-03388D66D249}"
+EndProject
+Global
+ GlobalSection(SolutionConfigurationPlatforms) = preSolution
+ Debug|Any CPU = Debug|Any CPU
+ Release|Any CPU = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(ProjectConfigurationPlatforms) = postSolution
+ {792D3445-2C8A-43F8-A586-03388D66D249}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {792D3445-2C8A-43F8-A586-03388D66D249}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {792D3445-2C8A-43F8-A586-03388D66D249}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {792D3445-2C8A-43F8-A586-03388D66D249}.Release|Any CPU.Build.0 = Release|Any CPU
+ EndGlobalSection
+ GlobalSection(SolutionProperties) = preSolution
+ HideSolutionNode = FALSE
+ EndGlobalSection
+ GlobalSection(ExtensibilityGlobals) = postSolution
+ SolutionGuid = {C26453EB-0465-4FF9-A21B-737455131161}
+ EndGlobalSection
+EndGlobal
diff --git a/ExerciseTracker.TwilightSaw/Factory/HostFactory.cs b/ExerciseTracker.TwilightSaw/Factory/HostFactory.cs
new file mode 100644
index 00000000..80660ae0
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Factory/HostFactory.cs
@@ -0,0 +1,24 @@
+using ExerciseTracker.TwilightSaw.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+
+namespace ExerciseTracker.TwilightSaw.Factory;
+
+public class HostFactory
+{
+ public static IHost CreateDbHost(string[] args)
+ {
+ var builder = Host.CreateDefaultBuilder(args);
+ var configuration = builder.ConfigureServices((context, services) =>
+ {
+ var configuration = context.Configuration;
+ services.AddDbContext(options => options.UseSqlServer(configuration.GetConnectionString("DefaultConnection"))
+ .LogTo(Console.WriteLine, LogLevel.None)
+ .UseLazyLoadingProxies());
+ });
+ return configuration.Build();
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Helper/UserInput.cs b/ExerciseTracker.TwilightSaw/Helper/UserInput.cs
new file mode 100644
index 00000000..f3fcb25e
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Helper/UserInput.cs
@@ -0,0 +1,77 @@
+using System.Text.RegularExpressions;
+using ExerciseTracker.TwilightSaw.Model;
+using Spectre.Console;
+
+namespace ExerciseTracker.TwilightSaw.Helper;
+
+public class UserInput
+{
+
+ public static string CreateRegex(string regexString, string messageStart, string messageError)
+ {
+ var regex = new Regex(regexString);
+ var input = AnsiConsole.Prompt(
+ new TextPrompt($"[green]{messageStart} or 0 to exit:[/]")
+ .Validate(value => regex.IsMatch(value)
+ ? ValidationResult.Success()
+ : ValidationResult.Error($"[red]{messageError}[/]")));
+ Console.Clear();
+ return input;
+ }
+
+ public static string Create(string messageStart)
+ {
+ var input = AnsiConsole.Prompt(
+ new TextPrompt($"[green]{messageStart} or 0 to exit: [/]")
+ .AllowEmpty());
+ Console.Clear();
+ return input;
+ }
+
+ public static string CreateChoosingList(List variants, string backVariant)
+ {
+ variants.Add(backVariant);
+ return AnsiConsole.Prompt(new SelectionPrompt()
+ .Title("[blue]Please, choose an option from the list below:[/]")
+ .PageSize(10)
+ .MoreChoicesText("[grey](Move up and down to reveal more categories[/]")
+ .AddChoices(variants));
+ }
+
+ public static Exercise CreateExerciseChoosingList(List variants, string? backVariant)
+ {
+ variants.Add(new Exercise("",DateTime.Now, DateTime.Now, backVariant));
+ return AnsiConsole.Prompt(new SelectionPrompt()
+ .Title("[blue]Please, choose an option from the list below:[/]")
+ .PageSize(10)
+ .MoreChoicesText("[grey](Move up and down to reveal more categories[/]")
+ .UseConverter(
+ exercise => exercise.Comments != backVariant ? $"Date: {exercise.StartTime.ToShortDateString()}\n " +
+ $" Start Time: {exercise.StartTime.TimeOfDay}, " +
+ $"End Time: {exercise.EndTime.TimeOfDay}, " +
+ $"Duration: {exercise.Duration}\n" +
+ $" Comments: {exercise.Comments}" :
+ "[red]Return[/]"
+ )
+ .AddChoices(variants));
+ }
+
+ public static string CreateUpdateChoosingList(List variants, Exercise exercise, string backVariant)
+ {
+ var var = AnsiConsole.Prompt(new SelectionPrompt()
+ .Title("[blue]Please, choose an option from the list below:[/]")
+ .PageSize(10)
+ .MoreChoicesText("[grey](Move up and down to reveal more categories[/]")
+ .AddChoices(variants));
+
+ var selectedIndex = variants.IndexOf(var);
+ return selectedIndex switch
+ {
+ 0 => "1",
+ 1 => "2",
+ 2 => "3",
+ 3 => "4",
+ _ => "5"
+ };
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Helper/Validation.cs b/ExerciseTracker.TwilightSaw/Helper/Validation.cs
new file mode 100644
index 00000000..8ac66016
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Helper/Validation.cs
@@ -0,0 +1,44 @@
+using Spectre.Console;
+
+namespace ExerciseTracker.TwilightSaw.Helper;
+
+public class Validation
+{
+ public static string Validate(Action action, bool getMessage)
+ {
+ try
+ {
+ action();
+ }
+ catch (Exception e)
+ {
+ return getMessage ? e.Message : "";
+ }
+ return "Executed successfully";
+ }
+
+ public static string Validate(T action, bool getMessage, out T back)
+ {
+ try
+ {
+ back = action;
+ }
+ catch (Exception e)
+ {
+ back = default;
+ return e.Message;
+ }
+ return getMessage ? "Executed successfully" : "";
+ }
+
+ public static void EndMessage(string? message)
+ {
+ if (message != null)
+ {
+ AnsiConsole.MarkupLine($"[olive]{message}[/]");
+ AnsiConsole.Markup($"[grey]Press any key to continue.[/]");
+ Console.ReadKey(intercept: true);
+ }
+ Console.Clear();
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.Designer.cs b/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.Designer.cs
new file mode 100644
index 00000000..237e3738
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.Designer.cs
@@ -0,0 +1,55 @@
+//
+using System;
+using ExerciseTracker.TwilightSaw.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ExerciseTracker.TwilightSaw.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20241201093223_OnCreate")]
+ partial class OnCreate
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Proxies:ChangeTracking", false)
+ .HasAnnotation("Proxies:CheckEquality", false)
+ .HasAnnotation("Proxies:LazyLoading", true)
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ExerciseTracker.TwilightSaw.Model.Exercise", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Comments")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.HasKey("Id");
+
+ b.ToTable("Exercises");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.cs b/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.cs
new file mode 100644
index 00000000..2b281a50
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Migrations/20241201093223_OnCreate.cs
@@ -0,0 +1,37 @@
+using System;
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ExerciseTracker.TwilightSaw.Migrations
+{
+ ///
+ public partial class OnCreate : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.CreateTable(
+ name: "Exercises",
+ columns: table => new
+ {
+ Id = table.Column(type: "int", nullable: false)
+ .Annotation("SqlServer:Identity", "1, 1"),
+ StartTime = table.Column(type: "datetime2", nullable: false),
+ EndTime = table.Column(type: "datetime2", nullable: false),
+ Comments = table.Column(type: "nvarchar(max)", nullable: true)
+ },
+ constraints: table =>
+ {
+ table.PrimaryKey("PK_Exercises", x => x.Id);
+ });
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropTable(
+ name: "Exercises");
+ }
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.Designer.cs b/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.Designer.cs
new file mode 100644
index 00000000..97c763e8
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.Designer.cs
@@ -0,0 +1,59 @@
+//
+using System;
+using ExerciseTracker.TwilightSaw.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Migrations;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ExerciseTracker.TwilightSaw.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ [Migration("20241201174224_OnUpdateType")]
+ partial class OnUpdateType
+ {
+ ///
+ protected override void BuildTargetModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Proxies:ChangeTracking", false)
+ .HasAnnotation("Proxies:CheckEquality", false)
+ .HasAnnotation("Proxies:LazyLoading", true)
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ExerciseTracker.TwilightSaw.Model.Exercise", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Comments")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Exercises");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.cs b/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.cs
new file mode 100644
index 00000000..659dbfcf
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Migrations/20241201174224_OnUpdateType.cs
@@ -0,0 +1,29 @@
+using Microsoft.EntityFrameworkCore.Migrations;
+
+#nullable disable
+
+namespace ExerciseTracker.TwilightSaw.Migrations
+{
+ ///
+ public partial class OnUpdateType : Migration
+ {
+ ///
+ protected override void Up(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.AddColumn(
+ name: "Type",
+ table: "Exercises",
+ type: "nvarchar(max)",
+ nullable: false,
+ defaultValue: "");
+ }
+
+ ///
+ protected override void Down(MigrationBuilder migrationBuilder)
+ {
+ migrationBuilder.DropColumn(
+ name: "Type",
+ table: "Exercises");
+ }
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Migrations/AppDbContextModelSnapshot.cs b/ExerciseTracker.TwilightSaw/Migrations/AppDbContextModelSnapshot.cs
new file mode 100644
index 00000000..5e15a555
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Migrations/AppDbContextModelSnapshot.cs
@@ -0,0 +1,56 @@
+//
+using System;
+using ExerciseTracker.TwilightSaw.Data;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Infrastructure;
+using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+
+#nullable disable
+
+namespace ExerciseTracker.TwilightSaw.Migrations
+{
+ [DbContext(typeof(AppDbContext))]
+ partial class AppDbContextModelSnapshot : ModelSnapshot
+ {
+ protected override void BuildModel(ModelBuilder modelBuilder)
+ {
+#pragma warning disable 612, 618
+ modelBuilder
+ .HasAnnotation("ProductVersion", "9.0.0")
+ .HasAnnotation("Proxies:ChangeTracking", false)
+ .HasAnnotation("Proxies:CheckEquality", false)
+ .HasAnnotation("Proxies:LazyLoading", true)
+ .HasAnnotation("Relational:MaxIdentifierLength", 128);
+
+ SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
+
+ modelBuilder.Entity("ExerciseTracker.TwilightSaw.Model.Exercise", b =>
+ {
+ b.Property("Id")
+ .ValueGeneratedOnAdd()
+ .HasColumnType("int");
+
+ SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id"));
+
+ b.Property("Comments")
+ .HasColumnType("nvarchar(max)");
+
+ b.Property("EndTime")
+ .HasColumnType("datetime2");
+
+ b.Property("StartTime")
+ .HasColumnType("datetime2");
+
+ b.Property("Type")
+ .IsRequired()
+ .HasColumnType("nvarchar(max)");
+
+ b.HasKey("Id");
+
+ b.ToTable("Exercises");
+ });
+#pragma warning restore 612, 618
+ }
+ }
+}
diff --git a/ExerciseTracker.TwilightSaw/Model/Exercise.cs b/ExerciseTracker.TwilightSaw/Model/Exercise.cs
new file mode 100644
index 00000000..c6b55af1
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Model/Exercise.cs
@@ -0,0 +1,19 @@
+namespace ExerciseTracker.TwilightSaw.Model;
+
+public class Exercise(string type, DateTime startTime, DateTime endTime, string? comments)
+{
+ public int Id { get; set; }
+
+ public string Type { get; set; } = type;
+ public DateTime StartTime { get; set; } = startTime;
+ public DateTime EndTime { get; set; } = endTime;
+ public TimeSpan Duration
+ {
+ get
+ {
+ var duration = EndTime.TimeOfDay - StartTime.TimeOfDay;
+ return duration.Ticks >= 0 ? duration : duration.Add(TimeSpan.FromHours(24));
+ }
+ }
+ public string? Comments { get; set; } = comments;
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Program.cs b/ExerciseTracker.TwilightSaw/Program.cs
new file mode 100644
index 00000000..858e69c1
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Program.cs
@@ -0,0 +1,26 @@
+using ExerciseTracker.TwilightSaw.Controller;
+using ExerciseTracker.TwilightSaw.Data;
+using ExerciseTracker.TwilightSaw.Factory;
+using ExerciseTracker.TwilightSaw.Model;
+using ExerciseTracker.TwilightSaw.Repository;
+using ExerciseTracker.TwilightSaw.Service;
+using ExerciseTracker.TwilightSaw.View;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+var app = HostFactory.CreateDbHost(args);
+
+using var scope = app.Services.CreateScope();
+var context = scope.ServiceProvider.GetRequiredService();
+context.Database.Migrate();
+
+var configuration = new ConfigurationBuilder()
+ .SetBasePath(Directory.GetCurrentDirectory())
+ .AddJsonFile("appsettings.json")
+ .Build();
+
+new Menu(new ExerciseController(new ExerciseService(new ExerciseRepository(context),
+ new ExerciseDapperRepository(configuration)))).AddMenu();
+
+
diff --git a/ExerciseTracker.TwilightSaw/README.md b/ExerciseTracker.TwilightSaw/README.md
new file mode 100644
index 00000000..5ba57575
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/README.md
@@ -0,0 +1,55 @@
+# ExerciseTracker
+
+## Given Requirements:
+- [x] This is an application where you can record exercise data.
+- [x] You should be able to Add, Delete, Update and Read from a database, using the console.
+- [x] Using Entity Framework or/and raw SQL.
+- [x] Using SQL Server or SQLite.
+- [x] Use Dependency Injection in the repository.
+- [x] Application should have at least the following classes: UserInput, ExerciseController, ExerciseService and ExerciseRepository.
+- [x] The Exercise model class should have at least the following properties: {Id INT, DateStart DateTime, DateEnd DateTime, Duration TimeSpan, Comments string}.
+
+## Features
+
+* SQL Server database connection with Entity Framework and Dapper ORM.
+> [!IMPORTANT]
+> After downloading the project, you should check appsetting.json and write your own path to connect the db.
+>
+> 
+
+> [!IMPORTANT]
+> Also you should do starting migrations to create db with all necessary tables, simply write ```dotnet ef database update``` in CLI.
+>
+> 
+
+* A console based UI where you can navigate by user input.
+
+ 
+
+ 
+
+* CRUD abilities for your tracked exercises.
+
+ 
+
+ 
+
+* Repository pattern structure
+
+ 
+
+## Challenges and Learned Lessons
+- Repository pattern is very useful thing, you can swap different implementations of the same repository and it will work fine as well.
+- Moreover, you can say the same about interfaces as a whole.
+- Delegates, predicates and generic types is a must to know.
+
+## Areas to Improve
+- Better usage of delegates and generic type parameters.
+
+## Resources Used
+- C# Academy guidelines and roadmap.
+- ChatGPT for new information as EF usage, Repository Pattern, etc..
+- Spectre.Console documentation.
+- EF and Dapper ORM documentation.
+- Various StackOverflow articles.
+
diff --git a/ExerciseTracker.TwilightSaw/Repository/ExerciseDapperRepository.cs b/ExerciseTracker.TwilightSaw/Repository/ExerciseDapperRepository.cs
new file mode 100644
index 00000000..91730471
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Repository/ExerciseDapperRepository.cs
@@ -0,0 +1,51 @@
+using ExerciseTracker.TwilightSaw.Model;
+using System.IO;
+using Dapper;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+
+namespace ExerciseTracker.TwilightSaw.Repository;
+
+public class ExerciseDapperRepository(IConfiguration configuration) : IRepository where T : class
+{
+ private const string TableName = "Exercises";
+ public T GetById(int id)
+ {
+ using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
+ return connection.QuerySingleOrDefault($"SELECT * FROM {TableName} WHERE Id == @Id", new {Id = id});
+ }
+
+ public IEnumerable GetAllByType(Func predicate)
+ {
+ return GetAll().Where(predicate);
+ }
+
+ public IEnumerable GetAll()
+ {
+ using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
+ return connection.Query($"SELECT * FROM {TableName}").ToList();
+ }
+
+ public void Add(T entity)
+ {
+ using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
+ var properties = new List { "StartTime", "EndTime", "Comments", "Type" };
+ var columns = string.Join(", ", properties);
+ var values = string.Join(", ", properties.Select(p => $"@{p}"));
+ var insertQuery = $"INSERT INTO {TableName} ({columns}) VALUES ({values})";
+ connection.Execute(insertQuery, entity);
+ }
+
+ public void Update(T entity)
+ {
+ using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
+ const string updateQuery = $"UPDATE {TableName} SET StartTime = @StartTime, EndTime = @EndTime, Comments = @Comments, Type = @Type WHERE Id = @Id";
+ connection.Execute(updateQuery, entity);
+ }
+
+ public void Delete(int id)
+ {
+ using var connection = new SqlConnection(configuration.GetConnectionString("DefaultConnection"));
+ connection.Execute($"DELETE FROM {TableName} WHERE Id = @Id", new { Id = id });
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Repository/ExerciseRepository.cs b/ExerciseTracker.TwilightSaw/Repository/ExerciseRepository.cs
new file mode 100644
index 00000000..4557eadd
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Repository/ExerciseRepository.cs
@@ -0,0 +1,45 @@
+using ExerciseTracker.TwilightSaw.Data;
+using Microsoft.EntityFrameworkCore;
+using System.Collections.Generic;
+
+namespace ExerciseTracker.TwilightSaw.Repository;
+
+public class ExerciseRepository(DbContext context) : IRepository where T : class
+{
+ private readonly DbSet _dbSet = context.Set();
+ public T GetById(int id)
+ {
+ return _dbSet.Find(id);
+ }
+
+ public IEnumerable GetAllByType(Func predicate)
+ {
+ return _dbSet.Where(predicate).ToList();
+ }
+
+ public IEnumerable GetAll()
+ {
+ return _dbSet.ToList();
+ }
+
+ public void Add(T entity)
+ {
+ _dbSet.Add(entity);
+ context.SaveChanges();
+ }
+
+ public void Update(T entity)
+ {
+ _dbSet.Update(entity);
+ context.SaveChanges();
+ }
+
+ public void Delete(int id)
+ {
+ var entity = _dbSet.Find(id);
+ if (entity == null) return;
+ _dbSet.Remove(entity);
+ context.SaveChanges();
+
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Repository/IRepository.cs b/ExerciseTracker.TwilightSaw/Repository/IRepository.cs
new file mode 100644
index 00000000..c3b4875a
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Repository/IRepository.cs
@@ -0,0 +1,11 @@
+namespace ExerciseTracker.TwilightSaw.Repository;
+
+public interface IRepository
+{
+ T GetById(int id);
+ IEnumerable GetAllByType(Func predicate);
+ IEnumerable GetAll();
+ void Add(T entity);
+ void Update(T entity);
+ void Delete(int id);
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/Service/ExerciseService.cs b/ExerciseTracker.TwilightSaw/Service/ExerciseService.cs
new file mode 100644
index 00000000..f28cefdb
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/Service/ExerciseService.cs
@@ -0,0 +1,40 @@
+using ExerciseTracker.TwilightSaw.Model;
+using ExerciseTracker.TwilightSaw.Repository;
+
+namespace ExerciseTracker.TwilightSaw.Service;
+
+public class ExerciseService(IRepository repository, IRepository dapperRepository)
+{
+ public void AddExercise(Exercise exercise)
+ {
+ if(exercise.Type == "Cardio")
+ repository.Add(exercise);
+ else dapperRepository.Add(exercise);
+ }
+
+ public List GetExercises()
+ {
+ return repository.GetAll().ToList();
+ }
+
+ public Exercise GetExercise(int id)
+ {
+ return repository.GetById(id);
+ }
+ public List GetExerciseByType(string type)
+ {
+ return repository.GetAllByType(t => t.Type == type).ToList();
+ }
+
+ public void UpdateExercise(Exercise exercise)
+ {
+ if (exercise.Type == "Cardio")
+ repository.Update(exercise);
+ else dapperRepository.Update(exercise);
+ }
+
+ public void DeleteExercise(int id)
+ {
+ repository.Delete(id);
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/View/Menu.cs b/ExerciseTracker.TwilightSaw/View/Menu.cs
new file mode 100644
index 00000000..5f17dbd4
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/View/Menu.cs
@@ -0,0 +1,62 @@
+using ExerciseTracker.TwilightSaw.Controller;
+using ExerciseTracker.TwilightSaw.Helper;
+using Spectre.Console;
+
+namespace ExerciseTracker.TwilightSaw.View;
+
+public class Menu(ExerciseController controller)
+{
+ public void AddMenu()
+ {
+ while(true)
+ {
+ Console.Clear();
+ AnsiConsole.Write(new Rule("[olive]Welcome to the Exercise Tracker![/]"));
+ var typeInput = UserInput.CreateChoosingList(["Cardio", "Weights"], "Exit");
+ if (typeInput == "Exit") break;
+ var end = false;
+ while (!end)
+ {
+ Console.Clear();
+ AnsiConsole.Write(new Rule($"[olive]{typeInput}[/]"));
+ switch (UserInput.CreateChoosingList(["Add the exercise", "Your exercises"], "Return"))
+ {
+ case "Add the exercise":
+ controller.AddExercise(typeInput);
+ break;
+ case "Your exercises":
+ ShowExercise(typeInput);
+ break;
+ case "Return":
+ end = true;
+ break;
+ }
+ }
+ }
+ }
+
+ private void ShowExercise(string typeInput)
+ {
+ while (true)
+ {
+ var chosenExercise = controller.GetExercise(typeInput);
+ Console.Clear();
+ AnsiConsole.Write(new Rule($"[olive]{chosenExercise.StartTime.ToShortDateString()} " +
+ $"{chosenExercise.StartTime.TimeOfDay} " +
+ $"{chosenExercise.EndTime.TimeOfDay}[/]"));
+ if (chosenExercise.Comments == "Return") break;
+ switch (UserInput.CreateChoosingList(["Update exercise information", "Delete the exercise"],
+ "Return"))
+ {
+ case "Update exercise information":
+ controller.ChangeExercise(chosenExercise);
+ break;
+ case "Delete the exercise":
+ controller.DeleteExercise(chosenExercise);
+ break;
+ case "Return":
+ break;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/ExerciseTracker.TwilightSaw/appsettings.json b/ExerciseTracker.TwilightSaw/appsettings.json
new file mode 100644
index 00000000..44185bd9
--- /dev/null
+++ b/ExerciseTracker.TwilightSaw/appsettings.json
@@ -0,0 +1,12 @@
+{
+ "ConnectionStrings": {
+ "DefaultConnection": "Server=(localdb)\\localdb;Database=db;Trusted_Connection=True;"
+ },
+ "Logging": {
+ "LogLevel": {
+ "Default": "None",
+ "Microsoft.AspNetCore": "Warning"
+ }
+ },
+ "AllowedHosts": "*"
+}
diff --git a/ExerciseTracker.TwilightSaw/images/appsettings.png b/ExerciseTracker.TwilightSaw/images/appsettings.png
new file mode 100644
index 00000000..9e60afbe
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/appsettings.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/crud.png b/ExerciseTracker.TwilightSaw/images/crud.png
new file mode 100644
index 00000000..f1b4a584
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/crud.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/crud2.png b/ExerciseTracker.TwilightSaw/images/crud2.png
new file mode 100644
index 00000000..d84d9d94
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/crud2.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/migrations.png b/ExerciseTracker.TwilightSaw/images/migrations.png
new file mode 100644
index 00000000..9fe68aad
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/migrations.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/repository.png b/ExerciseTracker.TwilightSaw/images/repository.png
new file mode 100644
index 00000000..20d2b479
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/repository.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/ui.png b/ExerciseTracker.TwilightSaw/images/ui.png
new file mode 100644
index 00000000..07e48d96
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/ui.png differ
diff --git a/ExerciseTracker.TwilightSaw/images/ui2.png b/ExerciseTracker.TwilightSaw/images/ui2.png
new file mode 100644
index 00000000..22f4303f
Binary files /dev/null and b/ExerciseTracker.TwilightSaw/images/ui2.png differ