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
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ run = "bash ./scripts/dev/minio-init.sh"

[tasks."dev:medusa:meili:reseed"]
description = "Rebuild Medusa search documents in the running local stack"
run = "bash ./scripts/dev/run-compose.sh exec medusa-be pnpm --filter medusa-be run addInitialSearchDocuments"
run = "bash ./scripts/dev/run-compose.sh exec medusa-be pnpm --filter medusa-be run rebuildSearchDocuments"

[tasks."dev:medusa:seed"]
description = "Seed initial Medusa data in the running local stack"
Expand Down
11 changes: 10 additions & 1 deletion apps/herbatika/src/app/p/[handle]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,27 @@ type ProductDetailPageProps = {
params: Promise<{
handle: string
}>
searchParams: Promise<{
variant?: string | string[]
}>
}

export default async function ProductDetailPage({
params,
searchParams,
}: ProductDetailPageProps) {
const { handle } = await params
const resolvedSearchParams = await searchParams
const initialVariantId =
typeof resolvedSearchParams.variant === "string"
? resolvedSearchParams.variant
: undefined
const { dehydratedState } =
await prefetchProductDetailPageStorefrontData(handle)

return (
<HydrationBoundary state={dehydratedState}>
<ProductDetail handle={handle} />
<ProductDetail handle={handle} initialVariantId={initialVariantId} />
</HydrationBoundary>
)
}
20 changes: 18 additions & 2 deletions apps/herbatika/src/components/herbatika-product-card.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ import { PRODUCT_FALLBACK_IMAGE } from "@/components/product-card/product-card.c
import { resolvePriceState } from "@/components/product-card/product-card.pricing"
import { resolveThumbnail } from "@/components/product-card/product-card.thumbnail"
import { resolveRegionCurrency } from "@/lib/storefront/region-selection"
import {
asStorefrontRecord,
asStorefrontString,
} from "@/lib/storefront/product-pricing"

