diff --git a/src/components/SocialProofSection/index.js b/src/components/SocialProofSection/index.js index e393474..0d34307 100644 --- a/src/components/SocialProofSection/index.js +++ b/src/components/SocialProofSection/index.js @@ -1,6 +1,171 @@ -import React from 'react'; +import React, {useCallback, useEffect, useRef, useState} from 'react'; import styles from './styles.module.css'; +const ROTATION_INTERVAL_MS = 6500; +const EXIT_DURATION_MS = 340; +const TRANSITION_DURATION_MS = 1200; +const GITHUB_REPO_URL = 'https://github.com/autobase-tech/autobase'; +const GITHUB_REPO_API = 'https://api.github.com/repos/autobase-tech/autobase'; +const GITHUB_STARS_FALLBACK = 4300; +const GITHUB_STARS_CACHE_KEY = 'autobase-github-stars'; +const GITHUB_STARS_CACHE_TTL_MS = 15 * 60 * 1000; +const EXTENSION_NAMES = ['postgis', 'vector', 'timescaledb', 'pg_partman', 'pg_cron', 'pgaudit']; +// Autobase was initially released Oct 8, 2019 (JavaScript months are zero-based). +const AUTOBASE_INITIAL_RELEASE_DATE = {year: 2019, month: 9, day: 8}; +const PRODUCTION_AGE_REFRESH_MS = 60 * 60 * 1000; + +const BANNERS = [ + { + id: 'time-to-value', + before: '1 Month of Infrastructure Work', + after: '10 Minutes in Autobase', + visual: 'time-to-value', + }, + { + id: 'production-history', + before: '', + after: 'in Production', + afterColor: 'text', + visual: 'production-history', + }, + { + id: 'open-source', + before: 'Open source forever', + after: null, + subtitleAccent: 'Trusted', + subtitle: 'by teams worldwide', + visual: 'open-source', + }, + { + id: 'extensions', + beforeAccent: '500+', + before: 'Extensions', + after: null, + subtitle: 'Go beyond vanilla PostgreSQL', + visual: 'extensions', + }, +]; + +function getRandomBannerOrder(length) { + const order = Array.from({length}, (_, index) => index); + + for (let index = order.length - 1; index > 0; index -= 1) { + const randomIndex = Math.floor(Math.random() * (index + 1)); + [order[index], order[randomIndex]] = [order[randomIndex], order[index]]; + } + + return order; +} + +function getProductionHistory(date = new Date()) { + const currentYear = date.getUTCFullYear(); + const currentMonth = date.getUTCMonth(); + const currentDay = date.getUTCDate(); + let elapsedMonths = + (currentYear - AUTOBASE_INITIAL_RELEASE_DATE.year) * 12 + + currentMonth + - AUTOBASE_INITIAL_RELEASE_DATE.month; + + if (currentDay < AUTOBASE_INITIAL_RELEASE_DATE.day) elapsedMonths -= 1; + + return { + currentYear, + years: Math.floor(elapsedMonths / 12), + months: elapsedMonths % 12, + }; +} + +function formatProductionAge({years, months}) { + const yearLabel = `${years} ${years === 1 ? 'Year' : 'Years'}`; + if (months === 0) return yearLabel; + + return `${yearLabel} · ${months} ${months === 1 ? 'Month' : 'Months'}`; +} + +function useProductionHistory() { + const [history, setHistory] = useState(() => getProductionHistory()); + + useEffect(() => { + const refreshTimer = window.setInterval(() => { + setHistory(getProductionHistory()); + }, PRODUCTION_AGE_REFRESH_MS); + + return () => window.clearInterval(refreshTimer); + }, []); + + return history; +} + +function usePrefersReducedMotion() { + const [prefersReducedMotion, setPrefersReducedMotion] = useState(false); + + useEffect(() => { + const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + const updatePreference = () => setPrefersReducedMotion(mediaQuery.matches); + + updatePreference(); + mediaQuery.addEventListener?.('change', updatePreference); + return () => mediaQuery.removeEventListener?.('change', updatePreference); + }, []); + + return prefersReducedMotion; +} + +function useGithubStars() { + const [stars, setStars] = useState({count: GITHUB_STARS_FALLBACK, isLive: false}); + + useEffect(() => { + const controller = new AbortController(); + + try { + const cachedValue = window.sessionStorage.getItem(GITHUB_STARS_CACHE_KEY); + const cached = cachedValue ? JSON.parse(cachedValue) : null; + + if ( + Number.isInteger(cached?.count) + && Date.now() - cached.timestamp < GITHUB_STARS_CACHE_TTL_MS + ) { + setStars({count: cached.count, isLive: true}); + return () => controller.abort(); + } + } catch { + // Storage can be unavailable in privacy-focused browser modes. + } + + fetch(GITHUB_REPO_API, { + headers: {Accept: 'application/vnd.github+json'}, + signal: controller.signal, + }) + .then((response) => { + if (!response.ok) throw new Error(`GitHub API responded with ${response.status}`); + return response.json(); + }) + .then((repository) => { + if (!Number.isInteger(repository.stargazers_count)) return; + + setStars({count: repository.stargazers_count, isLive: true}); + + try { + window.sessionStorage.setItem(GITHUB_STARS_CACHE_KEY, JSON.stringify({ + count: repository.stargazers_count, + timestamp: Date.now(), + })); + } catch { + // The live value still works even when it cannot be cached. + } + }) + .catch((error) => { + if (error.name !== 'AbortError') { + setStars({count: GITHUB_STARS_FALLBACK, isLive: false}); + } + }); + + return () => controller.abort(); + }, []); + + return stars; +} + function CalendarWithClockIcon() { return ( + ); + } + + if (type === 'open-source') { + const formattedStars = githubStars.count.toLocaleString('en-US'); + const displayedStars = githubStars.isLive ? formattedStars : `${formattedStars}+`; + + return ( + + + {displayedStars} + GitHub Stars + + + ); + } + + if (type === 'extensions') { + return ( + + + > EXTENSION CATALOG + + + {EXTENSION_NAMES.map((extension, index) => ( + + {extension} + + ))} + + + ); + } + return ( -
-
-

Time to value

-
- - {/* Left — text */} -
-

1 Month of Infrastructure Work

-

10 Minutes in Autobase

- -
+