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 00000000..26cbe78f Binary files /dev/null and b/Sangeerths.DrinkExplorer/scripts.sql differ