Skip to content

Commit 1866edb

Browse files
authored
Add tooltips when pointing at an ingredient in the instructions.
The tooltips repeat the quantity and preparation from the main ingredient list.
1 parent 5264967 commit 1866edb

File tree

9 files changed

+133
-11
lines changed

9 files changed

+133
-11
lines changed

webserver/e2e/recipe.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,26 @@ test("Shows author's name", async ({ page, testUser, testRecipe, testLogin }) =>
7575
await expect(page).toHaveURL(/.*\/r\/recipeauthor$/);
7676
await expect(page).toHaveTitle("User Name's Recipes");
7777
});
78+
79+
test('Shows ingredient tooltips', async ({ page, testUser, testRecipe }) => {
80+
const user = await testUser.create({ username: 'testuser', name: "User Name" });
81+
await testRecipe.create({
82+
author: { connect: { id: user.id } },
83+
name: "Test Recipe",
84+
slug: "test-recipe",
85+
ingredients: {
86+
create: [
87+
{ order: 0, amount: "1", unit: "cup", name: "flour", preparation: "sifted" },
88+
{ order: 2, amount: "3", name: "Funny ingredient'name" },
89+
]
90+
},
91+
steps: ["1. Add the flour.\n2. Add some Funny ingredient'name."],
92+
})
93+
await page.goto('/r/testuser/test-recipe');
94+
95+
await page.getByText("Add the flour").locator('span[tabindex]').hover();
96+
await expect.soft(page.getByRole('tooltip')).toHaveText("1 cup flour, sifted");
97+
98+
await page.getByText("Add some Funny").locator('span[tabindex]').hover();
99+
await expect.soft(page.getByRole('tooltip')).toHaveText("3 Funny ingredient'name");
100+
});

webserver/package.json

+2
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
"dset": "^3.1.3",
3838
"express": "^4.18.2",
3939
"google-auth-library": "^9.6.2",
40+
"lit": "^3.1.1",
41+
"mdast-util-find-and-replace": "^3.0.1",
4042
"n3": "^1.17.2",
4143
"prisma": "^5.9.1",
4244
"rdf-dereference": "^2.2.0",

webserver/pnpm-lock.yaml

+6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webserver/src/components/Markdown.astro

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,15 @@ import { md } from "@lib/markdown";
33
44
type Props = {
55
source: string;
6+
ingredientNames?: string[];
67
};
78
8-
const { source } = Astro.props;
9-
const processed = await md.process(source);
9+
const { source, ingredientNames } = Astro.props;
10+
const processed = md().data({ ingredientNames }).processSync(source);
1011
---
1112

13+
<script>
14+
import "@components/recipe-ingredient";
15+
</script>
16+
1217
<Fragment set:html={String(processed)} />
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import slugify from '@lib/slugify';
2+
import '@shoelace-style/shoelace/dist/components/tooltip/tooltip.js';
3+
import { LitElement, css, html } from 'lit';
4+
import { customElement, property, state } from 'lit/decorators.js';
5+
6+
@customElement('recipe-ingredient')
7+
export class RecipeIngredient extends LitElement {
8+
static override styles = css`
9+
span {
10+
background-color: var(--yellow-highlight);
11+
outline: solid thin var(--yellow-outline);
12+
border-radius: .5ex;
13+
}
14+
span:focus-visible {
15+
outline: solid var(--yellow-outline-focused);
16+
}
17+
`;
18+
19+
@property({ attribute: 'ingredient-id' })
20+
ingredientId: string | undefined = undefined;
21+
22+
@state()
23+
private fullIngredient: string = "";
24+
25+
override connectedCallback() {
26+
super.connectedCallback()
27+
if (this.ingredientId === undefined && this.textContent) {
28+
this.ingredientId = slugify(this.textContent);
29+
}
30+
this.fullIngredient = document.getElementById(this.ingredientId ?? "")?.innerText ?? "";
31+
}
32+
33+
override render() {
34+
if (this.fullIngredient === "") {
35+
return html`<slot></slot>`;
36+
} else {
37+
return html`<sl-tooltip content=${this.fullIngredient}><span tabindex=0><slot></slot></span></sl-tooltip>`;
38+
}
39+
}
40+
}

webserver/src/lib/markdown.tsx

+34-3
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,44 @@
11

