diff --git a/surfsense_web/app/(home)/page.tsx b/surfsense_web/app/(home)/page.tsx index b5a726423..e0478fce3 100644 --- a/surfsense_web/app/(home)/page.tsx +++ b/surfsense_web/app/(home)/page.tsx @@ -5,13 +5,11 @@ import { FeaturesBentoGrid } from "@/components/homepage/features-bento-grid"; import { FeaturesCards } from "@/components/homepage/features-card"; import { HeroSection } from "@/components/homepage/hero-section"; import ExternalIntegrations from "@/components/homepage/integrations"; -import { UseCasesGrid } from "@/components/homepage/use-cases-grid"; export default function HomePage() { return (
- diff --git a/surfsense_web/components/ui/expanded-gif-overlay.tsx b/surfsense_web/components/ui/expanded-gif-overlay.tsx index 56f343ded..49b8190ff 100644 --- a/surfsense_web/components/ui/expanded-gif-overlay.tsx +++ b/surfsense_web/components/ui/expanded-gif-overlay.tsx @@ -36,8 +36,7 @@ function ExpandedGifOverlay({ transition={{ duration: 0.25, ease: "easeOut" }} src={src} alt={alt} - className="max-h-[90vh] max-w-[90vw] rounded-2xl shadow-2xl" - onClick={(e) => e.stopPropagation()} + className="max-h-[90vh] max-w-[90vw] cursor-pointer rounded-2xl shadow-2xl" /> ); diff --git a/surfsense_web/components/ui/walkthrough-scroll.tsx b/surfsense_web/components/ui/walkthrough-scroll.tsx index c14aa968f..560e4dde5 100644 --- a/surfsense_web/components/ui/walkthrough-scroll.tsx +++ b/surfsense_web/components/ui/walkthrough-scroll.tsx @@ -1,87 +1,155 @@ "use client"; -import { AnimatePresence, motion, useScroll, useTransform } from "motion/react"; -import { useRef } from "react"; +import { AnimatePresence, motion } from "motion/react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay"; -const walkthroughSteps = [ +const carouselItems = [ { - step: 1, - title: "Login", - description: "Login to get started.", - src: "/homepage/hero_tutorial/LoginFlowGif.gif", - }, - { - step: 2, title: "Connect & Sync", - description: "Connect your connectors and sync. Enable periodic syncing to keep them updated.", + description: + "Connect data sources like Notion, Drive and Gmail. Enable periodic syncing to keep them updated.", src: "/homepage/hero_tutorial/ConnectorFlowGif.gif", }, { - step: 3, title: "Upload Documents", - description: "While connectors index, upload your documents directly.", + description: "Or upload your documents directly, including iamges and massive PDFs.", src: "/homepage/hero_tutorial/DocUploadGif.gif", }, + { + title: "Search & Citation", + description: + "Ask questions and get Perplexity-style cited responses from your knowledge base.", + src: "/homepage/hero_tutorial/BSNCGif.gif", + }, + { + title: "Document Mention Q&A", + description: "Mention specific documents in your queries for targeted answers.", + src: "/homepage/hero_tutorial/BQnaGif_compressed.gif", + }, + { + title: "Report Generation", + description: "Generate and export reports in many formats.", + src: "/homepage/hero_tutorial/ReportGenGif_compressed.gif", + }, + { + title: "Podcast Generation", + description: "Turn your knowledge into podcasts in under 20 seconds.", + src: "/homepage/hero_tutorial/PodcastGenGif.gif", + }, + { + title: "Image Generation", + description: "Generate images directly from your conversations.", + src: "/homepage/hero_tutorial/ImageGenGif.gif", + }, + { + title: "Realtime Chat", + description: "Chat together in realtime with your team.", + src: "/homepage/hero_realtime/RealTimeChatGif.gif", + }, + { + title: "Realtime Comments", + description: "Add comments and tag teammates on any message.", + src: "/homepage/hero_realtime/RealTimeCommentsFlow.gif", + }, ]; function WalkthroughCard({ - i, - step, + index, title, description, src, - progress, - range, - targetScale, + isActive, + onExpandedChange, }: { - i: number; - step: number; + index: number; title: string; description: string; src: string; - progress: ReturnType["scrollYProgress"]; - range: [number, number]; - targetScale: number; + isActive: boolean; + onExpandedChange?: (expanded: boolean) => void; }) { - const container = useRef(null); - const scale = useTransform(progress, range, [1, targetScale]); const { expanded, open, close } = useExpandedGif(); + useEffect(() => { + onExpandedChange?.(expanded); + }, [expanded, onExpandedChange]); + const imgRef = useRef(null); + const [frozenFrame, setFrozenFrame] = useState(null); + const [playKey, setPlayKey] = useState(0); + + const captureFrame = useCallback((img: HTMLImageElement) => { + try { + const canvas = document.createElement("canvas"); + canvas.width = img.naturalWidth; + canvas.height = img.naturalHeight; + canvas.getContext("2d")?.drawImage(img, 0, 0); + setFrozenFrame(canvas.toDataURL()); + } catch { + /* cross-origin or other issue */ + } + }, []); + + useEffect(() => { + if (isActive) { + setPlayKey((k) => k + 1); + setFrozenFrame(null); + } else { + const img = imgRef.current; + if (img && img.complete && img.naturalWidth > 0) { + captureFrame(img); + } + } + }, [isActive, captureFrame]); + + useEffect(() => { + if (!isActive && !frozenFrame) { + const img = new Image(); + img.onload = () => captureFrame(img); + img.src = src; + } + }, [isActive, frozenFrame, src, captureFrame]); + return ( <> -
- +
+ + {index + 1} + +
+

+ {title} +

+

+ {description} +

+
+
+
-
- - {step} - -
-

- {title} -

-

- {description} -

-
-
-
- {title} -
- + {isActive ? ( + {title} + ) : frozenFrame ? ( + {title} + ) : ( +
+ )} +
@@ -92,30 +160,145 @@ function WalkthroughCard({ } function WalkthroughScroll() { - const container = useRef(null); - const { scrollYProgress } = useScroll({ - target: container, - offset: ["start start", "end end"], - }); + const [activeIndex, setActiveIndex] = useState(0); + const [isPaused, setIsPaused] = useState(false); + const [isGifExpanded, setIsGifExpanded] = useState(false); + const [containerWidth, setContainerWidth] = useState(0); + const [cardHeight, setCardHeight] = useState(420); + const containerRef = useRef(null); + const activeCardRef = useRef(null); + + useEffect(() => { + const el = containerRef.current; + if (!el) return; + const update = () => setContainerWidth(el.offsetWidth); + update(); + const observer = new ResizeObserver(update); + observer.observe(el); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const el = activeCardRef.current; + if (!el) return; + const update = () => setCardHeight(el.offsetHeight); + update(); + const observer = new ResizeObserver(update); + observer.observe(el); + return () => observer.disconnect(); + }, [activeIndex, containerWidth]); + + useEffect(() => { + if (isPaused || isGifExpanded) return; + const timer = setTimeout(() => { + setActiveIndex((prev) => (prev >= carouselItems.length - 1 ? 0 : prev + 1)); + }, 8000); + return () => clearTimeout(timer); + }, [activeIndex, isPaused, isGifExpanded]); + + const cardWidth = + containerWidth < 640 + ? containerWidth * 0.85 + : containerWidth < 1024 + ? Math.min(containerWidth * 0.7, 680) + : Math.min(containerWidth * 0.55, 900); + + const baseOffset = + containerWidth < 640 + ? containerWidth * 0.2 + : containerWidth < 1024 + ? containerWidth * 0.15 + : 150; + + const stackGap = containerWidth < 640 ? 35 : containerWidth < 1024 ? 45 : 55; + const perspective = containerWidth < 640 ? 800 : containerWidth < 1024 ? 1000 : 1200; + + const getCardStyle = useCallback( + (index: number) => { + const diff = index - activeIndex; + + if (diff === 0) { + return { x: -cardWidth / 2, rotateY: 0, zIndex: 20, originX: 1 }; + } + + const dist = Math.abs(diff); + const isLeft = diff < 0; + const offset = baseOffset + (dist - 1) * stackGap; + + return { + x: -cardWidth / 2 + (isLeft ? -offset : offset), + rotateY: isLeft ? 90 : -90, + zIndex: 20 - dist, + originX: isLeft ? 0 : 1, + }; + }, + [activeIndex, cardWidth, baseOffset, stackGap], + ); return ( -
- {walkthroughSteps.map((project, i) => { - const targetScale = Math.max(0.6, 1 - (walkthroughSteps.length - i - 1) * 0.05); - return ( - +
setIsPaused(true)} + onMouseLeave={() => setIsPaused(false)} + onTouchStart={() => setIsPaused(true)} + onTouchEnd={() => setIsPaused(false)} + > +
+ {containerWidth > 0 && + carouselItems.map((item, i) => { + const style = getCardStyle(i); + return ( + + + + ); + })} +
+
+ +
+ {carouselItems.map((_, i) => ( +
); }