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. +> +> ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/appsettings.png) + +> [!IMPORTANT] +> Also you should do starting migrations to create db with all necessary tables, simply write ```dotnet ef database update``` in CLI. +> +> ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/migrations.png) + +* A console based UI where you can navigate by user input. + + ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/ui.png) + + ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/crud.png) + +* CRUD abilities for your tracked exercises. + + ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/ui2.png) + + ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/crud2.png) + +* Repository pattern structure + + ![image](https://github.com/TwilightSaw/CodeReviews.Console.ExerciseTracker/blob/main/ExerciseTracker.TwilightSaw/images/repository.png) + +## 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