2-
import rehypeSanitize from 'rehype-sanitize';
2+
import type { PhrasingContent, Root } from 'mdast';
3+
import { findAndReplace } from 'mdast-util-find-and-replace';
4+
import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
35
import rehypeStringify from 'rehype-stringify';
46
import remarkParse from 'remark-parse';
57
import remarkRehype from 'remark-rehype';
6-
import { unified } from 'unified';
8+
import { unified, type Plugin } from 'unified';
9+
10+
const wrapIngredients: Plugin<[], Root> = function () {
11+
function replace(value: string): PhrasingContent {
12+
return {
13+
type: 'text',
14+
value,
15+
data: {
16+
hName: 'recipe-ingredient',
17+
hChildren: [{ type: 'text', value }],
18+
}
19+
};
20+
}
21+
return (tree, _file) => {
22+
const ingredientNames = this.data('ingredientNames');
23+
if (ingredientNames) {
24+
findAndReplace(tree, ingredientNames.map(ingredient => [ingredient, replace]));
25+
}
26+
};
27+
};
28+
29+
declare module 'unified' {
30+
interface Data {
31+
ingredientNames?: Array<string> | undefined
32+
}
33+
}
734

835
export const md = unified()
936
.use(remarkParse)
37+
.use(wrapIngredients)
1038
.use(remarkRehype)
11-
.use(rehypeSanitize)
39+
.use(rehypeSanitize, {
40+
...defaultSchema,
41+
tagNames: [...defaultSchema.tagNames ?? [], 'recipe-ingredient'],
42+
})
1243
.use(rehypeStringify)
1344
.freeze();

webserver/src/pages/r/[username]/[slug].astro

+14-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import RenderSource from "@components/RenderSource.astro";
55
import Layout from "@layouts/Layout.astro";
66
import { getLogin } from "@lib/login-cookie";
77
import { prisma } from "@lib/prisma";
8+
import slugify from "@lib/slugify";
89
import type {
910
Category,
1011
Recipe,
@@ -51,14 +52,20 @@ if (slug && username) {
5152
});
5253
}
5354
54-
RenderSource == RenderSource;
55+
const ingredientNames = recipe?.ingredients.map(
56+
(ingredient) => ingredient.name
57+
);
5558
---
5659

5760
<script>
5861
import "@github/relative-time-element";
5962
</script>
6063

61-
<Layout title={recipe?.name ?? "No such recipe"} user={activeUser} showScreenLock>
64+
<Layout
65+
title={recipe?.name ?? "No such recipe"}
66+
user={activeUser}
67+
showScreenLock
68+
>
6269
{
6370
recipe ? (
6471
<div class="recipe" itemscope itemtype="https://schema.org/Recipe">
@@ -88,6 +95,7 @@ RenderSource == RenderSource;
8895
itemprop="recipeIngredient"
8996
itemscope
9097
itemtype="https://schema.org/HowToSupply"
98+
id={slugify(ingredient.name)}
9199
>
92100
{ingredient.amount ? (
93101
<span
@@ -118,13 +126,13 @@ RenderSource == RenderSource;
118126
<h3>Instructions</h3>
119127
{recipe.steps.length === 1 ? (
120128
<div itemprop="recipeInstructions">
121-
<Markdown source={recipe.steps[0]!} />
129+
<Markdown source={recipe.steps[0]!} {ingredientNames} />
122130
</div>
123131
) : (
124132
<ol>
125133
{recipe.steps.map((step) => (
126134
<li itemprop="recipeInstructions">
127-
<Markdown source={step} />
135+
<Markdown source={step} {ingredientNames} />
128136
</li>
129137
))}
130138
</ol>
@@ -139,7 +147,7 @@ RenderSource == RenderSource;
139147
<li itemprop="recipeCategory">
140148
<a
141149
href={`/search?category=${encodeURIComponent(
142-
category.name.replaceAll(" ", "_"),
150+
category.name.replaceAll(" ", "_")
143151
)}`}
144152
>
145153
{category.name}
@@ -164,7 +172,7 @@ RenderSource == RenderSource;
164172
/>
165173
) : (
166174
<RecipeNote note={note} />
167-
),
175+
)
168176
)}
169177
</ol>
170178
{activeUser ? (

webserver/src/style/main.css

+5
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
@import "@shoelace-style/shoelace/dist/themes/light.css";
2+
13
/*
24
35
header
@@ -11,6 +13,9 @@ container (flexbox)
1113
:root {
1214
--yellow: oklch(76% 0.136 79);
1315
--yellow-bg: oklch(95% 0.02 79);
16+
--yellow-highlight: oklch(97.5% 0.01 79);
17+
--yellow-outline: oklch(76% 0.136 79 / 0.25);
18+
--yellow-outline-focused: oklch(76% 0.136 79 / 0.5);
1419
--navy: oklch(29% 0.033 259);
1520
--light-navy: oklch(36% 0.054 259);
1621
}

webserver/tsconfig.json

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
"@style/*": ["src/style/*"]
1111
},
1212
"exactOptionalPropertyTypes": false,
13+
"experimentalDecorators": true,
14+
"useDefineForClassFields": false,
1315
"jsx": "preserve",
1416
"jsxImportSource": "solid-js"
1517
}

0 commit comments

Comments
 (0)