Skip to content

Commit cfa9e77

Browse files
committed
Replace the categories page API with an Astro Action.
1 parent f3debe4 commit cfa9e77

File tree

4 files changed

+122
-103
lines changed

4 files changed

+122
-103
lines changed

webserver/src/actions/index.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import cookingHistory from './cooking-history';
1+
import cookingHistory from "./cooking-history";
2+
import { recipesInCategory } from "./recipes-in-category";
23
import recipesInMonth from "./recipes-in-month";
34

45
export const server = {
56
cookingHistory,
67
recipesInMonth,
8+
recipesInCategory
79
};
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { getLogin } from "@lib/login-cookie";
2+
import { prisma } from "@lib/prisma";
3+
import type { CookingHistory, Prisma } from "@prisma/client";
4+
import { defineAction, type ActionAPIContext } from "astro:actions";
5+
import { z } from "astro:schema";
6+
7+
export type RecipesInCategoryResponse = {
8+
name: string;
9+
slug: string;
10+
author: {
11+
username: string;
12+
};
13+
cookingHistory: CookingHistory[] | undefined;
14+
}[];
15+
16+
const input = z.object({
17+
categoryIds: z.array(z.number().int()),
18+
author: z.string().optional(),
19+
});
20+
type Input = z.infer<typeof input>;
21+
22+
async function handler(
23+
{ categoryIds, author }: Input,
24+
{ cookies }: ActionAPIContext
25+
): Promise<RecipesInCategoryResponse> {
26+
const activeUser = await getLogin(cookies);
27+
28+
const where: Prisma.RecipeWhereInput = {
29+
categories: {
30+
some: {
31+
id: { in: [...categoryIds] },
32+
},
33+
},
34+
};
35+
if (author !== null) {
36+
where.author = { username: author };
37+
}
38+
39+
return await prisma.recipe.findMany({
40+
where,
41+
select: {
42+
name: true,
43+
slug: true,
44+
author: { select: { username: true } },
45+
cookingHistory:
46+
activeUser === null ? undefined : { where: { cookId: activeUser.id } },
47+
},
48+
orderBy: { name: "asc" },
49+
});
50+
}
51+
52+
export const recipesInCategory = defineAction({ input, handler });
Lines changed: 67 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import { escapeRegExp, filterListWithInitialMatchesFirst } from '@lib/util';
2-
import type { Category } from '@prisma/client';
1+
import { escapeRegExp, filterListWithInitialMatchesFirst } from "@lib/util";
2+
import type { Category } from "@prisma/client";
3+
import { actions } from "astro:actions";
34
import {
45
type Accessor,
56
type Component,
@@ -12,56 +13,47 @@ import {
1213
type Setter,
1314
Suspense,
1415
} from "solid-js";
15-
import { OneRecipe, type RecipeTitleWithLinkFields } from './OneRecipe';
16-
import { QueryDrivenTextField } from './QueryDrivenTextField';
17-
18-
async function fetchRecipesForCategory(categoryId: number, username: string | undefined) {
19-
const searchParams = new URLSearchParams();
20-
searchParams.set("id", `${categoryId}`);
21-
if (username) {
22-
searchParams.set("user", username);
23-
}
24-
let response = await fetch(`/api/recipes-in-category?${searchParams.toString()}`);
25-
let recipes = await response.json() as {
26-
name: string;
27-
slug: string;
28-
author: {
29-
username: string;
30-
};
31-
}[];
32-
return recipes;
33-
}
16+
import { OneRecipe, type RecipeTitleWithLinkFields } from "./OneRecipe";
17+
import { QueryDrivenTextField } from "./QueryDrivenTextField";
3418

3519
export const CategoryList: Component<{
3620
categories: Category[];
37-
username?: string;
3821
initialQuery: string;
39-
}> = props => {
22+
}> = (props) => {
4023
// Keeping the search filter updated:
41-
const [filter, setFilter] = createSignal(new URLSearchParams(props.initialQuery).get("filter") ?? "");
24+
const [filter, setFilter] = createSignal(
25+
new URLSearchParams(props.initialQuery).get("filter") ?? ""
26+
);
4227

4328
// Search for categories:
4429

45-
const filterRE = createMemo(() => new RegExp(escapeRegExp(filter()), 'i'));
30+
const filterRE = createMemo(() => new RegExp(escapeRegExp(filter()), "i"));
4631

4732
type PendingRecipeList = {
4833
needRecipes: Accessor<boolean>;
4934
setNeedRecipes: Setter<boolean>;
5035
recipes: Resource<RecipeTitleWithLinkFields[]>;
51-
}
36+
};
5237

5338
const recipesByCategory = createMemo(() => {
54-
const username = props.username;
55-
return new Map<number, PendingRecipeList>(props.categories.map(category => {
56-
const [needRecipes, setNeedRecipes] = createSignal(false);
57-
const [recipes] = createResource(needRecipes, () => fetchRecipesForCategory(category.id, username));
58-
return [category.id, { needRecipes, setNeedRecipes, recipes }];
59-
}));
39+
return new Map<number, PendingRecipeList>(
40+
props.categories.map((category) => {
41+
const [needRecipes, setNeedRecipes] = createSignal(false);
42+
const [recipes] = createResource(needRecipes, () =>
43+
actions.recipesInCategory.orThrow({ categoryIds: [category.id] })
44+
);
45+
return [category.id, { needRecipes, setNeedRecipes, recipes }];
46+
})
47+
);
6048
});
6149

62-
6350
const filteredCategories = createMemo(() =>
64-
filterListWithInitialMatchesFirst(props.categories, filterRE(), c => c.name));
51+
filterListWithInitialMatchesFirst(
52+
props.categories,
53+
filterRE(),
54+
(c) => c.name
55+
)
56+
);
6557