export type HerbatikaProductCardBaseProps = {
product: HttpTypes.StoreProduct
Expand All @@ -25,11 +29,23 @@ export function useHerbatikaProductCardState(
) {
const region = useRegionContext()
const currencyCode = resolveRegionCurrency(region)
const productHref = product.handle ? `/p/${product.handle}` : "/#"
const productRecord = asStorefrontRecord(product)
const searchResult = asStorefrontRecord(productRecord?.search_result)
const searchResultVariantId = asStorefrontString(searchResult?.variant_id)
const searchResultVariantTitle = asStorefrontString(
searchResult?.variant_title
)
const productHref = product.handle
? `/p/${product.handle}${searchResultVariantId ? `?variant=${encodeURIComponent(searchResultVariantId)}` : ""}`
: "/#"
const price = resolvePriceState(product, currencyCode, priceUnavailableLabel)
const thumbnail = resolveThumbnail(product)
const [imageSrc, setImageSrc] = useState(thumbnail)
const title = product.title?.trim() || product.handle?.trim() || product.id
const productTitle =
product.title?.trim() || product.handle?.trim() || product.id
const title = searchResultVariantTitle
? `${productTitle} – ${searchResultVariantTitle}`
: productTitle

useEffect(() => {
setImageSrc(thumbnail)
Expand Down
4 changes: 2 additions & 2 deletions apps/herbatika/src/components/product-detail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ import { useProductDetailController } from "@/components/product-detail/use-prod
import { RecentlyVisitedProductsSection } from "@/components/recently-visited-products-section"
import { runDetachedPromise } from "@/lib/storefront/detached-promise"

export function ProductDetail({ handle }: ProductDetailProps) {
export function ProductDetail({ handle, initialVariantId }: ProductDetailProps) {
const tCatalog = useTranslations("catalog")
const controller = useProductDetailController({ handle })
const controller = useProductDetailController({ handle, initialVariantId })
const [activeInfoSection, setActiveInfoSection] = useState<
string | undefined
>(controller.defaultInfoSectionValue)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export type Product = HttpTypes.StoreProduct

export type ProductDetailProps = {
handle: string
initialVariantId?: string
}

export type ProductPriceState = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import { useProductDetailData } from "@/components/product-detail/use-product-de

type UseProductDetailControllerProps = {
handle: string
initialVariantId?: string
}

export function useProductDetailController({
handle,
initialVariantId,
}: UseProductDetailControllerProps) {
const data = useProductDetailData({ handle })
const data = useProductDetailData({ handle, initialVariantId })
const actions = useProductDetailActions({
product: data.product,
quantity: data.quantity,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,15 @@ import { useRecordRecentlyVisitedProduct } from "@/lib/storefront/recently-visit
import { resolveRegionCurrency } from "@/lib/storefront/region-selection"
import { storefront } from "@/lib/storefront/storefront"

type UseProductDetailDataProps = { handle: string }
type UseProductDetailDataProps = {
handle: string
initialVariantId?: string
}

export function useProductDetailData({ handle }: UseProductDetailDataProps) {
export function useProductDetailData({
handle,
initialVariantId,
}: UseProductDetailDataProps) {
const tCatalog = useTranslations("catalog")
const tNavigation = useTranslations("navigation")
const region = useRegionContext()
Expand Down Expand Up @@ -179,9 +185,13 @@ export function useProductDetailData({ handle }: UseProductDetailDataProps) {

useEffect(() => {
setQuantity(1)
setSelectedVariantId(product?.variants?.[0]?.id ?? null)
setSelectedVariantId(
product?.variants?.some((variant) => variant.id === initialVariantId)
? (initialVariantId ?? null)
: (product?.variants?.[0]?.id ?? null)
)
setSelectedVolumeDiscountId(null)
}, [product?.variants])
}, [initialVariantId, product?.variants])

useEffect(() => {
if (availableQuantity === null || availableQuantity < 1) {
Expand Down
110 changes: 66 additions & 44 deletions apps/herbatika/src/components/search-results.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,21 @@ import { CategoryFacetsPanel } from "@/components/category/category-facets-panel
import { CategoryResultsSection } from "@/components/category/category-results-section"
import { RecentlyVisitedProductsSection } from "@/components/recently-visited-products-section"
import { PLP_PAGE_SIZE } from "@/lib/storefront/plp-query-state"
import { SearchEntityResults } from "./search/search-entity-results"
import { useSearchAutocomplete } from "./search/use-search-autocomplete"
import { useSearchListingController } from "./search/use-search-listing-controller"

export function SearchResults() {
const t = useTranslations("search")
const controller = useSearchListingController()
const safeTotalPages = Math.max(controller.catalogQuery.totalPages, 1)
const autocomplete = useSearchAutocomplete({
countryCode: controller.searchCountryCode,
query: controller.query,
currencyCode: controller.productsCurrencyCode,
enabled: controller.isSearchQueryEnabled,
regionId: controller.searchRegionId,
})

return (
<main className="mx-auto flex w-full max-w-max-w flex-col gap-search-page-gap p-search-page font-rubik 2xl:p-search-page-lg">
Expand Down Expand Up @@ -43,51 +52,64 @@ export function SearchResults() {
) : null}

{controller.query ? (
<CatalogListingShell
facets={
<CategoryFacetsPanel
activeFilterCount={controller.activeAsideFilterCount}
brandItems={controller.asideBrandItems}
currencyCode={controller.productsCurrencyCode}
formItems={controller.asideFormItems}
ingredientItems={controller.asideIngredientItems}
isLoading={controller.isFiltersLoading}
onBrandToggle={controller.onBrandToggle}
onFormToggle={controller.onFormToggle}
onIngredientToggle={controller.onIngredientToggle}
onPriceRangeCommit={controller.onPriceRangeCommit}
onReset={controller.onResetFilters}
onStatusToggle={controller.onStatusToggle}
priceBounds={controller.priceBounds}
selectedPriceRange={controller.selectedPriceRange}
statusItems={controller.asideStatusItems}
/>
}
results={
<CategoryResultsSection
activeSort={controller.queryState.sort}
catalogError={controller.catalogQuery.error}
categoriesError={null}
emptyMessage={t("results.empty", { query: controller.query })}
isEmpty={controller.products.length === 0}
isLoading={controller.isResultsLoading}
isProductAdding={controller.isProductAdding}
isRefreshing={controller.isResultsRefreshing}
layout="catalog"
onAddToCart={controller.onAddToCart}
onProductHoverEnd={controller.onProductHoverEnd}
onProductHoverStart={controller.onProductHoverStart}
onSortChange={controller.onSortChange}
page={controller.page}
pageSize={PLP_PAGE_SIZE}
products={controller.products}
showCategoryNotFound={false}
totalCount={controller.catalogQuery.totalCount}
totalPages={controller.catalogQuery.totalPages}
totalProducts={controller.catalogQuery.totalCount}
<>
<SearchEntityResults
brands={autocomplete.data.brands}
categories={autocomplete.data.categories}
content={autocomplete.data.content}
/>

<section className="space-y-300">
<h2 className="font-bold text-2xl text-fg-primary">
{t("autocomplete.sections.products")}
</h2>
<CatalogListingShell
facets={
<CategoryFacetsPanel
activeFilterCount={controller.activeAsideFilterCount}
brandItems={controller.asideBrandItems}
currencyCode={controller.productsCurrencyCode}
formItems={controller.asideFormItems}
ingredientItems={controller.asideIngredientItems}
isLoading={controller.isFiltersLoading}
onBrandToggle={controller.onBrandToggle}
onFormToggle={controller.onFormToggle}
onIngredientToggle={controller.onIngredientToggle}
onPriceRangeCommit={controller.onPriceRangeCommit}
onReset={controller.onResetFilters}
onStatusToggle={controller.onStatusToggle}
priceBounds={controller.priceBounds}
selectedPriceRange={controller.selectedPriceRange}
statusItems={controller.asideStatusItems}
/>
}
results={
<CategoryResultsSection
activeSort={controller.queryState.sort}
catalogError={controller.catalogQuery.error}
categoriesError={null}
emptyMessage={t("results.empty", { query: controller.query })}
isEmpty={controller.products.length === 0}
isLoading={controller.isResultsLoading}
isProductAdding={controller.isProductAdding}
isRefreshing={controller.isResultsRefreshing}
layout="catalog"
onAddToCart={controller.onAddToCart}
onProductHoverEnd={controller.onProductHoverEnd}
onProductHoverStart={controller.onProductHoverStart}
onSortChange={controller.onSortChange}
page={controller.page}
pageSize={PLP_PAGE_SIZE}
products={controller.products}
showCategoryNotFound={false}
totalCount={controller.catalogQuery.totalCount}
totalPages={controller.catalogQuery.totalPages}
totalProducts={controller.catalogQuery.totalCount}
/>
}
/>
}
/>
</section>
</>
) : (
<section className="rounded-lg border border-border-secondary bg-base p-400">
<p className="text-fg-secondary text-sm">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ const TYPE_ICON: Record<
> = {
brand: "token-icon-label",
category: "token-icon-box",
content: "token-icon-file-document",
}

export function SearchAutocompleteMedia({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { SearchAutocompletePanelSection } from "./search-autocomplete-panel
type SearchAutocompleteSectionTitles = {
brands: string
categories: string
content: string
products: string
}

Expand All @@ -14,6 +15,7 @@ export const createSearchAutocompleteSections = (
{ key: "product", title: titles.products, items: data.products },
{ key: "category", title: titles.categories, items: data.categories },
{ key: "brand", title: titles.brands, items: data.brands },
{ key: "content", title: titles.content, items: data.content },
]

export const clampSearchAutocompleteIndex = (
Expand Down
94 changes: 94 additions & 0 deletions apps/herbatika/src/components/search/search-entity-results.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
'use client'

import { Link } from '@techsio/ui-kit/atoms/link'
import NextLink from 'next/link'
import type { SearchAutocompleteSuggestion, SearchAutocompleteResponse } from '@/lib/search-autocomplete/search-autocomplete-types'
import { SearchAutocompleteMedia } from './search-autocomplete-media'

type SearchEntityResultSectionProps = {
items: SearchAutocompleteSuggestion[]
title: string
}

type SearchEntityResultsProps = Pick<SearchAutocompleteResponse, 'brands' | 'categories' | 'content'>

function SearchEntityResultSection({ items, title }: SearchEntityResultSectionProps) {
if (items.length === 0) {
return null
}

return (
<section className = 'space-y-200'>
<h3 className = 'font-semibold text-fg-primary text-lg'>
{ title }
</h3>

<ul className = 'grid gap-300 sm:grid-cols-2 lg:grid-cols-3'>
{
items.map((item) => (
<li key = { item.type + '-' + item.id }>
<Link
as = { NextLink }
className = 'group flex h-full min-w-0 items-center gap-300 rounded-lg border border-border-secondary bg-surface px-400 py-250 text-fg-primary shadow-sm transition-colors hover:border-primary/30 hover:bg-fill-secondary'
href = { item.href }
>
<SearchAutocompleteMedia
item = { item }
/>

<span className = 'min-w-0'>
<span className = 'block truncate font-semibold text-sm'>
{ item.title }
</span>

{
item.subtitle ? (
<span className = 'block truncate text-fg-secondary text-xs'>
{ item.subtitle }
</span>
) : null
}
</span>
</Link>
</li>
))
}
</ul>
</section>
)
}

export function SearchEntityResults({ brands, categories, content }: SearchEntityResultsProps) {
if (brands.length + categories.length + content.length === 0) {
return null
}

return (
<section
aria-labelledby = 'search-related-results-heading'
className = 'space-y-400 rounded-lg border border-border-secondary bg-base p-400'
>
<h2
className = 'font-bold text-2xl text-fg-primary'
id = 'search-related-results-heading'
>
Súvisiace výsledky
</h2>

<SearchEntityResultSection
items = { categories }
title = 'Kategórie'
/>

<SearchEntityResultSection
items = { brands }
title = 'Výrobcovia'
/>

<SearchEntityResultSection
items = { content }
title = 'Obsah'
/>
Comment on lines +71 to +91

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

fix: localise the entity-result labels.

The component renders Slovak labels directly. Non-Slovak locales will show these labels in Slovak.

Use useTranslations("search") and add translation keys for the related-results heading and each entity section. Keep the keys consistent with SearchResults.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/herbatika/src/components/search/search-entity-results.tsx` around lines
71 - 91, Update the component containing the related-results heading and
SearchEntityResultSection calls to use useTranslations("search") instead of
hardcoded Slovak labels. Add and use translation keys for the heading,
categories, brands, and content sections, matching the existing key naming
conventions in SearchResults.

</section>
)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Loading
Loading