Compare commits

..

9 Commits

Author SHA1 Message Date
914633a793 fixed the rest of it too 2025-08-16 13:09:13 +12:00
ba237d645e fixed tag row being a nuisance 2025-08-16 13:07:53 +12:00
b2c4b6e765 Got the detail view working again 2025-08-16 12:54:15 +12:00
cf44f19310 made new client the default export 2025-08-16 12:28:42 +12:00
b514655b5c progressively updating to use api 2025-08-16 12:22:10 +12:00
b3f92210fa Proxy forwards Query parameters too 2025-08-16 11:01:56 +12:00
2e54690ee4 Starting basic API client 2025-08-15 19:01:47 +12:00
f7e24657f5 Initial schema creation 2025-08-15 18:02:13 +12:00
0b1334d508 [PIE-13] New recipe page (!11)
Add `/recipe/new` path with form to add new recipe
Mostly designed but not fully: steps and ingredients are inputtable with final styling but it cannot be submitted yet, and other crucial components like description, rating, etc are not yet implemented.
Many design changes too cos i couldnt help myself

More additions will certainly be required but this PR is huge so I will split it out into more
Reviewed-on: #11
Co-authored-by: june <self@breadone.net>
Co-committed-by: june <self@breadone.net>
2025-08-15 16:12:02 +12:00
11 changed files with 124 additions and 68 deletions

View File

@@ -4,8 +4,7 @@ import TagRow from "./TagRow.astro"
const { recipe } = Astro.props;
const headerImage = await client.collection("images").getOne(recipe.images[0])
const image = await client.files.getRelativeURL(headerImage, headerImage.image)
const image = (await client.getRecipeImages(recipe.id))[0]
---
<div class="relative z-0 flex h-50">
@@ -19,7 +18,7 @@ const image = await client.files.getRelativeURL(headerImage, headerImage.image)
<!-- <p id="recipe-desc" class="text-white text-[10pt]"> {recipe.description} </p> -->
<div id="tag-row" class="">
<TagRow tagIds={recipe.tags}/>
<TagRow tags={recipe.expand.tags}/>
</div>
</div>

View File

