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
17 changes: 17 additions & 0 deletions apps/site/src/app/stack/bun-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* The Bun mark, drawn as a single-colour silhouette (with eyes + smile cut out
* via even-odd) in currentColor so it matches the Prisma mark's treatment.
*/
export function BunMark({ className }: { className?: string }) {
const silhouette =
"M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75Z";
const leftEye = "M20.19,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z";
const rightEye = "M44.96,33.29a5.51,5.51 0 1,0 11.02,0a5.51,5.51 0 1,0 -11.02,0Z";
const mouth =
"M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z";
return (
<svg viewBox="0 0 76 66" className={className} fill="currentColor" aria-hidden>
<path d={`${silhouette} ${leftEye} ${rightEye} ${mouth}`} fillRule="evenodd" />
</svg>
);
}
60 changes: 60 additions & 0 deletions apps/site/src/app/stack/framework-carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Marquee } from "@/components/marquee";
import { Action, Card } from "@prisma/eclipse";
import type { CSSProperties } from "react";
import { frameworks } from "./stack-data";

/**
* Vertical stack: a scrolling marquee of the frameworks Prisma Compute runs,
* with a Prisma Compute card beneath it. Both are full-width so the pairing
* reads cleanly on mobile. The compute icon matches the diagram node.
*/
export function FrameworkCarousel() {
return (
<div className="mx-auto flex w-full max-w-137 flex-col gap-4">
<Card className="overflow-hidden p-4">
<Marquee
direction="up"
fade
fillContainer={false}
className="h-58 w-full"
style={{ "--duration": "22s", "--gap": "0.875rem" } as CSSProperties}
>
{frameworks.map((f) => (
<div
key={f.name}
className="flex h-16 items-center gap-3.5 rounded-square-high border border-stroke-neutral bg-[linear-gradient(180deg,var(--color-background-default)_0%,var(--color-background-ppg)_260%)] px-5"
>
<div
className="grid size-10 shrink-0 place-items-center rounded-square border border-stroke-neutral-strong font-sans-display text-base font-black"
style={{ background: f.bg, color: f.color }}
>
{f.mono}
</div>
<div className="font-sans-display text-lg font-extrabold text-foreground-neutral">
{f.name}
</div>
</div>
))}
</Marquee>
</Card>

<div className="flex justify-center">
<i className="fa-regular fa-plus text-lg text-foreground-neutral-weaker" aria-hidden />
</div>

<Card className="flex-row items-center gap-4 bg-[linear-gradient(180deg,var(--color-background-default)_0%,var(--color-background-ppg)_262.5%)] p-5">
<Action color="ppg" size="4xl">
<i className="fa-regular fa-microchip text-2xl" aria-hidden />
</Action>
<div className="flex flex-col">
<span className="font-sans-display text-lg font-bold text-foreground-neutral">
Prisma Compute
</span>
<span className="text-sm text-foreground-neutral-weak">
Same runtime, same database, same deploy for any framework.
</span>
</div>
</Card>
</div>
);
}
272 changes: 215 additions & 57 deletions apps/site/src/app/stack/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { createPageMetadata } from "@/lib/page-metadata";
import { Card } from "@prisma/eclipse";
import { Layers } from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import { cn } from "@/lib/cn";
import {
stackCategories,
type StackCategory,
type StackLinkItem,
} from "./stack-data";
import { prisma_highlighter } from "@/lib/shiki_prisma";
import { Action, Button, Card, CodeBlock } from "@prisma/eclipse";
import type { ReactNode } from "react";
import { FrameworkCarousel } from "./framework-carousel";
import { PrismaMark } from "./prisma-mark";
import { Reveal } from "./reveal";
import { StackDiagram } from "./stack-diagram";
import { bunApis, postgresExtensions } from "./stack-data";
import styles from "./stack.module.css";

const title = "Prisma in your stack | Prisma";
const title = "Prisma Stack: a complete TypeScript foundation | Prisma";
const description =
"Prisma is a Node.js and TypeScript ORM that integrates easily with popular databases, and frameworks.";
"Prisma Compute, Prisma Postgres, Bun and TypeScript. A complete, opinionated stack with room for your frontend.";

