diff --git a/Seasoned.Backend/Controllers/RecipeController.cs b/Seasoned.Backend/Controllers/RecipeController.cs index 3733a4e..b2dc399 100644 --- a/Seasoned.Backend/Controllers/RecipeController.cs +++ b/Seasoned.Backend/Controllers/RecipeController.cs @@ -5,7 +5,6 @@ using Seasoned.Backend.Data; using System.Security.Claims; using Seasoned.Backend.Models; using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace Seasoned.Backend.Controllers; @@ -49,7 +48,7 @@ public class RecipeController : ControllerBase var recipe = new Recipe { Title = recipeDto.Title, - Description = recipeDto.Description, + Icon = recipeDto.Icon, Ingredients = recipeDto.Ingredients, Instructions = recipeDto.Instructions, CreatedAt = DateTime.UtcNow, @@ -62,6 +61,29 @@ public class RecipeController : ControllerBase return Ok(new { message = "Recipe saved to your collection!" }); } + [Authorize] + [HttpPut("update/{id}")] + public async Task UpdateRecipe(int id, [FromBody] Recipe updatedRecipe) + { + var userId = User.FindFirstValue(ClaimTypes.NameIdentifier); + + var existingRecipe = await _context.Recipes + .FirstOrDefaultAsync(r => r.Id == id && r.UserId == userId); + + if (existingRecipe == null) + { + return NotFound("Recipe not found or you do not have permission to edit it."); + } + + existingRecipe.Title = updatedRecipe.Title; + existingRecipe.Ingredients = updatedRecipe.Ingredients; + existingRecipe.Instructions = updatedRecipe.Instructions; + + await _context.SaveChangesAsync(); + + return Ok(new { message = "Recipe updated successfully!" }); + } + [Authorize] [HttpGet("my-collection")] public async Task>> GetMyRecipes() @@ -75,5 +97,4 @@ public class RecipeController : ControllerBase return Ok(myRecipes); } - } \ No newline at end of file diff --git a/Seasoned.Backend/DTOs/RecipeResponseDto.cs b/Seasoned.Backend/DTOs/RecipeResponseDto.cs index 1d2b8d5..dd81672 100644 --- a/Seasoned.Backend/DTOs/RecipeResponseDto.cs +++ b/Seasoned.Backend/DTOs/RecipeResponseDto.cs @@ -3,7 +3,7 @@ namespace Seasoned.Backend.DTOs; public class RecipeResponseDto { public string Title { get; set; } = string.Empty; - public string Description { get; set; } = string.Empty; + public string Icon { get; set; } = "mdi-silverware-fork-knife"; public List Ingredients { get; set; } = new(); public List Instructions { get; set; } = new(); } \ No newline at end of file diff --git a/Seasoned.Backend/Models/Recipe.cs b/Seasoned.Backend/Models/Recipe.cs index 38825ad..ec0eeff 100644 --- a/Seasoned.Backend/Models/Recipe.cs +++ b/Seasoned.Backend/Models/Recipe.cs @@ -3,7 +3,7 @@ namespace Seasoned.Backend.Models; public class Recipe { public int Id { get; set; } public string Title { get; set; } = string.Empty; - public string? Description { get; set; } + public string Icon { get; set; } = "mdi-silverware-fork-knife"; public List Ingredients { get; set; } = new(); public List Instructions { get; set; } = new(); public DateTime CreatedAt { get; set; } diff --git a/Seasoned.Backend/Services/RecipeService.cs b/Seasoned.Backend/Services/RecipeService.cs index 705304d..25396a4 100644 --- a/Seasoned.Backend/Services/RecipeService.cs +++ b/Seasoned.Backend/Services/RecipeService.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Http; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; + namespace Seasoned.Backend.Services; public class RecipeService : IRecipeService @@ -18,6 +19,7 @@ public class RecipeService : IRecipeService public async Task ParseRecipeImageAsync(IFormFile image) { var googleAI = new GoogleAI(_apiKey); + var model = googleAI.GenerativeModel("gemini-3.1-flash-lite-preview"); using var ms = new MemoryStream(); @@ -25,16 +27,19 @@ public class RecipeService : IRecipeService var base64Image = Convert.ToBase64String(ms.ToArray()); var prompt = @"Extract the recipe details from this image. + + RULES FOR THE 'icon' FIELD: + 1. Select a valid 'Material Design Icon' name (e.g., 'mdi-pasta', 'mdi-bread-slice', 'mdi-muffin', 'mdi-pizza'). + 2. If the recipe type is ambiguous or you cannot find a specific matching icon, you MUST return 'mdi-silverware-fork-knife'. + IMPORTANT: Return ONLY a raw JSON string. - DO NOT include markdown formatting (no ```json). - DO NOT include any text before or after the JSON. - All property names and string values MUST be enclosed in double quotes. + DO NOT include markdown formatting. JSON structure: { - ""title"": ""string"", - ""description"": ""string"", - ""ingredients"": [""string"", ""string""], - ""instructions"": [""string"", ""string""] + ""title"": ""string"", + ""icon"": ""string"", + ""ingredients"": [""string"", ""string""], + ""instructions"": [""string"", ""string""] }"; var config = new GenerationConfig { @@ -43,7 +48,8 @@ public class RecipeService : IRecipeService }; var request = new GenerateContentRequest(prompt, config); - await Task.Run(() => request.AddMedia(base64Image, "image/png")); + + await Task.Run(() => request.AddMedia(base64Image, image.ContentType ?? "image/png")); var response = await model.GenerateContent(request); string rawText = response.Text?.Trim() ?? ""; @@ -53,7 +59,7 @@ public class RecipeService : IRecipeService if (start == -1 || end == -1) { - return new RecipeResponseDto { Title = "Error", Description = "AI failed to generate a valid JSON block." }; + return new RecipeResponseDto { Title = "Error" }; } string cleanJson = rawText.Substring(start, (end - start) + 1); @@ -62,6 +68,12 @@ public class RecipeService : IRecipeService { var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true }; var result = JsonSerializer.Deserialize(cleanJson, options); + + if (result != null && string.IsNullOrEmpty(result.Icon)) + { + result.Icon = "mdi-silverware-fork-knife"; + } + return result ?? new RecipeResponseDto { Title = "Empty Response" }; } catch (JsonException ex) @@ -69,10 +81,7 @@ public class RecipeService : IRecipeService Console.WriteLine($"Raw AI Output: {rawText}"); Console.WriteLine($"Failed to parse JSON: {ex.Message}"); - return new RecipeResponseDto { - Title = "Parsing Error", - Description = "The AI response was malformed. Check logs." - }; + return new RecipeResponseDto { Title = "Parsing Error" }; } } } \ No newline at end of file diff --git a/Seasoned.Frontend/app/assets/css/app-theme.css b/Seasoned.Frontend/app/assets/css/app-theme.css index 2cbf2c8..56771a6 100644 --- a/Seasoned.Frontend/app/assets/css/app-theme.css +++ b/Seasoned.Frontend/app/assets/css/app-theme.css @@ -71,25 +71,27 @@ background: transparent !important; } -.ingredient-item { - border-bottom: 1px dotted #c1b18e; - font-style: italic; - color: #2c2925; -} - -.instruction-step { +.instruction-step, .ingredient-item { display: flex; + font-family: 'Libre Baskerville', serif; + font-size: 1.1rem; gap: 20px; line-height: 1.6; color: #2c2925; + margin: 0 !important; + padding: 0 !important; + flex: 1; } .step-number { - font-family: 'Libre Baskerville', serif; - font-weight: bold; - color: #3b4e1e; - font-size: 1.3rem; - min-width: 25px; + font-family: 'Libre Baskerville', serif !important; + font-weight: bold !important; + color: #3b4e1e !important; + font-size: 1.3rem !important; + text-align: center !important; + min-width: 30px; + line-height: 1.4 !important; + padding-top: 16px; } .separator { @@ -237,6 +239,5 @@ } .brand-icon-container .v-img { - /* Gives it a slightly 'stamped' look on the paper */ filter: drop-shadow(0px 1px 1px rgba(0,0,0,0.1)); } \ No newline at end of file diff --git a/Seasoned.Frontend/app/assets/css/gallery.css b/Seasoned.Frontend/app/assets/css/gallery.css index b01ec44..d20e705 100644 --- a/Seasoned.Frontend/app/assets/css/gallery.css +++ b/Seasoned.Frontend/app/assets/css/gallery.css @@ -47,4 +47,78 @@ .back-to-home-btn:hover { background-color: #4a3a2a !important; box-shadow: 0 4px 8px rgba(0,0,0,0.2) !important; +} + +.recipe-title-edit .v-field__input { + font-family: 'Libre Baskerville', serif !important; + font-size: 2.4rem !important; + color: #1e1408 !important; + text-align: center !important; +} + +.v-textarea .v-field { + background-color: rgba(93, 64, 55, 0.05) !important; + border-radius: 8px !important; +} + +.save-btn, .cancel-btn { + font-family: 'Libre Baskerville', serif !important; + text-transform: none !important; + font-weight: bold !important; + font-size: 1.1rem !important; + letter-spacing: 0px !important; + transition: all 0.3s ease; +} + +.save-btn { + color: #556b2f !important; +} + +.save-btn:hover { + background-color: rgba(85, 107, 47, 0.08) !important; +} + +.cancel-btn { + color: #8c4a32 !important; +} + +.cancel-btn:hover { + background-color: rgba(140, 74, 50, 0.05) !important; +} + +.recipe-title-edit .v-field__input { + font-family: 'Libre Baskerville', serif !important; + font-weight: 700 !important; + color: #1e1408 !important; + font-size: 2.4rem !important; + text-align: center !important; +} + +.v-textarea .v-field__input { + font-weight: 500 !important; + color: #2c2925 !important; + line-height: 1.6 !important; +} + +.v-field-label { + color: #5d4037 !important; + opacity: 0.6 !important; +} + +.v-field__outline { + --v-field-border-opacity: 1 !important; + color: #d1c7b7 !important; +} + +.v-field--focused .v-field__outline { + color: #556b2f !important; +} + +.recipe-title-edit.v-text-field .v-field__outline__line { + border-bottom-width: 2px !important; + color: #d1c7b7 !important; +} + +.recipe-title-edit.v-field--focused .v-field__outline__line { + color: #556b2f !important; } \ No newline at end of file diff --git a/Seasoned.Frontend/app/pages/gallery.vue b/Seasoned.Frontend/app/pages/gallery.vue index 04ed1ef..530948f 100644 --- a/Seasoned.Frontend/app/pages/gallery.vue +++ b/Seasoned.Frontend/app/pages/gallery.vue @@ -3,7 +3,7 @@
-

The Collection

+

Your Collection

Hand-Picked & Seasoned

@@ -23,7 +23,7 @@ -

Opening the Ledger...

+

Opening Collection...

@@ -37,7 +37,7 @@ style="border: 1px solid #e8e2d6;" > @@ -48,14 +48,24 @@

- - Open Recipe - + + + Open + + + Edit + +
@@ -64,22 +74,129 @@

Your collection is empty.

- Return to kitchen to add some + Return Home to add some
+ + + + + +
+ +

{{ selectedRecipe.title }}

+
+ + + + + +
+

+ + Ingredients +

+ + + + + + {{ ing }} + + +
+
+ + +

+ + Instructions +

+ + + +
+ {{ index + 1 }} +

{{ step }}

+
+
+
+ + + + Save Changes + + + + Cancel + + + + + +
+

+ Recorded on {{ new Date(selectedRecipe.createdAt).toLocaleDateString() }} +

+
+
+
\ No newline at end of file