mirror of
https://github.com/shadcn-ui/taxonomy
synced 2026-05-24 09:48:32 +00:00
fix: remove everything
This commit is contained in:
parent
20eb7e738a
commit
a4faa8e58a
35 changed files with 0 additions and 2127 deletions
|
|
@ -1,7 +0,0 @@
|
|||
interface AuthLayoutProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export default function AuthLayout({ children }: AuthLayoutProps) {
|
||||
return <div className="min-h-screen">{children}</div>
|
||||
}
|
||||
|
|
@ -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't have an account? Sign Up
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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't have any posts yet. Start creating content.
|
||||
</EmptyPlaceholder.Description>
|
||||
<PostCreateButton variant="outline" />
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
</div>
|
||||
</DashboardShell>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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'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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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'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't be charged.</strong>
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -1,10 +0,0 @@
|
|||
import { MetadataRoute } from "next"
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue