fix: remove everything

This commit is contained in:
shadcn 2023-04-25 19:21:08 +04:00
parent 20eb7e738a
commit a4faa8e58a
35 changed files with 0 additions and 2127 deletions

View file

@ -1,7 +0,0 @@
interface AuthLayoutProps {
children: React.ReactNode
}
export default function AuthLayout({ children }: AuthLayoutProps) {
return <div className="min-h-screen">{children}</div>
}

View file

@ -1,51 +0,0 @@
import { Metadata } from "next"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
import { UserAuthForm } from "@/components/user-auth-form"
export const metadata: Metadata = {
title: "Login",
description: "Login to your account",
}
export default function LoginPage() {
return (
<div className="container flex h-screen w-screen flex-col items-center justify-center">
<Link
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-4 top-4 md:left-8 md:top-8"
)}
>
<>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
Back
</>
</Link>
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-muted-foreground">
Enter your email to sign in to your account
</p>
</div>
<UserAuthForm />
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/register"
className="hover:text-brand underline underline-offset-4"
>
Don&apos;t have an account? Sign Up
</Link>
</p>
</div>
</div>
)
}

View file

@ -1,59 +0,0 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
import { UserAuthForm } from "@/components/user-auth-form"
export const metadata = {
title: "Create an account",
description: "Create an account to get started.",
}
export default function RegisterPage() {
return (
<div className="container grid h-screen w-screen flex-col items-center justify-center lg:max-w-none lg:grid-cols-2 lg:px-0">
<Link
href="/login"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Login
</Link>
<div className="hidden h-full bg-muted lg:block" />
<div className="lg:p-8">
<div className="mx-auto flex w-full flex-col justify-center space-y-6 sm:w-[350px]">
<div className="flex flex-col space-y-2 text-center">
<Icons.logo className="mx-auto h-6 w-6" />
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-sm text-muted-foreground">
Enter your email below to create your account
</p>
</div>
<UserAuthForm />
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"
className="hover:text-brand underline underline-offset-4"
>
Terms of Service
</Link>{" "}
and{" "}
<Link
href="/privacy"
className="hover:text-brand underline underline-offset-4"
>
Privacy Policy
</Link>
.
</p>
</div>
</div>
</div>
)
}

View file

@ -1,17 +0,0 @@
import { CardSkeleton } from "@/components/card-skeleton"
import { DashboardHeader } from "@/components/header"
import { DashboardShell } from "@/components/shell"
export default function DashboardBillingLoading() {
return (
<DashboardShell>
<DashboardHeader
heading="Billing"
text="Manage billing and your subscription plan."
/>
<div className="grid gap-10">
<CardSkeleton />
</div>
</DashboardShell>
)
}

View file

@ -1,76 +0,0 @@
import { redirect } from "next/navigation"
import { authOptions } from "@/lib/auth"
import { getCurrentUser } from "@/lib/session"
import { stripe } from "@/lib/stripe"
import { getUserSubscriptionPlan } from "@/lib/subscription"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { BillingForm } from "@/components/billing-form"
import { DashboardHeader } from "@/components/header"
import { Icons } from "@/components/icons"
import { DashboardShell } from "@/components/shell"
export const metadata = {
title: "Billing",
description: "Manage billing and your subscription plan.",
}
export default async function BillingPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
const subscriptionPlan = await getUserSubscriptionPlan(user.id)
// If user has a pro plan, check cancel status on Stripe.
let isCanceled = false
if (subscriptionPlan.isPro && subscriptionPlan.stripeSubscriptionId) {
const stripePlan = await stripe.subscriptions.retrieve(
subscriptionPlan.stripeSubscriptionId
)
isCanceled = stripePlan.cancel_at_period_end
}
return (
<DashboardShell>
<DashboardHeader
heading="Billing"
text="Manage billing and your subscription plan."
/>
<div className="grid gap-8">
<Alert className="!pl-14">
<Icons.warning />
<AlertTitle>This is a demo app.</AlertTitle>
<AlertDescription>
Taxonomy app is a demo app using a Stripe test environment. You can
find a list of test card numbers on the{" "}
<a
href="https://stripe.com/docs/testing#cards"
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-8"
>
Stripe docs
</a>
.
</AlertDescription>
</Alert>
<BillingForm
subscriptionPlan={{
...subscriptionPlan,
isCanceled,
}}
/>
</div>
</DashboardShell>
)
}

View file

@ -1,48 +0,0 @@
import { notFound } from "next/navigation"
import { dashboardConfig } from "@/config/dashboard"
import { getCurrentUser } from "@/lib/session"
import { MainNav } from "@/components/main-nav"
import { DashboardNav } from "@/components/nav"
import { SiteFooter } from "@/components/site-footer"
import { UserAccountNav } from "@/components/user-account-nav"
interface DashboardLayoutProps {
children?: React.ReactNode
}
export default async function DashboardLayout({
children,
}: DashboardLayoutProps) {
const user = await getCurrentUser()
if (!user) {
return notFound()
}
return (
<div className="flex min-h-screen flex-col space-y-6">
<header className="sticky top-0 z-40 border-b bg-background">
<div className="container flex h-16 items-center justify-between py-4">
<MainNav items={dashboardConfig.mainNav} />
<UserAccountNav
user={{
name: user.name,
image: user.image,
email: user.email,
}}
/>
</div>
</header>
<div className="container grid flex-1 gap-12 md:grid-cols-[200px_1fr]">
<aside className="hidden w-[200px] flex-col md:flex">
<DashboardNav items={dashboardConfig.sidebarNav} />
</aside>
<main className="flex w-full flex-1 flex-col overflow-hidden">
{children}
</main>
</div>
<SiteFooter className="border-t" />
</div>
)
}

View file

@ -1,21 +0,0 @@
import { DashboardHeader } from "@/components/header"
import { PostCreateButton } from "@/components/post-create-button"
import { PostItem } from "@/components/post-item"
import { DashboardShell } from "@/components/shell"
export default function DashboardLoading() {
return (
<DashboardShell>
<DashboardHeader heading="Posts" text="Create and manage posts.">
<PostCreateButton />
</DashboardHeader>
<div className="divide-border-200 divide-y rounded-md border">
<PostItem.Skeleton />
<PostItem.Skeleton />
<PostItem.Skeleton />
<PostItem.Skeleton />
<PostItem.Skeleton />
</div>
</DashboardShell>
)
}

View file

@ -1,63 +0,0 @@
import { redirect } from "next/navigation"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { getCurrentUser } from "@/lib/session"
import { EmptyPlaceholder } from "@/components/empty-placeholder"
import { DashboardHeader } from "@/components/header"
import { PostCreateButton } from "@/components/post-create-button"
import { PostItem } from "@/components/post-item"
import { DashboardShell } from "@/components/shell"
export const metadata = {
title: "Dashboard",
}
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
const posts = await db.post.findMany({
where: {
authorId: user.id,
},
select: {
id: true,
title: true,
published: true,
createdAt: true,
},
orderBy: {
updatedAt: "desc",
},
})
return (
<DashboardShell>
<DashboardHeader heading="Posts" text="Create and manage posts.">
<PostCreateButton />
</DashboardHeader>
<div>
{posts?.length ? (
<div className="divide-y divide-border rounded-md border">
{posts.map((post) => (
<PostItem key={post.id} post={post} />
))}
</div>
) : (
<EmptyPlaceholder>
<EmptyPlaceholder.Icon name="post" />
<EmptyPlaceholder.Title>No posts created</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
You don&apos;t have any posts yet. Start creating content.
</EmptyPlaceholder.Description>
<PostCreateButton variant="outline" />
</EmptyPlaceholder>
)}
</div>
</DashboardShell>
)
}

