mirror of
https://github.com/rjNemo/meal_planner
synced 2026-06-10 20:36:39 +00:00
make api call on the server add tests
This commit is contained in:
parent
277ede1ad3
commit
1539a03084
10 changed files with 151 additions and 69 deletions
6
TODO.md
6
TODO.md
|
|
@ -4,10 +4,10 @@
|
|||
- [x] use nuxt framework
|
||||
- [x] rewrite the random page, the current landing page
|
||||
- [x] rewrite the recipe page
|
||||
- [ ] deploy
|
||||
- [ ] nuxt image
|
||||
- [x] deploy
|
||||
- [x] nuxt image
|
||||
- [x] prettier and eslint
|
||||
- [ ] transition
|
||||
- [ ] pwa
|
||||
- [ ] seo, robots.txt
|
||||
- [ ] update the README
|
||||
- [x] update the README
|
||||
|
|
|
|||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -1,61 +0,0 @@
|
|||
import type { Recipe } from "~/types/recipe";
|
||||
|
||||
type Keyword = "random" | "filter" | "lookup" | "search";
|
||||
|
||||
export default async function (keyword: Keyword, param?: string) {
|
||||
const { data, pending, error } = await useAsyncData(
|
||||
keyword,
|
||||
async () => {
|
||||
const config = useRuntimeConfig();
|
||||
|
||||
let url = "";
|
||||
|
||||
switch (keyword) {
|
||||
case "random":
|
||||
url = `${config.apiUrl}random.php`;
|
||||
break;
|
||||
case "filter":
|
||||
url = "";
|
||||
break;
|
||||
case "lookup":
|
||||
url = `${config.apiUrl}${keyword}.php?i=${param}`;
|
||||
break;
|
||||
case "search":
|
||||
url = "";
|
||||
break;
|
||||
default:
|
||||
throw Error("unexpected URI parameters");
|
||||
}
|
||||
return await $fetch(url);
|
||||
},
|
||||
{ lazy: true },
|
||||
);
|
||||
|
||||
const tmp = computed(() => data.value?.meals?.[0]);
|
||||
|
||||
const names: string[] = [];
|
||||
const quantities: number[] = [];
|
||||
for (const [k, v] of Object.entries(tmp.value)) {
|
||||
if (k.startsWith("strIngredient") && !!v) {
|
||||
names.push(v);
|
||||
} else if (k.startsWith("strMeasure") && !!v) {
|
||||
quantities.push(v);
|
||||
}
|
||||
}
|
||||
|
||||
const recipe = reactive<Recipe>({
|
||||
title: tmp.value.strMeal,
|
||||
pictureUrl: tmp.value.strMealThumb,
|
||||
videoUrl: tmp.value.strYoutube,
|
||||
category: tmp.value.strCategory,
|
||||
origin: tmp.value.strArea,
|
||||
ingredients: names.map((name, i) => ({ name, quantity: quantities[i] })),
|
||||
instructions: tmp.value.strInstructions,
|
||||
});
|
||||
|
||||
return {
|
||||
recipe,
|
||||
pending,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ export default defineNuxtConfig({
|
|||
// The private keys which are only available server-side
|
||||
apiUrl: "",
|
||||
// Keys within public are also exposed client-side
|
||||
public: {},
|
||||
},
|
||||
ssr: true,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="hero min-h-screen bg-base-200">
|
||||
<div class="hero min-h-full bg-base-200">
|
||||
<div class="hero-content flex-col lg:flex-row-reverse">
|
||||
<NuxtImg src="/chef.svg" class="max-w-sm rounded-lg shadow-2xl" />
|
||||
<div>
|
||||
<h1 class="text-5xl font-bold prose">Eat Something New</h1>
|
||||
<p class="py-6 prose">Generate a random recipe.</p>
|
||||
<NuxtLink to="/random" class="btn btn-primary" external>
|
||||
<NuxtLink to="/random" class="btn btn-primary">
|
||||
Random Recipe Now
|
||||
</NuxtLink>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
const { recipe, pending, error } = await useRecipe("random");
|
||||
const {
|
||||
data: recipe,
|
||||
pending,
|
||||
error,
|
||||
} = await useFetch("/api/recipes", { lazy: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
12
server/api/recipes.get.ts
Normal file
12
server/api/recipes.get.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { parseRecipeData } from "~/utils/recipes";
|
||||
|
||||
export default defineEventHandler(async (_event) => {
|
||||
const { apiUrl } = useRuntimeConfig();
|
||||
|
||||
const data = await $fetch<{ meals: unknown }>(
|
||||
new URL("random.php", apiUrl).toString(),
|
||||
);
|
||||
|
||||
const recipes = parseRecipeData(data);
|
||||
return recipes[0];
|
||||
});
|
||||
|
|
@ -4,6 +4,6 @@ export type Recipe = {
|
|||
videoUrl: string;
|
||||
category: string;
|
||||
origin: string;
|
||||
ingredients: { name: string; quantity: number }[];
|
||||
ingredients: { name: string; quantity: string }[];
|
||||
instructions: string;
|
||||
};
|
||||
|
|
|
|||
95
utils/recipes.test.ts
Normal file
95
utils/recipes.test.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
import { describe, expect, it } from "vitest";
|
||||
import type { Recipe } from "~/types/recipe";
|
||||
import { parseRecipeData } from "~/utils/recipes";
|
||||
|
||||
const sampleApiResponse = {
|
||||
meals: [
|
||||
{
|
||||
idMeal: "52915",
|
||||
strMeal: "French Omelette",
|
||||
strDrinkAlternate: null,
|
||||
strCategory: "Miscellaneous",
|
||||
strArea: "French",
|
||||
strInstructions:
|
||||
"Get everything ready. Warm a 20cm (measured across the top) non-stick frying pan on a medium heat. Crack the eggs into a bowl and beat them with a fork so they break up and mix, but not as completely as you would for scrambled egg. With the heat on medium-hot, drop one knob of butter into the pan. It should bubble and sizzle, but not brown. Season the eggs with the Parmesan and a little salt and pepper, and pour into the pan.\r\nLet the eggs bubble slightly for a couple of seconds, then take a wooden fork or spatula and gently draw the mixture in from the sides of the pan a few times, so it gathers in folds in the centre. Leave for a few seconds, then stir again to lightly combine uncooked egg with cooked. Leave briefly again, and when partly cooked, stir a bit faster, stopping while there’s some barely cooked egg left. With the pan flat on the heat, shake it back and forth a few times to settle the mixture. It should slide easily in the pan and look soft and moist on top. A quick burst of heat will brown the underside.\r\nGrip the handle underneath. Tilt the pan down away from you and let the omelette fall to the edge. Fold the side nearest to you over by a third with your fork, and keep it rolling over, so the omelette tips onto a plate – or fold it in half, if that’s easier. For a neat finish, cover the omelette with a piece of kitchen paper and plump it up a bit with your fingers. Rub the other knob of butter over to glaze. Serve immediately.",
|
||||
strMealThumb:
|
||||
"https://www.themealdb.com/images/media/meals/yvpuuy1511797244.jpg",
|
||||
strTags: "Egg",
|
||||
strYoutube: "https://www.youtube.com/watch?v=qXPhVYpQLPA",
|
||||
strIngredient1: "Eggs",
|
||||
strIngredient2: "Butter",
|
||||
strIngredient3: "Parmesan",
|
||||
strIngredient4: "Tarragon",
|
||||
strIngredient5: "Parsley",
|
||||
strIngredient6: "Chives",
|
||||
strIngredient7: "Gruyère",
|
||||
strIngredient8: "",
|
||||
strIngredient9: "",
|
||||
strIngredient10: "",
|
||||
strIngredient11: "",
|
||||
strIngredient12: "",
|
||||
strIngredient13: "",
|
||||
strIngredient14: "",
|
||||
strIngredient15: "",
|
||||
strIngredient16: "",
|
||||
strIngredient17: "",
|
||||
strIngredient18: "",
|
||||
strIngredient19: "",
|
||||
strIngredient20: "",
|
||||
strMeasure1: "3",
|
||||
strMeasure2: "2 knobs",
|
||||
strMeasure3: "1 tsp",
|
||||
strMeasure4: "3 chopped",
|
||||
strMeasure5: "1 tbs chopped",
|
||||
strMeasure6: "1 tbs chopped",
|
||||
strMeasure7: "4 tbs",
|
||||
strMeasure8: "",
|
||||
strMeasure9: "",
|
||||
strMeasure10: "",
|
||||
strMeasure11: "",
|
||||
strMeasure12: "",
|
||||
strMeasure13: "",
|
||||
strMeasure14: "",
|
||||
strMeasure15: "",
|
||||
strMeasure16: "",
|
||||
strMeasure17: "",
|
||||
strMeasure18: "",
|
||||
strMeasure19: "",
|
||||
strMeasure20: "",
|
||||
strSource:
|
||||
"https://www.bbcgoodfood.com/recipes/1669/ultimate-french-omelette",
|
||||
strImageSource: null,
|
||||
strCreativeCommonsConfirmed: null,
|
||||
dateModified: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe("parseRecipeData", () => {
|
||||
it("should parse the API response into the Recipe type", () => {
|
||||
const expectedResult: Recipe[] = [
|
||||
{
|
||||
title: "French Omelette",
|
||||
pictureUrl:
|
||||
"https://www.themealdb.com/images/media/meals/yvpuuy1511797244.jpg",
|
||||
videoUrl: "https://www.youtube.com/watch?v=qXPhVYpQLPA",
|
||||
category: "Miscellaneous",
|
||||
origin: "French",
|
||||
ingredients: [
|
||||
{ name: "Eggs", quantity: "3" },
|
||||
{ name: "Butter", quantity: "2 knobs" },
|
||||
{ name: "Parmesan", quantity: "1 tsp" },
|
||||
{ name: "Tarragon", quantity: "3 chopped" },
|
||||
{ name: "Parsley", quantity: "1 tbs chopped" },
|
||||
{ name: "Chives", quantity: "1 tbs chopped" },
|
||||
{ name: "Gruyère", quantity: "4 tbs" },
|
||||
],
|
||||
instructions:
|
||||
"Get everything ready. Warm a 20cm (measured across the top) non-stick frying pan on a medium heat. Crack the eggs into a bowl and beat them with a fork so they break up and mix, but not as completely as you would for scrambled egg. With the heat on medium-hot, drop one knob of butter into the pan. It should bubble and sizzle, but not brown. Season the eggs with the Parmesan and a little salt and pepper, and pour into the pan.\r\nLet the eggs bubble slightly for a couple of seconds, then take a wooden fork or spatula and gently draw the mixture in from the sides of the pan a few times, so it gathers in folds in the centre. Leave for a few seconds, then stir again to lightly combine uncooked egg with cooked. Leave briefly again, and when partly cooked, stir a bit faster, stopping while there’s some barely cooked egg left. With the pan flat on the heat, shake it back and forth a few times to settle the mixture. It should slide easily in the pan and look soft and moist on top. A quick burst of heat will brown the underside.\r\nGrip the handle underneath. Tilt the pan down away from you and let the omelette fall to the edge. Fold the side nearest to you over by a third with your fork, and keep it rolling over, so the omelette tips onto a plate – or fold it in half, if that’s easier. For a neat finish, cover the omelette with a piece of kitchen paper and plump it up a bit with your fingers. Rub the other knob of butter over to glaze. Serve immediately.",
|
||||
},
|
||||
];
|
||||
|
||||
const result = parseRecipeData(sampleApiResponse);
|
||||
expect(result).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
33
utils/recipes.ts
Normal file
33
utils/recipes.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import type { Recipe } from "~/types/recipe";
|
||||
|
||||
export function parseRecipeData(data: { meals: unknown }): Recipe[] {
|
||||
return data.meals.map((meal: unknown) => {
|
||||
// Extract ingredients and measurements
|
||||
const ingredients: { name: string; quantity: string }[] = [];
|
||||
for (let i = 1; i <= 20; i++) {
|
||||
const ingredientName = meal[`strIngredient${i}`];
|
||||
const ingredientQuantity = meal[`strMeasure${i}`];
|
||||
if (
|
||||
ingredientName &&
|
||||
ingredientName.trim() &&
|
||||
ingredientQuantity &&
|
||||
ingredientQuantity.trim()
|
||||
) {
|
||||
ingredients.push({
|
||||
name: ingredientName.trim(),
|
||||
quantity: ingredientQuantity.trim(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
title: meal.strMeal,
|
||||
pictureUrl: meal.strMealThumb,
|
||||
videoUrl: meal.strYoutube,
|
||||
category: meal.strCategory,
|
||||
origin: meal.strArea,
|
||||
ingredients: ingredients,
|
||||
instructions: meal.strInstructions,
|
||||
};
|
||||
});
|
||||
}
|
||||
Loading…
Reference in a new issue