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.

Merge branch 'feature/Enysocial-MVP-Tasks#CU-86c8yzvp4' into mirror/main

+148 -25
+147 -24
app/components/RememberWhen.tsx
··· 1 1 "use client"; 2 2 3 - import Image from "next/image"; 3 + /* eslint-disable @next/next/no-img-element */ 4 + import { useEffect, useMemo, useState } from "react"; 4 5 import SectionIntroLabel from "./ui/SectionIntroLabel"; 5 6 import FadeIn from "./ui/FadeIn"; 7 + 8 + // Active GIF duration per slot (must match `items` order). 9 + // Adjust these values to control how long each GIF stays active. 10 + const GIF_INTERVALS_MS = [4000, 4000, 4000] as const; 11 + const FADE_IN_MS = 1000; 12 + const FADE_OUT_MS = 1000; 13 + const OVERLAP_MS = 100; 14 + const FADE_EASING = "linear"; 6 15 7 16 export default function RememberWhen() { 17 + const items = useMemo( 18 + () => [ 19 + { 20 + staticSrc: "/images/webp/money-lovers.webp", 21 + animatedSrc: "/images/webp/money-lovers.webp", 22 + mask: "mask-1", 23 + position: "bottom" as const, 24 + }, 25 + { 26 + staticSrc: "/images/webp/dancer.webp", 27 + animatedSrc: "/images/webp/dancer.webp", 28 + mask: "mask-2", 29 + position: "center" as const, 30 + }, 31 + { 32 + staticSrc: "/images/webp/cute-dog.webp", 33 + animatedSrc: "/images/webp/cute-dog.webp", 34 + mask: "mask-3", 35 + position: "top" as const, 36 + }, 37 + ], 38 + [], 39 + ); 40 + 41 + const [activeIndex, setActiveIndex] = useState(0); 42 + const [overlayOpacity, setOverlayOpacity] = useState(1); 43 + const [fadeMs, setFadeMs] = useState(FADE_IN_MS); 44 + const [staticFrameByIndex, setStaticFrameByIndex] = useState< 45 + Record<number, string> 46 + >({}); 47 + 48 + useEffect(() => { 49 + const durationMs = 50 + GIF_INTERVALS_MS[activeIndex] ?? GIF_INTERVALS_MS[0] ?? 6000; 51 + 52 + // Fade in, fade out 53 + const fadeInStartId = window.setTimeout(() => { 54 + setFadeMs(FADE_IN_MS); 55 + setOverlayOpacity(0); 56 + }, 0); 57 + const fadeInId = window.setTimeout(() => { 58 + setFadeMs(FADE_IN_MS); 59 + setOverlayOpacity(1); 60 + }, 30); 61 + 62 + const fadeOutAt = Math.max(0, durationMs - FADE_OUT_MS); 63 + const fadeOutId = window.setTimeout(() => { 64 + setFadeMs(FADE_OUT_MS); 65 + setOverlayOpacity(0); 66 + }, fadeOutAt); 67 + 68 + const nextId = window.setTimeout( 69 + () => { 70 + setActiveIndex((prev) => (prev + 1) % items.length); 71 + }, 72 + Math.max(0, durationMs - OVERLAP_MS), 73 + ); 74 + 75 + return () => { 76 + window.clearTimeout(fadeInStartId); 77 + window.clearTimeout(fadeInId); 78 + window.clearTimeout(fadeOutId); 79 + window.clearTimeout(nextId); 80 + }; 81 + }, [activeIndex, items.length]); 82 + 83 + useEffect(() => { 84 + let cancelled = false; 85 + 86 + const captureFirstFrame = async (src: string, index: number) => { 87 + const img = new window.Image(); 88 + img.crossOrigin = "anonymous"; 89 + 90 + await new Promise<void>((resolve) => { 91 + img.onload = () => resolve(); 92 + img.onerror = () => resolve(); 93 + img.src = src; 94 + }); 95 + 96 + if (cancelled) return; 97 + if (!img.naturalWidth || !img.naturalHeight) return; 98 + 99 + const canvas = document.createElement("canvas"); 100 + canvas.width = img.naturalWidth; 101 + canvas.height = img.naturalHeight; 102 + const ctx = canvas.getContext("2d"); 103 + if (!ctx) return; 104 + 105 + ctx.drawImage(img, 0, 0); 106 + 107 + try { 108 + const dataUrl = canvas.toDataURL("image/png"); 109 + if (cancelled) return; 110 + setStaticFrameByIndex((prev) => ({ ...prev, [index]: dataUrl })); 111 + } catch { 112 + // Canvas is tainted (CORS) or similar. Keep using item.staticSrc. 113 + } 114 + }; 115 + 116 + items.forEach((item, i) => { 117 + void captureFirstFrame(item.animatedSrc, i); 118 + }); 119 + 120 + return () => { 121 + cancelled = true; 122 + }; 123 + }, [items]); 124 + 8 125 return ( 9 126 <section className="relative px-6 py-24"> 10 127 {/* SVG clip path definitions — all shapes are 350x400 */} ··· 48 165 49 166 {/* Three masked photos */} 50 167 <div className="mt-12 flex items-center justify-center gap-6 md:gap-10"> 51 - {[ 52 - { 53 - src: "/images/pexels-shvets-production-7194971.jpg", 54 - mask: "mask-1", 55 - position: "bottom" as const, 56 - }, 57 - { 58 - src: "/images/pexels-didsss-7664407.jpg", 59 - mask: "mask-2", 60 - position: "center" as const, 61 - }, 62 - { 63 - src: "/images/pexels-shvets-production-7533377 1.png", 64 - mask: "mask-3", 65 - position: "top" as const, 66 - }, 67 - ].map((item, i) => ( 168 + {items.map((item, i) => ( 68 169 <FadeIn key={i} delay={200 + i * 150}> 69 170 <div 70 171 className="relative w-[28vw] h-[32vw] max-w-[263px] max-h-[300px] overflow-hidden" 71 172 style={{ clipPath: `url(#${item.mask})` }} 72 173 > 73 - <Image 74 - src={item.src} 174 + {/* Static base (always visible) */} 175 + <img 176 + src={staticFrameByIndex[i] ?? item.staticSrc} 75 177 alt="" 76 - width={526} 77 - height={600} 78 - className="h-full w-full object-cover" 79 - style={{ objectPosition: item.position }} 178 + className="absolute inset-0 h-full w-full object-cover" 179 + style={{ 180 + objectPosition: item.position, 181 + // Crossfade: as the animated overlay fades out, fade the static base in. 182 + opacity: activeIndex === i ? 1 - overlayOpacity : 1, 183 + transition: `opacity ${fadeMs}ms ${FADE_EASING}`, 184 + willChange: "opacity", 185 + }} 186 + draggable={false} 80 187 /> 188 + 189 + {/* Animated overlay (only one animates at a time) */} 190 + {activeIndex === i && ( 191 + <img 192 + src={item.animatedSrc} 193 + alt="" 194 + className="absolute inset-0 h-full w-full object-cover" 195 + style={{ 196 + objectPosition: item.position, 197 + opacity: overlayOpacity, 198 + transition: `opacity ${fadeMs}ms ${FADE_EASING}`, 199 + willChange: "opacity", 200 + }} 201 + draggable={false} 202 + /> 203 + )} 81 204 </div> 82 205 </FadeIn> 83 206 ))}
+1 -1
app/components/ValueCards.tsx
··· 37 37 return ( 38 38 <section id="values" className="relative py-24"> 39 39 <div className="relative"> 40 - <div className="hide-scrollbar flex gap-6 overflow-x-auto pb-4 -ml-[140px] -mr-[140px] lg:-ml-[80px] lg:-mr-[80px] pl-0 pr-0 lg:justify-center items-center"> 40 + <div className="hide-scrollbar flex touch-pan-x gap-6 overflow-x-auto overflow-y-hidden overscroll-x-contain pb-4 -ml-[140px] -mr-[140px] lg:-ml-[80px] lg:-mr-[80px] pl-0 pr-0 lg:justify-center items-center"> 41 41 {cards.map((card, i) => ( 42 42 <FadeIn key={i} delay={i * 120}> 43 43 <div
public/images/webp/cute-dog.webp

This is a binary file and will not be displayed.

public/images/webp/dancer.webp

This is a binary file and will not be displayed.

public/images/webp/money-lovers.webp

This is a binary file and will not be displayed.