@@ -1,27 +1,10 @@
---
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
}
}))
: []
const { tags } = Astro.props
---
<div class="">
{
tags.map(tag => (
(tags ?? []).map(tag => (
<a
href={`/tag/${tag.id}`}
class="text-white bg-white/20 px-2 mr-2 mt-2 rounded-md inline-block hover:bg-white/30"

View File

@@ -1,17 +1,7 @@
---
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.getRelativeURL(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))
)
const links = await client.getRecipeImages(recipe as string)
---

View File

@@ -26,4 +26,4 @@ const waitTime = formatTime(re.waittime)
{waitTime && (<p class="border-r px-2">{waitTime} Wait</p>)}
{re.rating !== 0 && (<p class="pl-2">{re.rating}</p><p class="text-white/40 text-xs">/10</p>)}
</div>
<TagRow tagIds={re.tags}/>
<TagRow tags={re.expand.tags}/>

View File

@@ -3,13 +3,10 @@ import client from "@/data/pocketbase";
const { ingredients, class: className } = Astro.props;
const ings = await Promise.all(
ingredients.map(async i => await client.collection("ingredients").getOne(i))
)
---
<div class={className}>
{ings.map(i => (
{ingredients.map(i => (
<div class="text-sm">
<p>• {i.quantity} {i.unit || " "} {i.name}</p>
</div>

View File

@@ -16,7 +16,7 @@ const { steps, class: className } = Astro.props
<p class="w-full md:flex-2/3 pr-1 text-left">{s.instruction}</p>
{s.ingredients && s.ingredients.length > 0 && (
<div class="w-full md:w-auto md:flex-2/5 mt-2 md:mt-0 md:ml-2 md:pl-3 md:border-l text-white/70 border-white/70">
<StepIngredientSideView class="text-left" ingredients={s.ingredients} />
<StepIngredientSideView class="text-left" ingredients={s.expand.ingredients} />
</div>
)}
</div>

View File

@@ -1,12 +1,60 @@
import Pocketbase from "pocketbase"
import Pocketbase, { type RecordListOptions } from "pocketbase"
import {
type Recipe,
type Ingredient,
type Step,
type Tag,
Collection
} from './schema'
const client = new Pocketbase("http://localhost:4321")
client.autoCancellation(false)
const raw_client = new Pocketbase("http://localhost:4321")
raw_client.autoCancellation(false)
// Return a relative url for file instead of full path including localhost, which breaks external access
client.files.getRelativeURL = (record: { [key: string]: any; }, filename: string, queryParams?: FileOptions | undefined) => {
const res = client.files.getURL(record, filename)
raw_client.files.getRelativeURL = (record: { [key: string]: any; }, filename: string, queryParams?: FileOptions | undefined) => {
const res = raw_client.files.getURL(record, filename)
return res.substring(21)
}
export { raw_client };
class APIClient {
client: Pocketbase
constructor() {
this.client = new Pocketbase("http://localhost:4321")
this.client.autoCancellation(false)
}
async getRecipesPage(page: number, perPage: number = 30, options: RecordListOptions) {
return await this.client.collection<Recipe>(Collection.RECIPES).getList(page, perPage, options)
}
async getAllRecipes() {
return await this.client.collection<Recipe>(Collection.RECIPES).getFullList({ expand: 'ingredients,tags,steps,images,steps.ingredients' })
}
async getRecipe(id: string) {
return await this.client.collection<Recipe>(Collection.RECIPES).getOne(id, { expand: 'ingredients,tags,steps,images,steps.ingredients' })
}
// IMAGE
async getImageURL(imgID: string, relative: boolean = true) {
const record = await this.client.collection("images").getOne(imgID)
const res = this.client.files.getURL(record, record.image)
return relative ? res.substring(21) : res
}
async getRecipeImages(recipeID: string) {
const re = await this.getRecipe(recipeID)
const imgIDs = re.images ?? []
const urls = Promise.all(
imgIDs.map(img => this.getImageURL(img))
)
return urls
}
}
const client = new APIClient()
export default client;

55
src/data/schema.ts Normal file
View File

@@ -0,0 +1,55 @@
// Base PB type
export interface BaseRecord {
id: string,
created: string,
updated: string
}
export interface Ingredient extends BaseRecord {
quantity: string,
unit: string,
name: string
}
export interface Step extends BaseRecord {
index: number,
instruction: string,
ingredients?: Ingredient[]
}
export interface Tag extends BaseRecord {
name: string
}
// not sure Image is the best type cos it might be quite heavy to get all the fields every time but
// it is here in case it is (a good idea)
export interface Image extends BaseRecord {
id: string
filename: string
url?: string
}
export interface Recipe extends BaseRecord {
name: string,
description?: string,
servings?: number,
images?: string[], // image IDs
ingredients: string[]
steps: string[],
tags?: string[]
expand: {
images?: Image[], // image IDs,
ingredients: Ingredient[]
steps: Step[],
tags?: Tag[]
}
}
export const Collection = {
RECIPES: 'recipes',
STEPS: 'steps',
INGREDIENTS: 'ingredients',
TAGS: 'tags',
IMAGES: 'images'
}

View File

@@ -5,8 +5,7 @@ import type { APIRoute } from "astro";
const getProxyUrl = (request: Request) => {
const proxyUrl = new URL(import.meta.env.PUBLIC_PB_URL);
const requestUrl = new URL(request.url);
return new URL(requestUrl.pathname, proxyUrl);
return new URL(requestUrl.pathname + requestUrl.search, proxyUrl);
};
export const ALL: APIRoute = async ({ request }) => {

View File

@@ -3,7 +3,7 @@ import PageLayout from "@/layouts/base"
import client from "@/data/pocketbase"
import OverviewCard from "@/components/Card/OverviewCard"
const recipies = await client.collection("recipes").getFullList()
const recipes = await client.getAllRecipes()
---
<PageLayout>
@@ -11,7 +11,7 @@ const recipies = await client.collection("recipes").getFullList()
<div class="grid md:gap-2 gap-3 grid-cols-2 md:grid-cols-2 lg:grid-cols-4 xl:grid-cols-8">
{
recipies.map(r => (
recipes.map(r => (
<OverviewCard recipe={r} />
))
}

View File

@@ -1,5 +1,6 @@
---
import client from "@/data/pocketbase";
import SiteLayout from "@/layouts/base";
import ImageCarousel from "@/components/Detail/ImageCarousel";
import IngredientTableView from "@/components/Detail/IngredientTableView";
@@ -8,41 +9,25 @@ import InfoView from "@/components/Detail/InfoView";
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)
)
)
const re = await client.getRecipe(recipeid as string)
---
<SiteLayout>
<div class="flex flex-col md:flex-row mx-auto justify-center w-full lg:max-w-3/4 xl:max-w-2/3 2xl:max-w-1/2">
<div class="flex md:flex-1/3 flex-col mt-2 md:mt-4 sticky">
<ImageCarousel class="w-full" recipe={re} />
<ImageCarousel class="w-full" recipe={recipeid} />
<p class="text-[28pt] font-bold leading-11 mt-2">{re.name}</p>
<!-- Details -->
<InfoView re={re} />
<p class="text-[22pt] font-bold 'md:mt-4'">Ingredients</p>
<IngredientTableView class:list={['md:w-80', 'px-4']} ingredients={ingredients} />
<IngredientTableView class:list={['md:w-80', 'px-4']} ingredients={re.expand.ingredients ?? []} />
</div>
<div class="flex mt-4 md:flex-2/3 w-full flex-col">
<!-- Steps -->
<StepView class="md:ml-3" steps={steps} />
<StepView class="md:ml-3" steps={re.expand.steps ?? []} />
</div>
</div>