From 84349cdb9313c403a3ee794404ea60bd0aeb0158 Mon Sep 17 00:00:00 2001 From: brunoamartinsacc Date: Mon, 27 Jul 2026 18:00:42 +0100 Subject: [PATCH 1/5] primeiro commit --- .../src/Library.Console/CommonActions.cs | 3 +- .../src/Library.Console/ConsoleApp.cs | 76 ++++++++++++++++++- .../src/Library.Console/ConsoleState.cs | 3 +- 3 files changed, 78 insertions(+), 4 deletions(-) diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs index 681f95c..a002da6 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/CommonActions.cs @@ -9,5 +9,6 @@ public enum CommonActions SearchPatrons = 4, RenewPatronMembership = 8, ReturnLoanedBook = 16, - ExtendLoanedBook = 32 + ExtendLoanedBook = 32, + SearchBooks = 64 } diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs index 9fc9750..f587937 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -2,6 +2,7 @@ using Library.ApplicationCore.Entities; using Library.ApplicationCore.Enums; using Library.Console; +using Library.Infrastructure.Data; public class ConsoleApp { @@ -16,13 +17,15 @@ public class ConsoleApp ILoanRepository _loanRepository; ILoanService _loanService; IPatronService _patronService; + JsonData _jsonData; - public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository) + public ConsoleApp(ILoanService loanService, IPatronService patronService, IPatronRepository patronRepository, ILoanRepository loanRepository, JsonData jsonData) { _patronRepository = patronRepository; _loanRepository = loanRepository; _loanService = loanService; _patronService = patronService; + _jsonData = jsonData; } public async Task Run() @@ -43,6 +46,9 @@ public async Task Run() case ConsoleState.LoanDetails: _currentState = await LoanDetails(); break; + case ConsoleState.SearchBooks: + _currentState = await SearchBooks(); + break; } } } @@ -92,6 +98,8 @@ static void PrintPatronsList(List matchingPatrons) } } + + async Task PatronSearchResults() { CommonActions options = CommonActions.Select | CommonActions.SearchPatrons | CommonActions.Quit; @@ -139,6 +147,7 @@ static CommonActions ReadInputOptions(CommonActions options, out int optionNumbe "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, + "b" when options.HasFlag(CommonActions.SearchBooks) => CommonActions.SearchBooks, _ when int.TryParse(userInput, out optionNumber) => CommonActions.Select, _ => CommonActions.Repeat }; @@ -178,6 +187,11 @@ static void WriteInputOptions(CommonActions options) { Console.WriteLine("Or type a number to select a list item."); } + if (options.HasFlag(CommonActions.SearchBooks)) + { + Console.WriteLine(" - \"b\" to search for books"); + } + } async Task PatronDetails() @@ -193,7 +207,7 @@ async Task PatronDetails() loanNumber++; } - CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership; + CommonActions options = CommonActions.SearchPatrons | CommonActions.Quit | CommonActions.Select | CommonActions.RenewPatronMembership | CommonActions.SearchBooks; CommonActions action = ReadInputOptions(options, out int selectedLoanNumber); if (action == CommonActions.Select) { @@ -225,10 +239,66 @@ async Task PatronDetails() selectedPatronDetails = (await _patronRepository.GetPatron(selectedPatronDetails.Id))!; return ConsoleState.PatronDetails; } + else if (action == CommonActions.SearchBooks) + { + return ConsoleState.SearchBooks; + } throw new InvalidOperationException("An input option is not handled."); } +//new method BM@ACC + async Task SearchBooks() + { + string title = ReadBookTitle(); + await _jsonData.EnsureDataLoaded(); + + Book? matchingBook = _jsonData.Books? + .FirstOrDefault(b => b.Title.Contains(title, StringComparison.OrdinalIgnoreCase)); + + if (matchingBook is null) + { + Console.WriteLine($"No matching book found for '{title}'."); + return ConsoleState.PatronDetails; + } + + BookItem? matchingBookItem = _jsonData.BookItems? + .FirstOrDefault(bi => bi.BookId == matchingBook.Id); + + if (matchingBookItem is null) + { + Console.WriteLine($"{matchingBook.Title} is available for loan"); + return ConsoleState.PatronDetails; + } + + Loan? activeLoan = _jsonData.Loans? + .FirstOrDefault(l => l.BookItemId == matchingBookItem.Id && l.ReturnDate == null); + + if (activeLoan is null) + { + Console.WriteLine($"{matchingBook.Title} is available for loan"); + } + else + { + Console.WriteLine($"{matchingBook.Title} is on loan to another patron. The return due date is {activeLoan.DueDate}."); + } + + return ConsoleState.PatronDetails; + } + +//new method BM@ACC + static string ReadBookTitle() + { + string? title = null; + while (string.IsNullOrWhiteSpace(title)) + { + Console.Write("Enter a book title to search: "); + title = Console.ReadLine(); + } + + return title; + } + async Task LoanDetails() { Console.WriteLine($"Book title: {selectedLoanDetails.BookItem!.Book!.Title}"); @@ -271,4 +341,6 @@ async Task LoanDetails() throw new InvalidOperationException("An input option is not handled."); } + + } diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs index e9117b6..4e1468d 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleState.cs @@ -6,5 +6,6 @@ public enum ConsoleState PatronSearchResults, PatronDetails, LoanDetails, - Quit + Quit, + SearchBooks } From de1da3b47334aaac77626f5ac1840daebfc65924 Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Mon, 27 Jul 2026 18:43:58 +0100 Subject: [PATCH 2/5] 0 --- .../src/Library.Console/ConsoleApp.cs | 85 ++++++++++--------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs index f587937..8c1944f 100644 --- a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/src/Library.Console/ConsoleApp.cs @@ -49,6 +49,12 @@ public async Task Run() case ConsoleState.SearchBooks: _currentState = await SearchBooks(); break; + case ConsoleState.Quit: + Console.WriteLine("Goodbye!"); + return; + default: + Console.WriteLine("Unknown application state."); + return; } } } @@ -138,12 +144,14 @@ static CommonActions ReadInputOptions(CommonActions options, out int optionNumbe { Console.WriteLine(); WriteInputOptions(options); - string? userInput = Console.ReadLine(); + string? userInput = Console.ReadLine()?.Trim().ToLowerInvariant(); action = userInput switch { "q" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, + "quit" when options.HasFlag(CommonActions.Quit) => CommonActions.Quit, "s" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, + "search" when options.HasFlag(CommonActions.SearchPatrons) => CommonActions.SearchPatrons, "m" when options.HasFlag(CommonActions.RenewPatronMembership) => CommonActions.RenewPatronMembership, "e" when options.HasFlag(CommonActions.ExtendLoanedBook) => CommonActions.ExtendLoanedBook, "r" when options.HasFlag(CommonActions.ReturnLoanedBook) => CommonActions.ReturnLoanedBook, @@ -241,50 +249,51 @@ async Task PatronDetails() } else if (action == CommonActions.SearchBooks) { - return ConsoleState.SearchBooks; + return await SearchBooks(); } throw new InvalidOperationException("An input option is not handled."); } //new method BM@ACC - async Task SearchBooks() - { - string title = ReadBookTitle(); - await _jsonData.EnsureDataLoaded(); - - Book? matchingBook = _jsonData.Books? - .FirstOrDefault(b => b.Title.Contains(title, StringComparison.OrdinalIgnoreCase)); - - if (matchingBook is null) - { - Console.WriteLine($"No matching book found for '{title}'."); - return ConsoleState.PatronDetails; - } - - BookItem? matchingBookItem = _jsonData.BookItems? - .FirstOrDefault(bi => bi.BookId == matchingBook.Id); - - if (matchingBookItem is null) - { - Console.WriteLine($"{matchingBook.Title} is available for loan"); - return ConsoleState.PatronDetails; - } - - Loan? activeLoan = _jsonData.Loans? - .FirstOrDefault(l => l.BookItemId == matchingBookItem.Id && l.ReturnDate == null); + async Task SearchBooks() + { + string? bookTitle = null; + while (string.IsNullOrWhiteSpace(bookTitle)) + { + Console.Write("Enter a book title to search for: "); + bookTitle = Console.ReadLine(); + } + + await _jsonData.EnsureDataLoaded(); + + var book = _jsonData.Books!.FirstOrDefault(b => string.Equals(b.Title, bookTitle, StringComparison.OrdinalIgnoreCase)); + if (book == null) + { + Console.WriteLine($"No book found with the title \"{bookTitle}\"."); + return ConsoleState.PatronDetails; + } + + var bookItem = _jsonData.BookItems!.FirstOrDefault(bi => bi.BookId == book.Id); + if (bookItem == null) + { + Console.WriteLine($"No book item found for the title \"{book.Title}\"."); + return ConsoleState.PatronDetails; + } + + var loan = _jsonData.Loans!.FirstOrDefault(l => l.BookItemId == bookItem.Id && l.ReturnDate == null); + if (loan == null) + { + Console.WriteLine($"\"{book.Title}\" is available for loan."); + } + else + { + Console.WriteLine($"\"{book.Title}\" is on loan to another patron. The return due date is {loan.DueDate}."); + } + + return ConsoleState.PatronDetails; + } - if (activeLoan is null) - { - Console.WriteLine($"{matchingBook.Title} is available for loan"); - } - else - { - Console.WriteLine($"{matchingBook.Title} is on loan to another patron. The return due date is {activeLoan.DueDate}."); - } - - return ConsoleState.PatronDetails; - } //new method BM@ACC static string ReadBookTitle() From 44e8b8b0e49018aa4b49d2f9352ebc5be8c3f31a Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Tue, 28 Jul 2026 18:20:06 +0100 Subject: [PATCH 3/5] Testing --- .../ApplicationCore/Enums/EnumHelperTests.cs | 41 +++++++++++ .../JsonLoanRepository/GetLoan.cs | 70 +++++++++++++++++++ .../tests/UnitTests/UnitTests.csproj | 8 +++ 3 files changed, 119 insertions(+) create mode 100644 LabFiles/03-develop-code-features/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/Enums/EnumHelperTests.cs create mode 100644 LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs diff --git a/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/Enums/EnumHelperTests.cs b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/Enums/EnumHelperTests.cs new file mode 100644 index 0000000..a7c4c0d --- /dev/null +++ b/LabFiles/03-develop-code-features/AccelerateDevGHCopilot/tests/UnitTests/ApplicationCore/Enums/EnumHelperTests.cs @@ -0,0 +1,41 @@ +using System.ComponentModel; +using Library.ApplicationCore.Enums; + +namespace Library.UnitTests.ApplicationCore.Enums; + +public class EnumHelperTests +{ + [Fact] + public void GetDescription_ReturnsDescription_WhenAttributeExists() + { + // Arrange + var value = SampleStatus.Active; + + // Act + var description = EnumHelper.GetDescription(value); + + // Assert + Assert.Equal("Active state", description); + } + + [Fact] + public void GetDescription_ReturnsEnumName_WhenAttributeIsMissing() + { + // Arrange + var value = SampleStatus.Inactive; + + // Act + var description = EnumHelper.GetDescription(value); + + // Assert + Assert.Equal(nameof(SampleStatus.Inactive), description); + } + + private enum SampleStatus + { + [Description("Active state")] + Active, + + Inactive + } +} diff --git a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs new file mode 100644 index 0000000..7c4a4e2 --- /dev/null +++ b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs @@ -0,0 +1,70 @@ +using Library.ApplicationCore; +using Library.ApplicationCore.Entities; +using Library.Infrastructure.Data; +using Microsoft.Extensions.Configuration; +using NSubstitute; + +namespace UnitTests.Infrastructure.JsonLoanRepositoryTests; + +public class GetLoanTest +{ + private readonly ILoanRepository _mockLoanRepository; + private readonly JsonLoanRepository _jsonLoanRepository; + private readonly IConfiguration _configuration; + private readonly JsonData _jsonData; + + public GetLoanTest() + { + _mockLoanRepository = Substitute.For(); + + var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + var jsonDirectory = Path.Combine(projectRoot, "src", "Library.Console", "bin", "Debug", "net9.0", "Json"); + + _configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["JsonPaths:Authors"] = Path.Combine(jsonDirectory, "Authors.json"), + ["JsonPaths:Books"] = Path.Combine(jsonDirectory, "Books.json"), + ["JsonPaths:BookItems"] = Path.Combine(jsonDirectory, "BookItems.json"), + ["JsonPaths:Patrons"] = Path.Combine(jsonDirectory, "Patrons.json"), + ["JsonPaths:Loans"] = Path.Combine(jsonDirectory, "Loans.json") + }) + .Build(); + + _jsonData = new JsonData(_configuration); + _jsonLoanRepository = new JsonLoanRepository(_jsonData); + } + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns loan when ID is found")] + public async Task GetLoan_ReturnsLoanWhenIdIsFound() + { + var loanId = 1; + var expectedLoan = new Loan + { + Id = loanId, + BookItemId = 17, + PatronId = 22, + LoanDate = DateTime.Parse("2023-12-08T00:40:43.1808862"), + DueDate = DateTime.Parse("2023-12-22T00:40:43.1808862"), + ReturnDate = null + }; + + _mockLoanRepository.GetLoan(loanId).Returns(expectedLoan); + + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + Assert.NotNull(actualLoan); + Assert.Equal(expectedLoan.Id, actualLoan!.Id); + } + + [Fact(DisplayName = "JsonLoanRepository.GetLoan: Returns null when ID is not found")] + public async Task GetLoan_ReturnsNullWhenIdIsNotFound() + { + var loanId = 999; + _mockLoanRepository.GetLoan(loanId).Returns((Loan?)null); + + var actualLoan = await _jsonLoanRepository.GetLoan(loanId); + + Assert.Null(actualLoan); + } +} diff --git a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj index a156d8f..c3dfc74 100644 --- a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj +++ b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/UnitTests.csproj @@ -23,6 +23,14 @@ + + + + Json\%(RecursiveDir)%(FileName)%(Extension) + PreserveNewest + + + From f3b5bf91134de2c5a745a9020133aed01eca35d7 Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Tue, 28 Jul 2026 18:25:44 +0100 Subject: [PATCH 4/5] testing2 --- .../Infrastructure/JsonLoanRepository/GetLoan.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs index 7c4a4e2..a6fcc07 100644 --- a/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs +++ b/LabFiles/04-develop-unit-tests-xunit/AccelerateDevGHCopilot/tests/UnitTests/Infrastructure/JsonLoanRepository/GetLoan.cs @@ -17,8 +17,12 @@ public GetLoanTest() { _mockLoanRepository = Substitute.For(); - var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); - var jsonDirectory = Path.Combine(projectRoot, "src", "Library.Console", "bin", "Debug", "net9.0", "Json"); + var jsonDirectory = Path.Combine(AppContext.BaseDirectory, "Json"); + if (!Directory.Exists(jsonDirectory)) + { + var projectRoot = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..", "..")); + jsonDirectory = Path.Combine(projectRoot, "src", "Library.Console", "Json"); + } _configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary From 09cbb7215e2d9ec31b5a3c92ae6cfbeff8a80caf Mon Sep 17 00:00:00 2001 From: Bruno Martins Date: Fri, 31 Jul 2026 19:47:52 +0100 Subject: [PATCH 5/5] Exercicos --- .github/copilot-instructions.md | 331 ++++++++++++++++++ .../Enums/EnumHelper.cs | 57 ++- .../Library.Infrastructure/Data/JsonData.cs | 43 +-- .../Data/JsonLoanRepository.cs | 24 +- ShoppingApp/app.js | 180 ++++++++++ ShoppingApp/checkout.html | 42 +++ ShoppingApp/index.html | 43 +++ ShoppingApp/product-details.html | 43 +++ ShoppingApp/shopping-cart.html | 42 +++ ShoppingApp/styles.css | 181 ++++++++++ VibeCodingPRD.md | 213 +++++++++++ Wireframes/CheckoutPageWireframe.txt | 16 + Wireframes/NavigationWireframe.txt | 24 ++ Wireframes/ProductDetailsPageWireframe.txt | 16 + Wireframes/ProductsPageWireframe.txt | 21 ++ Wireframes/ShoppingCartPageWireframe.txt | 20 ++ 16 files changed, 1226 insertions(+), 70 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 ShoppingApp/app.js create mode 100644 ShoppingApp/checkout.html create mode 100644 ShoppingApp/index.html create mode 100644 ShoppingApp/product-details.html create mode 100644 ShoppingApp/shopping-cart.html create mode 100644 ShoppingApp/styles.css create mode 100644 VibeCodingPRD.md create mode 100644 Wireframes/CheckoutPageWireframe.txt create mode 100644 Wireframes/NavigationWireframe.txt create mode 100644 Wireframes/ProductDetailsPageWireframe.txt create mode 100644 Wireframes/ProductsPageWireframe.txt create mode 100644 Wireframes/ShoppingCartPageWireframe.txt diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..17aa894 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,331 @@ +# Copilot Instructions for Shopping App Prototype + +## PRD + +# Product Requirements Document (PRD) for a Vibe Coding Prototype Shopping App + +## 1. Project Summary + +### Product +A static, client-side shopping prototype web application for browsing a small catalog of fruit products, viewing product details, adding items to a cart, and completing a simple checkout flow. + +### Purpose +The app is intended to demonstrate the basic interaction model of an online shopping experience using a lightweight prototype built with HTML, CSS, and JavaScript. + +### Target Audience +Online shoppers who want to explore a simple product catalog and experience a basic e-commerce workflow in a prototype environment. + +### Goals +- Allow users to browse a set of sample fruit products. +- Let users view more detailed product information. +- Support adding products to a cart and updating cart quantities. +- Provide a simple checkout summary and order processing interaction. +- Demonstrate navigation across multiple pages in a client-side app. + +## 2. Problem Overview + +The prototype should show how a simple shopping interface can be built without a backend. The app should focus on demonstrating key UI and interaction patterns, including: +- product browsing +- product details +- cart management +- checkout summary + +### Constraints +- The app must be static and client-side only. +- No backend functionality is required. +- No user authentication, payment processing, or database integration should be included. +- The app should use a small sample dataset for demonstration purposes. + +## 3. Scope + +### In Scope +- A Products page with product cards or list items +- A ProductDetails page for detailed item information +- A ShoppingCart page for quantity updates and removals +- A Checkout page for order summary and processing +- Left-side navigation between the pages +- Responsive layout behavior for desktop and narrow mobile widths +- Basic, visually appealing styling + +### Out of Scope +- User accounts +- Secure payment processing +- Real-order persistence +- Backend APIs +- Inventory management +- Shipping or tax calculation +- Database integration + +## 4. User Experience and Interface Requirements + +### Layout +- The app should include a left-side navigation menu for moving between pages. +- On narrow screens, the navigation should collapse into abbreviated labels, such as one- or two-letter abbreviations, when the display width drops below 600 pixels. +- The interface should scale automatically to display adequately on both large screens and phone-sized screens. + +### Styling +- The styling should be basic but visually appealing. +- The UI should use a clean, simple layout that is easy to understand. +- The design should prioritize readability and usability over polish or advanced responsiveness. + +## 5. Page Requirements + +### Products Page +The Products page should: +- Display a list of products. +- Show basic product information, including: + - product name + - price per unit + - image or emoji representation +- Provide a way to select a quantity for each product. +- Allow the user to add the selected quantity to the shopping cart. + +### ProductDetails Page +The ProductDetails page should: +- Display detailed information for the selected product. +- Show: + - product name + - description + - price per unit + - image or emoji representation +- Provide a way to navigate back to the Products page. + +### ShoppingCart Page +The ShoppingCart page should: +- Display the list of products added to the cart. +- Include: + - product name + - quantity + - total price for each product +- Allow the user to update the quantity of each item in the cart. +- Allow the user to remove products from the cart. + +### Checkout Page +The Checkout page should: +- Display a summary of items being purchased. +- Include: + - product name + - quantity + - price +- Clearly display the total price. +- Include an option labeled “Process Order”. + +## 6. Navigation Requirements + +The app must support simple navigation between the following pages: +- Products +- ProductDetails +- ShoppingCart +- Checkout + +### Navigation Behavior +- A left-side menu should allow navigation between pages. +- The navigation should remain functional and visible across the app. +- When the screen width drops below 600 pixels, the navigation bar should collapse to abbreviated labels, such as one- or two-letter short forms, while still maintaining page navigation. + +## 7. Sample Data + +The prototype should use a small sample dataset containing 10 fruit products. + +Each product should include: +- product name +- description +- price per unit +- quantity or unit type (for example: each, ounces, pounds) +- an emoji or simple image representation + +### Example Product Fields +- Product Name +- Description +- Price +- Unit +- Image/Emoji + +### Example Dataset Intent +Use a small, fixed catalog such as: +- Apple +- Banana +- Orange +- Strawberry +- Pineapple +- Watermelon +- Grapes +- Mango +- Peach +- Pear + +## 8. Functional Requirements + +### Core Use Cases +1. Browse available products. +2. Open a product’s details page. +3. Add one or more products to the cart. +4. Update quantities in the cart. +5. Remove products from the shopping cart. +6. Review checkout summary. +7. Process the order from the checkout page. + +### Functional Expectations +- Product selection should update the ProductDetails page. +- Cart updates should be reflected immediately on the ShoppingCart page. +- Checkout should show a summary of the user’s selected items and total. +- All interactions should work entirely on the client side. + +## 9. Technical Requirements + +### Implementation Stack +- HTML +- CSS +- JavaScript + +### Architecture +- Client-side web application +- Static prototype +- No backend services +- No external database + +### Technical Constraints +- Use a simple dataset stored locally in the app. +- Use JavaScript state or in-memory data to simulate cart behavior. +- Use standard web technologies only unless a small, simple library is explicitly required. + +## 10. Non-Functional Requirements + +- The app should be easy to use and understand. +- The UI should be readable on desktop and phone-sized screens. +- The navigation menu should remain workable even on narrow display widths. +- Basic styling should help the app feel polished enough for a prototype. + +## 11. Success Criteria + +The prototype should be considered successful if: +- Users can navigate between the required pages. +- Users can browse products and view product details. +- Users can add items to the cart and adjust quantities. +- Users can remove items from the cart. +- Users can view a checkout summary and complete the “Process Order” action. +- The interface works on both large and small screens with basic responsive behavior. + +## 12. Suggested PRD Notes for GitHub Copilot Agent + +The PRD should explicitly tell the agent to: +- Build a static HTML/CSS/JavaScript prototype. +- Include the four required pages. +- Use a sample dataset of 10 fruit products. +- Implement simple cart behavior. +- Support basic navigation and mobile-friendly collapsing navigation. +- Keep the app lightweight and prototype-focused. + +## Wireframe Guidance + +### Products Page Wireframe + +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +│ │ +│ When width < 600px: │ +│ [P] [D] [C] [Ch] │ +└──────────────────────────────────────────────────────────────┘ +│ Product Catalog │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 🍎 Apple │ │ 🍌 Banana │ │ 🍊 Orange │ │ +│ │ $1.25 each │ │ $0.75 each │ │ $1.10 each │ │ +│ │ Qty: [1] │ │ Qty: [1] │ │ Qty: [1] │ │ +│ │ [Add to Cart]│ │ [Add to Cart]│ │ [Add to Cart]│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 🍓 Strawberry│ │ 🍍 Pineapple│ │ 🍉 Watermelon│ │ +│ │ $2.50 each │ │ $3.00 each │ │ $4.00 each │ │ +│ │ Qty: [1] │ │ Qty: [1] │ │ Qty: [1] │ │ +│ │ [Add to Cart]│ │ [Add to Cart]│ │ [Add to Cart]│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + +### ProductDetails Page Wireframe + +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +│ │ +│ [← Back to Products] │ +└──────────────────────────────────────────────────────────────┘ + +│ Product Details │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 🍎 Apple │ │ +│ │ Price: $1.25 per unit │ │ +│ │ Description: Crisp and sweet, perfect for snacking. │ │ +│ │ │ │ +│ │ [Add to Cart] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + +### ShoppingCart Page Wireframe + +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +└──────────────────────────────────────────────────────────────┘ + +│ Shopping Cart │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 🍎 Apple Qty: [2] Total: $2.50 │ │ +│ │ [Update Qty] [Remove] │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ 🍌 Banana Qty: [1] Total: $0.75 │ │ +│ │ [Update Qty] [Remove] │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ 🍊 Orange Qty: [3] Total: $3.30 │ │ +│ │ [Update Qty] [Remove] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ Subtotal: $6.55 │ +│ [Proceed to Checkout] │ +└──────────────────────────────────────────────────────────────┘ + +### Checkout Page Wireframe + +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +└──────────────────────────────────────────────────────────────┘ + +│ Checkout │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Order Summary │ │ +│ │ 🍎 Apple Qty: 2 Price: $2.50 │ │ +│ │ 🍌 Banana Qty: 1 Price: $0.75 │ │ +│ │ 🍊 Orange Qty: 3 Price: $3.30 │ │ +│ │ │ │ +│ │ Total: $6.55 │ │ +│ │ [Process Order] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ + +### Navigation Wireframe + +Expanded Navigation: + +┌─────────────────────────┐ +│ Products │ +│ Product Details │ +│ Shopping Cart │ +│ Checkout │ +└─────────────────────────┘ + +Collapsed Navigation (width < 600px): + +┌─────────┐ +│ P │ D │ │ +│ C │ Ch │ +└─────────┘ + +Alternative compact collapsed version: + +┌─────┐ +│ P │ +│ D │ +│ C │ +│ Ch │ +└─────┘ diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs index 5369856..0949db8 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.ApplicationCore/Enums/EnumHelper.cs @@ -1,27 +1,48 @@ -using System.ComponentModel; -using System.Reflection; - -namespace Library.ApplicationCore.Enums; +namespace Library.ApplicationCore.Enums; public static class EnumHelper { - public static string GetDescription(Enum value) - { - if (value == null) - return string.Empty; - - FieldInfo fieldInfo = value.GetType().GetField(value.ToString())!; + private static readonly IReadOnlyDictionary LoanExtensionDescriptions = + new Dictionary + { + [LoanExtensionStatus.Success] = "Book loan extension was successful.", + [LoanExtensionStatus.LoanNotFound] = "Loan not found.", + [LoanExtensionStatus.LoanExpired] = "Cannot extend book loan as it already has expired. Return the book instead.", + [LoanExtensionStatus.MembershipExpired] = "Cannot extend book loan due to expired patron's membership.", + [LoanExtensionStatus.LoanReturned] = "Cannot extend book loan as the book is already returned.", + [LoanExtensionStatus.Error] = "Cannot extend book loan due to an error." + }; - DescriptionAttribute[] attributes = - (DescriptionAttribute[])fieldInfo.GetCustomAttributes(typeof(DescriptionAttribute), false); + private static readonly IReadOnlyDictionary LoanReturnDescriptions = + new Dictionary + { + [LoanReturnStatus.Success] = "Book was successfully returned.", + [LoanReturnStatus.LoanNotFound] = "Loan not found.", + [LoanReturnStatus.AlreadyReturned] = "Cannot return book as the book is already returned.", + [LoanReturnStatus.Error] = "Cannot return book due to an error." + }; - if (attributes != null && attributes.Length > 0) + private static readonly IReadOnlyDictionary MembershipRenewalDescriptions = + new Dictionary { - return attributes[0].Description; - } - else + [MembershipRenewalStatus.Success] = "Membership renewal was successful.", + [MembershipRenewalStatus.PatronNotFound] = "Patron not found.", + [MembershipRenewalStatus.TooEarlyToRenew] = "It is too early to renew the membership.", + [MembershipRenewalStatus.LoanNotReturned] = "Cannot renew membership due to an outstanding loan.", + [MembershipRenewalStatus.Error] = "Cannot renew membership due to an error." + }; + + public static string GetDescription(Enum? value) + { + if (value is null) + return string.Empty; + + return value switch { - return value.ToString(); - } + LoanExtensionStatus extensionStatus when LoanExtensionDescriptions.TryGetValue(extensionStatus, out var extensionDescription) => extensionDescription, + LoanReturnStatus returnStatus when LoanReturnDescriptions.TryGetValue(returnStatus, out var returnDescription) => returnDescription, + MembershipRenewalStatus membershipStatus when MembershipRenewalDescriptions.TryGetValue(membershipStatus, out var membershipDescription) => membershipDescription, + _ => value.ToString() ?? string.Empty + }; } } \ No newline at end of file diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs index 7af26a7..fbf12e3 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonData.cs @@ -97,58 +97,33 @@ public List GetPopulatedPatrons(IEnumerable patrons) public Patron GetPopulatedPatron(Patron p) { - Patron populated = new Patron + return new Patron { Id = p.Id, Name = p.Name, ImageName = p.ImageName, MembershipStart = p.MembershipStart, MembershipEnd = p.MembershipEnd, - Loans = new List() + Loans = Loans? + .Where(loan => loan.PatronId == p.Id) + .Select(GetPopulatedLoan) + .ToList() ?? new List() }; - - foreach (Loan loan in Loans!) - { - if (loan.PatronId == p.Id) - { - populated.Loans.Add(GetPopulatedLoan(loan)); - } - } - - return populated; } public Loan GetPopulatedLoan(Loan l) { - Loan populated = new Loan + return new Loan { Id = l.Id, BookItemId = l.BookItemId, PatronId = l.PatronId, LoanDate = l.LoanDate, DueDate = l.DueDate, - ReturnDate = l.ReturnDate + ReturnDate = l.ReturnDate, + BookItem = GetPopulatedBookItem(BookItems!.Single(bi => bi.Id == l.BookItemId)), + Patron = Patrons!.Single(p => p.Id == l.PatronId) }; - - foreach (BookItem bi in BookItems!) - { - if (bi.Id == l.BookItemId) - { - populated.BookItem = GetPopulatedBookItem(bi); - break; - } - } - - foreach (Patron p in Patrons!) - { - if (p.Id == l.PatronId) - { - populated.Patron = p; - break; - } - } - - return populated; } public BookItem GetPopulatedBookItem(BookItem bi) diff --git a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs index 2683283..58e0fb1 100644 --- a/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs +++ b/LabFiles/05-refactor-improve-existing-code/AccelerateDevGHCopilot/src/Library.Infrastructure/Data/JsonLoanRepository.cs @@ -16,28 +16,16 @@ public JsonLoanRepository(JsonData jsonData) { await _jsonData.EnsureDataLoaded(); - foreach (Loan loan in _jsonData.Loans!) - { - if (loan.Id == id) - { - Loan populated = _jsonData.GetPopulatedLoan(loan); - return populated; - } - } - return null; + var loan = _jsonData.Loans! + .FirstOrDefault(existingLoan => existingLoan.Id == id); + + return loan is null ? null : _jsonData.GetPopulatedLoan(loan); } public async Task UpdateLoan(Loan loan) { - Loan? existingLoan = null; - foreach (Loan l in _jsonData.Loans!) - { - if (l.Id == loan.Id) - { - existingLoan = l; - break; - } - } + var existingLoan = _jsonData.Loans! + .FirstOrDefault(l => l.Id == loan.Id); if (existingLoan != null) { diff --git a/ShoppingApp/app.js b/ShoppingApp/app.js new file mode 100644 index 0000000..43ea97e --- /dev/null +++ b/ShoppingApp/app.js @@ -0,0 +1,180 @@ +const products = [ + { id: 1, name: 'Apple', price: 1.25, unit: 'each', emoji: '🍎', description: 'Crisp and sweet, perfect for snacking.' }, + { id: 2, name: 'Banana', price: 0.75, unit: 'each', emoji: '🍌', description: 'A soft, sweet fruit ideal for breakfast.' }, + { id: 3, name: 'Orange', price: 1.10, unit: 'each', emoji: '🍊', description: 'Juicy and bright with a zesty flavor.' }, + { id: 4, name: 'Strawberry', price: 2.50, unit: 'box', emoji: '🍓', description: 'Fresh and fragrant with a sweet berry taste.' }, + { id: 5, name: 'Pineapple', price: 3.00, unit: 'each', emoji: '🍍', description: 'Tropical and refreshing with a tangy bite.' }, + { id: 6, name: 'Watermelon', price: 4.00, unit: 'whole', emoji: '🍉', description: 'Hydrating and juicy, great for summer.' }, + { id: 7, name: 'Grapes', price: 2.20, unit: 'bunch', emoji: '🍇', description: 'Sweet, plump grapes in a colorful bunch.' }, + { id: 8, name: 'Mango', price: 2.80, unit: 'each', emoji: '🥭', description: 'A rich tropical fruit with smooth texture.' }, + { id: 9, name: 'Peach', price: 1.85, unit: 'each', emoji: '🍑', description: 'Velvety skin and a delicate sweet flavor.' }, + { id: 10, name: 'Pear', price: 1.65, unit: 'each', emoji: '🍐', description: 'Mild and elegant with a crisp finish.' } +]; + +const cartKey = 'fruit-shop-cart'; + +function getCart() { + const saved = localStorage.getItem(cartKey); + return saved ? JSON.parse(saved) : []; +} + +function saveCart(cart) { + localStorage.setItem(cartKey, JSON.stringify(cart)); +} + +function addToCart(productId) { + const qtyInput = document.querySelector(`#qty-${productId}`); + const quantity = Number(qtyInput?.value || 1); + const cart = getCart(); + const existing = cart.find(item => item.id === productId); + + if (existing) existing.quantity += quantity; + else cart.push({ id: productId, quantity }); + + saveCart(cart); + window.location.href = 'shopping-cart.html'; +} + +function renderProducts() { + const list = document.getElementById('product-list'); + if (!list) return; + + list.innerHTML = products.map(product => ` +
+
${product.emoji}
+

