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