285 lines
9.6 KiB
TypeScript
285 lines
9.6 KiB
TypeScript
"use client";
|
|
|
|
import { useRef, useState } from "react";
|
|
import gsap from "gsap";
|
|
import ScrollTrigger from "gsap/ScrollTrigger";
|
|
import { useGSAP } from "@gsap/react";
|
|
|
|
if (typeof window !== "undefined") {
|
|
gsap.registerPlugin(ScrollTrigger, useGSAP);
|
|
}
|
|
|
|
type Post = {
|
|
handle: string;
|
|
statusIdParts: string[];
|
|
label: string;
|
|
author: string;
|
|
verified?: boolean;
|
|
note: string;
|
|
text: string;
|
|
date: string;
|
|
likes?: number;
|
|
};
|
|
|
|
const posts: Post[] = [
|
|
{
|
|
handle: "Teknium",
|
|
statusIdParts: ["204231", "894145", "7170790"],
|
|
label: "Signal",
|
|
author: "Teknium",
|
|
verified: true,
|
|
note: "Most important outside validation.",
|
|
text: "It can orchestrate agents but this looks a bit more advanced.",
|
|
date: "Apr 10, 2026",
|
|
likes: 4,
|
|
},
|
|
{
|
|
handle: "facito0",
|
|
statusIdParts: ["203638", "079647", "5547760"],
|
|
label: "Mood",
|
|
author: "FacitoO",
|
|
note: "A lightweight social proof hit from daily AO usage.",
|
|
text: "Me with @aoagents lately!",
|
|
date: "May 2, 2026",
|
|
},
|
|
{
|
|
handle: "buchireddy",
|
|
statusIdParts: ["206410", "814460", "7760628"],
|
|
label: "Builder",
|
|
author: "Buchi Reddy B",
|
|
verified: true,
|
|
note: "Went all-in early on the AO building blocks.",
|
|
text: "I really loved the building blocks present in @aoagents, hence we went all-in on that pretty early. Happy to share more details if it helps others.",
|
|
date: "Jun 9, 2026",
|
|
likes: 3,
|
|
},
|
|
{
|
|
handle: "oxwizzdom",
|
|
statusIdParts: ["204349", "124837", "6336484"],
|
|
label: "Code read",
|
|
author: "oxwizzdom",
|
|
verified: true,
|
|
note: "Weekend codebase teardown and minimal rebuild.",
|
|
text: "1/ @agent_wrapper & @composio shipped @aoagents a while back. runs 50 coding agents in parallel on the same repo. i spent a weekend reading the codebase. found 5 techniques that make it work.",
|
|
date: "Apr 14, 2026",
|
|
},
|
|
{
|
|
handle: "addddiiie",
|
|
statusIdParts: ["203717", "443270", "0211408"],
|
|
label: "Use case",
|
|
author: "Adi",
|
|
note: "Parallel dev agents framed in one clean line.",
|
|
text: "I just hired a few software devs to work for free cc - @aoagents",
|
|
date: "Mar 26, 2026",
|
|
likes: 9,
|
|
},
|
|
{
|
|
handle: "aoagents",
|
|
statusIdParts: ["205420", "723754", "8302804"],
|
|
label: "Official",
|
|
author: "Agent Orchestrator",
|
|
verified: true,
|
|
note: "A short official signal from the AO account.",
|
|
text: "Best as it gets.",
|
|
date: "May 18, 2026",
|
|
},
|
|
];
|
|
|
|
function postUrl(post: Post) {
|
|
return `https://twitter.com/${post.handle}/status/${post.statusIdParts.join("")}`;
|
|
}
|
|
|
|
function ArrowUpRightIcon({ className = "" }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
<path d="M7 7h10v10" />
|
|
<path d="M7 17 17 7" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function MessageCircleIcon({ className = "" }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
|
|
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function XSocialIcon({ className = "" }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<path d="M18.9 2.25h3.24l-7.08 8.09 8.33 11.41h-6.52l-5.11-6.91-5.84 6.91H2.66l7.57-8.67L2.25 2.25h6.69l4.62 6.3 5.34-6.3Zm-1.14 17.5h1.8L7.96 4.14H6.03l11.73 15.61Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function VerifiedIcon({ className = "" }: { className?: string }) {
|
|
return (
|
|
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<path d="M22.25 12c0-1.43-.88-2.67-2.19-3.34.46-1.39.2-2.9-.81-3.91s-2.52-1.27-3.91-.81c-.66-1.31-1.91-2.19-3.34-2.19s-2.67.88-3.33 2.19c-1.4-.46-2.91-.2-3.92.81s-1.26 2.52-.8 3.91c-1.31.67-2.2 1.91-2.2 3.34s.89 2.67 2.2 3.34c-.46 1.39-.21 2.9.8 3.91s2.52 1.26 3.91.81c.67 1.31 1.91 2.19 3.34 2.19s2.68-.88 3.34-2.19c1.39.45 2.9.2 3.91-.81s1.27-2.52.81-3.91c1.31-.67 2.19-1.91 2.19-3.34Zm-11.71 4.2L6.8 12.46l1.41-1.42 2.26 2.26 4.8-5.23 1.47 1.36-6.2 6.77Z" />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
export function LandingSocialProof() {
|
|
const containerRef = useRef<HTMLElement>(null);
|
|
|
|
useGSAP(() => {
|
|
const cards = gsap.utils.toArray<HTMLElement>(".gsap-tweet-card");
|
|
|
|
gsap.set(cards, { opacity: 0, y: 30 });
|
|
|
|
// Reveal each card as it enters the viewport. On mobile the masonry is a
|
|
// single tall column, so a single section-top trigger left the lower cards
|
|
// invisible until far past the heading; batching reveals them in step with
|
|
// the scroll on every layout.
|
|
const batch = ScrollTrigger.batch(cards, {
|
|
start: "top 90%",
|
|
once: true,
|
|
onEnter: (els: Element[]) => {
|
|
gsap.to(els, {
|
|
opacity: 1,
|
|
y: 0,
|
|
duration: 0.7,
|
|
stagger: 0.08,
|
|
ease: "power3.out",
|
|
});
|
|
},
|
|
});
|
|
|
|
ScrollTrigger.refresh();
|
|
|
|
return () => batch.forEach((t) => t.kill());
|
|
}, { scope: containerRef });
|
|
|
|
return (
|
|
<section
|
|
ref={containerRef}
|
|
id="testimonials"
|
|
data-testid="social-proof"
|
|
className="landing-section relative overflow-hidden border-t border-[color:var(--border)]"
|
|
>
|
|
<div className="container-page">
|
|
<div className="mx-auto max-w-[1320px]">
|
|
<div className="landing-section-header grid items-end gap-8 lg:grid-cols-12">
|
|
<div className="lg:col-span-7">
|
|
<div className="landing-eyebrow mb-4">In the wild</div>
|
|
<h2 className="landing-heading">
|
|
People are already <span className="landing-heading-muted">building around it.</span>
|
|
</h2>
|
|
</div>
|
|
<div className="lg:col-span-5">
|
|
<p className="landing-body-compact">
|
|
Real posts from builders, researchers, and early users — pulled straight from X.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="tweet-masonry">
|
|
{posts.map((post, index) => (
|
|
<TweetCard key={`${post.handle}-${index}`} post={post} index={index} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function Avatar({ post }: { post: Post }) {
|
|
const [failed, setFailed] = useState(false);
|
|
|
|
if (failed) {
|
|
return (
|
|
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-[color:var(--accent-soft)] text-sm font-bold text-[color:var(--accent)]">
|
|
{post.author.slice(0, 1)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<img
|
|
src={`https://unavatar.io/x/${post.handle}`}
|
|
alt={`${post.author} avatar`}
|
|
loading="lazy"
|
|
referrerPolicy="no-referrer"
|
|
onError={() => setFailed(true)}
|
|
className="h-10 w-10 shrink-0 rounded-full border border-[color:var(--border)] object-cover"
|
|
/>
|
|
);
|
|
}
|
|
|
|
function TweetCard({ post, index }: { post: Post; index: number }) {
|
|
const url = postUrl(post);
|
|
|
|
return (
|
|
<a
|
|
href={url}
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
data-testid={`tweet-card-${index}`}
|
|
aria-label={`Read ${post.author}'s post on X`}
|
|
className="gsap-tweet-card lift surface group mb-8 inline-block w-full break-inside-avoid overflow-hidden"
|
|
>
|
|
<div className="landing-card-header flex items-center justify-between gap-3 px-4 py-3">
|
|
<div className="flex min-w-0 items-center gap-2">
|
|
<MessageCircleIcon className="h-4 w-4 shrink-0 text-[color:var(--accent)]" />
|
|
<div className="min-w-0">
|
|
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-[color:var(--fg-muted)]">
|
|
{post.label}
|
|
</div>
|
|
<div className="truncate text-[13px] font-semibold text-[color:var(--fg)]">{post.author}</div>
|
|
</div>
|
|
</div>
|
|
<span className="inline-flex h-8 w-8 shrink-0 items-center justify-center text-[color:var(--fg-muted)] transition-colors group-hover:text-[color:var(--accent)]">
|
|
<ArrowUpRightIcon className="h-4 w-4" />
|
|
</span>
|
|
</div>
|
|
|
|
<div className="px-5 pb-5 pt-4">
|
|
<p className="mb-5 text-[13px] leading-relaxed text-[color:var(--fg-muted)]">{post.note}</p>
|
|
|
|
<div className="rounded-[10px] border border-[color:var(--border)] bg-[color:var(--bg-deep)] p-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="flex min-w-0 items-center gap-3">
|
|
<Avatar post={post} />
|
|
<div className="min-w-0">
|
|
<div className="flex items-center gap-1">
|
|
<span className="truncate text-[14px] font-semibold leading-tight text-[color:var(--fg)]">
|
|
{post.author}
|
|
</span>
|
|
{post.verified ? (
|
|
<VerifiedIcon className="h-3.5 w-3.5 shrink-0 text-[color:var(--accent)]" />
|
|
) : null}
|
|
</div>
|
|
<span className="truncate text-[12px] leading-tight text-[color:var(--fg-dim)]">@{post.handle}</span>
|
|
</div>
|
|
</div>
|
|
<XSocialIcon className="h-4 w-4 shrink-0 text-[color:var(--fg-muted)]" />
|
|
</div>
|
|
|
|
<p className="mt-4 whitespace-pre-line text-[15px] leading-relaxed text-[color:var(--fg)]">{post.text}</p>
|
|
|
|
<div className="mt-4 flex items-center justify-between border-t border-[color:var(--border)] pt-3">
|
|
<span className="text-[12px] text-[color:var(--fg-dim)]">{post.date}</span>
|
|
<span className="inline-flex items-center gap-3 text-[12px] text-[color:var(--fg-dim)]">
|
|
{typeof post.likes === "number" ? (
|
|
<span className="inline-flex items-center gap-1">
|
|
<svg className="h-3.5 w-3.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
|
|
<path d="M12 21s-7.5-4.6-10-9.3C.4 8.4 2 5 5.2 5c1.9 0 3.2 1 3.8 2.2H11C11.6 6 12.9 5 14.8 5 18 5 19.6 8.4 22 11.7 19.5 16.4 12 21 12 21Z" />
|
|
</svg>
|
|
{post.likes}
|
|
</span>
|
|
) : null}
|
|
<span className="font-medium text-[color:var(--fg-muted)] transition-colors group-hover:text-[color:var(--accent)]">
|
|
View on X
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</a>
|
|
);
|
|
}
|