${product.name}

+

${product.description}

+
$${product.price.toFixed(2)} / ${product.unit}
+ +

+ + Details +

+
+ `).join(''); +} + +function renderDetails() { + const detailRoot = document.getElementById('product-details-content'); + if (!detailRoot) return; + + const params = new URLSearchParams(window.location.search); + const productId = Number(params.get('id') || 1); + const product = products.find(item => item.id === productId) || products[0]; + + detailRoot.innerHTML = ` +
+
${product.emoji}
+

${product.name}

+

${product.description}

+
$${product.price.toFixed(2)} / ${product.unit}
+ +
+ `; +} + +function renderCart() { + const cartRoot = document.getElementById('cart-content'); + if (!cartRoot) return; + + const cart = getCart(); + if (!cart.length) { + cartRoot.innerHTML = '
Your cart is empty.
'; + return; + } + + const cartItems = cart.map(item => { + const product = products.find(p => p.id === item.id); + const total = product.price * item.quantity; + return ` +
+

${product.name}

+

Qty:

+

Total: $${total.toFixed(2)}

+ +
+ `; + }).join(''); + + const subtotal = cart.reduce((sum, item) => { + const product = products.find(p => p.id === item.id); + return sum + (product.price * item.quantity); + }, 0); + + cartRoot.innerHTML = ` +
${cartItems}
+
+ Subtotal: $${subtotal.toFixed(2)} +

