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
8 changes: 7 additions & 1 deletion frontend/next.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import type { NextConfig } from "next";
import path from "path";

const nextConfig: NextConfig = {
/* config options here */
// This repo holds two independent npm projects (contract tests at the root,
// this Next.js app in /frontend), so there are two lockfiles. Pin the
// workspace root to this directory so Turbopack stops guessing.
turbopack: {
root: path.resolve(__dirname),
},
};

export default nextConfig;
4 changes: 4 additions & 0 deletions frontend/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import { Hero } from "@/components/landing/hero";
import { WhoItsFor } from "@/components/landing/who-its-for";
import { Features } from "@/components/landing/features";
import { AssistantSection } from "@/components/landing/assistant-section";
import { FAQ } from "@/components/landing/faq";
import { CTASection } from "@/components/landing/cta-section";
import { Footer } from "@/components/landing/footer";
import Link from "next/link";
Expand Down Expand Up @@ -40,6 +42,8 @@ export default function LandingPage() {
<Hero />
<WhoItsFor />
<Features />
<AssistantSection />
<FAQ />
<CTASection />
<Footer />
</div>
Expand Down
198 changes: 198 additions & 0 deletions frontend/src/components/landing/assistant-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"use client";

import Link from "next/link";
import { motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import {
Zap,
Hash,
User,
Building2,
Search,
Sparkles,
ArrowRight,
} from "lucide-react";

const capabilities = [
{
icon: Hash,
title: "Look up any stream",
text: "Drop in a stream ID and see status, deposit, and exactly what's claimable right now.",
},
{
icon: User,
title: "Search by address",
text: "Paste a sender or recipient address to pull every stream flowing in or out of it.",
},
{
icon: Building2,
title: "Inspect a workspace",
text: "Check a registered DAO or business — streams created, total deposited, active status.",
},
{
icon: Zap,
title: "Read the chain live",
text: "Ask for the current block height and other on-chain state without leaving the app.",
},
];

// Preview of the OpenClaw panel — mirrors the real in-app widget so visitors
// see exactly what they get once they launch the app.
function AssistantPreview() {
const tabs = [
{ icon: Hash, label: "Stream", active: true },
{ icon: User, label: "Sender", active: false },
{ icon: User, label: "Recipient", active: false },
{ icon: Building2, label: "Workspace", active: false },
{ icon: Zap, label: "Block", active: false },
];

return (
<div className="flex w-full max-w-sm flex-col overflow-hidden rounded-2xl border border-border bg-surface-1 shadow-2xl">
{/* Header */}
<div className="flex items-center gap-2.5 border-b border-border bg-surface-0 px-4 py-3">
<div className="flex h-7 w-7 items-center justify-center rounded-lg bg-brand-500/10">
<Zap className="h-3.5 w-3.5 text-brand-400" />
</div>
<div>
<p className="text-sm font-semibold text-zinc-100">OpenClaw Assistant</p>
<p className="text-[10px] text-zinc-500">
Query streams, workspaces, and blockchain state
</p>
</div>
</div>

{/* Tabs */}
<div className="flex gap-1 overflow-x-auto border-b border-border px-2 py-1.5">
{tabs.map((t) => (
<span
key={t.label}
className={`flex items-center gap-1 whitespace-nowrap rounded-lg px-2.5 py-1 text-[11px] font-medium ${
t.active
? "bg-brand-500/10 text-brand-400"
: "text-zinc-500"
}`}
>
<t.icon className="h-3 w-3" />
{t.label}
</span>
))}
</div>

{/* Result */}
<div className="space-y-3 p-3">
<div className="rounded-xl border border-border bg-surface-0 p-3">
<div className="mb-2 flex items-center gap-1.5">
<span className="rounded-md bg-surface-3 px-1.5 py-0.5 text-[10px] font-medium uppercase text-zinc-400">
stream
</span>
<span className="font-mono text-[10px] text-zinc-600">7</span>
</div>
<div className="space-y-2 text-xs">
<div className="flex items-center justify-between">
<span className="font-medium text-zinc-200">Stream #7</span>
<span className="font-medium text-emerald-400">Active</span>
</div>
<div className="grid grid-cols-2 gap-x-3 gap-y-1 text-zinc-400">
<div>Sender</div>
<div className="truncate font-mono text-zinc-300">SP2J6ZY...</div>
<div>Recipient</div>
<div className="truncate font-mono text-zinc-300">SP3FBR2...</div>
<div>Deposited</div>
<div className="font-mono text-zinc-300">0.500000 sBTC</div>
<div>Claimable</div>
<div className="font-mono text-emerald-400">0.184210 sBTC</div>
<div>Progress</div>
<div className="font-mono text-zinc-300">36.8%</div>
</div>
</div>
</div>
</div>

{/* Input */}
<div className="flex gap-2 border-t border-border px-3 py-2.5">
<div className="flex flex-1 items-center rounded-lg border border-border bg-surface-2 px-3 py-2 text-xs text-zinc-500">
Stream ID (e.g. 7)
</div>
<div className="flex h-8 w-9 items-center justify-center rounded-lg bg-brand-500">
<Search className="h-3.5 w-3.5 text-white" />
</div>
</div>
</div>
);
}

export function AssistantSection() {
return (
<section className="px-6 py-24">
<div className="mx-auto max-w-6xl">
<div className="grid grid-cols-1 items-center gap-12 lg:grid-cols-2">
{/* Copy */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5 }}
>
<div className="mb-4 inline-flex items-center gap-2 rounded-full border border-brand-500/20 bg-brand-500/5 px-3 py-1 text-xs font-medium text-brand-400">
<Sparkles className="h-3.5 w-3.5" />
Built in: OpenClaw Assistant
</div>
<h2 className="text-3xl font-bold text-zinc-100 md:text-4xl">
An assistant that reads the chain for you
</h2>
<p className="mt-3 max-w-lg text-zinc-500">
OpenClaw lives inside the app. Ask it about any stream, address, or
workspace and it answers in a click — no block explorer, no raw
contract calls, no guesswork.
</p>

<div className="mt-8 grid grid-cols-1 gap-3 sm:grid-cols-2">
{capabilities.map((cap) => (
<div
key={cap.title}
className="rounded-2xl border border-border bg-surface-1 p-4 transition-colors hover:border-brand-500/20 hover:bg-surface-2"
>
<div className="mb-2.5 flex h-9 w-9 items-center justify-center rounded-xl bg-brand-500/10 text-brand-400">
<cap.icon className="h-4 w-4" />
</div>
<h3 className="text-sm font-semibold text-zinc-200">
{cap.title}
</h3>
<p className="mt-1 text-xs leading-relaxed text-zinc-500">
{cap.text}
</p>
</div>
))}
</div>

<div className="mt-8">
<Link href="/dashboard">
<Button size="lg">
Try OpenClaw in the app
<ArrowRight className="h-4 w-4" />
</Button>
</Link>
</div>
</motion.div>

{/* Preview */}
<motion.div
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.5, delay: 0.1 }}
className="flex justify-center lg:justify-end"
>
<div className="relative">
<div className="pointer-events-none absolute -inset-6 rounded-full bg-brand-500/10 blur-3xl" />
<div className="relative">
<AssistantPreview />
</div>
</div>
</motion.div>
</div>
</div>
</section>
);
}
160 changes: 160 additions & 0 deletions frontend/src/components/landing/faq.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
"use client";