View file

@ -1,18 +0,0 @@
import { Card } from "@/components/ui/card"
import { CardSkeleton } from "@/components/card-skeleton"
import { DashboardHeader } from "@/components/header"
import { DashboardShell } from "@/components/shell"
export default function DashboardSettingsLoading() {
return (
<DashboardShell>
<DashboardHeader
heading="Settings"
text="Manage account and website settings."
/>
<div className="grid gap-10">
<CardSkeleton />
</div>
</DashboardShell>
)
}

View file

@ -1,34 +0,0 @@
import { redirect } from "next/navigation"
import { authOptions } from "@/lib/auth"
import { getCurrentUser } from "@/lib/session"
import { DashboardHeader } from "@/components/header"
import { DashboardShell } from "@/components/shell"
import { UserNameForm } from "@/components/user-name-form"
export const metadata = {
title: "Settings",
description: "Manage account and website settings.",
}
export default async function SettingsPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
return (
<DashboardShell>
<DashboardHeader
heading="Settings"
text="Manage account and website settings."
/>
<div className="grid gap-10">
{user?.name ? (
<UserNameForm user={{ id: user.id, name: user.name }} />
) : null}
</div>
</DashboardShell>
)
}

View file

@ -1,106 +0,0 @@
import { notFound } from "next/navigation"
import { allDocs } from "contentlayer/generated"
import { getTableOfContents } from "@/lib/toc"
import { Mdx } from "@/components/mdx-components"
import { DocsPageHeader } from "@/components/page-header"
import { DocsPager } from "@/components/pager"
import { DashboardTableOfContents } from "@/components/toc"
import "@/styles/mdx.css"
import { Metadata } from "next"
import { absoluteUrl } from "@/lib/utils"
interface DocPageProps {
params: {
slug: string[]
}
}
async function getDocFromParams(params) {
const slug = params.slug?.join("/") || ""
const doc = allDocs.find((doc) => doc.slugAsParams === slug)
if (!doc) {
null
}
return doc
}
export async function generateMetadata({
params,
}: DocPageProps): Promise<Metadata> {
const doc = await getDocFromParams(params)
if (!doc) {
return {}
}
const url = process.env.NEXT_PUBLIC_APP_URL
const ogUrl = new URL(`${url}/api/og`)
ogUrl.searchParams.set("heading", doc.description ?? doc.title)
ogUrl.searchParams.set("type", "Documentation")
ogUrl.searchParams.set("mode", "dark")
return {
title: doc.title,
description: doc.description,
openGraph: {
title: doc.title,
description: doc.description,
type: "article",
url: absoluteUrl(doc.slug),
images: [
{
url: ogUrl.toString(),
width: 1200,
height: 630,
alt: doc.title,
},
],
},
twitter: {
card: "summary_large_image",
title: doc.title,
description: doc.description,
images: [ogUrl.toString()],
},
}
}
export async function generateStaticParams(): Promise<
DocPageProps["params"][]
> {
return allDocs.map((doc) => ({
slug: doc.slugAsParams.split("/"),
}))
}
export default async function DocPage({ params }: DocPageProps) {
const doc = await getDocFromParams(params)
if (!doc) {
notFound()
}
const toc = await getTableOfContents(doc.body.raw)
return (
<main className="relative py-6 lg:gap-10 lg:py-10 xl:grid xl:grid-cols-[1fr_300px]">
<div className="mx-auto w-full min-w-0">
<DocsPageHeader heading={doc.title} text={doc.description} />
<Mdx code={doc.body.code} />
<hr className="my-4 md:my-6" />
<DocsPager doc={doc} />
</div>
<div className="hidden text-sm xl:block">
<div className="sticky top-16 -mt-10 max-h-[calc(var(--vh)-4rem)] overflow-y-auto pt-10">
<DashboardTableOfContents toc={toc} />
</div>
</div>
</main>
)
}

View file

@ -1,17 +0,0 @@
import { docsConfig } from "@/config/docs"
import { DocsSidebarNav } from "@/components/sidebar-nav"
interface DocsLayoutProps {
children: React.ReactNode
}
export default function DocsLayout({ children }: DocsLayoutProps) {
return (
<div className="flex-1 md:grid md:grid-cols-[220px_1fr] md:gap-6 lg:grid-cols-[240px_1fr] lg:gap-10">
<aside className="fixed top-14 z-30 hidden h-[calc(100vh-3.5rem)] w-full shrink-0 overflow-y-auto border-r py-6 pr-2 md:sticky md:block lg:py-10">
<DocsSidebarNav items={docsConfig.sidebarNav} />
</aside>
{children}
</div>
)
}

View file

@ -1,116 +0,0 @@
import Link from "next/link"
import { notFound } from "next/navigation"
import { allGuides } from "contentlayer/generated"
import { getTableOfContents } from "@/lib/toc"
import { Icons } from "@/components/icons"
import { Mdx } from "@/components/mdx-components"
import { DocsPageHeader } from "@/components/page-header"
import { DashboardTableOfContents } from "@/components/toc"
import "@/styles/mdx.css"
import { Metadata } from "next"
import { absoluteUrl, cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
interface GuidePageProps {
params: {
slug: string[]
}
}
async function getGuideFromParams(params) {
const slug = params?.slug?.join("/")
const guide = allGuides.find((guide) => guide.slugAsParams === slug)
if (!guide) {
null
}
return guide
}
export async function generateMetadata({
params,
}: GuidePageProps): Promise<Metadata> {
const guide = await getGuideFromParams(params)
if (!guide) {
return {}
}
const url = process.env.NEXT_PUBLIC_APP_URL
const ogUrl = new URL(`${url}/api/og`)
ogUrl.searchParams.set("heading", guide.title)
ogUrl.searchParams.set("type", "Guide")
ogUrl.searchParams.set("mode", "dark")
return {
title: guide.title,
description: guide.description,
openGraph: {
title: guide.title,
description: guide.description,
type: "article",
url: absoluteUrl(guide.slug),
images: [
{
url: ogUrl.toString(),
width: 1200,
height: 630,
alt: guide.title,
},
],
},
twitter: {
card: "summary_large_image",
title: guide.title,
description: guide.description,
images: [ogUrl.toString()],
},
}
}
export async function generateStaticParams(): Promise<
GuidePageProps["params"][]
> {
return allGuides.map((guide) => ({
slug: guide.slugAsParams.split("/"),
}))
}
export default async function GuidePage({ params }: GuidePageProps) {
const guide = await getGuideFromParams(params)
if (!guide) {
notFound()
}
const toc = await getTableOfContents(guide.body.raw)
return (
<main className="relative py-6 lg:grid lg:grid-cols-[1fr_300px] lg:gap-10 lg:py-10 xl:gap-20">
<div>
<DocsPageHeader heading={guide.title} text={guide.description} />
<Mdx code={guide.body.code} />
<hr className="my-4" />
<div className="flex justify-center py-6 lg:py-10">
<Link
href="/guides"
className={cn(buttonVariants({ variant: "ghost" }))}
>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
See all guides
</Link>
</div>
</div>
<div className="hidden text-sm lg:block">
<div className="sticky top-16 -mt-10 max-h-[calc(var(--vh)-4rem)] overflow-y-auto pt-10">
<DashboardTableOfContents toc={toc} />
</div>
</div>
</main>
)
}

View file

@ -1,7 +0,0 @@
interface GuidesLayoutProps {
children: React.ReactNode
}
export default function GuidesLayout({ children }: GuidesLayoutProps) {
return <div className="mx-auto max-w-5xl">{children}</div>
}

View file

@ -1,65 +0,0 @@
import Link from "next/link"
import { allGuides } from "contentlayer/generated"
import { compareDesc } from "date-fns"
import { formatDate } from "@/lib/utils"
import { DocsPageHeader } from "@/components/page-header"
export const metadata = {
title: "Guides",
description:
"This section includes end-to-end guides for developing Next.js 13 apps.",
}
export default function GuidesPage() {
const guides = allGuides
.filter((guide) => guide.published)
.sort((a, b) => {
return compareDesc(new Date(a.date), new Date(b.date))
})
return (
<div className="py-6 lg:py-10">
<DocsPageHeader
heading="Guides"
text="This section includes end-to-end guides for developing Next.js 13 apps."
/>
{guides?.length ? (
<div className="grid gap-4 md:grid-cols-2 md:gap-6">
{guides.map((guide) => (
<article
key={guide._id}
className="group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg"
>
{guide.featured && (
<span className="absolute right-4 top-4 rounded-full px-3 py-1 text-xs font-medium">
Featured
</span>
)}
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2">
<h2 className="text-xl font-medium tracking-tight">
{guide.title}
</h2>
{guide.description && (
<p className="text-muted-foreground">{guide.description}</p>
)}
</div>
{guide.date && (
<p className="text-sm text-muted-foreground">
{formatDate(guide.date)}
</p>
)}
</div>
<Link href={guide.slug} className="absolute inset-0">
<span className="sr-only">View</span>
</Link>
</article>
))}
</div>
) : (
<p>No guides published.</p>
)}
</div>
)
}

View file

@ -1,44 +0,0 @@
import Link from "next/link"
import { docsConfig } from "@/config/docs"
import { siteConfig } from "@/config/site"
import { Icons } from "@/components/icons"
import { MainNav } from "@/components/main-nav"
import { DocsSearch } from "@/components/search"
import { DocsSidebarNav } from "@/components/sidebar-nav"
import { SiteFooter } from "@/components/site-footer"
interface DocsLayoutProps {
children: React.ReactNode
}
export default function DocsLayout({ children }: DocsLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
<header className="sticky top-0 z-40 w-full border-b bg-background">
<div className="container flex h-16 items-center space-x-4 sm:justify-between sm:space-x-0">
<MainNav items={docsConfig.mainNav}>
<DocsSidebarNav items={docsConfig.sidebarNav} />
</MainNav>
<div className="flex flex-1 items-center space-x-4 sm:justify-end">
<div className="flex-1 sm:grow-0">
<DocsSearch />
</div>
<nav className="flex space-x-4">
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
>
<Icons.gitHub className="h-7 w-7" />
<span className="sr-only">GitHub</span>
</Link>
</nav>
</div>
</div>
</header>
<div className="container flex-1">{children}</div>
<SiteFooter className="border-t" />
</div>
)
}

