mirror of
https://github.com/rjNemo/meal_planner
synced 2026-06-12 13:26:45 +00:00
feat: Add Zod validation for categories API response
This commit is contained in:
parent
296b2048e9
commit
2010270bcf
5 changed files with 64 additions and 39 deletions
|
|
@ -1,30 +1,28 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { Category } from "~/types/category";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
category: Category;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NuxtLink
|
<nuxt-link
|
||||||
:to="`/category/${category.strCategory}`"
|
:to="`/category/${category.name}`"
|
||||||
class="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
|
class="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
:src="category.strCategoryThumb"
|
:src="category.picture"
|
||||||
:alt="category.strCategory"
|
:alt="category.name"
|
||||||
class="mb-4 h-48 w-full object-cover rounded"
|
class="mb-4 h-48 w-full object-cover rounded"
|
||||||
/>
|
/>
|
||||||
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
<h5
|
||||||
{{ category.strCategory }}
|
class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white"
|
||||||
|
>
|
||||||
|
{{ category.name }}
|
||||||
</h5>
|
</h5>
|
||||||
<p class="font-normal text-gray-700 dark:text-gray-400">
|
<p class="font-normal text-gray-700 dark:text-gray-400">
|
||||||
{{ category.strCategoryDescription }}
|
{{ category.description }}
|
||||||
</p>
|
</p>
|
||||||
</NuxtLink>
|
</nuxt-link>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
interface Category {
|
|
||||||
strCategory: string
|
|
||||||
strCategoryThumb: string
|
|
||||||
strCategoryDescription: string
|
|
||||||
}
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
category: Category
|
|
||||||
}>()
|
|
||||||
</script>
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const { data: categories, status, error } = useCategories();
|
const { data: categories, status, error } = useCategories();
|
||||||
|
|
||||||
|
console.log(categories.value);
|
||||||
|
|
||||||
if (error.value) {
|
if (error.value) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
|
|
@ -15,25 +17,26 @@ if (error.value) {
|
||||||
<span class="loading loading-spinner loading-lg text-primary" />
|
<span class="loading loading-spinner loading-lg text-primary" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-else-if="categories?.length > 0"
|
v-else-if="categories!.length > 0"
|
||||||
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 my-8"
|
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 my-8"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.strCategory"
|
:key="category.name"
|
||||||
class="card bg-base-100 shadow-xl h-[28rem] sm:h-[32rem] md:h-[36rem] lg:h-[32rem]"
|
class="card bg-base-100 shadow-xl h-[28rem] sm:h-[32rem] md:h-[36rem] lg:h-[32rem]"
|
||||||
>
|
>
|
||||||
<figure>
|
<figure>
|
||||||
<img :src="category.strCategoryThumb" :alt="category.strCategory" />
|
<img :src="category.picture" :alt="category.name" />
|
||||||
</figure>
|
</figure>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h2 class="card-title">{{ category.strCategory }}</h2>
|
<h2 class="card-title">{{ category.name }}</h2>
|
||||||
<p class="line-clamp-6 text-sm">{{ category.strCategoryDescription }}</p>
|
<p class="line-clamp-6 text-sm">
|
||||||
|
{{ category.description }}
|
||||||
|
</p>
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<nuxt-link
|
<nuxt-link
|
||||||
:to="`/category/${category.strCategory}`"
|
:to="`/category/${category.name}`"
|
||||||
class="btn btn-primary"
|
class="btn btn-primary"
|
||||||
>
|
>
|
||||||
View Recipes
|
View Recipes
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
import { publicProcedure, router } from "../trpc";
|
import { publicProcedure, router } from "../trpc";
|
||||||
|
import {} from "~/types/recipe";
|
||||||
|
import {
|
||||||
|
categoriesResponseSchema,
|
||||||
|
type CategoriesResponse,
|
||||||
|
} from "~/types/category";
|
||||||
|
|
||||||
const { apiUrl } = useRuntimeConfig();
|
const { apiUrl } = useRuntimeConfig();
|
||||||
|
|
||||||
type Category = {
|
|
||||||
idCategory: string;
|
|
||||||
strCategory: string;
|
|
||||||
strCategoryThumb: string;
|
|
||||||
strCategoryDescription: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const categoryRouter = router({
|
export const categoryRouter = router({
|
||||||
listCategories: publicProcedure.query(async () => {
|
listCategories: publicProcedure.query(async () => {
|
||||||
const data = await $fetch<{ categories: Category[] }>(
|
const response = await $fetch<CategoriesResponse>(
|
||||||
new URL("categories.php", apiUrl).toString(),
|
new URL("categories.php", apiUrl).href,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!data?.categories) {
|
const result = categoriesResponseSchema.safeParse(response);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
throw createError({
|
throw createError({
|
||||||
statusCode: 500,
|
statusCode: 500,
|
||||||
statusMessage: "Failed to fetch categories",
|
statusMessage: "Invalid API response format",
|
||||||
|
data: result.error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return data.categories;
|
return result.data.categories.sort((a, b) => a.name.localeCompare(b.name));
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
|
||||||
22
types/category.ts
Normal file
22
types/category.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const categorySchema = z
|
||||||
|
.object({
|
||||||
|
idCategory: z.string(),
|
||||||
|
strCategory: z.string(),
|
||||||
|
strCategoryThumb: z.string().url(),
|
||||||
|
strCategoryDescription: z.string(),
|
||||||
|
})
|
||||||
|
.transform((c) => ({
|
||||||
|
identity: c.idCategory,
|
||||||
|
name: c.strCategory,
|
||||||
|
picture: c.strCategoryThumb,
|
||||||
|
description: c.strCategoryDescription,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const categoriesResponseSchema = z.object({
|
||||||
|
categories: z.array(categorySchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Category = z.infer<typeof categorySchema>;
|
||||||
|
export type CategoriesResponse = z.infer<typeof categoriesResponseSchema>;
|
||||||
|
|
@ -72,4 +72,5 @@ export const apiResponseSchema = z.object({
|
||||||
});
|
});
|
||||||
|
|
||||||
export type Meal = z.infer<typeof mealSchema>;
|
export type Meal = z.infer<typeof mealSchema>;
|
||||||
|
|
||||||
export type ApiResponse = z.infer<typeof apiResponseSchema>;
|
export type ApiResponse = z.infer<typeof apiResponseSchema>;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue