diff --git a/src/components/PlatformOverviewSection/index.js b/src/components/PlatformOverviewSection/index.js new file mode 100644 index 0000000..1ca102c --- /dev/null +++ b/src/components/PlatformOverviewSection/index.js @@ -0,0 +1,262 @@ +import React, { useEffect, useRef, useState } from 'react'; +import useBaseUrl from '@docusaurus/useBaseUrl'; +import { useColorMode } from '@docusaurus/theme-common'; +import styles from './styles.module.css'; + +const demos = [ + { + id: 'clusters', + label: 'Create Clusters', + file: 'clusters.mp4', + description: 'Create a production-ready PostgreSQL cluster.', + }, + { + id: 'parameters', + label: 'Edit Parameters', + file: 'parameters.mp4', + description: 'Edit cluster and system parameters from the platform interface.', + }, + { + id: 'yaml-editor', + label: 'YAML Editor', + file: 'yaml-editor.mp4', + description: 'Edit cluster configuration as declarative YAML.', + }, + { + id: 'sql-editor', + label: 'SQL Editor', + file: 'sql-editor.mp4', + description: 'Run SQL queries directly from the platform interface.', + }, +]; + +export default function PlatformOverviewSection() { + const [activeIndex, setActiveIndex] = useState(0); + const [mediaStatus, setMediaStatus] = useState('loading'); + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + const [isNearView, setIsNearView] = useState(false); + const [isInView, setIsInView] = useState(false); + const [hasStartedPlaying, setHasStartedPlaying] = useState(false); + const sectionRef = useRef(null); + const tabRefs = useRef([]); + const videoRef = useRef(null); + const prefetchedVideosRef = useRef(new Set()); + const { colorMode } = useColorMode(); + const mediaRoot = useBaseUrl('/video/platform-overview/'); + const activeDemo = demos[activeIndex]; + const themedFile = colorMode === 'dark' + ? activeDemo.file.replace(/\.mp4$/, '.dark.mp4') + : activeDemo.file; + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const updatePreference = () => setPrefersReducedMotion(mediaQuery.matches); + + updatePreference(); + mediaQuery.addEventListener?.('change', updatePreference); + return () => mediaQuery.removeEventListener?.('change', updatePreference); + }, []); + + useEffect(() => { + setMediaStatus('loading'); + setHasStartedPlaying(false); + }, [activeIndex, colorMode]); + + useEffect(() => { + if (!sectionRef.current) return undefined; + + if (!('IntersectionObserver' in window)) { + setIsNearView(true); + setIsInView(true); + return undefined; + } + + const preloadObserver = new IntersectionObserver( + ([entry]) => setIsNearView(entry.isIntersecting), + { rootMargin: '75% 0px', threshold: 0 } + ); + const playbackObserver = new IntersectionObserver( + ([entry]) => setIsInView(entry.isIntersecting && entry.intersectionRatio >= 0.35), + { threshold: 0.35 } + ); + + preloadObserver.observe(sectionRef.current); + playbackObserver.observe(sectionRef.current); + + return () => { + preloadObserver.disconnect(); + playbackObserver.disconnect(); + }; + }, []); + + useEffect(() => { + if (!isNearView || !videoRef.current || videoRef.current.readyState >= 3) return; + videoRef.current.load(); + }, [isNearView, activeIndex, themedFile]); + + useEffect(() => { + if (!videoRef.current) return; + + if (prefersReducedMotion || !isInView) { + videoRef.current.pause(); + return; + } + + videoRef.current.play().catch(() => {}); + }, [activeIndex, themedFile, mediaStatus, prefersReducedMotion, isInView]); + + useEffect(() => { + if (!hasStartedPlaying || !isInView || prefersReducedMotion) return undefined; + + const connection = navigator.connection + || navigator.mozConnection + || navigator.webkitConnection; + const shouldAvoidPrefetch = connection?.saveData + || ['slow-2g', '2g'].includes(connection?.effectiveType); + + if (shouldAvoidPrefetch) return undefined; + + const nextDemo = demos[(activeIndex + 1) % demos.length]; + const nextFile = colorMode === 'dark' + ? nextDemo.file.replace(/\.mp4$/, '.dark.mp4') + : nextDemo.file; + const nextVideoUrl = `${mediaRoot}${nextFile}`; + + if (prefetchedVideosRef.current.has(nextVideoUrl)) return undefined; + + const prefetchNextVideo = () => { + if (prefetchedVideosRef.current.has(nextVideoUrl)) return; + + const link = document.createElement('link'); + link.rel = 'prefetch'; + link.as = 'video'; + link.href = nextVideoUrl; + document.head.appendChild(link); + prefetchedVideosRef.current.add(nextVideoUrl); + }; + + const timeoutId = window.setTimeout(prefetchNextVideo, 1500); + return () => window.clearTimeout(timeoutId); + }, [activeIndex, colorMode, hasStartedPlaying, isInView, mediaRoot, prefersReducedMotion]); + + function selectTab(index) { + setActiveIndex(index); + } + + function handleVideoEnded() { + if (prefersReducedMotion || !isInView) return; + setActiveIndex((currentIndex) => (currentIndex + 1) % demos.length); + } + + function handleTabKeyDown(event, index) { + if (!['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) return; + + event.preventDefault(); + let nextIndex = index; + + if (event.key === 'ArrowLeft') nextIndex = (index - 1 + demos.length) % demos.length; + if (event.key === 'ArrowRight') nextIndex = (index + 1) % demos.length; + if (event.key === 'Home') nextIndex = 0; + if (event.key === 'End') nextIndex = demos.length - 1; + + selectTab(nextIndex); + tabRefs.current[nextIndex]?.focus(); + } + + return ( +
+
+

+ + Platform overview +

+ +
+ {demos.map((demo, index) => ( + + ))} +
+ +
+ + +
+
+
+
+
+ ); +} diff --git a/src/components/PlatformOverviewSection/styles.module.css b/src/components/PlatformOverviewSection/styles.module.css new file mode 100644 index 0000000..3a3565c --- /dev/null +++ b/src/components/PlatformOverviewSection/styles.module.css @@ -0,0 +1,297 @@ +.section { + position: relative; + width: 100vw; + left: 50%; + margin-left: -50vw; + background: var(--color-bg); +} + +.inner { + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 48px 80px 0; +} + +.sectionLabel { + display: flex; + align-items: center; + gap: 8px; + margin: 0 0 24px; + color: var(--color-text-muted); + font-size: 13px; + font-weight: var(--fw-regular); + letter-spacing: 0.08em; + line-height: 1; + text-transform: uppercase; +} + +.sectionLabel span, +.placeholderPrompt { + color: var(--color-primary); +} + +.tabPrompt { + color: var(--color-text-muted); +} + +.tabs { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 20px; + overflow-x: auto; + scrollbar-width: none; +} + +.tabs::-webkit-scrollbar { + display: none; +} + +.tab { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-height: 38px; + padding: 0 16px; + border: 1px solid var(--color-border); + border-radius: 0; + background: transparent; + color: var(--color-text-muted); + font-family: var(--font-base); + font-size: 14px; + font-weight: var(--fw-regular); + line-height: 1; + white-space: nowrap; + cursor: pointer; +} + +.tab:hover { + border-color: var(--color-text-muted); + color: var(--color-text); +} + +.tabActive, +.tabActive:hover { + border-color: var(--color-border); + color: var(--color-text); + box-shadow: inset 0 -2px 0 var(--color-primary); +} + +.tabActive .tabPrompt { + color: var(--color-primary); +} + +.tab:focus-visible { + outline: 2px solid var(--color-primary); + outline-offset: 3px; +} + +.browser { + overflow: hidden; + border: 1px solid var(--color-border); + border-radius: 10px 10px 0 0; + background: var(--color-bg); + box-shadow: 8px 8px 0 var(--shadow-pixel); +} + +.browserBar { + display: flex; + align-items: center; + min-height: 38px; + padding: 0 16px; + border-bottom: 1px solid var(--color-border); + background: var(--color-surface); +} + +.browserControls { + display: flex; + gap: 7px; +} + +.browserControls span { + width: 8px; + height: 8px; + border: 1px solid var(--color-text-muted); + opacity: 0.5; +} + +.browserControls span:first-child { + border-color: var(--color-primary); + background: var(--color-primary); + opacity: 1; +} + +.viewport { + position: relative; + aspect-ratio: 1440 / 762; + overflow: hidden; + background: var(--color-bg); +} + +.video { + position: absolute; + inset: 0 4px 4px; + z-index: 2; + display: block; + width: calc(100% - 8px); + height: calc(100% - 4px); + box-sizing: border-box; + background: var(--color-bg); + object-fit: contain; + opacity: 0; +} + +.videoReady { + opacity: 1; +} + +.placeholder { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + padding: 24px; + color: var(--color-text-muted); +} + +.placeholderHeader { + display: flex; + align-items: center; + gap: 8px; + padding-bottom: 16px; + border-bottom: 1px dashed var(--color-border); + font-size: 11px; + letter-spacing: 0.08em; +} + +.placeholderState { + margin-left: auto; + color: var(--color-primary); +} + +.placeholderBody { + display: grid; + grid-template-columns: 96px 1fr; + flex: 1; + min-height: 0; +} + +.mockSidebar { + display: flex; + flex-direction: column; + align-items: center; + gap: 22px; + padding: 28px 18px; + border-right: 1px solid var(--color-border); +} + +.mockSidebar span { + width: 28px; + height: 7px; + background: var(--color-border); +} + +.mockSidebar .mockActive { + background: var(--color-primary); +} + +.mockContent { + position: relative; + padding: 30px 34px; +} + +.mockTitle { + max-width: 620px; + color: var(--color-text); + font-size: clamp(16px, 2vw, 24px); + line-height: 1.35; +} + +.mockToolbar { + display: flex; + gap: 10px; + margin: 26px 0 20px; +} + +.mockToolbar span { + width: 92px; + height: 26px; + border: 1px solid var(--color-border); +} + +.mockToolbar span:first-child { + border-color: var(--color-primary); +} + +.mockGrid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 12px; +} + +.mockGrid span { + aspect-ratio: 1.65; + border: 1px solid var(--color-border); + background: var(--color-bg); +} + +.mediaHint { + position: absolute; + right: 34px; + bottom: 24px; + color: var(--color-text-muted); + font-size: 10px; + letter-spacing: 0.05em; + opacity: 0.7; +} + +@media (max-width: 1100px) { + .inner { padding: 44px 48px 0; } +} + +@media (max-width: 768px) { + .inner { padding: 36px 24px 0; } + .tabs { gap: 8px; } + .tab { min-height: 36px; padding: 0 12px; font-size: 13px; } + .placeholder { padding: 16px; } + .placeholderBody { grid-template-columns: 58px 1fr; } + .mockSidebar { gap: 16px; padding: 20px 12px; } + .mockSidebar span { width: 22px; } + .mockContent { padding: 22px; } + .mockGrid { grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; } + .mockGrid span:nth-child(n+7) { display: none; } + .mediaHint { right: 22px; bottom: 16px; } +} + +@media (max-width: 480px) { + .inner { padding: 32px 16px 0; } + .sectionLabel { margin-bottom: 18px; } + .tabs { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + overflow: visible; + } + .tab { width: 100%; padding: 0 8px; } + .browser { border-radius: 7px 7px 0 0; box-shadow: 5px 5px 0 var(--shadow-pixel); } + .browserBar { min-height: 34px; padding: 0 10px; } + .browserControls { gap: 4px; } + .browserControls span { width: 6px; height: 6px; } + .video { right: 2px; bottom: 2px; left: 2px; width: calc(100% - 4px); height: calc(100% - 2px); } + .placeholderHeader { padding-bottom: 10px; font-size: 9px; } + .placeholderBody { grid-template-columns: 42px 1fr; } + .mockSidebar { gap: 10px; padding: 14px 8px; } + .mockSidebar span { width: 18px; height: 5px; } + .mockContent { padding: 14px; } + .mockToolbar { margin: 14px 0 12px; } + .mockToolbar span { width: 54px; height: 18px; } + .mockGrid { gap: 6px; } + .mediaHint { display: none; } +} + +@media (prefers-reduced-motion: reduce) { + .video { + animation: none; + } +} diff --git a/src/pages/index.js b/src/pages/index.js index 7686c92..6a8ac7b 100644 --- a/src/pages/index.js +++ b/src/pages/index.js @@ -2,6 +2,7 @@ import React from 'react'; import Layout from '@theme/Layout'; import HeroSection from '@site/src/components/HeroSection'; import ArchDiagramSection from '@site/src/components/ArchDiagramSection'; +import PlatformOverviewSection from '@site/src/components/PlatformOverviewSection'; import ValuePropsSection from '@site/src/components/ValuePropsSection'; import ProductHighlightsSection from '@site/src/components/ProductHighlightsSection'; import CLISection from '@site/src/components/CLISection'; @@ -17,6 +18,7 @@ export default function Home() {
+ {/* */} diff --git a/static/img/autobase_create_cluster_demo.gif b/static/img/autobase_create_cluster_demo.gif deleted file mode 100644 index c98387f..0000000 Binary files a/static/img/autobase_create_cluster_demo.gif and /dev/null differ diff --git a/static/img/background.mp4 b/static/img/background.mp4 deleted file mode 100644 index 292577e..0000000 Binary files a/static/img/background.mp4 and /dev/null differ diff --git a/static/img/deploying.png b/static/img/deploying.png deleted file mode 100644 index fab98e7..0000000 Binary files a/static/img/deploying.png and /dev/null differ diff --git a/static/img/pg-console-home.png b/static/img/pg-console-home.png deleted file mode 100644 index cfb86c3..0000000 Binary files a/static/img/pg-console-home.png and /dev/null differ diff --git a/static/img/pg-console-login.png b/static/img/pg-console-login.png deleted file mode 100644 index c144e79..0000000 Binary files a/static/img/pg-console-login.png and /dev/null differ diff --git a/static/img/pg-console.png b/static/img/pg-console.png deleted file mode 100644 index 4399c04..0000000 Binary files a/static/img/pg-console.png and /dev/null differ diff --git a/static/img/postgresql_cluster.dark_mode.png b/static/img/postgresql_cluster.dark_mode.png deleted file mode 100644 index 3ff5c87..0000000 Binary files a/static/img/postgresql_cluster.dark_mode.png and /dev/null differ diff --git a/static/img/postgresql_cluster.png b/static/img/postgresql_cluster.png deleted file mode 100644 index 4b64038..0000000 Binary files a/static/img/postgresql_cluster.png and /dev/null differ diff --git a/static/video/platform-overview/README.md b/static/video/platform-overview/README.md new file mode 100644 index 0000000..cfab14a --- /dev/null +++ b/static/video/platform-overview/README.md @@ -0,0 +1,14 @@ +# Platform overview videos + +Place the locally hosted platform demo videos in this directory: + +- `clusters.mp4` +- `clusters.dark.mp4` +- `parameters.mp4` +- `parameters.dark.mp4` +- `yaml-editor.mp4` +- `yaml-editor.dark.mp4` +- `sql-editor.mp4` +- `sql-editor.dark.mp4` + +Recommended export: MP4 (H.264), muted, 1920×1080 (16:9), with `faststart` enabled for web playback. Keep the same dimensions and encoding settings for light and dark variants. diff --git a/static/video/platform-overview/clusters.dark.mp4 b/static/video/platform-overview/clusters.dark.mp4 new file mode 100644 index 0000000..000c616 Binary files /dev/null and b/static/video/platform-overview/clusters.dark.mp4 differ diff --git a/static/video/platform-overview/clusters.mp4 b/static/video/platform-overview/clusters.mp4 new file mode 100644 index 0000000..9a0e30a Binary files /dev/null and b/static/video/platform-overview/clusters.mp4 differ diff --git a/static/video/platform-overview/parameters.dark.mp4 b/static/video/platform-overview/parameters.dark.mp4 new file mode 100644 index 0000000..92d056f Binary files /dev/null and b/static/video/platform-overview/parameters.dark.mp4 differ diff --git a/static/video/platform-overview/parameters.mp4 b/static/video/platform-overview/parameters.mp4 new file mode 100644 index 0000000..b54274f Binary files /dev/null and b/static/video/platform-overview/parameters.mp4 differ diff --git a/static/video/platform-overview/sql-editor.dark.mp4 b/static/video/platform-overview/sql-editor.dark.mp4 new file mode 100644 index 0000000..34ebd90 Binary files /dev/null and b/static/video/platform-overview/sql-editor.dark.mp4 differ diff --git a/static/video/platform-overview/sql-editor.mp4 b/static/video/platform-overview/sql-editor.mp4 new file mode 100644 index 0000000..7d93206 Binary files /dev/null and b/static/video/platform-overview/sql-editor.mp4 differ diff --git a/static/video/platform-overview/yaml-editor.dark.mp4 b/static/video/platform-overview/yaml-editor.dark.mp4 new file mode 100644 index 0000000..9bb4d3c Binary files /dev/null and b/static/video/platform-overview/yaml-editor.dark.mp4 differ diff --git a/static/video/platform-overview/yaml-editor.mp4 b/static/video/platform-overview/yaml-editor.mp4 new file mode 100644 index 0000000..24955fb Binary files /dev/null and b/static/video/platform-overview/yaml-editor.mp4 differ