feat: Add Zod validation for categories API response

This commit is contained in:
Ruidy (aider) 2024-12-14 15:28:20 +01:00 committed by Ruidy
parent 296b2048e9
commit 2010270bcf
No known key found for this signature in database
GPG key ID: E00F51288CB857CC
5 changed files with 64 additions and 39 deletions

View file

@ -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>

View file

@ -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

View file

@ -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
View 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>;

View file

@ -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>;