mirror of
https://github.com/paperclipai/clipmart.git
synced 2026-11-07 06:15:58 -10:00
feat: add creator dashboard and listing wizard flow
This commit is contained in:
1 parent
6d78f765c1
commit
47c6882a07
15 files changed
+1042
-651
No files matched your search
+7
-115
@@ -1,120 +1,12 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const pillars = [
|
||||
{
|
||||
title: "System-Level Products",
|
||||
text: "ClipMart listings package reporting chains, approvals, and operating defaults so teams launch as systems, not isolated prompts.",
|
||||
},
|
||||
{
|
||||
title: "Open-Source Foundation",
|
||||
text: "Everything is designed for transparent operations and auditable behavior within Paperclip's governance model.",
|
||||
},
|
||||
{
|
||||
title: "Outcome-Oriented Teams",
|
||||
text: "Each blueprint is evaluated on practical output: shipping software, running growth loops, and resolving customer work reliably.",
|
||||
},
|
||||
];
|
||||
|
||||
const principles = [
|
||||
"Role clarity before model tuning",
|
||||
"Governance built-in, never bolted on",
|
||||
"Reusable skills over one-off prompts",
|
||||
"Install paths that keep local environments clean",
|
||||
];
|
||||
|
||||
export const metadata = {
|
||||
title: "About",
|
||||
};
|
||||
|
||||
export default function AboutPage() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-14 px-6 py-12 md:gap-18 md:py-16">
|
||||
<section className="grid gap-8 md:grid-cols-[1.2fr_0.8fr] md:items-end">
|
||||
<div className="fade-up space-y-5">
|
||||
<p className="section-label">About ClipMart</p>
|
||||
<h1 className="section-heading text-5xl leading-[1.03] md:text-7xl">
|
||||
The marketplace for autonomous company architecture.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg text-[var(--ink-700)] md:text-xl">
|
||||
We built ClipMart to let teams skip months of org design and operational trial-and-error.
|
||||
Every listing is a deployable operating model for Paperclip companies.
|
||||
</p>
|
||||
</div>
|
||||
<div className="panel fade-up stagger-2 rounded-3xl p-5">
|
||||
<p className="section-label">What We Ship</p>
|
||||
<ul className="mt-4 space-y-2 text-sm text-[var(--ink-800)]">
|
||||
<li>Org charts with explicit manager chains</li>
|
||||
<li>Agent prompts and reusable skill packs</li>
|
||||
<li>Governance defaults for approvals and budgets</li>
|
||||
<li>Project templates with workspace configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<p className="section-label fade-up">Core Pillars</p>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{pillars.map((pillar, index) => (
|
||||
<article
|
||||
key={pillar.title}
|
||||
className={`panel fade-up rounded-3xl p-5 ${index === 0 ? "stagger-1" : index === 1 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
<h2 className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink-950)]">
|
||||
{pillar.title}
|
||||
</h2>
|
||||
<p className="mt-3 text-sm text-[var(--ink-700)]">{pillar.text}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-6 rounded-3xl bg-[var(--ink-950)] p-6 text-[var(--sand-50)] md:grid-cols-[1fr_1.1fr] md:p-8">
|
||||
<div className="fade-up">
|
||||
<p className="font-[family-name:var(--font-mono)] text-xs tracking-[0.14em] uppercase text-[var(--sand-200)]">
|
||||
Product Principles
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-4xl md:text-5xl">
|
||||
How we evaluate every listing
|
||||
</h2>
|
||||
</div>
|
||||
<ul className="space-y-3">
|
||||
{principles.map((item, index) => (
|
||||
<li
|
||||
key={item}
|
||||
className={`fade-up rounded-2xl border border-white/20 bg-white/6 px-4 py-3 text-sm ${
|
||||
index < 2 ? "stagger-2" : "stagger-3"
|
||||
}`}
|
||||
>
|
||||
{item}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section className="panel fade-up stagger-4 rounded-3xl p-6 md:p-8">
|
||||
<p className="section-label">Build With Us</p>
|
||||
<h2 className="section-heading mt-2 text-4xl md:text-5xl">
|
||||
Publishing proven operating models is the new leverage.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-[var(--ink-700)]">
|
||||
If you run a high-performing Paperclip company, package your setup and publish it to
|
||||
ClipMart. We handle discovery, delivery, and creator monetization rails.
|
||||
<main className="mx-auto max-w-4xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8">
|
||||
<h1 className="font-serif text-4xl text-stone-900">About ClipMart</h1>
|
||||
<p className="mt-3 text-stone-700">
|
||||
ClipMart is the marketplace for whole-company Paperclip configurations.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="rounded-full bg-[var(--ink-950)] px-6 py-3 text-sm font-bold text-[var(--sand-50)] transition hover:bg-[var(--accent-700)]"
|
||||
>
|
||||
View Pricing
|
||||
</Link>
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-full border border-[var(--ink-300)] bg-white/80 px-6 py-3 text-sm font-bold text-[var(--ink-900)] transition hover:border-[var(--ink-900)]"
|
||||
>
|
||||
Back To Homepage
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { db } from "@/db";
|
||||
import { listings, creators, teamBlueprints } from "@/db/schema";
|
||||
import { eq, and, sql } from "drizzle-orm";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { getSession } from "@/lib/api-auth";
|
||||
|
||||
// GET /api/listings/:slug - Listing detail
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
export default function BrowsePage() {
|
||||
return (
|
||||
<div className="mx-auto max-w-7xl px-4 py-12 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold text-gray-900">Browse Marketplace</h1>
|
||||
<p className="mt-2 text-gray-600">
|
||||
Discover team blueprints, agent configs, skills, and governance
|
||||
templates.
|
||||
</p>
|
||||
<div className="mt-8 text-sm text-gray-400">
|
||||
Listings will appear here once the database is connected.
|
||||
<main className="mx-auto max-w-5xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8">
|
||||
<h1 className="font-serif text-4xl text-stone-900">Marketplace browse</h1>
|
||||
<p className="mt-3 text-stone-700">
|
||||
Listing browse UI is being built in a dedicated issue. Creator APIs and wizard flows are available now.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import Link from "next/link";
|
||||
import { db } from "@/db";
|
||||
import { creators, listings } from "@/db/schema";
|
||||
import { getSessionUserId } from "@/lib/api-auth";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import {
|
||||
ListingWizardForm,
|
||||
type ListingWizardInitialValues,
|
||||
} from "@/components/creator/listing-wizard-form";
|
||||
|
||||
function toStringArray(value: unknown): string[] {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.map((item) => String(item)).filter(Boolean);
|
||||
}
|
||||
|
||||
export default async function EditCreatorListingPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { id } = await params;
|
||||
const userId = await getSessionUserId();
|
||||
|
||||
if (!userId) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Edit listing</h1>
|
||||
<p className="mt-3 text-stone-600">Sign in and register as a creator first.</p>
|
||||
<Link
|
||||
href="/creator"
|
||||
className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100"
|
||||
>
|
||||
Go to creator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const creator = await db.query.creators.findFirst({
|
||||
where: eq(creators.userId, userId),
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
if (!creator) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Creator profile required</h1>
|
||||
<p className="mt-3 text-stone-600">Create your profile first.</p>
|
||||
<Link
|
||||
href="/creator"
|
||||
className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100"
|
||||
>
|
||||
Register creator profile
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const listing = await db.query.listings.findFirst({
|
||||
where: and(eq(listings.id, id), eq(listings.creatorId, creator.id)),
|
||||
});
|
||||
|
||||
if (!listing) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Listing not found</h1>
|
||||
<p className="mt-3 text-stone-600">
|
||||
This listing either does not exist or is not owned by your profile.
|
||||
</p>
|
||||
<Link
|
||||
href="/creator"
|
||||
className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100"
|
||||
>
|
||||
Back to dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const initialValues: ListingWizardInitialValues = {
|
||||
id: listing.id,
|
||||
type: listing.type as ListingWizardInitialValues["type"],
|
||||
title: listing.title,
|
||||
tagline: listing.tagline ?? "",
|
||||
description: listing.description ?? "",
|
||||
categories: toStringArray(listing.categories),
|
||||
tags: toStringArray(listing.tags),
|
||||
includedFiles: toStringArray(listing.includedFiles),
|
||||
readmeMarkdown: listing.readmeMarkdown ?? "",
|
||||
agentCount: listing.agentCount,
|
||||
price: Math.round(listing.price / 100),
|
||||
status: listing.status as ListingWizardInitialValues["status"],
|
||||
};
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl px-4 py-10 sm:px-6">
|
||||
<ListingWizardForm mode="edit" listingId={listing.id} initialValues={initialValues} />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import Link from "next/link";
|
||||
import { db } from "@/db";
|
||||
import { creators } from "@/db/schema";
|
||||
import { getSessionUserId } from "@/lib/api-auth";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { ListingWizardForm } from "@/components/creator/listing-wizard-form";
|
||||
|
||||
export default async function NewCreatorListingPage() {
|
||||
const userId = await getSessionUserId();
|
||||
|
||||
if (!userId) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Listing wizard</h1>
|
||||
<p className="mt-3 text-stone-600">Sign in and register as a creator first.</p>
|
||||
<Link
|
||||
href="/creator"
|
||||
className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100"
|
||||
>
|
||||
Go to creator dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const creator = await db.query.creators.findFirst({
|
||||
where: eq(creators.userId, userId),
|
||||
columns: { id: true },
|
||||
});
|
||||
|
||||
if (!creator) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Creator profile required</h1>
|
||||
<p className="mt-3 text-stone-600">Create your profile first, then publish listings.</p>
|
||||
<Link
|
||||
href="/creator"
|
||||
className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100"
|
||||
>
|
||||
Register creator profile
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl px-4 py-10 sm:px-6">
|
||||
<ListingWizardForm mode="create" />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import Link from "next/link";
|
||||
import { db } from "@/db";
|
||||
import { creators, listings } from "@/db/schema";
|
||||
import { getSessionUserId } from "@/lib/api-auth";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import { RegisterCreatorForm } from "@/components/creator/register-creator-form";
|
||||
|
||||
export default async function CreatorDashboardPage() {
|
||||
const userId = await getSessionUserId();
|
||||
|
||||
if (!userId) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Creator dashboard</h1>
|
||||
<p className="mt-3 text-stone-600">
|
||||
Sign in first, then return here to register and publish listings.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const creator = await db.query.creators.findFirst({
|
||||
where: eq(creators.userId, userId),
|
||||
});
|
||||
|
||||
if (!creator) {
|
||||
return (
|
||||
<main className="mx-auto max-w-3xl px-4 py-16 sm:px-6">
|
||||
<RegisterCreatorForm />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const creatorListings = await db
|
||||
.select()
|
||||
.from(listings)
|
||||
.where(eq(listings.creatorId, creator.id))
|
||||
.orderBy(desc(listings.updatedAt));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl space-y-8 px-4 py-10 sm:px-6">
|
||||
<section className="relative overflow-hidden rounded-3xl border border-stone-300 bg-gradient-to-br from-stone-950 via-stone-900 to-amber-900 p-8 text-stone-100">
|
||||
<div className="absolute right-0 top-0 h-40 w-40 translate-x-10 -translate-y-10 rounded-full bg-amber-300/30 blur-3xl" />
|
||||
<p className="text-xs uppercase tracking-[0.25em] text-stone-300">Creator Console</p>
|
||||
<h1 className="mt-3 font-serif text-4xl">{creator.displayName}</h1>
|
||||
<p className="mt-4 max-w-2xl text-sm text-stone-200">
|
||||
{creator.bio || "No bio yet. Add one to help buyers understand your expertise."}
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap gap-3 text-xs text-stone-300">
|
||||
<span className="rounded-full border border-stone-600 px-3 py-1">
|
||||
Installs: {creator.totalInstalls}
|
||||
</span>
|
||||
<span className="rounded-full border border-stone-600 px-3 py-1">
|
||||
Revenue: ${(creator.totalRevenue / 100).toFixed(2)}
|
||||
</span>
|
||||
<span className="rounded-full border border-stone-600 px-3 py-1">
|
||||
Public slug: {slugify(creator.displayName)}
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="rounded-3xl border border-stone-300 bg-stone-50 p-6">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<h2 className="font-serif text-2xl text-stone-900">Your listings</h2>
|
||||
<Link
|
||||
href="/creator/listings/new"
|
||||
className="rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100 transition hover:bg-stone-800"
|
||||
>
|
||||
New listing
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{creatorListings.length ? (
|
||||
<div className="mt-4 overflow-hidden rounded-2xl border border-stone-200">
|
||||
<table className="min-w-full divide-y divide-stone-200 text-left text-sm">
|
||||
<thead className="bg-stone-100 text-stone-600">
|
||||
<tr>
|
||||
<th className="px-4 py-3 font-semibold">Listing</th>
|
||||
<th className="px-4 py-3 font-semibold">Type</th>
|
||||
<th className="px-4 py-3 font-semibold">Status</th>
|
||||
<th className="px-4 py-3 font-semibold">Price</th>
|
||||
<th className="px-4 py-3 font-semibold">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-stone-200 bg-white text-stone-800">
|
||||
{creatorListings.map((listing) => (
|
||||
<tr key={listing.id}>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-semibold">{listing.title}</p>
|
||||
<p className="text-xs text-stone-500">{listing.tagline || "No tagline"}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-xs uppercase tracking-wide text-stone-600">
|
||||
{listing.type.replaceAll("_", " ")}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="rounded-full bg-stone-100 px-2 py-1 text-xs font-semibold text-stone-700">
|
||||
{listing.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">${(listing.price / 100).toFixed(2)}</td>
|
||||
<td className="px-4 py-3">
|
||||
<Link
|
||||
href={`/creator/listings/${listing.id}/edit`}
|
||||
className="text-sm font-semibold text-stone-900 underline-offset-4 hover:underline"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
) : (
|
||||
<p className="mt-4 text-sm text-stone-600">
|
||||
No listings yet. Start the wizard to publish your first package.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import Link from "next/link";
|
||||
import { db } from "@/db";
|
||||
import { creators, listings } from "@/db/schema";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { and, desc, eq } from "drizzle-orm";
|
||||
|
||||
export default async function CreatorProfilePage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ slug: string }>;
|
||||
}) {
|
||||
const { slug } = await params;
|
||||
|
||||
const allCreators = await db.select().from(creators);
|
||||
const creator = allCreators.find((entry) => slugify(entry.displayName) === slug);
|
||||
|
||||
if (!creator) {
|
||||
return (
|
||||
<main className="mx-auto max-w-4xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8 text-center">
|
||||
<h1 className="font-serif text-3xl text-stone-900">Creator not found</h1>
|
||||
<p className="mt-3 text-stone-600">No creator profile exists for this slug.</p>
|
||||
<Link href="/browse" className="mt-5 inline-flex rounded-xl bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100">
|
||||
Browse listings
|
||||
</Link>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
const publishedListings = await db
|
||||
.select()
|
||||
.from(listings)
|
||||
.where(and(eq(listings.creatorId, creator.id), eq(listings.status, "published")))
|
||||
.orderBy(desc(listings.updatedAt));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl space-y-8 px-4 py-10 sm:px-6">
|
||||
<section className="rounded-3xl border border-stone-300 bg-gradient-to-br from-stone-100 via-amber-50 to-white p-8">
|
||||
<p className="text-xs uppercase tracking-[0.25em] text-stone-500">Creator Profile</p>
|
||||
<h1 className="mt-3 font-serif text-4xl text-stone-900">{creator.displayName}</h1>
|
||||
<p className="mt-4 max-w-3xl text-stone-700">{creator.bio || "No bio provided."}</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="font-serif text-2xl text-stone-900">Published listings</h2>
|
||||
{publishedListings.length ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{publishedListings.map((listing) => (
|
||||
<article key={listing.id} className="rounded-2xl border border-stone-300 bg-stone-50 p-5">
|
||||
<p className="text-xs uppercase tracking-wide text-stone-600">
|
||||
{listing.type.replaceAll("_", " ")}
|
||||
</p>
|
||||
<h3 className="mt-2 font-serif text-2xl text-stone-900">{listing.title}</h3>
|
||||
<p className="mt-2 text-sm text-stone-700">{listing.tagline || "No tagline"}</p>
|
||||
<p className="mt-3 text-sm font-semibold text-stone-900">${(listing.price / 100).toFixed(2)}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-stone-600">No published listings yet.</p>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Link from "next/link";
|
||||
import { db } from "@/db";
|
||||
import { creators } from "@/db/schema";
|
||||
import { slugify } from "@/lib/slug";
|
||||
import { desc } from "drizzle-orm";
|
||||
|
||||
export default async function CreatorsIndexPage() {
|
||||
const allCreators = await db.select().from(creators).orderBy(desc(creators.totalInstalls));
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-5xl space-y-8 px-4 py-10 sm:px-6">
|
||||
<section className="rounded-3xl border border-stone-300 bg-stone-50 p-8">
|
||||
<p className="text-xs uppercase tracking-[0.25em] text-stone-500">Creator Directory</p>
|
||||
<h1 className="mt-3 font-serif text-4xl text-stone-900">Meet the makers</h1>
|
||||
</section>
|
||||
|
||||
{allCreators.length ? (
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{allCreators.map((creator) => (
|
||||
<article key={creator.id} className="rounded-2xl border border-stone-300 bg-white/70 p-5">
|
||||
<h2 className="font-serif text-2xl text-stone-900">{creator.displayName}</h2>
|
||||
<p className="mt-2 text-sm text-stone-700">{creator.bio || "No bio provided."}</p>
|
||||
<Link
|
||||
href={`/creators/${slugify(creator.displayName)}`}
|
||||
className="mt-4 inline-flex text-sm font-semibold text-stone-900 underline-offset-4 hover:underline"
|
||||
>
|
||||
View profile
|
||||
</Link>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-stone-600">No creators registered yet.</p>
|
||||
)}
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+16
-79
@@ -1,91 +1,28 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--sand-50: #f8f2e8;
|
||||
--sand-100: #f3e7d5;
|
||||
--sand-200: #ebdcc7;
|
||||
|
||||
--ink-50: #e6eaef;
|
||||
--ink-200: #b9c2cf;
|
||||
--ink-700: #3a4958;
|
||||
--ink-800: #2d3b4a;
|
||||
--ink-900: #1f2a36;
|
||||
--ink-950: #101922;
|
||||
|
||||
--accent-500: #2b7a6b;
|
||||
--accent-700: #1f5d52;
|
||||
--ember-500: #f18e49;
|
||||
--background: #f4efe6;
|
||||
--foreground: #1d1b17;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--font-sans: var(--font-body);
|
||||
--font-serif: var(--font-heading);
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
color: var(--ink-900);
|
||||
font-family: var(--font-sans), sans-serif;
|
||||
line-height: 1.45;
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-body), sans-serif;
|
||||
}
|
||||
|
||||
.grain {
|
||||
opacity: 0.2;
|
||||
background-image: radial-gradient(circle at 1px 1px, rgba(16, 25, 34, 0.12) 1px, transparent 0);
|
||||
background-size: 14px 14px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid color-mix(in oklab, var(--ink-200) 75%, white 25%);
|
||||
background: color-mix(in oklab, white 72%, var(--sand-100) 28%);
|
||||
box-shadow: 0 14px 32px rgba(16, 25, 34, 0.08);
|
||||
}
|
||||
|
||||
.section-label {
|
||||
color: var(--accent-700);
|
||||
font-family: var(--font-mono), monospace;
|
||||
font-size: 0.72rem;
|
||||
letter-spacing: 0.16em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.section-heading {
|
||||
color: var(--ink-950);
|
||||
font-family: var(--font-display), serif;
|
||||
letter-spacing: -0.015em;
|
||||
}
|
||||
|
||||
@keyframes rise-in {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(18px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.fade-up {
|
||||
animation: rise-in 640ms cubic-bezier(0.22, 0.8, 0.26, 1) both;
|
||||
}
|
||||
|
||||
.stagger-1 {
|
||||
animation-delay: 70ms;
|
||||
}
|
||||
|
||||
.stagger-2 {
|
||||
animation-delay: 140ms;
|
||||
}
|
||||
|
||||
.stagger-3 {
|
||||
animation-delay: 210ms;
|
||||
}
|
||||
|
||||
.stagger-4 {
|
||||
animation-delay: 280ms;
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-family: var(--font-heading), serif;
|
||||
}
|
||||
+17
-69
@@ -1,39 +1,24 @@
|
||||
import type { Metadata } from "next";
|
||||
import Link from "next/link";
|
||||
import { Fraunces, Manrope, Space_Mono } from "next/font/google";
|
||||
import { Fraunces, Manrope } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import { Header } from "@/components/header";
|
||||
import { Footer } from "@/components/footer";
|
||||
|
||||
const display = Fraunces({
|
||||
variable: "--font-display",
|
||||
const headingFont = Fraunces({
|
||||
variable: "--font-heading",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const sans = Manrope({
|
||||
variable: "--font-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const mono = Space_Mono({
|
||||
variable: "--font-mono",
|
||||
weight: ["400", "700"],
|
||||
const bodyFont = Manrope({
|
||||
variable: "--font-body",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
default: "ClipMart",
|
||||
template: "%s | ClipMart",
|
||||
},
|
||||
description:
|
||||
"Marketplace for full Paperclip company blueprints, agent packs, and operational skills.",
|
||||
title: "ClipMart",
|
||||
description: "Marketplace for Paperclip team blueprints and creator listings.",
|
||||
};
|
||||
|
||||
const navigation = [
|
||||
{ href: "/", label: "Home" },
|
||||
{ href: "/about", label: "About" },
|
||||
{ href: "/pricing", label: "Pricing" },
|
||||
];
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
@@ -41,51 +26,14 @@ export default function RootLayout({
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${display.variable} ${sans.variable} ${mono.variable}`}>
|
||||
<div className="pointer-events-none fixed inset-0 -z-10 bg-[radial-gradient(circle_at_15%_15%,rgba(43,122,107,0.14),transparent_38%),radial-gradient(circle_at_82%_22%,rgba(241,142,73,0.2),transparent_35%),linear-gradient(170deg,#f7f0e6_0%,#f2e8d9_48%,#f7f2e8_100%)]" />
|
||||
<div className="grain fixed inset-0 -z-10" />
|
||||
|
||||
<header className="sticky top-0 z-20 border-b border-[var(--ink-200)]/70 bg-[var(--sand-50)]/85 backdrop-blur">
|
||||
<div className="mx-auto flex w-full max-w-6xl items-center justify-between px-6 py-4">
|
||||
<Link href="/" className="font-[family-name:var(--font-display)] text-2xl font-semibold tracking-tight text-[var(--ink-950)]">
|
||||
ClipMart
|
||||
</Link>
|
||||
<nav className="flex items-center gap-1 rounded-full border border-[var(--ink-300)]/80 bg-white/50 p-1 text-sm">
|
||||
{navigation.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className="rounded-full px-4 py-1.5 font-semibold text-[var(--ink-700)] transition hover:bg-[var(--ink-900)] hover:text-[var(--sand-50)]"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main>{children}</main>
|
||||
|
||||
<footer className="border-t border-[var(--ink-200)] bg-[var(--sand-100)]/80">
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-6 px-6 py-10 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="font-[family-name:var(--font-display)] text-3xl text-[var(--ink-950)]">
|
||||
Open-source orchestration for zero-human companies.
|
||||
</p>
|
||||
<p className="mt-2 text-sm text-[var(--ink-700)]">
|
||||
Build faster with complete operating teams, not disconnected prompts.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-4 text-sm font-semibold text-[var(--ink-800)]">
|
||||
<Link href="/about" className="hover:text-[var(--accent-700)]">
|
||||
About
|
||||
</Link>
|
||||
<Link href="/pricing" className="hover:text-[var(--accent-700)]">
|
||||
Pricing
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
<body
|
||||
className={`${headingFont.variable} ${bodyFont.variable} bg-[radial-gradient(circle_at_top_right,_rgba(255,204,128,0.25),_transparent_45%),radial-gradient(circle_at_top_left,_rgba(202,213,255,0.22),_transparent_40%),#f4efe6] text-stone-900 antialiased`}
|
||||
>
|
||||
<div className="min-h-screen">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
+21
-213
@@ -1,224 +1,32 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const featuredBlueprints = [
|
||||
{
|
||||
name: "SaaS Launch Crew",
|
||||
tagline: "CEO + CTO + 3 specialists pre-wired to ship a funded MVP in days.",
|
||||
installs: 324,
|
||||
agents: 5,
|
||||
price: "$199",
|
||||
},
|
||||
{
|
||||
name: "Support Ops Grid",
|
||||
tagline: "Ticket triage, escalation, and customer comms with built-in governance rules.",
|
||||
installs: 188,
|
||||
agents: 7,
|
||||
price: "$149",
|
||||
},
|
||||
{
|
||||
name: "Growth Studio Team",
|
||||
tagline: "End-to-end content and demand generation with analytics accountability.",
|
||||
installs: 271,
|
||||
agents: 6,
|
||||
price: "$179",
|
||||
},
|
||||
];
|
||||
|
||||
const popularSkills = [
|
||||
"PR Quality Gate",
|
||||
"Incident Escalation",
|
||||
"Stripe Revenue Digest",
|
||||
"Customer Voice Classifier",
|
||||
"Spec-to-Story Mapper",
|
||||
"Release Readiness Report",
|
||||
];
|
||||
|
||||
const categories = [
|
||||
"Engineering",
|
||||
"Marketing",
|
||||
"Customer Success",
|
||||
"Finance",
|
||||
"Ops",
|
||||
"Product",
|
||||
"Compliance",
|
||||
"Founders",
|
||||
];
|
||||
|
||||
const steps = [
|
||||
{
|
||||
title: "Browse",
|
||||
copy: "Find complete company templates built for your target stage and team shape.",
|
||||
},
|
||||
{
|
||||
title: "Install",
|
||||
copy: "Deploy the org chart, governance rules, and project defaults into your Paperclip company.",
|
||||
},
|
||||
{
|
||||
title: "Run",
|
||||
copy: "Launch with clear reporting lines, approvals, and production-ready execution loops.",
|
||||
},
|
||||
];
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-16 px-6 py-12 md:gap-24 md:py-18">
|
||||
<section className="grid gap-8 md:grid-cols-[1.15fr_0.85fr] md:items-end">
|
||||
<div className="fade-up space-y-6">
|
||||
<p className="section-label">Marketplace For Autonomous Teams</p>
|
||||
<h1 className="section-heading max-w-2xl text-5xl leading-[1.02] md:text-7xl">
|
||||
Buy a company that is ready to ship.
|
||||
</h1>
|
||||
<p className="max-w-xl text-lg text-[var(--ink-700)] md:text-xl">
|
||||
ClipMart is where founders install full Paperclip operating teams: org charts,
|
||||
skills, approvals, and project templates bundled as one deployable blueprint.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="rounded-full bg-[var(--ink-950)] px-6 py-3 text-sm font-bold text-[var(--sand-50)] transition hover:bg-[var(--accent-700)]"
|
||||
>
|
||||
Explore Pricing
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="rounded-full border border-[var(--ink-300)] bg-white/80 px-6 py-3 text-sm font-bold text-[var(--ink-900)] transition hover:border-[var(--ink-900)]"
|
||||
>
|
||||
How ClipMart Works
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<aside className="panel fade-up stagger-2 rounded-3xl p-5 md:p-6">
|
||||
<p className="section-label">Live Blueprint Snapshot</p>
|
||||
<h2 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink-950)]">
|
||||
Product Team Accelerator
|
||||
</h2>
|
||||
<p className="mt-2 text-sm text-[var(--ink-700)]">
|
||||
6-agent structure with shipping workflows and decision checkpoints pre-configured.
|
||||
</p>
|
||||
<div className="mt-5 rounded-2xl border border-[var(--ink-200)] bg-[var(--sand-50)] p-4">
|
||||
<p className="text-xs font-bold tracking-[0.15em] text-[var(--accent-700)] uppercase">
|
||||
Org Shape
|
||||
</p>
|
||||
<ul className="mt-3 space-y-2 text-sm text-[var(--ink-900)]">
|
||||
<li>CEO {'->'} CTO {'->'} Engineering Lead</li>
|
||||
<li>CMO {'->'} Content Strategist</li>
|
||||
<li>COO {'->'} Ops Analyst</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="mt-4 flex items-center justify-between rounded-2xl bg-[var(--ink-950)] px-4 py-3 text-sm text-[var(--sand-50)]">
|
||||
<span>Install-ready package</span>
|
||||
<strong>$189</strong>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<div className="fade-up">
|
||||
<p className="section-label">Featured Blueprints</p>
|
||||
<h2 className="section-heading mt-2 text-4xl md:text-5xl">Teams people install first</h2>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{featuredBlueprints.map((item, index) => (
|
||||
<article
|
||||
key={item.name}
|
||||
className={`panel fade-up rounded-3xl p-5 ${index === 0 ? "stagger-1" : index === 1 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
<h3 className="font-[family-name:var(--font-display)] text-2xl text-[var(--ink-950)]">
|
||||
{item.name}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-[var(--ink-700)]">{item.tagline}</p>
|
||||
<div className="mt-5 flex items-center justify-between text-sm text-[var(--ink-800)]">
|
||||
<span>{item.agents} agents</span>
|
||||
<span>{item.installs} installs</span>
|
||||
<strong className="font-[family-name:var(--font-mono)] text-[var(--ink-950)]">
|
||||
{item.price}
|
||||
</strong>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 rounded-3xl bg-[var(--ink-950)] p-6 text-[var(--sand-50)] md:grid-cols-[1fr_1.3fr] md:p-8">
|
||||
<div className="fade-up">
|
||||
<p className="font-[family-name:var(--font-mono)] text-xs tracking-[0.14em] uppercase text-[var(--sand-200)]">
|
||||
Popular Skills
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-4xl">Operational modules you can plug in</h2>
|
||||
</div>
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
{popularSkills.map((skill, index) => (
|
||||
<div
|
||||
key={skill}
|
||||
className={`fade-up rounded-2xl border border-white/18 bg-white/6 px-4 py-3 text-sm ${index % 2 === 0 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
{skill}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-6">
|
||||
<div className="fade-up">
|
||||
<p className="section-label">How It Works</p>
|
||||
<h2 className="section-heading mt-2 text-4xl md:text-5xl">Browse, install, run.</h2>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
{steps.map((step, index) => (
|
||||
<article
|
||||
key={step.title}
|
||||
className={`panel fade-up rounded-3xl p-5 ${index === 0 ? "stagger-1" : index === 1 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
<p className="font-[family-name:var(--font-mono)] text-xs tracking-[0.14em] uppercase text-[var(--accent-700)]">
|
||||
0{index + 1}
|
||||
</p>
|
||||
<h3 className="mt-3 font-[family-name:var(--font-display)] text-3xl text-[var(--ink-950)]">
|
||||
{step.title}
|
||||
</h3>
|
||||
<p className="mt-2 text-sm text-[var(--ink-700)]">{step.copy}</p>
|
||||
</article>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="space-y-5">
|
||||
<div className="fade-up">
|
||||
<p className="section-label">Categories</p>
|
||||
<h2 className="section-heading mt-2 text-4xl md:text-5xl">Blueprints for every function</h2>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
{categories.map((category, index) => (
|
||||
<span
|
||||
key={category}
|
||||
className={`fade-up rounded-full border border-[var(--ink-200)] bg-white/70 px-4 py-2 text-sm font-semibold text-[var(--ink-800)] ${index < 4 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
{category}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel fade-up stagger-4 rounded-3xl p-6 md:p-8">
|
||||
<p className="section-label">Creator Program</p>
|
||||
<div className="mt-2 grid gap-6 md:grid-cols-[1fr_auto] md:items-end">
|
||||
<div>
|
||||
<h2 className="section-heading text-4xl md:text-5xl">
|
||||
Publish your team architecture. Get paid when companies install it.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-[var(--ink-700)]">
|
||||
Upload your blueprint bundle, set pricing in minutes, and earn each time another
|
||||
company launches with your playbook.
|
||||
</p>
|
||||
</div>
|
||||
<main className="mx-auto max-w-6xl space-y-10 px-4 py-10 sm:px-6">
|
||||
<section className="relative overflow-hidden rounded-3xl border border-stone-300 bg-gradient-to-br from-stone-950 via-stone-900 to-amber-900 p-8 text-stone-100 sm:p-12">
|
||||
<div className="absolute left-10 top-6 h-24 w-24 rounded-full bg-amber-300/30 blur-2xl" />
|
||||
<p className="text-xs uppercase tracking-[0.28em] text-stone-300">ClipMart</p>
|
||||
<h1 className="mt-4 max-w-3xl font-serif text-4xl leading-tight sm:text-5xl">
|
||||
Marketplace for complete AI company blueprints
|
||||
</h1>
|
||||
<p className="mt-4 max-w-2xl text-sm text-stone-200 sm:text-base">
|
||||
Discover team configurations, install them into Paperclip, or publish your own listing as a creator.
|
||||
</p>
|
||||
<div className="mt-7 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/about"
|
||||
className="inline-flex w-fit rounded-full bg-[var(--ember-500)] px-6 py-3 text-sm font-bold text-[var(--ink-950)] transition hover:bg-[var(--accent-500)] hover:text-[var(--sand-50)]"
|
||||
href="/creator"
|
||||
className="rounded-xl bg-stone-100 px-5 py-3 text-sm font-semibold text-stone-900 transition hover:bg-white"
|
||||
>
|
||||
Become A Creator
|
||||
Open creator dashboard
|
||||
</Link>
|
||||
<Link
|
||||
href="/creators"
|
||||
className="rounded-xl border border-stone-500 px-5 py-3 text-sm font-semibold text-stone-100 transition hover:border-stone-300"
|
||||
>
|
||||
Browse creators
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
+6
-127
@@ -1,131 +1,10 @@
|
||||
import Link from "next/link";
|
||||
|
||||
const plans = [
|
||||
{
|
||||
name: "Explorer",
|
||||
price: "$0",
|
||||
cadence: "always free",
|
||||
details: [
|
||||
"Browse and install free blueprints",
|
||||
"Save favorites and compare packages",
|
||||
"Public creator profile support",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Creator",
|
||||
price: "10%",
|
||||
cadence: "platform fee per paid sale",
|
||||
details: [
|
||||
"You keep 90% of paid listing revenue",
|
||||
"Stripe payout rails and reporting",
|
||||
"Priority review for featured placements",
|
||||
],
|
||||
},
|
||||
{
|
||||
name: "Team Launch",
|
||||
price: "$49",
|
||||
cadence: "per company / month",
|
||||
details: [
|
||||
"Private listing collections",
|
||||
"Install history and compliance audit view",
|
||||
"Cross-workspace deployment controls",
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const feeRows = [
|
||||
{ label: "Marketplace fee", value: "10% on paid listings" },
|
||||
{ label: "Creator payout", value: "90% to creator account" },
|
||||
{ label: "Free listings", value: "No platform fee" },
|
||||
{ label: "Buyer transaction", value: "Price shown on listing" },
|
||||
];
|
||||
|
||||
export const metadata = {
|
||||
title: "Pricing",
|
||||
};
|
||||
|
||||
export default function PricingPage() {
|
||||
return (
|
||||
<div className="mx-auto flex w-full max-w-6xl flex-col gap-14 px-6 py-12 md:gap-18 md:py-16">
|
||||
<section className="fade-up space-y-5">
|
||||
<p className="section-label">Pricing</p>
|
||||
<h1 className="section-heading max-w-3xl text-5xl leading-[1.03] md:text-7xl">
|
||||
Straightforward economics for buyers and creators.
|
||||
</h1>
|
||||
<p className="max-w-2xl text-lg text-[var(--ink-700)] md:text-xl">
|
||||
Buy only what you need, or monetize proven company setups with a transparent revenue split.
|
||||
No hidden infrastructure tax.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="grid gap-4 md:grid-cols-3">
|
||||
{plans.map((plan, index) => (
|
||||
<article
|
||||
key={plan.name}
|
||||
className={`panel fade-up rounded-3xl p-5 ${index === 0 ? "stagger-1" : index === 1 ? "stagger-2" : "stagger-3"}`}
|
||||
>
|
||||
<p className="section-label">{plan.name}</p>
|
||||
<p className="mt-3 font-[family-name:var(--font-display)] text-5xl text-[var(--ink-950)]">
|
||||
{plan.price}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-[var(--ink-700)]">{plan.cadence}</p>
|
||||
<ul className="mt-5 space-y-2 text-sm text-[var(--ink-800)]">
|
||||
{plan.details.map((detail) => (
|
||||
<li key={detail}>• {detail}</li>
|
||||
))}
|
||||
</ul>
|
||||
</article>
|
||||
))}
|
||||
</section>
|
||||
|
||||
<section className="grid gap-6 rounded-3xl bg-[var(--ink-950)] p-6 text-[var(--sand-50)] md:grid-cols-[0.95fr_1.05fr] md:p-8">
|
||||
<div className="fade-up">
|
||||
<p className="font-[family-name:var(--font-mono)] text-xs tracking-[0.14em] uppercase text-[var(--sand-200)]">
|
||||
Revenue Model
|
||||
</p>
|
||||
<h2 className="mt-2 font-[family-name:var(--font-display)] text-4xl md:text-5xl">
|
||||
Creator-first split, platform-level reliability.
|
||||
</h2>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{feeRows.map((row, index) => (
|
||||
<div
|
||||
key={row.label}
|
||||
className={`fade-up flex items-center justify-between rounded-2xl border border-white/20 bg-white/6 px-4 py-3 text-sm ${
|
||||
index < 2 ? "stagger-2" : "stagger-3"
|
||||
}`}
|
||||
>
|
||||
<span>{row.label}</span>
|
||||
<strong>{row.value}</strong>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="panel fade-up stagger-4 rounded-3xl p-6 md:p-8">
|
||||
<p className="section-label">Ready To Launch</p>
|
||||
<h2 className="section-heading mt-2 text-4xl md:text-5xl">
|
||||
Start with free blueprints, then scale into paid operating systems.
|
||||
</h2>
|
||||
<p className="mt-3 max-w-2xl text-[var(--ink-700)]">
|
||||
Every purchase includes install-ready structures for Paperclip companies, so you can deploy
|
||||
and adapt immediately.
|
||||
</p>
|
||||
<div className="mt-5 flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/"
|
||||
className="rounded-full bg-[var(--ink-950)] px-6 py-3 text-sm font-bold text-[var(--sand-50)] transition hover:bg-[var(--accent-700)]"
|
||||
>
|
||||
Browse Blueprints
|
||||
</Link>
|
||||
<Link
|
||||
href="/about"
|
||||
className="rounded-full border border-[var(--ink-300)] bg-white/80 px-6 py-3 text-sm font-bold text-[var(--ink-900)] transition hover:border-[var(--ink-900)]"
|
||||
>
|
||||
Learn More
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<main className="mx-auto max-w-4xl px-4 py-20 sm:px-6">
|
||||
<div className="rounded-3xl border border-stone-300 bg-stone-50 p-8">
|
||||
<h1 className="font-serif text-4xl text-stone-900">Pricing</h1>
|
||||
<p className="mt-3 text-stone-700">Set listing prices from $0 to $499 in the creator wizard.</p>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,544 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useTransition } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
type ListingType =
|
||||
| "team_blueprint"
|
||||
| "agent_blueprint"
|
||||
| "skill"
|
||||
| "governance_template";
|
||||
|
||||
type ListingStatus = "draft" | "published" | "archived";
|
||||
|
||||
const TYPE_OPTIONS: Array<{ value: ListingType; label: string; hint: string }> = [
|
||||
{
|
||||
value: "team_blueprint",
|
||||
label: "Team Blueprint",
|
||||
hint: "Complete company configuration with multi-agent org charts.",
|
||||
},
|
||||
{
|
||||
value: "agent_blueprint",
|
||||
label: "Agent Blueprint",
|
||||
hint: "Single agent package designed to plug into an existing org.",
|
||||
},
|
||||
{
|
||||
value: "skill",
|
||||
label: "Skill Bundle",
|
||||
hint: "Portable capability packs you can attach to multiple agents.",
|
||||
},
|
||||
{
|
||||
value: "governance_template",
|
||||
label: "Governance Template",
|
||||
hint: "Approval chains, budget policies, and escalation defaults.",
|
||||
},
|
||||
];
|
||||
|
||||
const STEPS = [
|
||||
"Type",
|
||||
"Basics",
|
||||
"Bundle",
|
||||
"Preview",
|
||||
"Pricing",
|
||||
"Review",
|
||||
] as const;
|
||||
|
||||
export type ListingWizardInitialValues = {
|
||||
id?: string;
|
||||
type?: ListingType;
|
||||
title?: string;
|
||||
tagline?: string;
|
||||
description?: string;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
includedFiles?: string[];
|
||||
readmeMarkdown?: string;
|
||||
agentCount?: number | null;
|
||||
price?: number;
|
||||
status?: ListingStatus;
|
||||
};
|
||||
|
||||
type ListingWizardFormProps = {
|
||||
mode: "create" | "edit";
|
||||
listingId?: string;
|
||||
initialValues?: ListingWizardInitialValues;
|
||||
};
|
||||
|
||||
function asCsv(items: string[] | undefined): string {
|
||||
if (!items) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return items.join(", ");
|
||||
}
|
||||
|
||||
function parseCsv(value: string): string[] {
|
||||
return value
|
||||
.split(",")
|
||||
.map((entry) => entry.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function derivePreviewNodes(type: ListingType, agentCount: number) {
|
||||
if (type === "skill" || type === "governance_template") {
|
||||
return ["Publisher", "Installer"];
|
||||
}
|
||||
|
||||
const count = Math.max(2, Math.min(12, agentCount || 3));
|
||||
|
||||
return ["Lead", ...Array.from({ length: count - 1 }, (_, index) => `Agent ${index + 1}`)];
|
||||
}
|
||||
|
||||
export function ListingWizardForm({
|
||||
mode,
|
||||
listingId,
|
||||
initialValues,
|
||||
}: ListingWizardFormProps) {
|
||||
const router = useRouter();
|
||||
const [step, setStep] = useState(0);
|
||||
|
||||
const [type, setType] = useState<ListingType>(
|
||||
initialValues?.type ?? "team_blueprint",
|
||||
);
|
||||
const [title, setTitle] = useState(initialValues?.title ?? "");
|
||||
const [tagline, setTagline] = useState(initialValues?.tagline ?? "");
|
||||
const [description, setDescription] = useState(initialValues?.description ?? "");
|
||||
const [categories, setCategories] = useState(asCsv(initialValues?.categories));
|
||||
const [tags, setTags] = useState(asCsv(initialValues?.tags));
|
||||
const [bundleMode, setBundleMode] = useState<"upload" | "manual">("upload");
|
||||
const [includedFiles, setIncludedFiles] = useState(asCsv(initialValues?.includedFiles));
|
||||
const [readmeMarkdown, setReadmeMarkdown] = useState(
|
||||
initialValues?.readmeMarkdown ?? "",
|
||||
);
|
||||
const [agentCount, setAgentCount] = useState(
|
||||
Math.max(0, initialValues?.agentCount ?? 0),
|
||||
);
|
||||
const [price, setPrice] = useState(initialValues?.price ?? 0);
|
||||
const [status, setStatus] = useState<ListingStatus>(
|
||||
initialValues?.status ?? "draft",
|
||||
);
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [isPending, startTransition] = useTransition();
|
||||
|
||||
const summary = useMemo(
|
||||
() => ({
|
||||
type,
|
||||
title: title.trim() || "Untitled listing",
|
||||
tagline: tagline.trim() || "No tagline yet",
|
||||
categories: parseCsv(categories),
|
||||
tags: parseCsv(tags),
|
||||
includedFiles: parseCsv(includedFiles),
|
||||
readmeMarkdown,
|
||||
agentCount,
|
||||
price,
|
||||
status,
|
||||
}),
|
||||
[
|
||||
agentCount,
|
||||
categories,
|
||||
includedFiles,
|
||||
price,
|
||||
readmeMarkdown,
|
||||
status,
|
||||
tagline,
|
||||
tags,
|
||||
title,
|
||||
type,
|
||||
],
|
||||
);
|
||||
|
||||
const maxStep = STEPS.length - 1;
|
||||
|
||||
const nextStep = () => setStep((current) => Math.min(current + 1, maxStep));
|
||||
const previousStep = () => setStep((current) => Math.max(current - 1, 0));
|
||||
|
||||
const submit = () => {
|
||||
setError(null);
|
||||
|
||||
if (!title.trim()) {
|
||||
setError("Title is required before publish.");
|
||||
setStep(1);
|
||||
return;
|
||||
}
|
||||
|
||||
startTransition(async () => {
|
||||
const payload = {
|
||||
type,
|
||||
title,
|
||||
tagline,
|
||||
description,
|
||||
categories: parseCsv(categories),
|
||||
tags: parseCsv(tags),
|
||||
includedFiles: parseCsv(includedFiles),
|
||||
readmeMarkdown,
|
||||
agentCount,
|
||||
price,
|
||||
status,
|
||||
};
|
||||
|
||||
const endpoint =
|
||||
mode === "create" ? "/api/creator/listings" : `/api/creator/listings/${listingId}`;
|
||||
const method = mode === "create" ? "POST" : "PATCH";
|
||||
|
||||
const response = await fetch(endpoint, {
|
||||
method,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const responsePayload = (await response.json().catch(() => null)) as
|
||||
| { error?: string }
|
||||
| null;
|
||||
setError(responsePayload?.error ?? "Unable to save listing");
|
||||
return;
|
||||
}
|
||||
|
||||
router.push("/creator");
|
||||
router.refresh();
|
||||
});
|
||||
};
|
||||
|
||||
const previewNodes = derivePreviewNodes(type, agentCount);
|
||||
|
||||
return (
|
||||
<div className="space-y-8 rounded-3xl border border-stone-300 bg-white/85 p-6 shadow-[0_24px_70px_-50px_rgba(34,31,25,0.8)] backdrop-blur sm:p-8">
|
||||
<div className="space-y-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{STEPS.map((label, index) => {
|
||||
const stateClass =
|
||||
index === step
|
||||
? "bg-stone-900 text-stone-100"
|
||||
: index < step
|
||||
? "bg-emerald-100 text-emerald-900"
|
||||
: "bg-stone-100 text-stone-500";
|
||||
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
type="button"
|
||||
onClick={() => setStep(index)}
|
||||
className={`rounded-full px-3 py-1 text-xs font-semibold tracking-wide transition ${stateClass}`}
|
||||
>
|
||||
{index + 1}. {label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<h1 className="font-serif text-3xl text-stone-900">
|
||||
{mode === "create" ? "New listing wizard" : "Edit listing wizard"}
|
||||
</h1>
|
||||
<p className="text-sm text-stone-600">
|
||||
Complete all six stages to publish a marketplace-ready listing.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{step === 0 ? (
|
||||
<section className="grid gap-3 sm:grid-cols-2">
|
||||
{TYPE_OPTIONS.map((option) => {
|
||||
const isActive = option.value === type;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={option.value}
|
||||
type="button"
|
||||
onClick={() => setType(option.value)}
|
||||
className={`rounded-2xl border p-4 text-left transition ${
|
||||
isActive
|
||||
? "border-stone-900 bg-stone-900 text-stone-100"
|
||||
: "border-stone-300 bg-stone-50 text-stone-800 hover:border-stone-500"
|
||||
}`}
|
||||
>
|
||||
<p className="font-semibold">{option.label}</p>
|
||||
<p className="mt-2 text-sm opacity-90">{option.hint}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{step === 1 ? (
|
||||
<section className="space-y-5">
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Title</span>
|
||||
<input
|
||||
value={title}
|
||||
onChange={(event) => setTitle(event.target.value)}
|
||||
maxLength={255}
|
||||
required
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="SaaS Team Launch Kit"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Tagline</span>
|
||||
<input
|
||||
value={tagline}
|
||||
onChange={(event) => setTagline(event.target.value)}
|
||||
maxLength={120}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="Deploy a full product and growth team in minutes."
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Description</span>
|
||||
<textarea
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
rows={6}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="Explain workflow, assumptions, and best fit buyer profile."
|
||||
/>
|
||||
</label>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2">
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Categories</span>
|
||||
<input
|
||||
value={categories}
|
||||
onChange={(event) => setCategories(event.target.value)}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="saas, growth, engineering"
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Tags</span>
|
||||
<input
|
||||
value={tags}
|
||||
onChange={(event) => setTags(event.target.value)}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="5-agent, codex, startup"
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{step === 2 ? (
|
||||
<section className="space-y-5">
|
||||
<div className="flex gap-2 rounded-xl bg-stone-100 p-1 text-sm">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBundleMode("upload")}
|
||||
className={`flex-1 rounded-lg px-3 py-2 font-semibold transition ${
|
||||
bundleMode === "upload"
|
||||
? "bg-white text-stone-900"
|
||||
: "text-stone-500 hover:text-stone-700"
|
||||
}`}
|
||||
>
|
||||
Upload bundle
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setBundleMode("manual")}
|
||||
className={`flex-1 rounded-lg px-3 py-2 font-semibold transition ${
|
||||
bundleMode === "manual"
|
||||
? "bg-white text-stone-900"
|
||||
: "text-stone-500 hover:text-stone-700"
|
||||
}`}
|
||||
>
|
||||
Manual entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{bundleMode === "upload" ? (
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Bundle files</span>
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(event) => {
|
||||
const fileNames = Array.from(event.target.files ?? []).map((file) => file.name);
|
||||
setIncludedFiles(fileNames.join(", "));
|
||||
}}
|
||||
className="w-full rounded-xl border border-dashed border-stone-400 bg-stone-50 px-3 py-8 text-sm text-stone-600"
|
||||
/>
|
||||
</label>
|
||||
) : (
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">
|
||||
Included files (comma separated)
|
||||
</span>
|
||||
<input
|
||||
value={includedFiles}
|
||||
onChange={(event) => setIncludedFiles(event.target.value)}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="manifest.json, README.md, agents/cto/AGENTS.md"
|
||||
/>
|
||||
</label>
|
||||
)}
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">README markdown</span>
|
||||
<textarea
|
||||
value={readmeMarkdown}
|
||||
onChange={(event) => setReadmeMarkdown(event.target.value)}
|
||||
rows={8}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 font-mono text-xs text-stone-900 outline-none focus:border-stone-800"
|
||||
placeholder="# Package overview\n\nDescribe setup instructions and outcomes."
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{step === 3 ? (
|
||||
<section className="space-y-5">
|
||||
<div className="grid gap-4 sm:grid-cols-[minmax(0,1fr)_220px]">
|
||||
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4">
|
||||
<p className="text-sm font-semibold text-stone-900">Live preview</p>
|
||||
<h3 className="mt-3 font-serif text-2xl text-stone-900">{summary.title}</h3>
|
||||
<p className="mt-2 text-sm text-stone-600">{summary.tagline}</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2 text-xs">
|
||||
{summary.categories.length ? (
|
||||
summary.categories.map((item) => (
|
||||
<span
|
||||
key={item}
|
||||
className="rounded-full bg-stone-200 px-2 py-1 font-medium text-stone-700"
|
||||
>
|
||||
{item}
|
||||
</span>
|
||||
))
|
||||
) : (
|
||||
<span className="text-stone-500">No categories yet</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="mt-4 text-xs text-stone-500">
|
||||
Files: {summary.includedFiles.length ? summary.includedFiles.join(", ") : "none"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-2xl border border-stone-900 bg-stone-900 p-4 text-stone-100">
|
||||
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-stone-300">
|
||||
Org preview
|
||||
</p>
|
||||
<div className="mt-3 space-y-2">
|
||||
{previewNodes.map((node, index) => (
|
||||
<div
|
||||
key={node}
|
||||
className={`rounded-lg border border-stone-600 px-3 py-2 text-xs ${
|
||||
index === 0 ? "bg-stone-100 text-stone-900" : "bg-stone-800"
|
||||
}`}
|
||||
>
|
||||
{node}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Approximate agent count</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={99}
|
||||
value={agentCount}
|
||||
onChange={(event) => setAgentCount(Number(event.target.value))}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
/>
|
||||
</label>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{step === 4 ? (
|
||||
<section className="space-y-5">
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Price (USD)</span>
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={499}
|
||||
value={price}
|
||||
onChange={(event) => setPrice(Number(event.target.value))}
|
||||
className="w-full accent-stone-900"
|
||||
/>
|
||||
<div className="flex items-center justify-between text-sm text-stone-600">
|
||||
<span>$0</span>
|
||||
<span className="rounded-full bg-stone-900 px-3 py-1 font-semibold text-stone-100">
|
||||
${price}
|
||||
</span>
|
||||
<span>$499</span>
|
||||
</div>
|
||||
</label>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{step === 5 ? (
|
||||
<section className="space-y-5">
|
||||
<div className="rounded-2xl border border-stone-200 bg-stone-50 p-4">
|
||||
<h3 className="font-serif text-xl text-stone-900">Final review</h3>
|
||||
<dl className="mt-4 grid gap-3 text-sm sm:grid-cols-2">
|
||||
<div>
|
||||
<dt className="font-semibold text-stone-600">Type</dt>
|
||||
<dd className="text-stone-900">{summary.type}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-semibold text-stone-600">Price</dt>
|
||||
<dd className="text-stone-900">${summary.price}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-semibold text-stone-600">Agent count</dt>
|
||||
<dd className="text-stone-900">{summary.agentCount || 0}</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt className="font-semibold text-stone-600">Tags</dt>
|
||||
<dd className="text-stone-900">{summary.tags.join(", ") || "none"}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<label className="block space-y-2">
|
||||
<span className="text-sm font-semibold text-stone-800">Publish state</span>
|
||||
<select
|
||||
value={status}
|
||||
onChange={(event) => setStatus(event.target.value as ListingStatus)}
|
||||
className="w-full rounded-xl border border-stone-300 bg-stone-50 px-3 py-2 text-stone-900 outline-none focus:border-stone-800"
|
||||
>
|
||||
<option value="draft">Draft</option>
|
||||
<option value="published">Published</option>
|
||||
<option value="archived">Archived</option>
|
||||
</select>
|
||||
</label>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{error ? <p className="text-sm font-semibold text-red-700">{error}</p> : null}
|
||||
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={previousStep}
|
||||
disabled={step === 0 || isPending}
|
||||
className="rounded-xl border border-stone-300 px-4 py-2 text-sm font-semibold text-stone-700 transition hover:border-stone-500 disabled:opacity-50"
|
||||
>
|
||||
Back
|
||||
</button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{step < maxStep ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={nextStep}
|
||||
className="rounded-xl bg-stone-900 px-5 py-2 text-sm font-semibold text-stone-100 transition hover:bg-stone-800"
|
||||
>
|
||||
Next step
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={submit}
|
||||
disabled={isPending}
|
||||
className="rounded-xl bg-emerald-700 px-5 py-2 text-sm font-semibold text-emerald-50 transition hover:bg-emerald-600 disabled:opacity-60"
|
||||
>
|
||||
{isPending ? "Saving..." : mode === "create" ? "Publish listing" : "Save changes"}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+15
-20
@@ -2,26 +2,21 @@ import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="border-t border-gray-200 bg-white">
|
||||
<div className="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col items-center justify-between gap-4 sm:flex-row">
|
||||
<p className="text-sm text-gray-500">
|
||||
ClipMart — The marketplace for Paperclip team blueprints
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<Link
|
||||
href="/about"
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href="/pricing"
|
||||
className="text-sm text-gray-500 hover:text-gray-700"
|
||||
>
|
||||
Pricing
|
||||
</Link>
|
||||
</div>
|
||||
<footer className="mt-16 border-t border-stone-300 bg-stone-50/70">
|
||||
<div className="mx-auto flex max-w-7xl flex-col items-center justify-between gap-4 px-4 py-8 sm:flex-row sm:px-6 lg:px-8">
|
||||
<p className="text-sm text-stone-600">
|
||||
ClipMart, the marketplace for Paperclip team blueprints
|
||||
</p>
|
||||
<div className="flex gap-6">
|
||||
<Link href="/about" className="text-sm text-stone-600 transition hover:text-stone-900">
|
||||
About
|
||||
</Link>
|
||||
<Link
|
||||
href="/creators"
|
||||
className="text-sm text-stone-600 transition hover:text-stone-900"
|
||||
>
|
||||
Creators
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
+16
-18
@@ -1,48 +1,46 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
|
||||
export function Header() {
|
||||
return (
|
||||
<header className="border-b border-gray-200 bg-white">
|
||||
<header className="sticky top-0 z-50 border-b border-stone-300/80 bg-stone-50/85 backdrop-blur-md">
|
||||
<div className="mx-auto flex h-16 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center gap-8">
|
||||
<Link href="/" className="text-xl font-bold text-gray-900">
|
||||
<Link href="/" className="font-serif text-2xl text-stone-900">
|
||||
ClipMart
|
||||
</Link>
|
||||
<nav className="hidden items-center gap-6 md:flex">
|
||||
<nav className="hidden items-center gap-5 md:flex">
|
||||
<Link
|
||||
href="/browse"
|
||||
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
||||
className="text-sm font-semibold text-stone-600 transition hover:text-stone-900"
|
||||
>
|
||||
Browse
|
||||
</Link>
|
||||
<Link
|
||||
href="/browse?type=team_blueprint"
|
||||
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
||||
href="/creator"
|
||||
className="text-sm font-semibold text-stone-600 transition hover:text-stone-900"
|
||||
>
|
||||
Teams
|
||||
Creator
|
||||
</Link>
|
||||
<Link
|
||||
href="/browse?type=skill"
|
||||
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
||||
href="/pricing"
|
||||
className="text-sm font-semibold text-stone-600 transition hover:text-stone-900"
|
||||
>
|
||||
Skills
|
||||
Pricing
|
||||
</Link>
|
||||
</nav>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link
|
||||
href="/sign-in"
|
||||
className="text-sm font-medium text-gray-600 hover:text-gray-900"
|
||||
href="/api/auth/sign-in"
|
||||
className="text-sm font-semibold text-stone-700 transition hover:text-stone-900"
|
||||
>
|
||||
Sign in
|
||||
</Link>
|
||||
<Link
|
||||
href="/sign-up"
|
||||
className="rounded-md bg-gray-900 px-4 py-2 text-sm font-medium text-white hover:bg-gray-800"
|
||||
href="/creator"
|
||||
className="rounded-lg bg-stone-900 px-4 py-2 text-sm font-semibold text-stone-100 transition hover:bg-stone-800"
|
||||
>
|
||||
Get started
|
||||
Start selling
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user