@graphcommerce/magento-product 10.1.0-canary.21 → 10.1.0-canary.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Change Log
2
2
 
3
+ ## 10.1.0-canary.23
4
+
5
+ ## 10.1.0-canary.22
6
+
7
+ ### Minor Changes
8
+
9
+ - [#2627](https://github.com/graphcommerce-org/graphcommerce/pull/2627) [`95c188f`](https://github.com/graphcommerce-org/graphcommerce/commit/95c188fcd0dc6cb4ca3edc9877d203d45fe18bb0) - Add `YoutubeEmbed` component — a lightweight lazy-loading YouTube player that defers iframe creation until the user clicks the poster. Uses preconnect on hover for fast playback start and is styled with MUI sx, so no external CSS is required. Supports playlists, no-cookie mode, custom aspect ratios and ad-network preconnect hints.
10
+
11
+ `ProductVideo` (used by `ProductPageGallery`) now delegates YouTube playback to `YoutubeEmbed`, so any product whose Magento `media_gallery` contains a YouTube video entry gets the new lazy-loading player on its product page. Vimeo and self-hosted video paths are unchanged. The Magento preview image is passed as the YoutubeEmbed `thumbnail` so the visible poster stays consistent with the rest of the gallery.
12
+
13
+ Fix `SidebarGallery` so it forwards the `Additional` and `slotProps` from each image to `MotionImageAspect`. Before this fix the gallery silently dropped both props, which meant any `Additional` overlay configured by `ProductPageGallery` (the `<ProductVideo>` overlay with its `PlayCircle` and the new `YoutubeEmbed`) never reached the DOM. That was a latent regression that made all video-gallery entries render as static images, with or without this PR's YouTube changes.
14
+
15
+ Fix `playwright.config.ts` so `npx playwright test` actually loads. The config previously imported `examples/magento-graphcms/next.config.ts`, which transitively pulled `@graphcommerce/next-config`'s ESM build into a CJS context and crashed with `ReferenceError: exports is not defined`. Replaced with an opt-in `PLAYWRIGHT_LOCALES` env var for the multi-locale projects that the next.config import was meant to drive. ([@paales](https://github.com/paales))
16
+
3
17
  ## 10.1.0-canary.21
4
18
 
5
19
  ## 10.1.0-canary.20
@@ -1,5 +1,5 @@
1
1
  import type { MotionImageAspectPropsAdditional } from '@graphcommerce/framer-scroller'
2
- import { Fab, iconPlay, IconSvg, sxx, type FabProps } from '@graphcommerce/next-ui'
2
+ import { Fab, iconPlay, IconSvg, sxx, YoutubeEmbed, type FabProps } from '@graphcommerce/next-ui'
3
3
  import { Avatar, Box, IconButton, styled, type SxProps, type Theme } from '@mui/material'
4
4
  import { m, type MotionProps, type MotionStyle } from 'framer-motion'
5
5
  import React, { useState } from 'react'
@@ -90,6 +90,11 @@ function getEmbedUrl(regularUrl: string, noCookie: boolean = true, muted: boolea
90
90
  return null
91
91
  }
92
92
 
93
+ function extractYoutubeId(src: string): string | null {
94
+ const match = src.match(youtubeRegExp)
95
+ return match?.[1] ?? null
96
+ }
97
+
93
98
  export function ProductVideo(props: ProductVideoProps) {
94
99
  const { video, autoplay, iframeProps, videoProps, sx, style, layout, width, height } = props
95
100
 
@@ -102,6 +107,33 @@ export function ProductVideo(props: ProductVideoProps) {
102
107
  const src = videoContent.video_url
103
108
  const title = videoContent.video_title || undefined
104
109
 
110
+ const youtubeId = extractYoutubeId(src)
111
+ if (youtubeId) {
112
+ return (
113
+ <YoutubeEmbed
114
+ id={youtubeId}
115
+ title={title ?? ''}
116
+ thumbnail={video?.url ?? undefined}
117
+ aspectWidth={width ?? 16}
118
+ aspectHeight={height ?? 9}
119
+ sx={sxx(
120
+ {
121
+ position: 'absolute',
122
+ top: '50%',
123
+ left: '50%',
124
+ transform: 'translate(-50%, -50%)',
125
+ width: '100%',
126
+ height: 'auto',
127
+ maxWidth: '99.6%',
128
+ maxHeight: '100%',
129
+ aspectRatio: width && height ? `${width} / ${height}` : '16 / 9',
130
+ },
131
+ sx,
132
+ )}
133
+ />
134
+ )
135
+ }
136
+
105
137
  const baseSx: SxProps<Theme> = (theme) => ({
106
138
  display: 'block',
107
139
  maxWidth: '99.6%',
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@graphcommerce/magento-product",
3
3
  "homepage": "https://www.graphcommerce.org/",
4
4
  "repository": "github:graphcommerce-org/graphcommerce",
5
- "version": "10.1.0-canary.21",
5
+ "version": "10.1.0-canary.23",
6
6
  "sideEffects": false,
7
7
  "prettier": "@graphcommerce/prettier-config-pwa",
8
8
  "eslintConfig": {
@@ -18,18 +18,18 @@
18
18
  "typescript": "5.9.3"
19
19
  },
20
20
  "peerDependencies": {
21
- "@graphcommerce/ecommerce-ui": "^10.1.0-canary.21",
22
- "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.21",
23
- "@graphcommerce/framer-next-pages": "^10.1.0-canary.21",
24
- "@graphcommerce/framer-scroller": "^10.1.0-canary.21",
25
- "@graphcommerce/graphql": "^10.1.0-canary.21",
26
- "@graphcommerce/graphql-mesh": "^10.1.0-canary.21",
27
- "@graphcommerce/image": "^10.1.0-canary.21",
28
- "@graphcommerce/magento-cart": "^10.1.0-canary.21",
29
- "@graphcommerce/magento-store": "^10.1.0-canary.21",
30
- "@graphcommerce/next-ui": "^10.1.0-canary.21",
31
- "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.21",
32
- "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.21",
21
+ "@graphcommerce/ecommerce-ui": "^10.1.0-canary.23",
22
+ "@graphcommerce/eslint-config-pwa": "^10.1.0-canary.23",
23
+ "@graphcommerce/framer-next-pages": "^10.1.0-canary.23",
24
+ "@graphcommerce/framer-scroller": "^10.1.0-canary.23",
25
+ "@graphcommerce/graphql": "^10.1.0-canary.23",
26
+ "@graphcommerce/graphql-mesh": "^10.1.0-canary.23",
27
+ "@graphcommerce/image": "^10.1.0-canary.23",
28
+ "@graphcommerce/magento-cart": "^10.1.0-canary.23",
29
+ "@graphcommerce/magento-store": "^10.1.0-canary.23",
30
+ "@graphcommerce/next-ui": "^10.1.0-canary.23",
31
+ "@graphcommerce/prettier-config-pwa": "^10.1.0-canary.23",
32
+ "@graphcommerce/typescript-config-pwa": "^10.1.0-canary.23",
33
33
  "@lingui/core": "^5",
34
34
  "@lingui/macro": "^5",
35
35
  "@lingui/react": "^5",
@@ -0,0 +1,62 @@
1
+ import { expect, test } from '@playwright/test'
2
+
3
+ /**
4
+ * Validates that a product whose `media_gallery` contains a YouTube video
5
+ * renders the lazy-loading `YoutubeEmbed` component in the product gallery,
6
+ * shows the Magento preview image as the poster, and swaps in the YouTube
7
+ * iframe on click.
8
+ *
9
+ * Backend prerequisite: the configured Magento backend must have a product
10
+ * at the URL below with at least one `ProductVideo` entry pointing at a
11
+ * YouTube URL. Override with `PRODUCT_URL` and `EXPECTED_YOUTUBE_ID` env
12
+ * vars when running against a different backend.
13
+ */
14
+ const PRODUCT_URL = process.env.PRODUCT_URL ?? '/p/spooky-girl-gc-1-sock'
15
+ const EXPECTED_YOUTUBE_ID = process.env.EXPECTED_YOUTUBE_ID ?? 'u_pe6qAhz5U'
16
+
17
+ test.describe('YoutubeEmbed in product media gallery', () => {
18
+ test('renders the YouTube poster, lazy-loads the iframe on click', async ({ page }) => {
19
+ await page.goto(PRODUCT_URL, { waitUntil: 'domcontentloaded' })
20
+
21
+ // Wait for the gallery to mount. We don't depend on a specific slide order
22
+ // — the YouTube slide may be 2nd, 3rd, etc. depending on the product.
23
+ await page.waitForSelector('[class*="SidebarGallery-root"]', { timeout: 30_000 })
24
+
25
+ const youtubeEmbed = page.locator('[class*="YoutubeEmbed-root"]').first()
26
+ await expect(youtubeEmbed).toBeAttached({ timeout: 15_000 })
27
+
28
+ // Magento preview image is forwarded as the YoutubeEmbed `thumbnail` prop,
29
+ // so the background should be the configured Magento media URL — not the
30
+ // YouTube auto-generated thumbnail at i.ytimg.com.
31
+ const initial = await youtubeEmbed.evaluate((el) => {
32
+ const styles = getComputedStyle(el)
33
+ return {
34
+ dataTitle: el.getAttribute('data-title'),
35
+ backgroundImage: styles.backgroundImage,
36
+ hasIframe: !!el.querySelector('iframe'),
37
+ playButtonLabel: el.querySelector('button[type="button"]')?.getAttribute('aria-label'),
38
+ }
39
+ })
40
+ expect(initial.hasIframe).toBe(false)
41
+ expect(initial.dataTitle).toBeTruthy()
42
+ expect(initial.playButtonLabel).toContain('Watch')
43
+ expect(initial.backgroundImage).not.toContain('i.ytimg.com')
44
+ expect(initial.backgroundImage).toContain('http')
45
+
46
+ // Hovering preconnects to youtube-nocookie.com so the iframe load is fast.
47
+ await youtubeEmbed.hover()
48
+ await expect(
49
+ page.locator('link[rel="preconnect"][href*="youtube"]'),
50
+ ).toHaveCount(1, { timeout: 5_000 })
51
+
52
+ // Click swaps the poster for the actual YouTube iframe with autoplay=1.
53
+ await youtubeEmbed.click()
54
+ const iframe = youtubeEmbed.locator('iframe')
55
+ await expect(iframe).toBeAttached({ timeout: 5_000 })
56
+
57
+ const src = await iframe.getAttribute('src')
58
+ expect(src).toContain(EXPECTED_YOUTUBE_ID)
59
+ expect(src).toContain('autoplay=1')
60
+ expect(src).toMatch(/youtube(-nocookie)?\.com/)
61
+ })
62
+ })