From b4fe98680acbdf422321c11c7fe5e08f4b5a17e7 Mon Sep 17 00:00:00 2001 From: Hillgrove Date: Fri, 17 Jul 2026 16:45:16 +0200 Subject: [PATCH 1/3] Project Finished! --- PhoneBook.Hillgrove/.vscode/settings.json | 3 + .../PhoneBook.Hillgrove/AppRunner.cs | 111 ++++++++++ .../Controllers/CategoryController.cs | 106 ++++++++++ .../Controllers/ContactController.cs | 197 ++++++++++++++++++ .../Controllers/EmailController.cs | 41 ++++ .../Data/PhoneBookContext.cs | 41 ++++ .../PhoneBook.Hillgrove/Data/SeedData.cs | 122 +++++++++++ .../PhoneBook.Hillgrove/Models/Category.cs | 8 + .../PhoneBook.Hillgrove/Models/Contact.cs | 10 + .../Models/EmailMessage.cs | 3 + .../PhoneBook.Hillgrove/Models/Enums/Enums.cs | 29 +++ .../PhoneBook.Hillgrove.csproj | 19 ++ .../PhoneBook.Hillgrove/Program.cs | 17 ++ .../Repositories/CategoryRepository.cs | 37 ++++ .../Repositories/ContactRepository.cs | 65 ++++++ .../Services/CategoryService.cs | 37 ++++ .../Services/ContactService.cs | 72 +++++++ .../Services/EmailService.cs | 15 ++ .../Validation/Validators.cs | 45 ++++ .../PhoneBook.Hillgrove/Views/CategoryView.cs | 21 ++ .../PhoneBook.Hillgrove/Views/ContactView.cs | 53 +++++ .../PhoneBook.Hillgrove/Views/EmailView.cs | 22 ++ .../PhoneBook.Hillgrove/Views/MenuView.cs | 53 +++++ .../PhoneBook.Hillgrove/Views/MessageView.cs | 21 ++ .../PhoneBook.Hillgrove/Views/PromptView.cs | 81 +++++++ PhoneBook.Hillgrove/PhoneBook.slnx | 3 + 26 files changed, 1232 insertions(+) create mode 100644 PhoneBook.Hillgrove/.vscode/settings.json create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/AppRunner.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/CategoryController.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/ContactController.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/EmailController.cs create mode 100755 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/SeedData.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Category.cs create mode 100755 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Contact.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/EmailMessage.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Enums/Enums.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/PhoneBook.Hillgrove.csproj create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Program.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/CategoryRepository.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/ContactRepository.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/CategoryService.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/ContactService.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/EmailService.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Validation/Validators.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/CategoryView.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/ContactView.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/EmailView.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MenuView.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MessageView.cs create mode 100644 PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/PromptView.cs create mode 100755 PhoneBook.Hillgrove/PhoneBook.slnx diff --git a/PhoneBook.Hillgrove/.vscode/settings.json b/PhoneBook.Hillgrove/.vscode/settings.json new file mode 100644 index 00000000..3ea0ce38 --- /dev/null +++ b/PhoneBook.Hillgrove/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dotnet.automaticallyCreateSolutionInWorkspace": false +} \ No newline at end of file diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/AppRunner.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/AppRunner.cs new file mode 100644 index 00000000..2768c20e --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/AppRunner.cs @@ -0,0 +1,111 @@ +using PhoneBook.Hillgrove.Controllers; +using PhoneBook.Hillgrove.Models.Enums; +using PhoneBook.Hillgrove.Views; + +namespace PhoneBook.Hillgrove; + +internal static class AppRunner +{ + public static void Run() + { + var isRunning = true; + + while (isRunning) + { + var option = MenuView.GetMainMenuOption(); + + switch (option) + { + case MainMenu.Contacts: + RunContactsMenu(); + break; + + case MainMenu.Categories: + RunCategoriesMenu(); + break; + + case MainMenu.SendEmail: + EmailController.SendEmail(); + break; + + case MainMenu.Quit: + isRunning = false; + break; + } + } + } + + private static void RunContactsMenu() + { + var inContactsMenu = true; + + while (inContactsMenu) + { + var option = MenuView.GetContactMenuOption(); + + switch (option) + { + case ContactMenu.AddContact: + ContactController.AddContact(); + break; + + case ContactMenu.UpdateContact: + ContactController.UpdateContact(); + break; + + case ContactMenu.ShowContact: + ContactController.ShowContact(); + break; + + case ContactMenu.ShowContacts: + ContactController.ShowContacts(); + break; + + case ContactMenu.ShowContactsByCategory: + ContactController.ShowContactsByCategory(); + break; + + case ContactMenu.DeleteContact: + ContactController.DeleteContact(); + break; + + case ContactMenu.Back: + inContactsMenu = false; + break; + } + } + } + + private static void RunCategoriesMenu() + { + var inCategoriesMenu = true; + + while (inCategoriesMenu) + { + var option = MenuView.GetCategoryMenuOption(); + + switch (option) + { + case CategoryMenu.AddCategory: + CategoryController.AddCategory(); + break; + + case CategoryMenu.UpdateCategory: + CategoryController.UpdateCategory(); + break; + + case CategoryMenu.ShowCategories: + CategoryController.ShowCategories(); + break; + + case CategoryMenu.DeleteCategory: + CategoryController.DeleteCategory(); + break; + + case CategoryMenu.Back: + inCategoriesMenu = false; + break; + } + } + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/CategoryController.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/CategoryController.cs new file mode 100644 index 00000000..ae5e435e --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/CategoryController.cs @@ -0,0 +1,106 @@ +using PhoneBook.Hillgrove.Services; +using PhoneBook.Hillgrove.Views; + +namespace PhoneBook.Hillgrove.Controllers; + +internal static class CategoryController +{ + public static void AddCategory() + { + var name = PromptView.PromptUserForInput("Category name: "); + + try + { + CategoryService.AddCategory(name); + MessageView.ShowSuccess("Category added."); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to add category. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void UpdateCategory() + { + try + { + var categories = CategoryService.GetCategories(); + var category = PromptView.PromptUserForItemOrNotify( + "Select category: ", + categories, + c => c.Name, + "No categories found." + ); + if (category is null) + return; + + var newName = PromptView.PromptUserForOptionalInput( + $"Name ({category.Name}), leave empty to keep: " + ); + + CategoryService.UpdateCategory( + category, + string.IsNullOrWhiteSpace(newName) ? null : newName + ); + + MessageView.ShowSuccess("Category updated."); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to update category. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void ShowCategories() + { + try + { + var categories = CategoryService.GetCategories(); + + CategoryView.ShowCategories(categories); + } + catch (Exception) + { + MessageView.ShowError("Failed to get categories. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void DeleteCategory() + { + try + { + var categories = CategoryService.GetCategories(); + var category = PromptView.PromptUserForItemOrNotify( + "Select category: ", + categories, + c => c.Name, + "No categories found." + ); + if (category is null) + return; + + CategoryService.DeleteCategory(category); + MessageView.ShowSuccess("Category deleted."); + } + catch (Exception) + { + MessageView.ShowError("Failed to delete category. Please try again."); + } + + PromptView.WaitForKeyPress(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/ContactController.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/ContactController.cs new file mode 100644 index 00000000..e94d6bec --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/ContactController.cs @@ -0,0 +1,197 @@ +using PhoneBook.Hillgrove.Services; +using PhoneBook.Hillgrove.Views; + +namespace PhoneBook.Hillgrove.Controllers; + +internal static class ContactController +{ + public static void AddContact() + { + var name = PromptView.PromptUserForInput("Name: "); + var email = PromptView.PromptUserForInput("Email: "); + var phoneNumber = PromptView.PromptUserForInput("Phonenumber: "); + + var categories = CategoryService.GetCategories(); + var selectedCategories = PromptView.PromptUserForItems( + "Select categories (optional): ", + categories, + c => c.Name, + [] + ); + + try + { + ContactService.AddContact( + name, + email, + phoneNumber, + selectedCategories.Select(c => c.CategoryId).ToList() + ); + MessageView.ShowSuccess("Contact added."); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to add contact. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void UpdateContact() + { + try + { + var contacts = ContactService.GetContacts(); + var contact = PromptView.PromptUserForItemOrNotify( + "Select contact: ", + contacts, + c => c.Name, + "No contacts found." + ); + if (contact is null) + return; + + var newName = PromptView.PromptUserForOptionalInput( + $"Name ({contact.Name}), leave empty to keep: " + ); + var newEmail = PromptView.PromptUserForOptionalInput( + $"Email ({contact.Email}), leave empty to keep: " + ); + var newPhoneNumber = PromptView.PromptUserForOptionalInput( + $"Phonenumber ({contact.PhoneNumber}), leave empty to keep: " + ); + + var categories = CategoryService.GetCategories(); + var preSelected = categories + .Where(c => contact.Categories.Any(cc => cc.CategoryId == c.CategoryId)) + .ToList(); + var selectedCategories = PromptView.PromptUserForItems( + $"Select categories for {contact.Name}: ", + categories, + c => c.Name, + preSelected + ); + + ContactService.UpdateContact( + contact, + selectedCategories.Select(c => c.CategoryId).ToList(), + string.IsNullOrWhiteSpace(newName) ? null : newName, + string.IsNullOrWhiteSpace(newEmail) ? null : newEmail, + string.IsNullOrWhiteSpace(newPhoneNumber) ? null : newPhoneNumber + ); + + MessageView.ShowSuccess("Contact updated."); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to update contact. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void ShowContact() + { + try + { + var contacts = ContactService.GetContacts(); + var contact = PromptView.PromptUserForItemOrNotify( + "Select contact: ", + contacts, + c => c.Name, + "No contacts found." + ); + if (contact is null) + return; + + ContactView.ShowContact(contact); + + PromptView.WaitForKeyPress(); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to get contact. Please try again."); + } + } + + public static void ShowContacts() + { + try + { + var contacts = ContactService.GetContacts(); + + ContactView.ShowContacts(contacts); + } + catch (Exception) + { + MessageView.ShowError("Failed to get contacts. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void ShowContactsByCategory() + { + try + { + var categories = CategoryService.GetCategories(); + var category = PromptView.PromptUserForItemOrNotify( + "Filter by category: ", + categories, + c => c.Name, + "No categories found." + ); + if (category is null) + return; + + var contacts = ContactService.GetContactsByCategory(category.CategoryId); + if (contacts.Count == 0) + MessageView.ShowMessage("No contacts found in this category."); + else + ContactView.ShowContacts(contacts); + } + catch (Exception) + { + MessageView.ShowError("Failed to get contacts. Please try again."); + } + + PromptView.WaitForKeyPress(); + } + + public static void DeleteContact() + { + try + { + var contacts = ContactService.GetContacts(); + var contact = PromptView.PromptUserForItemOrNotify( + "Select contact: ", + contacts, + c => c.Name, + "No contacts found." + ); + if (contact is null) + return; + + ContactService.DeleteContact(contact); + MessageView.ShowSuccess("Contact deleted"); + } + catch (Exception) + { + MessageView.ShowError("Failed to delete contact. Please try again"); + } + + PromptView.WaitForKeyPress(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/EmailController.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/EmailController.cs new file mode 100644 index 00000000..8d792ade --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Controllers/EmailController.cs @@ -0,0 +1,41 @@ +using PhoneBook.Hillgrove.Services; +using PhoneBook.Hillgrove.Views; + +namespace PhoneBook.Hillgrove.Controllers; + +internal static class EmailController +{ + public static void SendEmail() + { + try + { + var contacts = ContactService.GetContacts(); + var contact = PromptView.PromptUserForItemOrNotify( + "Select contact: ", + contacts, + c => c.Name, + "No contacts found." + ); + if (contact is null) + return; + + var subject = PromptView.PromptUserForInput("Subject: "); + var body = PromptView.PromptUserForInput("Message: "); + + var email = EmailService.CreateEmailMessage(subject, body); + + EmailView.ShowSentEmail(contact, email); + MessageView.ShowSuccess("Email sent."); + } + catch (ArgumentException ex) + { + MessageView.ShowError(ex.Message); + } + catch (Exception) + { + MessageView.ShowError("Failed to send email. Please try again."); + } + + PromptView.WaitForKeyPress(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs new file mode 100755 index 00000000..ccc6b926 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs @@ -0,0 +1,41 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; +using PhoneBook.Hillgrove.Models; + +namespace PhoneBook.Hillgrove.Data; + +internal class PhoneBookContext : DbContext +{ + public DbSet Contacts { get; set; } + public DbSet Categories { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + var dbPath = Path.Combine(AppContext.BaseDirectory, "phonebook.db"); + + optionsBuilder + .UseSqlite($"Data Source={dbPath}") + .UseSeeding((context, _) => SeedData.Seed(context)) + .UseAsyncSeeding( + (context, _, CancellationToken) => SeedData.SeedAsync(context, CancellationToken) + ) + // .EnableSensitiveDataLogging() + // .UseLoggerFactory(GetLoggerFactory()) + ; + } + + private ILoggerFactory? GetLoggerFactory() + { + var loggerFactory = LoggerFactory.Create(builder => + { + builder.AddConsole(); + builder.AddFilter( + (category, level) => + category == DbLoggerCategory.Database.Command.Name + && level == LogLevel.Information + ); + }); + + return loggerFactory; + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/SeedData.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/SeedData.cs new file mode 100644 index 00000000..7a064d21 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/SeedData.cs @@ -0,0 +1,122 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Hillgrove.Models; + +namespace PhoneBook.Hillgrove.Data; + +internal class SeedData +{ + public static void Seed(DbContext context) + { + if (context.Set().Any()) + return; + + var categories = GetCategories(); + context.Set().AddRange(categories); + context.Set().AddRange(GetContacts(categories)); + + context.SaveChanges(); + } + + internal static async Task SeedAsync(DbContext context, CancellationToken cancellationToken) + { + if (await context.Set().AnyAsync(cancellationToken)) + return; + + var categories = GetCategories(); + context.Set().AddRange(categories); + context.Set().AddRange(GetContacts(categories)); + + await context.SaveChangesAsync(cancellationToken); + } + + private static List GetCategories() + { + return new List() + { + new() { Name = "Family" }, + new() { Name = "Friends" }, + new() { Name = "Work" }, + }; + } + + private static List GetContacts(List categories) + { + var family = categories[0]; + var friends = categories[1]; + var work = categories[2]; + + return new List() + { + new() + { + Name = "Alice Johnson", + Email = "alice.johnson@example.com", + PhoneNumber = "555-0101", + Categories = [family, friends], + }, + new() + { + Name = "Bob Smith", + Email = "bob.smith@example.com", + PhoneNumber = "555-0102", + Categories = [work], + }, + new() + { + Name = "Carol Williams", + Email = "carol.williams@example.com", + PhoneNumber = "555-0103", + Categories = [family], + }, + new() + { + Name = "David Brown", + Email = "david.brown@example.com", + PhoneNumber = "555-0104", + Categories = [work, friends], + }, + new() + { + Name = "Emma Davis", + Email = "emma.davis@example.com", + PhoneNumber = "555-0105", + Categories = [family], + }, + new() + { + Name = "Frank Miller", + Email = "frank.miller@example.com", + PhoneNumber = "555-0106", + Categories = [work], + }, + new() + { + Name = "Grace Wilson", + Email = "grace.wilson@example.com", + PhoneNumber = "555-0107", + Categories = [friends], + }, + new() + { + Name = "Henry Moore", + Email = "henry.moore@example.com", + PhoneNumber = "555-0108", + Categories = [work, family], + }, + new() + { + Name = "Isabel Taylor", + Email = "isabel.taylor@example.com", + PhoneNumber = "555-0109", + Categories = [friends], + }, + new() + { + Name = "Jack Anderson", + Email = "jack.anderson@example.com", + PhoneNumber = "555-0110", + Categories = [], + }, + }; + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Category.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Category.cs new file mode 100644 index 00000000..19795efb --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Category.cs @@ -0,0 +1,8 @@ +namespace PhoneBook.Hillgrove.Models; + +public class Category +{ + public int CategoryId { get; set; } + public required string Name { get; set; } + public List Contacts { get; set; } = []; +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Contact.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Contact.cs new file mode 100755 index 00000000..b20885e8 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Contact.cs @@ -0,0 +1,10 @@ +namespace PhoneBook.Hillgrove.Models; + +public class Contact +{ + public int ContactId { get; set; } + public required string Name { get; set; } + public required string Email { get; set; } + public required string PhoneNumber { get; set; } + public List Categories { get; set; } = []; +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/EmailMessage.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/EmailMessage.cs new file mode 100644 index 00000000..237999ab --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/EmailMessage.cs @@ -0,0 +1,3 @@ +namespace PhoneBook.Hillgrove.Models; + +internal record EmailMessage(string Subject, string Body); diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Enums/Enums.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Enums/Enums.cs new file mode 100644 index 00000000..5ca85bee --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Models/Enums/Enums.cs @@ -0,0 +1,29 @@ +namespace PhoneBook.Hillgrove.Models.Enums; + +internal enum MainMenu +{ + Contacts, + Categories, + SendEmail, + Quit, +} + +internal enum ContactMenu +{ + AddContact, + UpdateContact, + ShowContact, + ShowContacts, + ShowContactsByCategory, + DeleteContact, + Back, +} + +internal enum CategoryMenu +{ + AddCategory, + UpdateCategory, + ShowCategories, + DeleteCategory, + Back, +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/PhoneBook.Hillgrove.csproj b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/PhoneBook.Hillgrove.csproj new file mode 100644 index 00000000..c0ba0ebc --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/PhoneBook.Hillgrove.csproj @@ -0,0 +1,19 @@ + + + Exe + net10.0 + enable + enable + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Program.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Program.cs new file mode 100644 index 00000000..6d0f9272 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Program.cs @@ -0,0 +1,17 @@ +using PhoneBook.Hillgrove; +using PhoneBook.Hillgrove.Data; +using PhoneBook.Hillgrove.Views; + +try +{ + using var context = new PhoneBookContext(); + context.Database.EnsureDeleted(); + context.Database.EnsureCreated(); +} +catch (Exception) +{ + MessageView.ShowError("Failed to set up the database. Please try again."); + return; +} + +AppRunner.Run(); diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/CategoryRepository.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/CategoryRepository.cs new file mode 100644 index 00000000..950b4c2c --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/CategoryRepository.cs @@ -0,0 +1,37 @@ +using PhoneBook.Hillgrove.Data; +using PhoneBook.Hillgrove.Models; + +namespace PhoneBook.Hillgrove.Repositories; + +internal static class CategoryRepository +{ + public static void AddCategory(Category category) + { + using var db = new PhoneBookContext(); + + db.Categories.Add(category); + db.SaveChanges(); + } + + public static void UpdateCategory(Category category) + { + using var db = new PhoneBookContext(); + + db.Categories.Update(category); + db.SaveChanges(); + } + + public static List GetCategories() + { + using var db = new PhoneBookContext(); + return db.Categories.ToList(); + } + + public static void DeleteCategory(Category category) + { + using var db = new PhoneBookContext(); + + db.Categories.Remove(category); + db.SaveChanges(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/ContactRepository.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/ContactRepository.cs new file mode 100644 index 00000000..4bd9d34d --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Repositories/ContactRepository.cs @@ -0,0 +1,65 @@ +using Microsoft.EntityFrameworkCore; +using PhoneBook.Hillgrove.Data; +using PhoneBook.Hillgrove.Models; + +namespace PhoneBook.Hillgrove.Repositories; + +internal static class ContactRepository +{ + public static void AddContact(Contact contact, List categoryIds) + { + using var db = new PhoneBookContext(); + + if (categoryIds.Count > 0) + contact.Categories = db + .Categories.Where(c => categoryIds.Contains(c.CategoryId)) + .ToList(); + + db.Contacts.Add(contact); + db.SaveChanges(); + } + + public static void UpdateContact(Contact contact, List categoryIds) + { + using var db = new PhoneBookContext(); + + var trackedContact = db + .Contacts.Include(c => c.Categories) + .Single(c => c.ContactId == contact.ContactId); + + trackedContact.Name = contact.Name; + trackedContact.Email = contact.Email; + trackedContact.PhoneNumber = contact.PhoneNumber; + + var selectedCategories = db + .Categories.Where(c => categoryIds.Contains(c.CategoryId)) + .ToList(); + trackedContact.Categories.Clear(); + trackedContact.Categories.AddRange(selectedCategories); + + db.SaveChanges(); + } + + public static List GetContacts() + { + using var db = new PhoneBookContext(); + return db.Contacts.Include(c => c.Categories).ToList(); + } + + public static List GetContactsByCategory(int categoryId) + { + using var db = new PhoneBookContext(); + return db + .Contacts.Include(c => c.Categories) + .Where(c => c.Categories.Any(cat => cat.CategoryId == categoryId)) + .ToList(); + } + + public static void DeleteContact(Contact contact) + { + using var db = new PhoneBookContext(); + + db.Contacts.Remove(contact); + db.SaveChanges(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/CategoryService.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/CategoryService.cs new file mode 100644 index 00000000..9f00142b --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/CategoryService.cs @@ -0,0 +1,37 @@ +using PhoneBook.Hillgrove.Models; +using PhoneBook.Hillgrove.Repositories; +using PhoneBook.Hillgrove.Validation; + +namespace PhoneBook.Hillgrove.Services; + +internal static class CategoryService +{ + public static void AddCategory(string name) + { + Validators.ValidateCategoryName(name); + + var category = new Category { Name = name }; + CategoryRepository.AddCategory(category); + } + + public static void UpdateCategory(Category category, string? newName = null) + { + if (newName != null) + { + Validators.ValidateCategoryName(newName); + category.Name = newName; + } + + CategoryRepository.UpdateCategory(category); + } + + public static void DeleteCategory(Category category) + { + CategoryRepository.DeleteCategory(category); + } + + public static List GetCategories() + { + return CategoryRepository.GetCategories(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/ContactService.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/ContactService.cs new file mode 100644 index 00000000..f429fa3f --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/ContactService.cs @@ -0,0 +1,72 @@ +using PhoneBook.Hillgrove.Models; +using PhoneBook.Hillgrove.Repositories; +using PhoneBook.Hillgrove.Validation; + +namespace PhoneBook.Hillgrove.Services; + +internal static class ContactService +{ + public static void AddContact( + string name, + string email, + string phoneNumber, + List categoryIds + ) + { + Validators.ValidateName(name); + Validators.ValidateEmail(email); + Validators.ValidatePhoneNumber(phoneNumber); + + var contact = new Contact + { + Name = name, + Email = email, + PhoneNumber = phoneNumber, + }; + ContactRepository.AddContact(contact, categoryIds); + } + + public static void UpdateContact( + Contact contact, + List categoryIds, + string? newName = null, + string? newEmail = null, + string? newPhoneNumber = null + ) + { + if (newName != null) + { + Validators.ValidateName(newName); + contact.Name = newName; + } + + if (newEmail != null) + { + Validators.ValidateEmail(newEmail); + contact.Email = newEmail; + } + + if (newPhoneNumber != null) + { + Validators.ValidatePhoneNumber(newPhoneNumber); + contact.PhoneNumber = newPhoneNumber; + } + + ContactRepository.UpdateContact(contact, categoryIds); + } + + public static void DeleteContact(Contact contact) + { + ContactRepository.DeleteContact(contact); + } + + public static List GetContacts() + { + return ContactRepository.GetContacts(); + } + + public static List GetContactsByCategory(int categoryId) + { + return ContactRepository.GetContactsByCategory(categoryId); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/EmailService.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/EmailService.cs new file mode 100644 index 00000000..67548e6f --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Services/EmailService.cs @@ -0,0 +1,15 @@ +using PhoneBook.Hillgrove.Models; +using PhoneBook.Hillgrove.Validation; + +namespace PhoneBook.Hillgrove.Services; + +internal static class EmailService +{ + public static EmailMessage CreateEmailMessage(string subject, string body) + { + Validators.ValidateSubject(subject); + Validators.ValidateBody(body); + + return new EmailMessage(subject, body); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Validation/Validators.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Validation/Validators.cs new file mode 100644 index 00000000..f9be851f --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Validation/Validators.cs @@ -0,0 +1,45 @@ +using System.Text.RegularExpressions; + +namespace PhoneBook.Hillgrove.Validation; + +internal static class Validators +{ + private static readonly Regex EmailPattern = new(@"^[^@\s]+@[^@\s]+\.[^@\s]+$"); + private static readonly Regex PhonePattern = new(@"^\d{8}$"); + + public static void ValidateName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Name cannot be empty."); + } + + public static void ValidateEmail(string email) + { + if (!EmailPattern.IsMatch(email)) + throw new ArgumentException("Invalid email format. Expected: name@example.com"); + } + + public static void ValidatePhoneNumber(string phoneNumber) + { + if (!PhonePattern.IsMatch(phoneNumber)) + throw new ArgumentException("Invalid phone number. Expected: 8 digits, e.g. 12345678"); + } + + public static void ValidateSubject(string subject) + { + if (string.IsNullOrWhiteSpace(subject)) + throw new ArgumentException("Subject cannot be empty."); + } + + public static void ValidateBody(string body) + { + if (string.IsNullOrWhiteSpace(body)) + throw new ArgumentException("Message cannot be empty."); + } + + public static void ValidateCategoryName(string name) + { + if (string.IsNullOrWhiteSpace(name)) + throw new ArgumentException("Category name cannot be empty."); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/CategoryView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/CategoryView.cs new file mode 100644 index 00000000..26cc7a54 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/CategoryView.cs @@ -0,0 +1,21 @@ +using PhoneBook.Hillgrove.Models; +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal class CategoryView +{ + public static void ShowCategories(List categories) + { + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("Name"); + + foreach (var category in categories) + { + table.AddRow(category.CategoryId.ToString(), category.Name); + } + + AnsiConsole.Write(table); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/ContactView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/ContactView.cs new file mode 100644 index 00000000..a70a8112 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/ContactView.cs @@ -0,0 +1,53 @@ +using PhoneBook.Hillgrove.Models; +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal class ContactView +{ + public static void ShowContacts(List contacts) + { + var table = new Table(); + table.AddColumn("Id"); + table.AddColumn("Name"); + table.AddColumn("Email"); + table.AddColumn("Phone Number"); + table.AddColumn("Categories"); + + foreach (var contact in contacts) + { + table.AddRow( + contact.ContactId.ToString(), + contact.Name, + contact.Email, + contact.PhoneNumber, + FormatCategories(contact) + ); + } + + AnsiConsole.Write(table); + } + + internal static void ShowContact(Contact contact) + { + var panel = new Panel( + $@"Id: {contact.ContactId} +Name: {contact.Name} +Email: {contact.Email} +Phone Number: {contact.PhoneNumber} +Categories: {FormatCategories(contact)}" + ); + + panel.Header = new PanelHeader("Contact Info"); + panel.Padding = new Padding(2, 1, 2, 1); + + AnsiConsole.Write(panel); + } + + private static string FormatCategories(Contact contact) + { + return contact.Categories.Count > 0 + ? string.Join(", ", contact.Categories.Select(c => c.Name)) + : "-"; + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/EmailView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/EmailView.cs new file mode 100644 index 00000000..de054592 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/EmailView.cs @@ -0,0 +1,22 @@ +using PhoneBook.Hillgrove.Models; +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal static class EmailView +{ + public static void ShowSentEmail(Contact contact, EmailMessage email) + { + var panel = new Panel( + $@"To: {contact.Name} <{contact.Email}> +Subject: {email.Subject} + +{email.Body}" + ); + + panel.Header = new PanelHeader("Email Sent"); + panel.Padding = new Padding(2, 1, 2, 1); + + AnsiConsole.Write(panel); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MenuView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MenuView.cs new file mode 100644 index 00000000..b7622b89 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MenuView.cs @@ -0,0 +1,53 @@ +using PhoneBook.Hillgrove.Models.Enums; +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal static class MenuView +{ + public static MainMenu GetMainMenuOption() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Phonebook Mainmenu") + .AddChoices( + MainMenu.Contacts, + MainMenu.Categories, + MainMenu.SendEmail, + MainMenu.Quit + ) + ); + } + + public static ContactMenu GetContactMenuOption() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Contacts Menu") + .AddChoices( + ContactMenu.AddContact, + ContactMenu.UpdateContact, + ContactMenu.ShowContact, + ContactMenu.ShowContacts, + ContactMenu.ShowContactsByCategory, + ContactMenu.DeleteContact, + ContactMenu.Back + ) + ); + } + + public static CategoryMenu GetCategoryMenuOption() + { + return AnsiConsole.Prompt( + new SelectionPrompt() + .Title("Categories Menu") + .AddChoices( + CategoryMenu.AddCategory, + CategoryMenu.UpdateCategory, + CategoryMenu.ShowCategories, + CategoryMenu.DeleteCategory, + CategoryMenu.Back + ) + ); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MessageView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MessageView.cs new file mode 100644 index 00000000..50f0754d --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/MessageView.cs @@ -0,0 +1,21 @@ +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal static class MessageView +{ + public static void ShowMessage(string message) + { + AnsiConsole.MarkupLine($"[yellow]{message}[/]"); + } + + public static void ShowSuccess(string message) + { + AnsiConsole.MarkupLine($"[green]{message}[/]"); + } + + public static void ShowError(string message) + { + AnsiConsole.MarkupLine($"[red]{message}[/]"); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/PromptView.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/PromptView.cs new file mode 100644 index 00000000..cdfc853b --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Views/PromptView.cs @@ -0,0 +1,81 @@ +using Spectre.Console; + +namespace PhoneBook.Hillgrove.Views; + +internal static class PromptView +{ + public static T PromptUserForInput(string prompt) + { + return AnsiConsole.Ask(prompt); + } + + public static string PromptUserForOptionalInput(string prompt) + { + return AnsiConsole.Prompt(new TextPrompt(prompt).AllowEmpty()); + } + + public static T PromptUserForItem(string title, List items, Func display) + where T : notnull + { + var selectedItem = AnsiConsole.Prompt( + new SelectionPrompt().Title(title).UseConverter(display).AddChoices(items) + ); + + return selectedItem; + } + + public static T? PromptUserForItemOrNotify( + string title, + List items, + Func display, + string emptyMessage + ) + where T : class + { + if (items.Count == 0) + { + MessageView.ShowMessage(emptyMessage); + WaitForKeyPress(); + return null; + } + + return PromptUserForItem(title, items, display); + } + + public static List PromptUserForItems( + string title, + List items, + Func display, + List preSelected + ) + where T : notnull + { + if (items.Count == 0) + return []; + + var prompt = new MultiSelectionPrompt() + .Title(title) + .NotRequired() + .UseConverter(display) + .AddChoices(items); + + foreach (var item in preSelected) + { + prompt.Select(item); + } + + return AnsiConsole.Prompt(prompt); + } + + public static bool PromptUserForConfirmation(string message) + { + return AnsiConsole.Confirm(message); + } + + public static void WaitForKeyPress() + { + AnsiConsole.Write("Press any key to continue..."); + Console.ReadKey(); + AnsiConsole.Clear(); + } +} diff --git a/PhoneBook.Hillgrove/PhoneBook.slnx b/PhoneBook.Hillgrove/PhoneBook.slnx new file mode 100755 index 00000000..eede5ec8 --- /dev/null +++ b/PhoneBook.Hillgrove/PhoneBook.slnx @@ -0,0 +1,3 @@ + + + From 9232bc2eb9bb8477aa4ec2e2f97bbbf80ec23d9b Mon Sep 17 00:00:00 2001 From: Hillgrove Date: Fri, 17 Jul 2026 18:28:07 +0200 Subject: [PATCH 2/3] refactor(PhoneBook.Hillgrove): remove unused logging code and clean up configuration --- .../Data/PhoneBookContext.cs | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs index ccc6b926..6709b3cc 100755 --- a/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs +++ b/PhoneBook.Hillgrove/PhoneBook.Hillgrove/Data/PhoneBookContext.cs @@ -1,5 +1,4 @@ using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; using PhoneBook.Hillgrove.Models; namespace PhoneBook.Hillgrove.Data; @@ -17,25 +16,7 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) .UseSqlite($"Data Source={dbPath}") .UseSeeding((context, _) => SeedData.Seed(context)) .UseAsyncSeeding( - (context, _, CancellationToken) => SeedData.SeedAsync(context, CancellationToken) - ) - // .EnableSensitiveDataLogging() - // .UseLoggerFactory(GetLoggerFactory()) - ; - } - - private ILoggerFactory? GetLoggerFactory() - { - var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddConsole(); - builder.AddFilter( - (category, level) => - category == DbLoggerCategory.Database.Command.Name - && level == LogLevel.Information + (context, _, cancellationToken) => SeedData.SeedAsync(context, cancellationToken) ); - }); - - return loggerFactory; } } From 37712a9c0113c91967e6868ffac40c4f7429ed13 Mon Sep 17 00:00:00 2001 From: Hillgrove Date: Fri, 17 Jul 2026 18:28:39 +0200 Subject: [PATCH 3/3] add(readme): create initial README with project details and features --- PhoneBook.Hillgrove/README.md | 64 +++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 PhoneBook.Hillgrove/README.md diff --git a/PhoneBook.Hillgrove/README.md b/PhoneBook.Hillgrove/README.md new file mode 100644 index 00000000..9a9b7e0b --- /dev/null +++ b/PhoneBook.Hillgrove/README.md @@ -0,0 +1,64 @@ +# PhoneBook.Hillgrove + +PhoneBook.Hillgrove is a console-based phonebook app built with .NET and EF Core. It starts by creating a fresh SQLite database, then opens a menu-driven interface where you can manage contacts, categories, and a fake email action that is used as a simple demo instead of a real mail system. + +## Features + +- Add, update, view, and delete contacts +- Group contacts into categories and filter contacts by category +- Send a simulated email to a contact + +## Tech stack + +- .NET 10 / C# +- Entity Framework Core with SQLite +- Spectre.Console for the terminal UI + +## Getting started + +Requires the .NET 10 SDK. From the repo root: + +``` +dotnet run --project PhoneBook.Hillgrove +``` + +## How the app works + +The app follows a simple flow: + +1. On startup it resets and recreates the database. +2. The main menu lets you choose between contacts, categories, sending email, or quitting. +3. Inside the contacts menu, you can add, update, view, filter by category, and delete contacts. +4. Inside the categories menu, you can add, update, view, and delete categories. +5. The email option lets you pick a contact, enter a subject and message, and preview the sent email. It is intentionally fake: built to look like a real email service from the caller's side, but it doesn't actually send anything, so I didn't need to wire up real SMTP credentials or an email provider just for this project. + +The database is deleted and recreated on each run so every start begins from the same known state, which made it easier to test the menus and data flow without old data getting in the way. This also matches how the tutorial video I followed handled it. + +## Challenges + +The project brief included a few challenge ideas: + +- Create a functionality that allows users to add the contact's e-mail address and send an e-mail message from the app: done. The app stores contact emails and includes a send-email flow. +- Expand the app by creating categories of contacts (i.e. Family, Friends, Work, etc): done. Contacts can be grouped into categories and filtered by them. +- What if you want to send not only e-mails but SMS?: not implemented. The cleanest next step would be a shared notification interface, such as a common sender abstraction with `Send(...)`, backed by email and SMS implementations. +- Create a unit tests project and test your validation methods, passing several invalid and valid inputs: not done. The validators are simple guard clauses with little regression risk, and testing them felt like practice for its own sake rather than something that would change the project. I'd rather spend that time on the next project, even if it costs some testing practice. + +My take on the SMS idea. Without a refactor, this would mean duplicating the whole flow (pick a contact, enter a message, confirm) for a second feature that's nearly identical to the first. If I did refactor, I would normally reach for interfaces and depend on an abstraction instead of concrete classes. A template method is another option, where a shared base class holds the flow and only the delivery step gets overridden per channel. Interfaces scale better if the two channels start differing in more than just delivery, while a template method is simpler if delivery really is the only difference. + +## Thought process + +I worked on the app one slice at a time, or feature by feature, revisiting the architecture along the way since it's more of an n-layered design than a strict MVC one. I also thought about using async, but it was not necessary for this app since it is only one user and one thread. + +I used both `UseSeeding` and `UseSeedingAsync` because EF Core expects both to be set up together. The app only calls the sync path today, but this way the same seed logic keeps working if that ever changes to async. + +## What was hard? + +Figuring out each layer's responsibility took some getting used to, since the architecture looks a bit like MVC but is really n-layered. EF Core was also new to me, so seeding and relationships took extra time to click. + +## What was easy? + +Flow control, input handling, and the overall menu structure came together quickly. + +## What have I learned? + +How EF Core's newer seeding hooks (`UseSeeding`/`UseSeedingAsync`) work, compared to older approaches like `HasData`. \ No newline at end of file