diff --git a/CLAUDE.md b/CLAUDE.md
index c630133..66b12d7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -36,10 +36,14 @@ Chosen:
- Client state: Zustand with persist middleware for the fridge (`localStorage`)
- Dataset / domain: recipes, built from two Kaggle datasets (Epicurious ratings + a
recipe/image dataset for photos), see README.md "Data"
+- GraphQL client: Apollo Client. Its normalized cache also satisfies the assignment's
+ state-management requirement for server data; Zustand (above) covers local UI state
Still open (record the decision here and in README.md when made):
-- GraphQL client, E2E tool, UI/component lib: TBD
+- E2E tool, UI/component lib: TBD
+- Server-side filtering for "My Fridge" ingredients (tracked in a follow-up issue); for now
+ it only matches within the page already loaded from the server, not the whole dataset
## Repo layout
diff --git a/README.md b/README.md
index 95db018..990e4de 100644
--- a/README.md
+++ b/README.md
@@ -13,23 +13,25 @@ Hosted on the group VM: http://it2810-13.idi.ntnu.no/project2/
| ---------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Home** (`/`) | A search field (debounced by 300 ms, or press Enter to search right away), filters for category and minimum rating, and the results as a grid of recipe cards. The result count is read out to screen readers when the filters change. |
| **Recipe detail** (`/recipes/:id`) | Photo, rating, categories, nutrition per serving, ingredients and directions. Each recipe gets its own browser tab title. |
-| **My Fridge** (`/fridge`) | Add the ingredients you have, then "Find recipes" shows the recipes that use all of them. |
+| **My Fridge** (`/fridge`) | Add the ingredients you have (typo-tolerant autocomplete), then "Find recipes" shows recipes that use any of them; extra ingredients in the fridge don't exclude a match. |
| **404** | Shown for any unknown route. |
Search terms and filters are kept in the URL (`?q=&category=&rating=&ingredients=`),
so the Back button and shared links bring you back to the same results.
**Current status:** the backend (GraphQL API over PostgreSQL with full-text
-search, filtering, sorting and paging) is working. The frontend still uses
-mock data (`frontend/src/mocks/`) and filters it in the browser
-(`frontend/src/utils/recipeFilters.ts`). Next step is to connect it to the
-API, so that search, filtering and sorting run on the server over the whole
-dataset. User-generated data (such as reviews or ratings) is not built yet.
+search, filtering, sorting and paging) is running on the VM, and the
+frontend is connected to it with Apollo Client (`frontend/src/graphql/`) -
+search, category/rating filtering, sorting and "Load more" paging all run
+on the server over the whole dataset. The exception is "My Fridge": the
+backend has no ingredient filter yet, so it only matches within the page
+already loaded (tracked as a follow-up issue). User-generated data (such as
+reviews or ratings) is not built yet.
## Architecture
```
-Browser ── React + TypeScript (Vite), React Router
+Browser ── React + TypeScript (Vite), React Router, Apollo Client
│ served by Apache at /project2 on the VM
│ GraphQL
▼
@@ -41,9 +43,10 @@ PostgreSQL 17: recipes table, tsvector full-text search, GIN/B-tree indexes
- **Frontend** (`frontend/src/`): `pages/` holds one component per route,
`components/` holds shared parts (Navbar, Footer, RecipeCard,
- RecipeFilter), `styles/` has one CSS file per component, and `utils/` has
- plain logic such as reading filters from the URL. Tests (`*.test.tsx`) sit
- next to the code they test.
+ RecipeFilter), `styles/` has one CSS file per component, `graphql/` has
+ the Apollo Client instance and queries, and `utils/` has plain logic such
+ as reading filters from the URL. Tests (`*.test.tsx`) sit next to the
+ code they test.
- **Backend** (`backend/src/`): `schema.ts` holds the GraphQL types and
resolvers and builds parameterised SQL in `buildRecipeQuery`.
`server.test.ts` tests the API against a stub database.
diff --git a/frontend/package.json b/frontend/package.json
index 2977531..447ec29 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -12,6 +12,8 @@
"typecheck": "tsc -b"
},
"dependencies": {
+ "@apollo/client": "^4.3.3",
+ "graphql": "^16.14.2",
"react": "^19.2.8",
"react-dom": "^19.2.8",
"react-router": "^8.4.0",
diff --git a/frontend/src/App.test.tsx b/frontend/src/App.test.tsx
index 01e6a8f..c41626b 100644
--- a/frontend/src/App.test.tsx
+++ b/frontend/src/App.test.tsx
@@ -1,13 +1,35 @@
import { render, screen } from '@testing-library/react'
+import { MockedProvider } from '@apollo/client/testing/react'
import { MemoryRouter } from 'react-router'
import { expect, test } from 'vitest'
import App from './App'
+import { RECIPES_QUERY } from './graphql/queries'
+
+const mocks = [
+ {
+ request: {
+ query: RECIPES_QUERY,
+ variables: {
+ search: undefined,
+ filter: { categories: undefined, minRating: undefined },
+ sort: undefined,
+ limit: 20,
+ offset: 0,
+ },
+ },
+ result: {
+ data: { recipes: { totalCount: 0, hasMore: false, items: [] } },
+ },
+ },
+]
const renderAt = (path: string) =>
render(
-
-
- ,
+
+
+
+
+ ,
)
test('renders the home page heading', () => {
diff --git a/frontend/src/components/RecipeCard.tsx b/frontend/src/components/RecipeCard.tsx
index 3669afb..92eb6e5 100644
--- a/frontend/src/components/RecipeCard.tsx
+++ b/frontend/src/components/RecipeCard.tsx
@@ -1,9 +1,9 @@
import { Link } from 'react-router'
-import type { Recipe } from '../types/recipe'
+import type { RecipeListItem } from '../graphql/queries'
import '../styles/RecipeCard.css'
type RecipeCardProps = {
- recipe: Recipe
+ recipe: RecipeListItem
}
export default function RecipeCard({ recipe }: RecipeCardProps) {
diff --git a/frontend/src/components/RecipeFilter.test.tsx b/frontend/src/components/RecipeFilter.test.tsx
index 50c9b06..6ecb41a 100644
--- a/frontend/src/components/RecipeFilter.test.tsx
+++ b/frontend/src/components/RecipeFilter.test.tsx
@@ -40,8 +40,8 @@ test('Enter searches immediately', () => {
test('selects update the URL and can be cleared', () => {
const category = screen.getByLabelText('Category')
- fireEvent.change(category, { target: { value: 'vegetarian' } })
- expect(url()).toBe('?category=vegetarian')
+ fireEvent.change(category, { target: { value: 'Vegetarian' } })
+ expect(url()).toBe('?category=Vegetarian')
fireEvent.change(category, { target: { value: '' } })
expect(url()).toBe('')
@@ -49,7 +49,7 @@ test('selects update the URL and can be cleared', () => {
test('offers beef and other common bases as category filters', () => {
const category = screen.getByLabelText('Category')
- fireEvent.change(category, { target: { value: 'beef' } })
- expect(url()).toBe('?category=beef')
+ fireEvent.change(category, { target: { value: 'Beef' } })
+ expect(url()).toBe('?category=Beef')
expect(screen.getByRole('option', { name: 'Lamb' })).toBeInTheDocument()
})
diff --git a/frontend/src/components/RecipeFilter.tsx b/frontend/src/components/RecipeFilter.tsx
index 7e0e894..5c58742 100644
--- a/frontend/src/components/RecipeFilter.tsx
+++ b/frontend/src/components/RecipeFilter.tsx
@@ -69,21 +69,22 @@ export default function RecipeFilter() {
onChange={(event) => setParam('category', event.target.value)}
>
-
- {/* Common bases for a dish. Values match dataset category names
- case-insensitively (see filterRecipes / buildRecipeQuery). */}
-
-
-
-
-
-
-
-
-
-
-
-
+
+ {/* Common bases for a dish. Values must match the dataset's
+ category strings exactly: the server does an array-contains
+ match (buildRecipeQuery), not a case-insensitive search. */}
+
+
+
+
+
+
+
+
+
+
+
+