From 5a5ebb9478e77e599c8940342b6f57216dae4d4c Mon Sep 17 00:00:00 2001 From: jzhartman Date: Mon, 16 Feb 2026 23:19:48 -0500 Subject: [PATCH 1/5] Copied from personal repo --- .../Cards/Add/AddCardCommand.cs | 3 + .../Cards/Add/AddCardHandler.cs | 25 ++ .../Cards/Delete/DeleteCardByIdHandler.cs | 18 ++ .../Cards/EditCard/EditCardCommand.cs | 3 + .../Cards/EditCard/EditCardHandler.cs | 22 ++ .../EditCardTextBySideCommand.cs | 5 + .../EditCardTextBySideHandler.cs | 45 +++ .../Cards/GetAllByStackId/CardResponse.cs | 3 + .../Cards/GetAllByStackId/GetAllByStackId.cs | 44 +++ .../UpdateCardCountersHandler.cs | 25 ++ .../ValidateCardTextBySide.cs | 43 +++ .../ValidateCardTextBySideCommand.cs | 5 + .../DependencyInjection.cs | 45 +++ .../FlashCards.Application/Enums/CardSide.cs | 8 + .../FlashCards.Application.csproj | 21 ++ .../Interfaces/ICardRepository.cs | 19 ++ .../Interfaces/IStackRepository.cs | 14 + .../Interfaces/IStudySessionRepository.cs | 14 + .../GetAverageScorePerMonthByYearHandler.cs | 49 ++++ .../GetAverageScorePerMonthResponse.cs | 6 + .../GetSessionCountPerMonthHandler.cs | 49 ++++ .../GetSessionCountPerMonthResponse.cs | 5 + .../SessionPerMonthReportHandler.cs | 9 + .../SessionPerMonthReportRequest.cs | 5 + .../SessionPerMonthReportResponse.cs | 9 + .../Stacks/Add/AddStackHandler.cs | 31 +++ .../Stacks/Delete/DeleteByIdHandler.cs | 25 ++ .../GetAll/GetAllStacksWithCountsHandler.cs | 45 +++ .../GetAll/StackNamesWithCountsResponse.cs | 3 + .../Add/AddStudySessionCommand.cs | 4 + .../Add/AddStudySessionHandler.cs | 31 +++ .../GetAll/GetAllStudySessionsHandler.cs | 40 +++ .../GetAll/StudySessionResponse.cs | 4 + .../GetAllSessionYears/GetAllSessionYears.cs | 23 ++ .../DependencyInjection.cs | 34 +++ .../FlashCards.ConsoleUI/Enums/Actions.cs | 8 + .../Enums/MainMenuItem.cs | 13 + .../Enums/ReviewStackMenuItem.cs | 10 + .../FlashCards.ConsoleUI.csproj | 18 ++ .../Input/ConsoleInput.cs | 109 ++++++++ .../Models/StackViewModel.cs | 17 ++ .../Output/ConsoleOutput.cs | 89 ++++++ .../Services/MainMenuService.cs | 237 ++++++++++++++++ .../Services/ReportService.cs | 42 +++ .../Services/ReviewStackMenuService.cs | 260 ++++++++++++++++++ .../Services/StudySessionService.cs | 106 +++++++ .../Views/AverageScorePerMonthView.cs | 61 ++++ .../Views/CardListView.cs | 36 +++ .../Views/MainMenuView.cs | 26 ++ .../Views/ReviewStackMenuView.cs | 24 ++ .../Views/SessionCountPerMonthView.cs | 61 ++++ .../Views/SessionYearSelectionView.cs | 14 + .../Views/StackListView.cs | 38 +++ .../Views/StudySessionListView.cs | 42 +++ .../Views/StudySessionView.cs | 26 ++ .../FlashCards.Core/Entities/Card.cs | 29 ++ .../FlashCards.Core/Entities/SessionReport.cs | 20 ++ .../FlashCards.Core/Entities/Stack.cs | 25 ++ .../FlashCards.Core/Entities/StudySession.cs | 12 + .../FlashCards.Core/FlashCards.Core.csproj | 9 + .../FlashCards.Core/Validation/Error.cs | 6 + .../FlashCards.Core/Validation/Errors.cs | 32 +++ .../FlashCards.Core/Validation/Result.cs | 19 ++ .../FlashCards.DB/FlashCards.DB.sqlproj | 71 +++++ .../FlashCards.DB/dbo/Card.sql | 11 + .../FlashCards.DB/dbo/Stack.sql | 6 + .../FlashCards.DB/dbo/StudySession.sql | 11 + .../Dapper/DapperWrapper.cs | 32 +++ .../Dapper/IDapperWrapper.cs | 19 ++ .../DependencyInjection.cs | 34 +++ .../FlashCards.Infrastructure.csproj | 21 ++ .../Initialization/DbInitializer.cs | 192 +++++++++++++ .../Repositories/CardRepository.cs | 122 ++++++++ .../Repositories/StackRepository.cs | 75 +++++ .../Repositories/StudySessionRepository.cs | 99 +++++++ jzhartman.FlashCards/FlashCardsApp.slnx | 9 + 76 files changed, 2825 insertions(+) create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardCommand.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/Add/AddCardHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/Delete/DeleteCardByIdHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardCommand.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/EditCard/EditCardHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideCommand.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/EditTextBySide/EditCardTextBySideHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/CardResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/GetAllByStackId/GetAllByStackId.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/UpdateCardCounters/UpdateCardCountersHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySide.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Cards/ValidateCardTextBySide/ValidateCardTextBySideCommand.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/DependencyInjection.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Enums/CardSide.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/FlashCards.Application.csproj create mode 100644 jzhartman.FlashCards/FlashCards.Application/Interfaces/ICardRepository.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Interfaces/IStackRepository.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Interfaces/IStudySessionRepository.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthByYearHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetAverageScorePerMonth/GetAverageScorePerMonthResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionCountPerMonth/GetSessionCountPerMonthResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportRequest.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Reports/GetSessionsPerMonth/SessionPerMonthReportResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Stacks/Add/AddStackHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Stacks/Delete/DeleteByIdHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/GetAllStacksWithCountsHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/Stacks/GetAll/StackNamesWithCountsResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionCommand.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/StudySessions/Add/AddStudySessionHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/GetAllStudySessionsHandler.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAll/StudySessionResponse.cs create mode 100644 jzhartman.FlashCards/FlashCards.Application/StudySessions/GetAllSessionYears/GetAllSessionYears.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/DependencyInjection.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/Actions.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/MainMenuItem.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Enums/ReviewStackMenuItem.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/FlashCards.ConsoleUI.csproj create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Input/ConsoleInput.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Models/StackViewModel.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Output/ConsoleOutput.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Services/MainMenuService.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReportService.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Services/ReviewStackMenuService.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Services/StudySessionService.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/CardListView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/MainMenuView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/ReviewStackMenuView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionYearSelectionView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StackListView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionListView.cs create mode 100644 jzhartman.FlashCards/FlashCards.ConsoleUI/Views/StudySessionView.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Entities/Card.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Entities/SessionReport.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Entities/Stack.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Entities/StudySession.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/FlashCards.Core.csproj create mode 100644 jzhartman.FlashCards/FlashCards.Core/Validation/Error.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Validation/Errors.cs create mode 100644 jzhartman.FlashCards/FlashCards.Core/Validation/Result.cs create mode 100644 jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj create mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql create mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql create mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/DapperWrapper.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Dapper/IDapperWrapper.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/DependencyInjection.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/FlashCards.Infrastructure.csproj create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Initialization/DbInitializer.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/CardRepository.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StackRepository.cs create mode 100644 jzhartman.FlashCards/FlashCards.Infrastructure/Repositories/StudySessionRepository.cs create mode 100644 jzhartman.FlashCards/FlashCardsApp.slnx 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..80bcc490 --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs @@ -0,0 +1,61 @@ +using FlashCards.Application.Reports.GetAverageScorePerMonth; +using Spectre.Console; + +namespace FlashCards.ConsoleUI.Views; + +public class AverageScorePerMonthView +{ + public void Render(List reports, int year) + { + int i = 1; + 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..8bcc166d --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs @@ -0,0 +1,61 @@ +using FlashCards.Application.Reports.GetSessionCountPerMonth; +using Spectre.Console; + +namespace FlashCards.ConsoleUI.Views; + +public class SessionCountPerMonthView +{ + public void Render(List reports, int year) + { + int i = 1; + 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.DB/FlashCards.DB.sqlproj b/jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj new file mode 100644 index 00000000..52b2f9e0 --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj @@ -0,0 +1,71 @@ + + + + Debug + AnyCPU + FlashCards.Db + 2.0 + 4.1 + {e1156da2-7174-4300-b660-308387d6d249} + Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider + Database + + + FlashCards.DB + FlashCards.DB + 1033, CI + BySchemaAndSchemaType + True + v4.7.2 + CS + Properties + False + True + True + + + bin\Release\ + $(MSBuildProjectName).sql + False + pdbonly + true + false + true + prompt + 4 + + + bin\Debug\ + $(MSBuildProjectName).sql + false + true + full + false + true + true + prompt + 4 + + + 11.0 + + True + 11.0 + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql new file mode 100644 index 00000000..3d3da763 --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql @@ -0,0 +1,11 @@ +CREATE TABLE [dbo].[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, + CONSTRAINT [FK_Card_ToTStack] FOREIGN KEY ([StackId]) REFERENCES [Stack]([Id]) +) diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql new file mode 100644 index 00000000..98e1234a --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql @@ -0,0 +1,6 @@ +CREATE TABLE [dbo].[Stack] +( + [Id] INT NOT NULL PRIMARY KEY IDENTITY, + [Name] NVARCHAR(32) NOT NULL, + +) diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql new file mode 100644 index 00000000..253022a7 --- /dev/null +++ b/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql @@ -0,0 +1,11 @@ +CREATE TABLE [dbo].[StudySession] +( + [Id] INT NOT NULL PRIMARY KEY IDENTITY, + [Time] DATETIME NOT NULL, + [StackId] INT NOT NULL, + [Score] FLOAT NOT NULL, + [CountStudied] INT NOT NULL, + [CountCorrect] INT NOT NULL, + [CountIncorrect] INT NOT NULL, + CONSTRAINT [FK_StudySession_ToTStack] FOREIGN KEY ([StackId]) REFERENCES [Stack]([Id]) +) 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 @@ + + + + + + + + + From 1404bc87e03bb081efc73aa6dcd061ceb2e5617e Mon Sep 17 00:00:00 2001 From: Jason Hartman <129107535+jzhartman@users.noreply.github.com> Date: Mon, 16 Feb 2026 23:50:49 -0500 Subject: [PATCH 2/5] Add README.md for Flash Cards App Added detailed documentation for the Flash Cards App, including description, getting started instructions, program operation, and project requirements. --- README.md | 80 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 README.md 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 From 7eb5116171f739ee7b20e395dc9d27fcbd452e16 Mon Sep 17 00:00:00 2001 From: jzhartman Date: Tue, 17 Feb 2026 00:00:53 -0500 Subject: [PATCH 3/5] Removed obsolete SQL porject Removed some important stuff from git ignore --- .../FlashCards.App/FlashCards.App.csproj | 28 +++++++++++++++++ .../FlashCards.App/Program.cs | 31 +++++++++++++++++++ .../FlashCards.App/appsettings.json | 6 ++++ 3 files changed, 65 insertions(+) create mode 100644 jzhartman.FlashCards/FlashCards.App/FlashCards.App.csproj create mode 100644 jzhartman.FlashCards/FlashCards.App/Program.cs create mode 100644 jzhartman.FlashCards/FlashCards.App/appsettings.json 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 From 0014968b0b224dc577a6b6ba7d8e0ee3b5cf1d80 Mon Sep 17 00:00:00 2001 From: jzhartman Date: Tue, 17 Feb 2026 00:01:23 -0500 Subject: [PATCH 4/5] Another commit --- .../FlashCards.DB/FlashCards.DB.sqlproj | 71 ------------------- .../FlashCards.DB/dbo/Card.sql | 11 --- .../FlashCards.DB/dbo/Stack.sql | 6 -- .../FlashCards.DB/dbo/StudySession.sql | 11 --- 4 files changed, 99 deletions(-) delete mode 100644 jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj delete mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql delete mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql delete mode 100644 jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql diff --git a/jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj b/jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj deleted file mode 100644 index 52b2f9e0..00000000 --- a/jzhartman.FlashCards/FlashCards.DB/FlashCards.DB.sqlproj +++ /dev/null @@ -1,71 +0,0 @@ - - - - Debug - AnyCPU - FlashCards.Db - 2.0 - 4.1 - {e1156da2-7174-4300-b660-308387d6d249} - Microsoft.Data.Tools.Schema.Sql.Sql150DatabaseSchemaProvider - Database - - - FlashCards.DB - FlashCards.DB - 1033, CI - BySchemaAndSchemaType - True - v4.7.2 - CS - Properties - False - True - True - - - bin\Release\ - $(MSBuildProjectName).sql - False - pdbonly - true - false - true - prompt - 4 - - - bin\Debug\ - $(MSBuildProjectName).sql - false - true - full - false - true - true - prompt - 4 - - - 11.0 - - True - 11.0 - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql deleted file mode 100644 index 3d3da763..00000000 --- a/jzhartman.FlashCards/FlashCards.DB/dbo/Card.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE [dbo].[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, - CONSTRAINT [FK_Card_ToTStack] FOREIGN KEY ([StackId]) REFERENCES [Stack]([Id]) -) diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql deleted file mode 100644 index 98e1234a..00000000 --- a/jzhartman.FlashCards/FlashCards.DB/dbo/Stack.sql +++ /dev/null @@ -1,6 +0,0 @@ -CREATE TABLE [dbo].[Stack] -( - [Id] INT NOT NULL PRIMARY KEY IDENTITY, - [Name] NVARCHAR(32) NOT NULL, - -) diff --git a/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql b/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql deleted file mode 100644 index 253022a7..00000000 --- a/jzhartman.FlashCards/FlashCards.DB/dbo/StudySession.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE [dbo].[StudySession] -( - [Id] INT NOT NULL PRIMARY KEY IDENTITY, - [Time] DATETIME NOT NULL, - [StackId] INT NOT NULL, - [Score] FLOAT NOT NULL, - [CountStudied] INT NOT NULL, - [CountCorrect] INT NOT NULL, - [CountIncorrect] INT NOT NULL, - CONSTRAINT [FK_StudySession_ToTStack] FOREIGN KEY ([StackId]) REFERENCES [Stack]([Id]) -) From cb77d465b7f159c87c9ba19b17e598957ff39703 Mon Sep 17 00:00:00 2001 From: jzhartman Date: Tue, 17 Feb 2026 00:05:47 -0500 Subject: [PATCH 5/5] Removed unusued variables in reports views --- .../FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs | 1 - .../FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs index 80bcc490..fd99f93d 100644 --- a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs +++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/AverageScorePerMonthView.cs @@ -7,7 +7,6 @@ public class AverageScorePerMonthView { public void Render(List reports, int year) { - int i = 1; var table = new Table() .RoundedBorder() .BorderColor(Color.Blue) diff --git a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs index 8bcc166d..ef420ae9 100644 --- a/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs +++ b/jzhartman.FlashCards/FlashCards.ConsoleUI/Views/SessionCountPerMonthView.cs @@ -7,7 +7,6 @@ public class SessionCountPerMonthView { public void Render(List reports, int year) { - int i = 1; var table = new Table() .RoundedBorder() .BorderColor(Color.Blue)