diff --git a/CHANGELOG.md b/CHANGELOG.md index 54b1591f..82bb0248 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Public documentation. - User custom lists feature: users can now create, manage, and delete their own named recipe lists - Tests! ### Changed +- Made the repository public. - Made authentication cookie persist for longer, helps mobile Safari - Reorganized the source code so it's all under `src` diff --git a/README.md b/README.md index b185950c..810fe3eb 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,9 @@ CookTime is a recipe management system! It is composed of: -1. REST API -1. Browser application -1. PostgreSQL schemas and functions for storing and querying recipes +1. REST API (C# ASP.NET app in `src/CookTime`) +1. Browser application (React SPA in `src/CookTime/client-app`) +1. PostgreSQL schemas and functions for storing and querying recipes (SQL statements in `src/CookTime/Scripts`) With CookTime you can do things like: @@ -15,16 +15,54 @@ With CookTime you can do things like: 1. Compute a recipe's nutrition facts 1. Track grocery lists of ingredients to make sets of recipes +## Features + +### Recipe Index + +Search by whatever you want! + +![CookTime homepage](docs/media/cooktime_homepage_2026_02_01.png) + +### Grocery Lists + +Aggregate ingredients into one grocery list! + +![CookTime grocery list](docs/media/cooktime_groceries_2026_02_01.png) + +### Nutrition Facts + +Automatically computed nutrition facts using USDA nutrition data! + +![Recipe nutrition facts](docs/media/cooktime_nutrition_2026_02_01.png) + +### Recipe scaling + +Scale recipes to make more or less servings! + +![Recipe scaling and highlighted ingredients](docs/media/cooktime_recipe_instructions_2026_02_01.png) + +### Recipe lists + +Come up with your own recipe lists, and share them with the world! + +![Cooktime lists](docs/media/cooktime_lists_2026_02_01.png) + +### Social sign on + +No more username and passwords! + +![Sign in with Google](docs/media/cooktime_signin_2026_02_01.png) + ## Getting started Assuming [Docker Desktop][Docker Desktop] installed on Linux or macOS, -use the `scripts` directory contents to get started from in /babe-algorithms: +use the `scripts` directory (found at the root of the repo) contents to get started: ```shell scripts/server ``` -Then open to get started! +Then open in a browser! I recommend using VSCode to develop CookTime, for that you will need to install: diff --git a/docs/media/cooktime_groceries_2026_02_01.png b/docs/media/cooktime_groceries_2026_02_01.png new file mode 100644 index 00000000..f9bbdb3e Binary files /dev/null and b/docs/media/cooktime_groceries_2026_02_01.png differ diff --git a/docs/media/cooktime_homepage_2026_02_01.png b/docs/media/cooktime_homepage_2026_02_01.png new file mode 100644 index 00000000..fa579330 Binary files /dev/null and b/docs/media/cooktime_homepage_2026_02_01.png differ diff --git a/docs/media/cooktime_lists_2026_02_01.png b/docs/media/cooktime_lists_2026_02_01.png new file mode 100644 index 00000000..19492a2b Binary files /dev/null and b/docs/media/cooktime_lists_2026_02_01.png differ diff --git a/docs/media/cooktime_nutrition_2026_02_01.png b/docs/media/cooktime_nutrition_2026_02_01.png new file mode 100644 index 00000000..a6d929b4 Binary files /dev/null and b/docs/media/cooktime_nutrition_2026_02_01.png differ diff --git a/docs/media/cooktime_recipe_instructions_2026_02_01.png b/docs/media/cooktime_recipe_instructions_2026_02_01.png new file mode 100644 index 00000000..89486fa0 Binary files /dev/null and b/docs/media/cooktime_recipe_instructions_2026_02_01.png differ diff --git a/docs/media/cooktime_signin_2026_02_01.png b/docs/media/cooktime_signin_2026_02_01.png new file mode 100644 index 00000000..1b6de434 Binary files /dev/null and b/docs/media/cooktime_signin_2026_02_01.png differ diff --git a/docs/migration-status.md b/docs/migration-status.md deleted file mode 100644 index 5156e5ad..00000000 --- a/docs/migration-status.md +++ /dev/null @@ -1,215 +0,0 @@ -# PostgreSQL Migration Implementation Status - -## ✅ Completed - -### 1. Migration Infrastructure - -- [x] **000_migration_tracker.sql** - Migration tracking table -- [x] **run_migrations.sh** - Bash script to execute migrations in order with checksum tracking - -### 2. Database Schema (SQL Scripts) - -- [x] **001_schema.sql** - Complete schema with 12 tables in `cooktime` schema - - recipes, recipe_components, ingredient_requirements, recipe_steps - - ingredients, categories, category_recipe - - recipe_lists, recipe_requirements - - images, reviews, nutrition_facts - - Full-text search with tsvector + GIN indexes - - Triggers for last_modified_date - -- [x] **002_validation.sql** - JSON validation functions - - validate_recipe_json, validate_component_json - - validate_ingredient_requirement_json, validate_recipe_list_json - - validate_ingredient_json - -- [x] **003_write_functions.sql** - Write operations - - create_recipe (with CTEs for components/steps/ingredients) - - update_recipe (delete and recreate pattern) - - delete_recipe - - create_or_update_nutrition_data (UPSERT based on source_ids) - - create_ingredient - - create_recipe_list, add_recipe_to_list - - create_category - -- [x] **004_read_functions.sql** - Read operations returning JSON - - get_recipe_with_details (nested JSON with jsonb_agg) - - search_recipes_by_name (full-text search) - - search_recipes_by_ingredient - - get_recipes (paginated) - - get_user_recipe_lists - - get_recipe_list_with_recipes - - get_recipe_images - - get_ingredient (with nutrition facts) - - search_ingredients (trigram similarity) - - get_recipe_reviews - - get_categories - -- [x] **005_images.sql** - Image storage migration - - Add storage_url column for Azure Blob URLs - - create_image, get_ingredient_images functions - -### 3. C# Data Transfer Objects (DTOs) - -Created 10 DTO files in `/src/Models/Contracts/` with `[JsonPropertyName]` attributes: - -- [x] **RecipeCreateDto.cs** - Recipe create/update DTOs -- [x] **RecipeDetailDto.cs** - Recipe detail and summary DTOs -- [x] **ComponentDto.cs** - Component DTOs -- [x] **IngredientRequirementDto.cs** - Ingredient requirement DTO -- [x] **RecipeStepDto.cs** - Recipe step DTO -- [x] **IngredientDto.cs** - Ingredient DTOs -- [x] **RecipeListDto.cs** - Recipe list DTOs -- [x] **CategoryDto.cs** - Category DTOs -- [x] **NutritionDataDto.cs** - Nutrition data DTO -- [x] **ReviewDto.cs** - Review DTOs - -### 4. Repository Pattern Implementation - -Created 5 repositories in `/src/Services/Repositories/`: - -- [x] **IRecipeRepository** / **RecipeRepository** - Recipe CRUD operations -- [x] **IIngredientRepository** / **IngredientRepository** - Ingredient operations -- [x] **IRecipeListRepository** / **RecipeListRepository** - Recipe list operations -- [x] **ICategoryRepository** / **CategoryRepository** - Category operations -- [x] **IReviewRepository** / **ReviewRepository** - Review operations - -All repositories: - -- Use `NpgsqlDataSource` for connection management -- Call stored procedures defined in SQL scripts -- Deserialize JSON results to DTOs with camelCase property naming -- Handle null results appropriately - -### 5. Dependency Injection Setup - -- [x] Updated **Program.cs** to register `NpgsqlDataSource` as singleton -- [x] Registered all 5 repository interfaces as scoped services -- [x] Added DATABASE_URL environment variable fallback - ---- - -## ⏳ Remaining Work - -### 6. Update Controllers - -Need to refactor controllers to use repositories instead of DbContext: - -#### High Priority Controllers - -- [ ] **MultiPartRecipeController.cs** → Rename to **RecipeController.cs** - - Remove `ApplicationDbContext` injection - - Inject `IRecipeRepository`, `ICategoryRepository`, `IReviewRepository` - - Replace LINQ queries with repository methods - - Update action methods to use DTOs - -- [ ] **CartController.cs** → Rename to **RecipeListController.cs** - - Remove `ApplicationDbContext` injection - - Inject `IRecipeListRepository` - - Replace LINQ queries with repository methods - - Update action methods to use DTOs - -- [ ] **IngredientController.cs** - - Remove `ApplicationDbContext` injection - - Inject `IIngredientRepository` - - Replace LINQ queries with repository methods - - Update action methods to use DTOs - -#### Lower Priority Controllers - -- [ ] **CategoryController.cs** - Update to use `ICategoryRepository` -- [ ] **IImageController.cs** - May need updates if it queries recipes/ingredients - -### 7. Remove Entity Framework - -- [ ] Delete entire **/Migrations/** folder (70+ migration files) -- [ ] Remove EF NuGet packages from **babe-algorithms.csproj**: - - Microsoft.EntityFrameworkCore - - Microsoft.EntityFrameworkCore.Design - - Npgsql.EntityFrameworkCore.PostgreSQL -- [ ] Delete **ApplicationDbContext.cs** class (or keep minimal version for Identity only) -- [ ] Remove `AddDbContext` call from Program.cs (keep only for Identity if needed) -- [ ] Remove all EF-related code from `ConfigureDatabase` method in Program.cs - -### 8. Testing & Data Migration - -- [ ] Set DATABASE_URL environment variable -- [ ] Run `./src/Scripts/run_migrations.sh` to apply all migrations -- [ ] Export existing data using: `dotnet run export` -- [ ] Write data import script to load NDJSON into PostgreSQL -- [ ] Test all API endpoints with new repository implementation -- [ ] Verify full-text search functionality -- [ ] Verify image URL handling - -### 9. Cleanup & Documentation - -- [ ] Update README.md with new architecture -- [ ] Document stored procedure usage -- [ ] Remove obsolete model classes if any -- [ ] Update API documentation - ---- - -## Migration Execution Steps - -1. **Backup existing database** - - ```bash - pg_dump $DATABASE_URL > backup_before_migration.sql - ``` - -2. **Run migrations** - - ```bash - export DATABASE_URL="postgresql://user:pass@host:port/dbname" - ./src/Scripts/run_migrations.sh - ``` - -3. **Export data from EF** - - ```bash - dotnet run export - ``` - - Creates NDJSON files: `recipes.ndjson`, `ingredients.ndjson`, `images.ndjson` - -4. **Import data** (script TBD) - - Parse NDJSON files - - Call stored procedures to insert data - - Handle foreign key relationships - -5. **Update controllers** (in progress) - - Replace DbContext with repositories - - Update to use DTOs - -6. **Remove EF dependencies** - - Delete migrations - - Remove packages - - Clean up Program.cs - -7. **Test thoroughly** - - All CRUD operations - - Search functionality - - User authentication (Identity still uses EF) - - Image handling - ---- - -## Key Architecture Decisions - -✓ **Database Naming**: snake_case for all DB objects -✓ **JSON Naming**: camelCase for API consistency -✓ **Schema**: All app tables in `cooktime` schema, Identity in `public` schema -✓ **Enum**: cooktime.unit type for measurement units -✓ **Search**: pg_trgm + tsvector with GIN indexes -✓ **Transactions**: Managed within stored procedures -✓ **Connection**: Single NpgsqlDataSource singleton, reads from DATABASE_URL -✓ **DTOs**: Use [JsonPropertyName] attributes for camelCase serialization -✓ **Repositories**: Call stored procedures, deserialize JSON results - ---- - -## Next Steps - -**Immediate**: Update controllers to use repositories -**Short-term**: Remove EF dependencies, test migration -**Long-term**: Implement data import script, full system testing diff --git a/docs/postgres-migration-plan.md b/docs/postgres-migration-plan.md deleted file mode 100644 index d6d20c5a..00000000 --- a/docs/postgres-migration-plan.md +++ /dev/null @@ -1,619 +0,0 @@ -# PostgreSQL Migration Plan: Replace Entity Framework - -## Overview - -Replace Entity Framework entirely with plain PostgreSQL schema and stored procedures using snake_case database objects, camelCase JSON properties, and stored procedures handling all data operations. - -## Database Design Decisions - -- **Schema**: All application tables in `cooktime` schema -- **Identity Tables**: Keep in `public` schema (ASP.NET Core Identity standard) -- **Naming Convention**: snake_case for database objects (tables, columns, functions) -- **JSON Properties**: camelCase for JSON in stored procedures and DTOs -- **Schema Qualification**: Always use fully qualified names (e.g., `cooktime.recipes`) -- **Connection String**: Read from `DATABASE_URL` environment variable, no search_path assumption -- **Error Handling**: Use PostgreSQL `RAISE EXCEPTION` for errors, catch in C# repositories -- **Transactions**: Managed within stored procedures for data consistency - -## Migration Steps - -### 1. Create Core Schema DDL Script - -**File**: `src/Scripts/001_schema.sql` - -**Contents**: - -- `CREATE SCHEMA IF NOT EXISTS cooktime` -- Define `cooktime.unit` enum (tablespoon, teaspoon, cup, ounce, pound, gram, kilogram, milliliter, liter, fluid_ounce, pint, quart, gallon, count) -- Create 19 tables with snake_case names: - - `cooktime.recipes` (renamed from MultiPartRecipes) - - `cooktime.recipe_components` (renamed from RecipeComponents) - - `cooktime.ingredient_requirements` (renamed from MultiPartIngredientRequirements) - - `cooktime.recipe_steps` (renamed from MultiPartRecipeSteps) - - `cooktime.ingredients` - - `cooktime.categories` - - `cooktime.category_recipe` (join table) - - `cooktime.recipe_lists` (renamed from Carts) - - `cooktime.recipe_requirements` (join table with quantity multiplier) - - `cooktime.images` - - `cooktime.reviews` - - `cooktime.nutrition_facts` (new unified table) -- All primary keys (uuid or serial) -- All foreign keys fully qualified to `cooktime.table_name` -- Check constraints where applicable -- Indexes including GIN index on `cooktime.recipes.search_vector` for full-text search -- Keep `public."AspNetUsers"` and other Identity tables unchanged - -**Key Table Schemas**: - -**recipes**: - -- id (uuid, PK) -- name (text, not null) -- description (text) -- cooking_minutes (double precision) -- servings (integer) -- prep_minutes (double precision) -- bake_temp_f (double precision) -- calories (integer) -- source (text) -- search_vector (tsvector) - generated column -- created_date (timestamptz) -- last_modified_date (timestamptz) -- owner_id (text, FK to public."AspNetUsers") - -**recipe_components**: - -- id (uuid, PK) -- name (text) -- position (integer) -- recipe_id (uuid, FK to cooktime.recipes) - -**ingredient_requirements**: - -- id (uuid, PK) -- ingredient_id (uuid, FK to cooktime.ingredients, nullable) -- recipe_component_id (uuid, FK to cooktime.recipe_components) -- unit (cooktime.unit enum) -- quantity (double precision) -- position (integer) -- description (text) - freeform text for imprecise ingredients - -**recipe_steps**: - -- id (serial, PK) -- recipe_component_id (uuid, FK to cooktime.recipe_components) -- instruction (text) - -**recipe_lists**: - -- id (uuid, PK) -- name (text, default 'List') -- description (text) -- creation_date (timestamp) -- is_public (boolean) -- owner_id (text, FK to public."AspNetUsers") - -**recipe_requirements**: - -- id (uuid, PK) -- recipe_list_id (uuid, FK to cooktime.recipe_lists) -- recipe_id (uuid, FK to cooktime.recipes) -- quantity (double precision) - scale multiplier for ingredient calculations - -**nutrition_facts**: - -- id (uuid, PK) -- source_ids (jsonb, not null) - e.g., `{"source": "usda_sr", "ndbNumber": 123}` or `{"source": "usda_branded", "gtinUpc": "012345"}` -- names (text[], not null) -- unit_mass (double precision, nullable) -- density (double precision, nullable) -- nutrition_data (jsonb, not null) - full nutrition information - -**images**: - -- id (uuid, PK) -- storage_url (text, not null) - Azure Blob Storage URL -- uploaded_date (timestamptz) -- static_image_name (text) -- recipe_id (uuid, FK to cooktime.recipes, nullable) -- ingredient_id (uuid, FK to cooktime.ingredients, nullable) - -### 2. Define JSON Validation Functions - -**File**: `src/Scripts/002_validation.sql` - -**Functions**: - -**`cooktime.validate_recipe_json(jsonb) RETURNS boolean`** - -- Check camelCase properties: `name`, `components`, `cookingMinutes`, `servings` -- Validate `components` is array -- Validate each component has required fields -- `RAISE EXCEPTION` with descriptive messages for validation failures - -**`cooktime.validate_component_json(jsonb) RETURNS boolean`** - -- Check properties: `name`, `position`, `steps`, `ingredients` -- Validate arrays are properly structured - -**`cooktime.validate_ingredient_requirement_json(jsonb) RETURNS boolean`** - -- Check properties: `ingredientId`, `quantity`, `unit` -- Validate unit is valid enum value - -**`cooktime.validate_recipe_list_json(jsonb) RETURNS boolean`** - -- Check properties: `name`, `ownerId` - -### 3. Create Write Stored Procedures - -**File**: `src/Scripts/003_write_functions.sql` - -**Functions**: - -**`cooktime.create_recipe(recipe_json jsonb) RETURNS uuid`** - -- BEGIN/COMMIT transaction -- Validate JSON with `cooktime.validate_recipe_json()` -- Extract recipe properties from JSON -- INSERT into `cooktime.recipes` RETURNING id -- Use CTEs to insert into: - - `cooktime.recipe_components` (from `components` array) - - `cooktime.ingredient_requirements` (from nested `ingredients` arrays) - - `cooktime.recipe_steps` (from nested `steps` arrays) -- COMMIT and return recipe id - -**`cooktime.update_recipe(recipe_id uuid, recipe_json jsonb)`** - -- BEGIN transaction -- Validate recipe exists -- DELETE existing components/ingredients/steps (cascade) -- Re-insert with new data from JSON -- UPDATE recipe table fields -- COMMIT - -**`cooktime.delete_recipe(recipe_id uuid)`** - -- Handle cascade deletes for components, requirements, steps -- DELETE from `cooktime.recipes` - -**`cooktime.create_or_update_nutrition_data(nutrition_json jsonb) RETURNS uuid`** - -- Extract: `sourceIds`, `names`, `unitMass`, `density`, `nutritionData` -- UPSERT based on matching `source_ids` jsonb -- Return nutrition facts id - -**`cooktime.create_ingredient(ingredient_json jsonb) RETURNS uuid`** - -- Extract: `name`, `defaultServingSizeUnit`, `nutritionFactsId` -- INSERT into `cooktime.ingredients` - -**`cooktime.create_recipe_list(list_json jsonb) RETURNS uuid`** - -- Extract: `name`, `description`, `ownerId`, `isPublic` -- INSERT into `cooktime.recipe_lists` - -**`cooktime.add_recipe_to_list(list_id uuid, recipe_id uuid, quantity_multiplier float)`** - -- INSERT into `cooktime.recipe_requirements` -- Validate both list and recipe exist - -### 4. Create Read Stored Procedures - -**File**: `src/Scripts/004_read_functions.sql` - -**Functions**: - -**`cooktime.get_recipe_with_details(recipe_id uuid) RETURNS jsonb`** - -- JOIN `cooktime.recipes`, `cooktime.recipe_components`, `cooktime.ingredient_requirements`, `cooktime.recipe_steps` -- Use `jsonb_build_object()` with camelCase keys -- Use `jsonb_agg()` to nest components containing steps and ingredients arrays -- Return single JSON document with complete recipe structure - -**`cooktime.search_recipes_by_name(search_term text) RETURNS SETOF jsonb`** - -- Use `cooktime.recipes.search_vector @@ plainto_tsquery('english', search_term)` -- Return array of recipe summary JSON objects - -**`cooktime.search_recipes_by_ingredient(ingredient_id uuid) RETURNS SETOF jsonb`** - -- JOIN through `cooktime.ingredient_requirements` -- Return recipes containing specified ingredient - -**`cooktime.get_user_recipe_lists(user_id text) RETURNS SETOF jsonb`** - -- SELECT from `cooktime.recipe_lists` -- Include recipe counts -- Return array of list summary JSON objects - -**`cooktime.get_recipe_list_with_recipes(list_id uuid) RETURNS jsonb`** - -- JOIN `cooktime.recipe_lists`, `cooktime.recipe_requirements`, `cooktime.recipes` -- Include quantity multipliers for each recipe -- Return complete list with nested recipes - -**`cooktime.get_recipe_images(recipe_id uuid) RETURNS SETOF jsonb`** - -- SELECT from `cooktime.images` WHERE recipe_id matches -- Return camelCase JSON array - -### 5. Alter Images Table Schema - -**File**: `src/Scripts/005_images.sql` - -**Operations**: - -- `ALTER TABLE cooktime.images DROP COLUMN IF EXISTS image_data CASCADE` -- `ADD COLUMN IF NOT EXISTS storage_url text NOT NULL DEFAULT ''` - -**Functions**: - -**`cooktime.create_image(image_json jsonb) RETURNS uuid`** - -- Extract: `storageUrl`, `recipeId`, `ingredientId`, `staticImageName` -- INSERT into `cooktime.images` - -**Note**: Actual Azure Blob upload of existing image byte data happens in separate operational migration outside SQL scripts. - -### 6. Define C# DTOs and Contracts - -**Location**: `src/Models/Contracts/` - -**Files to Create**: - -**`RecipeCreateDto.cs`** - -```csharp -public class RecipeCreateDto -{ - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("description")] - public string? Description { get; set; } - - [JsonPropertyName("components")] - public List Components { get; set; } - - [JsonPropertyName("cookingMinutes")] - public double? CookingMinutes { get; set; } - - [JsonPropertyName("servings")] - public int? Servings { get; set; } - - [JsonPropertyName("prepMinutes")] - public double? PrepMinutes { get; set; } - - [JsonPropertyName("bakeTempF")] - public double? BakeTempF { get; set; } - - [JsonPropertyName("source")] - public string? Source { get; set; } -} -``` - -**`RecipeDetailDto.cs`** - -- All recipe data including nested components with full details - -**`ComponentDto.cs`** - -```csharp -public class ComponentDto -{ - [JsonPropertyName("name")] - public string Name { get; set; } - - [JsonPropertyName("position")] - public int Position { get; set; } - - [JsonPropertyName("steps")] - public List Steps { get; set; } - - [JsonPropertyName("ingredients")] - public List Ingredients { get; set; } -} -``` - -**`RecipeStepDto.cs`** - -```csharp -public class RecipeStepDto -{ - [JsonPropertyName("instruction")] - public string Instruction { get; set; } -} -``` - -**`IngredientRequirementDto.cs`** - -```csharp -public class IngredientRequirementDto -{ - [JsonPropertyName("ingredientId")] - public Guid? IngredientId { get; set; } - - [JsonPropertyName("quantity")] - public double Quantity { get; set; } - - [JsonPropertyName("unit")] - public string Unit { get; set; } - - [JsonPropertyName("position")] - public int Position { get; set; } - - [JsonPropertyName("description")] - public string? Description { get; set; } -} -``` - -**`RecipeListDto.cs`** -**`RecipeRequirementDto.cs`** (with `Quantity` multiplier) -**`NutritionDataDto.cs`** - -### 7. Create Repository Classes - -**Location**: `src/Services/Repositories/` - -**Files to Create**: - -**`IRecipeRepository.cs`** (interface) -**`RecipeRepository.cs`** - -- Inject `IConfiguration` for connection string -- Methods: - - `Task CreateAsync(RecipeCreateDto dto)` - - Serialize to JSON - - Call `SELECT cooktime.create_recipe(@json::jsonb)` - - Return recipe id - - `Task GetDetailAsync(Guid id)` - - Call `SELECT * FROM cooktime.get_recipe_with_details(@id)` - - Deserialize JSON result - - `Task> SearchByNameAsync(string searchTerm)` - - `Task UpdateAsync(Guid id, RecipeCreateDto dto)` - - `Task DeleteAsync(Guid id)` -- Catch `PostgresException` and translate to domain-specific exceptions -- Use `await using var connection = new NpgsqlConnection(connectionString)` - -**`IIngredientRepository.cs`** (interface) -**`IngredientRepository.cs`** - -**`IRecipeListRepository.cs`** (interface) -**`RecipeListRepository.cs`** (replaces CartRepository/CartService) - -**`INutritionRepository.cs`** (interface) -**`NutritionRepository.cs`** - -**`IImageRepository.cs`** (interface) -**`ImageRepository.cs`** - -### 8. Update Controllers - -**Location**: `src/Controllers/` - -**Changes**: - -**Rename Files**: - -- `MultiPartRecipeController.cs` → `RecipeController.cs` -- `CartController.cs` → `RecipeListController.cs` - -**Update All Controllers**: - -- Remove `DbContext` injection -- Inject appropriate repository interfaces -- Replace all LINQ queries with repository method calls -- Remove all `.Include()`, `.ThenInclude()`, `.Where()`, `.FirstOrDefaultAsync()`, `.SaveChangesAsync()` patterns -- Use DTOs for request/response instead of EF entities - -**Controllers to Update**: - -- `RecipeController.cs` (renamed) -- `IngredientController.cs` -- `RecipeListController.cs` (renamed) -- `IImageController.cs` -- `AccountController.cs` -- `SEOController.cs` - -### 9. Remove Entity Framework Dependencies - -**Actions**: - -1. **Delete Migrations folder**: `src/Migrations/` (all `*Migration.cs`, `*Migration.Designer.cs` files, `ApplicationDbContextModelSnapshot.cs`) - -2. **Remove NuGet packages** from `src/babe-algorithms.csproj`: - - `Microsoft.EntityFrameworkCore` - - `Microsoft.EntityFrameworkCore.Design` - - `Microsoft.EntityFrameworkCore.Tools` - - `Npgsql.EntityFrameworkCore.PostgreSQL` - -3. **Delete DbContext class file**: Remove `ApplicationDbContext.cs` - -4. **Update `Startup.cs`/`Program.cs`**: - - Remove `services.AddDbContext()` - - Register repositories: - - ```csharp - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - ``` - - - Keep entire ASP.NET Core Identity configuration: - - ```csharp - services.AddIdentity() - .AddEntityFrameworkStores() - .AddDefaultTokenProviders(); - ``` - -5. **Keep Identity Tables**: `public."AspNetUsers"`, `public."AspNetRoles"`, `public."AspNetUserRoles"`, `public."AspNetUserClaims"`, `public."AspNetUserLogins"`, `public."AspNetUserTokens"`, `public."AspNetRoleClaims"` - -### 10. Create Migration Runner System - -**Files**: - -**`src/Scripts/000_migration_tracker.sql`** - -```sql -CREATE SCHEMA IF NOT EXISTS cooktime; - -CREATE TABLE IF NOT EXISTS cooktime.schema_migrations ( - id serial PRIMARY KEY, - script_name text UNIQUE NOT NULL, - applied_at timestamptz DEFAULT now(), - checksum text -); -``` - -**`src/Scripts/run_migrations.sh`** - -```bash -#!/bin/bash -set -e - -# Read connection string from environment -DB_URL="${DATABASE_URL}" - -if [ -z "$DB_URL" ]; then - echo "Error: DATABASE_URL environment variable not set" - exit 1 -fi - -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - -# Initialize migration tracker -psql "$DB_URL" -f "$SCRIPT_DIR/000_migration_tracker.sql" - -# Get list of SQL files in order -for sql_file in "$SCRIPT_DIR"/*.sql; do - filename=$(basename "$sql_file") - - # Skip the migration tracker itself - if [ "$filename" = "000_migration_tracker.sql" ]; then - continue - fi - - # Check if already applied - applied=$(psql "$DB_URL" -t -c "SELECT COUNT(*) FROM cooktime.schema_migrations WHERE script_name = '$filename'") - - if [ "$applied" -eq 0 ]; then - echo "Applying migration: $filename" - - # Calculate checksum - checksum=$(md5sum "$sql_file" | awk '{print $1}') - - # Execute migration - psql "$DB_URL" -f "$sql_file" - - # Record in tracker - psql "$DB_URL" -c "INSERT INTO cooktime.schema_migrations (script_name, checksum) VALUES ('$filename', '$checksum')" - - echo "✓ Applied: $filename" - else - echo "⊘ Skipped (already applied): $filename" - fi -done - -echo "All migrations completed successfully" -``` - -**Usage**: - -```bash -export DATABASE_URL="postgresql://user:password@localhost:5432/dbname" -chmod +x src/Scripts/run_migrations.sh -./src/Scripts/run_migrations.sh -``` - -**Function Updates**: All numbered SQL files after 001 should use pattern: - -```sql --- Drop existing functions if schema changes -DROP FUNCTION IF EXISTS cooktime.create_recipe CASCADE; -DROP FUNCTION IF EXISTS cooktime.update_recipe CASCADE; - --- Create new versions -CREATE FUNCTION cooktime.create_recipe(recipe_json jsonb) RETURNS uuid AS $$ -... -$$ LANGUAGE plpgsql; -``` - -## Tables Summary - -### Tables Created (19 in `cooktime` schema) - -1. `recipes` (renamed from MultiPartRecipes) -2. `recipe_components` (renamed from RecipeComponents) -3. `ingredient_requirements` (renamed from MultiPartIngredientRequirements) -4. `recipe_steps` (renamed from MultiPartRecipeSteps) -5. `ingredients` -6. `categories` -7. `category_recipe` (join table) -8. `recipe_lists` (renamed from Carts) -9. `recipe_requirements` (join table with quantity multiplier) -10. `images` -11. `reviews` -12. `nutrition_facts` (new unified table) -13-19. Plus other supporting tables - -### Tables Kept (7 in `public` schema) - -1. `AspNetUsers` -2. `AspNetRoles` -3. `AspNetUserRoles` -4. `AspNetUserClaims` -5. `AspNetUserLogins` -6. `AspNetUserTokens` -7. `AspNetRoleClaims` - -### Tables Removed (8 legacy tables) - -1. `Recipes` (legacy simple recipes) -2. `RecipeSteps` (legacy) -3. `IngredientRequirements` (legacy) -4. `CategoryRecipe` (legacy join) -5. `Events` (unused feature) -6. `Carts` (renamed to recipe_lists) -7. `CartIngredients` (removed, recipes only in lists) -8. `StandardReferenceNutritionData` (consolidated to nutrition_facts) -9. `BrandedNutritionData` (consolidated to nutrition_facts) - -## Key Changes Summary - -- **Entity Naming**: MultiPartRecipe → Recipe (simplified) -- **Purpose Clarity**: Cart → RecipeList -- **Nutrition Consolidation**: Two tables → Single `nutrition_facts` with jsonb -- **Image Storage**: Byte arrays → Azure Blob URLs -- **Data Access**: Entity Framework ORM → Raw Npgsql + stored procedures -- **Schema Management**: EF Migrations → Numbered SQL scripts with tracking table -- **Transactions**: Application-managed → Stored procedure-managed -- **Validation**: EF attributes → PostgreSQL validation functions -- **Relationships**: Navigation properties → Explicit JOINs in stored procedures - -## Testing Strategy - -Create separate test database with same `cooktime` schema for integration tests. Test against realistic PostgreSQL instance without affecting production data. - -## Migration Rollback Strategy - -Rely on database backups for rollback during initial development. No down migration scripts initially - add later if needed for production scenarios. - -## Next Steps - -1. Define specification for exporting recipes from database -2. Implement core schema DDL script (001_schema.sql) -3. Implement validation functions (002_validation.sql) -4. Implement write stored procedures (003_write_functions.sql) -5. Implement read stored procedures (004_read_functions.sql) -6. Implement image schema changes (005_images.sql) -7. Create C# DTOs and contracts -8. Implement repository classes -9. Update controllers -10. Remove EF dependencies -11. Test with test database -12. Migrate production data diff --git a/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.css b/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.css index b74f7650..7af53c2b 100644 --- a/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.css +++ b/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.css @@ -15,4 +15,16 @@ .navbar-toggler { color: var(--themePrimary) } +} + +/* Center GitHub link on mobile when navbar is collapsed */ +@media (max-width: 767.98px) { + .github-link { + text-align: center; + width: 100%; + display: flex; + justify-content: center; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + } } \ No newline at end of file diff --git a/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.tsx b/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.tsx index 7f391e37..6036212a 100644 --- a/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.tsx +++ b/src/CookTime/client-app/src/components/NavigationBar/NavigationBar.tsx @@ -3,6 +3,7 @@ import { Button, Container, Form, Nav, Navbar, NavDropdown } from "react-bootstr import { Form as RouterForm, Link } from "react-router"; import imgs from "../../assets"; import "./NavigationBar.css" +import { GitHubIcon } from "../SVG"; import { RequireAuth, useAuthentication } from "../Authentication/AuthenticationContext"; import { UserDetails } from "src/shared/AuthenticationProvider"; import { RECIPE_CREATE_PAGE_PATH } from "src/pages/RecipeCreation"; @@ -135,6 +136,14 @@ export function NavigationBar({ categories }: { categories: string[] }) { + + {GitHubIcon} + diff --git a/src/CookTime/client-app/src/components/SVG.tsx b/src/CookTime/client-app/src/components/SVG.tsx index d3464478..c5b0ded4 100644 --- a/src/CookTime/client-app/src/components/SVG.tsx +++ b/src/CookTime/client-app/src/components/SVG.tsx @@ -1,4 +1,6 @@ import { SVGProps } from "react"; -export const Fa6SolidStar = +export const GitHubIcon = + +export const Fa6SolidStar = export const Fa6RegularStar = \ No newline at end of file