From 5ccf25e7afd70bc4285c9b750851edcb6e5ecf9d Mon Sep 17 00:00:00 2001 From: sange Date: Sun, 26 Jul 2026 00:10:00 +0530 Subject: [PATCH] Project Finished Initial implementation of DrinkExplorer console app Added solution, project, and core files for DrinkExplorer. Set up database schema, configuration, and main logic for browsing, searching, and managing drinks and favorites. Implemented Spectre.Console-based UI, models, repository, and services. Included comprehensive README and SQL setup script. --- Sangeerths.DrinkExplorer/DrinkExplorer.slnx | 3 + .../DrinkExplorer/Controller/Menu.cs | 160 +++++++++++ .../DrinkExplorer/DrinkExplorer.csproj | 29 ++ .../DrinkExplorer/Models/CategoryResponse.cs | 16 ++ .../DrinkExplorer/Models/Drink.cs | 13 + .../DrinkExplorer/Models/DrinkResponse.cs | 8 + .../DrinkExplorer/Models/DrinkView.cs | 11 + .../DrinkExplorer/Program.cs | 4 + .../Repository/FavoriteDrinkRepository.cs | 107 ++++++++ .../Services/FavouriteDrinkService.cs | 107 ++++++++ .../Services/ManageDrinksService.cs | 256 ++++++++++++++++++ .../DrinkExplorer/Validation/ConsoleUI.cs | 68 +++++ .../Validation/InputValidator.cs | 22 ++ .../DrinkExplorer/appsettings.json | 5 + Sangeerths.DrinkExplorer/README.md | 88 ++++++ Sangeerths.DrinkExplorer/scripts.sql | Bin 0 -> 9182 bytes 16 files changed, 897 insertions(+) create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer.slnx create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Controller/Menu.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/DrinkExplorer.csproj create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Models/CategoryResponse.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Models/Drink.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkResponse.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkView.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Program.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Repository/FavoriteDrinkRepository.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Services/FavouriteDrinkService.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Services/ManageDrinksService.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Validation/ConsoleUI.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/Validation/InputValidator.cs create mode 100644 Sangeerths.DrinkExplorer/DrinkExplorer/appsettings.json create mode 100644 Sangeerths.DrinkExplorer/README.md create mode 100644 Sangeerths.DrinkExplorer/scripts.sql diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer.slnx b/Sangeerths.DrinkExplorer/DrinkExplorer.slnx new file mode 100644 index 00000000..b09f0888 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Controller/Menu.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Controller/Menu.cs new file mode 100644 index 00000000..51670cb6 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Controller/Menu.cs @@ -0,0 +1,160 @@ +using DrinkExplorer.Services; +using DrinkExplorer.Validation; +using Spectre.Console; + +namespace DrinkExplorer.Controller +{ + public class Menu + { + private readonly ConsoleUI _consoleUI; + private readonly ManageDrinksService _manageDrinksService; + private readonly FavouriteDrinkService _favouriteDrinkService; + private enum CategoryOptions + { + DrinksMenu, + MyFavourites, + Exit + } + + private enum DrinksOptions + { + ViewAll, + SearchDrinks, + Goback + } + + private enum MyFavouritesOptions + { + ViewAll, + ViewCount, + GoBack + } + + private T PromptEnum(string title, params (string Label, T Value)[] options) where T : struct + { + var labels = new List(); + var map = new Dictionary(); + foreach (var (label, value) in options) + { + labels.Add(label); + map[label] = value; + } + + string selected = AnsiConsole.Prompt( + new SelectionPrompt() + .Title(title) + .AddChoices(labels)); + + return map[selected]; + } + + public Menu() + { + _consoleUI = new ConsoleUI(); + _manageDrinksService = new ManageDrinksService(); + _favouriteDrinkService = new FavouriteDrinkService(); + } + + public async Task Start() + { + var running = true; + while (running) + { + _consoleUI.ShowHeader(); + var choice = PromptEnum( + "Choose your operation", + ("Drinks Menu", CategoryOptions.DrinksMenu), + ("My Favourites", CategoryOptions.MyFavourites), + ("Exit", CategoryOptions.Exit)); + + switch (choice) + { + case CategoryOptions.DrinksMenu: + await ManageDrinksAsync(); + break; + case CategoryOptions.MyFavourites: + ManageMyFavourites(); + break; + case CategoryOptions.Exit: + running = false; + _consoleUI.ShowGoodbye(); + break; + } + } + } + + private async Task ManageDrinksAsync() + { + var isSubMenu = true; + while (isSubMenu) + { + _consoleUI.ShowHeader(); + var choice = PromptEnum( + "Choose your operation", + ("View All Categories", DrinksOptions.ViewAll), + ("Search Drink", DrinksOptions.SearchDrinks), + ("Go Back", DrinksOptions.Goback)); + + try + { + switch (choice) + { + case DrinksOptions.ViewAll: + await _manageDrinksService.ViewAllCategoriesAsync(); + break; + case DrinksOptions.SearchDrinks: + await _manageDrinksService.SearchDrinksAsync(); + break; + case DrinksOptions.Goback: + isSubMenu = false; + break; + } + } + catch (Exception ex) + { + _consoleUI.ShowError(ex.Message); + _consoleUI.Pause(); + } + } + } + + private void ManageMyFavourites() + { + var isSubMenu = true; + while (isSubMenu) + { + _consoleUI.ShowHeader(); + var choice = PromptEnum( + "Choose your operation", + ("View All Favourite Drinks", MyFavouritesOptions.ViewAll), + ("Most Viewed Drink", MyFavouritesOptions.ViewCount), + ("Go Back", MyFavouritesOptions.GoBack)); + + try + { + switch (choice) + { + case MyFavouritesOptions.ViewAll: + _favouriteDrinkService.ViewAllDrinks(); + _consoleUI.Pause(); + break; + case MyFavouritesOptions.ViewCount: + _favouriteDrinkService.DisplayMostViewedDrink(); + _consoleUI.Pause(); + break; + case MyFavouritesOptions.GoBack: + isSubMenu = false; + break; + } + + AnsiConsole.Clear(); + } + catch (Exception ex) + { + _consoleUI.ShowError(ex.Message); + _consoleUI.Pause(); + } + } + } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/DrinkExplorer.csproj b/Sangeerths.DrinkExplorer/DrinkExplorer/DrinkExplorer.csproj new file mode 100644 index 00000000..881d8547 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/DrinkExplorer.csproj @@ -0,0 +1,29 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + PreserveNewest + + + + + + + + + + + + + diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Models/CategoryResponse.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/CategoryResponse.cs new file mode 100644 index 00000000..8c49bc29 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/CategoryResponse.cs @@ -0,0 +1,16 @@ +using System.Text.Json.Serialization; + +namespace DrinkExplorer.Models +{ + public class CategoryResponse + { + [JsonPropertyName("drinks")] + public List? Drinks { get; set; } + } + + public class Category + { + [JsonPropertyName("strCategory")] + public string? StrCategory { get; set; } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Models/Drink.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/Drink.cs new file mode 100644 index 00000000..f530175a --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/Drink.cs @@ -0,0 +1,13 @@ +namespace DrinkExplorer.Models +{ + public class Drink + { + public string? IdDrink { get; set; } + public string? StrDrink { get; set; } + public string? StrDrinkThumb { get; set; } + public string? StrCategory { get; set; } + public string? StrAlcoholic { get; set; } + public string? StrGlass { get; set; } + public string? StrInstructions { get; set; } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkResponse.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkResponse.cs new file mode 100644 index 00000000..87e697e7 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkResponse.cs @@ -0,0 +1,8 @@ +namespace DrinkExplorer.Models +{ + public class DrinkResponse + { + public List? Drinks { get; set; } + + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkView.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkView.cs new file mode 100644 index 00000000..041a524c --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Models/DrinkView.cs @@ -0,0 +1,11 @@ +namespace DrinkExplorer.Models +{ + public class DrinkView + { + public string DrinkId { get; set; } = string.Empty; + + public string DrinkName { get; set; } = string.Empty; + + public int ViewCount { get; set; } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Program.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Program.cs new file mode 100644 index 00000000..d78b544c --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Program.cs @@ -0,0 +1,4 @@ +using DrinkExplorer.Controller; + +Menu menu = new Menu(); +await menu.Start(); \ No newline at end of file diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Repository/FavoriteDrinkRepository.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Repository/FavoriteDrinkRepository.cs new file mode 100644 index 00000000..a21ce424 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Repository/FavoriteDrinkRepository.cs @@ -0,0 +1,107 @@ +using DrinkExplorer.Models; +using Microsoft.Data.SqlClient; +using Microsoft.Extensions.Configuration; + +namespace DrinkExplorer.Database +{ + public class FavoriteDrinkRepository + { + private readonly string _connectionString; + + public FavoriteDrinkRepository() + { + var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) + .Build(); + + _connectionString = configuration.GetConnectionString("DefaultConnection") + ?? throw new InvalidOperationException( + "Connection string 'DefaultConnection' not found in appsettings.json."); + } + + public void InsertFavouriteDrink(Drink drink) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + using var command = new SqlCommand("INSERT INTO FavouriteDrinks (DrinkId, DrinkName, Category, Alcoholic, Glass, Instructions, DrinkImageUrl) VALUES (@IdDrink, @StrDrink, @StrCategory, @StrAlcoholic, @StrGlass, @StrInstructions, @StrDrinkThumb)", connection); + command.Parameters.AddWithValue("@IdDrink", drink.IdDrink); + command.Parameters.AddWithValue("@StrDrink", drink.StrDrink); + command.Parameters.AddWithValue("@StrCategory", drink.StrCategory); + command.Parameters.AddWithValue("@StrAlcoholic", drink.StrAlcoholic); + command.Parameters.AddWithValue("@StrGlass", drink.StrGlass); + command.Parameters.AddWithValue("@StrInstructions", drink.StrInstructions); + command.Parameters.AddWithValue("@StrDrinkThumb", drink.StrDrinkThumb); + command.ExecuteNonQuery(); + } + + public DrinkResponse ShowAllFavourites() + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + using var command = new SqlCommand("SELECT * FROM FavouriteDrinks ORDER BY DrinkName", connection); + using var reader = command.ExecuteReader(); + var favouriteDrinks = new DrinkResponse + { + Drinks = new List() + }; + + while (reader.Read()) + { + favouriteDrinks.Drinks.Add(new Drink + { + IdDrink = reader.GetString(1), + StrDrink = reader.GetString(2), + StrCategory = reader.IsDBNull(3) ? null : reader.GetString(3), + StrAlcoholic = reader.IsDBNull(4) ? null : reader.GetString(4), + StrGlass = reader.IsDBNull(5) ? null : reader.GetString(5), + StrInstructions = reader.IsDBNull(6) ? null : reader.GetString(6), + StrDrinkThumb = reader.IsDBNull(7) ? null : reader.GetString(7) + }); + } + return favouriteDrinks; + } + + public void IncrementViewCount(Drink drink) + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + const string query = """ + IF EXISTS + (SELECT 1 FROM DrinkViews WHERE DrinkId = @DrinkId) + BEGIN + UPDATE DrinkViews SET ViewCount = ViewCount + 1 WHERE DrinkId = @DrinkId + END + ELSE + BEGIN + INSERT INTO DrinkViews (DrinkId, DrinkName, ViewCount ) VALUES( @DrinkId, @DrinkName, 1) + END + """; + + using var command = new SqlCommand(query, connection); + command.Parameters.AddWithValue("@DrinkId",drink.IdDrink); + command.Parameters.AddWithValue("@DrinkName", drink.StrDrink); + command.ExecuteNonQuery(); + } + + public List GetMostViewedDrinks() + { + using var connection = new SqlConnection(_connectionString); + connection.Open(); + using var command = new SqlCommand("SELECT * FROM DrinkViews ORDER BY ViewCount DESC", connection); + using var reader = command.ExecuteReader(); + var mostViewedDrinks = new List(); + + while (reader.Read()) + { + mostViewedDrinks.Add(new DrinkView + { + DrinkId = reader.GetString(0), + DrinkName = reader.GetString(1), + ViewCount = reader.GetInt32(2) + }); + } + return mostViewedDrinks; + } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Services/FavouriteDrinkService.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Services/FavouriteDrinkService.cs new file mode 100644 index 00000000..f9671a64 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Services/FavouriteDrinkService.cs @@ -0,0 +1,107 @@ +using DrinkExplorer.Database; +using DrinkExplorer.Models; +using DrinkExplorer.Validation; +using Spectre.Console; + +namespace DrinkExplorer.Services +{ + public class FavouriteDrinkService + { + private readonly FavoriteDrinkRepository _favoriteDrinkRepository; + private readonly ConsoleUI _consoleUI; + + public FavouriteDrinkService() + { + _consoleUI = new ConsoleUI(); + _favoriteDrinkRepository = new FavoriteDrinkRepository(); + } + + public void ViewAllDrinks() + { + DrinkResponse favouriteDrinks = _favoriteDrinkRepository.ShowAllFavourites(); + if (favouriteDrinks == null || favouriteDrinks.Drinks.Count == 0) + { + _consoleUI.ShowError("No Favourite Drinks found"); + return; + } + + var table = new Table + { + Border = TableBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Expand = true + }; + table.AddColumn("[bold cyan]# [/]"); + table.AddColumn("[bold cyan]Drink Name[/]"); + table.AddColumn("[bold cyan]Category[/]"); + table.AddColumn("[bold cyan]Alcoholic[/]"); + table.AddColumn("[bold cyan]Glass[/]"); + + for (int i = 0; i < favouriteDrinks.Drinks.Count; i++) + { + var drink = favouriteDrinks.Drinks[i]; + table.AddRow( + (i + 1).ToString(), + Markup.Escape(drink.StrDrink ?? "N/A"), + Markup.Escape(drink.StrCategory ?? "N/A"), + Markup.Escape(drink.StrAlcoholic ?? "N/A"), + Markup.Escape(drink.StrGlass ?? "N/A")); + } + + var panel = new Panel(table) + { + Header = new PanelHeader( + "[bold aqua]MY FAVOURITE DRINKS[/]"), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Padding = new Padding(1, 1) + }; + + AnsiConsole.Write(Align.Center(panel)); + } + + public void DisplayMostViewedDrink() + { + List mostViewedDrinks = _favoriteDrinkRepository.GetMostViewedDrinks(); + if(mostViewedDrinks == null || mostViewedDrinks.Count == 0) + { + _consoleUI.ShowError("No drink view data found."); + return; + } + + var table = new Table + { + Border = TableBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Expand = true + }; + table.AddColumn("[bold cyan]# [/]"); + table.AddColumn("[bold cyan]Drink Id[/]"); + table.AddColumn("[bold cyan]Drink Name[/]"); + table.AddColumn(new TableColumn("[bold cyan]View Count[/]").RightAligned()); + + for (int i = 0; i < mostViewedDrinks.Count;i++) + { + var drink = mostViewedDrinks[i]; + table.AddRow( + (i + 1).ToString(), + Markup.Escape(drink.DrinkId ?? "[grey]Unknown[/]"), + Markup.Escape(drink.DrinkName ?? "[grey]Unknown[/]"), + Markup.Escape(drink.ViewCount.ToString() ?? "[grey]Unknown[/]") + ); + } + + var panel = new Panel(table) + { + Header = new PanelHeader( + "[bold aqua]MY MOST VIEWED DRINKS[/]"), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Padding = new Padding(1, 1) + }; + + AnsiConsole.Write(Align.Center(panel)); + } + + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Services/ManageDrinksService.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Services/ManageDrinksService.cs new file mode 100644 index 00000000..3805fe35 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Services/ManageDrinksService.cs @@ -0,0 +1,256 @@ +using DrinkExplorer.Database; +using DrinkExplorer.Models; +using DrinkExplorer.Validation; +using SixLabors.ImageSharp; +using Spectre.Console; +using System.Diagnostics; +using System.Net.Http.Json; + +namespace DrinkExplorer.Services +{ + public class ManageDrinksService + { + private readonly HttpClient _httpClient; + private readonly ConsoleUI _consoleUI; + private readonly InputValidator _inputValidator; + private readonly FavoriteDrinkRepository _favoriteDrinkRepository; + + public ManageDrinksService() + { + _httpClient = new HttpClient + { + BaseAddress = new Uri("https://www.thecocktaildb.com/api/json/v1/1/") + }; + _consoleUI = new ConsoleUI(); + _inputValidator = new InputValidator(); + _favoriteDrinkRepository = new FavoriteDrinkRepository(); + } + + public async Task ViewAllCategoriesAsync() + { + var categories = await GetCategoriesAsync(); + if (categories.Count == 0) + { + _consoleUI.ShowError("No categories found."); + _consoleUI.Pause(); + return; + } + + var selectedCategory = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[bold cyan]Select a category[/]") + .AddChoices(categories)); + + var drinks = await GetDrinksByCategoryAsync(selectedCategory); + if (drinks.Count == 0) + { + _consoleUI.ShowError($"No drinks found in {selectedCategory}."); + _consoleUI.Pause(); + return; + } + + var selectedDrink = AnsiConsole.Prompt( + new SelectionPrompt() + .Title($"[bold cyan]Select a drink from {selectedCategory}[/]") + .UseConverter(drink =>drink.StrDrink ?? "Unknown Drink") + .AddChoices(drinks)); + + var drinkDetails = await GetDrinkByIdAsync(selectedDrink.IdDrink!); + + if (drinkDetails == null) + { + _consoleUI.ShowError("Unable to retrieve drink details."); + _consoleUI.Pause(); + return; + } + _favoriteDrinkRepository.IncrementViewCount(drinkDetails); + await DisplayDrinkDetailsAsync(drinkDetails); + } + + public async Task SearchDrinksAsync() + { + try + { + var drinkName = AnsiConsole.Ask("[bold cyan]Enter the drink name to search:[/]"); + if (!_inputValidator.ValidateDrinkName(drinkName)) + { + _consoleUI.ShowError("Error fetching the Drink"); + _consoleUI.Pause(); + return; + } + + var drinks = await SearchDrinkFromAPI(drinkName); + if (drinks.Count == 0) + { + _consoleUI.ShowError($"No drinks found for '{drinkName}'."); + _consoleUI.Pause(); + return; + } + var selectedDrink = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[bold cyan]Select a drink[/]") + .UseConverter(drink => + drink.StrDrink ?? "Unknown Drink") + .AddChoices(drinks)); + var drinkDetails = await GetDrinkByIdAsync(selectedDrink.IdDrink!); + if (drinkDetails == null) + { + _consoleUI.ShowError("Unable to retrieve drink details."); + _consoleUI.Pause(); + return; + } + + _favoriteDrinkRepository.IncrementViewCount(drinkDetails); + await DisplayDrinkDetailsAsync(drinkDetails); + } + catch (Exception ex) + { + _consoleUI.ShowError(ex.Message); + _consoleUI.Pause(); + } + } + private async Task DisplayDrinkDetailsAsync(Drink drink) + { + if (string.IsNullOrWhiteSpace(drink.StrDrinkThumb)) + { + _consoleUI.ShowError("Drink image is not available."); + _consoleUI.Pause(); + return; + } + + var table = new Table + { + Border = TableBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Expand = true + }; + table.AddColumn(new TableColumn("[bold cyan]Property[/]").Width(15)); + table.AddColumn(new TableColumn("[bold cyan]Details[/]")); + table.AddRow("[bold]Name[/]",Markup.Escape(drink.StrDrink ?? "N/A")); + table.AddRow("[bold]Category[/]",Markup.Escape(drink.StrCategory ?? "N/A")); + table.AddRow("[bold]Alcoholic[/]",Markup.Escape(drink.StrAlcoholic ?? "N/A")); + table.AddRow("[bold]Glass[/]",Markup.Escape(drink.StrGlass ?? "N/A")); + table.AddRow("[bold]Instructions[/]",Markup.Escape(drink.StrInstructions ?? "N/A")); + + var panel = new Panel(table) + { + Header = new PanelHeader("[bold aqua]DRINK DETAILS[/]"), + Border = BoxBorder.Rounded, + BorderStyle = new Style(Spectre.Console.Color.Aqua), + Padding = new Padding(1, 1), + Expand = true + }; + + while (true) + { + AnsiConsole.Clear(); + _consoleUI.ShowHeader(); + AnsiConsole.Write(Align.Center(panel)); + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[bold cyan]What would you like to do?[/]") + .AddChoices( + "Open Drink Image", + "Add to Favourites", + "Go Back")); + + switch (choice) + { + case "Open Drink Image": + OpenDrinkImage(drink.StrDrinkThumb); + _consoleUI.ShowInfo("Opening drink image in your browser..."); + _consoleUI.Pause(); + break; + case "Add to Favourites": + _favoriteDrinkRepository.InsertFavouriteDrink(drink); + _consoleUI.ShowSuccess("Drink added to favourite list"); + _consoleUI.Pause(); + break; + case "Go Back": + return; + + } + + } + } + + private void OpenDrinkImage(string imageUrl) + { + try + { + Process.Start(new ProcessStartInfo + { + FileName = imageUrl, + UseShellExecute = true + }); + } + catch (Exception ex) + { + _consoleUI.ShowError($"Unable to open drink image: {ex.Message}"); + _consoleUI.Pause(); + } + } + + public async Task GetDrinkByIdAsync(string drinkId) + { + try + { + var response = await _httpClient.GetFromJsonAsync($"lookup.php?i={Uri.EscapeDataString(drinkId)}"); + return response?.Drinks?.FirstOrDefault(); + } + catch (HttpRequestException ex) + { + _consoleUI.ShowError($"Error fetching drink details: {ex.Message}"); + _consoleUI.Pause(); + return null; + } + } + public async Task> GetDrinksByCategoryAsync(string category) + { + try + { + var response = await _httpClient.GetFromJsonAsync($"filter.php?c={Uri.EscapeDataString(category)}"); + return response?.Drinks ?? new List(); + } + catch (HttpRequestException ex) + { + _consoleUI.ShowError($"Error fetching drinks by category: {ex.Message}"); + _consoleUI.Pause(); + return new List(); + } + } + + private async Task> SearchDrinkFromAPI(string drinkName) + { + try + { + var response = await _httpClient.GetFromJsonAsync($"search.php?s={Uri.EscapeDataString(drinkName)}"); + return response?.Drinks ?? new List(); + } + catch (HttpRequestException ex) + { + _consoleUI.ShowError(ex.Message); + _consoleUI.Pause(); + return new List(); + } + } + private async Task> GetCategoriesAsync() + { + try + { + var response = await _httpClient.GetFromJsonAsync("list.php?c=list"); + return response?.Drinks + ?.Select(x => x.StrCategory) + .Where(x => !string.IsNullOrEmpty(x)) + .ToList() + ?? new List(); + } + catch (HttpRequestException ex) + { + _consoleUI.ShowError($"Error fetching categories: {ex.Message}"); + _consoleUI.Pause(); + return new List(); + } + } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/ConsoleUI.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/ConsoleUI.cs new file mode 100644 index 00000000..631c4289 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/ConsoleUI.cs @@ -0,0 +1,68 @@ +using Spectre.Console; + +namespace DrinkExplorer.Validation +{ + public class ConsoleUI + { + public void ShowHeader() + { + AnsiConsole.Clear(); + + var header = new FigletText("DrinkExplorer").Centered().Color(Color.Aqua); + AnsiConsole.Write(header); + AnsiConsole.WriteLine(); + var rule = new Rule + { + Justification = Justify.Center, + Style = new Style(Color.Aqua) + }; + AnsiConsole.Write(rule); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[bold cyan]Welcome to DrinkExplorer[/]"); + AnsiConsole.MarkupLine("[grey]Discover drinks, explore recipes, and manage your favourites.[/]"); + AnsiConsole.WriteLine(); + } + + public void Pause() + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[grey]Press any key to continue...[/]"); + Console.ReadKey(true); + } + + public void ShowSuccess(string message) + { + AnsiConsole.MarkupLine($"[bold green][[ SUCCESS ]][/] {message}"); + } + + public void ShowError(string message) + { + AnsiConsole.MarkupLine($"[bold red][[ ERROR ]][/] {message}"); + } + + public void ShowInfo(string message) + { + AnsiConsole.MarkupLine($"[bold cyan][[ INFO ]][/] {message}"); + } + + public void ShowGoodbye() + { + AnsiConsole.Clear(); + var goodbyeText = new FigletText("Goodbye!").Centered().Color(Color.Aqua); + AnsiConsole.Write(goodbyeText); + AnsiConsole.WriteLine(); + + var panel = new Panel(new Markup("[bold cyan]Thank you for using DrinkExplorer![/]\n[grey]We hope to see you again soon to discover more recipes.[/]")) + { + Border = BoxBorder.Rounded, + BorderStyle = new Style(Color.Aqua), + Padding = new Padding(3, 1, 3, 1), + Expand = false + }; + + AnsiConsole.Write(Align.Center(panel)); + AnsiConsole.WriteLine(); + AnsiConsole.WriteLine(); + } + } +} \ No newline at end of file diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/InputValidator.cs b/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/InputValidator.cs new file mode 100644 index 00000000..5412d319 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/Validation/InputValidator.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Xml.Linq; + +namespace DrinkExplorer.Validation +{ + public class InputValidator + { + public InputValidator() { } + + public bool ValidateDrinkName(string drinkName) + { + if (string.IsNullOrWhiteSpace(drinkName)) return false; + + string trimmed = drinkName.Trim(); + return trimmed.Length >= 2 + && trimmed.Length <= 50 + && trimmed.All(c => char.IsLetter(c) || char.IsWhiteSpace(c)); + } + } +} diff --git a/Sangeerths.DrinkExplorer/DrinkExplorer/appsettings.json b/Sangeerths.DrinkExplorer/DrinkExplorer/appsettings.json new file mode 100644 index 00000000..16396947 --- /dev/null +++ b/Sangeerths.DrinkExplorer/DrinkExplorer/appsettings.json @@ -0,0 +1,5 @@ +{ + "ConnectionStrings": { + "DefaultConnection": "Server=(localdb)\\MSSQLLocalDB;Database=DrinkInfo;Trusted_Connection=True;TrustServerCertificate=True;" + } +} \ No newline at end of file diff --git a/Sangeerths.DrinkExplorer/README.md b/Sangeerths.DrinkExplorer/README.md new file mode 100644 index 00000000..48185d65 --- /dev/null +++ b/Sangeerths.DrinkExplorer/README.md @@ -0,0 +1,88 @@ +# DrinkExplorer 🍸 + +A colorful, interactive C# console application for discovering cocktails and drinks, built on top of [TheCocktailDB](https://www.thecocktaildb.com/api.php) API. Browse drinks by category, search by name, view detailed recipes, save favourites, and track your most-viewed drinks — all from a slick terminal UI powered by [Spectre.Console](https://spectreconsole.net/). + +## Features + +- **Browse by Category** — View all available drink categories and drill down into drinks within each one. +- **Search Drinks** — Look up drinks by name directly from TheCocktailDB. +- **Drink Details** — See category, alcoholic content, glass type, and full preparation instructions in a styled panel. +- **Favourites** — Save drinks to a local favourites list for quick access later. +- **View Tracking** — Automatically tracks how many times each drink has been viewed, with a "Most Viewed Drinks" leaderboard. + + +## Tech Stack + +- **.NET / C#** +- **[Spectre.Console](https://spectreconsole.net/)** — console UI (tables, panels, prompts, selection menus) +- **[SixLabors.ImageSharp](https://sixlabors.com/products/imagesharp/)** — image handling +- **TheCocktailDB API** — drink data source (`https://www.thecocktaildb.com/api/json/v1/1/`) +- Local database/repository layer for storing favourites and view counts + +## Project Structure + +``` +DrinkExplorer/ +├── Controller/ +│ └── Menu.cs # Top-level navigation and menu flow +├── Services/ +│ ├── ManageDrinksService.cs # Fetches/searches drinks, shows drink details +│ └── FavouriteDrinkService.cs # Displays saved favourites and most-viewed drinks +├── Database/ +│ └── FavoriteDrinkRepository.cs # Persistence for favourites and view counts +├── Models/ +│ ├── Drink.cs +│ ├── DrinkResponse.cs +│ ├── DrinkView.cs +│ └── CategoryResponse.cs +├── Validation/ +│ └── InputValidator.cs # Validates user input (e.g., drink name search) +└── Program.cs # Application entry point +``` + +## Getting Started + +### Prerequisites + +- [.NET SDK](https://dotnet.microsoft.com/download) (6.0 or later recommended) +- SQL Server (local or remote instance) +- An internet connection (the app calls the live TheCocktailDB API) + +### Installation + +1. Clone the repository: + ```bash + git clone https://github.com/Sangeerths/DrinkExplorer.git + cd DrinkExplorer + ``` +2. Restore dependencies: + ```bash + dotnet restore + ``` +3. Set up the database by running `scripts.sql` against your SQL Server instance to create the required tables. +4. Run the application: + ```bash + dotnet run + ``` + +## Usage + +On launch, you'll be greeted with the main menu: + +- **Drinks Menu** + - **View All Categories** — browse drinks grouped by category + - **Search Drink** — search for a drink by name + - **Go Back** — return to the main menu +- **My Favourites** + - **View All Favourite Drinks** — see everything you've saved + - **Most Viewed Drink** — see your top viewed drinks, ranked + - **Go Back** — return to the main menu +- **Exit** — quit the application + +From any drink's detail view, you can open its image in a browser or add it to your favourites list. + +## Notes + +- Drink data is sourced live from TheCocktailDB's free API tier, so results depend on that service's availability. +- Favourites and view counts are stored locally, so your saved drinks persist between sessions. + diff --git a/Sangeerths.DrinkExplorer/scripts.sql b/Sangeerths.DrinkExplorer/scripts.sql new file mode 100644 index 0000000000000000000000000000000000000000..26cbe78fcd61e9b73f5eea742e62de2507adb73f GIT binary patch literal 9182 zcmeI2Yfl?T6o%((EA>C@H=v5RBqeDgRjOGV$HLgu=9&bpLU2h^LMtF?`sdre&m0ds zyZA!Hu2iX!g)h4^XU_FKm*t^ZFj6A)j=)7|m?l;%b`>*bIQDrT!q4sk35A6!ON1oOb zx8feUb^pGqZ;y3XR7R zmfst98{!au*_HQa{`7sV>yFP}b5EqH7)LtdB~gT|jaO`X-i@C_QQ?$<+ZV-D--xch z-nqsJi}H$&qF~c~loii)ZR9=+KR_37>$Sf+l6@E4l3tvAqBUn8h4|Hy$G1d#nw-m$ z|5STGD!yL{pEAXiod+^l%3YPP;G0@-1k`g|457vyr%h2dh_j{NVPfL(#AE9DJ^+7uamrQlCr*I%}YF?5w&ZBX@uS zZj15E?d&L^fbfxF7fREeWvUNybJii?0i6R3VQ}2MnRmzQgmaAf2hm+(5m82h1=dPl}Ii_cJgTTxJMNmY~4{t z#KU*J?Co)WuLqS9FC7}CmdNypp11TJIji$0sun|~`B9w7!b9bM{I3pO=NLkDGtdi< z&g(B?d&gUxxPB4cse>D=Oi#9}O53u+bKhH3?_*#DXmKDr52f9Pqy)Y3c03)Fi}g^4 zv}E;x{)lX=O3NCiwxQz0AFK30>l2A-t7#3hL!6iOQHShkn!b`u631C4X+b&eSZ!U$ z>4q?Uk1f`{)}`r0_Ol*^D%Iu<)uIVrMAwszr2(7i$5VMTIJzZYCVSAcSBGIuSq<-@ zf}`H4uQCn&C8%J2yFH#WSk=K*tH>MTC-^Ym>7#~v#ob_{-l$4FPu;QYZihK$N#XQj8LD%vsbuu1_cZQf` zp1I`D2^PwPUR|BV9`Z2NCfeKAKDzsI?7+cOQCam=Zm)+cdS-aZ6p5^$`hu%GCYc}E zOfa`mK-DlyWcS8+&ze8c&Wt6dN1$3F>d;0}$D!`yw47>~&ssJvYJ2+rz%8|bp&zzE zQ?U(9LS}ZxE4@9oH7W51I+xj8yui~V{2?0olTLd0Auo+%6|A5|CA%rG%~Fo9A}yFN(g=zB6X^QqbNg56S01wV?X z>oGl^i`e(cyp*3T(1BS+W5{5LKwdGNo}~-Ar_@WhBxJLh6PRTaHA5Is@KChC63a;z zp|@u$boqwgVHRvyQr_DU;)Io!g~84sNAaAv6R~FuOp`5J28<#G;VE95*L~Agm-8^# zY85wrYq(|IykYBY{;)4+;{`>l*fX+u_XA?w2M0Fgi=Vs)@f3+UbdC)FPehofkvtwc zLR5?4Jm%sljy+jelW(ov85?q%{|%H$hD~B_GSj197)Sm+SnMIA~)}P0=D|3^_Nyk6m=IQC4 zbaXyPwvZ=cdd28ZZm^8{&?Yw29+NOR_{&!$rW3U1($yldc`@_g&KT9hGyD10iG9Sd z;;WtNvJ;i@tc;ncE~65$nL+43X64_IU&-HA6+z?6DoLseo-I;WQwily)v1#feborp uRy$A$OsebRsz2`!rvFz>%yH=bLMOx-v0vu@KNITh#A6M7$Ng%tYyTHd^ZE7w literal 0 HcmV?d00001