diff --git a/FlashcardsApp/FlashcardsApp.sln b/FlashcardsApp/FlashcardsApp.sln new file mode 100644 index 00000000..08b4e9af --- /dev/null +++ b/FlashcardsApp/FlashcardsApp.sln @@ -0,0 +1,22 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.12.35707.178 d17.12 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "FlashcardsApp", "FlashcardsApp\FlashcardsApp.csproj", "{E87AE218-DC73-4974-9E12-5FE3DE56D12B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E87AE218-DC73-4974-9E12-5FE3DE56D12B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E87AE218-DC73-4974-9E12-5FE3DE56D12B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E87AE218-DC73-4974-9E12-5FE3DE56D12B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E87AE218-DC73-4974-9E12-5FE3DE56D12B}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/FlashcardsApp/FlashcardsApp/DTOs/FlashcardDTO.cs b/FlashcardsApp/FlashcardsApp/DTOs/FlashcardDTO.cs new file mode 100644 index 00000000..8c6a6888 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/DTOs/FlashcardDTO.cs @@ -0,0 +1,10 @@ +namespace FlashcardsApp.DTOs; + +internal class FlashcardDTO +{ + public int DisplayNumber { get; set; } + public int FlashcardId { get; set; } + public string Front { get; set; } + public string Back { get; set; } + public DateTime CreatedDate { get; set; } +} diff --git a/FlashcardsApp/FlashcardsApp/FlashcardsApp.csproj b/FlashcardsApp/FlashcardsApp/FlashcardsApp.csproj new file mode 100644 index 00000000..47fd89d8 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/FlashcardsApp.csproj @@ -0,0 +1,24 @@ + + + + Exe + net9.0 + enable + enable + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/FlashcardsApp/FlashcardsApp/Models/Flashcard.cs b/FlashcardsApp/FlashcardsApp/Models/Flashcard.cs new file mode 100644 index 00000000..c1342ff7 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/Models/Flashcard.cs @@ -0,0 +1,10 @@ +namespace FlashcardsApp.Models; + +internal class Flashcard +{ + public int FlashcardId { get; set; } + public int StackId { get; set; } + public string Front { get; set; } + public string Back { get; set; } + public DateTime CreatedDate { get; set; } +} diff --git a/FlashcardsApp/FlashcardsApp/Models/Stack.cs b/FlashcardsApp/FlashcardsApp/Models/Stack.cs new file mode 100644 index 00000000..d6dd572f --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/Models/Stack.cs @@ -0,0 +1,9 @@ +namespace FlashcardsApp.Models; + +internal class Stack +{ + public int StackId { get; set; } + public string Name { get; set; } + public string Description { get; set; } + public DateTime CreatedDate { get; set; } +} diff --git a/FlashcardsApp/FlashcardsApp/Models/StudySession.cs b/FlashcardsApp/FlashcardsApp/Models/StudySession.cs new file mode 100644 index 00000000..75801bcd --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/Models/StudySession.cs @@ -0,0 +1,9 @@ +namespace FlashcardsApp.Models; + +internal class StudySession +{ + public int SessionId { get; set; } + public int StackId { get; set; } + public string Score { get; set; } + public DateTime StudyDate { get; set; } +} diff --git a/FlashcardsApp/FlashcardsApp/Program.cs b/FlashcardsApp/FlashcardsApp/Program.cs new file mode 100644 index 00000000..8eb95405 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/Program.cs @@ -0,0 +1,45 @@ +using Microsoft.Extensions.Configuration; +using FlashcardsApp.Services; +using FlashcardsApp.UI.Core; + +namespace FlashcardsApp; + +class Program +{ + static IConfiguration? config; // a variable of type IConfiguration + static DatabaseService? databaseService; + static void Main(string[] args) + { + try + { + config = new ConfigurationBuilder() // Starting with an empty configuration but will return an IConfiguration object + .SetBasePath(Directory.GetCurrentDirectory()) // tells the builder where to look for configuration files (appsettings.json) + .AddJsonFile("appsettings.json") // what file it should be reading + .Build(); // creates the the IConfiguration object + + string? connectionString = config.GetConnectionString("Default"); //GetConnectionString("Default") specifically looks for "ConnectionString" in appsettings.json + // and return the value associated with "Default" + if (connectionString == null) + { + throw new Exception("Connection string not found\n"); + + } + + databaseService = new(connectionString); + databaseService.TestConnection(); + + Console.WriteLine("Successfully connected to server!\n"); + } + catch (Exception ex) + { + Console.WriteLine($"Error loading database: {ex.Message}\n"); + Console.WriteLine("Press Any Key to Exit..."); + Console.ReadKey(); + return; + } + + MenuHandler menuHandler = new(databaseService); + + menuHandler.MainMenu(); + } +} \ No newline at end of file diff --git a/FlashcardsApp/FlashcardsApp/Services/DatabaseService.cs b/FlashcardsApp/FlashcardsApp/Services/DatabaseService.cs new file mode 100644 index 00000000..92eb4159 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/Services/DatabaseService.cs @@ -0,0 +1,308 @@ +using Dapper; +using FlashcardsApp.DTOs; +using FlashcardsApp.Models; +using FlashcardsApp.UI.Helpers; +using Microsoft.Data.SqlClient; //for sql server connection + +namespace FlashcardsApp.Services; + +internal class DatabaseService +{ + private readonly string _connectionString; + + internal DatabaseService(string connectionString) + { + _connectionString = connectionString; + } + + public void TestConnection() + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + } + } + + internal void DeleteFlashcard(int stackId, int flashcardId) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "DELETE FROM Flashcards WHERE StackId = @StackId AND FlashcardId = @FlashcardId"; + + var rowsAffected = connection.Execute(sql, new { StackId = stackId, FlashcardId = flashcardId }); + + if (rowsAffected > 0) + { + Console.WriteLine("\nDeletion successful."); + Console.WriteLine($"\n{rowsAffected} row(s) deleted."); + } + else + { + Console.WriteLine("\nDeletion has failed"); + } + } + } + + internal void DeleteStack(int stackId) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "DELETE FROM Stacks WHERE StackId = @StackId"; + + var rowsAffected = connection.Execute(sql, new { StackId = stackId }); + + if (rowsAffected > 0) + { + Console.WriteLine("\nDeletion successful."); + Console.WriteLine($"\n{rowsAffected} row(s) deleted."); + } + else + { + Console.WriteLine("\nDeletion has failed"); + } + } + } + + + internal void GetAllStacks() + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "SELECT * FROM Stacks"; + + var stacks = connection.Query(sql).ToList(); + + TableVisualization.ShowStacksTable(stacks); + } + } + + internal List GetAllStacksAsList() + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "SELECT * FROM Stacks"; + + var stacks = connection.Query(sql).ToList(); + + return stacks; + } + } + + internal Flashcard? GetFlashcardByFlashcardId(int flashcardId, int stackId) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "SELECT FlashcardId, StackId, Front, Back, CreatedDate FROM Flashcards WHERE StackId = @StackId AND FlashcardId = @FlashcardId"; + + var flashcard = connection.QuerySingleOrDefault(sql, new { StackId = stackId, FlashcardId = flashcardId }); + + if (flashcard == null) + { + Console.WriteLine($"\nNo flashcard found with ID: {flashcardId} in stack {stackId}"); + return null; + } + + return flashcard; + } + } + + + internal List GetFlashcardsByID(int stackId) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = @" + SELECT + ROW_NUMBER() OVER (ORDER BY FlashcardId) as DisplayNumber, + FlashcardId, + Front, + Back, + CreatedDate + FROM Flashcards + WHERE StackId = @StackId"; + + var flashcards = connection.Query(sql, new { StackId = stackId }).ToList(); + + TableVisualization.ShowFlashcardsTable(flashcards); + + return flashcards; + } + } + + internal List GetFlashcardsByStackID(int stackId) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = @" + SELECT + FlashcardId, + StackId, + Front, + Back, + CreatedDate + FROM Flashcards + WHERE StackId = @StackId"; + + var flashcards = connection.Query(sql, new { StackId = stackId }).ToList(); + + return flashcards; + } + } + + internal void GetStudyHistory() + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "SELECT * FROM StudySessions"; + + var sessions = connection.Query(sql).ToList(); + + var sql2 = "SELECT * FROM Stacks"; + + var stacks = connection.Query(sql2).ToList(); + + if (sessions.Count > 0) + { + TableVisualization.ShowStudySessionsTable(sessions, stacks); + } + else + { + Console.WriteLine("\nNo sessions to view!"); + } + } + } + + internal void Post(Stack stack) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "INSERT INTO Stacks (Name, Description, CreatedDate) VALUES (@Name, @Description, @CreatedDate)"; + + var rowsAffected = connection.Execute(sql, new + { + Name = stack.Name, + Description = stack.Description, + CreatedDate = stack.CreatedDate, + }); + + if (rowsAffected > 0) + { + Console.WriteLine("\nSuccessfully added stack!"); + Console.WriteLine($"\n{rowsAffected} row(s) inserted."); + } + else + { + Console.WriteLine("\nFailed to add stack!"); + } + } + } + + internal void PostFlashcard(Flashcard flashcard) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var checkSql = @"SELECT COUNT(1) + FROM Flashcards + WHERE StackId = @StackId + AND Front = @Front"; + + var exists = connection.ExecuteScalar(checkSql, new + { + StackId = flashcard.StackId, + Front = flashcard.Front + }) > 0; + + if (exists) + { + Console.WriteLine("\nA flashcard with this front already exists in this stack!"); + return; + } + + var sql = "INSERT INTO Flashcards (StackId, Front, Back, CreatedDate) VALUES (@StackId, @Front, @Back, @CreatedDate)"; + + var rowsAffected = connection.Execute(sql, new + { + StackId = flashcard.StackId, + Front = flashcard.Front, + Back = flashcard.Back, + CreatedDate = flashcard.CreatedDate + }); + + if (rowsAffected > 0) + { + Console.WriteLine("\nSuccessfully added flashcard to stack!"); + Console.WriteLine($"\n{rowsAffected} row(s) inserted."); + } + else + { + Console.WriteLine("\nFailed to add flashcard!"); + } + } + } + + internal void PostStudySession(StudySession session) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = "INSERT INTO StudySessions (StackId, Score, StudyDate) VALUES (@StackId, @Score, @StudyDate)"; + + var rowsAffected = connection.Execute(sql, new + { + StackId = session.StackId, + Score = session.Score, + StudyDate = session.StudyDate, + }); + + if (rowsAffected > 0) + { + Console.WriteLine("\nStudy Session was successfully uploaded!"); + Console.WriteLine($"\n{rowsAffected} row(s) inserted."); + } + else + { + Console.WriteLine("\nFailed to upload session!"); + } + } + } + + internal void UpdateFlashcard(int stackId, int flashcardId, Flashcard flashcard) + { + using (var connection = new SqlConnection(_connectionString)) + { + connection.Open(); + + var sql = @"UPDATE Flashcards SET Front = @Front, Back = @Back, CreatedDate = @CreatedDate WHERE StackId = @StackId AND FlashcardId = @FlashcardId"; + + var rowsAffected = connection.Execute(sql, new + { + Front = flashcard.Front, + Back = flashcard.Back, + CreatedDate = flashcard.CreatedDate, + StackId = stackId, + FlashcardId = flashcardId + }); + } + } +} diff --git a/FlashcardsApp/FlashcardsApp/UI/Core/InputValidator.cs b/FlashcardsApp/FlashcardsApp/UI/Core/InputValidator.cs new file mode 100644 index 00000000..af507d57 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Core/InputValidator.cs @@ -0,0 +1,93 @@ +using Spectre.Console; + +namespace FlashcardsApp.UI.Core; + +public class InputValidator +{ + public string GetStackName() + { + while (true) + { + Console.Write("\nEnter a name for the stack of flashcards OR 0 to return: "); + string? name = Console.ReadLine(); + + if (name == "0") + return string.Empty; + + if (string.IsNullOrEmpty(name)) + { + Console.WriteLine("\nName cannot be empty."); + continue; + } + + if (name.Length > 100) + { + Console.WriteLine("\nName cannot be longer than 100 characters."); + continue; + } + + return name; + } + } + + public string GetStackDescription() + { + Console.Write("\nIf you would like to add a description type it here or press enter: "); + string? description; + + do + { + description = Console.ReadLine(); + + if (string.IsNullOrWhiteSpace(description)) + { + return "No description provided"; + } + + if (description.Length > 500) + { + Console.WriteLine("\nDescription cannot be longer than 500 characters"); + Console.Write("\nEnter description again: "); + } + } while (description.Length > 500); + + return description; + } + + public string GetFlashcardContent(string side) + { + while (true) + { + Console.Write($"\n\nEnter the content for the {side} of the card OR 0 to return: "); + string? content = Console.ReadLine(); + + if (content == "0") + return string.Empty; + + if (string.IsNullOrEmpty(content)) + { + Console.WriteLine($"\n{side} cannot be empty."); + continue; + } + + if (content.Length > 500) + { + Console.WriteLine($"\n{side} cannot be longer than 500 characters."); + continue; + } + + return content; + } + } + + public bool GetConfirmation(string message) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title(message) + .AddChoices(new[] { "Yes", "No" })); + + return choice == "Yes"; + } +} + diff --git a/FlashcardsApp/FlashcardsApp/UI/Core/MenuHandler.cs b/FlashcardsApp/FlashcardsApp/UI/Core/MenuHandler.cs new file mode 100644 index 00000000..c1ed0b9f --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Core/MenuHandler.cs @@ -0,0 +1,64 @@ +using FlashcardsApp.Services; +using FlashcardsApp.UI.Features; +using Spectre.Console; + +namespace FlashcardsApp.UI.Core; + +internal class MenuHandler +{ + private readonly DatabaseService _databaseService; + private readonly StackUI _stackUI; + private readonly FlashcardUI _flashcardUI; + private readonly StudyUI _studyUI; + + internal MenuHandler(DatabaseService databaseService) + { + _databaseService = databaseService; + _stackUI = new StackUI(databaseService); + _flashcardUI = new FlashcardUI(databaseService); + _studyUI = new StudyUI(databaseService); + } + + internal void MainMenu() + { + Console.Clear(); + bool closeApp = false; + while (closeApp == false) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("MAIN MENU") + .AddChoices(new[] { + "View All Stacks", + "Create New Stack", + "Manage Stack", + "Study a Stack", + "View Study History", + "Close Application" + })); + + switch (choice) + { + case "View All Stacks": + _databaseService.GetAllStacks(); + break; + case "Create New Stack": + _stackUI.CreateStack(); + break; + case "Manage Stack": + _stackUI.StacksListMenu(); + break; + case "Study a Stack": + _studyUI.StudyMain(); + break; + case "View Study History": + _databaseService.GetStudyHistory(); + break; + case "Close Application": + closeApp = true; + Environment.Exit(0); + break; + } + } + } +} diff --git a/FlashcardsApp/FlashcardsApp/UI/Features/FlashcardUI.cs b/FlashcardsApp/FlashcardsApp/UI/Features/FlashcardUI.cs new file mode 100644 index 00000000..f9b552bd --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Features/FlashcardUI.cs @@ -0,0 +1,151 @@ +using FlashcardsApp.DTOs; +using FlashcardsApp.Models; +using FlashcardsApp.Services; +using FlashcardsApp.UI.Core; +using Spectre.Console; + +namespace FlashcardsApp.UI.Features; + +internal class FlashcardUI +{ + private readonly DatabaseService _databaseService; + private readonly InputValidator _validator; + + internal FlashcardUI(DatabaseService databaseService) + { + _databaseService = databaseService; + _validator = new InputValidator(); + } + + public void ViewCards(int stackId) + { + _databaseService.GetFlashcardsByID(stackId); + } + + public void CreateFlashcard(int stackId) + { + Flashcard flashcard = new(); + + string front = GetFrontContent(); + if (string.IsNullOrEmpty(front)) return; + + string back = GetBackContent(); + if (string.IsNullOrEmpty(back)) return; + + flashcard.StackId = stackId; + flashcard.Front = front; + flashcard.Back = back; + flashcard.CreatedDate = DateTime.Now; + + _databaseService.PostFlashcard(flashcard); + Console.WriteLine("\nFlashcard created successfully!"); + } + + public void UpdateFlashcard(int stackId) + { + List flashcards = _databaseService.GetFlashcardsByID(stackId); + + if (!flashcards.Any()) + { + Console.WriteLine("\nNo flashcards in this stack!"); + return; + } + + Dictionary cardMapping = flashcards.ToDictionary( + f => $"Front: {f.Front}\t\t| Back: {f.Back}", + f => f.FlashcardId); + + var choices = cardMapping.Keys.ToList(); + choices.Add("Return to Stack Menu"); + + var selectedCard = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a card to update.") + .AddChoices(choices)); + + if (selectedCard == "Return to Stack Menu") + { + return; + } + + int flashcardId = cardMapping[selectedCard]; + + Flashcard? flashcard = _databaseService.GetFlashcardByFlashcardId(flashcardId, stackId); + if (flashcard == null) + { + return; + } + + bool finished = false; + while (!finished) + { + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select what to update, save, or exit.") + .AddChoices(new[] + { + "Front", + "Back", + "Save Changes", + "Return to Stack Menu" + })); + + switch (choice) + { + case "Front": + string newFront = GetFrontContent(); + if (!string.IsNullOrEmpty(newFront)) + flashcard.Front = newFront; + break; + case "Back": + string newBack = GetBackContent(); + if (!string.IsNullOrEmpty(newBack)) + flashcard.Back = newBack; + break; + case "Save Changes": + flashcard.CreatedDate = DateTime.Now; + _databaseService.UpdateFlashcard(stackId, flashcardId, flashcard); + finished = true; + break; + case "Return to Stack Menu": + return; + } + } + } + + public void DeleteFlashcard(int stackId) + { + List flashcards = _databaseService.GetFlashcardsByID(stackId); + + Dictionary cardMapping = flashcards.ToDictionary( + f => $"Front: {f.Front}\t\t| Back: {f.Back}", + f => f.FlashcardId); + + var choices = cardMapping.Keys.ToList(); + choices.Add("Return to Stack Menu"); + + var selectedCard = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a card to delete.") + .AddChoices(choices)); + + if (selectedCard == "Return to Stack Menu") + { + return; + } + + int flashcardId = cardMapping[selectedCard]; + _databaseService.DeleteFlashcard(stackId, flashcardId); + } + + private string GetFrontContent() + { + return _validator.GetFlashcardContent("front"); + } + + private string GetBackContent() + { + return _validator.GetFlashcardContent("back"); + } + +} diff --git a/FlashcardsApp/FlashcardsApp/UI/Features/StackUI.cs b/FlashcardsApp/FlashcardsApp/UI/Features/StackUI.cs new file mode 100644 index 00000000..0f5d5f7a --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Features/StackUI.cs @@ -0,0 +1,179 @@ +using FlashcardsApp.Models; +using FlashcardsApp.Services; +using FlashcardsApp.UI.Core; +using Spectre.Console; + +namespace FlashcardsApp.UI.Features; + +internal class StackUI +{ + private readonly DatabaseService _databaseService; + private readonly FlashcardUI _flashcardUI; + private readonly InputValidator _validator; + + internal StackUI(DatabaseService databaseService) + { + _databaseService = databaseService; + _flashcardUI = new FlashcardUI(databaseService); + _validator = new InputValidator(); + } + + public void StacksListMenu() + { + while (true) + { + Console.Clear(); + _databaseService.GetAllStacks(); + + List stacks = _databaseService.GetAllStacksAsList(); + if (!stacks.Any()) + { + Console.WriteLine("\nNo stacks found. Create a stack first!"); + Console.WriteLine("\nPress Enter to return to main menu..."); + Console.ReadLine(); + return; + } + + List stackNames = stacks.Select(s => s.Name).ToList(); + stackNames.Add("Return to Main Menu"); + + var selectedStack = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a stack to manage.") + .AddChoices(stackNames)); + + if (selectedStack == "Return to Main Menu") + { + return; + } + + ManageStack(selectedStack); + } + } + + private void ManageStack(string selectedStack) + { + while (true) + { + Console.Clear(); + + var action = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"Managing stack: {selectedStack}") + .AddChoices(new[] + { + "View Cards", + "Add Card", + "Update Card", + "Delete Card", + "Delete this Stack", + "Return to Stack Menu", + "Return to Main Menu" + })); + + int stackId = GetStackId(selectedStack); + if (stackId == -1) + { + Console.WriteLine("\nError accessing stack. Returning to stack menu..."); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + return; + } + + switch (action) + { + case "View Cards": + _flashcardUI.ViewCards(stackId); + break; + case "Add Card": + _flashcardUI.CreateFlashcard(stackId); + break; + case "Update Card": + _flashcardUI.UpdateFlashcard(stackId); + break; + case "Delete Card": + _flashcardUI.DeleteFlashcard(stackId); + break; + case "Delete this Stack": + if (DeleteStack(stackId, selectedStack)) + return; + break; + case "Return to Stack Menu": + return; + case "Return to Main Menu": + return; + } + + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + } + } + + public void CreateStack() + { + string name = _validator.GetStackName(); + if (string.IsNullOrEmpty(name)) + return; + + string description = _validator.GetStackDescription(); + + Stack stack = new() + { + Name = name, + Description = description, + CreatedDate = DateTime.Now + }; + + try + { + _databaseService.Post(stack); + Console.WriteLine("\nStack created successfully!"); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + } + catch (Exception ex) + { + Console.WriteLine($"\nError creating stack: {ex.Message}"); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + } + } + + private bool DeleteStack(int stackId, string selectedStack) + { + if (_validator.GetConfirmation($"\nAre you sure you want to delete '{selectedStack}' stack?")) + { + try + { + _databaseService.DeleteStack(stackId); + Console.WriteLine("\nStack deleted successfully!"); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + return true; + } + catch (Exception ex) + { + Console.WriteLine($"\nError deleting stack: {ex.Message}"); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + return false; + } + } + return false; + } + + private int GetStackId(string selectedStack) + { + try + { + List stacks = _databaseService.GetAllStacksAsList(); + Stack stack = stacks.First(s => s.Name == selectedStack); + return stack.StackId; + } + catch (Exception) + { + Console.WriteLine("\nError finding stack."); + return -1; + } + } +} diff --git a/FlashcardsApp/FlashcardsApp/UI/Features/StudyUI.cs b/FlashcardsApp/FlashcardsApp/UI/Features/StudyUI.cs new file mode 100644 index 00000000..edf7f071 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Features/StudyUI.cs @@ -0,0 +1,149 @@ +using FlashcardsApp.Models; +using FlashcardsApp.Services; +using FlashcardsApp.UI.Core; +using Spectre.Console; + +namespace FlashcardsApp.UI.Features; + +internal class StudyUI +{ + private readonly DatabaseService _databaseService; + private readonly InputValidator _inputValidator; + + internal StudyUI(DatabaseService databaseService) + { + _databaseService = databaseService; + _inputValidator = new InputValidator(); + } + + internal void StudyMain() + { + bool continueStudy = true; + while (continueStudy) + { + if (!GetStackToStudy(out int stackId)) + { + return; + } + + if (RunStudySession(stackId, out string finalScore)) + { + AddStudySession(stackId, finalScore); + } + + continueStudy = _inputValidator.GetConfirmation("Would you like to study a stack again?"); + } + } + + private bool GetStackToStudy(out int stackId) + { + stackId = -1; + Console.Clear(); + _databaseService.GetAllStacks(); + + List stacks = _databaseService.GetAllStacksAsList(); + if (!stacks.Any()) + { + Console.WriteLine("\nNo stacks found. Create a stack first!"); + Console.WriteLine("\nPress Enter to return to main menu..."); + Console.ReadLine(); + return false; + } + + Dictionary stackMapping = stacks.ToDictionary( + s => $"Name: {s.Name} | {s.Description}", + s => s.StackId); + + var choices = stackMapping.Keys.ToList(); + choices.Add("Return to Main Menu"); + + var selectedStack = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Select a stack to study.") + .AddChoices(choices)); + + if (selectedStack == "Return to Main Menu") + return false; + + stackId = stackMapping[selectedStack]; + return true; + } + + private bool RunStudySession(int stackId, out string finalScore) + { + finalScore = ""; + List flashcards = _databaseService.GetFlashcardsByStackID(stackId); + if (!flashcards.Any()) + { + Console.WriteLine("\nNo flashcards in this stack!"); + Console.WriteLine("\nPress Enter to continue..."); + Console.ReadLine(); + return false; + } + + var studyData = PrepareStudyData(flashcards); + int score = StudyFlashcards(studyData); + + finalScore = $"{score}/{studyData.Count}"; + Console.WriteLine($"\nYou got a score of {finalScore}"); + return true; + } + + private Dictionary PrepareStudyData(List flashcards) + { + return flashcards.ToDictionary( + f => $"Front: {f.Front}", + f => $"{f.Back}"); + } + + private int StudyFlashcards(Dictionary cardMapping) + { + int score = 0; + var flashcardFront = cardMapping.Keys.ToList(); + + Console.WriteLine($"\nPress Enter to Start"); + Console.ReadLine(); + + foreach (var front in flashcardFront) + { + score += ProcessFlashcard(front, cardMapping[front]); + } + + return score; + } + + private int ProcessFlashcard(string front, string correctAnswer) + { + Console.Clear(); + Console.WriteLine(front); + Console.Write("\nAnswer: "); + string? answer = Console.ReadLine()?.Trim().ToLower(); + + bool isCorrect = answer == correctAnswer.ToLower(); + ShowAnswerFeedback(answer, correctAnswer, isCorrect); + + return isCorrect ? 1 : 0; + } + + private void ShowAnswerFeedback(string userAnswer, string correctAnswer, bool isCorrect) + { + Console.WriteLine($"\nAnswer = {correctAnswer.ToLower()}\n" + + $"Your Answer: {userAnswer}\n"); + + AnsiConsole.MarkupLine(isCorrect ? "[green]Correct![/]" : "[red]Incorrect![/]"); + + Console.WriteLine("\nPress enter to Continue"); + Console.ReadLine(); + } + + private void AddStudySession(int stackId, string finalScore) + { + StudySession session = new StudySession(); + + session.StackId = stackId; + session.Score = finalScore; + session.StudyDate = DateTime.Now; + + _databaseService.PostStudySession(session); + } +} diff --git a/FlashcardsApp/FlashcardsApp/UI/Helpers/TableVisualization.cs b/FlashcardsApp/FlashcardsApp/UI/Helpers/TableVisualization.cs new file mode 100644 index 00000000..48f7af57 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/UI/Helpers/TableVisualization.cs @@ -0,0 +1,99 @@ +using FlashcardsApp.DTOs; +using FlashcardsApp.Models; +using Spectre.Console; + +namespace FlashcardsApp.UI.Helpers; + +internal class TableVisualization +{ + internal static void ShowStacksTable(List stacks) + { + Console.WriteLine("\n"); + + var table = new Table(); + + table.AddColumn("ID"); + table.AddColumn("Name"); + table.AddColumn("Description"); + table.AddColumn("Date Created"); + + foreach (var stack in stacks) + { + table.AddRow( + stack.StackId.ToString(), + stack.Name, + stack.Description, + stack.CreatedDate.ToString() + ); + } + + table.Border(TableBorder.Square); + table.BorderColor(Color.Blue); + + AnsiConsole.Write(table); + Console.WriteLine("\n"); + } + + internal static void ShowFlashcardsTable(List flashcards) + { + Console.WriteLine("\n"); + + var table = new Table(); + + table.AddColumn("ID"); + table.AddColumn("Front"); + table.AddColumn("Back"); + table.AddColumn("Date Created"); + + foreach (var card in flashcards) + { + var idPoperty = typeof(FlashcardDTO).GetProperty("DisplayNumber"); + var frontProperty = typeof(FlashcardDTO).GetProperty("Front"); + var backProperty = typeof(FlashcardDTO).GetProperty("Back"); + var createdDateProperty = typeof(FlashcardDTO).GetProperty("CreatedDate"); + + table.AddRow( + idPoperty?.GetValue(card)?.ToString() ?? "NULL", + frontProperty?.GetValue(card)?.ToString() ?? "NULL", + backProperty?.GetValue(card)?.ToString() ?? "NULL", + createdDateProperty?.GetValue(card)?.ToString() ?? "NULL" + ); + } + + table.Border(TableBorder.Square); + table.BorderColor(Color.Green); + + AnsiConsole.Write(table); + Console.WriteLine("\n"); + } + + internal static void ShowStudySessionsTable(List sessions, List stacks) + { + Console.Write("\n"); + + Dictionary stackDictionary = stacks.ToDictionary(s => s.StackId, s => s.Name); + + var table = new Table(); + + table.AddColumn("Stack Studied"); + table.AddColumn("Score"); + table.AddColumn("Study Date"); + + foreach (var session in sessions) + { + string stackName = stackDictionary.GetValueOrDefault(session.StackId, "NULL"); + + table.AddRow( + stackName, + session.Score?.ToString() ?? "NULL", + session.StudyDate.ToString() + ); + } + + table.Border(TableBorder.Square); + table.BorderColor(Color.Orange1); + + AnsiConsole.Write(table); + Console.WriteLine("\n"); + } +} diff --git a/FlashcardsApp/FlashcardsApp/appsettings.json b/FlashcardsApp/FlashcardsApp/appsettings.json new file mode 100644 index 00000000..11f20947 --- /dev/null +++ b/FlashcardsApp/FlashcardsApp/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "Default": "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=FlashcardsDB;Integrated Security=True" + } +}