e5d1080fb9
Implements the six previously-unscoped feature ideas plus a mobile layout fix reported via screenshot: - Mobile: Recipes/Collections/Pantry/Meal Plan/Shopping Lists headers now stack and wrap instead of clipping buttons on narrow viewports. - Recipe diff/compare view: word/list diff against any past version, next to Restore in version history. - Shared meal plans & shopping lists: new shoppingListMembers/ mealPlanMembers tables (viewer/editor roles, mirrors collectionMembers), share dialogs, membership-checked routes. - PDF cookbook export: /print/collection/[id] renders a whole collection with page breaks, using the existing print-CSS pattern instead of adding a PDF rendering dependency. - Grocery delivery handoff: shopping lists can copy-as-text (works today) or send to Instacart once INSTACART_API_KEY is configured (stub adapter — real API needs a partner agreement). - Personalized "For You" feed tab: ranks public recipes by tag/ dietary overlap with the user's favorited/highly-rated history. - PWA: added manifest.json + icons on top of the existing service worker so the app is installable; cook-mode pages were already cached for offline use. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
120 lines
5.0 KiB
TypeScript
120 lines
5.0 KiB
TypeScript
import {
|
|
pgTable,
|
|
text,
|
|
timestamp,
|
|
integer,
|
|
boolean,
|
|
date,
|
|
decimal,
|
|
pgEnum,
|
|
index,
|
|
} from "drizzle-orm/pg-core";
|
|
import { relations } from "drizzle-orm";
|
|
import { users } from "./users";
|
|
import { recipes } from "./recipes";
|
|
import { ingredients } from "./recipes";
|
|
import { collectionMemberRoleEnum } from "./social";
|
|
|
|
export const mealTypeEnum = pgEnum("meal_type", ["breakfast", "lunch", "dinner", "snack"]);
|
|
export const weekdayEnum = pgEnum("weekday", ["mon", "tue", "wed", "thu", "fri", "sat", "sun"]);
|
|
|
|
export const mealPlans = pgTable("meal_plans", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
weekStart: date("week_start").notNull(),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
});
|
|
|
|
export const mealPlanEntries = pgTable("meal_plan_entries", {
|
|
id: text("id").primaryKey(),
|
|
mealPlanId: text("meal_plan_id").notNull().references(() => mealPlans.id, { onDelete: "cascade" }),
|
|
day: weekdayEnum("day").notNull(),
|
|
mealType: mealTypeEnum("meal_type").notNull(),
|
|
recipeId: text("recipe_id").references(() => recipes.id, { onDelete: "set null" }),
|
|
servings: integer("servings").notNull().default(2),
|
|
note: text("note"),
|
|
});
|
|
|
|
export const pantryItems = pgTable("pantry_items", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }),
|
|
rawName: text("raw_name").notNull(),
|
|
quantity: decimal("quantity", { precision: 10, scale: 4 }),
|
|
unit: text("unit"),
|
|
expiresAt: timestamp("expires_at"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
});
|
|
|
|
export const shoppingLists = pgTable("shopping_lists", {
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").references(() => users.id, { onDelete: "cascade" }),
|
|
name: text("name").notNull(),
|
|
generatedAt: timestamp("generated_at"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
});
|
|
|
|
export const shoppingListItems = pgTable("shopping_list_items", {
|
|
id: text("id").primaryKey(),
|
|
listId: text("list_id").notNull().references(() => shoppingLists.id, { onDelete: "cascade" }),
|
|
ingredientId: text("ingredient_id").references(() => ingredients.id, { onDelete: "set null" }),
|
|
rawName: text("raw_name").notNull(),
|
|
quantity: decimal("quantity", { precision: 10, scale: 4 }),
|
|
unit: text("unit"),
|
|
aisle: text("aisle"),
|
|
checked: boolean("checked").notNull().default(false),
|
|
});
|
|
|
|
export const shoppingListMembers = pgTable("shopping_list_members", {
|
|
id: text("id").primaryKey(),
|
|
listId: text("list_id").notNull().references(() => shoppingLists.id, { onDelete: "cascade" }),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
role: collectionMemberRoleEnum("role").notNull().default("viewer"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("shopping_list_members_list_idx").on(t.listId),
|
|
index("shopping_list_members_user_idx").on(t.userId),
|
|
]);
|
|
|
|
export const mealPlanMembers = pgTable("meal_plan_members", {
|
|
id: text("id").primaryKey(),
|
|
mealPlanId: text("meal_plan_id").notNull().references(() => mealPlans.id, { onDelete: "cascade" }),
|
|
userId: text("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
role: collectionMemberRoleEnum("role").notNull().default("viewer"),
|
|
createdAt: timestamp("created_at").notNull().defaultNow(),
|
|
}, (t) => [
|
|
index("meal_plan_members_plan_idx").on(t.mealPlanId),
|
|
index("meal_plan_members_user_idx").on(t.userId),
|
|
]);
|
|
|
|
export const mealPlansRelations = relations(mealPlans, ({ one, many }) => ({
|
|
user: one(users, { fields: [mealPlans.userId], references: [users.id] }),
|
|
entries: many(mealPlanEntries),
|
|
members: many(mealPlanMembers),
|
|
}));
|
|
|
|
export const mealPlanEntriesRelations = relations(mealPlanEntries, ({ one }) => ({
|
|
mealPlan: one(mealPlans, { fields: [mealPlanEntries.mealPlanId], references: [mealPlans.id] }),
|
|
recipe: one(recipes, { fields: [mealPlanEntries.recipeId], references: [recipes.id] }),
|
|
}));
|
|
|
|
export const mealPlanMembersRelations = relations(mealPlanMembers, ({ one }) => ({
|
|
mealPlan: one(mealPlans, { fields: [mealPlanMembers.mealPlanId], references: [mealPlans.id] }),
|
|
user: one(users, { fields: [mealPlanMembers.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const shoppingListsRelations = relations(shoppingLists, ({ one, many }) => ({
|
|
user: one(users, { fields: [shoppingLists.userId], references: [users.id] }),
|
|
items: many(shoppingListItems),
|
|
members: many(shoppingListMembers),
|
|
}));
|
|
|
|
export const shoppingListItemsRelations = relations(shoppingListItems, ({ one }) => ({
|
|
list: one(shoppingLists, { fields: [shoppingListItems.listId], references: [shoppingLists.id] }),
|
|
}));
|
|
|
|
export const shoppingListMembersRelations = relations(shoppingListMembers, ({ one }) => ({
|
|
list: one(shoppingLists, { fields: [shoppingListMembers.listId], references: [shoppingLists.id] }),
|
|
user: one(users, { fields: [shoppingListMembers.userId], references: [users.id] }),
|
|
}));
|