Proceed to Checkout

+
+ `; + + cartRoot.querySelectorAll('.qty-update').forEach(input => { + input.addEventListener('change', (event) => { + const id = Number(event.target.dataset.id); + const value = Math.max(1, Number(event.target.value || 1)); + const cart = getCart(); + const item = cart.find(entry => entry.id === id); + if (item) item.quantity = value; + saveCart(cart); + renderCart(); + }); + }); +} + +function removeFromCart(productId) { + const cart = getCart().filter(item => item.id !== productId); + saveCart(cart); + renderCart(); +} + +function renderCheckout() { + const checkoutRoot = document.getElementById('checkout-content'); + if (!checkoutRoot) return; + + const cart = getCart(); + if (!cart.length) { + checkoutRoot.innerHTML = '
Your cart is empty.
'; + return; + } + + const items = cart.map(item => { + const product = products.find(p => p.id === item.id); + return ` +
+

${product.name}

+

Qty: ${item.quantity} | Price: $${product.price.toFixed(2)}

+
+ `; + }).join(''); + + const total = cart.reduce((sum, item) => { + const product = products.find(p => p.id === item.id); + return sum + (product.price * item.quantity); + }, 0); + + checkoutRoot.innerHTML = ` +
${items}
+
+ Total: $${total.toFixed(2)} +

