diff --git a/README.md b/README.md
new file mode 100644
index 00000000..07a125eb
--- /dev/null
+++ b/README.md
@@ -0,0 +1,80 @@
+# Flash Cards App
+
+This is a simple, console based application for creating and studying stacks of flash cards.
+
+## Description
+
+The core unit in the Flash Cards App is a stack. A stack consists of individual, unique cards that have text on both sides.
+A stack can be created or deleted by the user and must have a unique name. Users can add, edit, or delete cards within each stack.
+A stack can be studied to test the user's recall. Assessment in performed by the user and reported to the console.
+This is used to store the study session data, which can be retreived later.
+Additionally, reports can be generated to provide the total number of sessions per mon, per year, by stack, as well as the average score per month, per year, by stack.
+
+## Getting Started
+
+### Technologies
+
+* C# Console Project
+* Spectre.Console
+* SQL Server
+* Dapper
+
+### Initial Setup
+
+* Clone Repository
+* In FlashCards.App > appsettings.json update connection string to a valid SQL Server database
+* Run the application to build the database and see with starter data
+
+## Program Operation
+
+### Main Menu
+The main menu gets the list of stacks from the database and prints the name, card count, and study session count.
+It then provides the following options:
+* Review Cards in Stack
+* Create New Stack
+* Delete Stack
+* Begin Study Session
+* View Past Study Sessions
+* View Reports
+* Exit
+
+Review Stack takes the user to the Review Stack Menu (see next section).
+
+Creating a stack allows the user to enter a new stack name. Stack names must be unique and cannot be blank.
+
+Deleting a stack will delete the stack, all cards contained in that stack, as well as all study sessions associated with it. This cannot be undone.
+
+Beginning a study session will enter the study mode. In this mode, a stack is selected and the cards are randomly shuffled and shown to the user.
+The user is able to assess their own results and the session is stored in the database.
+
+Viewing past sessions prints a paginated list of all study sessions stored in the database.
+Users can navigate through pages to view all sessions.
+Sessions can be sorted by stack name, time, or score.
+
+View reports will prompt the user for a year (based on existing study sessions within the database) and use it to generate two reports.
+One will display the total number of sessions per month for that year organized by stack.
+The other will display the average score per month for that year organized by stack.
+
+Exit will close the application.
+
+### Review Stack Menu
+The Review Stack Menu is a submenu that enables the user to manage a single stack.
+It provides the following options:
+
+* Review Cards in Stack
+* Add Card to Stack
+* Edit Card Text
+* Delete Card from Stack
+* Return to Main Menu
+
+Review Stack provides a way for the user to practice their ability to recall the card information.
+Results are not recorded.
+
+Create, Edit, And Delete cards allows the user to manage the individual cards within a stack.
+All cards must have text on both sides.
+Each card's front text must be unique compared to the front text of all other cards wtihin the stack, as should the back text.
+
+Return to Main Menu returns to the main menu.
+
+## Project Requirements
+This project follows the guidelines for The C Sharp Academy Intermediate Console Application Flash Cards as found here: https://www.thecsharpacademy.com/project/14/flashcards
diff --git a/jzhartman.FlashCards/FlashCards.App/FlashCards.App.csproj b/jzhartman.FlashCards/FlashCards.App/FlashCards.App.csproj
new file mode 100644
index 00000000..abc1bfdf
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.App/FlashCards.App.csproj
@@ -0,0 +1,28 @@
+
+
+
+ Exe
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
+
diff --git a/jzhartman.FlashCards/FlashCards.App/Program.cs b/jzhartman.FlashCards/FlashCards.App/Program.cs
new file mode 100644
index 00000000..9fc030bb
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.App/Program.cs
@@ -0,0 +1,31 @@
+using FlashCards.Application;
+using FlashCards.ConsoleUI;
+using FlashCards.ConsoleUI.Controllers;
+using FlashCards.Infrastructure;
+using FlashCards.Infrastructure.Initialization;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FlashCards.App;
+
+internal class Program
+{
+ static void Main(string[] args)
+ {
+ var config = new ConfigurationBuilder()
+ .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
+ .Build();
+
+ var services = new ServiceCollection();
+ services.AddApplication();
+ services.AddInfrastructure(config);
+ services.AddConsoleUI();
+
+ var provider = services.BuildServiceProvider();
+ var initializer = provider.GetRequiredService();
+ initializer.Initialize();
+
+ var mainMenu = provider.GetRequiredService();
+ mainMenu.Run();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.App/appsettings.json b/jzhartman.FlashCards/FlashCards.App/appsettings.json
new file mode 100644
index 00000000..2ba07b4c
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.App/appsettings.json
@@ -0,0 +1,6 @@
+{
+ "ConnectionStrings": {
+ "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=FlashCardsAppDb;Integrated Security=True;Connect Timeout=30;Encrypt=True;TrustServerCertificate=False;ApplicationIntent=ReadWrite;MultiSubnetFailover=False",
+ }
+
+}
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardCommand.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardCommand.cs
new file mode 100644
index 00000000..5d6f98fc
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardCommand.cs
@@ -0,0 +1,3 @@
+namespace FlashCards.Application.Cards.Add;
+
+public record AddCardCommand(int StackId, string FrontText, string BackText);
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardHandler.cs
new file mode 100644
index 00000000..ea65e018
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardHandler.cs
@@ -0,0 +1,25 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Cards.Add;
+
+public class AddCardHandler
+{
+ private readonly ICardRepository _cardRepo;
+
+ public AddCardHandler(ICardRepository cardRepo)
+ {
+ _cardRepo = cardRepo;
+ }
+
+ public Result Handle(AddCardCommand cardCommand)
+ {
+ var card = new Card(cardCommand.StackId, cardCommand.FrontText, cardCommand.BackText);
+ var id = _cardRepo.Add(card);
+ card.SetId(id);
+
+ if (id > 0) return Result.Success(new(id, cardCommand.StackId, cardCommand.FrontText, cardCommand.BackText, 0, 0, 0));
+ else return Result.Failure(Errors.InvalidId);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/Delete/DeleteCardByIdHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/Delete/DeleteCardByIdHandler.cs
new file mode 100644
index 00000000..f37b10ca
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/Delete/DeleteCardByIdHandler.cs
@@ -0,0 +1,18 @@
+using FlashCards.Application.Interfaces;
+
+namespace FlashCards.Application.Cards.Delete;
+
+public class DeleteCardByIdHandler
+{
+ private readonly ICardRepository _repo;
+
+ public DeleteCardByIdHandler(ICardRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public void Handle(int id)
+ {
+ _repo.DeleteById(id);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardCommand.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardCommand.cs
new file mode 100644
index 00000000..8720eb04
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardCommand.cs
@@ -0,0 +1,3 @@
+namespace FlashCards.Application.Cards.EditTextBySide;
+
+public record EditCardCommand(int StackId, string FrontText, string BackText);
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardHandler.cs
new file mode 100644
index 00000000..f5d7e63c
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardHandler.cs
@@ -0,0 +1,22 @@
+using FlashCards.Application.Cards.EditTextBySide;
+using FlashCards.Application.Interfaces;
+
+namespace FlashCards.Application.Cards.EditCard;
+
+public class EditCardHandler
+{
+ private readonly ICardRepository _cardRepo;
+ private readonly IStackRepository _stackRepo;
+
+ public EditCardHandler(ICardRepository cardRepo, IStackRepository stackRepo)
+ {
+ _cardRepo = cardRepo;
+ _stackRepo = stackRepo;
+ }
+ public void Handle(CardResponse card, EditCardCommand editedCard)
+ {
+ int id = _cardRepo.GetIdByTextAndStackId(card.StackId, card.FrontText, card.BackText);
+
+ _cardRepo.UpdateCardText(id, editedCard.FrontText, editedCard.BackText);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideCommand.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideCommand.cs
new file mode 100644
index 00000000..e790aa57
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideCommand.cs
@@ -0,0 +1,5 @@
+using FlashCards.Application.Enums;
+
+namespace FlashCards.Application.Cards.EditTextBySide;
+
+public record EditCardTextBySideCommand(string Text, CardSide Side);
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideHandler.cs
new file mode 100644
index 00000000..9c68e0b5
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideHandler.cs
@@ -0,0 +1,45 @@
+using FlashCards.Application.Enums;
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Cards.EditTextBySide;
+
+public class EditCardTextBySideHandler
+{
+ private readonly ICardRepository _cardRepo;
+
+ public EditCardTextBySideHandler(ICardRepository cardRepo)
+ {
+ _cardRepo = cardRepo;
+ }
+
+ public Result Handle(CardResponse card, EditCardTextBySideCommand editedCard)
+ {
+ var cardId = _cardRepo.GetIdByTextAndStackId(card.StackId, card.FrontText, card.BackText);
+
+ if (editedCard.Side == CardSide.Front)
+ {
+ if (string.IsNullOrWhiteSpace(editedCard.Text))
+ return Result.Success(card.FrontText);
+
+ if (_cardRepo.ExistsByFrontTextExcludingId(editedCard.Text, card.StackId, cardId))
+ return Result.Failure(Errors.CardFrontTextExists);
+
+ if (editedCard.Text.Length > 250)
+ return Result.Failure(Errors.CardTextLengthTooLong);
+ }
+ if (editedCard.Side == CardSide.Back)
+ {
+ if (string.IsNullOrWhiteSpace(editedCard.Text))
+ return Result.Success(card.BackText);
+
+ if (_cardRepo.ExistsByBackTextExcludingId(editedCard.Text, card.StackId, cardId))
+ return Result.Failure(Errors.CardBackTextExists);
+
+ if (editedCard.Text.Length > 250)
+ return Result.Failure(Errors.CardTextLengthTooLong);
+ }
+
+ return Result.Success(editedCard.Text);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/CardResponse.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/CardResponse.cs
new file mode 100644
index 00000000..1205bea9
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/CardResponse.cs
@@ -0,0 +1,3 @@
+namespace FlashCards.Application.Cards;
+
+public record CardResponse(int Id, int StackId, string FrontText, string BackText, int TimesStudied, int TimesCorrect, int TimesIncorrect);
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/GetAllByStackId.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/GetAllByStackId.cs
new file mode 100644
index 00000000..26f1394c
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/GetAllByStackId.cs
@@ -0,0 +1,44 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Cards.GetAllByStackId;
+
+public class GetAllByStackId
+
+{
+ private readonly ICardRepository _cardRepo;
+ private readonly IStackRepository _stackRepo;
+
+ public GetAllByStackId(ICardRepository cardRepo, IStackRepository stackRepo)
+ {
+ _cardRepo = cardRepo;
+ _stackRepo = stackRepo;
+ }
+
+ public Result> Handle(int stackId)
+ {
+ if (_stackRepo.ExistsById(stackId) == false)
+ return Result>.Failure(Errors.NoStacksExist);
+
+ var cards = _cardRepo.GetAllByStackId(stackId);
+
+ if (cards.Count == 0)
+ return Result>.Failure(Errors.NoCardsExist);
+ else
+ return Result>.Success(BuildResponse(cards, stackId));
+ }
+
+ private List BuildResponse(List cards, int stackId)
+ {
+ var outputs = new List();
+
+ foreach (var card in cards)
+ {
+ var cardResponse = new CardResponse(card.Id, stackId, card.FrontText, card.BackText, card.TimesStudied, card.TimesCorrect, card.TimesIncorrect);
+ outputs.Add(cardResponse);
+ }
+
+ return outputs;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/UpdateCardCounters/UpdateCardCountersHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/UpdateCardCounters/UpdateCardCountersHandler.cs
new file mode 100644
index 00000000..81dad4c1
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/UpdateCardCounters/UpdateCardCountersHandler.cs
@@ -0,0 +1,25 @@
+using FlashCards.Application.Interfaces;
+
+namespace FlashCards.Application.Cards.UpdateCardCounters;
+
+public class UpdateCardCountersHandler
+{
+ private readonly ICardRepository _repo;
+
+ public UpdateCardCountersHandler(ICardRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public void Handle(List cardsCorrect, List cardsIncorrect)
+ {
+ foreach (var card in cardsCorrect)
+ {
+ _repo.UpdateCardCounters(card, 1, 1, 0);
+ }
+ foreach (var card in cardsIncorrect)
+ {
+ _repo.UpdateCardCounters(card, 1, 0, 1);
+ }
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySide.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySide.cs
new file mode 100644
index 00000000..4d1a5561
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySide.cs
@@ -0,0 +1,43 @@
+using FlashCards.Application.Enums;
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Cards.ValidateCardTextBySide;
+
+public class ValidateCardTextBySide
+{
+ private readonly ICardRepository _cardRepo;
+
+ public ValidateCardTextBySide(ICardRepository cardRepo)
+ {
+ _cardRepo = cardRepo;
+ }
+
+ public Result Handle(ValidateCardTextBySideCommand card)
+ {
+ if (card.Side == CardSide.Front)
+ {
+ if (_cardRepo.ExistsByFrontText(card.Text, card.StackId))
+ return Result.Failure(Errors.CardFrontTextExists);
+
+ if (string.IsNullOrWhiteSpace(card.Text))
+ return Result.Failure(Errors.CardFrontTextRequired);
+
+ if (card.Text.Length > 250)
+ return Result.Failure(Errors.CardTextLengthTooLong);
+ }
+ if (card.Side == CardSide.Back)
+ {
+ if (_cardRepo.ExistsByBackText(card.Text, card.StackId))
+ return Result.Failure(Errors.CardBackTextExists);
+
+ if (string.IsNullOrWhiteSpace(card.Text))
+ return Result.Failure(Errors.CardBackTextRequired);
+
+ if (card.Text.Length > 250)
+ return Result.Failure(Errors.CardTextLengthTooLong);
+ }
+
+ return Result.Success(card.Text);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySideCommand.cs b/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySideCommand.cs
new file mode 100644
index 00000000..557adcf6
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySideCommand.cs
@@ -0,0 +1,5 @@
+using FlashCards.Application.Enums;
+
+namespace FlashCards.Application.Cards.ValidateCardTextBySide;
+
+public record ValidateCardTextBySideCommand(int StackId, string Text, CardSide Side);
diff --git a/jzhartman.FlashCards/FlashCards.Application/DependencyInjection.cs b/jzhartman.FlashCards/FlashCards.Application/DependencyInjection.cs
new file mode 100644
index 00000000..03fce564
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/DependencyInjection.cs
@@ -0,0 +1,45 @@
+using FlashCards.Application.Cards.Add;
+using FlashCards.Application.Cards.Delete;
+using FlashCards.Application.Cards.EditCard;
+using FlashCards.Application.Cards.EditTextBySide;
+using FlashCards.Application.Cards.GetAllByStackId;
+using FlashCards.Application.Cards.UpdateCardCounters;
+using FlashCards.Application.Cards.ValidateCardTextBySide;
+using FlashCards.Application.Reports.GetAverageScorePerMonth;
+using FlashCards.Application.Reports.GetSessionCountPerMonth;
+using FlashCards.Application.Stacks.Add;
+using FlashCards.Application.Stacks.Delete;
+using FlashCards.Application.Stacks.GetAll;
+using FlashCards.Application.StudySessions.Add;
+using FlashCards.Application.StudySessions.GetAll;
+using FlashCards.Application.StudySessions.GetAllSessionYears;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FlashCards.Application;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddApplication(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Enums/CardSide.cs b/jzhartman.FlashCards/FlashCards.Application/Enums/CardSide.cs
new file mode 100644
index 00000000..6ca4607a
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Enums/CardSide.cs
@@ -0,0 +1,8 @@
+namespace FlashCards.Application.Enums;
+
+public enum CardSide
+{
+ Front,
+ Back
+}
+
diff --git a/jzhartman.FlashCards/FlashCards.Application/FlashCards.Application.csproj b/jzhartman.FlashCards/FlashCards.Application/FlashCards.Application.csproj
new file mode 100644
index 00000000..2c3b1957
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/FlashCards.Application.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jzhartman.FlashCards/FlashCards.Application/Interfaces/ICardRepository.cs b/jzhartman.FlashCards/FlashCards.Application/Interfaces/ICardRepository.cs
new file mode 100644
index 00000000..c38146d2
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Interfaces/ICardRepository.cs
@@ -0,0 +1,19 @@
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.Interfaces;
+
+public interface ICardRepository
+{
+ int Add(Card card);
+ void DeleteAllByStackId(int id);
+ void DeleteById(int id);
+ bool ExistsByBackText(string text, int stackId);
+ bool ExistsByBackTextExcludingId(string text, int stackId, int cardId);
+ bool ExistsByFrontText(string text, int stackId);
+ bool ExistsByFrontTextExcludingId(string text, int stackId, int cardId);
+ List GetAllByStackId(int id);
+ int GetCardCountByStackId(int id);
+ int GetIdByTextAndStackId(int stackId, string frontText, string backText);
+ void UpdateCardCounters(int id, int studied, int correct, int incorrect);
+ void UpdateCardText(int id, string frontText, string backText);
+}
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStackRepository.cs b/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStackRepository.cs
new file mode 100644
index 00000000..68bc28f5
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStackRepository.cs
@@ -0,0 +1,14 @@
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.Interfaces;
+
+public interface IStackRepository
+{
+ Stack GetById(int id);
+ List GetAll();
+ int Add(string name);
+ void DeleteById(int id);
+ bool ExistsByName(string name);
+ int GetIdByName(string name);
+ bool ExistsById(int id);
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStudySessionRepository.cs b/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStudySessionRepository.cs
new file mode 100644
index 00000000..cb25325c
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Interfaces/IStudySessionRepository.cs
@@ -0,0 +1,14 @@
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.Interfaces;
+
+public interface IStudySessionRepository
+{
+ void Add(StudySession session);
+ List GetAll();
+ int GetSessionCountByStackId(int id);
+ void DeleteAllByStackId(int id);
+ List GetAverageScoreByMonthByYear(int year);
+ int[] GetAllSessionYears();
+ List GetSessionCountByMonthByYear(int year);
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthByYearHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthByYearHandler.cs
new file mode 100644
index 00000000..fabaaf40
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthByYearHandler.cs
@@ -0,0 +1,49 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.Reports.GetAverageScorePerMonth;
+
+public class GetAverageScorePerMonthByYearHandler
+{
+ private readonly IStudySessionRepository _studyRepo;
+
+ public GetAverageScorePerMonthByYearHandler(IStudySessionRepository studyRepo)
+ {
+ _studyRepo = studyRepo;
+ }
+
+ public List Handle(int year)
+ {
+ var report = _studyRepo.GetAverageScoreByMonthByYear(year);
+
+ return ReportMapper(report);
+ }
+
+ private List ReportMapper(List report)
+ {
+ var mappedReport = new List();
+
+ foreach (var row in report)
+ {
+ mappedReport.Add(new(
+ row.StackId,
+ row.StackName,
+ row.SessionYear,
+ row.January,
+ row.February,
+ row.March,
+ row.April,
+ row.May,
+ row.June,
+ row.July,
+ row.August,
+ row.September,
+ row.October,
+ row.November,
+ row.December
+ ));
+ }
+
+ return mappedReport;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthResponse.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthResponse.cs
new file mode 100644
index 00000000..492541b7
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthResponse.cs
@@ -0,0 +1,6 @@
+namespace FlashCards.Application.Reports.GetAverageScorePerMonth;
+
+public record GetAverageScorePerMonthResponse(int StackId, string StackName, string Year, double January, double February,
+ double March, double April, double May, double June, double July, double August, double September, double October,
+ double November, double December);
+
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthHandler.cs
new file mode 100644
index 00000000..ceb2ee7e
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthHandler.cs
@@ -0,0 +1,49 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.Reports.GetSessionCountPerMonth;
+
+public class GetSessionCountPerMonthHandler
+{
+ private readonly IStudySessionRepository _studyRepo;
+
+ public GetSessionCountPerMonthHandler(IStudySessionRepository studyRepo)
+ {
+ _studyRepo = studyRepo;
+ }
+
+ public List Handle(int year)
+ {
+ var report = _studyRepo.GetSessionCountByMonthByYear(year);
+
+ return ReportMapper(report);
+ }
+
+ private List ReportMapper(List report)
+ {
+ var mappedReport = new List();
+
+ foreach (var row in report)
+ {
+ mappedReport.Add(new(
+ row.StackId,
+ row.StackName,
+ row.SessionYear,
+ row.January,
+ row.February,
+ row.March,
+ row.April,
+ row.May,
+ row.June,
+ row.July,
+ row.August,
+ row.September,
+ row.October,
+ row.November,
+ row.December
+ ));
+ }
+
+ return mappedReport;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthResponse.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthResponse.cs
new file mode 100644
index 00000000..d5598fc1
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthResponse.cs
@@ -0,0 +1,5 @@
+namespace FlashCards.Application.Reports.GetSessionCountPerMonth;
+
+public record GetSessionCountPerMonthResponse(int StackId, string StackName, string Year, double January, double February,
+ double March, double April, double May, double June, double July, double August, double September, double October,
+ double November, double December);
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportHandler.cs
new file mode 100644
index 00000000..bd95aa44
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportHandler.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FlashCards.Application.Reports.GetSessionsPerMonth;
+
+internal class SessionPerMonthReportHandler
+{
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportRequest.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportRequest.cs
new file mode 100644
index 00000000..bafb5e2d
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportRequest.cs
@@ -0,0 +1,5 @@
+namespace FlashCards.Application.Reports.GetSessionsPerMonth;
+
+internal class SessionPerMonthReportRequest
+{
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportResponse.cs b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportResponse.cs
new file mode 100644
index 00000000..20531604
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportResponse.cs
@@ -0,0 +1,9 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace FlashCards.Application.Reports.GetSessionsPerMonth;
+
+internal class SessionPerMonthReportResponse
+{
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Stacks/Add/AddStackHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Stacks/Add/AddStackHandler.cs
new file mode 100644
index 00000000..cd87e0be
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Stacks/Add/AddStackHandler.cs
@@ -0,0 +1,31 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Stacks.Add;
+
+public class AddStackHandler
+{
+ private readonly IStackRepository _repo;
+
+ public AddStackHandler(IStackRepository repo)
+ {
+ _repo = repo;
+ }
+
+ public Result Handle(string name)
+ {
+ if (_repo.ExistsByName(name))
+ return Result.Failure(Errors.StackNameExists);
+
+ if (String.IsNullOrWhiteSpace(name))
+ return Result.Failure(Errors.StackNameRequired);
+
+ if (name.Length > 32)
+ return Result.Failure(Errors.StackNameTooLong);
+
+ _repo.Add(name);
+
+ return Result.Success(name);
+ }
+
+}
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/Stacks/Delete/DeleteByIdHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Stacks/Delete/DeleteByIdHandler.cs
new file mode 100644
index 00000000..2bba8368
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Stacks/Delete/DeleteByIdHandler.cs
@@ -0,0 +1,25 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Application.Stacks.GetAll;
+
+namespace FlashCards.Application.Stacks.Delete;
+
+public class DeleteByIdHandler
+{
+ private readonly ICardRepository _cardRepo;
+ private readonly IStackRepository _stackRepo;
+ private readonly IStudySessionRepository _sessionRepo;
+
+ public DeleteByIdHandler(ICardRepository cardRepo, IStackRepository stackRepo, IStudySessionRepository sessionRepo)
+ {
+ _cardRepo = cardRepo;
+ _stackRepo = stackRepo;
+ _sessionRepo = sessionRepo;
+ }
+
+ public void Handle(StackNamesWithCountsResponse stack)
+ {
+ _cardRepo.DeleteAllByStackId(stack.Id);
+ _sessionRepo.DeleteAllByStackId(stack.Id);
+ _stackRepo.DeleteById(stack.Id);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/GetAllStacksWithCountsHandler.cs b/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/GetAllStacksWithCountsHandler.cs
new file mode 100644
index 00000000..60535b41
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/GetAllStacksWithCountsHandler.cs
@@ -0,0 +1,45 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.Stacks.GetAll;
+
+public class GetAllStacksWithCountsHandler
+{
+ private readonly IStackRepository _stackRepo;
+ private readonly ICardRepository _cardRepo;
+ private readonly IStudySessionRepository _sessionRepo;
+
+ public GetAllStacksWithCountsHandler(IStackRepository stackRepo, ICardRepository cardRepo, IStudySessionRepository sessionRepo)
+ {
+ _stackRepo = stackRepo;
+ _cardRepo = cardRepo;
+ _sessionRepo = sessionRepo;
+ }
+
+ public Result> Handle()
+ {
+ var stacks = _stackRepo.GetAll();
+
+ if (stacks.Count == 0)
+ return Result>.Failure(Errors.NoStacksExist);
+ else
+ return Result>.Success(BuildResponse(stacks));
+ }
+
+ private List BuildResponse(List stacks)
+ {
+ var stackResponses = new List();
+
+ foreach (var stack in stacks)
+ {
+ int cardCount = _cardRepo.GetCardCountByStackId(stack.Id);
+ var sessionCount = _sessionRepo.GetSessionCountByStackId(stack.Id);
+
+ var stackResponse = new StackNamesWithCountsResponse(stack.Id, stack.Name, cardCount, sessionCount);
+ stackResponses.Add(stackResponse);
+ }
+
+ return stackResponses;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/StackNamesWithCountsResponse.cs b/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/StackNamesWithCountsResponse.cs
new file mode 100644
index 00000000..5b55a386
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/StackNamesWithCountsResponse.cs
@@ -0,0 +1,3 @@
+namespace FlashCards.Application.Stacks.GetAll;
+
+public record StackNamesWithCountsResponse(int Id, string Name, int CardCount, int SessionCount);
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionCommand.cs b/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionCommand.cs
new file mode 100644
index 00000000..c300c714
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionCommand.cs
@@ -0,0 +1,4 @@
+namespace FlashCards.Application.StudySessions.Add;
+
+public record AddStudySessionCommand(int StackId, string StackName, DateTime SessionDate, double Score,
+ int CardsStudied, int CardsCorrect, int CardsIncorrect);
diff --git a/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionHandler.cs b/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionHandler.cs
new file mode 100644
index 00000000..c9b1aafd
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionHandler.cs
@@ -0,0 +1,31 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+
+namespace FlashCards.Application.StudySessions.Add;
+
+public class AddStudySessionHandler
+{
+ private readonly IStudySessionRepository _studyRepo;
+ private readonly IStackRepository _stackRepo;
+
+ public AddStudySessionHandler(IStudySessionRepository repo, IStackRepository stackRepo)
+ {
+ _studyRepo = repo;
+ _stackRepo = stackRepo;
+ }
+
+ public void Handle(AddStudySessionCommand session)
+ {
+ //var stackId = _stackRepo.GetIdByName(session.StackName);
+ _studyRepo.Add(new StudySession
+ {
+ SessionDate = session.SessionDate,
+ StackId = session.StackId,
+ Score = session.Score,
+ CountStudied = session.CardsStudied,
+ CountCorrect = session.CardsCorrect,
+ CountIncorrect = session.CardsIncorrect
+ });
+
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/GetAllStudySessionsHandler.cs b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/GetAllStudySessionsHandler.cs
new file mode 100644
index 00000000..3e832ba3
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/GetAllStudySessionsHandler.cs
@@ -0,0 +1,40 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.StudySessions.GetAll;
+
+public class GetAllStudySessionsHandler
+{
+ private readonly IStackRepository _stackRepo;
+ private readonly IStudySessionRepository _studyRepo;
+
+ public GetAllStudySessionsHandler(IStackRepository stackRepo, IStudySessionRepository studyRepo)
+ {
+ _stackRepo = stackRepo;
+ _studyRepo = studyRepo;
+ }
+
+ public Result> Handle()
+ {
+ var sessions = StudySessionMapper(_studyRepo.GetAll());
+
+ if (sessions == null || sessions.Count == 0)
+ return Result>.Failure(Errors.NoStudySessions);
+ else
+ return Result>.Success(sessions);
+ }
+
+ private List StudySessionMapper(List sessions)
+ {
+ var output = new List();
+
+ foreach (var session in sessions)
+ {
+ var stack = _stackRepo.GetById(session.StackId);
+ var sessionResponse = new StudySessionResponse(session.SessionDate, stack.Name, session.Score, session.CountStudied, session.CountCorrect, session.CountIncorrect);
+ output.Add(sessionResponse);
+ }
+ return output;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/StudySessionResponse.cs b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/StudySessionResponse.cs
new file mode 100644
index 00000000..43325386
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/StudySessionResponse.cs
@@ -0,0 +1,4 @@
+namespace FlashCards.Application.StudySessions.GetAll;
+
+public record StudySessionResponse(DateTime SessionDate, string StackName, double Score, int CountStudied, int CountCorrect, int CountIncorrect);
+
diff --git a/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAllSessionYears/GetAllSessionYears.cs b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAllSessionYears/GetAllSessionYears.cs
new file mode 100644
index 00000000..54bec391
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAllSessionYears/GetAllSessionYears.cs
@@ -0,0 +1,23 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Validation;
+
+namespace FlashCards.Application.StudySessions.GetAllSessionYears;
+
+public class GetAllSessionYears
+{
+ private readonly IStudySessionRepository _studyRepo;
+
+ public GetAllSessionYears(IStudySessionRepository studyRepo)
+ {
+ _studyRepo = studyRepo;
+ }
+
+ public Result Handle()
+ {
+ var sessionYears = _studyRepo.GetAllSessionYears();
+
+ if (sessionYears == null || sessionYears.Count() == 0) return Result.Failure(Errors.NoStudySessions);
+
+ return Result.Success(sessionYears);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/DependencyInjection.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/DependencyInjection.cs
new file mode 100644
index 00000000..861be117
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/DependencyInjection.cs
@@ -0,0 +1,34 @@
+using FlashCards.ConsoleUI.Controllers;
+using FlashCards.ConsoleUI.Handlers;
+using FlashCards.ConsoleUI.Input;
+using FlashCards.ConsoleUI.Output;
+using FlashCards.ConsoleUI.Services;
+using FlashCards.ConsoleUI.Views;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace FlashCards.ConsoleUI;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddConsoleUI(this IServiceCollection services)
+ {
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/Actions.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/Actions.cs
new file mode 100644
index 00000000..bc500cd4
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/Actions.cs
@@ -0,0 +1,8 @@
+namespace FlashCards.ConsoleUI.Enums;
+
+public enum Actions
+{
+ Create,
+ Edit,
+ Delete
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/MainMenuItem.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/MainMenuItem.cs
new file mode 100644
index 00000000..6a577d44
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/MainMenuItem.cs
@@ -0,0 +1,13 @@
+namespace FlashCards.ConsoleUI.Enums;
+
+public enum MainMenuItem
+{
+ ReviewStack,
+ CreateStack,
+ DeleteStack,
+ StudyStack,
+ ViewPastSessions,
+ Report,
+ Exit
+}
+
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/ReviewStackMenuItem.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/ReviewStackMenuItem.cs
new file mode 100644
index 00000000..994bd03c
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/ReviewStackMenuItem.cs
@@ -0,0 +1,10 @@
+namespace FlashCards.ConsoleUI.Enums;
+
+public enum ReviewStackMenuItem
+{
+ ReviewCards,
+ AddCard,
+ EditCard,
+ DeleteCard,
+ Return
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/FlashCards.ConsoleUI.csproj b/jzhartman.FlashCards/FlashCards.ConsoleUI/FlashCards.ConsoleUI.csproj
new file mode 100644
index 00000000..52cecdce
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/FlashCards.ConsoleUI.csproj
@@ -0,0 +1,18 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Input/ConsoleInput.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Input/ConsoleInput.cs
new file mode 100644
index 00000000..95249900
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Input/ConsoleInput.cs
@@ -0,0 +1,109 @@
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Input;
+
+public class ConsoleInput
+{
+ public int GetRecordIdFromUser(string message, int minValue, int maxValue)
+ {
+ var id = AnsiConsole.Prompt(
+ new TextPrompt(message)
+ .Validate(input =>
+ {
+ if (input < minValue) return Spectre.Console.ValidationResult.Error($"[red]ERROR:[/] A record for this value does not exist. Please enter a value between [yellow]{minValue}[/] and [yellow]{maxValue}[/].\r\n");
+ else if (input > maxValue) return Spectre.Console.ValidationResult.Error($"[red]ERROR:[/] A record for this value does not exist. Please enter a value between [yellow]{minValue}[/] and [yellow]{maxValue}[/].\r\n");
+ else return Spectre.Console.ValidationResult.Success();
+ }));
+
+ return id;
+ }
+ public bool GetDeleteStackConfirmationFromUser(string stackName, int cardCount, int sessionCount)
+ {
+ AddEmptyLines(1);
+
+ string promptText = $"[yellow]WARNING![/]You are about to delete the stack [green]{stackName}[/].\r\n" +
+ $"This will also delete all [blue]{cardCount}[/] cards and [blue]{sessionCount}[/] sessions that reference it.";
+
+ var confirmation = AnsiConsole.Prompt(
+ new TextPrompt(promptText)
+ .AddChoice(true)
+ .AddChoice(false)
+ .WithConverter(choice => choice ? "y" : "n"));
+
+ return confirmation;
+ }
+ public bool GetDeleteCardConfirmationFromUser(string frontText, string backText)
+ {
+ AddEmptyLines(1);
+
+ string promptText = $"[yellow]WARNING![/]You are about to delete the card with the following data:" +
+ $"\r\n[green]Front Text:[/] {frontText}" +
+ $"\r\n[green]Back Text:[/] {backText}" +
+ "\r\n\r\nWould you like to proceed?";
+
+ return ConfirmationSelection(promptText);
+ }
+
+ public bool GetEditCardConfirmationFromUser(string originalFrontText, string originalBackText, string newFrontText, string newBackText)
+ {
+ AddEmptyLines(1);
+
+ string promptText = $"[yellow]WARNING![/]The following change(s) are being made to a card:";
+
+ if (originalFrontText != newFrontText)
+ promptText += $"\r\n\t[blue]Front Text[/] changed from: [green]{originalFrontText}[/] to [yellow]{newFrontText}[/]";
+ if (originalBackText != newBackText)
+ promptText += $"\r\n\t[blue]Back Text[/] changed from: [green]{originalBackText}[/] to [yellow]{newBackText}[/]";
+
+ promptText += "\r\n\r\nConfirm changes: ";
+
+ return ConfirmationSelection(promptText);
+ }
+ public bool GetPassStateFromUser()
+ {
+ var choice = AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Did you pass or fail?")
+ .AddChoices("PASS", "FAIL"));
+
+ return (choice == "PASS" ? true : false);
+ }
+ public bool ContinueStudyMode()
+ {
+ Console.Write("Press \'ESC\' to exit study mode, or any other key to continue...");
+
+ return Console.ReadKey().Key != ConsoleKey.Escape;
+
+ }
+ public string GetTextInputFromUser(string message, int topSpaces = 0, int bottomSpaces = 0)
+ {
+ AddEmptyLines(topSpaces);
+ AnsiConsole.Markup($"{message}: ");
+ AddEmptyLines(bottomSpaces);
+ return Console.ReadLine();
+ }
+ public void PressAnyKeyToFlipCard()
+ {
+ Console.Write("Press any key to flip card...");
+ Console.ReadKey();
+ }
+ public void PressAnyKeyToContinue(int topSpaces = 1, string message = "Press any key to continue...")
+ {
+ AddEmptyLines(topSpaces);
+ Console.Write(message);
+ Console.ReadKey();
+ }
+ private bool ConfirmationSelection(string promptText, string affirmative = "y", string negative = "n")
+ {
+ return AnsiConsole.Prompt(
+ new TextPrompt(promptText)
+ .AddChoice(true)
+ .AddChoice(false)
+ .WithConverter(choice => choice ? affirmative : negative));
+ }
+ private void AddEmptyLines(int count)
+ {
+ for (int i = 0; i < count; i++) AnsiConsole.WriteLine();
+ }
+
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Models/StackViewModel.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Models/StackViewModel.cs
new file mode 100644
index 00000000..b9fa79b8
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Models/StackViewModel.cs
@@ -0,0 +1,17 @@
+using FlashCards.Application.Cards;
+
+namespace FlashCards.ConsoleUI.Models;
+
+public class StackViewModel
+{
+ public int StackId { get; set; }
+ public string Name { get; set; }
+ public List Cards { get; set; }
+
+ public StackViewModel(int stackId, string name, List cards)
+ {
+ StackId = stackId;
+ Name = name;
+ Cards = cards;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Output/ConsoleOutput.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Output/ConsoleOutput.cs
new file mode 100644
index 00000000..95b59e04
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Output/ConsoleOutput.cs
@@ -0,0 +1,89 @@
+using FlashCards.Application.Cards;
+using FlashCards.Application.Enums;
+using FlashCards.Core.Validation;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Output;
+
+public class ConsoleOutput
+{
+ public void PrintAppTitle()
+ {
+ var figlet = new FigletText("FLASH CARDS v1")
+ .Color(Color.Blue);
+ AnsiConsole.Write(figlet);
+ }
+ public void PrintPageTitle(string title)
+ {
+ AnsiConsole.Clear();
+ AnsiConsole.MarkupLine($"[bold green]{title.ToUpper()}[/]\r\n");
+ }
+
+ public void PrintCardTextInPanel(string text)
+ {
+ var panel = new Panel(new Align(new Markup(text), HorizontalAlignment.Center, VerticalAlignment.Middle)) { Width = 36, Height = 10 };
+ AnsiConsole.Write(panel);
+ }
+ public void PrintCardTextInSideBySidePanels(string frontText, string backText)
+ {
+ var frontPanel = new Panel(new Align(new Markup(frontText), HorizontalAlignment.Center, VerticalAlignment.Middle)) { Width = 36, Height = 10 };
+ var backPanel = new Panel(new Align(new Markup(backText), HorizontalAlignment.Center, VerticalAlignment.Middle)) { Width = 36, Height = 10 };
+ var columns = new Columns(frontPanel, backPanel).Collapse();
+ AnsiConsole.Write(columns);
+ }
+ public void PrintCard(CardResponse card, int i)
+ {
+ Console.WriteLine($"{i}: {card.FrontText} \t {card.BackText}");
+ }
+ public void PrintValidationErrorsFromCollection(List errors)
+ {
+ foreach (var error in errors)
+ {
+ AnsiConsole.MarkupLine($"[red]ERROR:[/] {error.Description}");
+ }
+ }
+
+ public void PrintSuccessMessage(string message) => AnsiConsole.MarkupLine($"[green]SUCCESS:[/] {message}");
+ public void PrintCancellationMessage(string action, string item) => AnsiConsole.MarkupLine($"[yellow]CANCELLED:[/] {action} of {item}!");
+ public void PrintNoEditsMadeMessage() => AnsiConsole.MarkupLine($"\r\n[yellow]No changes made to card![/]");
+ public void PrintReviewCardsKeypressOptions(CardSide side, int index, int cardCount)
+ {
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Control");
+ table.AddColumn("Key");
+
+ if (side != CardSide.Front && index > 0) table.AddRow("Previous Card", "A");
+ if (side != CardSide.Front && index < cardCount) table.AddRow("Next Card", "D");
+ table.AddRow("Shuffle Deck", "W");
+ table.AddRow("Return to Menu", "Q");
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+ public void PrintStudySessionKeypressOptions()
+ {
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Control");
+ table.AddColumn("Key");
+
+ table.AddRow("Previous Page", "A");
+ table.AddRow("Next Page", "D");
+ table.AddRow("Sort By Score", "W");
+ table.AddRow("Sort By Stack", "E");
+ table.AddRow("Sort By Time", "R");
+ table.AddRow("Return to Menu", "Q");
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/MainMenuService.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/MainMenuService.cs
new file mode 100644
index 00000000..3115b162
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/MainMenuService.cs
@@ -0,0 +1,237 @@
+using FlashCards.Application.Stacks.Add;
+using FlashCards.Application.Stacks.Delete;
+using FlashCards.Application.Stacks.GetAll;
+using FlashCards.Application.StudySessions.GetAll;
+using FlashCards.Application.StudySessions.GetAllSessionYears;
+using FlashCards.ConsoleUI.Enums;
+using FlashCards.ConsoleUI.Handlers;
+using FlashCards.ConsoleUI.Input;
+using FlashCards.ConsoleUI.Output;
+using FlashCards.ConsoleUI.Services;
+using FlashCards.ConsoleUI.Views;
+using FlashCards.Core.Validation;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Controllers;
+
+public class MainMenuService
+{
+ private readonly ConsoleInput _input;
+ private readonly ConsoleOutput _output;
+
+ private readonly StudySessionService _studySessionService;
+ private readonly ReviewStackMenuService _stackMenuService;
+ private readonly ReportService _reportService;
+
+ private readonly GetAllStacksWithCountsHandler _getAllStackNamesAndCounts;
+ private readonly AddStackHandler _addStack;
+ private readonly DeleteByIdHandler _deleteStack;
+ private readonly GetAllStudySessionsHandler _getAllStudySessions;
+ private readonly GetAllSessionYears _getAllSessionYears;
+
+ private readonly MainMenuView _mainMenu;
+ private readonly StackListView _stackList;
+ private readonly StudySessionListView _studySessionList;
+
+
+ public MainMenuService(ReviewStackMenuService stackMenuService, StudySessionService studySessionService, ReportService reportService,
+ ConsoleInput input, ConsoleOutput output, MainMenuView mainMenu, StackListView stackList, StudySessionListView studySessionList,
+ GetAllStacksWithCountsHandler getAllStackNamesAndCounts, AddStackHandler addStack,
+ DeleteByIdHandler deleteStack, GetAllStudySessionsHandler getAllStudySessions, GetAllSessionYears getAllSessionYears
+ )
+ {
+ _stackMenuService = stackMenuService;
+ _studySessionService = studySessionService;
+ _reportService = reportService;
+
+ _input = input;
+ _output = output;
+
+ _mainMenu = mainMenu;
+ _stackList = stackList;
+ _studySessionList = studySessionList;
+
+ _getAllStackNamesAndCounts = getAllStackNamesAndCounts;
+ _addStack = addStack;
+ _deleteStack = deleteStack;
+ _getAllStudySessions = getAllStudySessions;
+ _getAllSessionYears = getAllSessionYears;
+
+ }
+ public void Run()
+ {
+ bool exitApp = false;
+
+ while (exitApp == false)
+ {
+ _output.PrintPageTitle("MAIN MENU");
+
+ var stacks = GetStacks();
+
+ _stackList.Render(stacks);
+
+ MainMenuItem[] menuItems = Enum.GetValues();
+
+ if (stacks.Count <= 0) menuItems = new MainMenuItem[2] { MainMenuItem.CreateStack, MainMenuItem.Exit };
+
+ var selection = _mainMenu.Render(menuItems);
+
+ switch (selection)
+ {
+ case MainMenuItem.ReviewStack: HandleReviewStack(stacks); break;
+ case MainMenuItem.CreateStack: HandleCreateStack(); break;
+ case MainMenuItem.DeleteStack: HandleDeleteStack(stacks); break;
+ case MainMenuItem.StudyStack: HandleStudy(stacks); break;
+ case MainMenuItem.ViewPastSessions: HandleViewPastSessions(stacks); break;
+ case MainMenuItem.Report: HandleReports(); break;
+ case MainMenuItem.Exit: exitApp = true; break;
+ default: AnsiConsole.Markup("[bold red]ERROR:[/] Invalid input!"); break;
+ }
+ }
+ }
+
+ private List GetStacks()
+ {
+ var result = _getAllStackNamesAndCounts.Handle();
+
+ if (result.IsFailure)
+ {
+ _output.PrintValidationErrorsFromCollection(result.Errors);
+ return new List();
+ }
+ else
+ {
+ return result.Value;
+ }
+ }
+ private void HandleReviewStack(List stacks)
+ {
+ var message = "Please enter the [yellow]ID[/] of the stack you wish to review:";
+ int id = _input.GetRecordIdFromUser(message, 1, stacks.Count);
+ _stackMenuService.Run(stacks[id - 1]);
+ }
+ private void HandleCreateStack()
+ {
+ bool stackNameValid = false;
+
+ while (stackNameValid == false)
+ {
+ var input = _input.GetTextInputFromUser("Enter stack name");
+
+ var result = _addStack.Handle(input);
+
+ if (result.IsFailure) _output.PrintValidationErrorsFromCollection(result.Errors);
+
+ else
+ {
+ _output.PrintSuccessMessage($"Created stack {result.Value}!");
+ stackNameValid = true;
+ }
+ }
+ _input.PressAnyKeyToContinue();
+ }
+ private void HandleDeleteStack(List stacks)
+ {
+ var message = "Please enter the [yellow]ID[/] of the stack you wish to delete:";
+ int id = _input.GetRecordIdFromUser(message, 1, stacks.Count);
+ var stack = stacks[id - 1];
+
+ if (_input.GetDeleteStackConfirmationFromUser(stack.Name, stack.CardCount, stack.SessionCount))
+ {
+ _deleteStack.Handle(stack);
+ _output.PrintSuccessMessage($"Deleted [green]{stack.Name}[/] stack!");
+ }
+ else _output.PrintCancellationMessage("deletion", $"{stack.Name} stack");
+
+ _input.PressAnyKeyToContinue();
+ }
+ private void HandleStudy(List stacks)
+ {
+ var message = "Please enter the [yellow]ID[/] of the stack you wish to study:";
+ int id = _input.GetRecordIdFromUser(message, 1, stacks.Count);
+ _studySessionService.Run(stacks[id - 1]);
+ }
+ private void HandleViewPastSessions(List stacks)
+ {
+ var result = _getAllStudySessions.Handle();
+
+ if (result.IsFailure)
+ {
+ _output.PrintValidationErrorsFromCollection(new List { Errors.NoStudySessions });
+ _input.PressAnyKeyToContinue();
+ }
+ else
+ {
+ RenderPastSessionList(result.Value);
+ }
+ }
+ private void RenderPastSessionList(List sessions)
+ {
+ bool continueReview = true;
+ ConsoleKeyInfo keyInfo;
+ int startIndex = 0;
+
+ while (continueReview)
+ {
+ _output.PrintPageTitle("REVIEW STACK MENU");
+ _studySessionList.Render(sessions, startIndex);
+ _output.PrintStudySessionKeypressOptions();
+
+ bool validKey = false;
+
+ while (validKey == false)
+ {
+ keyInfo = Console.ReadKey(true);
+
+ switch (keyInfo.Key)
+ {
+ case ConsoleKey.E:
+ sessions = sessions.OrderBy(s => s.StackName).ToList();
+ validKey = true;
+ break;
+ case ConsoleKey.R:
+ sessions = sessions.OrderBy(s => s.SessionDate).ToList();
+ validKey = true;
+ break;
+ case ConsoleKey.W:
+ sessions = sessions.OrderBy(s => s.Score).ToList();
+ validKey = true;
+ break;
+ case ConsoleKey.A:
+ startIndex -= 15;
+ validKey = true;
+ break;
+ case ConsoleKey.D:
+ startIndex += 15;
+ validKey = true;
+ break;
+ case ConsoleKey.Q:
+ validKey = true;
+ continueReview = false;
+ break;
+ default:
+ Console.WriteLine($"Invalid keypress -- see options");
+ break;
+ }
+ }
+
+ if (startIndex > sessions.Count) startIndex = 0;
+ if (startIndex >= 0 && startIndex < 15) startIndex = 0;
+ if (startIndex < 0) startIndex = sessions.Count - sessions.Count % 15;
+ }
+ }
+ private void HandleReports()
+ {
+ var result = _getAllSessionYears.Handle();
+
+ if (result.IsFailure)
+ {
+ _output.PrintValidationErrorsFromCollection(result.Errors);
+ _input.PressAnyKeyToContinue();
+ }
+ else
+ {
+ _reportService.Run(result.Value);
+ }
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReportService.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReportService.cs
new file mode 100644
index 00000000..861522c1
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReportService.cs
@@ -0,0 +1,42 @@
+using FlashCards.Application.Reports.GetAverageScorePerMonth;
+using FlashCards.Application.Reports.GetSessionCountPerMonth;
+using FlashCards.ConsoleUI.Input;
+using FlashCards.ConsoleUI.Views;
+
+namespace FlashCards.ConsoleUI.Services;
+
+public class ReportService
+{
+ private readonly ConsoleInput _input;
+ private readonly SessionCountPerMonthView _sessionnCountView;
+ private readonly AverageScorePerMonthView _averageScoreView;
+ private readonly GetAverageScorePerMonthByYearHandler _averageScoreReport;
+ private readonly GetSessionCountPerMonthHandler _sessionCountReport;
+ private readonly SessionYearSelectionView _yearSelection;
+
+ public ReportService(GetAverageScorePerMonthByYearHandler averageScoreReport, GetSessionCountPerMonthHandler sessionCountReport,
+ SessionCountPerMonthView sessionCountView, AverageScorePerMonthView averageScoreView,
+ SessionYearSelectionView yearSelection, ConsoleInput input)
+ {
+ _input = input;
+ _averageScoreReport = averageScoreReport;
+ _sessionCountReport = sessionCountReport;
+ _sessionnCountView = sessionCountView;
+ _averageScoreView = averageScoreView;
+ _yearSelection = yearSelection;
+ }
+
+ public void Run(int[] sessionYears)
+ {
+
+ var year = _yearSelection.Render(sessionYears);
+
+ var averageScoreReports = _averageScoreReport.Handle(year);
+ var sessionCountReports = _sessionCountReport.Handle(year);
+
+ _averageScoreView.Render(averageScoreReports, year);
+ _sessionnCountView.Render(sessionCountReports, year);
+
+ _input.PressAnyKeyToContinue(1, "Press any key to return to the Main Menu...");
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReviewStackMenuService.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReviewStackMenuService.cs
new file mode 100644
index 00000000..0fba11b2
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReviewStackMenuService.cs
@@ -0,0 +1,260 @@
+using FlashCards.Application.Cards;
+using FlashCards.Application.Cards.Add;
+using FlashCards.Application.Cards.Delete;
+using FlashCards.Application.Cards.EditCard;
+using FlashCards.Application.Cards.EditTextBySide;
+using FlashCards.Application.Cards.GetAllByStackId;
+using FlashCards.Application.Cards.ValidateCardTextBySide;
+using FlashCards.Application.Enums;
+using FlashCards.Application.Stacks.GetAll;
+using FlashCards.ConsoleUI.Enums;
+using FlashCards.ConsoleUI.Input;
+using FlashCards.ConsoleUI.Models;
+using FlashCards.ConsoleUI.Output;
+using FlashCards.ConsoleUI.Views;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Handlers;
+
+public class ReviewStackMenuService
+{
+ private readonly ConsoleInput _input;
+ private readonly ConsoleOutput _output;
+
+ private readonly ReviewStackMenuView _menu;
+ private readonly CardListView _cardListView;
+
+ private readonly GetAllByStackId _getAllCardsByStackId;
+ private readonly AddCardHandler _addCard;
+ private readonly ValidateCardTextBySide _getCardText;
+ private readonly EditCardTextBySideHandler _editCardTextBySide;
+ private readonly EditCardHandler _editCard;
+ private readonly DeleteCardByIdHandler _deleteCardById;
+
+ public ReviewStackMenuService(ConsoleInput input, ConsoleOutput output, ReviewStackMenuView menu, CardListView cardListView,
+ GetAllByStackId getAllCardsByStackId, ValidateCardTextBySide getCardText, AddCardHandler addCard,
+ EditCardTextBySideHandler editCardFrontText, EditCardHandler editCard, DeleteCardByIdHandler deleteCardById)
+ {
+ _input = input;
+ _output = output;
+ _menu = menu;
+ _cardListView = cardListView;
+
+ _getAllCardsByStackId = getAllCardsByStackId;
+ _getCardText = getCardText;
+ _addCard = addCard;
+ _editCardTextBySide = editCardFrontText;
+ _editCard = editCard;
+ _deleteCardById = deleteCardById;
+ }
+
+ public void Run(StackNamesWithCountsResponse stack)
+ {
+ while (true)
+ {
+ var fullStack = BuildFullStack(stack);
+
+ _output.PrintPageTitle($"REVIEWING STACK: {fullStack.Name}");
+
+ _cardListView.Render(fullStack.Cards);
+
+ ReviewStackMenuItem[] menuItems = Enum.GetValues();
+
+ if (fullStack.Cards.Count <= 0) menuItems = new ReviewStackMenuItem[2] { ReviewStackMenuItem.AddCard, ReviewStackMenuItem.Return };
+
+ var selection = _menu.Render(menuItems);
+
+ switch (selection)
+ {
+ case ReviewStackMenuItem.ReviewCards: HandleReviewCards(fullStack.Cards); break;
+ case ReviewStackMenuItem.AddCard: HandleAddCard(fullStack); break;
+ case ReviewStackMenuItem.EditCard: HandleEditCard(fullStack); break;
+ case ReviewStackMenuItem.DeleteCard: HandleDeleteCard(fullStack.Cards); break;
+ case ReviewStackMenuItem.Return: return;
+ default: AnsiConsole.Markup("[bold red]ERROR:[/] Invalid input!"); break;
+ }
+ }
+ }
+ private StackViewModel BuildFullStack(StackNamesWithCountsResponse stack)
+ {
+ var cards = GetCards(stack.Id);
+
+ return new StackViewModel(stack.Id, stack.Name, cards);
+ }
+ private List GetCards(int stackId)
+ {
+ var result = _getAllCardsByStackId.Handle(stackId);
+
+ if (result.IsFailure)
+ {
+ _output.PrintValidationErrorsFromCollection(result.Errors);
+ return new List();
+ }
+ else
+ {
+ return result.Value;
+ }
+ }
+ private void HandleReviewCards(List cards)
+ {
+ var shuffleableCards = cards;
+ bool continueReview = true;
+ ConsoleKeyInfo keyInfo;
+ int i = 0;
+
+ while (continueReview)
+ {
+ _output.PrintPageTitle("REVIEW STACK MENU");
+
+ _output.PrintCardTextInPanel(shuffleableCards[i].FrontText);
+ _input.PressAnyKeyToFlipCard();
+
+ _output.PrintPageTitle("REVIEW STACK MENU");
+ _output.PrintCardTextInSideBySidePanels(shuffleableCards[i].FrontText, shuffleableCards[i].BackText);
+
+ _output.PrintReviewCardsKeypressOptions(CardSide.Back, i, shuffleableCards.Count);
+
+ bool validKey = false;
+
+ while (validKey == false)
+ {
+ keyInfo = Console.ReadKey(true);
+
+ switch (keyInfo.Key)
+ {
+ case ConsoleKey.A:
+ if (i > 0) i--;
+ else i = shuffleableCards.Count - 1;
+ validKey = true;
+ break;
+ case ConsoleKey.D:
+ if (i < shuffleableCards.Count - 1) i++;
+ else i = 0;
+ validKey = true;
+ break;
+ case ConsoleKey.W:
+ shuffleableCards = ShuffleStack(shuffleableCards);
+ i = 0;
+ validKey = true;
+ break;
+ case ConsoleKey.Q:
+ validKey = true;
+ continueReview = false;
+ break;
+ default:
+ Console.WriteLine($"Invalid keypress -- see options");
+ break;
+ }
+ }
+ }
+ }
+ private List ShuffleStack(List cards)
+ {
+ CardResponse[] cardsArray = cards.ToArray();
+ Random.Shared.Shuffle(cardsArray);
+ return cardsArray.ToList();
+ }
+ private void HandleAddCard(StackViewModel fullStack)
+ {
+ var card = new AddCardCommand(fullStack.StackId,
+ GetCardText(fullStack.StackId, CardSide.Front),
+ GetCardText(fullStack.StackId, CardSide.Back));
+
+ var result = _addCard.Handle(card);
+
+ if (result.IsSuccess) _output.PrintSuccessMessage($"Added card to {fullStack.Name}!");
+ else Console.WriteLine("ERROR MESSAGE");
+ _input.PressAnyKeyToContinue(2);
+ }
+ private string GetCardText(int stackId, CardSide cardSide)
+ {
+ bool textValid = false;
+ var output = string.Empty;
+
+ while (textValid == false)
+ {
+ var cardData = new ValidateCardTextBySideCommand(stackId,
+ _input.GetTextInputFromUser($"Enter {cardSide} text"),
+ cardSide);
+
+ var result = _getCardText.Handle(cardData);
+
+ if (result.IsFailure) _output.PrintValidationErrorsFromCollection(result.Errors);
+
+ else
+ {
+ output = result.Value;
+ textValid = true;
+ }
+ }
+ return output;
+ }
+ private void HandleDeleteCard(List cards)
+ {
+ var message = "Please enter the [yellow]ID[/] of the card you wish to delete:";
+ var card = cards[_input.GetRecordIdFromUser(message, 1, cards.Count) - 1];
+
+ if (_input.GetDeleteCardConfirmationFromUser(card.FrontText, card.BackText))
+ {
+ _deleteCardById.Handle(card.Id);
+ _output.PrintSuccessMessage($"Deleted [yellow]{card.FrontText}[/] card!");
+ }
+ else _output.PrintCancellationMessage("Deletion", "card");
+ _input.PressAnyKeyToContinue(2);
+ }
+ private void HandleEditCard(StackViewModel stack)
+ {
+ var message = "Please enter the [yellow]ID[/] of the card you wish to edit:";
+ var cardIndex = _input.GetRecordIdFromUser(message, 1, stack.Cards.Count) - 1;
+ var originalCard = stack.Cards[cardIndex];
+
+ var editedCard = new EditCardCommand(stack.StackId,
+ GetEditedTextFromUser(originalCard, CardSide.Front),
+ GetEditedTextFromUser(originalCard, CardSide.Back));
+
+
+ if ((originalCard.FrontText == editedCard.FrontText) && (originalCard.BackText == editedCard.BackText))
+ {
+ _output.PrintNoEditsMadeMessage();
+ return;
+ }
+
+ bool confirmEdit = _input.GetEditCardConfirmationFromUser(originalCard.FrontText, originalCard.BackText, editedCard.FrontText, editedCard.BackText);
+
+ if (confirmEdit)
+ {
+ _editCard.Handle(originalCard, editedCard);
+ _output.PrintSuccessMessage("Edited card data!");
+ }
+ else _output.PrintCancellationMessage("editing", "card text");
+ _input.PressAnyKeyToContinue(2);
+ }
+ private string GetEditedTextFromUser(CardResponse card, CardSide cardSide)
+ {
+ bool textValid = false;
+ var textInput = string.Empty;
+
+ var currentCardText = (cardSide == CardSide.Front) ? $"{card.FrontText}" : $"{card.BackText}";
+ var promptText = $"Original Card {cardSide} Text: [green]{currentCardText}[/]\r\nEnter new text or leave blank to keep original";
+
+ while (textValid == false)
+ {
+ textInput = _input.GetTextInputFromUser(promptText, 1);
+
+ var editedCardSide = new EditCardTextBySideCommand(textInput, cardSide);
+
+ var result = _editCardTextBySide.Handle(card, editedCardSide);
+
+ if (!result.IsSuccess)
+ {
+ foreach (var error in result.Errors)
+ {
+ AnsiConsole.WriteLine(error.Description);
+ }
+ }
+ textValid = result.IsSuccess;
+ textInput = result.Value;
+ }
+ return textInput;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/StudySessionService.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/StudySessionService.cs
new file mode 100644
index 00000000..ec092575
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Services/StudySessionService.cs
@@ -0,0 +1,106 @@
+using FlashCards.Application.Cards;
+using FlashCards.Application.Cards.GetAllByStackId;
+using FlashCards.Application.Cards.UpdateCardCounters;
+using FlashCards.Application.Stacks.GetAll;
+using FlashCards.Application.StudySessions.Add;
+using FlashCards.ConsoleUI.Input;
+using FlashCards.ConsoleUI.Output;
+using FlashCards.ConsoleUI.Views;
+
+namespace FlashCards.ConsoleUI.Handlers;
+
+public class StudySessionService
+{
+ private readonly IServiceProvider _provider;
+ private readonly ConsoleInput _input;
+ private readonly ConsoleOutput _output;
+
+ private readonly GetAllByStackId _getAllByStackId;
+ private readonly AddStudySessionHandler _addStudySessionHandler;
+ private readonly UpdateCardCountersHandler _updateCardCounterHandler;
+
+ private readonly StudySessionView _studySession;
+
+ public StudySessionService(IServiceProvider provider, ConsoleInput input, ConsoleOutput output,
+ GetAllByStackId getAllByStackId, AddStudySessionHandler addStudySessionHandler,
+ UpdateCardCountersHandler updateCardCounterHandler, StudySessionView studySession)
+ {
+ _provider = provider;
+ _input = input;
+ _output = output;
+ _getAllByStackId = getAllByStackId;
+ _addStudySessionHandler = addStudySessionHandler;
+ _updateCardCounterHandler = updateCardCounterHandler;
+ _studySession = studySession;
+ }
+
+ public void Run(StackNamesWithCountsResponse stack)
+ {
+ _output.PrintPageTitle("STUDY MODE");
+
+ var cardsCorrect = new List();
+ var cardsIncorrect = new List();
+
+
+ var result = _getAllByStackId.Handle(stack.Id);
+
+ if (result.IsFailure)
+ {
+ _output.PrintValidationErrorsFromCollection(result.Errors);
+
+ }
+ else
+ {
+ var cards = result.Value;
+
+ cards = ShuffleStack(cards);
+
+ var session = StudyCards(stack.Id, stack.Name, cards, cardsCorrect, cardsIncorrect);
+ _addStudySessionHandler.Handle(session);
+ _updateCardCounterHandler.Handle(cardsCorrect, cardsIncorrect);
+ _studySession.Render(session);
+ }
+
+ _input.PressAnyKeyToContinue();
+ }
+
+ private AddStudySessionCommand StudyCards(int stackId, string stackName, List cards, List cardsCorrect, List cardsIncorrect)
+ {
+ int cardsStudied = 0;
+
+ foreach (var card in cards)
+ {
+ _output.PrintPageTitle("STUDY MODE");
+
+ _output.PrintCardTextInPanel(card.FrontText);
+ _input.PressAnyKeyToContinue(1, "Press any key to flip card...");
+ _output.PrintCardTextInPanel(card.BackText);
+ bool correctAnswer = _input.GetPassStateFromUser();
+
+ if (correctAnswer)
+ {
+ Console.WriteLine("Congratulations!");
+ cardsCorrect.Add(card.Id);
+ }
+ else
+ {
+ Console.WriteLine("Too bad! Keep studying!");
+ cardsIncorrect.Add(card.Id);
+ }
+
+ cardsStudied++;
+ if (cardsStudied == cards.Count) break;
+ if (_input.ContinueStudyMode() == false) break;
+ }
+
+ double score = (double)cardsCorrect.Count / cardsStudied * 100;
+ return new AddStudySessionCommand(stackId, stackName, DateTime.Now, score, cardsStudied, cardsCorrect.Count, cardsIncorrect.Count);
+ }
+
+ private List ShuffleStack(List cards)
+ {
+ CardResponse[] cardsArray = cards.ToArray();
+ Random.Shared.Shuffle(cardsArray);
+ return cardsArray.ToList();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs
new file mode 100644
index 00000000..fd99f93d
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs
@@ -0,0 +1,60 @@
+using FlashCards.Application.Reports.GetAverageScorePerMonth;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class AverageScorePerMonthView
+{
+ public void Render(List reports, int year)
+ {
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Stack Name", col => col.NoWrap().LeftAligned());
+ table.AddColumn("Jan", col => col.Width(5).Centered());
+ table.AddColumn("Feb", col => col.Width(5).Centered());
+ table.AddColumn("Mar", col => col.Width(5).Centered());
+ table.AddColumn("Apr", col => col.Width(5).Centered());
+ table.AddColumn("May", col => col.Width(5).Centered());
+ table.AddColumn("Jun", col => col.Width(5).Centered());
+ table.AddColumn("Jul", col => col.Width(5).Centered());
+ table.AddColumn("Aug", col => col.Width(5).Centered());
+ table.AddColumn("Sep", col => col.Width(5).Centered());
+ table.AddColumn("Oct", col => col.Width(5).Centered());
+ table.AddColumn("Nov", col => col.Width(5).Centered());
+ table.AddColumn("Dec", col => col.Width(5).Centered());
+ table.AddColumn("Annual");
+ table.Title = new TableTitle($"Average Score By Month for Year: {year}");
+
+
+ foreach (var report in reports)
+ {
+ table.AddRow(report.StackName,
+ report.January.ToString("f1"),
+ report.February.ToString("f1"),
+ report.March.ToString("f1"),
+ report.April.ToString("f1"),
+ report.May.ToString("f1"),
+ report.June.ToString("f1"),
+ report.July.ToString("f1"),
+ report.August.ToString("f1"),
+ report.September.ToString("f1"),
+ report.October.ToString("f1"),
+ report.November.ToString("f1"),
+ report.December.ToString("f1"),
+ AnnualSum(report));
+ }
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+
+ private string AnnualSum(GetAverageScorePerMonthResponse report)
+ {
+ return ((double)(report.January + report.February + report.March + report.April + report.May + report.June + report.July
+ + report.August + report.September + report.October + report.November + report.December) / 12).ToString("f1");
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/CardListView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/CardListView.cs
new file mode 100644
index 00000000..4d6c1a55
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/CardListView.cs
@@ -0,0 +1,36 @@
+using FlashCards.Application.Cards;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class CardListView
+{
+ public void Render(CardResponse card)
+ {
+ Render(new List() { card });
+ }
+ public void Render(List cards)
+ {
+ int i = 1;
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Id");
+ table.AddColumn("Front Text");
+ table.AddColumn("Back Text");
+
+ if (cards.Count == 0) table.AddRow("", "", "");
+
+ foreach (var card in cards)
+ {
+ table.AddRow(i.ToString(), card.FrontText, card.BackText);
+ i++;
+ }
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/MainMenuView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/MainMenuView.cs
new file mode 100644
index 00000000..b82839f3
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/MainMenuView.cs
@@ -0,0 +1,26 @@
+using FlashCards.ConsoleUI.Enums;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class MainMenuView
+{
+ public MainMenuItem Render(MainMenuItem[] menuItems)
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select from the options below:")
+ .UseConverter(menu => menu switch
+ {
+ MainMenuItem.ReviewStack => "Review Cards in Stack",
+ MainMenuItem.CreateStack => "Create New Stack",
+ MainMenuItem.DeleteStack => "Delete Stack",
+ MainMenuItem.StudyStack => "Begin Study Session",
+ MainMenuItem.ViewPastSessions => "View Past Study Sessions",
+ MainMenuItem.Report => "View Reports",
+ MainMenuItem.Exit => "Exit",
+ _ => menu.ToString()
+ })
+ .AddChoices(menuItems));
+ }
+}
\ No newline at end of file
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/ReviewStackMenuView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/ReviewStackMenuView.cs
new file mode 100644
index 00000000..932f687f
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/ReviewStackMenuView.cs
@@ -0,0 +1,24 @@
+using FlashCards.ConsoleUI.Enums;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class ReviewStackMenuView
+{
+ public ReviewStackMenuItem Render(ReviewStackMenuItem[] menuItems)
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select from the options below:")
+ .UseConverter(menu => menu switch
+ {
+ ReviewStackMenuItem.ReviewCards => "Review Cards in Stack",
+ ReviewStackMenuItem.AddCard => "Add Card to Stack",
+ ReviewStackMenuItem.EditCard => "Edit Card Text",
+ ReviewStackMenuItem.DeleteCard => "Delete Card from Stack",
+ ReviewStackMenuItem.Return => "Return to Main Menu",
+ _ => menu.ToString()
+ })
+ .AddChoices(menuItems));
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs
new file mode 100644
index 00000000..ef420ae9
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs
@@ -0,0 +1,60 @@
+using FlashCards.Application.Reports.GetSessionCountPerMonth;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class SessionCountPerMonthView
+{
+ public void Render(List reports, int year)
+ {
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Stack Name", col => col.NoWrap().LeftAligned());
+ table.AddColumn("Jan", col => col.Width(5).Centered());
+ table.AddColumn("Feb", col => col.Width(5).Centered());
+ table.AddColumn("Mar", col => col.Width(5).Centered());
+ table.AddColumn("Apr", col => col.Width(5).Centered());
+ table.AddColumn("May", col => col.Width(5).Centered());
+ table.AddColumn("Jun", col => col.Width(5).Centered());
+ table.AddColumn("Jul", col => col.Width(5).Centered());
+ table.AddColumn("Aug", col => col.Width(5).Centered());
+ table.AddColumn("Sep", col => col.Width(5).Centered());
+ table.AddColumn("Oct", col => col.Width(5).Centered());
+ table.AddColumn("Nov", col => col.Width(5).Centered());
+ table.AddColumn("Dec", col => col.Width(5).Centered());
+ table.AddColumn("Annual");
+ table.Title = new TableTitle($"Count of Sessions Per Month for Year: {year}");
+
+
+ foreach (var report in reports)
+ {
+ table.AddRow(report.StackName,
+ report.January.ToString(),
+ report.February.ToString(),
+ report.March.ToString(),
+ report.April.ToString(),
+ report.May.ToString(),
+ report.June.ToString(),
+ report.July.ToString(),
+ report.August.ToString(),
+ report.September.ToString(),
+ report.October.ToString(),
+ report.November.ToString(),
+ report.December.ToString(),
+ AnnualSum(report));
+ }
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+
+ private string AnnualSum(GetSessionCountPerMonthResponse report)
+ {
+ return ((double)(report.January + report.February + report.March + report.April + report.May + report.June + report.July
+ + report.August + report.September + report.October + report.November + report.December)).ToString();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionYearSelectionView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionYearSelectionView.cs
new file mode 100644
index 00000000..5601db05
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionYearSelectionView.cs
@@ -0,0 +1,14 @@
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class SessionYearSelectionView
+{
+ public int Render(int[] sessionYear)
+ {
+ return AnsiConsole.Prompt(
+ new SelectionPrompt()
+ .Title("Select the year to view a report:")
+ .AddChoices(sessionYear));
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StackListView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StackListView.cs
new file mode 100644
index 00000000..4e681cb6
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StackListView.cs
@@ -0,0 +1,38 @@
+using FlashCards.Application.Stacks.GetAll;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class StackListView
+{
+ public void Render(StackNamesWithCountsResponse stack)
+ {
+ Render(new List() { stack });
+ }
+ public void Render(List stacks)
+ {
+ int i = 1;
+ var table = new Table()
+ .RoundedBorder()
+ .BorderColor(Color.Blue)
+ .ShowRowSeparators();
+
+ table.AddColumn("Id");
+ table.AddColumn("Stack Name");
+ table.AddColumn("Card Count");
+ table.AddColumn("Session Count");
+
+
+ if (stacks.Count == 0) table.AddRow("", "", "", "");
+
+ foreach (var stack in stacks)
+ {
+ table.AddRow(i.ToString(), stack.Name, stack.CardCount.ToString(), stack.SessionCount.ToString());
+ i++;
+ }
+
+ AnsiConsole.Write(table);
+
+ Console.WriteLine();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionListView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionListView.cs
new file mode 100644
index 00000000..e521b656
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionListView.cs
@@ -0,0 +1,42 @@
+using FlashCards.Application.StudySessions.GetAll;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class StudySessionListView
+{
+ public void Render(List sessions, int startIndex)
+ {
+ var table = new Table();
+
+ table.AddColumn("Id");
+ table.AddColumn("Stack Name");
+ table.AddColumn("Score");
+ table.AddColumn("Time");
+ table.AddColumn("# Studied");
+ table.AddColumn("# Correct");
+ table.AddColumn("# Incorrect");
+
+ for (int i = startIndex; i < startIndex + 15; i++)
+ {
+ if (i < sessions.Count - 1)
+ {
+ table.AddRow((i + 1).ToString(),
+ sessions[i].StackName,
+ sessions[i].Score.ToString("F1"),
+ sessions[i].SessionDate.ToString("yyyy-MM-dd HH:mm"),
+ sessions[i].CountStudied.ToString(),
+ sessions[i].CountCorrect.ToString(),
+ sessions[i].CountIncorrect.ToString());
+ }
+ else
+ {
+ table.AddRow("", "", "", "", "", "", "");
+ }
+ }
+
+ AnsiConsole.WriteLine("SESSION RESULTS:");
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(table);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionView.cs
new file mode 100644
index 00000000..447a74cf
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionView.cs
@@ -0,0 +1,26 @@
+using FlashCards.Application.StudySessions.Add;
+using Spectre.Console;
+
+namespace FlashCards.ConsoleUI.Views;
+
+public class StudySessionView
+{
+ public void Render(AddStudySessionCommand session)
+ {
+ var table = new Table();
+
+ table.AddColumn("Stack Name");
+ table.AddColumn("Score");
+ table.AddColumn("Time");
+ table.AddColumn("# Studied");
+ table.AddColumn("# Correct");
+ table.AddColumn("# Incorrect");
+
+ table.AddRow(session.StackName, session.Score.ToString("F1"), session.SessionDate.ToString("yyyy-MM-dd HH:mm"), session.CardsStudied.ToString(),
+ session.CardsCorrect.ToString(), session.CardsIncorrect.ToString());
+
+ AnsiConsole.WriteLine("SESSION RESULTS:");
+ AnsiConsole.WriteLine();
+ AnsiConsole.Write(table);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Entities/Card.cs b/jzhartman.FlashCards/FlashCards.Core/Entities/Card.cs
new file mode 100644
index 00000000..d64a3b01
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Entities/Card.cs
@@ -0,0 +1,29 @@
+namespace FlashCards.Core.Entities
+{
+ public class Card
+ {
+ public int Id { get; set; }
+ public int StackId { get; set; }
+ public string FrontText { get; set; }
+ public string BackText { get; set; }
+ public Stack Stack { get; set; }
+ public int TimesIncorrect { get; set; }
+ public int TimesCorrect { get; set; }
+ public int TimesStudied { get; set; }
+
+
+ public Card() { }
+
+ public Card(int stackId, string frontText, string backText)
+ {
+ StackId = stackId;
+ FrontText = frontText;
+ BackText = backText;
+ }
+
+ public void SetId(int id)
+ {
+ Id = id;
+ }
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Entities/SessionReport.cs b/jzhartman.FlashCards/FlashCards.Core/Entities/SessionReport.cs
new file mode 100644
index 00000000..8d4579be
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Entities/SessionReport.cs
@@ -0,0 +1,20 @@
+namespace FlashCards.Core.Entities;
+
+public class SessionReport
+{
+ public int StackId { get; set; }
+ public string StackName { get; set; }
+ public string SessionYear { get; set; }
+ public double January { get; set; }
+ public double February { get; set; }
+ public double March { get; set; }
+ public double April { get; set; }
+ public double May { get; set; }
+ public double June { get; set; }
+ public double July { get; set; }
+ public double August { get; set; }
+ public double September { get; set; }
+ public double October { get; set; }
+ public double November { get; set; }
+ public double December { get; set; }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Entities/Stack.cs b/jzhartman.FlashCards/FlashCards.Core/Entities/Stack.cs
new file mode 100644
index 00000000..a5641e76
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Entities/Stack.cs
@@ -0,0 +1,25 @@
+namespace FlashCards.Core.Entities;
+
+public class Stack
+{
+ public int Id { get; set; }
+ public string Name { get; set; }
+ public List Cards { get; set; }
+
+ public Stack()
+ {
+
+ }
+
+ public Stack(int id, string name)
+ {
+ Id = id;
+ Name = name;
+ }
+ public Stack(int id, string name, List cards)
+ {
+ Id = id;
+ Name = name;
+ Cards = cards;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Entities/StudySession.cs b/jzhartman.FlashCards/FlashCards.Core/Entities/StudySession.cs
new file mode 100644
index 00000000..bfafa857
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Entities/StudySession.cs
@@ -0,0 +1,12 @@
+namespace FlashCards.Core.Entities;
+
+public class StudySession
+{
+ public int Id { get; set; }
+ public DateTime SessionDate { get; set; }
+ public int StackId { get; set; }
+ public double Score { get; set; }
+ public int CountStudied { get; set; }
+ public int CountCorrect { get; set; }
+ public int CountIncorrect { get; set; }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/FlashCards.Core.csproj b/jzhartman.FlashCards/FlashCards.Core/FlashCards.Core.csproj
new file mode 100644
index 00000000..b7601447
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/FlashCards.Core.csproj
@@ -0,0 +1,9 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
diff --git a/jzhartman.FlashCards/FlashCards.Core/Validation/Error.cs b/jzhartman.FlashCards/FlashCards.Core/Validation/Error.cs
new file mode 100644
index 00000000..0797025a
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Validation/Error.cs
@@ -0,0 +1,6 @@
+namespace FlashCards.Core.Validation;
+
+public sealed record Error(string Code, string Description)
+{
+ public static readonly Error None = new(string.Empty, string.Empty);
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Validation/Errors.cs b/jzhartman.FlashCards/FlashCards.Core/Validation/Errors.cs
new file mode 100644
index 00000000..fbb15b15
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Validation/Errors.cs
@@ -0,0 +1,32 @@
+namespace FlashCards.Core.Validation;
+
+public static class Errors
+{
+ public static readonly Error None = Error.None;
+
+ public static readonly Error StackNameRequired = new("StackNameRequired", "Stack name cannot be blank.");
+
+ public static readonly Error StackNameExists = new("StackNameExists", "A stack with that name already exists.");
+
+ public static readonly Error NoStacksExist = new("NoStacksExist", "No stacks exist.");
+
+ public static readonly Error NoCardsExist = new("NoCardsExist", "No cards exist in the current stack.");
+
+ public static readonly Error CardFrontTextExists = new("CardFrontTextExists", "A card with that front text already exists.");
+
+ public static readonly Error CardBackTextExists = new("CardBackTextExists", "A card with that back text already exists.");
+
+ public static readonly Error CardFrontTextRequired = new("CardFrontRequired", "Card front text cannot be blank.");
+
+ public static readonly Error CardBackTextRequired = new("CardBackRequired", "Card back text cannot be blank.");
+
+ public static readonly Error StackEmpty = new("StackEmpty", "The selected stack contains no cards!");
+
+ public static readonly Error InvalidId = new("InvalidId", "Id value is invalid!");
+
+ public static readonly Error StackNameTooLong = new("StackNameTooLong", "The stack name cannot exceed 25 characters!");
+
+ public static readonly Error NoStudySessions = new("NoStudySession", "No study sessions exist for the current selection!");
+
+ public static readonly Error CardTextLengthTooLong = new("CardTextLengthTooLong", "Card text cannot exceed 250 characters!");
+}
diff --git a/jzhartman.FlashCards/FlashCards.Core/Validation/Result.cs b/jzhartman.FlashCards/FlashCards.Core/Validation/Result.cs
new file mode 100644
index 00000000..c6033535
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Core/Validation/Result.cs
@@ -0,0 +1,19 @@
+namespace FlashCards.Core.Validation;
+
+public record Result
+{
+ public bool IsSuccess { get; }
+ public bool IsFailure => !IsSuccess;
+ public T? Value { get; }
+ public List Errors { get; }
+
+ private Result(bool isSuccess, T? value, List errors)
+ {
+ IsSuccess = isSuccess;
+ Value = value;
+ Errors = errors;
+ }
+
+ public static Result Success(T value) => new Result(true, value, new List());
+ public static Result Failure(params Error[] errors) => new Result(false, default, errors.ToList());
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/DapperWrapper.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/DapperWrapper.cs
new file mode 100644
index 00000000..ad38d89b
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/DapperWrapper.cs
@@ -0,0 +1,32 @@
+using Dapper;
+using System.Data;
+
+namespace FlashCards.Infrastructure.Dapper;
+
+public class DapperWrapper : IDapperWrapper
+{
+ public void Execute(IDbConnection connection, string sql, object parameters = null)
+ {
+ connection.Execute(sql, parameters);
+ }
+
+ public IEnumerable Query(IDbConnection connection, string sql, object parameters = null)
+ {
+ return connection.Query(sql, parameters);
+ }
+
+ public IEnumerable Query(
+ IDbConnection connection,
+ string sql,
+ Func map,
+ object parameters = null,
+ string splitOn = "Id")
+ {
+ return connection.Query(sql, map, parameters, splitOn: splitOn);
+ }
+
+ public T QuerySingle(IDbConnection connection, string sql, object parameters = null)
+ {
+ return connection.QuerySingle(sql, parameters);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/IDapperWrapper.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/IDapperWrapper.cs
new file mode 100644
index 00000000..808a6a10
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/IDapperWrapper.cs
@@ -0,0 +1,19 @@
+using System.Data;
+
+namespace FlashCards.Infrastructure.Dapper;
+
+public interface IDapperWrapper
+{
+ T QuerySingle(IDbConnection connection, string sql, object parameters = null);
+
+ IEnumerable Query(IDbConnection connection, string sql, object parameters = null);
+
+ IEnumerable Query(
+ IDbConnection connection,
+ string sql,
+ Func map,
+ object parameters = null,
+ string splitOn = "Id");
+
+ void Execute(IDbConnection connection, string sql, object parameters = null);
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/DependencyInjection.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/DependencyInjection.cs
new file mode 100644
index 00000000..4d570512
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/DependencyInjection.cs
@@ -0,0 +1,34 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Infrastructure.Dapper;
+using FlashCards.Infrastructure.Initialization;
+using FlashCards.Infrastructure.Repositories;
+using Microsoft.Data.SqlClient;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using System.Data;
+
+namespace FlashCards.Infrastructure;
+
+public static class DependencyInjection
+{
+ public static IServiceCollection AddInfrastructure(this IServiceCollection services,
+ IConfiguration configuration)
+ {
+ var connectionString = configuration.GetConnectionString("Default");
+
+ // Register IDbConnection Factory
+ services.AddScoped(sp => new SqlConnection(connectionString));
+
+ services.AddScoped(sp => new DbInitializer(connectionString));
+
+ // Register Dapper Wrapper
+ services.AddScoped();
+
+ // Register all repositories
+ services.AddScoped();
+ services.AddScoped();
+ services.AddScoped();
+
+ return services;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/FlashCards.Infrastructure.csproj b/jzhartman.FlashCards/FlashCards.Infrastructure/FlashCards.Infrastructure.csproj
new file mode 100644
index 00000000..b1c4b9e5
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/FlashCards.Infrastructure.csproj
@@ -0,0 +1,21 @@
+
+
+
+ net10.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Initialization/DbInitializer.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Initialization/DbInitializer.cs
new file mode 100644
index 00000000..1776dd44
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Initialization/DbInitializer.cs
@@ -0,0 +1,192 @@
+using Dapper;
+using Microsoft.Data.SqlClient;
+
+namespace FlashCards.Infrastructure.Initialization;
+
+public class DbInitializer
+{
+ private readonly string _connectionString;
+
+ public DbInitializer(string connectionString)
+ {
+ _connectionString = connectionString;
+ }
+
+ public void Initialize()
+ {
+ CreateDatabaseIfNotExists();
+ CreateStackTableIfNotExists();
+ CreateCardTableIfNotExists();
+ CreateStudySessionTableIfNotExists();
+ SeedTablesIfStacksNotExist();
+ }
+
+ private void CreateDatabaseIfNotExists()
+ {
+ var builder = new SqlConnectionStringBuilder(_connectionString); // Parses connection string into useful properties
+ var databaseName = builder.InitialCatalog; // Sets databaseName to the InitialCatalog property in the connection string
+
+ builder.InitialCatalog = "master"; // Resets to master because it can't create a database from within the database to create
+
+ using var connection = new SqlConnection(builder.ConnectionString);
+ connection.Open();
+
+ var sql = $@"
+ IF NOT EXISTS (SELECT name FROM sys.databases WHERE name = '{databaseName}')
+ BEGIN
+ CREATE DATABASE [{databaseName}];
+ END";
+
+ connection.Execute(sql);
+ }
+
+ private void CreateStackTableIfNotExists()
+ {
+ using var connection = new SqlConnection(_connectionString);
+ connection.Open();
+
+ var sql = @"
+ IF NOT EXISTS (SELECT * FROM sysobjects WHERE name='Stack' AND xType='U')
+ CREATE TABLE Stack (
+ Id INT NOT NULL PRIMARY KEY IDENTITY,
+ Name NVARCHAR(32) NOT NULL
+ );";
+
+ connection.Execute(sql);
+ }
+
+ private void CreateCardTableIfNotExists()
+ {
+ using var connection = new SqlConnection(_connectionString);
+ connection.Open();
+
+ var sql = @"
+ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='Card')
+ CREATE TABLE Card (
+ Id INT NOT NULL PRIMARY KEY IDENTITY,
+ StackId INT NOT NULL,
+ FrontText NVARCHAR(250) NOT NULL,
+ BackText NVARCHAR(250) NOT NULL,
+ TimesStudied INT NOT NULL DEFAULT 0,
+ TimesCorrect INT NOT NULL DEFAULT 0,
+ TimesIncorrect INT NOT NULL DEFAULT 0,
+ FOREIGN KEY (StackId) REFERENCES Stack(Id)
+ );";
+
+ connection.Execute(sql);
+ }
+
+ private void CreateStudySessionTableIfNotExists()
+ {
+ using var connection = new SqlConnection(_connectionString);
+ connection.Open();
+
+ var sql = @"
+ IF NOT EXISTS (SELECT * FROM sys.tables WHERE name='StudySession')
+ CREATE TABLE StudySession (
+ Id INT NOT NULL PRIMARY KEY IDENTITY,
+ SessionDate DATETIME NOT NULL,
+ StackId INT NOT NULL,
+ Score FLOAT NOT NULL,
+ CountStudied INT NOT NULL,
+ CountCorrect INT NOT NULL,
+ CountIncorrect INT NOT NULL,
+ FOREIGN KEY (StackId) REFERENCES Stack(Id)
+ );";
+
+ connection.Execute(sql);
+ }
+
+ private void SeedTablesIfStacksNotExist()
+ {
+ using var connection = new SqlConnection(_connectionString);
+ connection.Open();
+
+ var seed = @"
+ IF NOT EXISTS (SELECT 1 FROM Stack)
+ BEGIN
+ INSERT INTO Stack (Name)
+ VALUES ('Pasta');
+
+ DECLARE @PastaStackId INT = SCOPE_IDENTITY();
+
+ INSERT INTO Stack (Name)
+ VALUES ('Famous People');
+
+ DECLARE @PeopleStackId INT = SCOPE_IDENTITY();
+
+ INSERT INTO Card (StackId, FrontText, BackText)
+ VALUES (@PastaStackId, 'Elbows', 'Better than knees'),
+ (@PastaStackId, 'Shells', 'Noodles or snail houses'),
+ (@PastaStackId, 'Lasagna', 'Noodle blanket'),
+ (@PastaStackId, 'Rotini', 'Twirly noodle'),
+ (@PastaStackId, 'Ziti', 'Doubles as a straw'),
+ (@PastaStackId, 'Penne', 'Sharp ziti'),
+ (@PastaStackId, 'Spaghetti', 'Thin and wiry'),
+ (@PastaStackId, 'Angel Hair', 'Spaghetti with anorexia'),
+ (@PastaStackId, 'Linguine', 'Spaghetti that ate too much... spaghetti?'),
+ (@PastaStackId, 'Orzo', 'Admit it, this is just rice, people...'),
+ (@PeopleStackId, 'Gandalf', 'Elderly fellow, big gray beard, pointy hat'),
+ (@PeopleStackId, 'Strider', 'This is no mere ranger. He is Aragorn, son of Arathorn. You owe him your allegience'),
+ (@PeopleStackId, 'Samwise', 'The Brave');
+
+ INSERT INTO StudySession (SessionDate, StackId, Score, CountStudied, CountCorrect, CountIncorrect)
+ VALUES ('2026-01-01 11:00:00', @PastaStackId, 100, 10, 10, 0),
+ ('2026-01-02 15:00:00', @PastaStackId, 70, 10, 7, 3),
+ ('2026-01-03 21:00:00', @PastaStackId, 80, 5, 4, 1),
+ ('2026-01-05 11:00:00', @PeopleStackId, 100, 3, 3, 0),
+
+ ('2025-01-01 7:00:00', @PeopleStackId, 80, 10, 8, 2),
+ ('2025-01-02 7:00:00', @PeopleStackId, 60, 10, 6, 4),
+ ('2025-02-01 7:00:00', @PeopleStackId, 90, 10, 9, 1),
+ ('2025-02-02 7:00:00', @PeopleStackId, 30, 10, 3, 7),
+ ('2025-03-01 7:00:00', @PeopleStackId, 40, 10, 4, 6),
+ ('2025-03-02 7:00:00', @PeopleStackId, 50, 10, 5, 5),
+ ('2025-04-01 7:00:00', @PeopleStackId, 60, 10, 6, 4),
+ ('2025-04-02 7:00:00', @PeopleStackId, 70, 10, 7, 3),
+ ('2025-05-01 7:00:00', @PeopleStackId, 80, 10, 8, 2),
+ ('2025-05-02 7:00:00', @PeopleStackId, 90, 10, 9, 1),
+ ('2025-06-01 7:00:00', @PeopleStackId, 100, 10, 10, 0),
+ ('2025-06-02 7:00:00', @PeopleStackId, 90, 10, 9, 1),
+ ('2025-07-01 7:00:00', @PeopleStackId, 80, 10, 8, 2),
+ ('2025-07-02 7:00:00', @PeopleStackId, 70, 10, 7, 3),
+ ('2025-08-01 7:00:00', @PeopleStackId, 60, 10, 6, 4),
+ ('2025-08-02 7:00:00', @PeopleStackId, 50, 10, 5, 5),
+ ('2025-09-01 7:00:00', @PeopleStackId, 40, 10, 4, 6),
+ ('2025-09-02 7:00:00', @PeopleStackId, 50, 10, 5, 5),
+ ('2025-10-01 7:00:00', @PeopleStackId, 60, 10, 6, 4),
+ ('2025-10-02 7:00:00', @PeopleStackId, 70, 10, 7, 3),
+ ('2025-11-01 7:00:00', @PeopleStackId, 80, 10, 8, 2),
+ ('2025-11-02 7:00:00', @PeopleStackId, 90, 10, 9, 1),
+ ('2025-12-01 7:00:00', @PeopleStackId, 100, 10, 10, 0),
+ ('2025-12-02 7:00:00', @PeopleStackId, 90, 10, 9, 1),
+
+ ('2025-01-01 7:00:00', @PastaStackId, 80, 10, 8, 2),
+ ('2025-01-02 7:00:00', @PastaStackId, 60, 10, 6, 4),
+ ('2025-02-01 7:00:00', @PastaStackId, 90, 10, 9, 1),
+ ('2025-02-02 7:00:00', @PastaStackId, 30, 10, 3, 7),
+ ('2025-03-01 7:00:00', @PastaStackId, 40, 10, 4, 6),
+ ('2025-03-02 7:00:00', @PastaStackId, 50, 10, 5, 5),
+ ('2025-04-01 7:00:00', @PastaStackId, 60, 10, 6, 4),
+ ('2025-04-02 7:00:00', @PastaStackId, 70, 10, 7, 3),
+ ('2025-05-01 7:00:00', @PastaStackId, 80, 10, 8, 2),
+ ('2025-05-02 7:00:00', @PastaStackId, 90, 10, 9, 1),
+ ('2025-06-01 7:00:00', @PastaStackId, 100, 10, 10, 0),
+ ('2025-06-02 7:00:00', @PastaStackId, 90, 10, 9, 1),
+ ('2025-07-01 7:00:00', @PastaStackId, 80, 10, 8, 2),
+ ('2025-07-02 7:00:00', @PastaStackId, 70, 10, 7, 3),
+ ('2025-08-01 7:00:00', @PastaStackId, 60, 10, 6, 4),
+ ('2025-08-02 7:00:00', @PastaStackId, 50, 10, 5, 5),
+ ('2025-09-01 7:00:00', @PastaStackId, 40, 10, 4, 6),
+ ('2025-09-02 7:00:00', @PastaStackId, 50, 10, 5, 5),
+ ('2025-10-01 7:00:00', @PastaStackId, 60, 10, 6, 4),
+ ('2025-10-02 7:00:00', @PastaStackId, 70, 10, 7, 3),
+ ('2025-11-01 7:00:00', @PastaStackId, 80, 10, 8, 2),
+ ('2025-11-02 7:00:00', @PastaStackId, 90, 10, 9, 1),
+ ('2025-12-01 7:00:00', @PastaStackId, 100, 10, 10, 0),
+ ('2025-12-02 7:00:00', @PastaStackId, 90, 10, 9, 1)
+ END";
+
+ connection.Execute(seed);
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/CardRepository.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/CardRepository.cs
new file mode 100644
index 00000000..49a0f4b4
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/CardRepository.cs
@@ -0,0 +1,122 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Infrastructure.Dapper;
+using System.Data;
+
+namespace FlashCards.Infrastructure.Repositories;
+
+public class CardRepository : ICardRepository
+{
+
+ private readonly IDbConnection _connection;
+ private readonly IDapperWrapper _dapper;
+
+ public CardRepository(IDbConnection connection, IDapperWrapper dapper)
+ {
+ _connection = connection;
+ _dapper = dapper;
+ }
+
+ public int Add(Card card)
+ {
+ var sql = @"Insert into Card (StackId, FrontText, BackText)
+ Values (@StackId, @FrontText, @BackText);
+ Select cast(scope_identity() as int)";
+
+ return _dapper.QuerySingle(_connection, sql, card);
+ }
+
+
+ public void DeleteById(int id)
+ {
+ var sql = @"delete from Card
+ where Id = @id";
+
+ _dapper.Execute(_connection, sql, new { Id = id });
+ }
+ public void DeleteAllByStackId(int id)
+ {
+ var sql = @"delete c from Card c
+ inner join stack s on s.id = c.StackId
+ where s.Id = @Id";
+
+ _dapper.Execute(_connection, sql, new { Id = id });
+ }
+
+
+ public void UpdateCardText(int id, string frontText, string backText)
+ {
+ var sql = @"UPDATE card
+ SET FrontText = @frontText, BackText = @backText
+ WHERE Id = @id;";
+
+ _dapper.Execute(_connection, sql, new { Id = id, FrontText = frontText, BackText = backText });
+ }
+ public void UpdateCardCounters(int id, int studied, int correct, int incorrect)
+ {
+ var sql = @"update card
+ set TimesStudied = TimesStudied + @studied, TimesCorrect = TimesCorrect + @correct, TimesIncorrect = TimesIncorrect + @incorrect
+ where Id = @Id";
+
+ _dapper.Execute(_connection, sql, new { Id = id, studied = studied, correct = correct, incorrect = incorrect });
+ }
+
+
+ public List GetAllByStackId(int id)
+ {
+ var sql = @"select * from Card
+ where StackId = @Id";
+
+ return _dapper.Query(_connection, sql, new { Id = id }).ToList();
+ }
+ public int GetCardCountByStackId(int id)
+ {
+ var sql = @"select count(*)
+ from Card c
+ inner join stack s on s.id = c.stackid
+ where s.Id = @Id";
+
+ return _dapper.Query(_connection, sql, new { Id = id }).First();
+ }
+ public int GetIdByTextAndStackId(int stackId, string frontText, string backText)
+ {
+ var sql = @"select Id from card where UPPER(FrontText) = UPPER(@FrontText) AND UPPER(BackText) = UPPER(@BackText) AND StackId = @StackId";
+
+ return _dapper.QuerySingle(_connection, sql, new { StackId = stackId, FrontText = frontText, BackText = backText });
+ }
+
+
+ public bool ExistsByFrontText(string text, int stackId)
+ {
+ var sql = @"select 1 from card where UPPER(FrontText) = UPPER(@FrontText) AND StackId = @StackId";
+
+ int exists = _dapper.Query(_connection, sql, new { FrontText = text, StackId = stackId }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+ public bool ExistsByFrontTextExcludingId(string text, int stackId, int cardId)
+ {
+ var sql = @"select 1 from card where UPPER(FrontText) = UPPER(@FrontText) AND StackId = @StackId AND Id != @CardId";
+
+ int exists = _dapper.Query(_connection, sql, new { FrontText = text, StackId = stackId, CardId = cardId }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+ public bool ExistsByBackText(string text, int stackId)
+ {
+ var sql = @"select 1 from card where UPPER(BackText) = UPPER(@BackText) AND StackId = @StackId";
+
+ int exists = _dapper.Query(_connection, sql, new { BackText = text, StackId = stackId }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+ public bool ExistsByBackTextExcludingId(string text, int stackId, int cardId)
+ {
+ var sql = @"select 1 from card where UPPER(BackText) = UPPER(@BackText) AND StackId = @StackId AND Id != @CardId";
+
+ int exists = _dapper.Query(_connection, sql, new { BackText = text, StackId = stackId, CardId = cardId }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StackRepository.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StackRepository.cs
new file mode 100644
index 00000000..9c83f330
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StackRepository.cs
@@ -0,0 +1,75 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Infrastructure.Dapper;
+using System.Data;
+
+namespace FlashCards.Infrastructure.Repositories;
+
+public class StackRepository : IStackRepository
+{
+ private readonly IDbConnection _connection;
+ private readonly IDapperWrapper _dapper;
+
+ public StackRepository(IDbConnection connection, IDapperWrapper dapper)
+ {
+ _connection = connection;
+ _dapper = dapper;
+ }
+
+ public int Add(string name)
+ {
+ var sql = @"insert into Stack (Name)
+ values (@Name);
+ select cast(scope_identity() as int)";
+
+ return _dapper.QuerySingle(_connection, sql, new { Name = name });
+ }
+
+
+ public void DeleteById(int id)
+ {
+ var sql = @"delete from Stack
+ where Id = @id";
+
+ _dapper.Execute(_connection, sql, new { Id = id });
+ }
+
+
+ public List GetAll()
+ {
+ var sql = @"select Id, Name
+ from Stack";
+
+ return _dapper.Query(_connection, sql).ToList();
+ }
+ public Stack GetById(int id)
+ {
+ var sql = @"select name from Stack where Id = @Id";
+
+ return _dapper.QuerySingle(_connection, sql, new { Id = id });
+ }
+ public int GetIdByName(string name)
+ {
+ var sql = @"select Id from Stack where Name = @Name";
+
+ return _dapper.QuerySingle(_connection, sql, new { Name = name });
+ }
+
+
+ public bool ExistsByName(string name)
+ {
+ var sql = @"select 1 from stack where UPPER(Name) = UPPER(@Name)";
+
+ int exists = _dapper.Query(_connection, sql, new { Name = name }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+ public bool ExistsById(int id)
+ {
+ var sql = @"select 1 from stack where Id = @Id";
+
+ int exists = _dapper.Query(_connection, sql, new { Id = id }).FirstOrDefault();
+
+ return exists == 1 ? true : false;
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StudySessionRepository.cs b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StudySessionRepository.cs
new file mode 100644
index 00000000..72aabdfd
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StudySessionRepository.cs
@@ -0,0 +1,99 @@
+using FlashCards.Application.Interfaces;
+using FlashCards.Core.Entities;
+using FlashCards.Infrastructure.Dapper;
+using System.Data;
+
+namespace FlashCards.Infrastructure.Repositories;
+
+public class StudySessionRepository : IStudySessionRepository
+{
+ private readonly IDbConnection _connection;
+ private readonly IDapperWrapper _dapper;
+
+ public StudySessionRepository(IDbConnection connection, IDapperWrapper dapper)
+ {
+ _connection = connection;
+ _dapper = dapper;
+ }
+
+ public void Add(StudySession session)
+ {
+ var sql = @"insert into StudySession (SessionDate, StackId, Score, CountStudied, CountCorrect, CountIncorrect)
+ values (@SessionDate, @StackId, @Score, @CountStudied, @CountCorrect, @CountIncorrect)";
+
+ _dapper.Execute(_connection, sql, session);
+ }
+
+
+ public void DeleteAllByStackId(int id)
+ {
+ var sql = @"delete u from StudySession u
+ inner join stack s on s.id = u.StackId
+ where s.Id = @Id";
+
+ _dapper.Execute(_connection, sql, new { Id = id });
+ }
+
+
+ public List GetAll()
+ {
+ var sql = @"select * from StudySession";
+
+ return _dapper.Query(_connection, sql).ToList();
+ }
+ public int GetSessionCountByStackId(int id)
+ {
+ var sql = @"select count(*)
+ from StudySession t
+ inner join stack s on s.id = t.StackId
+ where s.Id = @Id";
+
+ return _dapper.Query(_connection, sql, new { Id = id }).First();
+ }
+
+ public List GetAverageScoreByMonthByYear(int year)
+ {
+ var sql = @"WITH newTable AS(
+ SELECT ss.StackId, s.Name as StackName, CAST(ss.Score as FLOAT) as FloatScore, Year(ss.SessionDate) as SessionYear, Month(ss.SessionDate) as SessionMonth
+ FROM StudySession ss
+ inner join Stack s on s.Id = ss.StackId),
+ Pivoted AS(
+ select * from newTable
+ pivot(
+ avg(FloatScore)
+ for SessionMonth in ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
+ ) as P)
+ select StackId, StackName, SessionYear, [1] as January,[2] as February,[3] as March,[4] as April,[5] as May,[6] as June,[7] as July,[8] as August,[9] as September,[10] as October,[11] as November,[12] as December
+ from Pivoted
+ where SessionYear = @ReportYear";
+
+ return _dapper.Query(_connection, sql, new { ReportYear = year }).ToList();
+ }
+
+ public List GetSessionCountByMonthByYear(int year)
+ {
+ var sql = @"WITH newTable AS(
+ SELECT ss.StackId, s.Name as StackName, Year(ss.SessionDate) as SessionYear, Month(ss.SessionDate) as SessionMonth
+ FROM StudySession ss
+ inner join Stack s on s.Id = ss.StackId),
+ Pivoted AS(
+ select * from newTable
+ pivot(
+ COUNT(SessionMonth)
+ for SessionMonth in ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
+ ) as P)
+ select StackId, StackName, SessionYear, [1] as January,[2] as February,[3] as March,[4] as April,[5] as May,[6] as June,[7] as July,[8] as August,[9] as September,[10] as October,[11] as November,[12] as December
+ from Pivoted
+ where SessionYear = @ReportYear";
+
+ return _dapper.Query(_connection, sql, new { ReportYear = year }).ToList();
+ }
+
+ public int[] GetAllSessionYears()
+ {
+ var sql = @"select Year(SessionDate) from StudySession
+ group by Year(SessionDate)";
+
+ return _dapper.Query(_connection, sql).ToArray();
+ }
+}
diff --git a/jzhartman.FlashCards/FlashCardsApp.slnx b/jzhartman.FlashCards/FlashCardsApp.slnx
new file mode 100644
index 00000000..09b82aee
--- /dev/null
+++ b/jzhartman.FlashCards/FlashCardsApp.slnx
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+