@bearmenu/ui 0.8.9 → 0.8.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-24RFI4RQ.js +167 -0
- package/dist/{chunk-UKBNQO6Q.js → chunk-2CXU5WZO.js} +1 -1
- package/dist/chunk-2XCZJWTV.js +121 -0
- package/dist/{chunk-62ES6N32.js → chunk-43RC7MHY.js} +1 -1
- package/dist/chunk-5RWTOT5Y.js +72 -0
- package/dist/{chunk-Q6QH52LJ.js → chunk-6TEEY5YA.js} +2 -2
- package/dist/{chunk-AXV5O2IZ.js → chunk-AKQ57NXY.js} +3 -3
- package/dist/chunk-CLLY6XB3.js +41 -0
- package/dist/chunk-DTXNDLSE.js +125 -0
- package/dist/chunk-F6FMZJDG.js +113 -0
- package/dist/{chunk-2WDJR6K7.js → chunk-G7IFL4Z4.js} +4 -1
- package/dist/chunk-H3ZZDX5L.js +61 -0
- package/dist/{chunk-H3BV4VAG.js → chunk-JS3EINZD.js} +2 -2
- package/dist/chunk-MZL7CGCL.js +109 -0
- package/dist/{chunk-TH6W2PWG.js → chunk-UDTG3RSE.js} +1 -1
- package/dist/chunk-WCTHZKU7.js +37 -0
- package/dist/{chunk-TT3WFS6S.js → chunk-YOKHBAIH.js} +11 -11
- package/dist/components/alert-dialog.js +1 -1
- package/dist/components/alert.d.ts +2 -2
- package/dist/components/avatar.d.ts +3 -3
- package/dist/components/badge.d.ts +1 -1
- package/dist/components/badge.js +1 -1
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +1 -1
- package/dist/components/calendar.js +1 -1
- package/dist/components/card.d.ts +4 -4
- package/dist/components/carousel.js +1 -1
- package/dist/components/combobox.js +1 -1
- package/dist/components/command.d.ts +2 -2
- package/dist/components/concept-compare-rail.d.ts +50 -0
- package/dist/components/concept-compare-rail.js +5 -0
- package/dist/components/concept-compare.d.ts +75 -38
- package/dist/components/concept-compare.js +209 -133
- package/dist/components/concept-convergence.d.ts +41 -0
- package/dist/components/concept-convergence.js +53 -0
- package/dist/components/convergence.d.ts +25 -0
- package/dist/components/convergence.js +4 -0
- package/dist/components/dialog.d.ts +2 -2
- package/dist/components/empty-state.js +1 -1
- package/dist/components/event-card.js +1 -1
- package/dist/components/event-compact-row.js +1 -1
- package/dist/components/event-detail-panel.js +2 -2
- package/dist/components/event-index-row.js +1 -1
- package/dist/components/event-list-row-wide.js +2 -2
- package/dist/components/event-list-row.js +2 -2
- package/dist/components/explainer-steps.d.ts +34 -0
- package/dist/components/explainer-steps.js +36 -0
- package/dist/components/filter-category-row.js +1 -1
- package/dist/components/glass-ground.d.ts +61 -0
- package/dist/components/glass-ground.js +3 -0
- package/dist/components/haptic-button.js +1 -1
- package/dist/components/hub-surface.js +11 -1
- package/dist/components/hue-chip.d.ts +70 -0
- package/dist/components/hue-chip.js +3 -0
- package/dist/components/input-group.d.ts +2 -2
- package/dist/components/input.d.ts +1 -1
- package/dist/components/item.d.ts +4 -4
- package/dist/components/kbd.d.ts +2 -2
- package/dist/components/kitchen-dossier.d.ts +88 -0
- package/dist/components/kitchen-dossier.js +186 -0
- package/dist/components/link-button.js +1 -1
- package/dist/components/menu-dead-end.d.ts +56 -0
- package/dist/components/menu-dead-end.js +49 -0
- package/dist/components/menu-family-block.d.ts +98 -0
- package/dist/components/menu-family-block.js +153 -0
- package/dist/components/menu-family-card.d.ts +33 -27
- package/dist/components/menu-family-card.js +41 -85
- package/dist/components/menu-field.js +92 -2
- package/dist/components/menu-filter-chips.d.ts +86 -0
- package/dist/components/menu-filter-chips.js +122 -0
- package/dist/components/menu-group-rows.d.ts +53 -0
- package/dist/components/menu-group-rows.js +67 -0
- package/dist/components/menu-hub-hero.js +10 -5
- package/dist/components/menu-index-header.d.ts +37 -0
- package/dist/components/menu-index-header.js +68 -0
- package/dist/components/menu-index-list.d.ts +48 -0
- package/dist/components/menu-index-list.js +96 -0
- package/dist/components/menu-intent-hero.d.ts +69 -0
- package/dist/components/menu-intent-hero.js +87 -0
- package/dist/components/menu-item.js +3 -3
- package/dist/components/menu-kitchen-list.d.ts +82 -0
- package/dist/components/menu-kitchen-list.js +5 -0
- package/dist/components/menu-mocks.d.ts +37 -2
- package/dist/components/menu-mocks.js +152 -27
- package/dist/components/menu-types.d.ts +182 -6
- package/dist/components/motion-icon-controls.js +2 -2
- package/dist/components/motion-icon-set.js +6 -6
- package/dist/components/motion-icon.d.ts +22 -1
- package/dist/components/motion-icon.js +1 -1
- package/dist/components/multi-select-combobox.js +2 -2
- package/dist/components/native-select.d.ts +2 -2
- package/dist/components/pagination.js +1 -1
- package/dist/components/photo-ground.d.ts +64 -0
- package/dist/components/photo-ground.js +3 -0
- package/dist/components/place-about-tab.js +2 -2
- package/dist/components/place-details-mobile.js +3 -3
- package/dist/components/place-types.d.ts +2 -2
- package/dist/components/qa-block.d.ts +36 -0
- package/dist/components/qa-block.js +24 -0
- package/dist/components/scroll-fade.js +3 -121
- package/dist/components/searchable-select.js +1 -1
- package/dist/components/select.d.ts +1 -1
- package/dist/components/signature-chip.d.ts +2 -2
- package/dist/components/signature-chip.js +2 -2
- package/dist/components/sliding-panels.js +1 -1
- package/dist/components/snap-rail.d.ts +26 -3
- package/dist/components/snap-rail.js +3 -109
- package/dist/components/spinner.d.ts +4 -4
- package/dist/components/story.d.ts +34 -0
- package/dist/components/story.js +3 -0
- package/dist/components/tag.d.ts +4 -4
- package/dist/components/text.d.ts +5 -5
- package/dist/components/toggle.d.ts +1 -1
- package/dist/components/top-comparison.d.ts +28 -0
- package/dist/components/top-comparison.js +76 -0
- package/dist/components/top-cross-link.d.ts +34 -0
- package/dist/components/top-cross-link.js +101 -0
- package/dist/components/top-ranking-entry.d.ts +53 -0
- package/dist/components/top-ranking-entry.js +217 -0
- package/dist/components/top-ranking-hero.d.ts +61 -0
- package/dist/components/top-ranking-hero.js +99 -0
- package/dist/components/top-ranking-legend.d.ts +69 -0
- package/dist/components/top-ranking-legend.js +124 -0
- package/dist/components/top-types.d.ts +109 -0
- package/dist/components/top-types.js +1 -0
- package/dist/components/venue-grade-dimensions.d.ts +3 -1
- package/dist/components/venue-grade-dimensions.js +1 -1
- package/dist/components/venue-quotes.d.ts +1 -1
- package/dist/components/venue-quotes.js +1 -1
- package/dist/components/venue-wall.js +5 -3
- package/dist/components/weekday-theme-banner.js +1 -1
- package/dist/components/weekend-panel.js +2 -2
- package/package.json +1 -1
- package/src/components/badge.tsx +1 -1
- package/src/components/button.tsx +2 -2
- package/src/components/concept-compare-rail.tsx +132 -0
- package/src/components/concept-compare.stories.tsx +122 -54
- package/src/components/concept-compare.tsx +368 -196
- package/src/components/concept-convergence.stories.tsx +53 -0
- package/src/components/concept-convergence.tsx +90 -0
- package/src/components/convergence.stories.tsx +54 -0
- package/src/components/convergence.tsx +177 -0
- package/src/components/explainer-steps.stories.tsx +46 -0
- package/src/components/explainer-steps.tsx +74 -0
- package/src/components/glass-ground.tsx +104 -0
- package/src/components/hub-surface.tsx +11 -1
- package/src/components/hue-chip.stories.tsx +76 -0
- package/src/components/hue-chip.test.tsx +62 -0
- package/src/components/hue-chip.tsx +99 -0
- package/src/components/kitchen-dossier.stories.tsx +108 -0
- package/src/components/kitchen-dossier.tsx +321 -0
- package/src/components/menu-dead-end.stories.tsx +67 -0
- package/src/components/menu-dead-end.tsx +127 -0
- package/src/components/menu-family-block.stories.tsx +91 -0
- package/src/components/menu-family-block.tsx +303 -0
- package/src/components/menu-family-card.stories.tsx +43 -35
- package/src/components/menu-family-card.tsx +115 -142
- package/src/components/menu-filter-chips.stories.tsx +107 -0
- package/src/components/menu-filter-chips.tsx +238 -0
- package/src/components/menu-group-rows.stories.tsx +54 -0
- package/src/components/menu-group-rows.tsx +133 -0
- package/src/components/menu-hub-hero.test.tsx +39 -0
- package/src/components/menu-hub-hero.tsx +17 -5
- package/src/components/menu-index-header.stories.tsx +72 -0
- package/src/components/menu-index-header.tsx +125 -0
- package/src/components/menu-index-list.stories.tsx +50 -0
- package/src/components/menu-index-list.tsx +149 -0
- package/src/components/menu-intent-hero.stories.tsx +67 -0
- package/src/components/menu-intent-hero.tsx +189 -0
- package/src/components/menu-kitchen-list.stories.tsx +67 -0
- package/src/components/menu-kitchen-list.test.tsx +78 -0
- package/src/components/menu-kitchen-list.tsx +284 -0
- package/src/components/menu-mocks.ts +198 -23
- package/src/components/menu-types.ts +189 -6
- package/src/components/motion-icon-controls.tsx +11 -11
- package/src/components/motion-icon-set.tsx +6 -5
- package/src/components/motion-icon.test.tsx +23 -1
- package/src/components/motion-icon.tsx +20 -0
- package/src/components/photo-ground.tsx +147 -0
- package/src/components/place-types.ts +2 -2
- package/src/components/qa-block.stories.tsx +43 -0
- package/src/components/qa-block.tsx +60 -0
- package/src/components/signature-chip.tsx +3 -3
- package/src/components/snap-rail.tsx +58 -15
- package/src/components/story.tsx +225 -0
- package/src/components/top-comparison.stories.tsx +78 -0
- package/src/components/top-comparison.tsx +151 -0
- package/src/components/top-cross-link.stories.tsx +84 -0
- package/src/components/top-cross-link.tsx +171 -0
- package/src/components/top-ranking-entry.stories.tsx +171 -0
- package/src/components/top-ranking-entry.test.tsx +105 -0
- package/src/components/top-ranking-entry.tsx +425 -0
- package/src/components/top-ranking-hero.stories.tsx +76 -0
- package/src/components/top-ranking-hero.tsx +218 -0
- package/src/components/top-ranking-legend.stories.tsx +78 -0
- package/src/components/top-ranking-legend.tsx +239 -0
- package/src/components/top-types.ts +110 -0
- package/src/components/venue-grade-dimensions.tsx +2 -2
- package/src/components/venue-quotes.stories.tsx +1 -1
- package/src/components/venue-quotes.tsx +4 -2
- package/src/components/venue-wall.tsx +9 -4
- package/src/components/weekday-theme-banner.tsx +1 -1
- package/dist/chunk-THOVPZNN.js +0 -37
- package/dist/chunk-YS6BHKZO.js +0 -92
- package/dist/components/ranked-venue-list.d.ts +0 -49
- package/dist/components/ranked-venue-list.js +0 -87
- package/dist/components/signature-line-list.d.ts +0 -45
- package/dist/components/signature-line-list.js +0 -99
- package/dist/components/venue-busyness-strip.d.ts +0 -35
- package/dist/components/venue-busyness-strip.js +0 -37
- package/dist/components/venue-signature-block.d.ts +0 -39
- package/dist/components/venue-signature-block.js +0 -6
- package/dist/components/venue-spotlight.d.ts +0 -74
- package/dist/components/venue-spotlight.js +0 -138
- package/src/components/ranked-venue-list.stories.tsx +0 -80
- package/src/components/ranked-venue-list.tsx +0 -177
- package/src/components/signature-line-list.stories.tsx +0 -85
- package/src/components/signature-line-list.tsx +0 -136
- package/src/components/venue-busyness-strip.stories.tsx +0 -66
- package/src/components/venue-busyness-strip.tsx +0 -64
- package/src/components/venue-signature-block.stories.tsx +0 -68
- package/src/components/venue-signature-block.tsx +0 -71
- package/src/components/venue-spotlight.stories.tsx +0 -141
- package/src/components/venue-spotlight.tsx +0 -236
- package/dist/{chunk-XWMRABZW.js → chunk-HSIYYA2E.js} +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { render, screen } from "@testing-library/react";
|
|
3
|
+
import { MenuKitchenList, type MenuKitchenListLabels } from "./menu-kitchen-list";
|
|
4
|
+
import type { MenuKitchenPreview } from "./menu-types";
|
|
5
|
+
|
|
6
|
+
const labels: MenuKitchenListLabels = {
|
|
7
|
+
openVenue: (name) => `Open ${name}`,
|
|
8
|
+
openDish: (name) => `See ${name}`,
|
|
9
|
+
openNow: "Open now",
|
|
10
|
+
showMore: "Show 10 more",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const kitchen: MenuKitchenPreview = {
|
|
14
|
+
place: { id: "p1", slug: "salt-and-stone", name: "Salt & Stone", open_now: true },
|
|
15
|
+
href: "/places/salt-and-stone",
|
|
16
|
+
metaLabel: "courtyard · Centru",
|
|
17
|
+
hoursLabel: "kitchen until 23:00",
|
|
18
|
+
priceLabel: "62 lei",
|
|
19
|
+
dish: {
|
|
20
|
+
name: "Truffle Tagliatelle",
|
|
21
|
+
href: "/menu/dishes/truffle-tagliatelle",
|
|
22
|
+
kicker: "MOST NAMED HERE",
|
|
23
|
+
meta: "17 reviews name it",
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
describe("MenuKitchenList", () => {
|
|
28
|
+
/**
|
|
29
|
+
* The reason this component exists rather than a flag on `RankedVenueList`.
|
|
30
|
+
* If these ever collapse into one link the surface has silently lost the
|
|
31
|
+
* drawer, which is the second half of what the row is for.
|
|
32
|
+
*/
|
|
33
|
+
it("gives the row and the dish separate destinations", () => {
|
|
34
|
+
render(<MenuKitchenList kitchens={[kitchen]} labels={labels} />);
|
|
35
|
+
|
|
36
|
+
const venue = screen.getByRole("link", { name: "Open Salt & Stone" });
|
|
37
|
+
const dish = screen.getByRole("link", { name: "See Truffle Tagliatelle" });
|
|
38
|
+
|
|
39
|
+
expect(venue).toHaveAttribute("href", "/places/salt-and-stone");
|
|
40
|
+
expect(dish).toHaveAttribute("href", "/menu/dishes/truffle-tagliatelle");
|
|
41
|
+
// Anchors must be siblings, never nested — nesting is invalid HTML and
|
|
42
|
+
// breaks middle-click on both targets.
|
|
43
|
+
expect(venue.contains(dish)).toBe(false);
|
|
44
|
+
expect(dish.contains(venue)).toBe(false);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `must_try` arrives from a list endpoint, so roughly a third of entries have
|
|
49
|
+
* no `slug`. Those must render as content, not as a link to nowhere.
|
|
50
|
+
*/
|
|
51
|
+
it("renders an inert dish line when the must-try has no menu row behind it", () => {
|
|
52
|
+
const noHref = { ...kitchen, dish: { name: "Risotto ai Funghi", kicker: "MOST NAMED HERE" } };
|
|
53
|
+
render(<MenuKitchenList kitchens={[noHref]} labels={labels} />);
|
|
54
|
+
|
|
55
|
+
expect(screen.getByText("Risotto ai Funghi")).toBeInTheDocument();
|
|
56
|
+
expect(screen.queryByRole("link", { name: /^See / })).not.toBeInTheDocument();
|
|
57
|
+
expect(screen.getByRole("link", { name: "Open Salt & Stone" })).toBeInTheDocument();
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("still reads as a venue with no dish at all", () => {
|
|
61
|
+
const bare: MenuKitchenPreview = {
|
|
62
|
+
place: { id: "p2", slug: "green-room", name: "Green Room" },
|
|
63
|
+
href: "/places/green-room",
|
|
64
|
+
};
|
|
65
|
+
render(<MenuKitchenList kitchens={[bare]} labels={labels} />);
|
|
66
|
+
|
|
67
|
+
expect(screen.getByText("Green Room")).toBeInTheDocument();
|
|
68
|
+
expect(screen.getByRole("link", { name: "Open Green Room" })).toBeInTheDocument();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("omits the show-more control until the caller wires one", () => {
|
|
72
|
+
const { rerender } = render(<MenuKitchenList kitchens={[kitchen]} labels={labels} />);
|
|
73
|
+
expect(screen.queryByRole("button", { name: "Show 10 more" })).not.toBeInTheDocument();
|
|
74
|
+
|
|
75
|
+
rerender(<MenuKitchenList kitchens={[kitchen]} labels={labels} onShowMore={() => {}} />);
|
|
76
|
+
expect(screen.getByRole("button", { name: "Show 10 more" })).toBeInTheDocument();
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
import { Check, ChevronRight, Clock, MessageSquare, Star } from "lucide-react";
|
|
2
|
+
import { cn } from "../lib/utils";
|
|
3
|
+
import { Separator } from "./separator";
|
|
4
|
+
import { Skeleton } from "./skeleton";
|
|
5
|
+
import type { MenuKitchenPreview, MenuLinkComponent, MenuLinkProps } from "./menu-types";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MenuKitchenList — the intent page's spine.
|
|
9
|
+
*
|
|
10
|
+
* Venues ranked by how often reviews name their dishes, each row carrying the
|
|
11
|
+
* dish that earned it its place. Not a grid of dishes: a shortlist of rooms you
|
|
12
|
+
* could walk into.
|
|
13
|
+
*
|
|
14
|
+
* ## Two targets, and why this is not `<RankedVenueList>`
|
|
15
|
+
*
|
|
16
|
+
* The row goes to the venue. The dish name opens the item drawer — the same
|
|
17
|
+
* drawer the dish tiles open, listing every kitchen that makes it and for how
|
|
18
|
+
* much. Those are two different destinations inside one row, and the hub's
|
|
19
|
+
* `<RankedVenueList>` wraps its ENTIRE row in a single `LinkComponent`, so it
|
|
20
|
+
* structurally cannot express a second one. That is the reason this exists
|
|
21
|
+
* rather than a variant flag on that component.
|
|
22
|
+
*
|
|
23
|
+
* The mechanism is a stretched overlay, as in `<MenuItem>`: the venue link is
|
|
24
|
+
* an absolutely-positioned sibling at `z-[1]`, and the dish link sits above it
|
|
25
|
+
* at `z-[2]`. Nesting one anchor inside another is invalid HTML and breaks
|
|
26
|
+
* middle-click on both.
|
|
27
|
+
*
|
|
28
|
+
* The dish name is underlined so it reads as a target before it is pressed. The
|
|
29
|
+
* chevron belongs to the venue and stays on the right, away from it — an
|
|
30
|
+
* unmarked second target inside a row that already navigates is a trap.
|
|
31
|
+
*
|
|
32
|
+
* ## The filtered state changes the row's reason for being there
|
|
33
|
+
*
|
|
34
|
+
* With a dish or dietary filter on, each row swaps its most-named dish for the
|
|
35
|
+
* one that actually matched, and the kicker changes with it ("MOST NAMED HERE"
|
|
36
|
+
* → "WHY IT MATCHES"). A filtered list whose rows still advertise their
|
|
37
|
+
* most-named dish is claiming the filter matched something it did not.
|
|
38
|
+
*
|
|
39
|
+
* ## Header
|
|
40
|
+
*
|
|
41
|
+
* Deliberately none. The page composes `<SectionHeader>` above it, the same way
|
|
42
|
+
* the events hub does, because the heading here carries a count and a "see the
|
|
43
|
+
* full ranking" link into `/top` that the list knows nothing about.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/** @deprecated Alias of `MenuLinkProps`; kept for symmetry with the other menu surfaces. */
|
|
47
|
+
export type MenuKitchenListLinkProps = MenuLinkProps;
|
|
48
|
+
|
|
49
|
+
function DefaultLink({ href, ...props }: MenuKitchenListLinkProps) {
|
|
50
|
+
return <a href={href} {...props} />;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type MenuKitchenListLabels = {
|
|
54
|
+
/** Accessible prefix for the row's venue link, e.g. "Open". */
|
|
55
|
+
openVenue: (name: string) => string;
|
|
56
|
+
/** Accessible name for the dish link, e.g. "See Truffle Tagliatelle". */
|
|
57
|
+
openDish: (name: string) => string;
|
|
58
|
+
/** Accessible name for the open-now dot. */
|
|
59
|
+
openNow: string;
|
|
60
|
+
/** Reveals the next page of rows, e.g. "Show 10 more". */
|
|
61
|
+
showMore?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type MenuKitchenListProps = {
|
|
65
|
+
kitchens: MenuKitchenPreview[];
|
|
66
|
+
labels: MenuKitchenListLabels;
|
|
67
|
+
/**
|
|
68
|
+
* Reveals more rows IN PLACE. The intent page pages ten at a time and never
|
|
69
|
+
* navigates for the eleventh — the list is the page.
|
|
70
|
+
*/
|
|
71
|
+
onShowMore?: () => void;
|
|
72
|
+
linkComponent?: MenuLinkComponent;
|
|
73
|
+
className?: string;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export function MenuKitchenList({
|
|
77
|
+
kitchens,
|
|
78
|
+
labels,
|
|
79
|
+
onShowMore,
|
|
80
|
+
linkComponent = DefaultLink,
|
|
81
|
+
className,
|
|
82
|
+
}: MenuKitchenListProps) {
|
|
83
|
+
return (
|
|
84
|
+
<div className={cn("flex flex-col", className)}>
|
|
85
|
+
<ol>
|
|
86
|
+
{kitchens.map((kitchen, index) => (
|
|
87
|
+
<li key={kitchen.place.slug} className="[&:first-child>div]:pt-0">
|
|
88
|
+
{/* The DS `Separator`, drawn BETWEEN rows rather than a `border-t`
|
|
89
|
+
with a `first:` reset on every row. It lives inside the `<li>`
|
|
90
|
+
because `<ol>` admits only list items, so a sibling element
|
|
91
|
+
between them would be invalid markup. */}
|
|
92
|
+
{index > 0 && <Separator />}
|
|
93
|
+
<KitchenRow kitchen={kitchen} labels={labels} linkComponent={linkComponent} />
|
|
94
|
+
</li>
|
|
95
|
+
))}
|
|
96
|
+
</ol>
|
|
97
|
+
|
|
98
|
+
{onShowMore && labels.showMore && (
|
|
99
|
+
<button
|
|
100
|
+
type="button"
|
|
101
|
+
onClick={onShowMore}
|
|
102
|
+
className="mt-2 self-start rounded-md py-2 text-[13px] font-bold text-accent transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none"
|
|
103
|
+
>
|
|
104
|
+
{labels.showMore}
|
|
105
|
+
</button>
|
|
106
|
+
)}
|
|
107
|
+
</div>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function KitchenRow({
|
|
112
|
+
kitchen,
|
|
113
|
+
labels,
|
|
114
|
+
linkComponent: LinkComponent,
|
|
115
|
+
}: {
|
|
116
|
+
kitchen: MenuKitchenPreview;
|
|
117
|
+
labels: MenuKitchenListLabels;
|
|
118
|
+
linkComponent: MenuLinkComponent;
|
|
119
|
+
}) {
|
|
120
|
+
const { place, dish } = kitchen;
|
|
121
|
+
const openNow = place.open_now === true;
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<div className="relative flex flex-wrap items-center gap-[11px] py-3 transition-colors hover:bg-muted/30 motion-reduce:transition-none lg:flex-nowrap lg:gap-4 lg:py-3.5">
|
|
125
|
+
{/* The venue link, stretched. `z-[1]` — the dish link sits above it. */}
|
|
126
|
+
<LinkComponent
|
|
127
|
+
href={kitchen.href}
|
|
128
|
+
aria-label={labels.openVenue(place.name)}
|
|
129
|
+
className="absolute inset-0 z-[1] rounded-xl focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-accent"
|
|
130
|
+
/>
|
|
131
|
+
|
|
132
|
+
<span className="relative h-[46px] w-[46px] shrink-0 overflow-hidden rounded-[11px] bg-[repeating-linear-gradient(135deg,oklch(0.78_0.018_55)_0_8px,oklch(0.84_0.012_58)_8px_16px)] lg:h-[58px] lg:w-[58px] lg:rounded-[13px]">
|
|
133
|
+
{place.thumbnail_url && (
|
|
134
|
+
// eslint-disable-next-line @next/next/no-img-element
|
|
135
|
+
<img
|
|
136
|
+
src={place.thumbnail_url}
|
|
137
|
+
alt=""
|
|
138
|
+
loading="lazy"
|
|
139
|
+
decoding="async"
|
|
140
|
+
className="h-full w-full object-cover"
|
|
141
|
+
/>
|
|
142
|
+
)}
|
|
143
|
+
<span
|
|
144
|
+
title={openNow ? labels.openNow : undefined}
|
|
145
|
+
className={cn(
|
|
146
|
+
"absolute right-[5px] top-[5px] h-[7px] w-[7px] rounded-full ring-2 ring-white/85",
|
|
147
|
+
openNow ? "bg-[var(--free,oklch(0.55_0.13_155))]" : "bg-[oklch(0.8_0.01_60)]"
|
|
148
|
+
)}
|
|
149
|
+
/>
|
|
150
|
+
</span>
|
|
151
|
+
|
|
152
|
+
<div className="flex min-w-0 flex-1 flex-col gap-0.5 lg:w-[226px] lg:flex-none">
|
|
153
|
+
<span className="truncate text-[14.5px] font-bold tracking-[-0.01em] lg:text-[15.5px]">
|
|
154
|
+
{place.name}
|
|
155
|
+
</span>
|
|
156
|
+
<span className="flex items-center gap-1 truncate text-xs text-muted-foreground">
|
|
157
|
+
{place.rating && (
|
|
158
|
+
<>
|
|
159
|
+
<Star className="h-[11px] w-[11px] shrink-0" aria-hidden />
|
|
160
|
+
{place.rating.rating}
|
|
161
|
+
{kitchen.metaLabel && " · "}
|
|
162
|
+
</>
|
|
163
|
+
)}
|
|
164
|
+
{kitchen.metaLabel}
|
|
165
|
+
</span>
|
|
166
|
+
{kitchen.hoursLabel && (
|
|
167
|
+
<span
|
|
168
|
+
className={cn(
|
|
169
|
+
"mt-[3px] inline-flex h-[21px] w-max items-center gap-[5px] rounded-md px-2 text-[10.5px] font-bold",
|
|
170
|
+
openNow
|
|
171
|
+
? "bg-[oklch(0.96_0.02_155)] text-[var(--free,oklch(0.45_0.13_155))]"
|
|
172
|
+
: "bg-muted text-muted-foreground"
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
<Clock className="h-2.5 w-2.5 shrink-0" aria-hidden />
|
|
176
|
+
{kitchen.hoursLabel}
|
|
177
|
+
</span>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
{dish && (
|
|
182
|
+
/* DESKTOP ONLY, where it is a column of its own beside the venue.
|
|
183
|
+
On the phone it wrapped to a third line under the row — kicker, dish,
|
|
184
|
+
mention count — which turned a four-line venue row into a seven-line
|
|
185
|
+
block and, ten rows down, buried the venue names the list exists to
|
|
186
|
+
be scanned for. The dish is not what a reader is choosing here; it is
|
|
187
|
+
one tap away on the venue's own page, with room to state it.
|
|
188
|
+
|
|
189
|
+
`order-5` puts it last in the visual order without moving it in the
|
|
190
|
+
DOM, where it belongs to the venue. */
|
|
191
|
+
<div className="hidden min-w-0 basis-full pl-[57px] lg:order-none lg:block lg:flex-1 lg:basis-auto lg:pl-0">
|
|
192
|
+
<span
|
|
193
|
+
className={cn(
|
|
194
|
+
"flex items-center gap-1.5 text-[9px] font-extrabold uppercase tracking-[0.1em]",
|
|
195
|
+
dish.matched ? "text-accent" : "text-[oklch(0.72_0.01_60)]"
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{dish.kicker}
|
|
199
|
+
{dish.matched && dish.matchLabel && (
|
|
200
|
+
<span className="inline-flex items-center gap-1 rounded-[5px] bg-accent-subtle px-1.5 py-px text-[10.5px] font-bold normal-case tracking-normal text-accent">
|
|
201
|
+
<Check className="h-2.5 w-2.5 shrink-0" aria-hidden />
|
|
202
|
+
{dish.matchLabel}
|
|
203
|
+
</span>
|
|
204
|
+
)}
|
|
205
|
+
</span>
|
|
206
|
+
|
|
207
|
+
{dish.href ? (
|
|
208
|
+
<LinkComponent
|
|
209
|
+
href={dish.href}
|
|
210
|
+
aria-label={labels.openDish(dish.name)}
|
|
211
|
+
className="relative z-[2] mt-0.5 block truncate font-serif text-[13.5px] italic underline decoration-[oklch(0.84_0.01_60)] decoration-1 underline-offset-[3px] transition-colors hover:text-accent hover:decoration-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring motion-reduce:transition-none lg:text-[15px]"
|
|
212
|
+
>
|
|
213
|
+
{dish.name}
|
|
214
|
+
</LinkComponent>
|
|
215
|
+
) : (
|
|
216
|
+
<span className="mt-0.5 block truncate font-serif text-[13.5px] italic lg:text-[15px]">
|
|
217
|
+
{dish.name}
|
|
218
|
+
</span>
|
|
219
|
+
)}
|
|
220
|
+
|
|
221
|
+
{dish.meta && (
|
|
222
|
+
<span className="mt-[3px] flex items-center gap-1 text-[11.5px] text-muted-foreground">
|
|
223
|
+
{!dish.matched && <MessageSquare className="h-2.5 w-2.5 shrink-0" aria-hidden />}
|
|
224
|
+
{dish.meta}
|
|
225
|
+
</span>
|
|
226
|
+
)}
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
|
|
230
|
+
<div className="ml-auto flex shrink-0 items-center gap-[11px] lg:ml-0">
|
|
231
|
+
{kitchen.priceLabel && (
|
|
232
|
+
<span className="text-base font-extrabold tabular-nums text-accent">
|
|
233
|
+
{kitchen.priceLabel}
|
|
234
|
+
</span>
|
|
235
|
+
)}
|
|
236
|
+
<ChevronRight className="h-[15px] w-[15px] shrink-0 text-muted-foreground" aria-hidden />
|
|
237
|
+
</div>
|
|
238
|
+
</div>
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ─── Skeleton ───────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The list's own loading state, at the row's own footprint.
|
|
246
|
+
*
|
|
247
|
+
* It lives HERE, beside the component, and that is the whole point: a skeleton
|
|
248
|
+
* kept in the consuming app drifts the moment the row's padding or thumbnail
|
|
249
|
+
* size changes, and the drift is invisible until a page reflows on load. The
|
|
250
|
+
* measurements below are the same literals the row above uses — change one and
|
|
251
|
+
* the diff shows you the other.
|
|
252
|
+
*/
|
|
253
|
+
export function MenuKitchenListSkeleton({
|
|
254
|
+
rows = 10,
|
|
255
|
+
className,
|
|
256
|
+
}: {
|
|
257
|
+
rows?: number;
|
|
258
|
+
className?: string;
|
|
259
|
+
}) {
|
|
260
|
+
return (
|
|
261
|
+
<div className={cn("flex flex-col", className)} aria-hidden>
|
|
262
|
+
{Array.from({ length: rows }, (_, index) => (
|
|
263
|
+
<div key={index}>
|
|
264
|
+
{index > 0 && <Separator />}
|
|
265
|
+
<div className="flex flex-wrap items-center gap-[11px] py-3 lg:flex-nowrap lg:gap-4 lg:py-3.5">
|
|
266
|
+
<Skeleton className="h-[46px] w-[46px] rounded-[11px] lg:h-[58px] lg:w-[58px] lg:rounded-[13px]" />
|
|
267
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1.5 lg:w-[226px] lg:flex-none">
|
|
268
|
+
<Skeleton className="h-4 w-[60%]" />
|
|
269
|
+
<Skeleton className="h-3 w-[45%]" />
|
|
270
|
+
<Skeleton className="h-[21px] w-[120px] rounded-md" />
|
|
271
|
+
</div>
|
|
272
|
+
{/* Desktop only, as the real row's dish column now is. */}
|
|
273
|
+
<div className="hidden min-w-0 basis-full pl-[57px] lg:order-none lg:block lg:flex-1 lg:basis-auto lg:pl-0">
|
|
274
|
+
<Skeleton className="h-2.5 w-[90px]" />
|
|
275
|
+
<Skeleton className="mt-1.5 h-4 w-[70%]" />
|
|
276
|
+
<Skeleton className="mt-1.5 h-3 w-[40%]" />
|
|
277
|
+
</div>
|
|
278
|
+
<Skeleton className="ml-auto h-5 w-[56px] lg:ml-0" />
|
|
279
|
+
</div>
|
|
280
|
+
</div>
|
|
281
|
+
))}
|
|
282
|
+
</div>
|
|
283
|
+
);
|
|
284
|
+
}
|
|
@@ -20,10 +20,17 @@
|
|
|
20
20
|
|
|
21
21
|
import type {
|
|
22
22
|
MenuConceptPreview,
|
|
23
|
+
MenuFamilyBlockPreview,
|
|
23
24
|
MenuFamilyPreview,
|
|
25
|
+
MenuFilterChip,
|
|
26
|
+
MenuIndexEntry,
|
|
27
|
+
MenuIntentFigure,
|
|
24
28
|
MenuIntentTile,
|
|
25
29
|
MenuItemPreview,
|
|
30
|
+
MenuKitchenPreview,
|
|
31
|
+
MenuLoosenOption,
|
|
26
32
|
MenuVenuePreview,
|
|
33
|
+
QaItem,
|
|
27
34
|
} from "./menu-types";
|
|
28
35
|
import type { PlacePreview } from "./place-types";
|
|
29
36
|
|
|
@@ -345,18 +352,6 @@ export const mockFamily: MenuFamilyPreview = {
|
|
|
345
352
|
price_high: 84,
|
|
346
353
|
price_median: 49,
|
|
347
354
|
venue_photo_urls: [],
|
|
348
|
-
signature: {
|
|
349
|
-
item: {
|
|
350
|
-
id: "f1",
|
|
351
|
-
slug: "truffle-tagliatelle",
|
|
352
|
-
name: "Truffle Tagliatelle",
|
|
353
|
-
kind: "dish",
|
|
354
|
-
mentions: 17,
|
|
355
|
-
sizes_and_prices: [{ size: 320, price: 62 }],
|
|
356
|
-
},
|
|
357
|
-
why: "Salt & Stone",
|
|
358
|
-
href: "/menu/dishes/truffle-tagliatelle",
|
|
359
|
-
},
|
|
360
355
|
};
|
|
361
356
|
|
|
362
357
|
/** The drinks counterpart, and the reason `chip.count` is optional. */
|
|
@@ -375,17 +370,6 @@ export const mockDrinkFamily: MenuFamilyPreview = {
|
|
|
375
370
|
price_high: 54,
|
|
376
371
|
price_median: 28,
|
|
377
372
|
venue_photo_urls: [],
|
|
378
|
-
signature: {
|
|
379
|
-
item: {
|
|
380
|
-
id: "f2",
|
|
381
|
-
slug: "house-negroni",
|
|
382
|
-
name: "House negroni",
|
|
383
|
-
kind: "drink",
|
|
384
|
-
mentions: 16,
|
|
385
|
-
sizes_and_prices: [{ size: 150, price: 32 }],
|
|
386
|
-
},
|
|
387
|
-
why: "The Copper Yard",
|
|
388
|
-
},
|
|
389
373
|
};
|
|
390
374
|
|
|
391
375
|
const intentGradient = (hue: number) =>
|
|
@@ -415,3 +399,194 @@ export const mockHubLabels = {
|
|
|
415
399
|
openNow: "Open now",
|
|
416
400
|
until: "until",
|
|
417
401
|
};
|
|
402
|
+
|
|
403
|
+
/* ── intent page ────────────────────────────────────────────────────────── */
|
|
404
|
+
|
|
405
|
+
/**
|
|
406
|
+
* The intent page's kitchen rows.
|
|
407
|
+
*
|
|
408
|
+
* The gaps are the gaps the API actually has, and they are the reason the row
|
|
409
|
+
* degrades the way it does: `must_try` entries arrive from a LIST endpoint, so
|
|
410
|
+
* a third of them carry no `slug` (nothing to open — the dish line goes inert)
|
|
411
|
+
* and none of them carry `mentions` (only `review_ids.length`, which is absent
|
|
412
|
+
* for census items — so the count line disappears rather than reading zero).
|
|
413
|
+
* A layout that survives these survives production.
|
|
414
|
+
*/
|
|
415
|
+
export const mockKitchens: MenuKitchenPreview[] = [
|
|
416
|
+
{
|
|
417
|
+
place: placeBySlug("salt-and-stone"),
|
|
418
|
+
href: "/places/salt-and-stone",
|
|
419
|
+
metaLabel: "courtyard · Centru",
|
|
420
|
+
hoursLabel: "kitchen until 23:00",
|
|
421
|
+
priceLabel: "62 lei",
|
|
422
|
+
dish: {
|
|
423
|
+
name: "Truffle Tagliatelle",
|
|
424
|
+
href: "/menu/dishes/truffle-tagliatelle",
|
|
425
|
+
kicker: "MOST NAMED HERE",
|
|
426
|
+
meta: "17 reviews name it",
|
|
427
|
+
},
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
place: placeBySlug("the-copper-yard"),
|
|
431
|
+
href: "/places/the-copper-yard",
|
|
432
|
+
metaLabel: "pizzeria · Centru",
|
|
433
|
+
hoursLabel: "kitchen until 23:30",
|
|
434
|
+
priceLabel: "54 lei",
|
|
435
|
+
dish: {
|
|
436
|
+
name: "Tagliatelle cu Hribi",
|
|
437
|
+
href: "/menu/dishes/tagliatelle-cu-hribi",
|
|
438
|
+
kicker: "MOST NAMED HERE",
|
|
439
|
+
meta: "14 reviews name it",
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
// Closed, and its must-try has no live menu row — the dish line is inert.
|
|
444
|
+
place: { ...placeBySlug("the-quiet-hours"), open_now: false },
|
|
445
|
+
href: "/places/the-quiet-hours",
|
|
446
|
+
metaLabel: "bistro · Gheorgheni",
|
|
447
|
+
hoursLabel: "closed · opens 12:00",
|
|
448
|
+
priceLabel: "58 lei",
|
|
449
|
+
dish: { name: "Risotto ai Funghi", kicker: "MOST NAMED HERE" },
|
|
450
|
+
},
|
|
451
|
+
{
|
|
452
|
+
// No dish at all: the row must still read as a venue.
|
|
453
|
+
place: placeBySlug("green-room-canteen"),
|
|
454
|
+
href: "/places/green-room-canteen",
|
|
455
|
+
metaLabel: "bistro · Centru",
|
|
456
|
+
priceLabel: "38 lei",
|
|
457
|
+
},
|
|
458
|
+
];
|
|
459
|
+
|
|
460
|
+
/** The same rows in a filtered state — each answering the filter, not volunteering. */
|
|
461
|
+
export const mockMatchedKitchens: MenuKitchenPreview[] = mockKitchens.slice(0, 3).map((kitchen) => ({
|
|
462
|
+
...kitchen,
|
|
463
|
+
dish: kitchen.dish && {
|
|
464
|
+
...kitchen.dish,
|
|
465
|
+
kicker: "WHY IT MATCHES",
|
|
466
|
+
meta: "mid-range · 300g",
|
|
467
|
+
matched: true,
|
|
468
|
+
matchLabel: "vegetarian",
|
|
469
|
+
},
|
|
470
|
+
}));
|
|
471
|
+
|
|
472
|
+
export const mockIntentFigures: MenuIntentFigure[] = [
|
|
473
|
+
{ key: "total", value: "247", label: "kitchens" },
|
|
474
|
+
{ key: "pizza", value: "120", label: "make a pizza", onSelect: () => {} },
|
|
475
|
+
{ key: "pasta", value: "96", label: "make pasta", onSelect: () => {} },
|
|
476
|
+
{ key: "open", value: "58", label: "open now", onSelect: () => {} },
|
|
477
|
+
];
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Note there is no zero-count chip here, and that is not an oversight: the
|
|
481
|
+
* facets endpoint omits values that would match nothing, so a chip can only
|
|
482
|
+
* become `empty` client-side, after it was offered and the reader's next filter
|
|
483
|
+
* emptied it. The `Dead` story constructs that case deliberately.
|
|
484
|
+
*/
|
|
485
|
+
export const mockFilterChips: MenuFilterChip[] = [
|
|
486
|
+
{ key: "open", label: "Open now", count: 58, active: true },
|
|
487
|
+
{ key: "price", label: "Mid-range", count: 96 },
|
|
488
|
+
{ key: "terrace", label: "Terrace", count: 31 },
|
|
489
|
+
{ key: "veg", label: "Has a vegetarian dish", count: 84 },
|
|
490
|
+
{ key: "late", label: "Serving past midnight", count: 4 },
|
|
491
|
+
{ key: "centru", label: "Centru", count: 90 },
|
|
492
|
+
];
|
|
493
|
+
|
|
494
|
+
export const mockLoosenOptions: MenuLoosenOption[] = [
|
|
495
|
+
{ key: "open", label: "Drop “Open now”", gainLabel: "+8 kitchens" },
|
|
496
|
+
{ key: "price", label: "Drop “Mid-range”", gainLabel: "+7 kitchens" },
|
|
497
|
+
{ key: "veg", label: "Drop “Vegetarian”", gainLabel: "+2 kitchens" },
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
/* ── the browse index ───────────────────────────────────────────────────── */
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* The long tail is the point of these fixtures.
|
|
504
|
+
*
|
|
505
|
+
* Cluj's cuisine facet runs from 457 down to 1, and twenty-two of the
|
|
506
|
+
* thirty-eight have fewer than twenty places. A layout tested only on the head
|
|
507
|
+
* of that list looks fine and then ships a page whose last two columns are
|
|
508
|
+
* single digits.
|
|
509
|
+
*/
|
|
510
|
+
export const mockIndexEntries: MenuIndexEntry[] = [
|
|
511
|
+
["Breakfast & brunch", 314],
|
|
512
|
+
["Romanian", 299],
|
|
513
|
+
["Bakery", 250],
|
|
514
|
+
["Italian", 247],
|
|
515
|
+
["Pizza", 245],
|
|
516
|
+
["Fast food", 229],
|
|
517
|
+
["Vegetarian", 172],
|
|
518
|
+
["American", 153],
|
|
519
|
+
["Vegan", 108],
|
|
520
|
+
["Mediterranean", 78],
|
|
521
|
+
["Seafood", 77],
|
|
522
|
+
["Eastern European", 67],
|
|
523
|
+
["Fusion", 61],
|
|
524
|
+
["Middle Eastern", 58],
|
|
525
|
+
["BBQ", 57],
|
|
526
|
+
["Deli", 51],
|
|
527
|
+
["Steakhouse", 49],
|
|
528
|
+
["Japanese", 34],
|
|
529
|
+
["French", 26],
|
|
530
|
+
["Sushi", 23],
|
|
531
|
+
["Turkish", 22],
|
|
532
|
+
["Chinese", 14],
|
|
533
|
+
["Greek", 12],
|
|
534
|
+
["Indian", 11],
|
|
535
|
+
["Thai", 7],
|
|
536
|
+
["German", 5],
|
|
537
|
+
["Lebanese", 3],
|
|
538
|
+
["Portuguese", 1],
|
|
539
|
+
].map(([name, count], index) => ({
|
|
540
|
+
slug: String(name).toLowerCase().replace(/[^a-z]+/g, "-"),
|
|
541
|
+
name: String(name),
|
|
542
|
+
count: Number(count),
|
|
543
|
+
href: `/menu/cuisines/${String(name).toLowerCase().replace(/[^a-z]+/g, "-")}`,
|
|
544
|
+
gradient: Object.values(mockGradients)[index % 5],
|
|
545
|
+
}));
|
|
546
|
+
|
|
547
|
+
export const mockFamilyBlock: MenuFamilyBlockPreview = {
|
|
548
|
+
slug: "italian",
|
|
549
|
+
name: "Italian",
|
|
550
|
+
venueCount: 247,
|
|
551
|
+
chips: [
|
|
552
|
+
{ slug: "pizza", label: "Pizza", count: 120 },
|
|
553
|
+
{ slug: "pasta", label: "Pasta", count: 96 },
|
|
554
|
+
{ slug: "salad", label: "Salad", count: 88 },
|
|
555
|
+
],
|
|
556
|
+
priceBandLabel: "22–86 lei",
|
|
557
|
+
photoUrls: [],
|
|
558
|
+
// Three, as the cuisines index sends, and stripped the same way: no dish and
|
|
559
|
+
// no price. The dish column is ~110px inside this card, which truncates the
|
|
560
|
+
// name and wraps its kicker to three lines; the price is already the band
|
|
561
|
+
// above.
|
|
562
|
+
venues: mockKitchens
|
|
563
|
+
.slice(0, 3)
|
|
564
|
+
.map((kitchen) => ({ ...kitchen, dish: undefined, priceLabel: undefined })),
|
|
565
|
+
href: "/menu/cuisines/italian",
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
/** No photographs, no venue, no price — the degraded block still reads. */
|
|
569
|
+
export const mockBareFamilyBlock: MenuFamilyBlockPreview = {
|
|
570
|
+
slug: "portuguese",
|
|
571
|
+
name: "Portuguese",
|
|
572
|
+
venueCount: 1,
|
|
573
|
+
href: "/menu/cuisines/portuguese",
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export const mockQa: QaItem[] = [
|
|
577
|
+
{
|
|
578
|
+
question: "Which Italian kitchen is best in Cluj?",
|
|
579
|
+
answer:
|
|
580
|
+
"Salt & Stone is named most often — seventeen reviews single out the truffle tagliatelle. The Copper Yard is second on volume and about eight lei cheaper across the menu.",
|
|
581
|
+
},
|
|
582
|
+
{
|
|
583
|
+
question: "What does an Italian main cost?",
|
|
584
|
+
answer:
|
|
585
|
+
"Pasta runs 38 to 84 lei and pizza 30 to 68 across these kitchens. Ninety-six of them sit in the mid-range price band.",
|
|
586
|
+
},
|
|
587
|
+
{
|
|
588
|
+
question: "Anywhere open late?",
|
|
589
|
+
answer:
|
|
590
|
+
"Four keep the kitchen going past midnight — The Copper Yard until 00:00 is the latest of the Italian rooms.",
|
|
591
|
+
},
|
|
592
|
+
];
|