demo / src /scrollingTranscript /components /ScrollingSections.tsx
kevinloffler
initial commit
e6a9f90
import React, { forwardRef, useImperativeHandle, useState, useEffect, useRef } from 'react';
import { Section } from '../../types/Conversation';
import SectionItem from './SectionItem';
export interface ScrollingSectionsProps {
sections: Section[];
onSectionChange?: (index: number) => void;
displayProp: string;
showProgress?: boolean;
displayBeforeAndAfter?: boolean;
startAtTop?: boolean;
onComplete?: () => void;
}
export interface ScrollingSectionsRef {
play: () => void;
pause: () => void;
goToSection: (index: number) => void;
}
const ScrollingSections = forwardRef<ScrollingSectionsRef, ScrollingSectionsProps>(
({ sections, onSectionChange, displayProp, showProgress = true, displayBeforeAndAfter = true, startAtTop = false, onComplete }, ref) => {
const [activeIndex, setActiveIndex] = useState(0);
const [isPlaying, setIsPlaying] = useState(false);
const [progress, setProgress] = useState(0);
const progressIntervalRef = useRef<number | null>(null);
const sectionTimeoutRef = useRef<number | null>(null);
const [elapsedTime, setElapsedTime] = useState(0); // in milliseconds
const startProgress = () => {
if (!isPlaying) return;
const currentDuration = sections[activeIndex]?.duration || 10;
const totalDurationMs = currentDuration * 1000;
const startTime = Date.now() - elapsedTime;
if (progressIntervalRef.current) {
window.clearInterval(progressIntervalRef.current);
}
progressIntervalRef.current = window.setInterval(() => {
const now = Date.now();
const newElapsed = now - startTime;
const newProgress = (newElapsed / totalDurationMs) * 100;
if (newProgress >= 100) {
window.clearInterval(progressIntervalRef.current!);
setProgress(100);
setElapsedTime(0);
goToNextSection();
} else {
setProgress(newProgress);
setElapsedTime(newElapsed);
}
}, 100);
};
const goToNextSection = () => {
setActiveIndex((prev) => (prev + 1) % sections.length);
onSectionChange?.((activeIndex + 1) % sections.length);
if ((activeIndex + 1) % sections.length === 0) {
pause();
onComplete?.();
}
};
const goToSection = (index: number) => {
if (index >= 0 && index < sections.length) {
setActiveIndex(index);
setProgress(0);
setElapsedTime(0); // reset elapsed time
onSectionChange?.(index);
}
};
const play = () => {
setIsPlaying(true);
};
const pause = () => {
setIsPlaying(false);
if (progressIntervalRef.current) {
window.clearInterval(progressIntervalRef.current);
}
if (sectionTimeoutRef.current) {
window.clearTimeout(sectionTimeoutRef.current);
}
};
useImperativeHandle(ref, () => ({
play,
pause,
goToSection
}));
useEffect(() => {
if (isPlaying) {
startProgress();
}
return () => {
if (progressIntervalRef.current) {
window.clearInterval(progressIntervalRef.current);
}
if (sectionTimeoutRef.current) {
window.clearTimeout(sectionTimeoutRef.current);
}
};
}, [activeIndex, isPlaying]);
const getSectionPosition = (index: number) => {
if (index === activeIndex) return 'active';
if (index === activeIndex - 1 && displayBeforeAndAfter) return 'before';
if (index === (activeIndex + 1) % sections.length && displayBeforeAndAfter) return 'after';
return null;
};
if (!sections || sections.length === 0) {
return <div style={{ textAlign: 'center', padding: '1.5rem' }}>No sections available</div>;
}
return (
<div className="sections-container">
<div className={startAtTop ? 'sections-viewport' : 'sections-viewport topmargin'}>
{sections.map((section, index) => {
const position = getSectionPosition(index);
if (position) {
return (
<SectionItem
key={index}
section={section}
position={position}
displayProp={displayProp}
/>
);
}
return null;
})}
</div>
{showProgress && (
<div className="progress-container">
<div
className="progress-bar"
style={{ width: `${progress}%` }}
/>
</div>
)}
</div>
);
}
);
export default ScrollingSections;