Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 31 additions & 13 deletions components/DashboardShowcase.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export default function DashboardShowcase() {
// chosen slide its full dwell time instead of flipping a moment later.
const [cycle, setCycle] = useState(0)
const count = SLIDES.length
const nextIndex = (active + 1) % count

// The showcase auto-advances for everyone, including visitors with
// "reduce motion" enabled. This is a deliberate product decision (matching
Expand Down Expand Up @@ -218,7 +219,12 @@ export default function DashboardShowcase() {
data-testid="dashboard-stage"
ref={stageRef}
>
{SLIDES.map((slide, index) => (
{SLIDES.map((slide, index) => ({ slide, index }))
.filter(
({ index }) =>
index === active || index === nextIndex
)
.map(({ slide, index }) => (
<img
key={slide.key}
ref={(node) => {
Expand All @@ -229,7 +235,8 @@ export default function DashboardShowcase() {
width={slide.width}
height={slide.height}
alt={slide.alt}
loading="lazy"
loading={index === active ? 'eager' : 'lazy'}
fetchPriority={index === active ? 'high' : 'low'}
decoding="async"
aria-hidden={index !== active}
data-testid="dashboard-slide"
Expand All @@ -241,7 +248,7 @@ export default function DashboardShowcase() {
opacity: index === active ? 1 : 0,
}}
/>
))}
))}
</div>
</div>

Expand Down Expand Up @@ -277,16 +284,27 @@ export default function DashboardShowcase() {
onClick={() => jumpTo(index)}
>
<span className={styles.thumb}>
<img
className={styles.thumbImg}
src={slide.src}
width={slide.width}
height={slide.height}
alt=""
loading="lazy"
decoding="async"
aria-hidden="true"
/>
{index === active || index === nextIndex ? (
<img
className={styles.thumbImg}
src={slide.src}
width={slide.width}
height={slide.height}
alt=""
loading={
index === active ? 'eager' : 'lazy'
}
decoding="async"
aria-hidden="true"
/>
) : (
<span
className={styles.thumbPlaceholder}
aria-hidden="true"
>
{index + 1}
</span>
)}
{index === active && (
<span
key={`${active}-${cycle}-${paused}`}
Expand Down
13 changes: 12 additions & 1 deletion components/DashboardShowcase.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,22 @@
and carries a countdown bar. */
.tabs {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin: 18px 0 0;
}

.thumbPlaceholder {
display: grid;
width: 100%;
height: 100%;
place-items: center;
background: linear-gradient(135deg, var(--ground), var(--panel));
color: var(--ink-3);
font-family: var(--font-mono);
font-size: 12px;
}

.tab {
display: flex;
flex-direction: column;
Expand Down
65 changes: 7 additions & 58 deletions components/Footer.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React from 'react'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import {
faGithub,
Expand All @@ -12,7 +12,6 @@ import {
OPENADAPT_STATS_SNAPSHOT,
} from 'data/repositoryStats'
import { BLOG_LINK, DEVELOPER_LINKS } from 'data/developerLinks'
import useRepositoryStats from 'hooks/useRepositoryStats'
import { track, EVENTS } from 'utils/analytics'
import repositoryStatsView from 'utils/repositoryStatsView'
import repositoryStatsSelection from 'utils/repositoryStatsSelection'
Expand Down Expand Up @@ -57,54 +56,10 @@ function ForkOcticon() {
)
}

function usePrefersReducedMotion() {
const [reduced, setReduced] = useState(false)
useEffect(() => {
if (typeof window === 'undefined' || !window.matchMedia) return
const media = window.matchMedia('(prefers-reduced-motion: reduce)')
const update = () => setReduced(media.matches)
update()
media.addEventListener?.('change', update)
return () => media.removeEventListener?.('change', update)
}, [])
return reduced
}

// The honest, live-ticking "GitHub · updated Ns ago" line. The relative time
// is recomputed locally (no network) on a gentle cadence: 1s normally so the
// seconds counter reads true, or 30s under prefers-reduced-motion so there is
// no visible per-second animation. The tick pauses while the tab is hidden and
// snaps to the current time when it returns. `suppressHydrationWarning` is the
// documented escape hatch for timestamps whose server and client renders may
// legitimately differ by a second.
// Repository counts refresh at build/ISR time. A visitor does not need a
// repeating timer or a client-side fetch for a value that changes over hours.
function RepositorySource({ stats }) {
const reducedMotion = usePrefersReducedMotion()
const [now, setNow] = useState(() => Date.now())

useEffect(() => {
const cadence = reducedMotion ? 30 * 1000 : 1000
let timer = null
const tick = () => setNow(Date.now())
const start = () => {
if (document.visibilityState === 'hidden') return
timer = setInterval(tick, cadence)
}
const onVisibility = () => {
clearInterval(timer)
if (document.visibilityState === 'visible') {
tick()
start()
}
}
start()
document.addEventListener('visibilitychange', onVisibility)
return () => {
clearInterval(timer)
document.removeEventListener('visibilitychange', onVisibility)
}
}, [reducedMotion])

const label = sourceLabel(stats, now)
const label = sourceLabel(stats, Date.now())
return (
<p
className={styles.repositorySource}
Expand Down Expand Up @@ -176,18 +131,12 @@ const CONNECT_COLUMN = [

export default function Footer({
repositoryStats = OPENADAPT_STATS_SNAPSHOT,
pollRepositoryStats = true,
}) {
const currentYear = new Date().getFullYear()
const qualificationHref = '/qualify'
const polledStats = useRepositoryStats(repositoryStats, {
enabled: pollRepositoryStats,
})
const stats = pollRepositoryStats
? polledStats
: validStats(repositoryStats)
? repositoryStats
: OPENADAPT_STATS_SNAPSHOT
const stats = validStats(repositoryStats)
? repositoryStats
: OPENADAPT_STATS_SNAPSHOT

return (
<div className={styles.footerContainer}>
Expand Down
17 changes: 14 additions & 3 deletions components/WorkflowQualificationForm.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import Link from 'next/link'

import {
QUALIFICATION_TIERS,
buildQualificationSalesTask,
scoreWorkflowQualification,
} from '../lib/workflowQualification.mjs'
import { EVENTS, track } from '../utils/analytics'
Expand Down Expand Up @@ -87,13 +88,20 @@ export default function WorkflowQualificationForm({ compact = false }) {
setError('')

const qualification = scoreWorkflowQualification(form)
const salesTask = buildQualificationSalesTask({
id: `qualification_${globalThis.crypto.randomUUID()}`,
qualification,
sourceRoute: compact ? '/' : '/qualify',
submittedAt: new Date().toISOString(),
})
const data = new URLSearchParams()
data.set('form-name', 'workflow-qualification')
for (const [key, value] of Object.entries(form)) {
data.set(key === 'botField' ? 'bot-field' : key, value)
}
data.set('qualificationScore', String(qualification.score))
data.set('qualificationTier', qualification.tier)
for (const [key, value] of Object.entries(salesTask)) {
data.set(key, value)
}

try {
const response = await fetch('/form.html', {
Expand Down Expand Up @@ -126,7 +134,7 @@ export default function WorkflowQualificationForm({ compact = false }) {
: 'qualification_form',
tier: qualification.tier,
})
setResult(qualification)
setResult({ ...qualification, salesTask })
setState('submitted')
} catch (submitError) {
console.error(submitError)
Expand All @@ -147,6 +155,9 @@ export default function WorkflowQualificationForm({ compact = false }) {
<p className="mt-3 max-w-2xl text-sm leading-relaxed text-ink-2">
{next.message}
</p>
<p className="mt-3 font-mono text-xs text-ink-3">
Reference: {result.salesTask.salesTaskId}
</p>
<div className="mt-6">
{external ? (
<a
Expand Down
21 changes: 14 additions & 7 deletions cypress/e2e/dashboard-showcase.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,23 @@ describe('Cloud product showcase', () => {
cy.get('#cloud-product').scrollIntoView().should('be.visible')

cy.get('[data-testid="dashboard-product-preview"]').within(() => {
// Keep only the active capture and its successor in the DOM. This
// preserves the automatic transition without loading every large
// dashboard capture during the first visit.
cy.get('[data-testid="dashboard-slide"]').should('have.length', 2)
cy.get('[data-testid="dashboard-slide"][data-active="true"]')
.should('have.length', 1)
.and('have.attr', 'loading', 'eager')
cy.get(
'[data-testid="dashboard-slide"]:not([data-active="true"])'
)
.should('have.length', 1)
.and('have.attr', 'loading', 'lazy')

// Every discovered tab must activate its matching, decoded slide.
cy.get('[data-testid="dashboard-slide"]').each(($img) => {
cy.wrap($img).should('have.attr', 'loading', 'lazy')
})
cy.get('[data-testid="dashboard-tab"]')
.should('have.length.at.least', 1)
.then(($tabs) => {
cy.get('[data-testid="dashboard-slide"]').should(
'have.length',
$tabs.length
)
cy.get('[data-testid="dashboard-dots"] button').should(
'have.length',
$tabs.length
Expand All @@ -28,6 +34,7 @@ describe('Cloud product showcase', () => {
cy.get('[data-testid="dashboard-slide"][data-active="true"]')
.should('have.length', 1)
.and('have.attr', 'data-slide', slide)
.and('have.attr', 'loading', 'eager')
.should(($img) =>
expect($img[0].naturalWidth).to.be.greaterThan(0)
)
Expand Down
Loading
Loading