diff --git a/app/api/feed/latest/route.ts b/app/api/feed/latest/route.ts new file mode 100644 index 0000000..41bd3b9 --- /dev/null +++ b/app/api/feed/latest/route.ts @@ -0,0 +1,20 @@ +import { NextResponse } from 'next/server'; +import { getFeedPosts } from '@/lib/linkedin-feed'; +import { OFFICIAL_HANDLES } from '@/lib/linkedin-feed-shared'; + +// Read at request time (the build has no database), then cached at the edge. +export const dynamic = 'force-dynamic'; + +/** Posts shown in the feed rail on the homepage; the full wall is /feed */ +const RAIL_SIZE = 10; + +// GET: the latest member posts with an excerpt, for the feed rail under Fellow voices +export async function GET() { + const posts = await getFeedPosts(); + const latest = posts + .filter((post) => !OFFICIAL_HANDLES.has(post.handle) && post.excerpt.trim()) + .slice(0, RAIL_SIZE); + return NextResponse.json(latest, { + headers: { 'Cache-Control': 'public, s-maxage=600, stale-while-revalidate=86400' }, + }); +} diff --git a/components/home/feed-rail.tsx b/components/home/feed-rail.tsx new file mode 100644 index 0000000..543a265 --- /dev/null +++ b/components/home/feed-rail.tsx @@ -0,0 +1,125 @@ +"use client" + +import { useEffect, useRef, useState } from "react" +import Link from "next/link" +import { ArrowLeft, ArrowRight, ArrowUpRight } from "lucide-react" + +import { FEED_TOPICS, type FeedPost } from "@/lib/linkedin-feed-shared" + +const dateFormat = new Intl.DateTimeFormat("en-GB", { day: "numeric", month: "short", year: "numeric", timeZone: "UTC" }) + +function initials(name: string) { + return name + .split(" ") + .filter(Boolean) + .slice(0, 2) + .map((word) => word[0]) + .join("") + .toUpperCase() +} + +/** + * Feed rail: the latest member posts from the /feed wall, under Fellow voices. + * Each card opens the post on LinkedIn and the last card opens the full wall. + * Renders nothing until posts load, so the section never shows an empty rail. + */ +export function FeedRail() { + const rail = useRef(null) + const [posts, setPosts] = useState([]) + + useEffect(() => { + fetch("/api/feed/latest") + .then((res) => (res.ok ? res.json() : [])) + .then((data) => setPosts(Array.isArray(data) ? data : [])) + .catch(() => setPosts([])) + }, []) + + if (posts.length === 0) return null + + const scrollBy = (direction: 1 | -1) => { + const el = rail.current + if (!el) return + el.scrollBy({ left: direction * Math.max(320, el.clientWidth * 0.8), behavior: "smooth" }) + } + + return ( +
+
+
+

Community feed

+

Shared on LinkedIn with #DevWeekends.

+
+
+ + See all posts → + + + +
+
+ +
+ {posts.map((post) => ( + + + + + {post.author} + {post.postedAt && {dateFormat.format(new Date(post.postedAt))}} + + + {post.excerpt} + + {FEED_TOPICS[post.topic] ?? FEED_TOPICS.community} + + LinkedIn + + (post by {post.author}, opens in a new tab) + + ))} + + + More + See every post on the community feed + Open the feed → + +
+
+ ) +} diff --git a/components/home/sections-bottom.tsx b/components/home/sections-bottom.tsx index 64b33dc..97f0a85 100644 --- a/components/home/sections-bottom.tsx +++ b/components/home/sections-bottom.tsx @@ -2,6 +2,7 @@ import { ArrowUpRight } from "lucide-react" import { SmartLink } from "./smart-link" import { LogoMark } from "./logo-mark" +import { FeedRail } from "./feed-rail" import { VoicesRail } from "./voices" import { LINKS, @@ -190,6 +191,7 @@ export function Voices() { return (
+
) }