import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { Plus, MessageCircle, Send, Github } from "lucide-react";
import { FEEDBACK_URL } from "@/lib/constants";

const faqs = [
{
q: "What is StackStream?",
a: "A Bitcoin-native payment streaming protocol. Instead of paying people in one lump sum on a payday, you open a stream and funds flow to them every few seconds. It runs on Stacks and settles on Bitcoin, so payments are final and can't be reversed.",
},
{
q: "Is it live? Can I use it now?",
a: "Yes. StackStream is live on Stacks mainnet at stackstream.xyz. Connect a Stacks wallet, create a stream, and it starts flowing immediately. You can watch a real balance tick up in the app right now.",
},
{
q: "Which tokens can I stream?",
a: "sBTC, STX, USDA, ALEX, xBTC, or any SIP-010 token. If it follows the SIP-010 standard on Stacks, you can stream it by the second.",
},
{
q: "Is my money safe while it's streaming?",
a: "Funds sit in on-chain escrow, not in anyone's pocket. The recipient can only ever claim what they've already earned, and everything not yet earned stays yours. Pause, top up, or cancel at any time and the unearned remainder comes straight back to your wallet.",
},
{
q: "What is OpenClaw?",
a: "OpenClaw is the assistant built into the app. Ask it about any stream, sender, recipient, or workspace and it pulls the live on-chain answer instantly — no block explorer or raw contract calls needed.",
},
{
q: "Who is StackStream for?",
a: "DAOs and protocol teams paying contributors, businesses running subscriptions or paying suppliers, freelancers who want to get paid as they work, and grant or bounty programs funding builders steadily instead of in one lump sum.",
},
{
q: "How is this different from a normal crypto payment?",
a: "A normal payment is one transfer at one moment. A stream is continuous — money moves second by second for as long as you set it to run, and either side can adjust or stop it mid-flow. Think of it as a tap you turn on, not a calendar reminder.",
},
{
q: "Is the code open and audited?",
a: "The Clarity smart contracts are open source on GitHub and covered by a full test suite that verifies funds are always conserved. You can read every contract and run the tests yourself.",
},
];

