mirror of
https://github.com/taogaetz/chefbible.git
synced 2025-12-06 11:47:24 -05:00
first pass at layout, created basic frontpage components, added station and time to schema
This commit is contained in:
parent
b0905697da
commit
161b83371e
1
.gitignore
vendored
1
.gitignore
vendored
@ -5,6 +5,7 @@ node_modules
|
|||||||
.vercel
|
.vercel
|
||||||
.netlify
|
.netlify
|
||||||
.wrangler
|
.wrangler
|
||||||
|
database.db
|
||||||
/.svelte-kit
|
/.svelte-kit
|
||||||
/build
|
/build
|
||||||
|
|
||||||
|
|||||||
21
package-lock.json
generated
21
package-lock.json
generated
@ -8,18 +8,18 @@
|
|||||||
"name": "chefbible",
|
"name": "chefbible",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@faker-js/faker": "^9.9.0",
|
|
||||||
"@prisma/client": "^6.14.0",
|
"@prisma/client": "^6.14.0",
|
||||||
"@prisma/extension-accelerate": "^2.0.2"
|
"@prisma/extension-accelerate": "^2.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@faker-js/faker": "^9.9.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
"@types/node": "^24.2.1",
|
"daisyui": "^5.0.50",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
@ -669,6 +669,7 @@
|
|||||||
"version": "9.9.0",
|
"version": "9.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.9.0.tgz",
|
||||||
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
|
"integrity": "sha512-OEl393iCOoo/z8bMezRlJu+GlRGlsKbUAN7jKB6LhnKoqKve5DXRpalbItIIcwnCjs1k/FOPjFzcA6Qn+H+YbA==",
|
||||||
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@ -1635,6 +1636,8 @@
|
|||||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.10.0"
|
"undici-types": "~7.10.0"
|
||||||
}
|
}
|
||||||
@ -2195,6 +2198,16 @@
|
|||||||
"node": ">=4"
|
"node": ">=4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/daisyui": {
|
||||||
|
"version": "5.0.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.0.50.tgz",
|
||||||
|
"integrity": "sha512-c1PweK5RI1C76q58FKvbS4jzgyNJSP6CGTQ+KkZYzADdJoERnOxFoeLfDHmQgxLpjEzlYhFMXCeodQNLCC9bow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||||
@ -4440,7 +4453,9 @@
|
|||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
|
|||||||
@ -19,10 +19,12 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/compat": "^1.2.5",
|
"@eslint/compat": "^1.2.5",
|
||||||
"@eslint/js": "^9.18.0",
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@faker-js/faker": "^9.9.0",
|
||||||
"@sveltejs/adapter-auto": "^6.0.0",
|
"@sveltejs/adapter-auto": "^6.0.0",
|
||||||
"@sveltejs/kit": "^2.22.0",
|
"@sveltejs/kit": "^2.22.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
"@sveltejs/vite-plugin-svelte": "^6.0.0",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.0.0",
|
||||||
|
"daisyui": "^5.0.50",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"eslint-config-prettier": "^10.0.1",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-svelte": "^3.0.0",
|
"eslint-plugin-svelte": "^3.0.0",
|
||||||
|
|||||||
@ -1,97 +0,0 @@
|
|||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."Recipe" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"description" TEXT,
|
|
||||||
"instructions" TEXT,
|
|
||||||
"photoUrl" TEXT,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "Recipe_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."Ingredient" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "Ingredient_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."RecipeIngredient" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"recipeId" TEXT NOT NULL,
|
|
||||||
"ingredientId" TEXT NOT NULL,
|
|
||||||
"quantity" DOUBLE PRECISION,
|
|
||||||
"unit" TEXT,
|
|
||||||
"prep" TEXT,
|
|
||||||
|
|
||||||
CONSTRAINT "RecipeIngredient_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."Allergen" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"description" TEXT,
|
|
||||||
|
|
||||||
CONSTRAINT "Allergen_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."Menu" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"name" TEXT NOT NULL,
|
|
||||||
"season" TEXT,
|
|
||||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
||||||
|
|
||||||
CONSTRAINT "Menu_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."MenuRecipe" (
|
|
||||||
"id" TEXT NOT NULL,
|
|
||||||
"menuId" TEXT NOT NULL,
|
|
||||||
"recipeId" TEXT NOT NULL,
|
|
||||||
"position" INTEGER,
|
|
||||||
|
|
||||||
CONSTRAINT "MenuRecipe_pkey" PRIMARY KEY ("id")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateTable
|
|
||||||
CREATE TABLE "public"."_IngredientAllergens" (
|
|
||||||
"A" TEXT NOT NULL,
|
|
||||||
"B" TEXT NOT NULL,
|
|
||||||
|
|
||||||
CONSTRAINT "_IngredientAllergens_AB_pkey" PRIMARY KEY ("A","B")
|
|
||||||
);
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Ingredient_name_key" ON "public"."Ingredient"("name");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE UNIQUE INDEX "Allergen_name_key" ON "public"."Allergen"("name");
|
|
||||||
|
|
||||||
-- CreateIndex
|
|
||||||
CREATE INDEX "_IngredientAllergens_B_index" ON "public"."_IngredientAllergens"("B");
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."RecipeIngredient" ADD CONSTRAINT "RecipeIngredient_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "public"."Recipe"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."RecipeIngredient" ADD CONSTRAINT "RecipeIngredient_ingredientId_fkey" FOREIGN KEY ("ingredientId") REFERENCES "public"."Ingredient"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."MenuRecipe" ADD CONSTRAINT "MenuRecipe_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "public"."Menu"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."MenuRecipe" ADD CONSTRAINT "MenuRecipe_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "public"."Recipe"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."_IngredientAllergens" ADD CONSTRAINT "_IngredientAllergens_A_fkey" FOREIGN KEY ("A") REFERENCES "public"."Allergen"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
|
|
||||||
-- AddForeignKey
|
|
||||||
ALTER TABLE "public"."_IngredientAllergens" ADD CONSTRAINT "_IngredientAllergens_B_fkey" FOREIGN KEY ("B") REFERENCES "public"."Ingredient"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
|
||||||
74
prisma/migrations/20250815161754_init/migration.sql
Normal file
74
prisma/migrations/20250815161754_init/migration.sql
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Recipe" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"instructions" TEXT,
|
||||||
|
"photoUrl" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Ingredient" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "RecipeIngredient" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"recipeId" TEXT NOT NULL,
|
||||||
|
"ingredientId" TEXT NOT NULL,
|
||||||
|
"quantity" REAL,
|
||||||
|
"unit" TEXT,
|
||||||
|
"prep" TEXT,
|
||||||
|
CONSTRAINT "RecipeIngredient_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "RecipeIngredient_ingredientId_fkey" FOREIGN KEY ("ingredientId") REFERENCES "Ingredient" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Allergen" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Menu" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"season" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "MenuRecipe" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"menuId" TEXT NOT NULL,
|
||||||
|
"recipeId" TEXT NOT NULL,
|
||||||
|
"position" INTEGER,
|
||||||
|
CONSTRAINT "MenuRecipe_menuId_fkey" FOREIGN KEY ("menuId") REFERENCES "Menu" ("id") ON DELETE RESTRICT ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "MenuRecipe_recipeId_fkey" FOREIGN KEY ("recipeId") REFERENCES "Recipe" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_IngredientAllergens" (
|
||||||
|
"A" TEXT NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
CONSTRAINT "_IngredientAllergens_A_fkey" FOREIGN KEY ("A") REFERENCES "Allergen" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
|
||||||
|
CONSTRAINT "_IngredientAllergens_B_fkey" FOREIGN KEY ("B") REFERENCES "Ingredient" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Ingredient_name_key" ON "Ingredient"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "Allergen_name_key" ON "Allergen"("name");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "_IngredientAllergens_AB_unique" ON "_IngredientAllergens"("A", "B");
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_IngredientAllergens_B_index" ON "_IngredientAllergens"("B");
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
-- RedefineTables
|
||||||
|
PRAGMA defer_foreign_keys=ON;
|
||||||
|
PRAGMA foreign_keys=OFF;
|
||||||
|
CREATE TABLE "new_Recipe" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"description" TEXT,
|
||||||
|
"instructions" TEXT,
|
||||||
|
"photoUrl" TEXT,
|
||||||
|
"time" TEXT NOT NULL DEFAULT 'Medium',
|
||||||
|
"station" TEXT NOT NULL DEFAULT 'Pans',
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO "new_Recipe" ("createdAt", "description", "id", "instructions", "name", "photoUrl", "updatedAt") SELECT "createdAt", "description", "id", "instructions", "name", "photoUrl", "updatedAt" FROM "Recipe";
|
||||||
|
DROP TABLE "Recipe";
|
||||||
|
ALTER TABLE "new_Recipe" RENAME TO "Recipe";
|
||||||
|
PRAGMA foreign_keys=ON;
|
||||||
|
PRAGMA defer_foreign_keys=OFF;
|
||||||
@ -1,3 +1,3 @@
|
|||||||
# Please do not edit this file manually
|
# Please do not edit this file manually
|
||||||
# It should be added in your version-control system (e.g., Git)
|
# It should be added in your version-control system (e.g., Git)
|
||||||
provider = "postgresql"
|
provider = "sqlite"
|
||||||
|
|||||||
@ -13,8 +13,8 @@ generator client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "sqlite"
|
||||||
url = env("DATABASE_URL")
|
url = "file:./database.db"
|
||||||
}
|
}
|
||||||
|
|
||||||
model Recipe {
|
model Recipe {
|
||||||
@ -23,6 +23,8 @@ model Recipe {
|
|||||||
description String?
|
description String?
|
||||||
instructions String?
|
instructions String?
|
||||||
photoUrl String?
|
photoUrl String?
|
||||||
|
time String @default("Medium") // Quick, Medium, Long
|
||||||
|
station String @default("Pans") // Garde Manger, Pans, Grill
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|
||||||
|
|||||||
@ -25,8 +25,10 @@ async function main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Ingredients (mix of spices, vegetables, meats, fruits)
|
// Ingredients (mix of spices, vegetables, meats, fruits)
|
||||||
const ingredients = await Promise.all(
|
const ingredientNames = new Set<string>();
|
||||||
Array.from({ length: 20 }).map(() => {
|
const ingredients: any[] = [];
|
||||||
|
|
||||||
|
while (ingredientNames.size < 20) {
|
||||||
const typePick = faker.helpers.arrayElement([
|
const typePick = faker.helpers.arrayElement([
|
||||||
() => faker.food.ingredient(),
|
() => faker.food.ingredient(),
|
||||||
() => faker.food.spice(),
|
() => faker.food.spice(),
|
||||||
@ -34,26 +36,96 @@ async function main() {
|
|||||||
() => faker.food.meat(),
|
() => faker.food.meat(),
|
||||||
() => faker.food.fruit(),
|
() => faker.food.fruit(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const name = typePick();
|
||||||
|
if (!ingredientNames.has(name)) {
|
||||||
|
ingredientNames.add(name);
|
||||||
const randomAllergens = faker.helpers.arrayElements(allergens, faker.number.int({ min: 0, max: 2 }));
|
const randomAllergens = faker.helpers.arrayElements(allergens, faker.number.int({ min: 0, max: 2 }));
|
||||||
return prisma.ingredient.create({
|
const ingredient = await prisma.ingredient.create({
|
||||||
data: {
|
data: {
|
||||||
name: typePick(),
|
name,
|
||||||
allergens: { connect: randomAllergens.map(a => ({ id: a.id })) },
|
allergens: { connect: randomAllergens.map(a => ({ id: a.id })) },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})
|
ingredients.push(ingredient);
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recipes with realistic kitchen data
|
||||||
|
const recipeData = [
|
||||||
|
{
|
||||||
|
name: 'Caesar Salad',
|
||||||
|
description: 'Classic Caesar salad with homemade dressing and croutons',
|
||||||
|
time: 'Quick',
|
||||||
|
station: 'Garde Manger'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Grilled Salmon',
|
||||||
|
description: 'Fresh salmon fillet grilled to perfection with herbs',
|
||||||
|
time: 'Medium',
|
||||||
|
station: 'Grill'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Beef Stir Fry',
|
||||||
|
description: 'Tender beef with vegetables in a savory sauce',
|
||||||
|
time: 'Quick',
|
||||||
|
station: 'Pans'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Braised Short Ribs',
|
||||||
|
description: 'Slow-cooked short ribs in red wine reduction',
|
||||||
|
time: 'Long',
|
||||||
|
station: 'Pans'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Charcuterie Board',
|
||||||
|
description: 'Artisanal meats and cheeses with accompaniments',
|
||||||
|
time: 'Quick',
|
||||||
|
station: 'Garde Manger'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Grilled Chicken Breast',
|
||||||
|
description: 'Herb-marinated chicken breast with seasonal vegetables',
|
||||||
|
time: 'Medium',
|
||||||
|
station: 'Grill'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pasta Carbonara',
|
||||||
|
description: 'Classic Italian pasta with eggs, cheese, and pancetta',
|
||||||
|
time: 'Medium',
|
||||||
|
station: 'Pans'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Beef Tenderloin',
|
||||||
|
description: 'Premium cut grilled to your preference',
|
||||||
|
time: 'Medium',
|
||||||
|
station: 'Grill'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Garden Salad',
|
||||||
|
description: 'Fresh mixed greens with house vinaigrette',
|
||||||
|
time: 'Quick',
|
||||||
|
station: 'Garde Manger'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Duck Confit',
|
||||||
|
description: 'Traditional French duck confit with crispy skin',
|
||||||
|
time: 'Long',
|
||||||
|
station: 'Pans'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
// Recipes
|
|
||||||
const recipes = await Promise.all(
|
const recipes = await Promise.all(
|
||||||
Array.from({ length: 10 }).map(async () => {
|
recipeData.map(async (recipeInfo) => {
|
||||||
const recipeIngredients = faker.helpers.arrayElements(ingredients, faker.number.int({ min: 3, max: 6 }));
|
const recipeIngredients = faker.helpers.arrayElements(ingredients, faker.number.int({ min: 3, max: 6 }));
|
||||||
return prisma.recipe.create({
|
return prisma.recipe.create({
|
||||||
data: {
|
data: {
|
||||||
name: faker.food.dish(),
|
name: recipeInfo.name,
|
||||||
description: faker.food.description(),
|
description: recipeInfo.description,
|
||||||
|
time: recipeInfo.time,
|
||||||
|
station: recipeInfo.station,
|
||||||
instructions: faker.lorem.paragraph(),
|
instructions: faker.lorem.paragraph(),
|
||||||
photoUrl: faker.image.urlLoremFlickr({ category: 'food' }),
|
photoUrl: null,
|
||||||
ingredients: {
|
ingredients: {
|
||||||
create: recipeIngredients.map(ing => ({
|
create: recipeIngredients.map(ing => ({
|
||||||
ingredientId: ing.id,
|
ingredientId: ing.id,
|
||||||
|
|||||||
48
src/app.css
48
src/app.css
@ -1 +1,49 @@
|
|||||||
@import 'tailwindcss';
|
@import 'tailwindcss';
|
||||||
|
@plugin "daisyui";
|
||||||
|
|
||||||
|
/* Global styles for Chef Bible */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
|
||||||
|
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
|
||||||
|
sans-serif;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
-moz-osx-font-smoothing: grayscale;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth scrolling */
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Better focus styles for accessibility */
|
||||||
|
button:focus-visible,
|
||||||
|
a:focus-visible {
|
||||||
|
outline: 2px solid #667eea;
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom scrollbar */
|
||||||
|
::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
|
background: #f1f5f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
362
src/lib/components/RecipeCard.svelte
Normal file
362
src/lib/components/RecipeCard.svelte
Normal file
@ -0,0 +1,362 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Recipe } from '@prisma-app/client';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
export let recipe: Recipe;
|
||||||
|
|
||||||
|
function handleClick() {
|
||||||
|
goto(`/recipe/${recipe.id}`);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="recipe-card" on:click={handleClick}>
|
||||||
|
<div class="image-container">
|
||||||
|
{#if recipe.photoUrl}
|
||||||
|
<img
|
||||||
|
src={recipe.photoUrl}
|
||||||
|
alt={recipe.name}
|
||||||
|
loading="lazy"
|
||||||
|
on:error={(e) => {
|
||||||
|
const target = e.target as HTMLImageElement;
|
||||||
|
target.style.display = 'none';
|
||||||
|
target.nextElementSibling?.classList.remove('hidden');
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<div class="placeholder-image hidden">
|
||||||
|
<div class="placeholder-icon">
|
||||||
|
<svg
|
||||||
|
width="48"
|
||||||
|
height="48"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="placeholder-text">
|
||||||
|
<span class="recipe-name">{recipe.name}</span>
|
||||||
|
<span class="recipe-station">{recipe.station}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-overlay">
|
||||||
|
<button class="view-button">
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z" />
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
View Recipe
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content">
|
||||||
|
<h3 class="title">{recipe.name}</h3>
|
||||||
|
{#if recipe.description}
|
||||||
|
<p class="description">{recipe.description}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<span class="time">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12,6 12,12 16,14" />
|
||||||
|
</svg>
|
||||||
|
{recipe.time || 'Medium'}
|
||||||
|
</span>
|
||||||
|
<span class="station">{recipe.station || 'Pans'}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.recipe-card {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||||
|
0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
border: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card:hover {
|
||||||
|
transform: translateY(-4px);
|
||||||
|
box-shadow:
|
||||||
|
0 20px 25px -5px rgba(0, 0, 0, 0.1),
|
||||||
|
0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 200px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card:hover .image-container img {
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
|
||||||
|
color: #64748b;
|
||||||
|
gap: 16px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(
|
||||||
|
45deg,
|
||||||
|
transparent 30%,
|
||||||
|
rgba(255, 255, 255, 0.1) 50%,
|
||||||
|
transparent 70%
|
||||||
|
);
|
||||||
|
animation: shimmer 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes shimmer {
|
||||||
|
0% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.3);
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon svg {
|
||||||
|
color: white;
|
||||||
|
filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-name {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1e293b;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.3;
|
||||||
|
max-width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-station {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #64748b;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
padding: 4px 12px;
|
||||||
|
border-radius: 12px;
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image span {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-align: center;
|
||||||
|
max-width: 80%;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.1) 100%);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-card:hover .image-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-button {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
color: #1f2937;
|
||||||
|
border: none;
|
||||||
|
padding: 12px 20px;
|
||||||
|
border-radius: 25px;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-button:hover {
|
||||||
|
background: white;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
padding: 20px;
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.3;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
flex: 1;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: 12px;
|
||||||
|
border-top: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time,
|
||||||
|
.station {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.time {
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.station {
|
||||||
|
color: #059669;
|
||||||
|
background: #d1fae5;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile optimizations */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.description {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-container {
|
||||||
|
height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon {
|
||||||
|
width: 60px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-icon svg {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-name {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-station {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
314
src/lib/components/SearchBar.svelte
Normal file
314
src/lib/components/SearchBar.svelte
Normal file
@ -0,0 +1,314 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from 'svelte';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
export let recipes: any[] = [];
|
||||||
|
export let placeholder: string = 'Search recipes...';
|
||||||
|
|
||||||
|
let searchTerm = '';
|
||||||
|
let filteredRecipes: any[] = [];
|
||||||
|
let showResults = false;
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
if (!searchTerm.trim()) {
|
||||||
|
filteredRecipes = [];
|
||||||
|
showResults = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const term = searchTerm.toLowerCase();
|
||||||
|
filteredRecipes = recipes.filter(
|
||||||
|
(recipe) =>
|
||||||
|
recipe.name.toLowerCase().includes(term) ||
|
||||||
|
recipe.description?.toLowerCase().includes(term) ||
|
||||||
|
recipe.station.toLowerCase().includes(term) ||
|
||||||
|
recipe.time.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
showResults = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRecipeClick(recipe: any) {
|
||||||
|
goto(`/recipe/${recipe.id}`);
|
||||||
|
searchTerm = '';
|
||||||
|
showResults = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleInputFocus() {
|
||||||
|
if (searchTerm.trim()) {
|
||||||
|
showResults = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let searchContainer: HTMLElement;
|
||||||
|
|
||||||
|
// Handle clicks outside the search container
|
||||||
|
function handleClickOutside(event: MouseEvent) {
|
||||||
|
if (searchContainer && !searchContainer.contains(event.target as Node)) {
|
||||||
|
showResults = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add event listener when component mounts
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="search-container" bind:this={searchContainer}>
|
||||||
|
<div class="search-input-wrapper">
|
||||||
|
<svg
|
||||||
|
class="search-icon"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="11" cy="11" r="8" />
|
||||||
|
<path d="m21 21-4.35-4.35" />
|
||||||
|
</svg>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={searchTerm}
|
||||||
|
on:input={handleSearch}
|
||||||
|
on:focus={handleInputFocus}
|
||||||
|
{placeholder}
|
||||||
|
class="search-input"
|
||||||
|
/>
|
||||||
|
{#if searchTerm}
|
||||||
|
<button
|
||||||
|
class="clear-button"
|
||||||
|
on:click={() => {
|
||||||
|
searchTerm = '';
|
||||||
|
showResults = false;
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M18 6L6 18M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if showResults && filteredRecipes.length > 0}
|
||||||
|
<div class="search-results">
|
||||||
|
{#each filteredRecipes as recipe}
|
||||||
|
<button class="result-item" on:click={() => handleRecipeClick(recipe)}>
|
||||||
|
<div class="result-content">
|
||||||
|
<h4 class="result-title">{recipe.name}</h4>
|
||||||
|
{#if recipe.description}
|
||||||
|
<p class="result-description">{recipe.description}</p>
|
||||||
|
{/if}
|
||||||
|
<div class="result-meta">
|
||||||
|
<span class="result-station">{recipe.station}</span>
|
||||||
|
<span class="result-time">{recipe.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<svg
|
||||||
|
class="result-arrow"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M9 18l6-6-6-6" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{:else if showResults && searchTerm.trim()}
|
||||||
|
<div class="search-results">
|
||||||
|
<div class="no-results">
|
||||||
|
<p>No recipes found for "{searchTerm}"</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input-wrapper:focus-within {
|
||||||
|
border-color: #667eea;
|
||||||
|
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: absolute;
|
||||||
|
left: 16px;
|
||||||
|
color: #9ca3af;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 14px 16px 14px 48px;
|
||||||
|
border: none;
|
||||||
|
outline: none;
|
||||||
|
font-size: 16px;
|
||||||
|
background: transparent;
|
||||||
|
color: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input::placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
position: absolute;
|
||||||
|
right: 12px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #9ca3af;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.15);
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
margin-top: 8px;
|
||||||
|
max-height: 400px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-item:hover {
|
||||||
|
background: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-content {
|
||||||
|
flex: 1;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-title {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-description {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6b7280;
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
line-height: 1.4;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 2;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-station,
|
||||||
|
.result-time {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-station {
|
||||||
|
background: #d1fae5;
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-time {
|
||||||
|
background: #fef3c7;
|
||||||
|
color: #d97706;
|
||||||
|
}
|
||||||
|
|
||||||
|
.result-arrow {
|
||||||
|
color: #9ca3af;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.no-results {
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
color: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.search-container {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
font-size: 16px; /* Prevents zoom on iOS */
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-results {
|
||||||
|
max-height: 300px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,10 +1,6 @@
|
|||||||
// place files you want to import through the `$lib` alias in this folder.
|
// place files you want to import through the `$lib` alias in this folder.
|
||||||
import { PrismaClient } from '@prisma-app/client';
|
import { PrismaClient } from '@prisma-app/client';
|
||||||
import { DATABASE_URL } from '$env/static/private';
|
|
||||||
import { withAccelerate } from '@prisma/extension-accelerate';
|
|
||||||
|
|
||||||
const prisma = new PrismaClient({
|
const prisma = new PrismaClient()
|
||||||
datasourceUrl: DATABASE_URL
|
|
||||||
}).$extends(withAccelerate());
|
|
||||||
|
|
||||||
export default prisma
|
export default prisma
|
||||||
8
src/routes/+layout.server.ts
Normal file
8
src/routes/+layout.server.ts
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import type { LayoutServerLoad } from './$types';
|
||||||
|
|
||||||
|
import prisma from '$lib/server/prisma';
|
||||||
|
|
||||||
|
export const load: LayoutServerLoad = async () => {
|
||||||
|
const recipes = await prisma.recipe.findMany();
|
||||||
|
return { recipes };
|
||||||
|
};
|
||||||
@ -1,12 +1,83 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import '../app.css';
|
import '../app.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
|
import SearchBar from '$lib/components/SearchBar.svelte';
|
||||||
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children, data } = $props<{ children: any; data: LayoutData }>();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<link rel="icon" href={favicon} />
|
<link rel="icon" href={favicon} />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
{@render children?.()}
|
<div class="layout">
|
||||||
|
<header class="global-header">
|
||||||
|
<div class="header-content">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="/">Chef Bible</a>
|
||||||
|
</div>
|
||||||
|
<SearchBar recipes={data.recipes} />
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main-content">
|
||||||
|
{@render children?.()}
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.layout {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-header {
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
padding: 16px 0;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-content {
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo a {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #1f2937;
|
||||||
|
text-decoration: none;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.header-content {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 0 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo a {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
// TODO: Fix this frickin bug!!!!
|
|
||||||
|
|
||||||
import prisma from '$lib/server/prisma';
|
|
||||||
import type { PageServerLoad } from './$types';
|
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
|
||||||
const recipes = await prisma.recipe.findMany();
|
|
||||||
return { recipes };
|
|
||||||
};
|
|
||||||
@ -1,7 +1,72 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageProps } from "./$types";
|
import type { PageProps } from './$types';
|
||||||
|
import RecipeCard from '$lib/components/RecipeCard.svelte';
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
let { data }: PageProps = $props();
|
||||||
console.log(data)
|
|
||||||
|
// Debug: Log the first recipe to see what fields are available
|
||||||
|
console.log('First recipe data:', data.recipes[0]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>Chef Bible - Recipes</title>
|
||||||
|
<meta name="description" content="Birbante" />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main class="container">
|
||||||
|
<section class="recipes-section">
|
||||||
|
<div class="recipes-grid">
|
||||||
|
{#each data.recipes as recipe}
|
||||||
|
<RecipeCard {recipe} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.container {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipes-section {
|
||||||
|
padding: 40px 20px;
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipes-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 24px;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile-first responsive design */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.recipes-section {
|
||||||
|
padding: 24px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipes-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 641px) and (max-width: 1024px) {
|
||||||
|
.recipes-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1025px) {
|
||||||
|
.recipes-grid {
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
|
||||||
|
gap: 28px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
22
src/routes/recipe/[id]/+page.server.ts
Normal file
22
src/routes/recipe/[id]/+page.server.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { PageServerLoad } from './$types';
|
||||||
|
import { error } from '@sveltejs/kit';
|
||||||
|
import prisma from '$lib/server/prisma';
|
||||||
|
|
||||||
|
export const load: PageServerLoad = async ({ params }) => {
|
||||||
|
const recipe = await prisma.recipe.findUnique({
|
||||||
|
where: { id: params.id },
|
||||||
|
include: {
|
||||||
|
ingredients: {
|
||||||
|
include: {
|
||||||
|
ingredient: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!recipe) {
|
||||||
|
throw error(404, 'Recipe not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { recipe };
|
||||||
|
};
|
||||||
341
src/routes/recipe/[id]/+page.svelte
Normal file
341
src/routes/recipe/[id]/+page.svelte
Normal file
@ -0,0 +1,341 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { PageProps } from './$types';
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
|
let { data }: PageProps = $props();
|
||||||
|
const { recipe } = data;
|
||||||
|
|
||||||
|
function goBack() {
|
||||||
|
goto('/');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:head>
|
||||||
|
<title>{recipe.name} - Chef Bible</title>
|
||||||
|
<meta name="description" content={recipe.description || `Recipe for ${recipe.name}`} />
|
||||||
|
</svelte:head>
|
||||||
|
|
||||||
|
<main class="recipe-page">
|
||||||
|
<header class="recipe-header">
|
||||||
|
<button class="back-button" on:click={goBack}>
|
||||||
|
<svg
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M19 12H5M12 19l-7-7 7-7" />
|
||||||
|
</svg>
|
||||||
|
Back to Recipes
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="recipe-content">
|
||||||
|
<div class="recipe-hero">
|
||||||
|
<div class="recipe-image">
|
||||||
|
{#if recipe.photoUrl}
|
||||||
|
<img src={recipe.photoUrl} alt={recipe.name} />
|
||||||
|
{:else}
|
||||||
|
<div class="placeholder-image">
|
||||||
|
<svg
|
||||||
|
width="80"
|
||||||
|
height="80"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M14.828 14.828a4 4 0 01-5.656 0M9 10h1m4 0h1m-6 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span>{recipe.name}</span>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recipe-info">
|
||||||
|
<h1 class="recipe-title">{recipe.name}</h1>
|
||||||
|
{#if recipe.description}
|
||||||
|
<p class="recipe-description">{recipe.description}</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="recipe-meta">
|
||||||
|
<div class="meta-item">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<polyline points="12,6 12,12 16,14" />
|
||||||
|
</svg>
|
||||||
|
<span>{recipe.time}</span>
|
||||||
|
</div>
|
||||||
|
<div class="meta-item">
|
||||||
|
<svg
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path d="M3 3h18v18H3z" />
|
||||||
|
<path d="M9 9h6v6H9z" />
|
||||||
|
</svg>
|
||||||
|
<span>{recipe.station}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="recipe-details">
|
||||||
|
{#if recipe.ingredients && recipe.ingredients.length > 0}
|
||||||
|
<section class="ingredients-section">
|
||||||
|
<h2>Ingredients</h2>
|
||||||
|
<ul class="ingredients-list">
|
||||||
|
{#each recipe.ingredients as recipeIngredient}
|
||||||
|
<li class="ingredient-item">
|
||||||
|
<span class="ingredient-name">{recipeIngredient.ingredient.name}</span>
|
||||||
|
{#if recipeIngredient.quantity || recipeIngredient.unit}
|
||||||
|
<span class="ingredient-amount">
|
||||||
|
{recipeIngredient.quantity}
|
||||||
|
{recipeIngredient.unit}
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
{#if recipeIngredient.prep}
|
||||||
|
<span class="ingredient-prep">({recipeIngredient.prep})</span>
|
||||||
|
{/if}
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#if recipe.instructions}
|
||||||
|
<section class="instructions-section">
|
||||||
|
<h2>Instructions</h2>
|
||||||
|
<div class="instructions-content">
|
||||||
|
{recipe.instructions}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.recipe-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-header {
|
||||||
|
padding: 20px;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: #6b7280;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.05);
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-content {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-hero {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 2fr;
|
||||||
|
gap: 40px;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 40px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-image img {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: linear-gradient(135deg, #f3f4f6 0%, #e5e7eb 100%);
|
||||||
|
color: #6b7280;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder-image svg {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 16px 0;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: #6b7280;
|
||||||
|
line-height: 1.6;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-meta {
|
||||||
|
display: flex;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
background: #f3f4f6;
|
||||||
|
border-radius: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-details {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredients-section,
|
||||||
|
.instructions-section {
|
||||||
|
background: white;
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 32px;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredients-section h2,
|
||||||
|
.instructions-section h2 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #1f2937;
|
||||||
|
margin: 0 0 24px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredients-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 0;
|
||||||
|
border-bottom: 1px solid #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #1f2937;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-amount {
|
||||||
|
color: #6b7280;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ingredient-prep {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-style: italic;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions-content {
|
||||||
|
line-height: 1.7;
|
||||||
|
color: #374151;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.recipe-hero {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
padding: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-details {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-content {
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recipe-meta {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user