View file

@ -1,18 +0,0 @@
import { Skeleton } from "@/components/ui/skeleton"
export default function Loading() {
return (
<div className="grid w-full gap-10">
<div className="flex w-full items-center justify-between">
<Skeleton className="h-[38px] w-[90px]" />
<Skeleton className="h-[38px] w-[80px]" />
</div>
<div className="mx-auto w-[800px] space-y-6">
<Skeleton className="h-[50px] w-full" />
<Skeleton className="h-[20px] w-2/3" />
<Skeleton className="h-[20px] w-full" />
<Skeleton className="h-[20px] w-full" />
</div>
</div>
)
}

View file

@ -1,19 +0,0 @@
import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
import { EmptyPlaceholder } from "@/components/empty-placeholder"
export default function NotFound() {
return (
<EmptyPlaceholder className="mx-auto max-w-[800px]">
<EmptyPlaceholder.Icon name="warning" />
<EmptyPlaceholder.Title>Uh oh! Not Found</EmptyPlaceholder.Title>
<EmptyPlaceholder.Description>
This post cound not be found. Please try again.
</EmptyPlaceholder.Description>
<Link href="/dashboard" className={buttonVariants({ variant: "ghost" })}>
Go to Dashboard
</Link>
</EmptyPlaceholder>
)
}

View file

@ -1,45 +0,0 @@
import { notFound, redirect } from "next/navigation"
import { Post, User } from "@prisma/client"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { getCurrentUser } from "@/lib/session"
import { Editor } from "@/components/editor"
async function getPostForUser(postId: Post["id"], userId: User["id"]) {
return await db.post.findFirst({
where: {
id: postId,
authorId: userId,
},
})
}
interface EditorPageProps {
params: { postId: string }
}
export default async function EditorPage({ params }: EditorPageProps) {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
const post = await getPostForUser(params.postId, user.id)
if (!post) {
notFound()
}
return (
<Editor
post={{
id: post.id,
title: post.title,
content: post.content,
published: post.published,
}}
/>
)
}

View file

@ -1,11 +0,0 @@
interface EditorProps {
children?: React.ReactNode
}
export default function EditorLayout({ children }: EditorProps) {
return (
<div className="container mx-auto grid items-start gap-10 py-8">
{children}
</div>
)
}

View file

@ -1,98 +0,0 @@
import { notFound } from "next/navigation"
import { allPages } from "contentlayer/generated"
import { Mdx } from "@/components/mdx-components"
import "@/styles/mdx.css"
import { Metadata } from "next"
import { siteConfig } from "@/config/site"
import { absoluteUrl } from "@/lib/utils"
interface PageProps {
params: {
slug: string[]
}
}
async function getPageFromParams(params) {
const slug = params?.slug?.join("/")
const page = allPages.find((page) => page.slugAsParams === slug)
if (!page) {
null
}
return page
}
export async function generateMetadata({
params,
}: PageProps): Promise<Metadata> {
const page = await getPageFromParams(params)
if (!page) {
return {}
}
const url = process.env.NEXT_PUBLIC_APP_URL
const ogUrl = new URL(`${url}/api/og`)
ogUrl.searchParams.set("heading", page.title)
ogUrl.searchParams.set("type", siteConfig.name)
ogUrl.searchParams.set("mode", "light")
return {
title: page.title,
description: page.description,
openGraph: {
title: page.title,
description: page.description,
type: "article",
url: absoluteUrl(page.slug),
images: [
{
url: ogUrl.toString(),
width: 1200,
height: 630,
alt: page.title,
},
],
},
twitter: {
card: "summary_large_image",
title: page.title,
description: page.description,
images: [ogUrl.toString()],
},
}
}
export async function generateStaticParams(): Promise<PageProps["params"][]> {
return allPages.map((page) => ({
slug: page.slugAsParams.split("/"),
}))
}
export default async function PagePage({ params }: PageProps) {
const page = await getPageFromParams(params)
if (!page) {
notFound()
}
return (
<article className="container max-w-3xl py-6 lg:py-12">
<div className="space-y-4">
<h1 className="inline-block font-heading text-4xl font-extrabold lg:text-5xl">
{page.title}
</h1>
{page.description && (
<p className="text-xl text-muted-foreground">{page.description}</p>
)}
</div>
<hr className="my-4" />
<Mdx code={page.body.code} />
</article>
)
}

View file

