fix: Stream scrolling effect
This commit is contained in:
parent
d1aeff6f82
commit
3b5f28f018
5 changed files with 116 additions and 77 deletions
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import React, { useEffect, useLayoutEffect, useState } from "react";
|
||||
import React, { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { RootState } from "@/store/store";
|
||||
import "../../utils/prism-languages";
|
||||
|
|
@ -34,6 +34,7 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
const [selectedSlide, setSelectedSlide] = useState(0);
|
||||
const [isFullscreen, setIsFullscreen] = useState(false);
|
||||
const [error, setError] = useState(false);
|
||||
const slidesScrollContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
|
||||
|
|
@ -41,6 +42,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
const { presentationData, isStreaming } = useSelector(
|
||||
(state: RootState) => state.presentationGeneration
|
||||
);
|
||||
const slidesLength = presentationData?.slides?.length ?? 0;
|
||||
const lastStreamingSlideIndex =
|
||||
slidesLength > 0
|
||||
? presentationData?.slides?.[slidesLength - 1]?.index
|
||||
: undefined;
|
||||
|
||||
// Auto-save functionality
|
||||
const { isSaving } = useAutoSave({
|
||||
|
|
@ -81,6 +87,39 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
|
||||
usePresentationUndoRedo();
|
||||
|
||||
useEffect(() => {
|
||||
if (!isStreaming) return;
|
||||
|
||||
const scrollContainer = slidesScrollContainerRef.current;
|
||||
if (!scrollContainer) return;
|
||||
|
||||
const frame = window.requestAnimationFrame(() => {
|
||||
if (slidesLength <= 1) {
|
||||
scrollContainer.scrollTo({ top: 0, behavior: "auto" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (lastStreamingSlideIndex === undefined) return;
|
||||
|
||||
const slideElement = document.getElementById(
|
||||
`slide-${lastStreamingSlideIndex}`
|
||||
);
|
||||
if (!slideElement) return;
|
||||
|
||||
const containerRect = scrollContainer.getBoundingClientRect();
|
||||
const slideRect = slideElement.getBoundingClientRect();
|
||||
const slideTop =
|
||||
slideRect.top - containerRect.top + scrollContainer.scrollTop;
|
||||
|
||||
scrollContainer.scrollTo({
|
||||
top: Math.max(slideTop, 0),
|
||||
behavior: "smooth",
|
||||
});
|
||||
});
|
||||
|
||||
return () => window.cancelAnimationFrame(frame);
|
||||
}, [isStreaming, lastStreamingSlideIndex, slidesLength]);
|
||||
|
||||
useEffect(() => {
|
||||
trackEvent(MixpanelEvent.Presentation_Editor_Viewed, {
|
||||
pathname,
|
||||
|
|
@ -151,8 +190,8 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
className="relative flex h-full flex-col overflow-hidden"
|
||||
>
|
||||
<PresentationHeader presentation_id={presentation_id} isPresentationSaving={isSaving} currentSlide={selectedSlide} />
|
||||
<div className="flex flex-1 min-h-0 gap-6 ">
|
||||
<div className="w-[120px] h-full shrink-0 self-start sticky top-0 mt-[18px]">
|
||||
<div className="flex flex-1 min-h-0 gap-6 overflow-hidden">
|
||||
<div className="w-[120px] h-full shrink-0 self-start sticky top-0 pt-[18px]">
|
||||
<SidePanel
|
||||
selectedSlide={selectedSlide}
|
||||
onSlideClick={handleSlideClick}
|
||||
|
|
@ -160,8 +199,11 @@ const PresentationPage: React.FC<PresentationPageProps> = ({
|
|||
loading={loading}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full min-w-0 h-full flex-1 mt-[18px]">
|
||||
<div className="font-inter h-full overflow-y-auto hide-scrollbar">
|
||||
<div className="w-full min-w-0 h-full flex-1 pt-[18px]">
|
||||
<div
|
||||
ref={slidesScrollContainerRef}
|
||||
className="font-inter h-full overflow-y-auto hide-scrollbar scroll-pt-[18px]"
|
||||
>
|
||||
<div className="w-full max-w-[1280px] min-h-full mx-auto flex flex-col items-center pb-8">
|
||||
{!presentationData ||
|
||||
loading ||
|
||||
|
|
|
|||
|
|
@ -19,11 +19,11 @@ import {
|
|||
} from "@dnd-kit/sortable";
|
||||
import { setPresentationData } from "@/store/slices/presentationGeneration";
|
||||
import { SortableSlide } from "./SortableSlide";
|
||||
import SlideScale from "../../components/PresentationRender";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { usePathname } from "next/navigation";
|
||||
import NewSlide from "./NewSlide";
|
||||
import { trackEvent, MixpanelEvent } from "@/utils/mixpanel";
|
||||
import { SlideThumbnailCard } from "./SlideThumbnailCard";
|
||||
|
||||
interface SidePanelProps {
|
||||
selectedSlide: number;
|
||||
|
|
@ -153,19 +153,13 @@ const SidePanel = ({
|
|||
{isStreaming ? (
|
||||
presentationData &&
|
||||
presentationData?.slides.map((slide: any, index: number) => (
|
||||
<div
|
||||
<SlideThumbnailCard
|
||||
key={`${slide.id}-${index}`}
|
||||
onClick={() => onSlideClick(index)}
|
||||
className={` cursor-pointer ring-2 rounded-[12px] transition-all duration-200 ${selectedSlide === index ? ' ring-[#5141e5]' : 'ring-gray-200'
|
||||
}`}
|
||||
>
|
||||
<div className=" bg-white pointer-events-none relative overflow-hidden aspect-video">
|
||||
<div className="absolute bg-gray-100/5 z-50 top-0 left-0 w-full h-full" />
|
||||
<div className="transform scale-[0.2] flex justify-center items-center origin-top-left w-[500%] h-[500%]">
|
||||
<SlideScale slide={slide} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
slide={slide}
|
||||
index={index}
|
||||
selected={selectedSlide === index}
|
||||
onClick={() => onSlideClick(slide.index ?? index)}
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<SortableContext
|
||||
|
|
|
|||
|
|
@ -103,30 +103,6 @@ const SlideContent = ({ slide, index, presentationId }: SlideContentProps) => {
|
|||
});
|
||||
}
|
||||
};
|
||||
// Scroll to the new slide when streaming and new slides are being generated
|
||||
useEffect(() => {
|
||||
if (
|
||||
presentationData &&
|
||||
presentationData?.slides &&
|
||||
presentationData.slides.length > 1 &&
|
||||
isStreaming
|
||||
) {
|
||||
// Scroll to the last slide (newly generated during streaming)
|
||||
const lastSlideIndex = presentationData.slides.length - 1;
|
||||
const slideElement = document.getElementById(
|
||||
`slide-${presentationData.slides[lastSlideIndex].index}`
|
||||
);
|
||||
if (slideElement) {
|
||||
slideElement.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "center",
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [presentationData?.slides?.length, isStreaming]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
if (slide.layout.includes("custom")) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import type { Slide } from "../../types/slide";
|
||||
import { V1ContentRender } from "../../components/V1ContentRender";
|
||||
|
||||
interface SlideThumbnailCardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
slide: Slide;
|
||||
index: number;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
const SCALE = 0.061;
|
||||
|
||||
export const SlideThumbnailCard = forwardRef<
|
||||
HTMLDivElement,
|
||||
SlideThumbnailCardProps
|
||||
>(({ slide, index, selected, className = "", style, ...props }, ref) => {
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={{
|
||||
backgroundColor: "var(--card-color, #ffffff)",
|
||||
borderColor: selected ? "#5141e5" : "var(--stroke, #e5e7eb)",
|
||||
...style,
|
||||
}}
|
||||
className={`cursor-pointer border relative p-1.5 rounded-[12px] overflow-hidden transition-all duration-200 ${
|
||||
selected ? "border-[#BDB4FE]" : "border-[#EDEEEF]"
|
||||
} ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<p className="pointer-events-none absolute -left-1 top-1/2 z-50 flex h-[18px] min-w-[18px] -translate-y-1/2 items-center justify-center rounded-full border border-[#EDEEEF] bg-white px-1 text-[10px] font-medium text-[#191919] shadow-sm">
|
||||
{index + 1}
|
||||
</p>
|
||||
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: `${720 * SCALE}px`, overflow: "hidden" }}
|
||||
>
|
||||
<div
|
||||
className="absolute top-0 left-0 rounded-[10px] overflow-hidden pointer-events-none"
|
||||
style={{
|
||||
width: 1280,
|
||||
height: 720,
|
||||
transformOrigin: "top left",
|
||||
transform: `scale(${SCALE})`,
|
||||
}}
|
||||
>
|
||||
<V1ContentRender slide={slide} isEditMode={true} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
SlideThumbnailCard.displayName = "SlideThumbnailCard";
|
||||
|
|
@ -1,15 +1,14 @@
|
|||
import { useSortable } from '@dnd-kit/sortable';
|
||||
import { CSS } from '@dnd-kit/utilities';
|
||||
import { Slide } from '../../types/slide';
|
||||
import type { Slide } from '../../types/slide';
|
||||
import { useRef } from 'react';
|
||||
import { V1ContentRender } from '../../components/V1ContentRender';
|
||||
import { SlideThumbnailCard } from './SlideThumbnailCard';
|
||||
interface SortableSlideProps {
|
||||
slide: Slide;
|
||||
index: number;
|
||||
selectedSlide: number;
|
||||
onSlideClick: (index: any) => void;
|
||||
}
|
||||
const SCALE = 0.0625;
|
||||
|
||||
export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: SortableSlideProps) {
|
||||
const lastClickTime = useRef(0);
|
||||
|
|
@ -26,8 +25,6 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
|
|||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
opacity: isDragging ? 0.5 : 1,
|
||||
backgroundColor: `var(--card-color, #ffffff)`,
|
||||
borderColor: selectedSlide === index ? `#5141e5` : `var(--stroke, #e5e7eb)`
|
||||
};
|
||||
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
|
|
@ -46,39 +43,15 @@ export function SortableSlide({ slide, index, selectedSlide, onSlideClick }: Sor
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
<SlideThumbnailCard
|
||||
ref={setNodeRef}
|
||||
slide={slide}
|
||||
index={index}
|
||||
selected={selectedSlide === index}
|
||||
style={style}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
onClick={handleClick}
|
||||
className={` cursor-pointer border relative p-1 rounded-[12px] transition-all duration-200 ${selectedSlide === index ? ' border-[#BDB4FE]' : 'border-[#EDEEEF]'
|
||||
}`}
|
||||
>
|
||||
<p className='absolute top-1/2 translate-y-1/2 -left-3 bg-white border border-[#EDEEEF] rounded-[40px] text-[#191919] text-[10px] font-medium px-1 z-50
|
||||
'>
|
||||
{index + 1}
|
||||
</p>
|
||||
|
||||
|
||||
<div
|
||||
className="relative"
|
||||
style={{ height: `${720 * SCALE}px`, overflow: "hidden" }}
|
||||
>
|
||||
|
||||
<div
|
||||
className="absolute top-0 left-0 pointer-events-none"
|
||||
style={{
|
||||
width: 1280,
|
||||
height: 720,
|
||||
transformOrigin: "top left",
|
||||
transform: `scale(${SCALE})`,
|
||||
}}
|
||||
>
|
||||
<V1ContentRender slide={slide} isEditMode={true} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue