diff --git a/apps/blog/src/components/PostCard.tsx b/apps/blog/src/components/PostCard.tsx index 79a80e9404..085fcaa104 100644 --- a/apps/blog/src/components/PostCard.tsx +++ b/apps/blog/src/components/PostCard.tsx @@ -1,11 +1,11 @@ -'use client'; +"use client"; -import Image from "next/image"; -import Link from "next/link"; -import { Badge, Card } from "@prisma/eclipse"; -import { cn } from "@prisma-docs/ui/lib/cn"; - -import { AuthorAvatarGroup } from "@/components/AuthorAvatarGroup"; +import { + PostCard as SharedPostCard, + type PostCardItem as SharedPostCardItem, +} from "@prisma-docs/ui/components/post-card"; +import { type AuthorProfile } from "@prisma-docs/ui/components/author-avatar-group"; +import { getAuthorProfiles } from "@/lib/authors"; import { formatDate, formatTag } from "@/lib/format"; import { withBlogBasePathForImageSrc } from "@/lib/url"; @@ -30,91 +30,35 @@ export function PostCard({ currentCategory: string; featured?: boolean; }) { - const isFeatured = featured; - const imageSizes = isFeatured ? "(min-width: 640px) 50vw, 100vw" : "384px"; - - const containerClassName = cn( - "group grid overflow-hidden", - isFeatured - ? "grid-cols-1 md:grid-cols-2 gap-4 bg-background-default rounded-square border border-stroke-neutral shadow-box-low" - : "sm:grid-cols-[1fr_384px] border-b pb-4 sm:pb-6 border-stroke-neutral gap-8", - ); - const imageClassName = cn( - "object-cover transition-transform duration-300 group-hover:scale-[1.02]", - !isFeatured && "rounded-square", - ); - const imageWrapperClassName = cn( - "relative aspect-video w-full h-full", - isFeatured ? "order-1" : "order-2 max-w-96 hidden sm:block", - ); - - const titleClassName = cn( - "text-foreground-neutral font-mona-sans mt-4 mb-2", - isFeatured - ? "text-2xl font-bold" - : "text-md md:text-lg font-[650] sm:font-bold", - ); - const excerptClassName = cn( - "text-sm text-foreground-neutral-weak line-clamp-2", - isFeatured && "leading-[20px]!", - ); - const authorClassName = cn( - "items-center gap-2 font-semibold text-sm mt-4 md:mt-0", - isFeatured ? "flex" : "hidden sm:flex", - ); - - const postBody = ( - <> -
-
- {post.tags && post.tags.length > 0 && ( - - )} - {post.date && ( - - {formatDate(new Date(post.date).toISOString())} - - )} -
- {post.title &&

{post.title}

} - {post.excerpt &&

{post.excerpt}

} + // Transform blog-specific post data to shared component format + const authorProfiles = post.author ? getAuthorProfiles([post.author]) : []; + const author: AuthorProfile | null = + authorProfiles.length > 0 + ? { + name: authorProfiles[0].name, + imageSrc: authorProfiles[0].imageSrc + ? withBlogBasePathForImageSrc(authorProfiles[0].imageSrc) + : null, + } + : null; + const badge = + post.tags && post.tags.length > 0 + ? formatTag( + currentCategory !== "show-all" ? currentCategory : post.tags[0], + ) + : null; -
- {post.author && ( - - )} - - ); + const sharedPost: SharedPostCardItem = { + url: post.url, + title: post.title, + date: formatDate(new Date(post.date).toISOString()), + excerpt: post.excerpt, + author, + imageSrc: post.imageSrc ? withBlogBasePathForImageSrc(post.imageSrc) : null, + imageAlt: post.imageAlt, + badge, + }; - return ( - - {post.imageSrc && ( -
- {post.imageAlt -
- )} - {isFeatured ? ( - - {postBody} - - ) : ( -
{postBody}
- )} - - ); + return ; } diff --git a/apps/site/package.json b/apps/site/package.json index f89768ff49..7aa57c86ad 100644 --- a/apps/site/package.json +++ b/apps/site/package.json @@ -13,17 +13,20 @@ }, "dependencies": { "@base-ui/react": "catalog:", - "@prisma/eclipse": "workspace:^", "@prisma-docs/ui": "workspace:*", + "@prisma/eclipse": "workspace:^", "cors": "^2.8.6", + "html-react-parser": "^5.2.17", "lucide-react": "catalog:", "next": "catalog:", "npm-to-yarn": "catalog:", + "posthog-js": "catalog:", "react": "catalog:", "react-dom": "catalog:", "react-tweet": "catalog:", - "posthog-js": "catalog:", "remark-directive": "catalog:", + "shiki": "3.22.0", + "tailwind-merge": "catalog:", "zod": "catalog:" }, "devDependencies": { @@ -32,11 +35,11 @@ "@types/node": "catalog:", "@types/react": "catalog:", "@types/react-dom": "catalog:", + "babel-plugin-react-compiler": "catalog:", "next-validate-link": "catalog:", "postcss": "catalog:", "tailwindcss": "catalog:", "tsx": "catalog:", - "typescript": "catalog:", - "babel-plugin-react-compiler": "catalog:" + "typescript": "catalog:" } } diff --git a/apps/site/public/icons/technologies/nextjs-light.svg b/apps/site/public/icons/technologies/nextjs-light.svg new file mode 100644 index 0000000000..a1268b1305 --- /dev/null +++ b/apps/site/public/icons/technologies/nextjs-light.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/site/public/icons/technologies/nextjs.svg b/apps/site/public/icons/technologies/nextjs.svg new file mode 100644 index 0000000000..65745d8990 --- /dev/null +++ b/apps/site/public/icons/technologies/nextjs.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/apps/site/public/icons/technologies/prisma.svg b/apps/site/public/icons/technologies/prisma.svg new file mode 100644 index 0000000000..e50dbcee81 --- /dev/null +++ b/apps/site/public/icons/technologies/prisma.svg @@ -0,0 +1 @@ + diff --git a/apps/site/public/icons/technologies/prisma_light.svg b/apps/site/public/icons/technologies/prisma_light.svg new file mode 100644 index 0000000000..7a199a7cbf --- /dev/null +++ b/apps/site/public/icons/technologies/prisma_light.svg @@ -0,0 +1 @@ + diff --git a/apps/site/public/illustrations/hero-grid.svg b/apps/site/public/illustrations/hero-grid.svg new file mode 100644 index 0000000000..8f0dd9b55f --- /dev/null +++ b/apps/site/public/illustrations/hero-grid.svg @@ -0,0 +1,364 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/site/public/photos/people/guillermo-rauch.jpeg b/apps/site/public/photos/people/guillermo-rauch.jpeg new file mode 100644 index 0000000000..73434bfdb3 Binary files /dev/null and b/apps/site/public/photos/people/guillermo-rauch.jpeg differ diff --git a/apps/site/src/app/(index)/page.tsx b/apps/site/src/app/(index)/page.tsx index 428125b655..5b8da6ea72 100644 --- a/apps/site/src/app/(index)/page.tsx +++ b/apps/site/src/app/(index)/page.tsx @@ -1,5 +1,8 @@ import type { Metadata } from "next"; -import { SITE_HOME_DESCRIPTION, SITE_HOME_TITLE } from "../../lib/blog-metadata"; +import { + SITE_HOME_DESCRIPTION, + SITE_HOME_TITLE, +} from "../../lib/site-metadata"; export const metadata: Metadata = { title: SITE_HOME_TITLE, diff --git a/apps/site/src/app/(prisma-with)/nestjs/page.tsx b/apps/site/src/app/(prisma-with)/nestjs/page.tsx new file mode 100644 index 0000000000..a37b0900f0 --- /dev/null +++ b/apps/site/src/app/(prisma-with)/nestjs/page.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import * as data from "../../../data/prisma-with/nestjs.json"; +import { PrismaWithLayout } from "../../../components/prisma-with/layout"; + +const codeExamples: Record = { + "static-data": `// app/blog/[slug]/page.tsx +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +// Return a list of 'params' to populate the [slug] dynamic segment +export async function generateStaticParams() { + const posts = await prisma.post.findMany() + + return posts.map((post) => ({ + slug: post.slug, + })) +} + +// Multiple versions of this page will be statically generated +// using the 'params' returned by 'generateStaticParams' +export default async function Page({ params }: { params: { slug: string } }) { + // Fetch the post based on slug + const post = await prisma.post.findUnique({ + where: { slug: params.slug }, + }) + + // Simple demo rendering + return ( +
+

{post?.title || 'Post not found'}

+

{post?.content || 'No content available'}

+
+ ) +}`, +}; + +export const metadata: Metadata = { + title: "Next.js Database with Prisma | Next-Generation ORM for SQL Databases", + description: + "Prisma is a next-generation ORM for Node.js & TypeScript. It's the easiest way to build Next.js apps with MySQL, PostgreSQL & SQL Server databases.", + alternates: { + canonical: "https://www.prisma.io/nextjs", + }, +}; + +export default async function NextJsPage() { + return ; +} diff --git a/apps/site/src/app/(prisma-with)/nextjs/page.tsx b/apps/site/src/app/(prisma-with)/nextjs/page.tsx new file mode 100644 index 0000000000..601cabc8ad --- /dev/null +++ b/apps/site/src/app/(prisma-with)/nextjs/page.tsx @@ -0,0 +1,152 @@ +import type { Metadata } from "next"; +import * as data from "../../../data/prisma-with/nextjs.json"; +import { PrismaWithLayout } from "../../../components/prisma-with/layout"; + +const codeExamples: Record = { + "static-data": `// app/blog/[slug]/page.tsx +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +// Return a list of 'params' to populate the [slug] dynamic segment +export async function generateStaticParams() { + const posts = await prisma.post.findMany() + + return posts.map((post) => ({ + slug: post.slug, + })) +} + +// Multiple versions of this page will be statically generated +// using the 'params' returned by 'generateStaticParams' +export default async function Page({ params }: { params: { slug: string } }) { + // Fetch the post based on slug + const post = await prisma.post.findUnique({ + where: { slug: params.slug }, + }) + + // Simple demo rendering + return ( +
+

{post?.title || 'Post not found'}

+

{post?.content || 'No content available'}

+
+ ) +}`, + "dynamic-data": `// app/dashboard/page.tsx + +import { auth } from '@/lib/auth' +import { PrismaClient } from '@prisma/client' + +const prisma = new PrismaClient() + +// Dynamic by default +export default async function DashboardPage() { + // Get the session using your auth solution + const session = await auth() + + if (!session) { + redirect('/login') + } + + const posts = await prisma.post.findMany({ + where: { + authorId: session.user.id + } + }) + + return ( +
    + {posts.map(post => ( +
  • {post.title}
  • + ))} +
+ ) +}`, + "server-actions": `// app/actions.ts +'use server' + +import { PrismaClient } from '@prisma/client' +import { revalidatePath } from 'next/cache' + +const prisma = new PrismaClient() + +export async function createPost(formData: FormData) { + const title = formData.get('title') as string + const content = formData.get('content') as string + + await prisma.post.create({ + data: { + title, + content + } + }) + + revalidatePath('/blog') +}`, + "api-routes": `// app/api/posts/route.ts + +import { PrismaClient } from '@prisma/client' +import { NextResponse } from 'next/server' + +const prisma = new PrismaClient() + +export async function GET() { + const posts = await prisma.post.findMany() + return NextResponse.json(posts) +} + +export async function POST(request: Request) { + const json = await request.json() + const post = await prisma.post.create({ + data: json + }) + return NextResponse.json(post) +} +`, + "client-components": `// app/components/PostList.tsx +'use client' + +import { useEffect, useState } from 'react' +import { Post } from '@prisma/client' + +export default function PostList({ + initialPosts +}: { + initialPosts: Post[] +}) { + const [posts, setPosts] = useState(initialPosts) + + return ( +
    + {posts.map(post => ( +
  • {post.title}
  • + ))} +
+ ) +} + +// app/blog/page.tsx +import { PrismaClient } from '@prisma/client' +import PostList from '../components/PostList' + +const prisma = new PrismaClient() + +export default async function BlogPage() { + const posts = await prisma.post.findMany() + return +}`, +}; + +export const metadata: Metadata = { + title: "Next.js Database with Prisma | Next-Generation ORM for SQL Databases", + description: + "Prisma is a next-generation ORM for Node.js & TypeScript. It's the easiest way to build Next.js apps with MySQL, PostgreSQL & SQL Server databases.", + alternates: { + canonical: "https://www.prisma.io/nextjs", + }, +}; + +export default async function NextJsPage() { + return ; +} diff --git a/apps/site/src/app/global.css b/apps/site/src/app/global.css index f0100f4a23..b22decad90 100644 --- a/apps/site/src/app/global.css +++ b/apps/site/src/app/global.css @@ -6,6 +6,10 @@ --color-fd-primary: var(--color-stroke-ppg); } +.hero { + @apply pt-30; +} + .bg-blog { background-color: var(--color-background-default); } @@ -39,51 +43,50 @@ .newsletter-bg { background: linear-gradient( - 59deg, - color-mix(in srgb, var(--color-foreground-ppg) 30%, transparent) 6.53%, + 59deg, + color-mix(in srgb, var(--color-foreground-ppg) 30%, transparent) 6.53%, var(--color-background-default) 74.71% - ) + ); } @keyframes glitch-1 { 0% { - clip-path: inset(20% 0 60% 0); + clip-path: inset(20% 0 60% 0); } 20% { - clip-path: inset(10% 0 85% 0); + clip-path: inset(10% 0 85% 0); } 40% { - clip-path: inset(40% 0 40% 0); + clip-path: inset(40% 0 40% 0); } 60% { - clip-path: inset(80% 0 5% 0); + clip-path: inset(80% 0 5% 0); } 80% { - clip-path: inset(50% 0 30% 0); + clip-path: inset(50% 0 30% 0); } 100% { - clip-path: inset(25% 0 55% 0); + clip-path: inset(25% 0 55% 0); } - } +} @keyframes glitch-2 { 0% { - clip-path: inset(80% 0 5% 0); + clip-path: inset(80% 0 5% 0); } 20% { - clip-path: inset(50% 0 30% 0); + clip-path: inset(50% 0 30% 0); } 40% { - clip-path: inset(20% 0 60% 0); + clip-path: inset(20% 0 60% 0); } 60% { - clip-path: inset(10% 0 85% 0); + clip-path: inset(10% 0 85% 0); } 80% { - clip-path: inset(40% 0 40% 0); + clip-path: inset(40% 0 40% 0); } 100% { - clip-path: inset(75% 0 15% 0); + clip-path: inset(75% 0 15% 0); } } - diff --git a/apps/site/src/app/layout.tsx b/apps/site/src/app/layout.tsx index 3fac2aaf3e..eea6746384 100644 --- a/apps/site/src/app/layout.tsx +++ b/apps/site/src/app/layout.tsx @@ -4,10 +4,12 @@ import "./global.css"; import { Inter } from "next/font/google"; import type { Metadata } from "next"; import Script from "next/script"; -import { SITE_HOME_DESCRIPTION, SITE_HOME_TITLE } from "@/lib/blog-metadata"; +import type React from "react"; +import { SITE_HOME_DESCRIPTION, SITE_HOME_TITLE } from "@/lib/site-metadata"; import { WebNavigation } from "@prisma-docs/ui/components/web-navigation"; import { Footer } from "@prisma-docs/ui/components/footer"; import { ThemeProvider } from "@prisma-docs/ui/components/theme-provider"; +import { FontAwesomeScript as WebFA } from "@prisma/eclipse"; const inter = Inter({ subsets: ["latin"], @@ -20,6 +22,28 @@ export const metadata: Metadata = { description: SITE_HOME_DESCRIPTION, }; +const themeInitScript = ` +(() => { + try { + const storageKey = "theme"; + const stored = localStorage.getItem(storageKey); + const prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches; + const resolved = + stored === "light" || stored === "dark" + ? stored + : prefersDark + ? "dark" + : "light"; + + const root = document.documentElement; + root.setAttribute("data-theme", resolved); + root.classList.toggle("dark", resolved === "dark"); + } catch { + // Ignore storage/media-query failures and use CSS defaults. + } +})(); +`; + function baseOptions() { return { nav: { @@ -69,9 +93,9 @@ function baseOptions() { icon: "fa-regular fa-message-code", }, { - text: "Get started", - url: "https://www.prisma.io/docs", - icon: "fa-regular fa-book-open", + text: "Prisma Partners", + url: "/partners", + icon: "fa-regular fa-lightbulb", }, { text: "Tutorials", @@ -108,8 +132,8 @@ function baseOptions() { ], }, { - url: "/partners", - text: "Partners", + url: "/docs", + text: "Docs", }, { url: "https://www.prisma.io/blog", @@ -121,18 +145,12 @@ function baseOptions() { export default function Layout({ children }: { children: React.ReactNode }) { return ( - + - +