Initial commit: YesChef family shopping list and recipe app

Backend (.NET 10 minimal API):
- Vertical slice architecture with feature folders
- Postgres via EF Core with initial migration
- JWT auth with family invite code registration
- REST endpoints for stores, shopping lists, items, recipes
- SignalR hub for real-time list collaboration (per-list groups
  and lists-overview group for live list creation/archival/progress)
- Multi-stage Dockerfile

Frontend (SvelteKit + Svelte 5 runes, Tailwind v4):
- Mobile-first PWA with web manifest and service worker
- Bottom-nav layout, login/register, lists overview, list detail,
  stores management, recipes (list/create/detail with add-to-list)
- SignalR client with reference-counted connection
- Real-time updates on both lists overview and list detail pages

Infrastructure:
- docker-compose.yml with postgres, backend, frontend services
  and Traefik labels for path-based routing (/api, /hubs to backend)
- .env.example with required config

End-to-end tests (Playwright):
- test-e2e.mjs: single-user flow (auth, stores, lists, items, recipes)
- test-e2e-multiuser.mjs: two-user real-time sync coverage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Josh Rogers
2026-05-06 19:32:39 -05:00
commit 48d30df07b
64 changed files with 5873 additions and 0 deletions
+57
View File
@@ -0,0 +1,57 @@
using Microsoft.EntityFrameworkCore;
using YesChef.Api.Entities;
namespace YesChef.Api.Data;
public class YesChefDb(DbContextOptions<YesChefDb> options) : DbContext(options)
{
public DbSet<User> Users => Set<User>();
public DbSet<Store> Stores => Set<Store>();
public DbSet<ShoppingList> ShoppingLists => Set<ShoppingList>();
public DbSet<ShoppingListItem> ShoppingListItems => Set<ShoppingListItem>();
public DbSet<Recipe> Recipes => Set<Recipe>();
public DbSet<RecipeIngredient> RecipeIngredients => Set<RecipeIngredient>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>(e =>
{
e.HasIndex(u => u.Name).IsUnique();
e.Property(u => u.Name).HasMaxLength(100);
});
modelBuilder.Entity<Store>(e =>
{
e.HasIndex(s => s.Name).IsUnique();
e.Property(s => s.Name).HasMaxLength(100);
});
modelBuilder.Entity<ShoppingList>(e =>
{
e.Property(l => l.Name).HasMaxLength(200);
e.HasOne(l => l.Store).WithMany().HasForeignKey(l => l.StoreId);
e.HasOne(l => l.CreatedByUser).WithMany().HasForeignKey(l => l.CreatedByUserId);
e.HasMany(l => l.Items).WithOne(i => i.ShoppingList).HasForeignKey(i => i.ShoppingListId).OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<ShoppingListItem>(e =>
{
e.Property(i => i.Name).HasMaxLength(300);
e.HasOne(i => i.CheckedByUser).WithMany().HasForeignKey(i => i.CheckedByUserId).OnDelete(DeleteBehavior.SetNull);
e.HasOne(i => i.Recipe).WithMany().HasForeignKey(i => i.RecipeId).OnDelete(DeleteBehavior.SetNull);
});
modelBuilder.Entity<Recipe>(e =>
{
e.Property(r => r.Title).HasMaxLength(300);
e.HasOne(r => r.CreatedByUser).WithMany().HasForeignKey(r => r.CreatedByUserId);
e.HasMany(r => r.Ingredients).WithOne(i => i.Recipe).HasForeignKey(i => i.RecipeId).OnDelete(DeleteBehavior.Cascade);
});
modelBuilder.Entity<RecipeIngredient>(e =>
{
e.Property(i => i.Name).HasMaxLength(200);
e.Property(i => i.Quantity).HasMaxLength(50);
});
}
}