From afdd64c8a13dc4c8dc04b59e209dd61e5441184b Mon Sep 17 00:00:00 2001 From: Justin Hall Date: Fri, 4 Aug 2023 20:59:46 -0600 Subject: [PATCH] fix: account for incomplete drink entries in Contentful --- app/routes/_app.$slug.tsx | 10 ++-- app/routes/_app._index.tsx | 5 +- app/routes/_app.search/route.tsx | 3 +- app/routes/_app.tags.$tag.tsx | 6 ++- app/routes/_app.tags._index.tsx | 5 +- app/types.ts | 22 ++++++--- app/utils/placeholder-images.server.ts | 64 +++++++++++++++---------- app/utils/prime-content-cache.server.ts | 2 +- 8 files changed, 72 insertions(+), 45 deletions(-) diff --git a/app/routes/_app.$slug.tsx b/app/routes/_app.$slug.tsx index c91a607a..ae127d40 100644 --- a/app/routes/_app.$slug.tsx +++ b/app/routes/_app.$slug.tsx @@ -14,7 +14,7 @@ import NavLink from '~/navigation/nav-link'; import Glass from '~/drinks/glass'; import DrinkSummary from '~/drinks/drink-summary'; import DrinkDetails from '~/drinks/drink-details'; -import type { DrinksResponse, EnhancedDrink } from '~/types'; +import type { Drink, DrinksResponse, EnhancedDrink } from '~/types'; export const loader = async ({ params, request }: LoaderArgs) => { if (!params.slug) throw json('Missing slug', 400); @@ -65,17 +65,17 @@ export const loader = async ({ params, request }: LoaderArgs) => { const { data: { - drinkCollection: { drinks }, + drinkCollection: { drinks: maybeDrinks }, }, } = queryResponseJson; + const drinks = maybeDrinks.filter((drink): drink is Drink => Boolean(drink)); + if (drinks.length === 0) { throw json({ message: 'Drink not found' }, 404); } - const drinksWithPlaceholderImages: Array = - await withPlaceholderImages(drinks); - + const drinksWithPlaceholderImages = await withPlaceholderImages(drinks); const [enhancedDrink] = drinksWithPlaceholderImages; if (enhancedDrink.notes) { diff --git a/app/routes/_app._index.tsx b/app/routes/_app._index.tsx index 7a6d3bba..f74760ec 100644 --- a/app/routes/_app._index.tsx +++ b/app/routes/_app._index.tsx @@ -6,7 +6,7 @@ import { cache } from '~/utils/cache.server'; import { withPlaceholderImages } from '~/utils/placeholder-images.server'; import Nav from '~/navigation/nav'; import DrinkList from '~/drinks/drink-list'; -import type { DrinksResponse, EnhancedDrink } from '~/types'; +import type { Drink, DrinksResponse, EnhancedDrink } from '~/types'; export type LoaderData = SerializeFrom; @@ -59,10 +59,11 @@ export const loader = async ({ request }: LoaderArgs) => { const { data: { - drinkCollection: { drinks }, + drinkCollection: { drinks: maybeDrinks }, }, } = queryResponseJson; + const drinks = maybeDrinks.filter((drink): drink is Drink => Boolean(drink)); const drinksWithPlaceholderImages = await withPlaceholderImages(drinks); const loaderData = { drinks: drinksWithPlaceholderImages }; diff --git a/app/routes/_app.search/route.tsx b/app/routes/_app.search/route.tsx index 44563c40..e569b5b0 100644 --- a/app/routes/_app.search/route.tsx +++ b/app/routes/_app.search/route.tsx @@ -82,10 +82,11 @@ export const loader = async ({ request }: LoaderArgs) => { const { data: { - drinkCollection: { drinks }, + drinkCollection: { drinks: maybeDrinks }, }, } = queryResponseJson; + const drinks = maybeDrinks.filter((drink): drink is Drink => Boolean(drink)); // sort results in the same order as slugs returned from Algolia drinks.sort((a, b) => slugs.indexOf(a.slug) - slugs.indexOf(b.slug)); diff --git a/app/routes/_app.tags.$tag.tsx b/app/routes/_app.tags.$tag.tsx index 81491c8c..77b0e9ac 100644 --- a/app/routes/_app.tags.$tag.tsx +++ b/app/routes/_app.tags.$tag.tsx @@ -12,7 +12,7 @@ import Nav from '~/navigation/nav'; import NavLink from '~/navigation/nav-link'; import NavDivider from '~/navigation/nav-divider'; import DrinkList from '~/drinks/drink-list'; -import type { DrinksResponse, EnhancedDrink } from '~/types'; +import type { Drink, DrinksResponse, EnhancedDrink } from '~/types'; export const loader = async ({ params, request }: LoaderArgs) => { if (!params.tag) throw json('Missing tag', 400); @@ -67,10 +67,12 @@ export const loader = async ({ params, request }: LoaderArgs) => { const { data: { - drinkCollection: { drinks }, + drinkCollection: { drinks: maybeDrinks }, }, } = queryResponseJson; + const drinks = maybeDrinks.filter((drink): drink is Drink => Boolean(drink)); + if (drinks.length === 0) { throw json({ message: 'No drinks found' }, 404); } diff --git a/app/routes/_app.tags._index.tsx b/app/routes/_app.tags._index.tsx index ef99010d..7daac846 100644 --- a/app/routes/_app.tags._index.tsx +++ b/app/routes/_app.tags._index.tsx @@ -10,7 +10,7 @@ import { getEnvVars } from '~/utils/env.server'; import { mergeMeta } from '~/utils/meta'; import { fetchGraphQL } from '~/utils/graphql.server'; import { cache } from '~/utils/cache.server'; -import type { DrinkTagsResponse } from '~/types'; +import type { Drink, DrinkTagsResponse } from '~/types'; export type LoaderData = SerializeFrom; @@ -52,10 +52,11 @@ export const loader = async ({ request }: LoaderArgs) => { const { data: { - drinkCollection: { drinks }, + drinkCollection: { drinks: maybeDrinks }, }, } = queryResponseJson; + const drinks = maybeDrinks.filter((drink): drink is Drink => Boolean(drink)); const uniqueTags = drinks.reduce>((acc, drink) => { drink.tags?.forEach((tag) => acc.add(tag)); return acc; diff --git a/app/types.ts b/app/types.ts index dbf40f44..9920ffce 100644 --- a/app/types.ts +++ b/app/types.ts @@ -15,7 +15,7 @@ export interface DrinksResponse { }>; data: { drinkCollection: { - drinks: Array; + drinks: Array; } | null; }; } @@ -28,20 +28,28 @@ export type DrinkTagsResponse = DrinksResponse & { }; }; +// Most of the fields will be null if you start to create a new Drink in +// Contentful without finishing it. export interface Drink { - title: string; + title: string | null; slug: string; image: { url: string; - }; - ingredients: Array; - calories: number; + } | null; + ingredients: Array | null; + calories: number | null; notes?: string; tags?: Array; } -export interface EnhancedDrink extends Drink { - image: Drink['image'] & { +export interface EnhancedDrink { + title: NonNullable; + slug: Drink['slug']; + image: NonNullable & { blurDataUrl: string; }; + ingredients: NonNullable; + calories: NonNullable; + notes?: Drink['notes']; + tags?: Drink['tags']; } diff --git a/app/utils/placeholder-images.server.ts b/app/utils/placeholder-images.server.ts index 18e0dd84..29709210 100644 --- a/app/utils/placeholder-images.server.ts +++ b/app/utils/placeholder-images.server.ts @@ -1,29 +1,43 @@ import { makeImageUrl } from '~/core/image'; -import type { Drink } from '~/types'; +import type { Drink, EnhancedDrink } from '~/types'; -export async function withPlaceholderImages(drinks: Array) { - return Promise.all( - drinks.map(async (drink) => { - const blurredImageUrl = makeImageUrl({ - baseImageUrl: drink.image.url, - width: 10, - quality: 90, - format: 'webp', - }); - const blurredImageResponse = await fetch(blurredImageUrl); - const blurredImageArrayBuffer = await blurredImageResponse.arrayBuffer(); - const blurredImageBase64String = btoa( - String.fromCharCode(...new Uint8Array(blurredImageArrayBuffer)), - ); - const blurDataUrl = `data:image/webp;base64,${blurredImageBase64String}`; +export async function withPlaceholderImages( + drinks: Array, +): Promise> { + return ( + await Promise.all( + drinks.map(async (drink) => { + if ( + drink.title === null || + drink.ingredients === null || + drink.calories === null || + !drink.image?.url + ) { + return null; + } - return { - ...drink, - image: { - ...drink.image, - blurDataUrl, - }, - }; - }), - ); + const blurredImageUrl = makeImageUrl({ + baseImageUrl: drink.image.url, + width: 10, + quality: 90, + format: 'webp', + }); + const blurredImageResponse = await fetch(blurredImageUrl); + const blurredImageArrayBuffer = + await blurredImageResponse.arrayBuffer(); + const blurredImageBase64String = btoa( + String.fromCharCode(...new Uint8Array(blurredImageArrayBuffer)), + ); + const blurDataUrl = `data:image/webp;base64,${blurredImageBase64String}`; + + return { + ...drink, + image: { + ...drink.image, + blurDataUrl, + }, + }; + }), + ) + ).filter((drink): drink is EnhancedDrink => drink !== null); } diff --git a/app/utils/prime-content-cache.server.ts b/app/utils/prime-content-cache.server.ts index f268fc8e..fe49f560 100644 --- a/app/utils/prime-content-cache.server.ts +++ b/app/utils/prime-content-cache.server.ts @@ -40,7 +40,7 @@ export async function primeContentCache() { allDrinksDataFnArgs, ); const allDrinksData: AllDrinksLoaderData = await allDrinksResponse.json(); - const { drinks } = allDrinksData; + const drinks = allDrinksData.drinks.filter(Boolean); // 3. Load and cache each individual drink const allSlugs = drinks.map(({ slug }) => slug);