@ -1,168 +0,0 @@
import { notFound } from "next/navigation"
import { allAuthors, allPosts } from "contentlayer/generated"
import { Mdx } from "@/components/mdx-components"
import "@/styles/mdx.css"
import { Metadata } from "next"
import Image from "next/image"
import Link from "next/link"
import { absoluteUrl, cn, formatDate } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
interface PostPageProps {
params: {
slug: string[]
}
}
async function getPostFromParams(params) {
const slug = params?.slug?.join("/")
const post = allPosts.find((post) => post.slugAsParams === slug)
if (!post) {
null
}
return post
}
export async function generateMetadata({
params,
}: PostPageProps): Promise<Metadata> {
const post = await getPostFromParams(params)
if (!post) {
return {}
}
const url = process.env.NEXT_PUBLIC_APP_URL
const ogUrl = new URL(`${url}/api/og`)
ogUrl.searchParams.set("heading", post.title)
ogUrl.searchParams.set("type", "Blog Post")
ogUrl.searchParams.set("mode", "dark")
return {
title: post.title,
description: post.description,
authors: post.authors.map((author) => ({
name: author,
})),
openGraph: {
title: post.title,
description: post.description,
type: "article",
url: absoluteUrl(post.slug),
images: [
{
url: ogUrl.toString(),
width: 1200,
height: 630,
alt: post.title,
},
],
},
twitter: {
card: "summary_large_image",
title: post.title,
description: post.description,
images: [ogUrl.toString()],
},
}
}
export async function generateStaticParams(): Promise<
PostPageProps["params"][]
> {
return allPosts.map((post) => ({
slug: post.slugAsParams.split("/"),
}))
}
export default async function PostPage({ params }: PostPageProps) {
const post = await getPostFromParams(params)
if (!post) {
notFound()
}
const authors = post.authors.map((author) =>
allAuthors.find(({ slug }) => slug === `/authors/${author}`)
)
return (
<article className="container relative max-w-3xl py-6 lg:py-10">
<Link
href="/blog"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute left-[-200px] top-14 hidden xl:inline-flex"
)}
>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
See all posts
</Link>
<div>
{post.date && (
<time
dateTime={post.date}
className="block text-sm text-muted-foreground"
>
Published on {formatDate(post.date)}
</time>
)}
<h1 className="mt-2 inline-block font-heading text-4xl font-extrabold leading-tight lg:text-5xl">
{post.title}
</h1>
{authors?.length ? (
<div className="mt-4 flex space-x-4">
{authors.map((author) =>
author ? (
<Link
key={author._id}
href={`https://twitter.com/${author.twitter}`}
className="flex items-center space-x-2 text-sm"
>
<Image
src={author.avatar}
alt={author.title}
width={42}
height={42}
className="rounded-full bg-white"
/>
<div className="flex-1 text-left leading-tight">
<p className="font-medium">{author.title}</p>
<p className="text-[12px] text-muted-foreground">
@{author.twitter}
</p>
</div>
</Link>
) : null
)}
</div>
) : null}
</div>
{post.image && (
<Image
src={post.image}
alt={post.title}
width={720}
height={405}
className="my-8 rounded-md border bg-muted transition-colors"
priority
/>
)}
<Mdx code={post.body.code} />
<hr className="mt-12" />
<div className="flex justify-center py-6 lg:py-10">
<Link href="/blog" className={cn(buttonVariants({ variant: "ghost" }))}>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
See all posts
</Link>
</div>
</article>
)
}

View file

@ -1,69 +0,0 @@
import Image from "next/image"
import Link from "next/link"
import { allPosts } from "contentlayer/generated"
import { compareDesc } from "date-fns"
import { formatDate } from "@/lib/utils"
export const metadata = {
title: "Blog",
}
export default async function BlogPage() {
const posts = allPosts
.filter((post) => post.published)
.sort((a, b) => {
return compareDesc(new Date(a.date), new Date(b.date))
})
return (
<div className="container max-w-4xl py-6 lg:py-10">
<div className="flex flex-col items-start gap-4 md:flex-row md:justify-between md:gap-8">
<div className="flex-1 space-y-4">
<h1 className="inline-block font-heading text-4xl font-extrabold tracking-tight lg:text-5xl">
Blog
</h1>
<p className="text-xl text-muted-foreground">
A blog built using Contentlayer. Posts are written in MDX.
</p>
</div>
</div>
<hr className="my-8" />
{posts?.length ? (
<div className="grid gap-10 sm:grid-cols-2">
{posts.map((post, index) => (
<article
key={post._id}
className="group relative flex flex-col space-y-2"
>
{post.image && (
<Image
src={post.image}
alt={post.title}
width={804}
height={452}
className="rounded-md border bg-muted transition-colors"
priority={index <= 1}
/>
)}
<h2 className="text-2xl font-extrabold">{post.title}</h2>
{post.description && (
<p className="text-muted-foreground">{post.description}</p>
)}
{post.date && (
<p className="text-sm text-muted-foreground">
{formatDate(post.date)}
</p>
)}
<Link href={post.slug} className="absolute inset-0">
<span className="sr-only">View Article</span>
</Link>
</article>
))}
</div>
) : (
<p>No posts published.</p>
)}
</div>
)
}

View file

@ -1,38 +0,0 @@
import Link from "next/link"
import { marketingConfig } from "@/config/marketing"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { MainNav } from "@/components/main-nav"
import { SiteFooter } from "@/components/site-footer"
interface MarketingLayoutProps {
children: React.ReactNode
}
export default async function MarketingLayout({
children,
}: MarketingLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
<header className="container z-40 bg-background">
<div className="flex h-20 items-center justify-between py-6">
<MainNav items={marketingConfig.mainNav} />
<nav>
<Link
href="/login"
className={cn(
buttonVariants({ variant: "secondary", size: "sm" }),
"px-4"
)}
>
Login
</Link>
</nav>
</div>
</header>
<main className="flex-1">{children}</main>
<SiteFooter />
</div>
)
}

View file