export const metadata = createPageMetadata({
title,
Expand All @@ -21,60 +20,219 @@ export const metadata = createPageMetadata({
ogImage: "/og/og-stack.png",
});

const ORM_CODE = `const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }, // fully typed
})`;

const EXT_CODE = `-- semantic search, no extra service
CREATE EXTENSION vector;

SELECT id, content
FROM docs
ORDER BY embedding <-> $query
LIMIT 5;`;

async function Code({ code, lang }: { code: string; lang: "typescript" | "sql" }) {
const html = await (await prisma_highlighter()).codeToHtml(code, { lang, theme: "prisma-dark" });
return (
<div className="mt-6 overflow-hidden rounded-[8px] border border-stroke-neutral bg-background-neutral-weaker px-5 py-4 [&>figure]:my-0 [&>figure>div]:bg-transparent [&>figure>div]:py-0">
<CodeBlock keepBackground className="border-none [&_pre]:bg-transparent">
<div dangerouslySetInnerHTML={{ __html: html }} />
</CodeBlock>
</div>
);
}

function SectionHead({
eyebrow,
title,
children,
}: {
eyebrow?: string;
title: string;
children?: ReactNode;
}) {
return (
<Reveal className="mx-auto flex max-w-2xl flex-col items-center gap-4 text-center">
{eyebrow ? <span className="type-title-sm text-foreground-ppg">{eyebrow}</span> : null}
<h2 className="type-title-4xl m-0 text-balance text-foreground-neutral">{title}</h2>
{children ? <p className="m-0 text-lg text-foreground-neutral-weak">{children}</p> : null}
</Reveal>
);
}

export default function StackPage() {
return (
<main className="flex-1 w-full -mt-24 bg-[linear-gradient(to_bottom,var(--color-background-ppg),var(--color-background-default)_50%)] text-foreground-neutral">
<div className="hero -mt-24 pt-64 pb-12 flex items-end justify-center px-4 relative">
<div className="absolute inset-0 pointer-events-none z-1 bg-[linear-gradient(180deg,var(--color-foreground-ppg)_0%,var(--color-background-default)_100%)] opacity-20" />
<div className="content relative z-2 my-12 py-12 flex flex-col gap-8">
<div className="flex flex-col gap-4 items-center text-center">
<div className="flex items-center gap-2 text-foreground-ppg-weak type-title-sm">
<i className="fa-regular fa-layer-group" />
<span>Prisma Stack</span>
</div>
<h1 className="text-4xl sm:text-5xl md:text-6xl stretch-display mb-0 text-center mt-0 font-sans-display text-foreground-neutral max-w-224 mx-auto">
Build with
<br /> your favorite tools
</h1>
</div>
<main className={`${styles.page} flex-1 bg-background-default text-foreground-neutral`}>
{/* ===== HERO ===== */}
<div className="hero relative -mt-24 flex items-end justify-center px-4 pt-40">
<div className="absolute inset-0 z-0 pointer-events-none bg-[linear-gradient(180deg,var(--color-foreground-ppg)_0%,var(--color-background-default)_100%)] opacity-20" />
<div className="absolute inset-0 z-0 pointer-events-none bg-[url('/illustrations/homepage/footer_grid.svg')] opacity-60" />
<div className="content relative z-2 flex max-w-3xl flex-col items-center gap-5 text-center">
<h1 className="stretch-display m-0 max-w-224 font-sans-display text-4xl text-foreground-neutral sm:text-5xl md:text-6xl">
Prisma Stack
</h1>
<h2 className="type-title-2xl m-0 text-foreground-neutral-weak">
Stop configuring. Start shipping.
</h2>
<p className="m-0 max-w-2xl text-lg text-foreground-neutral-weak">
Most apps have the same recurring problems. Prisma Stack solves them with the fewest
dependencies running on one platform. Great performance without learning and configuring
multiple frameworks or providers.
</p>
</div>
</div>

<section className="px-4 pb-16 md:pb-20">
<div className="mx-auto flex w-full max-w-[1024px] flex-col gap-12">
{stackCategories.map((category: StackCategory) => (
<div key={category.id} className="flex flex-col gap-4">
<h3 className="m-0 text-foreground-neutral text-3xl font-sans-display [font-variation-settings:'wght'_900]">
{category.title}
</h3>
<p className="m-0 text-foreground-neutral-weak">
{category.description}
</p>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{category.items.map((item: StackLinkItem) => (
<Link key={item.id} href={item.href} className="group">
<Card className="flex h-full flex-row items-center gap-3 p-4 transition-colors hover:border-stroke-neutral-strong hover:bg-surface-elevated dark:bg-[#0A101D]">
<Image
src={item.icon}
alt={item.name}
width={24}
height={24}
className={cn(
"size-6 shrink-0 object-contain",
item.invertInDark && "dark:invert",
)}
/>
<span className="font-sans text-lg font-semibold text-foreground-neutral">
{item.name}
<div className={styles.content}>
{/* ===== THE STACK, SIMPLIFIED + DIAGRAM ===== */}
<section className="px-4 py-12 md:py-16">
<div className="mx-auto flex w-full max-w-296 flex-col gap-10">
<StackDiagram />
<SectionHead eyebrow="How opinionated is it?" title="Every layer is replaceable.">
You can still deploy your app or database wherever you&apos;d like. Use any dependency
you&apos;re familiar with. Prisma Stack still works.
</SectionHead>
</div>
</section>

{/* ===== FRONTEND CAROUSEL ===== */}
<section className="px-4 py-12 md:py-16">
<div className="mx-auto flex w-full max-w-296 flex-col gap-10">
<SectionHead
eyebrow="Bring your own frontend"
title="We have no opinion here. On purpose."
>
Prisma Compute runs every major frontend framework. Same database, same runtime, same
deploy. Your call on the view layer.
</SectionHead>
<Reveal>
<FrameworkCarousel />
</Reveal>
</div>
</section>

{/* ===== BUN ===== */}
<section className="px-4 py-12 md:py-16">
<div className="mx-auto flex w-full max-w-296 flex-col gap-10">
<SectionHead title="Batteries included.">Bun simplifies your dependencies.</SectionHead>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{bunApis.map((b) => (
<Reveal key={b.api}>
<Card className="h-full gap-2 bg-[linear-gradient(180deg,var(--color-background-default)_0%,var(--color-background-ppg)_262.5%)]">
<code className="font-mono text-sm font-medium text-foreground-ppg-strong">
{b.api}
</code>
<p className="m-0 text-sm text-foreground-neutral-weak">{b.role}</p>
</Card>
</Reveal>
))}
</div>
</div>
</section>

{/* ===== DATA ===== */}
<section className="px-4 py-12 md:py-16">
<div className="mx-auto flex w-full max-w-296 flex-col gap-10">
<SectionHead eyebrow="Data layer" title="Prisma Postgres, done properly.">
A real Postgres database with a type-safe ORM on top and the extensions you actually
use, one line away.
</SectionHead>
<div className="grid grid-cols-1 gap-6 md:grid-cols-2">
<Reveal>
<Card className="h-full gap-4 p-8">
<div className="flex items-center gap-3">
<Action color="ppg" size="4xl">
<PrismaMark className="size-6" />
</Action>
<h3 className="type-title-lg m-0 text-foreground-neutral">Prisma ORM</h3>
</div>
<p className="m-0 text-foreground-neutral-weak">
Model your schema once. Get autocompletion, compile-time safety and migrations
for free.
</p>
<ul className="m-0 flex list-none flex-col gap-3 p-0 text-foreground-neutral">
<li className="flex gap-3">
<i className="fa-regular fa-check mt-1 text-foreground-ppg" aria-hidden />
Type-safe queries, end to end
</li>
<li className="flex gap-3">
<i className="fa-regular fa-check mt-1 text-foreground-ppg" aria-hidden />
Declarative schema and versioned migrations
</li>
<li className="flex gap-3">
<i className="fa-regular fa-check mt-1 text-foreground-ppg" aria-hidden />
Agent guardrails against unsafe writes
</li>
</ul>
<Code code={ORM_CODE} lang="typescript" />
</Card>
</Reveal>
<Reveal>
<Card className="h-full gap-4 p-8">
<div className="flex items-center gap-3">
<Action color="ppg" size="4xl">
<i className="fa-regular fa-cubes-stacked text-2xl" aria-hidden />
</Action>
<h3 className="type-title-lg m-0 text-foreground-neutral">
Postgres extensions
</h3>
</div>
<p className="m-0 text-foreground-neutral-weak">
Opinionated defaults you can swap. Turn on vector search, full-text and cron
without leaving Postgres.
</p>
<div className="flex flex-wrap gap-2">
{postgresExtensions.map((ext) => (
<span
key={ext}
className="rounded-square border border-stroke-ppg/30 bg-background-ppg px-3 py-1.5 font-mono text-xs text-foreground-ppg-strong"
>
{ext}
</span>
</Card>
</Link>
))}
))}
</div>
<Code code={EXT_CODE} lang="sql" />
</Card>
</Reveal>
</div>
</div>
</section>

{/* ===== FINAL CTA ===== */}
<div className="relative rounded-full bg-[url('/illustrations/homepage/footer_grid.svg')] bg-center before:pointer-events-none before:absolute before:inset-x-30 before:inset-y-[40%] before:-z-1 before:rounded-full before:bg-teal-400 before:blur-[100px] before:content-['']">
<section id="start" className="scroll-mt-24 px-4 py-16">
<div className="mx-auto flex w-fit flex-col items-center justify-center gap-8 text-center">
<div className="flex flex-col items-center gap-4">
<h2 className="type-title-2xl m-0 text-foreground-neutral">
Stop configuring. Start shipping.
</h2>
<p className="m-0 max-w-lg text-foreground-neutral-weak">
The database, the runtime and the compute layer are decided and connected. You
write features.
</p>
</div>
<div className="flex flex-col gap-4 md:flex-row">
<Button asChild variant="ppg" size="2xl">
<a href="https://console.prisma.io" className="flex items-center gap-2">
Start building
<i className="fa-regular fa-arrow-right" aria-hidden />
</a>
</Button>
<Button asChild variant="default-strong" size="2xl">
<a href="https://www.prisma.io/docs" className="flex items-center gap-2">
Read the docs
<i className="fa-regular fa-book-open" aria-hidden />
</a>
</Button>
</div>
<p className="m-0 text-sm text-foreground-neutral-weaker">
<code className="font-mono text-foreground-ppg-strong">npx create-prisma</code>
</p>
</div>
))}
</section>
</div>
</section>
</div>
</main>
);
}
8 changes: 8 additions & 0 deletions apps/site/src/app/stack/prisma-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** The Prisma prism mark, drawn in currentColor so it inherits its tile's colour. */
export function PrismaMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className} fill="currentColor" aria-hidden>
<path d="M21.8068 18.2848L13.5528.7565c-.207-.4382-.639-.7273-1.1286-.7541-.5023-.0293-.9523.213-1.2062.6253L2.266 15.1271c-.2773.4518-.2718 1.0091.0158 1.4555l4.3759 6.7786c.2608.4046.7127.6388 1.1823.6388.1332 0 .267-.0188.3987-.0577l12.7019-3.7568c.3891-.1151.7072-.3904.8737-.7553s.1633-.7828-.0075-1.1454zm-1.8481.7519L9.1814 22.2242c-.3292.0975-.6448-.1873-.5756-.5194l3.8501-18.4386c.072-.3448.5486-.3996.699-.0803l7.1288 15.138c.1344.2856-.019.6224-.325.7128z" />
</svg>
);
}
Loading
Loading