Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 13 additions & 8 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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
Expand All @@ -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 (
<div className="relative">
Expand Down
4 changes: 3 additions & 1 deletion components/app/docs/install-command.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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],
),
);
Expand Down
34 changes: 28 additions & 6 deletions components/app/landing/landing-component-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -26,12 +28,18 @@ export function LandingComponentCard({
category?: string;
variant?: CardVariant;
}) {
const cardRef = useRef<HTMLElement>(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 (
<article
ref={cardRef}
className={cn("group/card relative h-full", VARIANT_SPAN[variant])}
onPointerEnter={() => setHover(true)}
onPointerLeave={() => setHover(false)}
Expand All @@ -40,13 +48,24 @@ export function LandingComponentCard({
>
<Link
href={`/components/${category}/${component.slug}`}
prefetch={false}
aria-label={`View ${component.name}`}
className="absolute inset-0 z-20 rounded-3xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 focus-visible:ring-offset-background"
/>
<div className="relative flex h-full flex-col overflow-hidden rounded-3xl border border-border bg-card transition-colors duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] contain-[paint] group-hover/card:border-border-strong">
<PreviewFit hover={hover} maxScale={feature ? 1 : 0.82}>
{Preview ? <Preview /> : null}
</PreviewFit>
<div
className="relative flex h-full flex-col overflow-hidden rounded-3xl border border-border bg-card transition-colors duration-300 contain-[paint] group-hover/card:border-border-strong"
style={{ transitionTimingFunction: EASE_OUT_CSS }}
>
{shouldRenderPreview ? (
<PreviewFit hover={hover} maxScale={feature ? 1 : 0.82}>
{Preview ? <Preview /> : null}
</PreviewFit>
) : (
<div
aria-hidden="true"
className="relative m-2 mb-0 min-h-0 flex-1 rounded-[1.25rem] bg-background"
/>
)}

<div className="flex shrink-0 items-center justify-between gap-3 px-4 py-3.5">
<div className="min-w-0">
Expand All @@ -65,7 +84,10 @@ export function LandingComponentCard({
{component.description}
</p>
</div>
<ArrowUpRight className="h-4 w-4 shrink-0 -translate-x-1 text-muted-foreground opacity-0 transition-[opacity,transform] duration-300 ease-[cubic-bezier(0.23,1,0.32,1)] group-hover/card:translate-x-0 group-hover/card:opacity-100" />
<ArrowUpRight
className="h-4 w-4 shrink-0 -translate-x-1 text-muted-foreground opacity-0 transition-[opacity,transform] duration-300 group-hover/card:translate-x-0 group-hover/card:opacity-100"
style={{ transitionTimingFunction: EASE_OUT_CSS }}
/>
</div>
</div>
</article>
Expand Down
35 changes: 9 additions & 26 deletions components/app/landing/testimonial-card.tsx
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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 (
<a
href={t.url}
href={`https://x.com/${testimonial.user.username}/status/${testimonial.id}`}
target="_blank"
rel="noreferrer noopener"
className={cn(
Expand All @@ -55,12 +52,12 @@ export function TestimonialCard({
<div className="min-w-0">
<div className="flex items-center gap-1">
<span className="truncate font-medium text-foreground">
{t.user.name}
{testimonial.user.name}
</span>
{verified ? <VerifiedBadge /> : null}
{testimonial.user.verified ? <VerifiedBadge /> : null}
</div>
<span className="block truncate text-sm text-muted-foreground">
@{t.user.screen_name}
@{testimonial.user.username}
</span>
</div>
</div>
Expand All @@ -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
<span key={i}>{item.text}</span>
);
}
return (
// biome-ignore lint/suspicious/noArrayIndexKey: tweet parts are positional and stable
<span key={i} className="text-accent">
{item.text}
</span>
);
})}
{testimonial.text}
</p>
</a>
);
Expand Down
Loading
Loading