From ea3a32ccaf814fc130e71ec4a93888fea1edb708 Mon Sep 17 00:00:00 2001 From: Gabriel Kaszewski Date: Fri, 29 May 2026 00:52:01 +0200 Subject: [PATCH] feat(frontend): add LandingFeatures client component with scroll animation --- .../components/landing-features.tsx | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 thoughts-frontend/components/landing-features.tsx diff --git a/thoughts-frontend/components/landing-features.tsx b/thoughts-frontend/components/landing-features.tsx new file mode 100644 index 0000000..e7e4e1a --- /dev/null +++ b/thoughts-frontend/components/landing-features.tsx @@ -0,0 +1,66 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +const FEATURES = [ + { + icon: "✍️", + title: "Say it in 128", + body: "Short, focused thoughts. No bloat, no essays.", + }, + { + icon: "🎨", + title: "Make it yours", + body: "Customize your profile with CSS. Full creative control.", + }, + { + icon: "🔒", + title: "Your audience, your rules", + body: "Public, followers-only, unlisted, or direct — you decide every time.", + }, + { + icon: "🎬", + title: "Movies Diary", + body: "Posts from your Movies Diary appear as rich cards — ratings, posters, reviews. First-class, not an afterthought.", + }, +]; + +export function LandingFeatures() { + const ref = useRef(null); + + useEffect(() => { + const cards = Array.from( + ref.current?.querySelectorAll("[data-animate]") ?? [] + ); + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + entry.target.classList.add("visible"); + observer.unobserve(entry.target); + } + }); + }, + { threshold: 0.15 } + ); + cards.forEach((card) => observer.observe(card)); + return () => observer.disconnect(); + }, []); + + return ( +
+ {FEATURES.map((f, i) => ( +
+
{f.icon}
+

{f.title}

+

{f.body}

+
+ ))} +
+ ); +}