function FaqItem({
faq,
isOpen,
onToggle,
}: {
faq: { q: string; a: string };
isOpen: boolean;
onToggle: () => void;
}) {
return (
<div className="overflow-hidden rounded-2xl border border-border bg-surface-1 transition-colors hover:border-brand-500/20">
<button
onClick={onToggle}
className="flex w-full items-center justify-between gap-4 px-5 py-4 text-left sm:px-6 sm:py-5"
aria-expanded={isOpen}
>
<span className="text-sm font-medium text-zinc-100 sm:text-base">
{faq.q}
</span>
<span
className={`flex h-7 w-7 shrink-0 items-center justify-center rounded-full border border-border text-zinc-400 transition-all duration-300 ${
isOpen ? "rotate-45 border-brand-500/40 bg-brand-500/10 text-brand-400" : ""
}`}
>
<Plus className="h-4 w-4" />
</span>
</button>
<AnimatePresence initial={false}>
{isOpen && (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: "auto", opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{ duration: 0.28, ease: "easeInOut" }}
>
<p className="px-5 pb-5 text-sm leading-relaxed text-zinc-400 sm:px-6 sm:pb-6">
{faq.a}
</p>
</motion.div>
)}
</AnimatePresence>
</div>
);
}

export function FAQ() {
const [openIndex, setOpenIndex] = useState<number | null>(0);

return (
<section className="px-6 py-24">
<div className="mx-auto max-w-3xl">
<div className="mb-12 text-center">
<h2 className="text-3xl font-bold text-zinc-100 md:text-4xl">
Frequently asked questions
</h2>
<p className="mx-auto mt-3 max-w-lg text-zinc-500">
Everything you need to know about streaming money on Bitcoin. Still
curious? Reach out below — we answer fast.
</p>
</div>

<div className="space-y-3">
{faqs.map((faq, i) => (
<FaqItem
key={faq.q}
faq={faq}
isOpen={openIndex === i}
onToggle={() => setOpenIndex(openIndex === i ? null : i)}
/>
))}
</div>

{/* Feedback / contact strip */}
<div className="mt-10 rounded-2xl border border-brand-500/20 bg-linear-to-b from-brand-500/5 to-transparent p-6 text-center sm:p-8">
<div className="mx-auto mb-4 flex h-11 w-11 items-center justify-center rounded-xl bg-brand-500/10 text-brand-400">
<MessageCircle className="h-5 w-5" />
</div>
<h3 className="text-lg font-semibold text-zinc-100">
Still have a question?
</h3>
<p className="mx-auto mt-2 max-w-sm text-sm text-zinc-500">
Send us feedback or a question directly, or reach the team on
Telegram. Every message gets read.
</p>
<div className="mt-6 flex flex-col items-center justify-center gap-3 sm:flex-row">
<a
href={FEEDBACK_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-xl bg-brand-500 px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-brand-600"
>
<MessageCircle className="h-4 w-4" />
Send feedback
</a>
<a
href="https://t.me/dev_jaytee"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-xl border border-border bg-surface-2 px-5 py-2.5 text-sm font-medium text-zinc-300 transition-colors hover:border-brand-500/30 hover:text-zinc-100"
>
<Send className="h-4 w-4" />
Telegram
</a>
<a
href="https://github.com/jayteemoney/stackstream"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-xl border border-border bg-surface-2 px-5 py-2.5 text-sm font-medium text-zinc-300 transition-colors hover:border-brand-500/30 hover:text-zinc-100"
>
<Github className="h-4 w-4" />
GitHub
</a>
</div>
</div>
</div>
</section>
);
}
Loading
Loading