From b8de3e82e9dbbfa0165b3ceb2394efd576e7d1ab Mon Sep 17 00:00:00 2001 From: june Date: Wed, 13 Aug 2025 16:54:05 +1200 Subject: [PATCH] [PIE-8] Add recipe detail view And Other Major Changes (!6) - Adds /recipe/:id page - (Almost!) fully designed and functional recipe view - Has steps, images, ingredients, peripheral info (work/wait time, rating, description, servings) - New colour scheme! Much more monotone but in a nice way (imo) - New font, not condensed - Several more smaller design changes Co-authored-by: june Co-committed-by: june --- api/pb_migrations/1755039357_updated_steps.js | 28 ++++++ .../1755041844_created_ingredients.js | 97 +++++++++++++++++++ .../1755041886_updated_recipes.js | 28 ++++++ .../1755042306_updated_ingredients.js | 28 ++++++ api/pb_migrations/1755045585_updated_steps.js | 28 ++++++ .../1755056714_updated_recipes.js | 44 +++++++++ src/components/Card/OverviewCard.astro | 24 +++-- src/components/Card/TagRow.astro | 33 +++++++ src/components/Detail/ImageCarousel.astro | 58 +++++++++++ .../Detail/IngredientTableView.astro | 42 ++++++++ src/components/Header.astro | 13 +-- src/layouts/base.astro | 4 +- src/pages/index.astro | 16 ++- src/pages/recipe/[recipeid].astro | 79 +++++++++++++++ src/styles/global.css | 9 +- 15 files changed, 502 insertions(+), 29 deletions(-) create mode 100644 api/pb_migrations/1755039357_updated_steps.js create mode 100644 api/pb_migrations/1755041844_created_ingredients.js create mode 100644 api/pb_migrations/1755041886_updated_recipes.js create mode 100644 api/pb_migrations/1755042306_updated_ingredients.js create mode 100644 api/pb_migrations/1755045585_updated_steps.js create mode 100644 api/pb_migrations/1755056714_updated_recipes.js create mode 100644 src/components/Card/TagRow.astro create mode 100644 src/components/Detail/ImageCarousel.astro create mode 100644 src/components/Detail/IngredientTableView.astro create mode 100644 src/pages/recipe/[recipeid].astro diff --git a/api/pb_migrations/1755039357_updated_steps.js b/api/pb_migrations/1755039357_updated_steps.js new file mode 100644 index 0000000..33096e5 --- /dev/null +++ b/api/pb_migrations/1755039357_updated_steps.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_4284789913") + + // remove field + collection.fields.removeById("relation3666391351") + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_4284789913") + + // add field + collection.fields.addAt(3, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_842702175", + "hidden": false, + "id": "relation3666391351", + "maxSelect": 1, + "minSelect": 0, + "name": "recipe", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + })) + + return app.save(collection) +}) diff --git a/api/pb_migrations/1755041844_created_ingredients.js b/api/pb_migrations/1755041844_created_ingredients.js new file mode 100644 index 0000000..cd01b49 --- /dev/null +++ b/api/pb_migrations/1755041844_created_ingredients.js @@ -0,0 +1,97 @@ +/// +migrate((app) => { + const collection = new Collection({ + "createRule": null, + "deleteRule": null, + "fields": [ + { + "autogeneratePattern": "[a-z0-9]{15}", + "hidden": false, + "id": "text3208210256", + "max": 15, + "min": 15, + "name": "id", + "pattern": "^[a-z0-9]+$", + "presentable": false, + "primaryKey": true, + "required": true, + "system": true, + "type": "text" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text1579384326", + "max": 0, + "min": 0, + "name": "name", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "number2683508278", + "max": null, + "min": null, + "name": "quantity", + "onlyInt": false, + "presentable": false, + "required": false, + "system": false, + "type": "number" + }, + { + "autogeneratePattern": "", + "hidden": false, + "id": "text3703245907", + "max": 0, + "min": 0, + "name": "unit", + "pattern": "", + "presentable": false, + "primaryKey": false, + "required": false, + "system": false, + "type": "text" + }, + { + "hidden": false, + "id": "autodate2990389176", + "name": "created", + "onCreate": true, + "onUpdate": false, + "presentable": false, + "system": false, + "type": "autodate" + }, + { + "hidden": false, + "id": "autodate3332085495", + "name": "updated", + "onCreate": true, + "onUpdate": true, + "presentable": false, + "system": false, + "type": "autodate" + } + ], + "id": "pbc_3146854971", + "indexes": [], + "listRule": null, + "name": "ingredients", + "system": false, + "type": "base", + "updateRule": null, + "viewRule": null + }); + + return app.save(collection); +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3146854971"); + + return app.delete(collection); +}) diff --git a/api/pb_migrations/1755041886_updated_recipes.js b/api/pb_migrations/1755041886_updated_recipes.js new file mode 100644 index 0000000..bd0603b --- /dev/null +++ b/api/pb_migrations/1755041886_updated_recipes.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_842702175") + + // add field + collection.fields.addAt(8, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_3146854971", + "hidden": false, + "id": "relation1264587087", + "maxSelect": 999, + "minSelect": 0, + "name": "ingredients", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_842702175") + + // remove field + collection.fields.removeById("relation1264587087") + + return app.save(collection) +}) diff --git a/api/pb_migrations/1755042306_updated_ingredients.js b/api/pb_migrations/1755042306_updated_ingredients.js new file mode 100644 index 0000000..547ebf9 --- /dev/null +++ b/api/pb_migrations/1755042306_updated_ingredients.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_3146854971") + + // update collection data + unmarshal({ + "createRule": "", + "deleteRule": "", + "listRule": "", + "updateRule": "", + "viewRule": "" + }, collection) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_3146854971") + + // update collection data + unmarshal({ + "createRule": null, + "deleteRule": null, + "listRule": null, + "updateRule": null, + "viewRule": null + }, collection) + + return app.save(collection) +}) diff --git a/api/pb_migrations/1755045585_updated_steps.js b/api/pb_migrations/1755045585_updated_steps.js new file mode 100644 index 0000000..df7c5b5 --- /dev/null +++ b/api/pb_migrations/1755045585_updated_steps.js @@ -0,0 +1,28 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_4284789913") + + // add field + collection.fields.addAt(3, new Field({ + "cascadeDelete": false, + "collectionId": "pbc_3146854971", + "hidden": false, + "id": "relation1264587087", + "maxSelect": 999, + "minSelect": 0, + "name": "ingredients", + "presentable": false, + "required": false, + "system": false, + "type": "relation" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_4284789913") + + // remove field + collection.fields.removeById("relation1264587087") + + return app.save(collection) +}) diff --git a/api/pb_migrations/1755056714_updated_recipes.js b/api/pb_migrations/1755056714_updated_recipes.js new file mode 100644 index 0000000..e829e0d --- /dev/null +++ b/api/pb_migrations/1755056714_updated_recipes.js @@ -0,0 +1,44 @@ +/// +migrate((app) => { + const collection = app.findCollectionByNameOrId("pbc_842702175") + + // add field + collection.fields.addAt(9, new Field({ + "hidden": false, + "id": "number1485952547", + "max": null, + "min": null, + "name": "worktime", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + // add field + collection.fields.addAt(10, new Field({ + "hidden": false, + "id": "number2198822773", + "max": null, + "min": null, + "name": "waittime", + "onlyInt": true, + "presentable": false, + "required": false, + "system": false, + "type": "number" + })) + + return app.save(collection) +}, (app) => { + const collection = app.findCollectionByNameOrId("pbc_842702175") + + // remove field + collection.fields.removeById("number1485952547") + + // remove field + collection.fields.removeById("number2198822773") + + return app.save(collection) +}) diff --git a/src/components/Card/OverviewCard.astro b/src/components/Card/OverviewCard.astro index 84f2b0d..23618ee 100644 --- a/src/components/Card/OverviewCard.astro +++ b/src/components/Card/OverviewCard.astro @@ -1,27 +1,31 @@ --- import client from "@/data/pocketbase" +import TagRow from "./TagRow.astro" + const { recipe } = Astro.props; const headerImage = await client.collection("images").getOne(recipe.images[0]) const image = await client.files.getURL(headerImage, headerImage.image) --- -
+
-
-

{recipe.name}

-

{recipe.description}

+
+

{recipe.name}

+ -
- {recipe.tags.map(async tag => ( -

{ - (await client.collection("tags").getOne(tag)).name - }

- ))} +
+
+ + + + + +
\ No newline at end of file diff --git a/src/components/Card/TagRow.astro b/src/components/Card/TagRow.astro new file mode 100644 index 0000000..af56aec --- /dev/null +++ b/src/components/Card/TagRow.astro @@ -0,0 +1,33 @@ +--- +import client from "../../data/pocketbase" + +interface Props { + tagIds: string[] +} + +const { tagIds } = Astro.props + +const tags = tagIds && tagIds.length > 0 + ? await Promise.all(tagIds.map(async (tagId: string) => { + try { + const tagData = await client.collection("tags").getOne(tagId) + return { name: tagData.name, id: tagId } + } catch (error) { + return null + } + })) + : [] +--- + +
+ { + tags.map(tag => ( + + {tag.name} + + )) + } +
diff --git a/src/components/Detail/ImageCarousel.astro b/src/components/Detail/ImageCarousel.astro new file mode 100644 index 0000000..ece401e --- /dev/null +++ b/src/components/Detail/ImageCarousel.astro @@ -0,0 +1,58 @@ +--- +import client from "@/data/pocketbase"; +const { class: className, recipe } = Astro.props + +async function getLink(img: string) { + const record = await client.collection("images").getOne(img) + const link = await client.files.getURL(record, record.image) + return link +} + +// Use Promise.all to wait for all async operations to complete +const links = await Promise.all( + recipe.images.map((img: string) => getLink(img)) +) + +--- + + + +
+ + + +
+ + + + +
+
\ No newline at end of file diff --git a/src/components/Detail/IngredientTableView.astro b/src/components/Detail/IngredientTableView.astro new file mode 100644 index 0000000..1f38027 --- /dev/null +++ b/src/components/Detail/IngredientTableView.astro @@ -0,0 +1,42 @@ +--- +// const { ingredients } = Astro.props + +const { class: className, ingredients } = Astro.props +--- + + + + + + + + + + + + + { + ingredients.map(ing => ( + <> + + + + + + + )) + } + +
QuantityUnitFood
{ing.quantity}{ing.unit}{ing.name}
\ No newline at end of file diff --git a/src/components/Header.astro b/src/components/Header.astro index c582951..5ffff2c 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -1,13 +1,14 @@ -
+
-

