Add homepage social proof
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 2.0 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 2.9 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
@@ -38,7 +38,7 @@ import {
|
|||||||
getDownloadOptions,
|
getDownloadOptions,
|
||||||
useDetectedPlatform,
|
useDetectedPlatform,
|
||||||
AppleIcon,
|
AppleIcon,
|
||||||
AndroidIcon,
|
PlayStoreIcon,
|
||||||
TerminalIcon,
|
TerminalIcon,
|
||||||
GlobeIcon,
|
GlobeIcon,
|
||||||
} from "~/downloads";
|
} from "~/downloads";
|
||||||
@@ -87,6 +87,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
|||||||
<div className="bg-background">
|
<div className="bg-background">
|
||||||
<main className="p-6 md:p-20 md:pt-40 max-w-5xl mx-auto">
|
<main className="p-6 md:p-20 md:pt-40 max-w-5xl mx-auto">
|
||||||
<div className="space-y-24">
|
<div className="space-y-24">
|
||||||
|
<SocialProofWall />
|
||||||
<MultiProviderSection />
|
<MultiProviderSection />
|
||||||
<SelfHostedSection />
|
<SelfHostedSection />
|
||||||
<ServiceProxySection />
|
<ServiceProxySection />
|
||||||
@@ -129,6 +130,88 @@ const PI_BADGE_ICON = <PiIcon className="h-6 w-6" />;
|
|||||||
const FEATURED_AGENT_COUNT = 5;
|
const FEATURED_AGENT_COUNT = 5;
|
||||||
const ADDITIONAL_AGENT_COUNT = AGENT_PAGES.length - FEATURED_AGENT_COUNT;
|
const ADDITIONAL_AGENT_COUNT = AGENT_PAGES.length - FEATURED_AGENT_COUNT;
|
||||||
|
|
||||||
|
const SOCIAL_PROOF_TWEETS = [
|
||||||
|
{
|
||||||
|
name: "Cam",
|
||||||
|
handle: "@ceeebeeebeee",
|
||||||
|
date: "Apr 6, 2026",
|
||||||
|
avatar: "/social-proof/ceeebeeebeee.jpg",
|
||||||
|
url: "https://x.com/ceeebeeebeee/status/2041008798798864537",
|
||||||
|
text: "without a doubt the most slept on orchestrator right now. Open source, every OS, and a mobile experience that truly blew me away.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Erik Sherman",
|
||||||
|
handle: "@erikksherman",
|
||||||
|
date: "Apr 11, 2026",
|
||||||
|
avatar: "/social-proof/erikksherman.jpg",
|
||||||
|
url: "https://x.com/erikksherman/status/2043011630590751008",
|
||||||
|
text: "control agents from anywhere - mac, phone, web. one simple change transformed my health while INCREASING productivity",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Aman Kumar Jagdev",
|
||||||
|
handle: "@amankumarjagdev",
|
||||||
|
date: "Apr 16, 2026",
|
||||||
|
avatar: "/social-proof/amankumarjagdev.jpg",
|
||||||
|
url: "https://x.com/amankumarjagdev/status/2044815258414674307",
|
||||||
|
text: "I have tried 100s of agent orchestrator, cli and gui. the best one i have found. Please give it a try! it's really good",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "RUI",
|
||||||
|
handle: "@tietougongshiba",
|
||||||
|
date: "May 3, 2026",
|
||||||
|
avatar: "/social-proof/tietougongshiba.jpg",
|
||||||
|
url: "https://x.com/tietougongshiba/status/2050886374941925754",
|
||||||
|
text: "Being able to check and manage agent progress from my phone while I'm out is so convenient.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Jason Torres",
|
||||||
|
handle: "@jasontorres",
|
||||||
|
date: "May 11, 2026",
|
||||||
|
avatar: "/social-proof/jasontorres.jpg",
|
||||||
|
url: "https://x.com/jasontorres/status/2053875385515790731",
|
||||||
|
text: "Can interchange between Codex, Claude Code, Opencode, Pi. Stable mobile and desktop apps connected through a secure relay from your VMs.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "A9",
|
||||||
|
handle: "@aadtyn",
|
||||||
|
date: "May 29, 2026",
|
||||||
|
avatar: "/social-proof/aadtyn.jpg",
|
||||||
|
url: "https://x.com/aadtyn/status/2060371229773803943",
|
||||||
|
text: "cross platform agent orchestration with inbuilt relay and tailscale / self host daemon options + the best UI ive seen in this segment",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "boris evstratov",
|
||||||
|
handle: "@bevstratov",
|
||||||
|
date: "May 30, 2026",
|
||||||
|
avatar: "/social-proof/bevstratov.jpg",
|
||||||
|
url: "https://x.com/bevstratov/status/2060733983042781550",
|
||||||
|
text: "It’s an incredible piece of software. The last building block I needed to fully work from my phone. everything super smooth.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Arnold Gamboa",
|
||||||
|
handle: "@arnoldgamboa",
|
||||||
|
date: "May 28, 2026",
|
||||||
|
avatar: "/social-proof/arnoldgamboa.jpg",
|
||||||
|
url: "https://x.com/arnoldgamboa/status/2059832028099436921",
|
||||||
|
text: "Paseo is a really good interface for Pi. It’s not the only thing it does, but that’s my current use case for now.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Dong",
|
||||||
|
handle: "@dongnaebi",
|
||||||
|
date: "Apr 12, 2026",
|
||||||
|
avatar: "/social-proof/dongnaebi.jpg",
|
||||||
|
url: "https://x.com/dongnaebi/status/2043162391941398735",
|
||||||
|
text: "Paseo is the best software I've used this year. Absolutely amazing!",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
const SOCIAL_PROOF_ROWS = [
|
||||||
|
{ id: "top", tweets: SOCIAL_PROOF_TWEETS.slice(0, 5), reverse: false },
|
||||||
|
{ id: "bottom", tweets: SOCIAL_PROOF_TWEETS.slice(5), reverse: true },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type SocialProofTweet = (typeof SOCIAL_PROOF_TWEETS)[number];
|
||||||
|
|
||||||
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
||||||
const [hovered, setHovered] = React.useState(false);
|
const [hovered, setHovered] = React.useState(false);
|
||||||
const handleMouseEnter = React.useCallback(() => setHovered(true), []);
|
const handleMouseEnter = React.useCallback(() => setHovered(true), []);
|
||||||
@@ -173,17 +256,100 @@ function FeatureSection({
|
|||||||
whileInView={FADE_IN}
|
whileInView={FADE_IN}
|
||||||
viewport={VIEWPORT_60}
|
viewport={VIEWPORT_60}
|
||||||
transition={EASE_OUT_05}
|
transition={EASE_OUT_05}
|
||||||
className="space-y-8"
|
|
||||||
>
|
>
|
||||||
<div className="space-y-2">
|
<SectionTitle title={title} description={description} />
|
||||||
<h2 className="text-3xl font-medium">{title}</h2>
|
|
||||||
<p className="text-base text-muted-foreground max-w-lg">{description}</p>
|
|
||||||
</div>
|
|
||||||
{children}
|
{children}
|
||||||
</motion.section>
|
</motion.section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function SectionTitle({ title, description }: { title: string; description: string }) {
|
||||||
|
return (
|
||||||
|
<div className="mb-12 space-y-2">
|
||||||
|
<h2 className="text-3xl font-medium">{title}</h2>
|
||||||
|
<p className="text-base text-muted-foreground max-w-lg">{description}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SocialProofWall() {
|
||||||
|
return (
|
||||||
|
<motion.section
|
||||||
|
initial={FADE_IN_UP}
|
||||||
|
whileInView={FADE_IN}
|
||||||
|
viewport={VIEWPORT_60}
|
||||||
|
transition={EASE_OUT_05}
|
||||||
|
>
|
||||||
|
<SectionTitle
|
||||||
|
title="Loved by developers"
|
||||||
|
description="See what developers are saying about Paseo."
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="social-proof-marquee space-y-4 overflow-hidden">
|
||||||
|
{SOCIAL_PROOF_ROWS.map((row) => (
|
||||||
|
<SocialProofRow key={row.id} tweets={row.tweets} reverse={row.reverse} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</motion.section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SocialProofRow({
|
||||||
|
tweets,
|
||||||
|
reverse,
|
||||||
|
}: {
|
||||||
|
tweets: readonly SocialProofTweet[];
|
||||||
|
reverse: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="social-proof-row">
|
||||||
|
<div className={`social-proof-track ${reverse ? "social-proof-track-reverse" : ""}`}>
|
||||||
|
<div className="flex shrink-0 gap-4 px-2">
|
||||||
|
{tweets.map((tweet) => (
|
||||||
|
<SocialProofCard key={tweet.url} tweet={tweet} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 gap-4 px-2" aria-hidden="true">
|
||||||
|
{tweets.map((tweet) => (
|
||||||
|
<SocialProofCard key={`${tweet.url}-clone`} tweet={tweet} inert />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function SocialProofCard({ tweet, inert }: { tweet: SocialProofTweet; inert?: boolean }) {
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
href={tweet.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
tabIndex={inert ? -1 : undefined}
|
||||||
|
className="group flex h-[154px] w-[320px] shrink-0 flex-col justify-between overflow-hidden rounded-2xl border border-white/10 bg-white/[0.03] p-4 transition-colors hover:border-white/20 hover:bg-white/[0.05] md:w-[420px]"
|
||||||
|
aria-label={`Read ${tweet.name}'s original post`}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<div className="flex min-w-0 items-center gap-3">
|
||||||
|
<img
|
||||||
|
src={tweet.avatar}
|
||||||
|
alt=""
|
||||||
|
width={28}
|
||||||
|
height={28}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="h-7 w-7 shrink-0 rounded-full bg-white/10 object-cover"
|
||||||
|
/>
|
||||||
|
<p className="truncate text-sm font-medium text-white/60">{tweet.handle}</p>
|
||||||
|
</div>
|
||||||
|
<p className="social-proof-card-text mt-4 text-sm leading-relaxed text-white/72">
|
||||||
|
{tweet.text}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function MultiProviderSection() {
|
function MultiProviderSection() {
|
||||||
const providers = [
|
const providers = [
|
||||||
{ name: "Claude Code", icon: <ClaudeIcon size={28} /> },
|
{ name: "Claude Code", icon: <ClaudeIcon size={28} /> },
|
||||||
@@ -198,19 +364,8 @@ function MultiProviderSection() {
|
|||||||
title="Use the best agent for the job"
|
title="Use the best agent for the job"
|
||||||
description="Run multiple providers from a single interface. Paseo runs the native agent harness as you'd normally run it, with your skills, config and MCP servers intact."
|
description="Run multiple providers from a single interface. Paseo runs the native agent harness as you'd normally run it, with your skills, config and MCP servers intact."
|
||||||
>
|
>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
|
||||||
{providers.slice(0, 3).map((p) => (
|
{providers.map((p) => (
|
||||||
<div
|
|
||||||
key={p.name}
|
|
||||||
className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4"
|
|
||||||
>
|
|
||||||
<span className="text-white/80">{p.icon}</span>
|
|
||||||
<span className="font-medium">{p.name}</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-4 sm:w-full">
|
|
||||||
{providers.slice(3).map((p) => (
|
|
||||||
<div
|
<div
|
||||||
key={p.name}
|
key={p.name}
|
||||||
className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4"
|
className="flex items-center justify-center gap-3 rounded-xl border border-white/10 bg-white/[0.03] px-5 py-4"
|
||||||
@@ -877,7 +1032,7 @@ function GetStarted() {
|
|||||||
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
|
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
|
||||||
aria-label="Google Play"
|
aria-label="Google Play"
|
||||||
>
|
>
|
||||||
<AndroidIcon className="h-5 w-5" />
|
<PlayStoreIcon className="h-5 w-5" />
|
||||||
</a>
|
</a>
|
||||||
<ServerInstallButton />
|
<ServerInstallButton />
|
||||||
</div>
|
</div>
|
||||||
@@ -1422,7 +1577,7 @@ function CLISection() {
|
|||||||
title="Fully scriptable"
|
title="Fully scriptable"
|
||||||
description="Everything you can do in the app, you can do from the terminal."
|
description="Everything you can do in the app, you can do from the terminal."
|
||||||
>
|
>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="mb-3 flex flex-wrap gap-2">
|
||||||
{cliExamples.map((example, i) => (
|
{cliExamples.map((example, i) => (
|
||||||
<CLITabButton
|
<CLITabButton
|
||||||
key={example.title}
|
key={example.title}
|
||||||
@@ -1434,7 +1589,7 @@ function CLISection() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div className="mb-3">
|
||||||
<CLICodeBlock>{active.code}</CLICodeBlock>
|
<CLICodeBlock>{active.code}</CLICodeBlock>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,26 @@ export function AndroidIcon(props: React.SVGProps<SVGSVGElement>) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function PlayStoreIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path d="M3.27 2.17c-.2.21-.32.55-.32 1v17.66c0 .45.12.79.33 1l9.87-9.84z" fill="#00F076" />
|
||||||
|
<path
|
||||||
|
d="m16.43 8.72-3.28 3.27 3.29 3.28 3.9-2.22c1.13-.64 1.13-1.42 0-2.06z"
|
||||||
|
fill="#FFCE00"
|
||||||
|
/>
|
||||||
|
<path d="m13.15 11.99 3.28-3.27L4.63 2.01c-.56-.32-1.04-.28-1.36.16z" fill="#00D6FF" />
|
||||||
|
<path d="M3.28 21.83c.32.44.8.48 1.36.16l11.8-6.72-3.29-3.28z" fill="#FF3A44" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function WindowsIcon(props: React.SVGProps<SVGSVGElement>) {
|
export function WindowsIcon(props: React.SVGProps<SVGSVGElement>) {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -9,6 +9,55 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes social-proof-scroll {
|
||||||
|
from {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-marquee {
|
||||||
|
margin-inline: calc(50% - 50vw);
|
||||||
|
mask-image: linear-gradient(to right, transparent, black 8%, black 92%, transparent);
|
||||||
|
overflow-x: clip;
|
||||||
|
padding-inline: max(1.5rem, calc((100vw - 80rem) / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-row {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-track {
|
||||||
|
display: flex;
|
||||||
|
width: max-content;
|
||||||
|
animation: social-proof-scroll 46s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-track-reverse {
|
||||||
|
animation-direction: reverse;
|
||||||
|
animation-duration: 52s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-marquee:hover .social-proof-track {
|
||||||
|
animation-play-state: paused;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-proof-card-text {
|
||||||
|
display: -webkit-box;
|
||||||
|
overflow: hidden;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
-webkit-line-clamp: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.social-proof-track {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* Title font - centralized for easy changes */
|
/* Title font - centralized for easy changes */
|
||||||
.font-title {
|
.font-title {
|
||||||
font-family: inherit;
|
font-family: inherit;
|
||||||
|
|||||||