Experts-level management of the Laapak Medusa V2 system. Handles backend customizations, storefront data fetching, laptop specifications (metadata), Google Reviews DB caching, and WooCommerce migration workflows.
This skill provides the cognitive foundation for working with the Laapak Reports System's Medusa V2 backend and Next.js storefront.
laapak-backend)laapak-backend-storefront)Path: /media/saif/brain/Projects/Laapak-Softwares/Laapak-Report-System/Laapak-React-Site/laapak-backend
metadataProductspecssrc/admin/widgets/product-specs-widget.tsx: A custom widget placed before product details.@tanstack/react-query hooks (like useMutation) directly in widgets if they cause "No QueryClient set" errors. Use plain async functions with the Medusa SDK instead.src/admin/lib/sdk.ts provides the admin client instance.Path: /media/saif/brain/Projects/Laapak-Softwares/Laapak-Report-System/Laapak-React-Site/laapak-backend-storefront
src/lib/data/products.ts -> listProducts.
+metadata in the fields query parameter to ensure specs are available.src/modules/products/templates/product-info/index.tsx (Renders key specs in Arabic under the title - centered).src/modules/products/components/product-description-table/index.tsx (Parses plain text descriptions into tables).src/modules/products/components/image-gallery/index.tsx (Embla-based slider merging 360° video and images).src/modules/products/components/product-specs-summary/index.tsx (Minimalist vertical list of technical specs with icons, strictly left-aligned).src/modules/products/components/product-tabs/index.tsx (Contains accordions for Description and Specs).# Ensure Database Services are Running
# If the backend fails with "Pg connection failed", check if the containers are stopped.
docker start backend_postgres_1 backend_redis_1
# Backend
cd .../laapak-backend && npm run dev
# Storefront
cd .../laapak-backend-storefront && npm run dev
sdk.admin.product.update(id, { metadata: { specs: { ... } } }).laapak-backend/src/scripts/.import-woocommerce.ts: Main entry point for syncing products, extracting specs from Arabic descriptions, and handling media.Follow these five steps every time:
src/modules/<name>/models/<entity>.ts using model.define("table_name", { ... })src/modules/<name>/service.ts using class extends MedusaService({ Entity })src/modules/<name>/index.ts exporting Module("moduleName", { service }){ resolve: "./src/modules/<name>" } to modules[] in medusa-config.tsnpx medusa db:generate <moduleName> && npx medusa db:migrateSpecifications follow this structure in the database:
{
"specs": {
"processor": "Intel Core i7...",
"ram": "16 GB...",
"storage": "512 GB SSD...",
"gpu": "NVIDIA...",
"screen_size": "15.6 inch...",
"condition": "Excellent..."
}
}
When fetching products for the storefront, ensure the query looks like this:
const { products } = await sdk.store.product.list({
fields: "*variants.calculated_price,+variants.inventory_quantity,+metadata"
})
Architecture: Google API → (12h job) → Medusa DB → Store API → Next.js storefront
src/modules/google-reviews/ — GoogleReviewCache entity with reviews (JSON), rating, user_ratings_total, last_synced_atGET /store/google-reviews — public endpoint, reads from DB, no Google callsrc/jobs/sync-google-reviews.ts — cron 0 */12 * * *, upserts single cache rowsrc/lib/data/google-reviews.ts — calls Medusa API, NOT Google directlynpx medusa exec src/jobs/sync-google-reviews.tsGOOGLE_PLACES_API_KEY, GOOGLE_PLACE_IDAll icons from @medusajs/icons have hardcoded width="15" height="15" directly on the <svg> element. This means:
w-*/h-* classes have NO effect — they are overridden by the inline SVG attributewidth={48} as a prop is unreliable in some rendering paths@medusajs/icons with standard inline <svg> elementsPattern for replacement:
// ❌ BROKEN — Tailwind classes and size props don't work reliably
import { User } from "@medusajs/icons"
<User className="w-6 h-6" />
// ✅ CORRECT — Use inline SVG with explicit width/height attributes
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"
fill="none" stroke="currentColor" strokeWidth="1.5"
strokeLinecap="round" strokeLinejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
<circle cx="12" cy="7" r="4" />
</svg>
Files already fixed (all use inline SVGs):
src/modules/home/components/laapak-process/index.tsx — process step iconssrc/modules/layout/templates/nav/index.tsx — User, ShoppingCartsrc/modules/layout/components/cart-dropdown/index.tsx — ShoppingCartsrc/modules/home/components/hero/index.tsx — ArrowLeftMinisrc/modules/home/components/laptop-selector/index.tsx — CurrencyDollar, Adjustments, ChatBubbleLeftRightsrc/modules/store/components/search-bar/index.tsx — MagnifyingGlassMiniWhen using absolute positioned badge elements (e.g., step number badges) on top of icon circles:
relative container for the badge must be a dedicated wrapper div, NOT reused from a z-10 positioned parentz-10 alone does not create a positioning context without relative<div className="relative mb-6"> {/* Dedicated relative wrapper */}
<div style={{ width: 128, height: 128 }} className="rounded-full ...">
{/* icon */}
</div>
<div className="absolute -top-2 -right-2 ...">1</div> {/* Badge */}
</div>
npm run dev in laapak-backend crashes with KnexTimeoutError: SELECT 1 or Pg connection failed, it means the backend database containers are stopped. Run docker start backend_postgres_1 backend_redis_1 to start Medusa's dedicated backend dependencies.metadata field is explicitly requested in the Medusa API call.laapak-green and laapak-gray tokens for brand consistency./store/google-reviews returns 404, the cache hasn't been seeded yet. Run npx medusa exec src/jobs/sync-google-reviews.ts from the backend directory.The mobile /store page renders a full-screen, vertical snap-scroll "Reels" UI (Instagram-style) instead of the standard product grid.
src/modules/store/templates/infinite-products.tsx — Switches between desktop grid (hidden small:grid) and mobile reels (flex small:hidden). Uses h-[100dvh] for full-screen height. The Global Footer is appended at the end as a final snap-start element when no more products load.src/modules/products/components/product-reel/index.tsx — The per-product reel card. Renders full-bleed video or image background, text overlay, and three floating action buttons.src/modules/products/components/product-reel/add-to-cart.tsx — Toggle Add/Remove from Cart button for the Reels UI. Checks cart via retrieveCart() on mount to show correct state. Uses deleteLineItem for removal.h-[100dvh] (not calc(100vh-64px)) for the mobile scroll container because the Nav is now transparent and position: absolute over the Reels.LocalizedClientLink to product pagewa.me/<number>?text=<pre-filled Arabic message with product title + URL>StoreContext (src/lib/context/store-context.tsx) manages isSidebarOpen boolean state shared between:
NavFilterButton — the toggle button in the NavStoreTemplate — where the sidebar renders based on that stateThe StoreProvider MUST wrap BOTH the Nav and the store page children to share state. If placed only around Nav, the sidebar won't react. If placed only around StoreTemplate, the filter button in Nav won't affect it.
Correct placement: In the root layout src/app/[countryCode]/(main)/layout.tsx:
return (
<StoreProvider> {/* wraps both Nav and {props.children} */}
<Nav />
{props.children}
<Footer ... />
</StoreProvider>
)
⚠️
StoreProvideruses React Context (client-side). It CAN be used in a Server Component layout as a wrapper — Next.js handles this correctly as long as all the context state logic stays inside the"use client"provider component itself.
A NavHeader client component (src/modules/layout/templates/nav/nav-header.tsx) reads usePathname() and conditionally applies position: absolute + bg-gradient styling on the store page so the Nav overlays the Reels video.
On non-store pages: relative bg-white border-b shadow-sm (normal header).
On /store page (mobile only): absolute top-0 z-50 bg-gradient-to-b from-white/90 to-transparent overlay.
NavFilterButton (src/modules/layout/templates/nav/nav-filter-button.tsx):
pathname?.includes("/store")isSidebarOpen from useStoreContext()Medusa V2 already exposes calculated_amount (sale price) and original_amount (regular price) on variant.calculated_price. These are populated when a Price List with type sale is created.
src/modules/products/components/product-preview/price.tsx — PreviewPrice component:
price.price_type === "sale": shows strikethrough original_price, red sale calculated_price, and a percentage_diff% badgeprice.price_type !== "sale": shows price in Laapak Green normallyoriginal_amount will be the variant's default price, and calculated_amount will be the price list priceThe
PreviewPricecomponent handles all the rendering automatically — no custom metadata needed.
HotDeals ComponentUses the shared ProductSlider (Embla Carousel) instead of a static grid.
allHomeProducts.slice(0, 8) in page.tsx to allow more swipingProductSlider Component (src/modules/home/components/product-slider/index.tsx)Embla-based horizontal carousel. Apply this to any section needing a slider. Key config:
useEmblaCarousel(
{ align: "start", direction: "rtl", containScroll: "trimSnaps", dragFree: true, loop: true },
[Autoplay({ delay: 3000, stopOnInteraction: false })]
)
Structure:
ImageGallery taking up calc(100vh - 240px) screen height.ProductInfo → minimalist ProductSpecsSummary (Left aligned) → ProductActions → ProductTrustBadges → ProductTabs (Accordion).object-cover for a full-bleed "fitting" look.onContextMenu={(e) => e.preventDefault()}) and dragging (draggable={false}) on all gallery media.controlsList="nodownload" for the video tag.