@ -1,225 +0,0 @@
import Link from "next/link"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
async function getGitHubStars(): Promise<string | null> {
try {
const response = await fetch(
"https://api.github.com/repos/shadcn/taxonomy",
{
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${process.env.GITHUB_ACCESS_TOKEN}`,
},
next: {
revalidate: 60,
},
}
)
if (!response?.ok) {
return null
}
const json = await response.json()
return parseInt(json["stargazers_count"]).toLocaleString()
} catch (error) {
return null
}
}
export default async function IndexPage() {
const stars = await getGitHubStars()
return (
<>
<section className="space-y-6 pb-8 pt-6 md:pb-12 md:pt-10 lg:py-32">
<div className="container flex max-w-[64rem] flex-col items-center gap-4 text-center">
<Link
href={siteConfig.links.twitter}
className="rounded-2xl bg-muted px-4 py-1.5 text-sm font-medium"
target="_blank"
>
Follow along on Twitter
</Link>
<h1 className="font-heading text-3xl font-bold sm:text-5xl md:text-6xl lg:text-7xl">
An example app built using Next.js 13 server components.
</h1>
<p className="max-w-[42rem] leading-normal text-muted-foreground sm:text-xl sm:leading-8">
I&apos;m building a web app with Next.js 13 and open sourcing
everything. Follow along as we figure this out together.
</p>
<div className="space-x-4">
<Link href="/login" className={cn(buttonVariants({ size: "lg" }))}>
Get Started
</Link>
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className={cn(buttonVariants({ variant: "outline", size: "lg" }))}
>
GitHub
</Link>
</div>
</div>
</section>
<section
id="features"
className="container space-y-6 bg-slate-50 py-8 dark:bg-transparent md:py-12 lg:py-24"
>
<div className="mx-auto flex max-w-[58rem] flex-col items-center space-y-4 text-center">
<h2 className="font-heading text-3xl font-bold leading-[1.1] sm:text-3xl md:text-6xl">
Features
</h2>
<p className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
This project is an experiment to see how a modern app, with features
like auth, subscriptions, API routes, and static pages would work in
Next.js 13 app dir.
</p>
</div>
<div className="mx-auto grid justify-center gap-4 sm:grid-cols-2 md:max-w-[64rem] md:grid-cols-3">
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg viewBox="0 0 24 24" className="h-12 w-12 fill-current">
<path d="M11.572 0c-.176 0-.31.001-.358.007a19.76 19.76 0 0 1-.364.033C7.443.346 4.25 2.185 2.228 5.012a11.875 11.875 0 0 0-2.119 5.243c-.096.659-.108.854-.108 1.747s.012 1.089.108 1.748c.652 4.506 3.86 8.292 8.209 9.695.779.25 1.6.422 2.534.525.363.04 1.935.04 2.299 0 1.611-.178 2.977-.577 4.323-1.264.207-.106.247-.134.219-.158-.02-.013-.9-1.193-1.955-2.62l-1.919-2.592-2.404-3.558a338.739 338.739 0 0 0-2.422-3.556c-.009-.002-.018 1.579-.023 3.51-.007 3.38-.01 3.515-.052 3.595a.426.426 0 0 1-.206.214c-.075.037-.14.044-.495.044H7.81l-.108-.068a.438.438 0 0 1-.157-.171l-.05-.106.006-4.703.007-4.705.072-.092a.645.645 0 0 1 .174-.143c.096-.047.134-.051.54-.051.478 0 .558.018.682.154.035.038 1.337 1.999 2.895 4.361a10760.433 10760.433 0 0 0 4.735 7.17l1.9 2.879.096-.063a12.317 12.317 0 0 0 2.466-2.163 11.944 11.944 0 0 0 2.824-6.134c.096-.66.108-.854.108-1.748 0-.893-.012-1.088-.108-1.747-.652-4.506-3.859-8.292-8.208-9.695a12.597 12.597 0 0 0-2.499-.523A33.119 33.119 0 0 0 11.573 0zm4.069 7.217c.347 0 .408.005.486.047a.473.473 0 0 1 .237.277c.018.06.023 1.365.018 4.304l-.006 4.218-.744-1.14-.746-1.14v-3.066c0-1.982.01-3.097.023-3.15a.478.478 0 0 1 .233-.296c.096-.05.13-.054.5-.054z" />
</svg>
<div className="space-y-2">
<h3 className="font-bold">Next.js 13</h3>
<p className="text-sm text-muted-foreground">
App dir, Routing, Layouts, Loading UI and API routes.
</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg viewBox="0 0 24 24" className="h-12 w-12 fill-current">
<path d="M14.23 12.004a2.236 2.236 0 0 1-2.235 2.236 2.236 2.236 0 0 1-2.236-2.236 2.236 2.236 0 0 1 2.235-2.236 2.236 2.236 0 0 1 2.236 2.236zm2.648-10.69c-1.346 0-3.107.96-4.888 2.622-1.78-1.653-3.542-2.602-4.887-2.602-.41 0-.783.093-1.106.278-1.375.793-1.683 3.264-.973 6.365C1.98 8.917 0 10.42 0 12.004c0 1.59 1.99 3.097 5.043 4.03-.704 3.113-.39 5.588.988 6.38.32.187.69.275 1.102.275 1.345 0 3.107-.96 4.888-2.624 1.78 1.654 3.542 2.603 4.887 2.603.41 0 .783-.09 1.106-.275 1.374-.792 1.683-3.263.973-6.365C22.02 15.096 24 13.59 24 12.004c0-1.59-1.99-3.097-5.043-4.032.704-3.11.39-5.587-.988-6.38a2.167 2.167 0 0 0-1.092-.278zm-.005 1.09v.006c.225 0 .406.044.558.127.666.382.955 1.835.73 3.704-.054.46-.142.945-.25 1.44a23.476 23.476 0 0 0-3.107-.534A23.892 23.892 0 0 0 12.769 4.7c1.592-1.48 3.087-2.292 4.105-2.295zm-9.77.02c1.012 0 2.514.808 4.11 2.28-.686.72-1.37 1.537-2.02 2.442a22.73 22.73 0 0 0-3.113.538 15.02 15.02 0 0 1-.254-1.42c-.23-1.868.054-3.32.714-3.707.19-.09.4-.127.563-.132zm4.882 3.05c.455.468.91.992 1.36 1.564-.44-.02-.89-.034-1.345-.034-.46 0-.915.01-1.36.034.44-.572.895-1.096 1.345-1.565zM12 8.1c.74 0 1.477.034 2.202.093.406.582.802 1.203 1.183 1.86.372.64.71 1.29 1.018 1.946-.308.655-.646 1.31-1.013 1.95-.38.66-.773 1.288-1.18 1.87a25.64 25.64 0 0 1-4.412.005 26.64 26.64 0 0 1-1.183-1.86c-.372-.64-.71-1.29-1.018-1.946a25.17 25.17 0 0 1 1.013-1.954c.38-.66.773-1.286 1.18-1.868A25.245 25.245 0 0 1 12 8.098zm-3.635.254c-.24.377-.48.763-.704 1.16-.225.39-.435.782-.635 1.174-.265-.656-.49-1.31-.676-1.947.64-.15 1.315-.283 2.015-.386zm7.26 0c.695.103 1.365.23 2.006.387-.18.632-.405 1.282-.66 1.933a25.952 25.952 0 0 0-1.345-2.32zm3.063.675c.484.15.944.317 1.375.498 1.732.74 2.852 1.708 2.852 2.476-.005.768-1.125 1.74-2.857 2.475-.42.18-.88.342-1.355.493a23.966 23.966 0 0 0-1.1-2.98c.45-1.017.81-2.01 1.085-2.964zm-13.395.004c.278.96.645 1.957 1.1 2.98a23.142 23.142 0 0 0-1.086 2.964c-.484-.15-.944-.318-1.37-.5-1.732-.737-2.852-1.706-2.852-2.474 0-.768 1.12-1.742 2.852-2.476.42-.18.88-.342 1.356-.494zm11.678 4.28c.265.657.49 1.312.676 1.948-.64.157-1.316.29-2.016.39a25.819 25.819 0 0 0 1.341-2.338zm-9.945.02c.2.392.41.783.64 1.175.23.39.465.772.705 1.143a22.005 22.005 0 0 1-2.006-.386c.18-.63.406-1.282.66-1.933zM17.92 16.32c.112.493.2.968.254 1.423.23 1.868-.054 3.32-.714 3.708-.147.09-.338.128-.563.128-1.012 0-2.514-.807-4.11-2.28.686-.72 1.37-1.536 2.02-2.44 1.107-.118 2.154-.3 3.113-.54zm-11.83.01c.96.234 2.006.415 3.107.532.66.905 1.345 1.727 2.035 2.446-1.595 1.483-3.092 2.295-4.11 2.295a1.185 1.185 0 0 1-.553-.132c-.666-.38-.955-1.834-.73-3.703.054-.46.142-.944.25-1.438zm4.56.64c.44.02.89.034 1.345.034.46 0 .915-.01 1.36-.034-.44.572-.895 1.095-1.345 1.565-.455-.47-.91-.993-1.36-1.565z" />
</svg>
<div className="space-y-2">
<h3 className="font-bold">React 18</h3>
<p className="text-sm">
Server and Client Components. Use hook.
</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg viewBox="0 0 24 24" className="h-12 w-12 fill-current">
<path d="M0 12C0 5.373 5.373 0 12 0c4.873 0 9.067 2.904 10.947 7.077l-15.87 15.87a11.981 11.981 0 0 1-1.935-1.099L14.99 12H12l-8.485 8.485A11.962 11.962 0 0 1 0 12Zm12.004 12L24 12.004C23.998 18.628 18.628 23.998 12.004 24Z" />
</svg>
<div className="space-y-2">
<h3 className="font-bold">Database</h3>
<p className="text-sm text-muted-foreground">
ORM using Prisma and deployed on PlanetScale.
</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg viewBox="0 0 24 24" className="h-12 w-12 fill-current">
<path d="M12.001 4.8c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624C13.666 10.618 15.027 12 18.001 12c3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C16.337 6.182 14.976 4.8 12.001 4.8zm-6 7.2c-3.2 0-5.2 1.6-6 4.8 1.2-1.6 2.6-2.2 4.2-1.8.913.228 1.565.89 2.288 1.624 1.177 1.194 2.538 2.576 5.512 2.576 3.2 0 5.2-1.6 6-4.8-1.2 1.6-2.6 2.2-4.2 1.8-.913-.228-1.565-.89-2.288-1.624C10.337 13.382 8.976 12 6.001 12z" />
</svg>
<div className="space-y-2">
<h3 className="font-bold">Components</h3>
<p className="text-sm text-muted-foreground">
UI components built using Radix UI and styled with Tailwind
CSS.
</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1"
className="h-12 w-12 fill-current"
>
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<div className="space-y-2">
<h3 className="font-bold">Authentication</h3>
<p className="text-sm text-muted-foreground">
Authentication using NextAuth.js and middlewares.
</p>
</div>
</div>
</div>
<div className="relative overflow-hidden rounded-lg border bg-background p-2">
<div className="flex h-[180px] flex-col justify-between rounded-md p-6">
<svg viewBox="0 0 24 24" className="h-12 w-12 fill-current">
<path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z" />
</svg>
<div className="space-y-2">
<h3 className="font-bold">Subscriptions</h3>
<p className="text-sm text-muted-foreground">
Free and paid subscriptions using Stripe.
</p>
</div>
</div>
</div>
</div>
<div className="mx-auto text-center md:max-w-[58rem]">
<p className="leading-normal text-muted-foreground sm:text-lg sm:leading-7">
Taxonomy also includes a blog and a full-featured documentation site
built using Contentlayer and MDX.
</p>
</div>
</section>
<section id="open-source" className="container py-8 md:py-12 lg:py-24">
<div className="mx-auto flex max-w-[58rem] flex-col items-center justify-center gap-4 text-center">
<h2 className="font-heading text-3xl font-bold leading-[1.1] sm:text-3xl md:text-6xl">
Proudly Open Source
</h2>
<p className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
Taxonomy is open source and powered by open source software. <br />{" "}
The code is available on{" "}
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className="underline underline-offset-4"
>
GitHub
</Link>
.{" "}
</p>
{stars && (
<Link
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className="flex"
>
<div className="flex h-10 w-10 items-center justify-center space-x-2 rounded-md border border-muted bg-muted">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
className="h-5 w-5 text-foreground"
>
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"></path>
</svg>
</div>
<div className="flex items-center">
<div className="h-4 w-4 border-y-8 border-l-0 border-r-8 border-solid border-muted border-y-transparent"></div>
<div className="flex h-10 items-center rounded-md border border-muted bg-muted px-4 font-medium">
{stars} stars on GitHub
</div>
</div>
</Link>
)}
</div>
</section>
</>
)
}

View file

@ -1,69 +0,0 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
export const metadata = {
title: "Pricing",
}
export default function PricingPage() {
return (
<section className="container flex flex-col gap-6 py-8 md:max-w-[64rem] md:py-12 lg:py-24">
<div className="mx-auto flex w-full flex-col gap-4 md:max-w-[58rem]">
<h2 className="font-heading text-3xl font-bold leading-[1.1] sm:text-3xl md:text-6xl">
Simple, transparent pricing
</h2>
<p className="max-w-[85%] leading-normal text-muted-foreground sm:text-lg sm:leading-7">
Unlock all features including unlimited posts for your blog.
</p>
</div>
<div className="grid w-full items-start gap-10 rounded-lg border p-10 md:grid-cols-[1fr_200px]">
<div className="grid gap-6">
<h3 className="text-xl font-bold sm:text-2xl">
What&apos;s included in the PRO plan
</h3>
<ul className="grid gap-3 text-sm text-muted-foreground sm:grid-cols-2">
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Unlimited Posts
</li>
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Unlimited Users
</li>
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Custom domain
</li>
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Dashboard Analytics
</li>
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Access to Discord
</li>
<li className="flex items-center">
<Icons.check className="mr-2 h-4 w-4" /> Premium Support
</li>
</ul>
</div>
<div className="flex flex-col gap-4 text-center">
<div>
<h4 className="text-7xl font-bold">$19</h4>
<p className="text-sm font-medium text-muted-foreground">
Billed Monthly
</p>
</div>
<Link href="/login" className={cn(buttonVariants({ size: "lg" }))}>
Get Started
</Link>
</div>
</div>
<div className="mx-auto flex w-full max-w-[58rem] flex-col gap-4">
<p className="max-w-[85%] leading-normal text-muted-foreground sm:leading-7">
Taxonomy is a demo app.{" "}
<strong>You can test the upgrade and won&apos;t be charged.</strong>
</p>
</div>
</section>
)
}

View file

@ -1,148 +0,0 @@
import { ImageResponse } from "@vercel/og"
import { ogImageSchema } from "@/lib/validations/og"
export const runtime = "edge"
const interRegular = fetch(
new URL("../../../assets/fonts/Inter-Regular.ttf", import.meta.url)
).then((res) => res.arrayBuffer())
const interBold = fetch(
new URL("../../../assets/fonts/CalSans-SemiBold.ttf", import.meta.url)
).then((res) => res.arrayBuffer())
export async function GET(req: Request) {
try {
const fontRegular = await interRegular
const fontBold = await interBold
const url = new URL(req.url)
const values = ogImageSchema.parse(Object.fromEntries(url.searchParams))
const heading =
values.heading.length > 140
? `${values.heading.substring(0, 140)}...`
: values.heading
const { mode } = values
const paint = mode === "dark" ? "#fff" : "#000"
const fontSize = heading.length > 100 ? "70px" : "100px"
return new ImageResponse(
(
<div
tw="flex relative flex-col p-12 w-full h-full items-start"
style={{
color: paint,
background:
mode === "dark"
? "linear-gradient(90deg, #000 0%, #111 100%)"
: "white",
}}
>
<svg width="212" height="50" viewBox="0 0 212 50" fill="none">
<g clip-path="url(#a)" fill={paint}>
<path d="M99.715 9.784h26.128v4.823h-10.365v25.37h-5.182v-25.37h-10.58V9.784ZM56.746 9.784v4.823H35.803v7.757h16.842v4.823H35.803v7.967h20.943v4.823H30.62v-25.37h-.002V9.784h26.128ZM69.792 9.797H63.01l24.292 30.192h6.801L81.956 24.903 94.084 9.82l-6.782.01-8.742 10.856-8.768-10.89ZM76.751 31.363l-3.396-4.222L62.99 40.012h6.802l6.96-8.649Z" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M30.802 39.977 6.478 9.77H0v30.193h5.182V16.225l19.11 23.752h6.51Z"
/>
<path d="M127.008 39.792c-.38 0-.703-.131-.973-.394a1.267 1.267 0 0 1-.4-.959c-.004-.366.13-.681.4-.944.27-.263.593-.395.973-.395.365 0 .684.132.955.395.274.263.41.578.414.944-.004.25-.067.478-.193.682-.13.208-.295.37-.502.488a1.298 1.298 0 0 1-.674.183ZM135.853 27.073h2.296v8.847c-.003.814-.179 1.51-.523 2.094a3.477 3.477 0 0 1-1.447 1.346c-.614.311-1.334.47-2.152.47-.748 0-1.419-.135-2.016-.398a3.239 3.239 0 0 1-1.418-1.176c-.352-.519-.524-1.166-.524-1.941h2.301c.003.339.08.633.228.879.147.245.351.432.611.564.263.131.565.197.905.197.369 0 .685-.076.942-.232.256-.152.453-.38.59-.685.133-.301.203-.675.207-1.118v-8.847ZM147.598 30.533a1.67 1.67 0 0 0-.73-1.252c-.432-.301-.99-.45-1.675-.45-.481 0-.895.073-1.239.214-.345.146-.611.34-.794.585a1.423 1.423 0 0 0-.281.84c0 .264.063.492.186.683.123.193.288.356.502.487.211.135.446.246.703.336.259.09.519.166.779.228l1.197.294c.481.111.949.26 1.394.45.446.187.85.426 1.205.713.354.287.635.633.842 1.038.208.405.313.879.313 1.426 0 .737-.19 1.384-.573 1.944-.382.557-.933.993-1.657 1.308-.72.312-1.59.47-2.616.47-.99 0-1.854-.151-2.581-.456-.73-.301-1.299-.744-1.71-1.325-.41-.582-.632-1.29-.663-2.125h2.275c.032.436.172.8.411 1.094.242.29.558.505.945.65.389.142.825.215 1.306.215.502 0 .944-.076 1.327-.225.379-.149.678-.357.892-.626.218-.267.327-.582.33-.942-.003-.328-.102-.602-.292-.816-.193-.215-.459-.395-.8-.54a8.25 8.25 0 0 0-1.201-.39l-1.454-.368c-1.05-.266-1.882-.671-2.489-1.214-.611-.543-.913-1.263-.913-2.166 0-.74.203-1.391.615-1.948.407-.557.965-.99 1.671-1.298.709-.311 1.51-.463 2.401-.463.906 0 1.7.152 2.385.463.684.308 1.222.737 1.611 1.284a3.25 3.25 0 0 1 .605 1.882h-2.227Z" />
</g>
<path
d="M181.335 14.636V35h-5.528V19.727h-.119l-4.455 2.665v-4.693l5.011-3.063h5.091Zm12.136 20.642c-1.604 0-3.029-.275-4.276-.825-1.239-.557-2.214-1.322-2.923-2.297-.709-.974-1.067-2.094-1.074-3.36h5.568c.007.39.126.742.358 1.053.239.305.564.544.975.716.411.173.881.259 1.412.259.51 0 .961-.09 1.352-.269.391-.185.696-.44.915-.765.218-.325.325-.696.318-1.114a1.637 1.637 0 0 0-.378-1.094c-.252-.318-.606-.566-1.064-.745-.457-.18-.984-.269-1.581-.269h-2.068V22.75h2.068c.55 0 1.034-.09 1.452-.268.424-.18.752-.428.984-.746.239-.318.355-.683.348-1.094a1.824 1.824 0 0 0-.288-1.054 2.012 2.012 0 0 0-.835-.716c-.352-.172-.759-.258-1.223-.258-.504 0-.955.09-1.353.268a2.25 2.25 0 0 0-.924.746 1.891 1.891 0 0 0-.348 1.094h-5.29c.007-1.247.348-2.347 1.024-3.302.683-.954 1.617-1.703 2.804-2.247 1.187-.543 2.549-.815 4.087-.815 1.504 0 2.833.255 3.987.766 1.16.51 2.065 1.213 2.714 2.107.657.889.981 1.906.975 3.053.013 1.14-.378 2.075-1.174 2.804-.788.73-1.789 1.16-3.002 1.293v.159c1.644.179 2.88.683 3.708 1.511.829.822 1.237 1.856 1.223 3.102.007 1.194-.351 2.25-1.073 3.172-.716.922-1.714 1.644-2.993 2.168-1.273.524-2.741.785-4.405.785Z"
fill={paint}
/>
<rect
x="163"
y="1"
width="48"
height="48"
rx="9"
stroke={paint}
stroke-width="2"
/>
<defs>
<clipPath id="a">
<path fill={paint} d="M0 9.771h150v30.457H0z" />
</clipPath>
</defs>
</svg>
<div tw="flex flex-col flex-1 py-10">
<div
tw="flex text-xl uppercase font-bold tracking-tight"
style={{ fontFamily: "Inter", fontWeight: "normal" }}
>
{values.type}
</div>
<div
tw="flex leading-[1.1] text-[80px] font-bold"
style={{
fontFamily: "Cal Sans",
fontWeight: "bold",
marginLeft: "-3px",
fontSize,
}}
>
{heading}
</div>
</div>
<div tw="flex items-center w-full justify-between">
<div
tw="flex text-xl"
style={{ fontFamily: "Inter", fontWeight: "normal" }}
>
tx.shadcn.com
</div>
<div
tw="flex items-center text-xl"
style={{ fontFamily: "Inter", fontWeight: "normal" }}
>
<svg width="32" height="32" viewBox="0 0 48 48" fill="none">
<path
d="M30 44v-8a9.6 9.6 0 0 0-2-7c6 0 12-4 12-11 .16-2.5-.54-4.96-2-7 .56-2.3.56-4.7 0-7 0 0-2 0-6 3-5.28-1-10.72-1-16 0-4-3-6-3-6-3-.6 2.3-.6 4.7 0 7a10.806 10.806 0 0 0-2 7c0 7 6 11 12 11a9.43 9.43 0 0 0-1.7 3.3c-.34 1.2-.44 2.46-.3 3.7v8"
stroke={paint}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
<path
d="M18 36c-9.02 4-10-4-14-4"
stroke={paint}
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
/>
</svg>
<div tw="flex ml-2">github.com/shadcn/taxonomy</div>
</div>
</div>
</div>
),
{
width: 1200,
height: 630,
fonts: [
{
name: "Inter",
data: fontRegular,
weight: 400,
style: "normal",
},
{
name: "Cal Sans",
data: fontBold,
weight: 700,
style: "normal",
},
],
}
)
} catch (error) {
return new Response(`Failed to generate image`, {
status: 500,
})
}
}

View file

@ -1,93 +0,0 @@
import { getServerSession } from "next-auth"
import * as z from "zod"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { postPatchSchema } from "@/lib/validations/post"
const routeContextSchema = z.object({
params: z.object({
postId: z.string(),
}),
})
export async function DELETE(
req: Request,
context: z.infer<typeof routeContextSchema>
) {
try {
// Validate the route params.
const { params } = routeContextSchema.parse(context)
// Check if the user has access to this post.
if (!(await verifyCurrentUserHasAccessToPost(params.postId))) {
return new Response(null, { status: 403 })
}
// Delete the post.
await db.post.delete({
where: {
id: params.postId as string,
},
})
return new Response(null, { status: 204 })
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify(error.issues), { status: 422 })
}
return new Response(null, { status: 500 })
}
}
export async function PATCH(
req: Request,
context: z.infer<typeof routeContextSchema>
) {
try {
// Validate route params.
const { params } = routeContextSchema.parse(context)
// Check if the user has access to this post.
if (!(await verifyCurrentUserHasAccessToPost(params.postId))) {
return new Response(null, { status: 403 })
}
// Get the request body and validate it.
const json = await req.json()
const body = postPatchSchema.parse(json)
// Update the post.
// TODO: Implement sanitization for content.
await db.post.update({
where: {
id: params.postId,
},
data: {
title: body.title,
content: body.content,
},
})
return new Response(null, { status: 200 })
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify(error.issues), { status: 422 })
}
return new Response(null, { status: 500 })
}
}
async function verifyCurrentUserHasAccessToPost(postId: string) {
const session = await getServerSession(authOptions)
const count = await db.post.count({
where: {
id: postId,
authorId: session?.user.id,
},
})
return count > 0
}

View file

@ -1,92 +0,0 @@
import { getServerSession } from "next-auth/next"
import * as z from "zod"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { RequiresProPlanError } from "@/lib/exceptions"
import { getUserSubscriptionPlan } from "@/lib/subscription"
const postCreateSchema = z.object({
title: z.string(),
content: z.string().optional(),
})
export async function GET() {
try {
const session = await getServerSession(authOptions)
if (!session) {
return new Response("Unauthorized", { status: 403 })
}
const { user } = session
const posts = await db.post.findMany({
select: {
id: true,
title: true,
published: true,
createdAt: true,
},
where: {
authorId: user.id,
},
})
return new Response(JSON.stringify(posts))
} catch (error) {
return new Response(null, { status: 500 })
}
}
export async function POST(req: Request) {
try {
const session = await getServerSession(authOptions)
if (!session) {
return new Response("Unauthorized", { status: 403 })
}
const { user } = session
const subscriptionPlan = await getUserSubscriptionPlan(user.id)
// If user is on a free plan.
// Check if user has reached limit of 3 posts.
if (!subscriptionPlan?.isPro) {
const count = await db.post.count({
where: {
authorId: user.id,
},
})
if (count >= 3) {
throw new RequiresProPlanError()
}
}
const json = await req.json()
const body = postCreateSchema.parse(json)
const post = await db.post.create({
data: {
title: body.title,
content: body.content,
authorId: session.user.id,
},
select: {
id: true,
},
})
return new Response(JSON.stringify(post))
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify(error.issues), { status: 422 })
}
if (error instanceof RequiresProPlanError) {
return new Response("Requires Pro Plan", { status: 402 })
}
return new Response(null, { status: 500 })
}
}

View file

@ -1,50 +0,0 @@
import { getServerSession } from "next-auth/next"
import { z } from "zod"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { userNameSchema } from "@/lib/validations/user"
const routeContextSchema = z.object({
params: z.object({
userId: z.string(),
}),
})
export async function PATCH(
req: Request,
context: z.infer<typeof routeContextSchema>
) {
try {
// Validate the route context.
const { params } = routeContextSchema.parse(context)
// Ensure user is authentication and has access to this user.
const session = await getServerSession(authOptions)
if (!session?.user || params.userId !== session?.user.id) {
return new Response(null, { status: 403 })
}
// Get the request body and validate it.
const body = await req.json()
const payload = userNameSchema.parse(body)
// Update the user.
await db.user.update({
where: {
id: session.user.id,
},
data: {
name: payload.name,
},
})
return new Response(null, { status: 200 })
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify(error.issues), { status: 422 })
}
return new Response(null, { status: 500 })
}
}

View file

@ -1,61 +0,0 @@
import { getServerSession } from "next-auth/next"
import { z } from "zod"
import { proPlan } from "@/config/subscriptions"
import { authOptions } from "@/lib/auth"
import { stripe } from "@/lib/stripe"
import { getUserSubscriptionPlan } from "@/lib/subscription"
import { absoluteUrl } from "@/lib/utils"
const billingUrl = absoluteUrl("/dashboard/billing")
export async function GET(req: Request) {
try {
const session = await getServerSession(authOptions)
if (!session?.user || !session?.user.email) {
return new Response(null, { status: 403 })
}
const subscriptionPlan = await getUserSubscriptionPlan(session.user.id)
// The user is on the pro plan.
// Create a portal session to manage subscription.
if (subscriptionPlan.isPro && subscriptionPlan.stripeCustomerId) {
const stripeSession = await stripe.billingPortal.sessions.create({
customer: subscriptionPlan.stripeCustomerId,
return_url: billingUrl,
})
return new Response(JSON.stringify({ url: stripeSession.url }))
}
// The user is on the free plan.
// Create a checkout session to upgrade.
const stripeSession = await stripe.checkout.sessions.create({
success_url: billingUrl,
cancel_url: billingUrl,
payment_method_types: ["card"],
mode: "subscription",
billing_address_collection: "auto",
customer_email: session.user.email,
line_items: [
{
price: proPlan.stripePriceId,
quantity: 1,
},
],
metadata: {
userId: session.user.id,
},
})
return new Response(JSON.stringify({ url: stripeSession.url }))
} catch (error) {
if (error instanceof z.ZodError) {
return new Response(JSON.stringify(error.issues), { status: 422 })
}
return new Response(null, { status: 500 })
}
}

View file

@ -1,70 +0,0 @@
import { headers } from "next/headers"
import Stripe from "stripe"
import { db } from "@/lib/db"
import { stripe } from "@/lib/stripe"
export async function POST(req: Request) {
const body = await req.text()
const signature = headers().get("Stripe-Signature") as string
let event: Stripe.Event
try {
event = stripe.webhooks.constructEvent(
body,
signature,
process.env.STRIPE_WEBHOOK_SECRET || ""
)
} catch (error) {
return new Response(`Webhook Error: ${error.message}`, { status: 400 })
}
const session = event.data.object as Stripe.Checkout.Session
if (event.type === "checkout.session.completed") {
// Retrieve the subscription details from Stripe.
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
)
// Update the user stripe into in our database.
// Since this is the initial subscription, we need to update
// the subscription id and customer id.
await db.user.update({
where: {
id: session?.metadata?.userId,
},
data: {
stripeSubscriptionId: subscription.id,
stripeCustomerId: subscription.customer as string,
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
})
}
if (event.type === "invoice.payment_succeeded") {
// Retrieve the subscription details from Stripe.
const subscription = await stripe.subscriptions.retrieve(
session.subscription as string
)
// Update the price id and set the new period end.
await db.user.update({
where: {
stripeSubscriptionId: subscription.id,
},
data: {
stripePriceId: subscription.items.data[0].price.id,
stripeCurrentPeriodEnd: new Date(
subscription.current_period_end * 1000
),
},
})
}
return new Response(null, { status: 200 })
}

View file

@ -1,94 +0,0 @@
import { Inter as FontSans } from "next/font/google"
import localFont from "next/font/local"
import "@/styles/globals.css"
import { siteConfig } from "@/config/site"
import { absoluteUrl, cn } from "@/lib/utils"
import { Toaster } from "@/components/ui/toaster"
import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { ThemeProvider } from "@/components/theme-provider"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-sans",
})
// Font files can be colocated inside of `pages`
const fontHeading = localFont({
src: "../assets/fonts/CalSans-SemiBold.woff2",
variable: "--font-heading",
})
interface RootLayoutProps {
children: React.ReactNode
}
export const metadata = {
title: {
default: siteConfig.name,
template: `%s | ${siteConfig.name}`,
},
description: siteConfig.description,
keywords: [
"Next.js",
"React",
"Tailwind CSS",
"Server Components",
"Radix UI",
],
authors: [
{
name: "shadcn",
url: "https://shadcn.com",
},
],
creator: "shadcn",
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "white" },
{ media: "(prefers-color-scheme: dark)", color: "black" },
],
openGraph: {
type: "website",
locale: "en_US",
url: siteConfig.url,
title: siteConfig.name,
description: siteConfig.description,
siteName: siteConfig.name,
},
twitter: {
card: "summary_large_image",
title: siteConfig.name,
description: siteConfig.description,
images: [`${siteConfig.url}/og.jpg`],
creator: "@shadcn",
},
icons: {
icon: "/favicon.ico",
shortcut: "/favicon-16x16.png",
apple: "/apple-touch-icon.png",
},
manifest: `${siteConfig.url}/site.webmanifest`,
}
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html lang="en" suppressHydrationWarning>
<head />
<body
className={cn(
"min-h-screen bg-background font-sans antialiased",
fontSans.variable,
fontHeading.variable
)}
>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
{children}
<Analytics />
<Toaster />
<TailwindIndicator />
</ThemeProvider>
</body>
</html>
)
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

View file

@ -1,10 +0,0 @@
import { MetadataRoute } from "next"
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: "*",
allow: "/",
},
}
}