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
1 change: 1 addition & 0 deletions src/components/common/overall-layout/site-footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const productLinks = [
{ label: "Governance", href: "/governance" },
{ label: "DRep Explorer", href: "/governance/drep" },
{ label: "Import a wallet", href: "/wallets/import-wallet" },
{ label: "Blog", href: "/blog" },
];

const developerLinks = [
Expand Down
68 changes: 68 additions & 0 deletions src/components/pages/blog/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import Link from "next/link";
import type { ArticleMeta } from "@/lib/seo";
import { Reveal } from "@/components/ui/reveal";

/** Format an ISO date (YYYY-MM-DD) as e.g. "June 29, 2026" in UTC (stable). */
export function formatDate(iso: string): string {
if (!iso) return "";
const d = new Date(iso);
if (Number.isNaN(d.getTime())) return iso;
return d.toLocaleDateString("en-US", {
year: "numeric",
month: "long",
day: "numeric",
timeZone: "UTC",
});
}

export default function PageBlogIndex({ posts }: { posts: ArticleMeta[] }) {
return (
<div>
<section className="container mx-auto px-4 py-16 md:py-20">
<Reveal className="mx-auto max-w-3xl text-center">
<h1 className="text-4xl font-bold tracking-tight sm:text-5xl">Blog</h1>
<p className="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">
Guides and updates on Cardano multisig treasuries, governance, and
AI-agent automation — from the team behind Mesh Multisig.
</p>
</Reveal>

<div className="mx-auto mt-12 max-w-3xl space-y-6">
{posts.length === 0 ? (
<p className="text-center text-muted-foreground">
No posts yet — check back soon.
</p>
) : (
posts.map((post, i) => (
<Reveal key={post.slug} delayMs={i * 60}>
<Link
href={`/blog/${post.slug}`}
className="block rounded-2xl border border-zinc-200/70 bg-white/60 p-6 shadow-sm backdrop-blur-sm transition-all hover:-translate-y-0.5 hover:border-zinc-300 hover:shadow-md hover:no-underline dark:border-zinc-800/70 dark:bg-zinc-900/40 dark:hover:border-zinc-700"
>
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<time dateTime={post.date}>{formatDate(post.date)}</time>
{post.tags?.slice(0, 3).map((t) => (
<span
key={t}
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium"
>
{t}
</span>
))}
</div>
<h2 className="mt-2 text-xl font-semibold tracking-tight text-foreground sm:text-2xl">
{post.title}
</h2>
<p className="mt-2 text-muted-foreground">{post.description}</p>
<span className="mt-4 inline-flex items-center text-sm font-medium text-primary">
Read more →
</span>
</Link>
</Reveal>
))
)}
</div>
</section>
</div>
);
}
90 changes: 90 additions & 0 deletions src/components/pages/blog/post.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import Link from "next/link";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { ArrowLeft, Download } from "lucide-react";
import type { BlogPost } from "@/lib/blog";
import { Reveal } from "@/components/ui/reveal";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { formatDate } from "@/components/pages/blog";

// Prose styling via arbitrary child selectors — the repo has no @tailwindcss/typography.
const PROSE =
"max-w-none text-[15px] leading-relaxed text-muted-foreground " +
"[&_h2]:mt-10 [&_h2]:text-2xl [&_h2]:font-bold [&_h2]:tracking-tight [&_h2]:text-foreground " +
"[&_h3]:mt-8 [&_h3]:text-xl [&_h3]:font-semibold [&_h3]:text-foreground " +
"[&_p]:mt-4 [&_ul]:mt-4 [&_ul]:list-disc [&_ul]:pl-6 [&_ol]:mt-4 [&_ol]:list-decimal [&_ol]:pl-6 " +
"[&_li]:mt-1.5 [&_li>strong]:text-foreground " +
"[&_a]:font-medium [&_a]:text-primary [&_a]:underline [&_a]:underline-offset-2 " +
"[&_strong]:font-semibold [&_strong]:text-foreground " +
"[&_code]:rounded [&_code]:bg-muted [&_code]:px-1.5 [&_code]:py-0.5 [&_code]:text-sm [&_code]:font-mono " +
"[&_pre]:mt-4 [&_pre]:overflow-x-auto [&_pre]:rounded-lg [&_pre]:bg-zinc-950 [&_pre]:p-4 [&_pre]:text-sm [&_pre]:text-zinc-100 [&_pre_code]:bg-transparent [&_pre_code]:p-0 " +
"[&_blockquote]:mt-4 [&_blockquote]:border-l-2 [&_blockquote]:border-zinc-300 [&_blockquote]:pl-4 [&_blockquote]:italic dark:[&_blockquote]:border-zinc-700 " +
"[&_hr]:my-8 [&_hr]:border-border " +
"[&_table]:mt-4 [&_table]:w-full [&_th]:border [&_th]:border-border [&_th]:px-3 [&_th]:py-2 [&_th]:text-left [&_td]:border [&_td]:border-border [&_td]:px-3 [&_td]:py-2";

export default function PageBlogPost({ post }: { post: BlogPost }) {
const { meta, content } = post;

return (
<div>
<article className="container mx-auto px-4 py-12 md:py-16">
<div className="mx-auto max-w-3xl">
<Link
href="/blog"
className="inline-flex items-center gap-1 text-sm text-muted-foreground transition-colors hover:text-foreground"
>
<ArrowLeft className="h-4 w-4" />
All posts
</Link>

<Reveal className="mt-6">
<div className="flex flex-wrap items-center gap-2 text-xs text-muted-foreground">
<time dateTime={meta.date}>{formatDate(meta.date)}</time>
{meta.tags?.slice(0, 4).map((t) => (
<span
key={t}
className="rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium"
>
{t}
</span>
))}
</div>
<h1 className="mt-3 text-3xl font-bold tracking-tight sm:text-4xl">
{meta.title}
</h1>
<p className="mt-3 text-lg text-muted-foreground">
{meta.description}
</p>
</Reveal>

<Separator className="my-8" />

<div className={PROSE}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>{content}</ReactMarkdown>
</div>

{/* CTA */}
<div className="mt-12 rounded-2xl border border-zinc-200/70 bg-white/60 p-6 backdrop-blur-sm dark:border-zinc-800/70 dark:bg-zinc-900/40">
<h2 className="text-lg font-semibold">Ready to try it?</h2>
<p className="mt-2 text-sm text-muted-foreground">
Create a multisig wallet, or drop the skill into your AI agent to
automate the busywork — signing always stays with you.
</p>
<div className="mt-4 flex flex-col gap-3 sm:flex-row">
<Button asChild>
<Link href="/">Open Mesh Multisig</Link>
</Button>
<Button asChild variant="outline">
<a href="/api/skill" download="multisig-skill.md">
<Download className="mr-2 h-4 w-4" />
Download skill
</a>
</Button>
</div>
</div>
</div>
</article>
</div>
);
}
71 changes: 70 additions & 1 deletion src/components/pages/homepage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,9 @@ import CardUI from "@/components/ui/card-content";
import RowLabelInfo from "@/components/common/row-label-info";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { Database, Bot, Code, Download, Check } from "lucide-react";
import { Database, Bot, Code, Download, Check, Sparkles } from "lucide-react";
import { Reveal } from "@/components/ui/reveal";
import { Typewriter } from "@/components/ui/typewriter";
import {
MultisigWalletPreview,
WalletListPreview,
Expand All @@ -28,6 +29,16 @@ import {
StakingPreview,
} from "@/components/pages/homepage/previews";

// Example prompts cycled by the "Connect your AI agent" typewriter. The first
// is the literal flow most users start with; the rest hint at what an agent can
// do once it holds the multisig skill.
const AGENT_PROMPTS = [
"Connect to my https://multisig.meshjs.dev/ wallet",
"List the pending transactions on our treasury",
"Draft a 200 ₳ payout to the dev fund and request signatures",
"Show who still needs to sign the pending payout",
];

// DApp Card Component
function DappCard({ title, description, url }: { title: string; description: string; url: string }) {
const [ogImage, setOgImage] = useState<string | null>(null);
Expand Down Expand Up @@ -282,6 +293,64 @@ export function PageHomepage() {
</Reveal>
</section>

{/* Connect your AI agent – skill download + live prompt demo, up top */}
<section className="container mx-auto px-4 pb-8">
<Reveal className="mx-auto max-w-5xl">
<div className="overflow-hidden rounded-2xl border border-zinc-200/70 bg-white/60 p-6 shadow-sm backdrop-blur-sm dark:border-zinc-800/70 dark:bg-zinc-900/40 sm:p-8 md:p-10">
<div className="grid items-center gap-8 md:grid-cols-2">
{/* Copy + actions */}
<div>
<div className="inline-flex items-center gap-2 rounded-full border border-zinc-200 bg-muted/60 px-3 py-1 text-xs font-medium text-muted-foreground dark:border-zinc-800">
<Sparkles className="h-3.5 w-3.5" />
AI-native multisig
</div>
<h2 className="mt-4 text-3xl font-bold tracking-tight sm:text-4xl">
Connect your AI agent
</h2>
<p className="mt-3 text-muted-foreground">
Drop the multisig skill into Claude Code, Cursor, or any agent and let
it work alongside your treasury — read pending transactions, draft
payouts, and track approvals through the authenticated v1 API. The
agent can&apos;t sign for you: keys and signatures always stay with you
and your co-signers.
</p>
<div className="mt-6 flex flex-col gap-3 sm:flex-row sm:items-center">
<Button asChild size="lg">
<a href="/api/skill" download="multisig-skill.md">
<Download className="mr-2 h-4 w-4" />
Download skill
</a>
</Button>
<Button asChild size="lg" variant="outline">
<Link href="#developers-and-bots">
<Bot className="mr-2 h-4 w-4" />
Developer &amp; bot docs
</Link>
</Button>
</div>
</div>

{/* Animated agent prompt */}
<div className="rounded-xl border border-zinc-800 bg-zinc-950 p-4 font-mono text-sm text-zinc-100 shadow-lg sm:p-5">
<div className="flex items-center gap-1.5 pb-3">
<span className="h-3 w-3 rounded-full bg-red-400/80" />
<span className="h-3 w-3 rounded-full bg-yellow-400/80" />
<span className="h-3 w-3 rounded-full bg-green-400/80" />
<span className="ml-2 text-xs text-zinc-500">agent</span>
</div>
<div className="flex min-h-[5rem] items-start gap-2 leading-relaxed">
<span className="select-none text-emerald-400">›</span>
<Typewriter phrases={AGENT_PROMPTS} className="text-zinc-100" />
</div>
<div className="mt-3 border-t border-zinc-800 pt-3 text-xs text-zinc-500">
Read &amp; draft only — signing stays with you and your co-signers.
</div>
</div>
</div>
</div>
</Reveal>
</section>

<Separator className="my-8" />

{/* Multisig signing explainer */}
Expand Down
9 changes: 8 additions & 1 deletion src/components/ui/metatags.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export default function Metatags({
/** Open Graph object type. "website" for marketing pages, "article" for content. */
type = "website",
noindex = false,
/** Extra JSON-LD blocks (e.g. an Article) appended to the site-wide ones. */
extraJsonLd,
}: {
title?: string;
description?: string;
Expand All @@ -36,10 +38,15 @@ export default function Metatags({
path?: string;
type?: string;
noindex?: boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
extraJsonLd?: Record<string, any>[];
}) {
const canonical = absoluteUrl(path);
const imageUrl = absoluteUrl(image);
const jsonLd = JSON.stringify(buildJsonLd(path));
const jsonLd = JSON.stringify([
...buildJsonLd(path),
...(extraJsonLd ?? []),
]);

return (
<>
Expand Down
100 changes: 100 additions & 0 deletions src/components/ui/typewriter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { useEffect, useRef, useState } from "react";
import { cn } from "@/lib/utils";
import { useReducedMotion } from "@/hooks/useReducedMotion";

/**
* Types a cycling list of phrases one character at a time, then deletes and
* advances to the next — a classic "typewriter" prompt effect.
*
* - Honors `prefers-reduced-motion`: reduced-motion users see the first phrase
* rendered statically with no typing/deleting and a non-blinking caret.
* - SSR-safe: the first paint is an empty string (matches server output), so
* there is no hydration mismatch; animation only starts after mount.
* - All timers are cleared on unmount / dependency change.
*/
export function Typewriter({
phrases,
className,
typeMs = 45,
deleteMs = 25,
holdMs = 1600,
startDelayMs = 350,
}: {
phrases: string[];
className?: string;
/** ms per character while typing */
typeMs?: number;
/** ms per character while deleting */
deleteMs?: number;
/** ms to hold a fully-typed phrase before deleting */
holdMs?: number;
/** ms to wait before the first character is typed */
startDelayMs?: number;
}) {
const reduced = useReducedMotion();
const [text, setText] = useState("");
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);

const first = phrases[0] ?? "";

useEffect(() => {
if (reduced || phrases.length === 0) {
setText(first);
return;
}

let phrase = 0;
let char = 0;
let deleting = false;

const tick = () => {
const current = phrases[phrase] ?? "";

if (!deleting) {
char += 1;
setText(current.slice(0, char));
if (char === current.length) {
deleting = true;
timer.current = setTimeout(tick, holdMs);
return;
}
timer.current = setTimeout(tick, typeMs);
return;
}

char -= 1;
setText(current.slice(0, char));
if (char === 0) {
deleting = false;
phrase = (phrase + 1) % phrases.length;
timer.current = setTimeout(tick, typeMs * 4);
return;
}
timer.current = setTimeout(tick, deleteMs);
};

timer.current = setTimeout(tick, startDelayMs);
return () => {
if (timer.current) clearTimeout(timer.current);
};
// phrases is treated as a stable literal by callers; join keeps the effect
// honest if the list ever changes without remounting.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [reduced, phrases.join(""), typeMs, deleteMs, holdMs, startDelayMs]);

return (
<span className={cn("whitespace-pre-wrap break-words", className)}>
{text}
<span
aria-hidden="true"
className={cn(
"ml-0.5 inline-block w-[1px] -translate-y-[1px] self-stretch border-r-2 border-current align-middle",
reduced ? "opacity-70" : "animate-pulse",
)}
style={{ height: "1em" }}
/>
</span>
);
}

export default Typewriter;
Loading
Loading