+
+ `; +} + +function processOrder() { + localStorage.removeItem(cartKey); + alert('Order processed successfully.'); + window.location.href = 'index.html'; +} + +function initApp() { + renderProducts(); + renderDetails(); + renderCart(); + renderCheckout(); +} + +window.addEventListener('DOMContentLoaded', initApp); diff --git a/ShoppingApp/checkout.html b/ShoppingApp/checkout.html new file mode 100644 index 0000000..c014465 --- /dev/null +++ b/ShoppingApp/checkout.html @@ -0,0 +1,42 @@ + + + + + + Checkout - Fruit Shop Prototype + + + + + + + + diff --git a/ShoppingApp/index.html b/ShoppingApp/index.html new file mode 100644 index 0000000..a7423e1 --- /dev/null +++ b/ShoppingApp/index.html @@ -0,0 +1,43 @@ + + + + + + Products - Fruit Shop Prototype + + + +
+ + +
+ +
+
+
+ + + + diff --git a/ShoppingApp/product-details.html b/ShoppingApp/product-details.html new file mode 100644 index 0000000..89823c4 --- /dev/null +++ b/ShoppingApp/product-details.html @@ -0,0 +1,43 @@ + + + + + + Product Details - Fruit Shop Prototype + + + + + + + + diff --git a/ShoppingApp/shopping-cart.html b/ShoppingApp/shopping-cart.html new file mode 100644 index 0000000..5011a71 --- /dev/null +++ b/ShoppingApp/shopping-cart.html @@ -0,0 +1,42 @@ + + + + + + Shopping Cart - Fruit Shop Prototype + + + + + + + + diff --git a/ShoppingApp/styles.css b/ShoppingApp/styles.css new file mode 100644 index 0000000..1d7708d --- /dev/null +++ b/ShoppingApp/styles.css @@ -0,0 +1,181 @@ +:root { + --bg: #f7f8fc; + --panel: #ffffff; + --text: #1f2937; + --muted: #6b7280; + --accent: #2563eb; + --accent-2: #dbeafe; + --border: #d9e1ec; + --shadow: 0 8px 24px rgba(15, 23, 42, 0.08); +} + +* { + box-sizing: border-box; +} + +body { + margin: 0; + font-family: Arial, Helvetica, sans-serif; + background: var(--bg); + color: var(--text); +} + +.app-shell { + display: flex; + min-height: 100vh; +} + +.sidebar { + width: 240px; + background: #0f172a; + color: white; + padding: 20px 14px; + position: sticky; + top: 0; + align-self: stretch; +} + +.nav-menu { + display: flex; + flex-direction: column; + gap: 10px; +} + +.nav-item { + display: flex; + align-items: center; + text-decoration: none; + color: white; + background: rgba(255,255,255,0.08); + padding: 12px 14px; + border-radius: 10px; + font-weight: 700; +} + +.nav-item.active, +.nav-item:hover { + background: #1d4ed8; +} + +.nav-abbr { + display: none; +} + +.content { + flex: 1; + padding: 24px; +} + +.page-header { + margin-bottom: 20px; +} + +.page-header h1 { + margin: 0 0 4px; +} + +.catalog, +.details-card, +.cart-card, +.checkout-card { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 18px; +} + +.product-card, +.detail-card, +.cart-item, +.checkout-item { + background: var(--panel); + border: 1px solid var(--border); + border-radius: 14px; + padding: 18px; + box-shadow: var(--shadow); +} + +.product-emoji { + font-size: 3rem; + margin-bottom: 10px; +} + +.product-price, +.total-price { + font-size: 1.1rem; + font-weight: 700; + color: var(--accent); +} + +.qty-input, +.qty-update { + width: 60px; + padding: 6px; + border-radius: 6px; + border: 1px solid var(--border); +} + +.button, +.link-button, +.remove-button { + border: none; + border-radius: 8px; + cursor: pointer; + padding: 9px 12px; + font-weight: 700; +} + +.button, +.link-button { + background: var(--accent); + color: white; +} + +.remove-button { + background: #fee2e2; + color: #b91c1c; +} + +.back-link { + color: var(--accent); + text-decoration: none; + font-weight: 700; +} + +.cart-summary, +.checkout-summary { + margin-top: 16px; + border-top: 1px solid var(--border); + padding-top: 14px; +} + +@media (max-width: 600px) { + .app-shell { + flex-direction: row; + } + + .sidebar { + width: 78px; + padding: 12px 8px; + } + + .nav-menu { + flex-direction: column; + gap: 8px; + } + + .nav-item { + justify-content: center; + min-width: 44px; + padding: 10px 6px; + } + + .nav-text { + display: none; + } + + .nav-abbr { + display: inline; + font-size: 0.8rem; + font-weight: 700; + } +} diff --git a/VibeCodingPRD.md b/VibeCodingPRD.md new file mode 100644 index 0000000..25b18d9 --- /dev/null +++ b/VibeCodingPRD.md @@ -0,0 +1,213 @@ +# Product Requirements Document (PRD) for a Vibe Coding Prototype Shopping App + +## 1. Project Summary + +### Product +A static, client-side shopping prototype web application for browsing a small catalog of fruit products, viewing product details, adding items to a cart, and completing a simple checkout flow. + +### Purpose +The app is intended to demonstrate the basic interaction model of an online shopping experience using a lightweight prototype built with HTML, CSS, and JavaScript. + +### Target Audience +Online shoppers who want to explore a simple product catalog and experience a basic e-commerce workflow in a prototype environment. + +### Goals +- Allow users to browse a set of sample fruit products. +- Let users view more detailed product information. +- Support adding products to a cart and updating cart quantities. +- Provide a simple checkout summary and order processing interaction. +- Demonstrate navigation across multiple pages in a client-side app. + +## 2. Problem Overview + +The prototype should show how a simple shopping interface can be built without a backend. The app should focus on demonstrating key UI and interaction patterns, including: +- product browsing +- product details +- cart management +- checkout summary + +### Constraints +- The app must be static and client-side only. +- No backend functionality is required. +- No user authentication, payment processing, or database integration should be included. +- The app should use a small sample dataset for demonstration purposes. + +## 3. Scope + +### In Scope +- A Products page with product cards or list items +- A ProductDetails page for detailed item information +- A ShoppingCart page for quantity updates and removals +- A Checkout page for order summary and processing +- Left-side navigation between the pages +- Responsive layout behavior for desktop and narrow mobile widths +- Basic, visually appealing styling + +### Out of Scope +- User accounts +- Secure payment processing +- Real-order persistence +- Backend APIs +- Inventory management +- Shipping or tax calculation +- Database integration + +## 4. User Experience and Interface Requirements + +### Layout +- The app should include a left-side navigation menu for moving between pages. +- On narrow screens, the navigation should collapse into abbreviated labels, such as one- or two-letter abbreviations, when the display width drops below 300 pixels. +- The interface should scale automatically to display adequately on both large screens and phone-sized screens. + +### Styling +- The styling should be basic but visually appealing. +- The UI should use a clean, simple layout that is easy to understand. +- The design should prioritize readability and usability over polish or advanced responsiveness. + +## 5. Page Requirements + +### Products Page +The Products page should: +- Display a list of products. +- Show basic product information, including: + - product name + - price per unit + - image or emoji representation +- Provide a way to select a quantity for each product. +- Allow the user to add the selected quantity to the shopping cart. + +### ProductDetails Page +The ProductDetails page should: +- Display detailed information for the selected product. +- Show: + - product name + - description + - price per unit + - image or emoji representation +- Provide a way to navigate back to the Products page. + +### ShoppingCart Page +The ShoppingCart page should: +- Display the list of products added to the cart. +- Include: + - product name + - quantity + - total price for each product +- Allow the user to update the quantity of each item in the cart. +- Allow the user to remove products from the cart. + +### Checkout Page +The Checkout page should: +- Display a summary of items being purchased. +- Include: + - product name + - quantity + - price +- Clearly display the total price. +- Include an option labeled “Process Order”. + +## 6. Navigation Requirements + +The app must support simple navigation between the following pages: +- Products +- ProductDetails +- ShoppingCart +- Checkout + +### Navigation Behavior +- A left-side menu should allow navigation between pages. +- The navigation should remain functional and visible across the app. +- When the screen width drops below 300 pixels, the navigation bar should collapse to abbreviated labels, such as one- or two-letter short forms, while still maintaining page navigation. + +## 7. Sample Data + +The prototype should use a small sample dataset containing 10 fruit products. + +Each product should include: +- product name +- description +- price per unit +- quantity or unit type (for example: each, ounces, pounds) +- an emoji or simple image representation + +### Example Product Fields +- Product Name +- Description +- Price +- Unit +- Image/Emoji + +### Example Dataset Intent +Use a small, fixed catalog such as: +- Apple +- Banana +- Orange +- Strawberry +- Pineapple +- Watermelon +- Grapes +- Mango +- Peach +- Pear + +## 8. Functional Requirements + +### Core Use Cases +1. Browse available products. +2. Open a product’s details page. +3. Add one or more products to the cart. +4. Update quantities in the cart. +5. Remove products from the shopping cart. +6. Review checkout summary. +7. Process the order from the checkout page. + +### Functional Expectations +- Product selection should update the ProductDetails page. +- Cart updates should be reflected immediately on the ShoppingCart page. +- Checkout should show a summary of the user’s selected items and total. +- All interactions should work entirely on the client side. + +## 9. Technical Requirements + +### Implementation Stack +- HTML +- CSS +- JavaScript + +### Architecture +- Client-side web application +- Static prototype +- No backend services +- No external database + +### Technical Constraints +- Use a simple dataset stored locally in the app. +- Use JavaScript state or in-memory data to simulate cart behavior. +- Use standard web technologies only unless a small, simple library is explicitly required. + +## 10. Non-Functional Requirements + +- The app should be easy to use and understand. +- The UI should be readable on desktop and phone-sized screens. +- The navigation menu should remain workable even on narrow display widths. +- Basic styling should help the app feel polished enough for a prototype. + +## 11. Success Criteria + +The prototype should be considered successful if: +- Users can navigate between the required pages. +- Users can browse products and view product details. +- Users can add items to the cart and adjust quantities. +- Users can remove items from the cart. +- Users can view a checkout summary and complete the “Process Order” action. +- The interface works on both large and small screens with basic responsive behavior. + +## 12. Suggested PRD Notes for GitHub Copilot Agent + +The PRD should explicitly tell the agent to: +- Build a static HTML/CSS/JavaScript prototype. +- Include the four required pages. +- Use a sample dataset of 10 fruit products. +- Implement simple cart behavior. +- Support basic navigation and mobile-friendly collapsing navigation. +- Keep the app lightweight and prototype-focused. diff --git a/Wireframes/CheckoutPageWireframe.txt b/Wireframes/CheckoutPageWireframe.txt new file mode 100644 index 0000000..7511d8c --- /dev/null +++ b/Wireframes/CheckoutPageWireframe.txt @@ -0,0 +1,16 @@ +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +└──────────────────────────────────────────────────────────────┘ + +│ Checkout │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ Order Summary │ │ +│ │ 🍎 Apple Qty: 2 Price: $2.50 │ │ +│ │ 🍌 Banana Qty: 1 Price: $0.75 │ │ +│ │ 🍊 Orange Qty: 3 Price: $3.30 │ │ +│ │ │ │ +│ │ Total: $6.55 │ │ +│ │ [Process Order] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ diff --git a/Wireframes/NavigationWireframe.txt b/Wireframes/NavigationWireframe.txt new file mode 100644 index 0000000..fb59f8b --- /dev/null +++ b/Wireframes/NavigationWireframe.txt @@ -0,0 +1,24 @@ +Expanded Navigation: + +┌─────────────────────────┐ +│ Products │ +│ Product Details │ +│ Shopping Cart │ +│ Checkout │ +└─────────────────────────┘ + +Collapsed Navigation (width < 300px): + +┌─────────┐ +│ P │ D │ │ +│ C │ Ch │ +└─────────┘ + +Alternative compact collapsed version: + +┌─────┐ +│ P │ +│ D │ +│ C │ +│ Ch │ +└─────┘ diff --git a/Wireframes/ProductDetailsPageWireframe.txt b/Wireframes/ProductDetailsPageWireframe.txt new file mode 100644 index 0000000..ed75596 --- /dev/null +++ b/Wireframes/ProductDetailsPageWireframe.txt @@ -0,0 +1,16 @@ +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +│ │ +│ [← Back to Products] │ +└──────────────────────────────────────────────────────────────┘ + +│ Product Details │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 🍎 Apple │ │ +│ │ Price: $1.25 per unit │ │ +│ │ Description: Crisp and sweet, perfect for snacking. │ │ +│ │ │ │ +│ │ [Add to Cart] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +└──────────────────────────────────────────────────────────────┘ diff --git a/Wireframes/ProductsPageWireframe.txt b/Wireframes/ProductsPageWireframe.txt new file mode 100644 index 0000000..f4b00ad --- /dev/null +++ b/Wireframes/ProductsPageWireframe.txt @@ -0,0 +1,21 @@ +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +│ │ +│ When width < 300px: │ +│ [P] [D] [C] [Ch] │ +└──────────────────────────────────────────────────────────────┘ +│ Product Catalog │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 🍎 Apple │ │ 🍌 Banana │ │ 🍊 Orange │ │ +│ │ $1.25 each │ │ $0.75 each │ │ $1.10 each │ │ +│ │ Qty: [1] │ │ Qty: [1] │ │ Qty: [1] │ │ +│ │ [Add to Cart]│ │ [Add to Cart]│ │ [Add to Cart]│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ +│ │ 🍓 Strawberry│ │ 🍍 Pineapple│ │ 🍉 Watermelon│ │ +│ │ $2.50 each │ │ $3.00 each │ │ $4.00 each │ │ +│ │ Qty: [1] │ │ Qty: [1] │ │ Qty: [1] │ │ +│ │ [Add to Cart]│ │ [Add to Cart]│ │ [Add to Cart]│ │ +│ └─────────────┘ └─────────────┘ └─────────────┘ │ +└──────────────────────────────────────────────────────────────┘ diff --git a/Wireframes/ShoppingCartPageWireframe.txt b/Wireframes/ShoppingCartPageWireframe.txt new file mode 100644 index 0000000..68cfea3 --- /dev/null +++ b/Wireframes/ShoppingCartPageWireframe.txt @@ -0,0 +1,20 @@ +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar Navigation │ +│ [Products] [Details] [Cart] [Checkout] │ +└──────────────────────────────────────────────────────────────┘ + +│ Shopping Cart │ +│ ┌──────────────────────────────────────────────────────────┐ │ +│ │ 🍎 Apple Qty: [2] Total: $2.50 │ │ +│ │ [Update Qty] [Remove] │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ 🍌 Banana Qty: [1] Total: $0.75 │ │ +│ │ [Update Qty] [Remove] │ │ +│ ├──────────────────────────────────────────────────────────┤ │ +│ │ 🍊 Orange Qty: [3] Total: $3.30 │ │ +│ │ [Update Qty] [Remove] │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ Subtotal: $6.55 │ +│ [Proceed to Checkout] │ +└──────────────────────────────────────────────────────────────┘