diff --git a/.gitignore b/.gitignore index 0b26c11d..e1293c09 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ *.user *.userosscache *.sln.docstates +*.db # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/DataConnection.cs b/DataConnection.cs new file mode 100644 index 00000000..fc002850 --- /dev/null +++ b/DataConnection.cs @@ -0,0 +1,31 @@ +using Microsoft.Data.Sqlite; + +namespace Habit_Tracker_App; + +public class DataConnection +{ + static string databaseName = "Habit-Tracker"; + string tableName = "drinking_water"; + static string connectionString = $"Data Source={databaseName}.db"; + public DataConnection() + { + CreateTable(); + } + public void CreateTable() + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var tableCmd = conn.CreateCommand(); + + tableCmd.CommandText = @$"CREATE TABLE IF NOT EXISTS {tableName}( + Id INTEGER PRIMARY KEY AUTOINCREMENT, + Date TEXT, + Quantity INTEGER + )"; + + tableCmd.ExecuteNonQuery(); + conn.Close(); + } + } +} diff --git a/Habit-Tracker.db b/Habit-Tracker.db new file mode 100644 index 00000000..554c7a7a Binary files /dev/null and b/Habit-Tracker.db differ diff --git a/Habit.cs b/Habit.cs new file mode 100644 index 00000000..6cd09506 --- /dev/null +++ b/Habit.cs @@ -0,0 +1,8 @@ +namespace Habit_Tracker_App; + +internal class Habit +{ + public int HabitId { get; set; } + public required string Date { get; set; } + public int Quantity { get; set; } +} \ No newline at end of file diff --git a/Habit_Tracker.slnx b/Habit_Tracker.slnx new file mode 100644 index 00000000..6067de6f --- /dev/null +++ b/Habit_Tracker.slnx @@ -0,0 +1,3 @@ + + + diff --git a/Habit_Tracker_App.csproj b/Habit_Tracker_App.csproj new file mode 100644 index 00000000..a182699b --- /dev/null +++ b/Habit_Tracker_App.csproj @@ -0,0 +1,21 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + PreserveNewest + + + + diff --git a/Habit_Tracker_App.sln b/Habit_Tracker_App.sln new file mode 100644 index 00000000..c5598968 --- /dev/null +++ b/Habit_Tracker_App.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 18 +VisualStudioVersion = 18.0.11222.15 d18.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Habit_Tracker_App", "Habit_Tracker_App.csproj", "{030116D8-7B25-5231-1781-6774979F91DA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {030116D8-7B25-5231-1781-6774979F91DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {030116D8-7B25-5231-1781-6774979F91DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {030116D8-7B25-5231-1781-6774979F91DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {030116D8-7B25-5231-1781-6774979F91DA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {C7362BB7-ADA6-47C2-9FC5-2E3BD7EC1258} + EndGlobalSection +EndGlobal diff --git a/MenuUI.cs b/MenuUI.cs new file mode 100644 index 00000000..37fe75f8 --- /dev/null +++ b/MenuUI.cs @@ -0,0 +1,52 @@ +using Spectre.Console; + +namespace Habit_Tracker_App; +public static class MenuUI +{ + public static void WelcomeMessage() + { + AnsiConsole.MarkupLine("[bold orange3]Welcome to the Habit Tracker Application![/]\n"); + AnsiConsole.MarkupLine("[bold orange3]In this application you will manage the drinking water habit. To Continue, please press Enter... [/]"); + Console.ReadKey(); + } + public static string GetMenuChoice() + { + Console.Clear(); + string userInput; + + while (true) + { + var menuTitle = new Rule("[bold orange3]Main Menu[/]"); + menuTitle.Justification = Justify.Left; + AnsiConsole.Write(menuTitle); + Console.WriteLine(); + AnsiConsole.MarkupLine($"[green]1[/] - Add an entry"); + AnsiConsole.MarkupLine($"[green]2[/] - View saved entries"); + AnsiConsole.MarkupLine($"[green]3[/] - Update an entry"); + AnsiConsole.MarkupLine($"[green]4[/] - Delete an entry"); + AnsiConsole.MarkupLine($"[red]X[/] - Exit the application"); + AnsiConsole.Markup("[green]Your Selection: [/]"); + userInput = Console.ReadLine().ToLower(); + + switch (userInput) + { + case "1": + case "2": + case "3": + case "4": + case "x": + return userInput; + default: + AnsiConsole.MarkupLine("[red]Invalid entry. Please type a [/][green]menu choice[/]"); + break; + } + } + } + public static void AddSpace(int number) + { + for (int i = 0; i < number; i++) + { + Console.WriteLine(); + } + } +} diff --git a/Program.cs b/Program.cs new file mode 100644 index 00000000..aab6dcbe --- /dev/null +++ b/Program.cs @@ -0,0 +1,314 @@ +using Microsoft.Data.Sqlite; +using Spectre.Console; +using System.Text.RegularExpressions; + +namespace Habit_Tracker_App; +public class Program +{ + static string databaseName = "Habit-Tracker"; + static string tableName = "drinking_water"; + static string connectionString = $"Data Source={databaseName}.db"; + DataConnection conn = new(); + static void Main(string[] args) + { + string userChoice = ""; + MenuUI.WelcomeMessage(); + + // Main Loop + while (userChoice != "x") + { + userChoice = MenuUI.GetMenuChoice(); + MenuUI.AddSpace(3); + + switch (userChoice) + { + case "1": // Add entry + InsertRecord(); + Console.ReadLine(); + break; + case "2": // View saved entries + ViewAllRecords(); + AnsiConsole.MarkupLine("[bold orange3]Press Enter to continue...[/]"); + Console.ReadKey(); + break; + case "3": // Update an entry + UpdateRecord(); + AnsiConsole.MarkupLine("[bold orange3]Press Enter to continue...[/]"); + Console.ReadKey(); + break; + case "4": // Delete an entry + DeleteRecord(); + AnsiConsole.MarkupLine("[bold orange3]Press Enter to continue...[/]"); + Console.ReadKey(); + break; + case "x": // Exit application + AnsiConsole.MarkupLine("[bold red]Press Enter to exit...[/]"); + Console.ReadKey(); + break; + default: + AnsiConsole.MarkupLine("[bold red]Error - Invalid entry[/]"); + Console.ReadKey(); + break; + } + } + } + + private static void DeleteRecord() + { + int idSelection = GetValidRecordID(); + int rowCount; + + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var command = conn.CreateCommand(); + command.CommandText = @$"DELETE FROM {tableName} + WHERE Id = '{idSelection}'"; + rowCount = command.ExecuteNonQuery(); + conn.Close(); + } + AnsiConsole.Markup($"[bold orange3]You Deleted {rowCount} Row(s).[/]"); + } + private static void UpdateRecord() + { + string inputDate; + int inputQuantity; + int rowsAffected; + bool exitLoop = false; + + //Prompt and return valid record ID + int idSelection = GetValidRecordID(); + GetSingleRecord(idSelection); + + //Get valid date entry + do + { + inputDate = GetDate(); + if (inputDate == "") + continue; + else + exitLoop = true; + } + while (exitLoop == false); + + //Get valid quantity entry + do + { + inputQuantity = GetQuantity(); + if (inputQuantity == 0) + continue; + else + exitLoop = true; + } + while (exitLoop == false); + + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var command = conn.CreateCommand(); + command.CommandText = @$"UPDATE {tableName} + SET Date = '{inputDate}', Quantity = '{inputQuantity}' + WHERE Id = '{idSelection}'"; + rowsAffected = command.ExecuteNonQuery(); + conn.Close(); + } + AnsiConsole.Markup($"[bold orange3]{rowsAffected} record updated.[/]"); + } + private static void ViewAllRecords() + { + List tableData = GetAllRecords(); + + var recordsTable = new Spectre.Console.Table(); + recordsTable.AddColumn("[bold orange3]ID[/]") + .AddColumn("[bold orange3]Date[/]") + .AddColumn("[bold orange3]Glasses Drank[/]"); + + foreach (Habit row in tableData) + { + recordsTable = recordsTable.AddRow(row.HabitId.ToString(), row.Date, row.Quantity.ToString()); + } + AnsiConsole.Write(recordsTable); + } + private static List GetAllRecords() + { + // This was moved out of ViewAllRecords method so it could also be used for record selection in other methods. + List tableData = new List(); + try + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var command = conn.CreateCommand(); + command.CommandText = @$"SELECT Id, Date, Quantity FROM {tableName}"; + + using (SqliteDataReader reader = command.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + tableData.Add(new Habit + { + HabitId = reader.GetInt32(0), + Date = reader.GetString(1), + Quantity = reader.GetInt32(2) + }); + } + } + else + { + AnsiConsole.MarkupLine($"[bold red]No records found[/]"); + Console.ReadLine(); + } + } + conn.Close(); + return tableData; + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[bold red]An error occurred: {ex.Message}[/]"); + return new List(); + } + } + private static void GetSingleRecord(int ID) //IMPROVE - Could create overload function with GetAllRecords if no parameters passed + { + List tableData = new(); + try + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var command = conn.CreateCommand(); + command.CommandText = @$"SELECT Id, Date, Quantity FROM {tableName} + WHERE Id = {ID}"; + using (SqliteDataReader reader = command.ExecuteReader()) + { + if (reader.HasRows) + { + while (reader.Read()) + { + tableData.Add(new Habit + { + HabitId = reader.GetInt32(0), + Date = reader.GetString(1), + Quantity = reader.GetInt32(2) + }); + } + } + else + { + AnsiConsole.MarkupLine($"[bold red]No records found[/]"); + Console.ReadLine(); + } + } + conn.Close(); + } + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[bold red]An error occurred: {ex.Message}[/]"); + } + } + private static void InsertRecord() + { + string inputDate; + int inputQuantity; + bool exitLoop = false; + //Get valid date entry + do + { + inputDate = GetDate(); + if (inputDate == "") + continue; + else + exitLoop = true; + } + while (exitLoop == false); + //Get valid quantity entry + + do + { + inputQuantity = GetQuantity(); + if (inputQuantity == 0) + continue; + else + exitLoop = true; + } + while (exitLoop == false); + + //Save to database + try + { + using (var conn = new SqliteConnection(connectionString)) + { + conn.Open(); + var command = conn.CreateCommand(); + command.CommandText = @$"INSERT INTO {tableName}(Date, Quantity) + VALUES('{inputDate}','{inputQuantity}')"; + command.ExecuteNonQuery(); + conn.Close(); + } + AnsiConsole.MarkupLine("[bold orange3]Record Saved![/]"); + AnsiConsole.MarkupLine("[bold orange3]Details:[/]"); + AnsiConsole.MarkupLine($"[bold orange3]Date: [/]{inputDate}"); + AnsiConsole.MarkupLine($"[bold orange3]Quantity: [/]{inputQuantity}"); + AnsiConsole.MarkupLine($"[bold orange3]Date: [/]{inputDate}"); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[bold red]An error occurred: {ex.Message}[/]"); + } + } + private static string GetDate() + { + AnsiConsole.Markup("[green]Please enter the date of your entry (MM-DD-YYYY): [/]"); + string? input = Console.ReadLine(); + + if (Regex.IsMatch(input, @"\d{2}-\d{2}-\d{4}")) + return input; + else + AnsiConsole.MarkupLine("[bold red]Invalid Input. Please try again[/]"); + return ""; + } + private static int GetQuantity() + { + AnsiConsole.Markup("[green]Please enter whole number of glasses to log [/]"); + int quantity; + + while(!int.TryParse(Console.ReadLine(), out quantity)) + { + AnsiConsole.MarkupLine("[bold red]Invalid Input. Please try again[/]"); + } + return quantity; + } + private static int GetValidRecordID() + { + // Retrieve all record ID's from DB + List recordsTable = GetAllRecords(); + List validRecord = new(); + foreach (Habit record in recordsTable) + { + validRecord.Add(record.HabitId); + } + ViewAllRecords(); + + string input; + int validInput; + bool exitLoop = false; + + // Validate selection to current record ID's + do + { + AnsiConsole.Markup("[bold orange3]Please select the ID of the record: [/]"); + input = Console.ReadLine(); + + if (!int.TryParse(input, out validInput) || !validRecord.Contains(validInput)) + AnsiConsole.MarkupLine("[bold red]Error - Invalid entry[/]"); + else + exitLoop = true; + } + while (exitLoop == false); + return validInput; + } +} \ No newline at end of file diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json new file mode 100644 index 00000000..ef03e5df --- /dev/null +++ b/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "Habit_Tracker_App": { + "commandName": "Project", + "workingDirectory": "C:\\Users\\tripp\\source\\repos\\TheCSharpAcademy\\ConsoleProjects\\Habit-Tracker\\" + } + } +} \ No newline at end of file diff --git a/ReadMe.md b/ReadMe.md new file mode 100644 index 00000000..7a104949 --- /dev/null +++ b/ReadMe.md @@ -0,0 +1,33 @@ +# Habit Tracker Created by Atizzle07 + +A simple console-based habit tracker written in C#. +It uses **SQLite** for data persistence and Spectre Console for basic UI enhancement + +This project is based on the Habit Tracker exercise from **CSharp Academy**, with a few personal tweaks and improvements. + +--- +## Features +- Add new records (date + quantity) +- View all records in a formatted table +- Update and Delete existing entries +- Data stored locally in a SQLite database file +- Basic error handling and input validation +--- +## Tech Stack + +- Application Type: .NET (Console App) +- Language: C# +- Database: SQLite +- Console UI: [Spectre.Console](https://spectreconsole.net/) +--- +## Project Files and Structure + +- `Program.cs` – Application entry point and main menu loop. +- `Habit.cs` – Model class representing a habit record. Contains these properties: + - `HabitId`, `Date`, `Quantity`). +- `DataConnection.cs` – Handles SQLite connection. Creates DB if it doesn't already exist. +- `MenuUI.cs` - Handles Main Menu display + +--- + +