6658
function onToggleCategory(categoryId: number) {
6759
recipesByCategory().get(categoryId)?.setNeedRecipes(true);
@@ -75,27 +67,44 @@ export const CategoryList: Component<{
7567
}
7668
});
7769

78-
return <>
79-
<QueryDrivenTextField queryParam='filter' value={filter()} onInput={setFilter}>Filter</QueryDrivenTextField>
80-
<section id="categories">
81-
<ul class="details">
82-
<For each={filteredCategories()}>{category =>
83-
<li>
84-
<details
85-
open={filteredCategories().length === 1}
86-
onToggle={[onToggleCategory, category.id]}>
87-
<summary>{category.name}</summary>
88-
<Suspense fallback={<p>Loading...</p>}>
89-
<ul>
90-
<For each={recipesByCategory().get(category.id)?.recipes()}>{recipe =>
91-
<li><OneRecipe recipe={recipe} /></li>
92-
}</For>
93-
</ul>
94-
</Suspense>
95-
</details>
96-
</li>
97-
}</For>
98-
</ul>
99-
</section>
100-
</>;
101-
}
70+
return (
71+
<>
72+
<QueryDrivenTextField
73+
queryParam="filter"
74+
value={filter()}
75+
onInput={setFilter}
76+
>
77+
Filter
78+
</QueryDrivenTextField>
79+
<section id="categories">
80+
<ul class="details">
81+
<For each={filteredCategories()}>
82+
{(category) => (
83+
<li>
84+
<details
85+
open={filteredCategories().length === 1}
86+
onToggle={[onToggleCategory, category.id]}
87+
>
88+
<summary>{category.name}</summary>
89+
<Suspense fallback={<p>Loading...</p>}>
90+
<ul>
91+
<For
92+
each={recipesByCategory().get(category.id)?.recipes()}
93+
>
94+
{(recipe) => (
95+
<li>
96+
<OneRecipe recipe={recipe} />
97+
</li>
98+
)}
99+
</For>
100+
</ul>
101+
</Suspense>
102+
</details>
103+
</li>
104+
)}
105+
</For>
106+
</ul>
107+
</section>
108+
</>
109+
);
110+
};

webserver/src/pages/api/recipes-in-category.ts

Lines changed: 0 additions & 44 deletions
This file was deleted.

0 commit comments

Comments
 (0)