Reci

-

pie

+

Recipie

+ 🥧
-
- +
+ new + tags + search
\ No newline at end of file diff --git a/src/layouts/base.astro b/src/layouts/base.astro index 42a0407..f738f0a 100644 --- a/src/layouts/base.astro +++ b/src/layouts/base.astro @@ -10,7 +10,9 @@ import Header from "@/components/Header";
- +
+ +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 906caf3..bf1ef86 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -7,16 +7,14 @@ const recipies = await client.collection("recipes").getFullList() --- -
- + -
- { - recipies.map(r => ( - - )) - } -
+
+ { + recipies.map(r => ( + + )) + }
diff --git a/src/pages/recipe/[recipeid].astro b/src/pages/recipe/[recipeid].astro new file mode 100644 index 0000000..7e3df49 --- /dev/null +++ b/src/pages/recipe/[recipeid].astro @@ -0,0 +1,79 @@ +--- +import client from "@/data/pocketbase"; +import SiteLayout from "@/layouts/base"; +import ImageCarousel from "@/components/Detail/ImageCarousel"; +import IngredientTableView from "@/components/Detail/IngredientTableView"; + +const { recipeid } = Astro.params; + +const re = await client.collection("recipes").getOne(recipeid ?? "0"); + +const stepIds = re.steps + +let steps = await Promise.all( + stepIds.map(async s => + await client.collection("steps").getOne(s) + ) +) + +steps = steps.sort((a, b) => a.index - b.index); + +const ingredients = await Promise.all( + re.ingredients.map(async s => + await client.collection("ingredients").getOne(s) + ) +) + +function formatTime(seconds) { + if (seconds === 0) return null + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + let result = ""; + if (h > 0) result += `${h}h`; + if (m > 0) result += `${m}m`; + if (result === "") result = "0m"; + return result; +} + +const workTime = formatTime(re.worktime) +const waitTime = formatTime(re.waittime) +--- + + +
+
+ +

{re.name}

+ + +

{re.description}

+
+ {re.servings !== 0 && (

Serves: {re.servings}

)} + {workTime && (

{workTime} work

)} + {waitTime && (

{waitTime} wait

)} + {re.rating !== 0 && (

{re.rating}⭐️

)} +
+ +

Ingredients

+ +
+ +
+ + + +
+

Steps

+ { steps.map(s => ( +
+

Step {s.index + 1}

+

{s.instruction}

+
+ )) } +
+ + +
+ +
+
\ No newline at end of file diff --git a/src/styles/global.css b/src/styles/global.css index bebe217..12ec143 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -1,7 +1,10 @@ @import "tailwindcss"; html { - /* @apply bg-[#1d1f21]; */ - @apply bg-[#fafafa]; - @apply font-stretch-condensed; + @apply bg-[#1d1f21]; + /* @apply bg-[#fafafa]; */ + @apply text-white; + /* @apply font-; */ + @apply font-sans; + /* font-family: 'SF Pro Display', 'Segoe UI', 'Helvetica Neue', Arial, 'Noto Sans', sans-serif; */ } \ No newline at end of file