diff --git a/app/page.tsx b/app/page.tsx index 0e32c27..973cc18 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -35,8 +35,6 @@ const CURATED: { category: string; slug: string }[] = [ { category: "blocks", slug: "bloom-menu" }, ]; -// Grid of live-preview cards. The first tile is promoted to a large "feature" -// showcase. const GRID_CLASS = "grid grid-cols-1 gap-4 [grid-auto-rows:19rem] sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4"; @@ -73,12 +71,6 @@ function SectionHeader({ } export default function Home() { - const curatedComponents = CURATED.flatMap(({ category, slug }) => { - const cat = registry.find((c) => c.slug === category); - const comp = cat?.components.find((c) => c.slug === slug); - return comp ? [{ category, component: comp }] : []; - }); - const newComponents = registry .flatMap((category) => category.components @@ -89,6 +81,19 @@ export default function Home() { .sort((a, b) => (b.component.launchedAt ?? "").localeCompare(a.component.launchedAt ?? ""), ); + const newComponentKeys = new Set( + newComponents.map( + ({ category, component }) => `${category}/${component.slug}`, + ), + ); + const curatedComponents = CURATED.flatMap(({ category, slug }) => { + const cat = registry.find((c) => c.slug === category); + const comp = cat?.components.find((c) => c.slug === slug); + return comp ? [{ category, component: comp }] : []; + }).filter( + ({ category, component }) => + !newComponentKeys.has(`${category}/${component.slug}`), + ); return (
diff --git a/components/app/docs/install-command.tsx b/components/app/docs/install-command.tsx index a8dd5db..8ac5151 100644 --- a/components/app/docs/install-command.tsx +++ b/components/app/docs/install-command.tsx @@ -23,7 +23,9 @@ const CYCLE_MS = 1800; const COMPONENT_SLUGS = registry.flatMap((cat) => cat.components.flatMap((comp) => comp.examples - ? comp.examples.filter((e) => e.installSlug).map((e) => e.installSlug!) + ? comp.examples.flatMap((example) => + example.installSlug ? [example.installSlug] : [], + ) : [comp.slug], ), ); diff --git a/components/app/landing/landing-component-card.tsx b/components/app/landing/landing-component-card.tsx index b5af372..4d46aa3 100644 --- a/components/app/landing/landing-component-card.tsx +++ b/components/app/landing/landing-component-card.tsx @@ -2,11 +2,13 @@ import Link from "next/link"; import { ArrowUpRight } from "lucide-react"; -import { useState } from "react"; +import { useInView } from "motion/react"; +import { useRef, useState } from "react"; import type { ComponentEntry } from "@/lib/registry"; import { NewBadge } from "@/components/app/docs/new-badge"; import { PreviewFit } from "@/components/app/landing/preview-fit"; import { getPreview } from "@/components/previews"; +import { EASE_OUT_CSS } from "@/lib/ease"; import { cn } from "@/lib/utils"; export type CardVariant = "default" | "wide" | "feature"; @@ -26,12 +28,18 @@ export function LandingComponentCard({ category?: string; variant?: CardVariant; }) { + const cardRef = useRef(null); + const shouldRenderPreview = useInView(cardRef, { + once: true, + margin: "400px 0px", + }); const Preview = getPreview(category, component.slug); const [hover, setHover] = useState(false); const feature = variant === "feature"; return (
setHover(true)} onPointerLeave={() => setHover(false)} @@ -40,13 +48,24 @@ export function LandingComponentCard({ > -
- - {Preview ? : null} - +
+ {shouldRenderPreview ? ( + + {Preview ? : null} + + ) : ( +
diff --git a/components/app/landing/testimonial-card.tsx b/components/app/landing/testimonial-card.tsx index e4810f9..ebb6004 100644 --- a/components/app/landing/testimonial-card.tsx +++ b/components/app/landing/testimonial-card.tsx @@ -1,5 +1,4 @@ -import { enrichTweet } from "react-tweet"; -import type { Tweet } from "react-tweet/api"; +import type { Testimonial } from "@/components/app/landing/testimonials-data"; import { cn } from "@/lib/utils"; function VerifiedBadge() { @@ -16,20 +15,18 @@ function VerifiedBadge() { } export function TestimonialCard({ - tweet, + testimonial, compact = false, }: { - tweet: Tweet; + testimonial: Testimonial; compact?: boolean; }) { - const t = enrichTweet(tweet); - const verified = t.user.is_blue_verified || t.user.verified; // Swap Twitter's 48px `_normal` avatar for the crisper 73px `_bigger`. - const avatar = t.user.profile_image_url_https.replace("_normal", "_bigger"); + const avatar = testimonial.user.avatar.replace("_normal", "_bigger"); return (
- {t.user.name} + {testimonial.user.name} - {verified ? : null} + {testimonial.user.verified ? : null}
- @{t.user.screen_name} + @{testimonial.user.username}
@@ -73,21 +70,7 @@ export function TestimonialCard({ : "mt-4 text-[15px] leading-relaxed", )} > - {t.entities.map((item, i) => { - if (item.type === "media") return null; - if (item.type === "text") { - return ( - // biome-ignore lint/suspicious/noArrayIndexKey: tweet parts are positional and stable - {item.text} - ); - } - return ( - // biome-ignore lint/suspicious/noArrayIndexKey: tweet parts are positional and stable - - {item.text} - - ); - })} + {testimonial.text}

); diff --git a/components/app/landing/testimonials-data.ts b/components/app/landing/testimonials-data.ts new file mode 100644 index 0000000..eddb8ca --- /dev/null +++ b/components/app/landing/testimonials-data.ts @@ -0,0 +1,211 @@ +export type Testimonial = { + id: string; + text: string; + user: { + name: string; + username: string; + avatar: string; + verified: boolean; + }; +}; + +export const TESTIMONIALS = [ + { + id: "2070915664668512304", + text: "Really like the animations throughout @saurra3h's ui-components.\n\nA lot of the components have smooth, thoughtfully designed interactions.", + user: { + name: "ericts", + username: "erictsdotcom", + avatar: + "https://pbs.twimg.com/profile_images/2037069613461372928/pZVqHmPg_normal.jpg", + verified: false, + }, + }, + { + id: "2073486052665537002", + text: "@saurra3h @paper Super cool!", + user: { + name: "Stephen Haney", + username: "stephenhaney", + avatar: + "https://pbs.twimg.com/profile_images/1998948097662259200/PfaIVhfU_normal.jpg", + verified: true, + }, + }, + { + id: "2073135185370227162", + text: "this > any metric\n\nthis is exactly why i keep building beui", + user: { + name: "Saurabh | NodeOps", + username: "saurra3h", + avatar: + "https://pbs.twimg.com/profile_images/1865446187295178752/thuJzCdX_normal.jpg", + verified: true, + }, + }, + { + id: "2072978320036348221", + text: "@saurra3h @paper damn just explored the components, i'm using it ASAP.", + user: { + name: "Amanjot Singh", + username: "amans_twt", + avatar: + "https://pbs.twimg.com/profile_images/1837376254607077376/k9hrX4mP_normal.jpg", + verified: true, + }, + }, + { + id: "2070129442157191185", + text: "@saurra3h Your Component Library is amazing been using it for a long time glad to see it constantly evolving", + user: { + name: "Hossain Jahed", + username: "EaseMizeUI", + avatar: + "https://pbs.twimg.com/profile_images/2016098268883910657/U5EKdUq5_normal.jpg", + verified: false, + }, + }, + { + id: "2073494103153586236", + text: "@saurra3h @motiondotdev I appreciate the work you’re doing at Beui! It’s great.", + user: { + name: "Enes Aktaş", + username: "ensaktas", + avatar: + "https://pbs.twimg.com/profile_images/2067486981840396288/p1iCw56a_normal.jpg", + verified: true, + }, + }, + { + id: "2071327003790184684", + text: "@saurra3h just check it out looks clean, gonna use it!", + user: { + name: "Tyler Gibbs", + username: "Tylerbryy", + avatar: + "https://pbs.twimg.com/profile_images/2068918593962536960/gmAsn6Nj_normal.jpg", + verified: true, + }, + }, + { + id: "2069456887184318562", + text: "@saurra3h @vercel @rauchg My dude. These are great", + user: { + name: "Myles Franklin", + username: "myles_franklin", + avatar: + "https://pbs.twimg.com/profile_images/1793338348398071808/-SrMDjm9_normal.jpg", + verified: true, + }, + }, + { + id: "2066804142719275062", + text: "@saurra3h I’ve been looking through it in awe man it’s incredible", + user: { + name: "Jack", + username: "jacksportfolio", + avatar: + "https://pbs.twimg.com/profile_images/2054257567350607872/Mt6Fhivs_normal.jpg", + verified: true, + }, + }, + { + id: "2071206392925765751", + text: "@saurra3h adding this to ndle app, great work!!", + user: { + name: "Abhishek", + username: "abhishk_084", + avatar: + "https://pbs.twimg.com/profile_images/2013536935558455296/gxHOeBex_normal.jpg", + verified: true, + }, + }, + { + id: "2069415701874720806", + text: "@saurra3h @vercel @rauchg the animations are so cool, love to see more!", + user: { + name: "Thrishank", + username: "thrishank007", + avatar: + "https://pbs.twimg.com/profile_images/1920539073321553920/Vgu4EqC3_normal.jpg", + verified: false, + }, + }, + { + id: "2073188569506587028", + text: "@saurra3h @shadcn love the component looks super clean!!", + user: { + name: "Daniel White", + username: "dwhitedesign", + avatar: + "https://pbs.twimg.com/profile_images/2057915040057982976/gSIeaZnQ_normal.jpg", + verified: true, + }, + }, + { + id: "2069333890506936655", + text: "@saurra3h @vercel @rauchg Pretty cool work, I love the animations", + user: { + name: "Adetunji | Software Engineer (Web & Mobile)", + username: "itzadetunji1", + avatar: + "https://pbs.twimg.com/profile_images/2008803706284691456/oiK7MclR_normal.jpg", + verified: true, + }, + }, + { + id: "2071800087940870242", + text: "@saurra3h @motiondotdev spent way too long clicking this just for the animation", + user: { + name: "Nick Venturi", + username: "nickventuri", + avatar: + "https://pbs.twimg.com/profile_images/2047053248427638784/lY1d_G02_normal.jpg", + verified: true, + }, + }, + { + id: "2069108073839435853", + text: "@saurra3h Loved the Glitch template. Applying to my site right now! :-)", + user: { + name: "Alvin", + username: "AlvBckr", + avatar: + "https://pbs.twimg.com/profile_images/2066598344563052544/z8pbOXSj_normal.jpg", + verified: true, + }, + }, + { + id: "2071704269816811735", + text: "@alibey_10 @saurra3h Really nice work 🫡", + user: { + name: "Corentin", + username: "CorentinClichy", + avatar: + "https://pbs.twimg.com/profile_images/1911752944212406272/t6lZLeFg_normal.jpg", + verified: false, + }, + }, + { + id: "2069459958857650245", + text: "@saurra3h this is gorgeous", + user: { + name: "AJ", + username: "attaboiaj", + avatar: + "https://pbs.twimg.com/profile_images/1620798411338547200/AAlPAkWT_normal.jpg", + verified: false, + }, + }, + { + id: "2071569532796256411", + text: "SWEET!!", + user: { + name: "MIRACLE CHIKI 🇳🇬", + username: "Udochiki", + avatar: + "https://pbs.twimg.com/profile_images/2001436282224795648/YFxqfgJB_normal.jpg", + verified: false, + }, + }, +] satisfies Testimonial[]; diff --git a/components/app/landing/testimonials.tsx b/components/app/landing/testimonials.tsx index b6b11a5..eb56637 100644 --- a/components/app/landing/testimonials.tsx +++ b/components/app/landing/testimonials.tsx @@ -1,50 +1,12 @@ -import type { Tweet } from "react-tweet/api"; -import { getTweet } from "react-tweet/api"; import { TestimonialCard } from "@/components/app/landing/testimonial-card"; +import { TESTIMONIALS } from "@/components/app/landing/testimonials-data"; import { Marquee } from "@/components/motion/marquee"; -// Public tweets shown as social proof. IDs only — the content is pulled from -// Twitter's syndication API on the server and rendered statically, so no -// async component streams into the client (which trips a React 19 / Next 15 -// Flight bug: "chunk.reason.enqueueModel is not a function"). -const TWEET_IDS = [ - "2070915664668512304", - "2073486052665537002", - "2073135185370227162", - "2072978320036348221", - "2070129442157191185", - "2073494103153586236", - "2071327003790184684", - "2069456887184318562", - "2066804142719275062", - "2071206392925765751", - "2069415701874720806", - "2073188569506587028", - "2069333890506936655", - "2071800087940870242", - "2069108073839435853", - "2071704269816811735", - "2069459958857650245", - "2071569532796256411", -]; - -export async function Testimonials() { - const tweets = await Promise.all( - TWEET_IDS.map(async (id) => { - try { - return await getTweet(id); - } catch { - return undefined; - } - }), - ); - const found = tweets.filter((t): t is Tweet => t != null); - if (found.length === 0) return null; - +export function Testimonials() { // Split into two rows that scroll in opposite directions. - const mid = Math.ceil(found.length / 2); - const rowOne = found.slice(0, mid); - const rowTwo = found.slice(mid); + const mid = Math.ceil(TESTIMONIALS.length / 2); + const rowOne = TESTIMONIALS.slice(0, mid); + const rowTwo = TESTIMONIALS.slice(mid); return (
@@ -59,13 +21,21 @@ export async function Testimonials() {
- {rowOne.map((tweet) => ( - + {rowOne.map((testimonial) => ( + ))} - {rowTwo.map((tweet) => ( - + {rowTwo.map((testimonial) => ( + ))}