Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 73 additions & 21 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ function readPaging(args: { limit?: number; offset?: number }) {
// Flere enn ca. 300 prikker blir uleselig og tungt å tegne.
const TASTE_MAP_MAX = 300;

// Find wine by food: viner med minst så mange vurderinger kommer først.
// Ellers havner ukjente viner med én vurdering øverst, fordi de tilfeldigvis
// passer til flest retter i kategorien.
const FOOD_POPULAR_MIN = 50;

/**
* ORDER BY for hver sortering i vinkatalogen.
* Klienten sender bare navnet (enum WineSort), og SQL-en hentes herfra,
Expand Down Expand Up @@ -223,6 +228,30 @@ const typeDefs = `#graphql
type FoodCategory {
name: String!
wineCount: Int!
# Rettene i kategorien, verdier fra Harmonize, f.eks. ["Beef", "Lamb"]
foods: [String!]!
}
# En vin på Find wine by food, med rettene i den valgte kategorien som
# vinen passer til ifølge Harmonize
type FoodWine {
id: ID!
name: String!
type: String!
abv: Float
body: String
acidity: String
winery: String!
region: String!
country: String!
avgRating: Float
ratingCount: Int!
matchedFoods: [String!]!
}
type FoodWinePage {
items: [FoodWine!]!
totalCount: Int!
}
# Sortering i vinkatalogen
Expand Down Expand Up @@ -304,11 +333,15 @@ const typeDefs = `#graphql
foodCategories: [FoodCategory!]!
# Viner som passer til en matkategori. food snevrer inn til én rett i
# kategorien. De som passer til flest retter i kategorien kommer først,
# deretter de flest har vurdert.
winesByFoodCategory(
category: String!
food: String
limit: Int = 20
offset: Int = 0
): WinePage!
): FoodWinePage!
# Viner til smakskartet. Bare viner med både fylde og syre kommer med.
tasteMap(filter: TasteFilter, limit: Int = 100): WinePage!
Expand Down Expand Up @@ -468,7 +501,10 @@ const resolvers = {
foodCategories: async () => {
const result = await pool.query(
`
SELECT f.category AS name, count(DISTINCT wf.wine_id)::int AS "wineCount"
SELECT
f.category AS name,
count(DISTINCT wf.wine_id)::int AS "wineCount",
array_agg(DISTINCT f.name ORDER BY f.name) AS foods
FROM foods f
JOIN wine_foods wf ON wf.food_id = f.id
WHERE f.category IS NOT NULL
Expand All @@ -481,46 +517,62 @@ const resolvers = {

winesByFoodCategory: async (
_: unknown,
args: { category: string; limit?: number; offset?: number },
args: {
category: string;
food?: string | null;
limit?: number;
offset?: number;
},
) => {
const { limit, offset } = readPaging(args);
const inCategory = `
EXISTS (
SELECT 1
FROM wine_foods wf
JOIN foods f ON f.id = wf.food_id
WHERE wf.wine_id = w.id AND f.category = $1
)`;

// Én rad per vin, med rettene i kategorien vinen passer til.
// Med food må vinen passe til akkurat den retten, men listen viser
// fortsatt alle rettene i kategorien den passer til.
const matches = `
SELECT wf.wine_id, array_agg(f.name ORDER BY f.name) AS foods
FROM wine_foods wf
JOIN foods f ON f.id = wf.food_id
WHERE f.category = $1
GROUP BY wf.wine_id
HAVING $2::text IS NULL OR bool_or(f.name = $2::text)`;
const params = [args.category, args.food || null];

const winesResult = await pool.query(
`
WITH matches AS (${matches})
SELECT
w.id,
w.name,
w.type,
w.abv,
w.abv::float AS abv,
w.body,
w.acidity,
wi.name AS winery,
r.name AS region,
c.name AS country,
w.avg_rating AS "avgRating",
w.rating_count AS "ratingCount"
FROM wines w
w.avg_rating::float AS "avgRating",
w.rating_count AS "ratingCount",
m.foods AS "matchedFoods"
FROM matches m
JOIN wines w ON w.id = m.wine_id
JOIN wineries wi ON w.winery_id = wi.id
JOIN regions r ON w.region_id = r.id
JOIN countries c ON w.country_code = c.code
WHERE ${inCategory}
ORDER BY w.name, w.id
LIMIT $2
OFFSET $3
ORDER BY
w.rating_count >= $3 DESC,
cardinality(m.foods) DESC,
w.rating_count DESC,
w.id
LIMIT $4
OFFSET $5
`,
[args.category, limit, offset],
[...params, FOOD_POPULAR_MIN, limit, offset],
);

const countResult = await pool.query(
`SELECT count(*)::int AS count FROM wines w WHERE ${inCategory}`,
[args.category],
`WITH matches AS (${matches}) SELECT count(*)::int AS count FROM matches`,
params,
);

return {
Expand Down
3 changes: 3 additions & 0 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Link, Route, Routes, useParams } from 'react-router';
import SiteHeader from './components/SiteHeader';
import ExploreByTaste from './components/ExploreByTaste';
import WineCatalog from './components/WineCatalog';
import FindWineByFood from './components/FindWineByFood';

/**
* NB: Temporary placeholder. The detail page is not built yet.
Expand All @@ -24,6 +25,7 @@ function WineDetailsPlaceholder() {
*
* SiteHeader is shown on every page. Routes picks the page from the URL:
* - / Explore by taste (front page)
* - /food Find wine by food
* - /wines the wine catalog
* - /wines/:id detail page (placeholder for now)
*
Expand All @@ -45,6 +47,7 @@ export default function App() {
</main>
}
/>
<Route path="/food" element={<FindWineByFood />} />
<Route path="/wines/:id" element={<WineDetailsPlaceholder />} />
</Routes>
</>
Expand Down
Binary file added client/src/assets/food/cheese.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/dessert.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/fish.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/meat.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/pasta.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/poultry.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/snacks.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/spicy.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added client/src/assets/food/vegetarian.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading