@cntrl-site/sdk-nextjs 1.8.18 → 1.8.19-alpha.1

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.
Binary file
Binary file
@@ -12,22 +12,19 @@ const useCntrlContext_1 = require("../../provider/useCntrlContext");
12
12
  const useSectionRegistry_1 = require("../../utils/ArticleRectManager/useSectionRegistry");
13
13
  const color_1 = require("@cntrl-site/color");
14
14
  const useLayoutContext_1 = require("../useLayoutContext");
15
+ const SectionVideo_1 = require("./SectionVideo");
16
+ const SectionImage_1 = require("./SectionImage");
15
17
  const DEFAULT_COLOR = 'rgba(0, 0, 0, 0)';
16
- function isVideoUrl(url) {
17
- const videoExtensions = ['.mp4', '.webm', '.ogg'];
18
- return videoExtensions.some(ext => url.toLowerCase().endsWith(ext));
19
- }
20
18
  const Section = ({ section, data, children }) => {
21
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
19
+ var _a;
22
20
  const id = (0, react_1.useId)();
23
21
  const sectionRef = (0, react_1.useRef)(null);
24
22
  const { layouts, customSections } = (0, useCntrlContext_1.useCntrlContext)();
25
23
  const layout = (0, useLayoutContext_1.useLayoutContext)();
26
24
  const layoutValues = [section.height, section.color, (_a = section.media) !== null && _a !== void 0 ? _a : {}];
27
25
  const SectionComponent = section.name ? customSections.getComponent(section.name) : undefined;
28
- const isVideo = layout && ((_c = (_b = section.media) === null || _b === void 0 ? void 0 : _b[layout]) === null || _c === void 0 ? void 0 : _c.url) ? isVideoUrl(String((_e = (_d = section.media) === null || _d === void 0 ? void 0 : _d[layout]) === null || _e === void 0 ? void 0 : _e.url)) : false;
29
- const videoRef = (0, react_1.useRef)(null);
30
26
  (0, useSectionRegistry_1.useSectionRegistry)(section.id, sectionRef.current);
27
+ const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
31
28
  const getSectionVisibilityStyles = () => {
32
29
  return layouts
33
30
  .sort((a, b) => a.startsWith - b.startsWith)
@@ -42,34 +39,10 @@ const Section = ({ section, data, children }) => {
42
39
  }`;
43
40
  }, '');
44
41
  };
45
- (0, react_1.useEffect)(() => {
46
- if (!isVideo || !sectionRef.current || !videoRef.current)
47
- return;
48
- const video = videoRef.current;
49
- const section = sectionRef.current;
50
- const observer = new IntersectionObserver(([entry]) => {
51
- if (entry.isIntersecting) {
52
- video.style.display = 'block';
53
- video.play().catch(() => { });
54
- }
55
- else {
56
- video.style.display = 'none';
57
- video.pause();
58
- }
59
- }, {
60
- root: null,
61
- rootMargin: '50px',
62
- threshold: 0
63
- });
64
- observer.observe(section);
65
- return () => observer.disconnect();
66
- }, [isVideo]);
67
42
  if (SectionComponent)
68
43
  return (0, jsx_runtime_1.jsx)("div", { ref: sectionRef, children: (0, jsx_runtime_1.jsx)(SectionComponent, { data: data, children: children }) });
69
- return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: `section-${section.id}`, id: section.name, ref: sectionRef, children: [layout && ((_g = (_f = section.media) === null || _f === void 0 ? void 0 : _f[layout]) === null || _g === void 0 ? void 0 : _g.url) && ((_j = (_h = section.media) === null || _h === void 0 ? void 0 : _h[layout]) === null || _j === void 0 ? void 0 : _j.size) !== 'none' && sectionRef.current && ((0, jsx_runtime_1.jsx)("div", { className: `section-background-overlay-${section.id}`, children: (0, jsx_runtime_1.jsx)("div", { className: `section-background-wrapper-${section.id}`, children: isVideo ? ((0, jsx_runtime_1.jsx)("video", { ref: videoRef, autoPlay: true, loop: true, muted: true, playsInline: true, preload: "auto", className: `video-background-${section.id}`, children: (0, jsx_runtime_1.jsx)("source", { src: (_m = (_l = (_k = section.media) === null || _k === void 0 ? void 0 : _k[layout]) === null || _l === void 0 ? void 0 : _l.url) !== null && _m !== void 0 ? _m : '' }) })) : ((0, jsx_runtime_1.jsx)("img", { src: (_q = (_p = (_o = section.media) === null || _o === void 0 ? void 0 : _o[layout]) === null || _p === void 0 ? void 0 : _p.url) !== null && _q !== void 0 ? _q : '', className: `image-background-${section.id}` })) }, `section-background-wrapper-${section.id}`) })), children] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
70
- ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) => {
71
- var _a;
72
- return (`
44
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)("div", { className: `section-${section.id}`, id: section.name, ref: sectionRef, children: [layoutMedia && layoutMedia.size !== 'none' && sectionRef.current && ((0, jsx_runtime_1.jsx)("div", { className: `section-background-overlay-${section.id}`, children: (0, jsx_runtime_1.jsxs)("div", { className: `section-background-wrapper-${section.id}`, children: [layoutMedia.type === 'video' && ((0, jsx_runtime_1.jsx)(SectionVideo_1.SectionVideo, { container: sectionRef.current, sectionId: section.id, media: layoutMedia })), layoutMedia.type === 'image' && ((0, jsx_runtime_1.jsx)(SectionImage_1.SectionImage, { media: layoutMedia, sectionId: section.id }))] }, `section-background-wrapper-${section.id}`) })), children] }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
45
+ ${(0, sdk_1.getLayoutStyles)(layouts, layoutValues, ([height, color, media]) => (`
73
46
  .section-${section.id} {
74
47
  height: ${getSectionHeight(height)};
75
48
  position: relative;
@@ -87,24 +60,7 @@ const Section = ({ section, data, children }) => {
87
60
  height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
88
61
  width: 100%;
89
62
  }
90
- .video-background-${section.id} {
91
- object-fit: ${(_a = media === null || media === void 0 ? void 0 : media.size) !== null && _a !== void 0 ? _a : 'cover'};
92
- width: ${(media === null || media === void 0 ? void 0 : media.offsetX) === null || (media === null || media === void 0 ? void 0 : media.size) === 'cover' ? '100%' : 'auto'};
93
- height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : '100%'};
94
- position: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? 'sticky' : 'relative'};
95
- top: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : 'unset'};
96
- ${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
97
- }
98
- .image-background-${section.id} {
99
- object-fit: ${media === null || media === void 0 ? void 0 : media.size};
100
- width: ${(media === null || media === void 0 ? void 0 : media.offsetX) === null ? '100%' : 'auto'};
101
- height: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : '100%'};
102
- position: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? 'sticky' : 'relative'};
103
- top: ${(media === null || media === void 0 ? void 0 : media.position) === 'fixed' ? '100vh' : 'unset'};
104
- ${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
105
- }
106
- `);
107
- })}
63
+ `))}
108
64
  ${getSectionVisibilityStyles()}
109
65
  ` })] }));
110
66
  };
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SectionImage = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const style_1 = __importDefault(require("styled-jsx/style"));
10
+ const SectionImage = ({ media, sectionId }) => {
11
+ const id = (0, react_1.useId)();
12
+ const { url, size, position, offsetX } = media;
13
+ const isContainHeight = size === 'contain-height';
14
+ const hasOffsetX = offsetX !== null && size === 'contain';
15
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("img", { src: url, className: `image-background-${sectionId}` }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
16
+ .image-background-${sectionId} {
17
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
18
+ width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
19
+ height: ${position === 'fixed' ? '100vh' : '100%'};
20
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
21
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
22
+ top: ${position === 'fixed' ? '100vh' : 'unset'};
23
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
24
+ ${offsetX ? 'max-width: 100vw;' : ''}
25
+ }
26
+ ` })] }));
27
+ };
28
+ exports.SectionImage = SectionImage;
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SectionVideo = void 0;
7
+ const jsx_runtime_1 = require("react/jsx-runtime");
8
+ const react_1 = require("react");
9
+ const style_1 = __importDefault(require("styled-jsx/style"));
10
+ const SectionVideo = ({ container, sectionId, media }) => {
11
+ const [video, setVideo] = (0, react_1.useState)(null);
12
+ const { url, size, position, offsetX, coverUrl, play } = media;
13
+ const id = (0, react_1.useId)();
14
+ const [isPlaying, setIsPlaying] = (0, react_1.useState)(false);
15
+ const handleCoverClick = () => {
16
+ if (!video || play !== 'on-click')
17
+ return;
18
+ if (isPlaying) {
19
+ video.pause();
20
+ }
21
+ else {
22
+ video.play();
23
+ }
24
+ };
25
+ (0, react_1.useEffect)(() => {
26
+ if (!video || play !== 'auto')
27
+ return;
28
+ const observer = new IntersectionObserver(([entry]) => {
29
+ if (entry.isIntersecting) {
30
+ video.style.display = 'block';
31
+ video.play().catch(() => { });
32
+ }
33
+ else {
34
+ video.style.display = 'none';
35
+ video.pause();
36
+ }
37
+ }, {
38
+ root: null,
39
+ rootMargin: '50px',
40
+ threshold: 0
41
+ });
42
+ observer.observe(container);
43
+ return () => observer.disconnect();
44
+ }, [container, play]);
45
+ (0, react_1.useEffect)(() => {
46
+ if (!video || play !== 'on-click')
47
+ return;
48
+ video.currentTime = 0.01;
49
+ video.pause();
50
+ }, [play]);
51
+ const isContainHeight = size === 'contain-height';
52
+ const hasOffsetX = offsetX !== null && size === 'contain';
53
+ return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("video", { ref: setVideo, autoPlay: play === 'auto', loop: true, style: { opacity: !isPlaying && play === 'on-click' ? 0 : 1 }, controls: false, muted: play === 'auto', playsInline: true, preload: "auto", className: `video-background-${sectionId}`, onPlay: () => setIsPlaying(true), onPause: () => setIsPlaying(false), children: (0, jsx_runtime_1.jsx)("source", { src: `${url}#t=0.001` }) }), (0, jsx_runtime_1.jsx)("div", { className: `video-background-${sectionId}-cover-container`, onClick: handleCoverClick, children: coverUrl && play === 'on-click' && ((0, jsx_runtime_1.jsx)("img", { src: coverUrl, alt: "Video cover", className: `video-background-${sectionId}-cover`, style: { opacity: isPlaying ? 0 : 1 } })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
54
+ .video-background-${sectionId}-cover-container {
55
+ position: absolute;
56
+ pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
57
+ left: 0;
58
+ width: 100%;
59
+ height: 100%;
60
+ top: 0;
61
+ }
62
+ .video-background-${sectionId}-cover {
63
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
64
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
65
+ width: ${isContainHeight ? 'auto' : '100%'};
66
+ height: ${position === 'fixed' ? '100vh' : '100%'};
67
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
68
+ transition: opacity 0.1s ease-in-out;
69
+ top: ${position === 'fixed' ? '100vh' : '0'};
70
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
71
+ ${hasOffsetX ? 'max-width: 100vw;' : ''}
72
+ }
73
+ .video-background-${sectionId} {
74
+ object-fit: ${isContainHeight ? 'unset' : size !== null && size !== void 0 ? size : 'cover'};
75
+ width: ${isContainHeight ? 'auto' : '100%'};
76
+ height: ${position === 'fixed' ? '100vh' : '100%'};
77
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
78
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
79
+ top: ${position === 'fixed' ? '100vh' : 'unset'};
80
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
81
+ }
82
+ ` })] }));
83
+ };
84
+ exports.SectionVideo = SectionVideo;
@@ -73,13 +73,7 @@ const VideoItem = ({ item, sectionId, onResize, interactionCtrl, onVisibilityCha
73
73
  ? ((0, jsx_runtime_1.jsx)(ScrollPlaybackVideo_1.ScrollPlaybackVideo, { sectionId: sectionId, src: item.commonParams.url, playbackParams: scrollPlayback, style: inlineStyles, className: `video video-playback-wrapper video-${item.id}` }))
74
74
  : ((0, jsx_runtime_1.jsx)(jsx_runtime_1.Fragment, { children: hasGLEffect && isFXAllowed
75
75
  ? ((0, jsx_runtime_1.jsx)("canvas", { style: inlineStyles, ref: fxCanvas, className: `video-canvas video-${item.id}`, width: rectWidth, height: rectHeight }))
76
- : ((0, jsx_runtime_1.jsx)("video", { poster: (_p = item.commonParams.coverUrl) !== null && _p !== void 0 ? _p : '', ref: videoRef, autoPlay: true, muted: true, onPlay: () => {
77
- console.log('play');
78
- }, onPause: () => {
79
- console.log('pause');
80
- }, onEnded: () => {
81
- console.log('ended');
82
- }, loop: true, playsInline: true, className: `video video-${item.id}`, style: inlineStyles, children: (0, jsx_runtime_1.jsx)("source", { src: item.commonParams.url }) })) })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
76
+ : ((0, jsx_runtime_1.jsx)("video", { poster: (_p = item.commonParams.coverUrl) !== null && _p !== void 0 ? _p : '', ref: videoRef, autoPlay: true, muted: true, loop: true, playsInline: true, className: `video video-${item.id}`, style: inlineStyles, children: (0, jsx_runtime_1.jsx)("source", { src: item.commonParams.url }) })) })) }), (0, jsx_runtime_1.jsx)(style_1.default, { id: id, children: `
83
77
  .video-wrapper-${item.id} {
84
78
  position: absolute;
85
79
  overflow: hidden;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cntrl-site/sdk-nextjs",
3
- "version": "1.8.18",
3
+ "version": "1.8.19-alpha.1",
4
4
  "description": "SDK for Next.js",
5
5
  "author": "arsen@momdesign.nyc",
6
6
  "license": "MIT",
@@ -31,7 +31,7 @@
31
31
  "@antfu/eslint-config": "^3.8.0",
32
32
  "@cntrl-site/color": "^1.0.0",
33
33
  "@cntrl-site/effects": "^1.4.0",
34
- "@cntrl-site/sdk": "^1.22.16",
34
+ "@cntrl-site/sdk": "^1.22.18-alpha.0",
35
35
  "@types/vimeo__player": "^2.18.0",
36
36
  "@vimeo/player": "^2.25.0",
37
37
  "html-react-parser": "^3.0.1",
@@ -1,4 +1,4 @@
1
- import { FC, ReactElement, useEffect, useId, useRef, useState, useContext } from 'react';
1
+ import { FC, ReactElement, useId, useRef } from 'react';
2
2
  import JSXStyle from 'styled-jsx/style';
3
3
  import {
4
4
  getLayoutMediaQuery,
@@ -11,6 +11,8 @@ import { useCntrlContext } from '../../provider/useCntrlContext';
11
11
  import { useSectionRegistry } from '../../utils/ArticleRectManager/useSectionRegistry';
12
12
  import { CntrlColor } from '@cntrl-site/color';
13
13
  import { useLayoutContext } from '../useLayoutContext';
14
+ import { SectionVideo } from './SectionVideo';
15
+ import { SectionImage } from './SectionImage';
14
16
 
15
17
  type SectionChild = ReactElement<any, any>;
16
18
  const DEFAULT_COLOR = 'rgba(0, 0, 0, 0)';
@@ -21,11 +23,6 @@ interface Props {
21
23
  data?: any;
22
24
  }
23
25
 
24
- function isVideoUrl(url: string): boolean {
25
- const videoExtensions = ['.mp4', '.webm', '.ogg'];
26
- return videoExtensions.some(ext => url.toLowerCase().endsWith(ext));
27
- }
28
-
29
26
  export const Section: FC<Props> = ({ section, data, children }) => {
30
27
  const id = useId();
31
28
  const sectionRef = useRef<HTMLDivElement | null>(null);
@@ -33,9 +30,8 @@ export const Section: FC<Props> = ({ section, data, children }) => {
33
30
  const layout = useLayoutContext();
34
31
  const layoutValues: Record<string, any>[] = [section.height, section.color, section.media ?? {}];
35
32
  const SectionComponent = section.name ? customSections.getComponent(section.name) : undefined;
36
- const isVideo = layout && section.media?.[layout]?.url ? isVideoUrl(String(section.media?.[layout]?.url)) : false;
37
- const videoRef = useRef<HTMLVideoElement | null>(null);
38
33
  useSectionRegistry(section.id, sectionRef.current);
34
+ const layoutMedia = layout && section.media && section.media[layout] ? section.media[layout] : undefined;
39
35
 
40
36
  const getSectionVisibilityStyles = () => {
41
37
  return layouts
@@ -52,30 +48,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
52
48
  }, '');
53
49
  };
54
50
 
55
- useEffect(() => {
56
- if (!isVideo || !sectionRef.current || !videoRef.current) return;
57
- const video = videoRef.current;
58
- const section = sectionRef.current;
59
- const observer = new IntersectionObserver(
60
- ([entry]) => {
61
- if (entry.isIntersecting) {
62
- video.style.display = 'block';
63
- video.play().catch(() => {});
64
- } else {
65
- video.style.display = 'none';
66
- video.pause();
67
- }
68
- },
69
- {
70
- root: null,
71
- rootMargin: '50px',
72
- threshold: 0
73
- }
74
- );
75
- observer.observe(section);
76
- return () => observer.disconnect();
77
- }, [isVideo]);
78
-
79
51
  if (SectionComponent) return <div ref={sectionRef}><SectionComponent data={data}>{children}</SectionComponent></div>;
80
52
 
81
53
  return (
@@ -85,26 +57,17 @@ export const Section: FC<Props> = ({ section, data, children }) => {
85
57
  id={section.name}
86
58
  ref={sectionRef}
87
59
  >
88
- {layout && section.media?.[layout]?.url && section.media?.[layout]?.size !== 'none' && sectionRef.current && (
60
+ {layoutMedia && layoutMedia.size !== 'none' && sectionRef.current && (
89
61
  <div className={`section-background-overlay-${section.id}`}>
90
62
  <div
91
63
  key={`section-background-wrapper-${section.id}`}
92
64
  className={`section-background-wrapper-${section.id}`}
93
65
  >
94
- {isVideo ? (
95
- <video
96
- ref={videoRef}
97
- autoPlay
98
- loop
99
- muted
100
- playsInline
101
- preload="auto"
102
- className={`video-background-${section.id}`}
103
- >
104
- <source src={section.media?.[layout]?.url ?? ''} />
105
- </video>
106
- ) : (
107
- <img src={section.media?.[layout]?.url ?? ''} className={`image-background-${section.id}`} />
66
+ {layoutMedia.type === 'video' && (
67
+ <SectionVideo container={sectionRef.current} sectionId={section.id} media={layoutMedia} />
68
+ )}
69
+ {layoutMedia.type === 'image' && (
70
+ <SectionImage media={layoutMedia} sectionId={section.id} />
108
71
  )}
109
72
  </div>
110
73
  </div>
@@ -131,22 +94,6 @@ export const Section: FC<Props> = ({ section, data, children }) => {
131
94
  height: ${media?.position === 'fixed' ? `calc(${getSectionHeight(height)} + 200vh)` : getSectionHeight(height)};
132
95
  width: 100%;
133
96
  }
134
- .video-background-${section.id} {
135
- object-fit: ${media?.size ?? 'cover'};
136
- width: ${media?.offsetX === null || media?.size === 'cover' ? '100%' : 'auto'};
137
- height: ${media?.position === 'fixed' ? '100vh' : '100%'};
138
- position: ${media?.position === 'fixed' ? 'sticky' : 'relative'};
139
- top: ${media?.position === 'fixed' ? '100vh' : 'unset'};
140
- ${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
141
- }
142
- .image-background-${section.id} {
143
- object-fit: ${media?.size};
144
- width: ${media?.offsetX === null ? '100%' : 'auto'};
145
- height: ${media?.position === 'fixed' ? '100vh' : '100%'};
146
- position: ${media?.position === 'fixed' ? 'sticky' : 'relative'};
147
- top: ${media?.position === 'fixed' ? '100vh' : 'unset'};
148
- ${media && media.offsetX !== null && media.size !== 'cover' ? `margin-left: ${media.offsetX * 100}vw;` : ''}
149
- }
150
97
  `
151
98
  ))
152
99
  }
@@ -0,0 +1,40 @@
1
+ import { FC, useId } from 'react';
2
+ import JSXStyle from 'styled-jsx/style';
3
+
4
+ export type TSectionImage = {
5
+ url: string;
6
+ type: 'image';
7
+ size: string;
8
+ position: string;
9
+ offsetX: number | null;
10
+ };
11
+
12
+ interface Props {
13
+ media: TSectionImage;
14
+ sectionId: string;
15
+ }
16
+
17
+ export const SectionImage: FC<Props> = ({ media, sectionId }) => {
18
+ const id = useId();
19
+ const { url, size, position, offsetX } = media;
20
+ const isContainHeight = size === 'contain-height';
21
+ const hasOffsetX = offsetX !== null && size === 'contain';
22
+ return (
23
+ <>
24
+ <img src={url} className={`image-background-${sectionId}`} />
25
+ <JSXStyle id={id}>{`
26
+ .image-background-${sectionId} {
27
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
28
+ width: ${isContainHeight || hasOffsetX ? 'auto' : '100%'};
29
+ height: ${position === 'fixed' ? '100vh' : '100%'};
30
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
31
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
32
+ top: ${position === 'fixed' ? '100vh' : 'unset'};
33
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
34
+ ${offsetX ? 'max-width: 100vw;' : ''}
35
+ }
36
+ `}
37
+ </JSXStyle>
38
+ </>
39
+ );
40
+ };
@@ -0,0 +1,121 @@
1
+ import { FC, useEffect, useId, useRef, useState } from 'react';
2
+ import JSXStyle from 'styled-jsx/style';
3
+
4
+ export type TSectionVideo = {
5
+ url: string;
6
+ size: string;
7
+ type: 'video';
8
+ play: 'on-click' | 'auto';
9
+ position: string;
10
+ coverUrl: string | null;
11
+ offsetX: number | null;
12
+ };
13
+
14
+ interface Props {
15
+ container: HTMLDivElement;
16
+ sectionId: string;
17
+ media: TSectionVideo;
18
+ }
19
+
20
+ export const SectionVideo: FC<Props> = ({ container, sectionId, media }) => {
21
+ const [video, setVideo] = useState<HTMLVideoElement | null>(null);
22
+ const { url, size, position, offsetX, coverUrl, play } = media;
23
+ const id = useId();
24
+ const [isPlaying, setIsPlaying] = useState(false);
25
+
26
+ const handleCoverClick = () => {
27
+ if (!video || play !== 'on-click') return;
28
+ if (isPlaying) {
29
+ video.pause();
30
+ } else {
31
+ video.play();
32
+ }
33
+ };
34
+
35
+ useEffect(() => {
36
+ if (!video || play !== 'auto') return;
37
+ const observer = new IntersectionObserver(
38
+ ([entry]) => {
39
+ if (entry.isIntersecting) {
40
+ video.style.display = 'block';
41
+ video.play().catch(() => {});
42
+ } else {
43
+ video.style.display = 'none';
44
+ video.pause();
45
+ }
46
+ },
47
+ {
48
+ root: null,
49
+ rootMargin: '50px',
50
+ threshold: 0
51
+ }
52
+ );
53
+ observer.observe(container);
54
+ return () => observer.disconnect();
55
+ }, [container, play]);
56
+
57
+ useEffect(() => {
58
+ if (!video || play !== 'on-click') return;
59
+ video.currentTime = 0.01;
60
+ video.pause();
61
+ }, [play]);
62
+
63
+ const isContainHeight = size === 'contain-height';
64
+ const hasOffsetX = offsetX !== null && size === 'contain';
65
+
66
+ return (
67
+ <>
68
+ <video
69
+ ref={setVideo}
70
+ autoPlay={play === 'auto'}
71
+ loop
72
+ style={{ opacity: !isPlaying && play === 'on-click' ? 0 : 1 }}
73
+ controls={false}
74
+ muted={play === 'auto'}
75
+ playsInline
76
+ preload="auto"
77
+ className={`video-background-${sectionId}`}
78
+ onPlay={() => setIsPlaying(true)}
79
+ onPause={() => setIsPlaying(false)}
80
+ >
81
+ <source src={`${url}#t=0.001`} />
82
+ </video>
83
+ <div className={`video-background-${sectionId}-cover-container`} onClick={handleCoverClick}>
84
+ {coverUrl && play === 'on-click' && (
85
+ <img src={coverUrl} alt="Video cover" className={`video-background-${sectionId}-cover`} style={{ opacity: isPlaying ? 0 : 1 }} />
86
+ )}
87
+ </div>
88
+ <JSXStyle id={id}>{`
89
+ .video-background-${sectionId}-cover-container {
90
+ position: absolute;
91
+ pointer-events: ${play === 'on-click' ? 'auto' : 'none'};
92
+ left: 0;
93
+ width: 100%;
94
+ height: 100%;
95
+ top: 0;
96
+ }
97
+ .video-background-${sectionId}-cover {
98
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
99
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
100
+ width: ${isContainHeight ? 'auto' : '100%'};
101
+ height: ${position === 'fixed' ? '100vh' : '100%'};
102
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
103
+ transition: opacity 0.1s ease-in-out;
104
+ top: ${position === 'fixed' ? '100vh' : '0'};
105
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
106
+ ${hasOffsetX ? 'max-width: 100vw;' : ''}
107
+ }
108
+ .video-background-${sectionId} {
109
+ object-fit: ${isContainHeight ? 'unset' : size ?? 'cover'};
110
+ width: ${isContainHeight ? 'auto' : '100%'};
111
+ height: ${position === 'fixed' ? '100vh' : '100%'};
112
+ transform: ${isContainHeight ? 'translateX(-50%)' : 'none'};
113
+ position: ${position === 'fixed' ? 'sticky' : 'relative'};
114
+ top: ${position === 'fixed' ? '100vh' : 'unset'};
115
+ margin-left: ${isContainHeight ? '50%' : (hasOffsetX ? `${offsetX * 100}vw` : '0')};
116
+ }
117
+ `}
118
+ </JSXStyle>
119
+ </>
120
+ );
121
+ };
@@ -120,15 +120,6 @@ export const VideoItem: FC<ItemProps<TVideoItem>> = ({ item, sectionId, onResize
120
120
  ref={videoRef}
121
121
  autoPlay
122
122
  muted
123
- onPlay={() => {
124
- console.log('play');
125
- }}
126
- onPause={() => {
127
- console.log('pause');
128
- }}
129
- onEnded={() => {
130
- console.log('ended');
131
- }}
132
123
  loop
133
124
  playsInline
134
125
  className={`video video-${item.id}`}