The source code for our eny.social landing page, which is mirrored in a different repository as part of the CI setup. eny.social
social-network eny local-first
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(animation): aadd staggered fade-in animations for stage content

Sam Sauer 75d28e6e a33e77d3

+215 -89
+10 -4
app/components/FooterQuote.tsx
··· 1 + "use client"; 2 + 3 + import FadeIn from "./ui/FadeIn"; 4 + 1 5 export default function FooterQuote() { 2 6 return ( 3 7 <section className="px-6 py-24"> 4 8 <div className="mx-auto max-w-4xl text-center"> 5 - <blockquote className="quote"> 6 - &ldquo;Social media should be public infrastructure. Like roads, libraries or public 7 - spaces. Open, transparent and built for the people who use it.&rdquo; 8 - </blockquote> 9 + <FadeIn> 10 + <blockquote className="quote"> 11 + &ldquo;Social media should be public infrastructure. Like roads, libraries or public 12 + spaces. Open, transparent and built for the people who use it.&rdquo; 13 + </blockquote> 14 + </FadeIn> 9 15 </div> 10 16 </section> 11 17 );
+1
app/components/Hero.tsx
··· 3 3 import { useState, useEffect } from "react"; 4 4 import NavLink from "./ui/NavLink"; 5 5 import GrainedBlob from "./GrainedBlob"; 6 + import FadeIn from "./ui/FadeIn"; 6 7 7 8 const slides = [ 8 9 { word: "people", image: "/images/pexels-kindelmedia-7148409 1.png" },
+33 -23
app/components/RememberWhen.tsx
··· 1 + "use client"; 2 + 1 3 import SectionIntroLabel from "./ui/SectionIntroLabel"; 4 + import FadeIn from "./ui/FadeIn"; 2 5 3 6 export default function RememberWhen() { 4 7 return ( ··· 31 34 </svg> 32 35 33 36 <div className="relative mx-auto max-w-4xl text-center"> 34 - <SectionIntroLabel>Remember when</SectionIntroLabel> 37 + <FadeIn> 38 + <SectionIntroLabel>Remember when</SectionIntroLabel> 39 + </FadeIn> 35 40 36 - <h2 className="mt-6"> 37 - Social media used to be{" "} 38 - <span className="italic text-tangerine">fun?</span> 39 - </h2> 41 + <FadeIn delay={100}> 42 + <h2 className="mt-6"> 43 + Social media used to be{" "} 44 + <span className="italic text-tangerine">fun?</span> 45 + </h2> 46 + </FadeIn> 40 47 41 48 {/* Three masked photos */} 42 49 <div className="mt-12 flex items-center justify-center gap-6 md:gap-10"> ··· 57 64 position: "top" as const, 58 65 }, 59 66 ].map((item, i) => ( 60 - <div 61 - key={i} 62 - className="relative w-[197px] h-[225px] md:w-[263px] md:h-[300px] overflow-hidden" 63 - style={{ clipPath: `url(#${item.mask})` }} 64 - > 65 - <img 66 - src={item.src} 67 - alt="" 68 - className="h-full w-full object-cover" 69 - style={{ objectPosition: item.position }} 70 - /> 71 - </div> 67 + <FadeIn key={i} delay={200 + i * 150}> 68 + <div 69 + className="relative w-[197px] h-[225px] md:w-[263px] md:h-[300px] overflow-hidden" 70 + style={{ clipPath: `url(#${item.mask})` }} 71 + > 72 + <img 73 + src={item.src} 74 + alt="" 75 + className="h-full w-full object-cover" 76 + style={{ objectPosition: item.position }} 77 + /> 78 + </div> 79 + </FadeIn> 72 80 ))} 73 81 </div> 74 82 75 - <p className="section-copy mx-auto mt-12 max-w-2xl"> 76 - Finding new people. Discovering communities that get you. Tools that 77 - actually help. Somewhere along the way, that got buried under ads, 78 - algorithms, and engagement traps.{" "} 79 - <strong>We&apos;re building it back.</strong> 80 - </p> 83 + <FadeIn delay={650}> 84 + <p className="section-copy mx-auto mt-12 max-w-2xl"> 85 + Finding new people. Discovering communities that get you. Tools that 86 + actually help. Somewhere along the way, that got buried under ads, 87 + algorithms, and engagement traps.{" "} 88 + <strong>We&apos;re building it back.</strong> 89 + </p> 90 + </FadeIn> 81 91 </div> 82 92 83 93 {/* Decorative curved line */}
+22 -13
app/components/StartingInOffenbach.tsx
··· 2 2 3 3 import SectionIntroLabel from "./ui/SectionIntroLabel"; 4 4 import ButtonCta from "./ui/ButtonCta"; 5 + import FadeIn from "./ui/FadeIn"; 5 6 6 7 export default function StartingInOffenbach() { 7 8 return ( ··· 19 20 </svg> 20 21 21 22 <div className="relative mx-auto max-w-4xl text-center"> 22 - <SectionIntroLabel>Starting in Offenbach</SectionIntroLabel> 23 + <FadeIn> 24 + <SectionIntroLabel>Starting in Offenbach</SectionIntroLabel> 25 + </FadeIn> 23 26 24 - <h2 className="mt-6"> 25 - Your city. Your community.{" "} 26 - <span className="italic text-tangerine">One app.</span> 27 - </h2> 27 + <FadeIn delay={100}> 28 + <h2 className="mt-6"> 29 + Your city. Your community.{" "} 30 + <span className="italic text-tangerine">One app.</span> 31 + </h2> 32 + </FadeIn> 28 33 29 - <p className="section-copy mx-auto mt-8 max-w-2xl"> 30 - Find your neighbors, discover local events, navigate city services 31 - from finding an apartment to figuring out trash collection day. 32 - Everything Offenbach, in one place. 33 - </p> 34 + <FadeIn delay={200}> 35 + <p className="section-copy mx-auto mt-8 max-w-2xl"> 36 + Find your neighbors, discover local events, navigate city services 37 + from finding an apartment to figuring out trash collection day. 38 + Everything Offenbach, in one place. 39 + </p> 40 + </FadeIn> 34 41 35 - <ButtonCta href="/offenbach" variant="ghost" className="mt-10"> 36 - discover Offenbach 37 - </ButtonCta> 42 + <FadeIn delay={300}> 43 + <ButtonCta href="/offenbach" variant="ghost" className="mt-10"> 44 + discover Offenbach 45 + </ButtonCta> 46 + </FadeIn> 38 47 </div> 39 48 </section> 40 49 );
+38 -7
app/components/Waitlist.tsx
··· 1 1 "use client"; 2 2 3 + import { useRef, useState, useEffect } from "react"; 3 4 import ButtonCta from "./ui/ButtonCta"; 4 5 import SectionIntroLabel from "./ui/SectionIntroLabel"; 5 6 ··· 50 51 ]; 51 52 52 53 export default function Waitlist() { 54 + const sectionRef = useRef<HTMLElement>(null); 55 + const [isVisible, setIsVisible] = useState(false); 56 + 57 + useEffect(() => { 58 + const el = sectionRef.current; 59 + if (!el) return; 60 + const observer = new IntersectionObserver( 61 + ([entry]) => { 62 + if (entry.isIntersecting) { 63 + setIsVisible(true); 64 + observer.disconnect(); 65 + } 66 + }, 67 + { threshold: 0.2 } 68 + ); 69 + observer.observe(el); 70 + return () => observer.disconnect(); 71 + }, []); 72 + 53 73 return ( 54 - <section id="waitlist" className="relative overflow-hidden px-6 py-32"> 74 + <section 75 + ref={sectionRef} 76 + id="waitlist" 77 + className="relative overflow-hidden px-6 py-32" 78 + > 55 79 {/* Floating avatars */} 56 80 {avatars.map((avatar, i) => ( 57 81 <div 58 82 key={i} 59 - className={`absolute rounded-full ${colors[i]} opacity-40 lg:opacity-90`} 83 + className={`absolute rounded-full ${colors[i]} transition-all duration-700 ease-out ${ 84 + isVisible ? "opacity-40 lg:opacity-90 scale-100" : "opacity-0 scale-0" 85 + }`} 60 86 style={{ 61 87 top: avatar.top, 62 88 left: "left" in avatar ? avatar.left : undefined, 63 89 right: "right" in avatar ? avatar.right : undefined, 64 90 width: avatar.size, 65 91 height: avatar.size, 66 - animation: `${i % 2 === 0 ? "float" : "float-slow"} ${ 67 - 6 + i 68 - }s ease-in-out infinite`, 92 + transitionDelay: isVisible ? `${i * 120}ms` : "0ms", 93 + animation: isVisible 94 + ? `${i % 2 === 0 ? "float" : "float-slow"} ${6 + i}s ease-in-out infinite` 95 + : "none", 69 96 animationDelay: `${avatar.delay}s`, 70 97 }} 71 98 > ··· 110 137 /> 111 138 </svg> 112 139 113 - <div className="relative mx-auto max-w-2xl text-center"> 140 + <div className={`relative mx-auto max-w-2xl text-center transition-all duration-700 ease-out ${ 141 + isVisible ? "opacity-100 translate-y-0" : "opacity-0 translate-y-8" 142 + }`} 143 + style={{ transitionDelay: isVisible ? "300ms" : "0ms" }} 144 + > 114 145 <SectionIntroLabel>Come on in</SectionIntroLabel> 115 146 116 147 <h2 className="mt-6"> ··· 118 149 </h2> 119 150 120 151 <p className="section-copy mx-auto mt-6 max-w-lg"> 121 - The algorithms shaping your reality shouldn't be controlled by 152 + The algorithms shaping your reality shouldn&apos;t be controlled by 122 153 shareholders in Silicon Valley. At eny.social, you own your feed, your 123 154 data, and your voice. 124 155 </p>
+53 -42
app/components/WeBelieve.tsx
··· 1 + "use client"; 2 + 1 3 import SectionIntroLabel from "./ui/SectionIntroLabel"; 4 + import FadeIn from "./ui/FadeIn"; 2 5 3 6 export default function WeBelieve() { 4 7 return ( ··· 7 10 <div className="grid items-center gap-12 lg:grid-cols-2"> 8 11 {/* Text */} 9 12 <div> 10 - <SectionIntroLabel>We believe</SectionIntroLabel> 13 + <FadeIn> 14 + <SectionIntroLabel>We believe</SectionIntroLabel> 15 + </FadeIn> 11 16 12 - <h2 className="mt-6"> 13 - Your feed. 14 - <br /> 15 - Your data. 16 - <br /> 17 - <span className="italic text-pacific">Your voice.</span> 18 - </h2> 17 + <FadeIn delay={100}> 18 + <h2 className="mt-6"> 19 + Your feed. 20 + <br /> 21 + Your data. 22 + <br /> 23 + <span className="italic text-pacific">Your voice.</span> 24 + </h2> 25 + </FadeIn> 19 26 20 - <p className="section-copy mt-8 max-w-lg"> 21 - The algorithms shaping your reality shouldn't be controlled by 22 - shareholders in Silicon Valley. At eny.social, you own your feed, 23 - your data, and your voice. 24 - </p> 27 + <FadeIn delay={200}> 28 + <p className="section-copy mt-8 max-w-lg"> 29 + The algorithms shaping your reality shouldn&apos;t be controlled by 30 + shareholders in Silicon Valley. At eny.social, you own your feed, 31 + your data, and your voice. 32 + </p> 33 + </FadeIn> 25 34 </div> 26 35 27 36 {/* Photos + blob */} 28 - <div className="relative"> 29 - {/* Monte Carlo blob */} 30 - <svg 31 - className="absolute -right-12 -top-12 h-80 w-80 opacity-20" 32 - viewBox="0 0 400 400" 33 - fill="none" 34 - > 35 - <path 36 - d="M300 200C300 280 260 340 200 360C140 380 60 320 40 240C20 160 80 60 160 40C240 20 300 100 300 200Z" 37 - fill="var(--monte-carlo)" 38 - /> 39 - </svg> 37 + <FadeIn delay={300} direction="right"> 38 + <div className="relative"> 39 + {/* Monte Carlo blob */} 40 + <svg 41 + className="absolute -right-12 -top-12 h-80 w-80 opacity-20" 42 + viewBox="0 0 400 400" 43 + fill="none" 44 + > 45 + <path 46 + d="M300 200C300 280 260 340 200 360C140 380 60 320 40 240C20 160 80 60 160 40C240 20 300 100 300 200Z" 47 + fill="var(--monte-carlo)" 48 + /> 49 + </svg> 40 50 41 - <div className="relative flex items-center justify-center gap-4"> 42 - {/* Circular photo */} 43 - <div className="h-48 w-48 overflow-hidden rounded-full md:h-56 md:w-56"> 44 - <img 45 - src="/images/pexels-davner-ribeiro-2711547-4574405.jpg" 46 - alt="Community" 47 - className="h-full w-full object-cover" 48 - /> 49 - </div> 50 - {/* Rounded rectangle photo */} 51 - <div className="h-56 w-40 overflow-hidden rounded-3xl md:h-64 md:w-44"> 52 - <img 53 - src="/images/pexels-guilhermealmeida-1858175.png" 54 - alt="Connection" 55 - className="h-full w-full object-cover" 56 - /> 51 + <div className="relative flex items-center justify-center gap-4"> 52 + {/* Circular photo */} 53 + <div className="h-48 w-48 overflow-hidden rounded-full md:h-56 md:w-56"> 54 + <img 55 + src="/images/pexels-davner-ribeiro-2711547-4574405.jpg" 56 + alt="Community" 57 + className="h-full w-full object-cover" 58 + /> 59 + </div> 60 + {/* Rounded rectangle photo */} 61 + <div className="h-56 w-40 overflow-hidden rounded-3xl md:h-64 md:w-44"> 62 + <img 63 + src="/images/pexels-guilhermealmeida-1858175.png" 64 + alt="Connection" 65 + className="h-full w-full object-cover" 66 + /> 67 + </div> 57 68 </div> 58 69 </div> 59 - </div> 70 + </FadeIn> 60 71 </div> 61 72 </div> 62 73
+58
app/components/ui/FadeIn.tsx
··· 1 + "use client"; 2 + 3 + import { useRef, useState, useEffect } from "react"; 4 + 5 + interface FadeInProps { 6 + children: React.ReactNode; 7 + className?: string; 8 + delay?: number; 9 + direction?: "up" | "down" | "left" | "right" | "none"; 10 + } 11 + 12 + export default function FadeIn({ 13 + children, 14 + className = "", 15 + delay = 0, 16 + direction = "up", 17 + }: FadeInProps) { 18 + const ref = useRef<HTMLDivElement>(null); 19 + const [isVisible, setIsVisible] = useState(false); 20 + 21 + useEffect(() => { 22 + const el = ref.current; 23 + if (!el) return; 24 + const observer = new IntersectionObserver( 25 + ([entry]) => { 26 + if (entry.isIntersecting) { 27 + setIsVisible(true); 28 + observer.disconnect(); 29 + } 30 + }, 31 + { threshold: 0.15 } 32 + ); 33 + observer.observe(el); 34 + return () => observer.disconnect(); 35 + }, []); 36 + 37 + const transforms = { 38 + up: "translate-y-6", 39 + down: "-translate-y-6", 40 + left: "translate-x-6", 41 + right: "-translate-x-6", 42 + none: "", 43 + }; 44 + 45 + return ( 46 + <div 47 + ref={ref} 48 + className={`transition-all duration-700 ease-out ${ 49 + isVisible 50 + ? "opacity-100 translate-x-0 translate-y-0" 51 + : `opacity-0 ${transforms[direction]}` 52 + } ${className}`} 53 + style={{ transitionDelay: isVisible ? `${delay}ms` : "0ms" }} 54 + > 55 + {children} 56 + </div> 57 + ); 58 + }