feat: update to Next 13.3

This commit is contained in:
shadcn 2023-04-24 15:10:49 +04:00
parent 03ef292d26
commit c606420fe7
124 changed files with 4791 additions and 4091 deletions

View file

@ -15,10 +15,17 @@
},
"settings": {
"tailwindcss": {
"callees": ["cn"]
"callees": ["cn"],
"config": "tailwind.config.js"
},
"next": {
"rootDir": true
}
}
},
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"parser": "@typescript-eslint/parser"
}
]
}

View file

@ -6,23 +6,18 @@ An open source application built using the new router, server components and eve
> This app is a work in progress. I'm building this in public. You can follow the progress on Twitter [@shadcn](https://twitter.com/shadcn).
> See the roadmap below.
## Demo
![screenshot-2](https://user-images.githubusercontent.com/124599/198038921-2b16b18b-cb4d-44b1-bd1d-6419d4a8d92c.png)
## About this project
Right now, I'm using this project as an experiment to see how a modern app (with features like authentication, subscriptions, API routes, static pages for docs ...etc) would work in Next.js 13 and server components.
This project as an experiment to see how a modern app (with features like authentication, subscriptions, API routes, static pages for docs ...etc) would work in Next.js 13 and server components.
I'll be posting updates and issues here.
**This is not a starter template.**
A few people have asked me to turn this into a starter. I think we could do that once the new features are out of beta.
## Note on Performance
> **Warning**
> This app is using the canary releases for Next.js 13 and React 18. The new router and app dir is still in beta and not production-ready.
> NextAuth.js, which is used for authentication, is also not fully supported in Next.js 13 and RSC.
> This app is using the unstable releases for Next.js 13 and React 18. The new router and app dir is still in beta and not production-ready.
> **Expect some performance hits when testing the dashboard**.
> If you see something broken, you can ping me [@shadcn](https://twitter.com/shadcn).
@ -32,6 +27,8 @@ A few people have asked me to turn this into a starter. I think we could do that
- Routing, Layouts, Nested Layouts and Layout Groups
- Data Fetching, Caching and Mutation
- Loading UI
- Route handlers
- Metadata files
- Server and Client Components
- API Routes and Middlewares
- Authentication using **NextAuth.js**
@ -51,8 +48,7 @@ A few people have asked me to turn this into a starter. I think we could do that
- [x] ~Subscriptions using Stripe~
- [x] ~Responsive styles~
- [x] ~Add OG image for blog using @vercel/og~
- [ ] Add tests
- [ ] Dark mode
- [x] Dark mode
## Known Issues
@ -61,6 +57,7 @@ A list of things not working right now:
1. ~GitHub authentication (use email)~
2. ~[Prisma: Error: ENOENT: no such file or directory, open '/var/task/.next/server/chunks/schema.prisma'](https://github.com/prisma/prisma/issues/16117)~
3. ~[Next.js 13: Client side navigation does not update head](https://github.com/vercel/next.js/issues/42414)~
4. [Cannot use opengraph-image.tsx inside catch-all routes](https://github.com/vercel/next.js/issues/48162)
## Why not tRPC, Turborepo or X?

View file

@ -2,8 +2,8 @@ import { Metadata } from "next"
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
import { UserAuthForm } from "@/components/user-auth-form"
export const metadata: Metadata = {
@ -18,7 +18,7 @@ export default function LoginPage() {
href="/"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute top-4 left-4 md:top-8 md:left-8"
"absolute left-4 top-4 md:left-8 md:top-8"
)}
>
<>
@ -32,12 +32,12 @@ export default function LoginPage() {
<h1 className="text-2xl font-semibold tracking-tight">
Welcome back
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
<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-slate-500 dark:text-slate-400">
<p className="px-8 text-center text-sm text-muted-foreground">
<Link
href="/register"
className="hover:text-brand underline underline-offset-4"

View file

@ -1,8 +1,8 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
import { UserAuthForm } from "@/components/user-auth-form"
export const metadata = {
@ -17,12 +17,12 @@ export default function RegisterPage() {
href="/login"
className={cn(
buttonVariants({ variant: "ghost" }),
"absolute top-4 right-4 md:top-8 md:right-8"
"absolute right-4 top-4 md:right-8 md:top-8"
)}
>
Login
</Link>
<div className="hidden h-full bg-slate-100 lg:block" />
<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">
@ -30,12 +30,12 @@ export default function RegisterPage() {
<h1 className="text-2xl font-semibold tracking-tight">
Create an account
</h1>
<p className="text-sm text-slate-500 dark:text-slate-400">
<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-slate-500 dark:text-slate-400">
<p className="px-8 text-center text-sm text-muted-foreground">
By clicking continue, you agree to our{" "}
<Link
href="/terms"

View file

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

View file

@ -4,10 +4,18 @@ 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"
import { Card } from "@/components/ui/card"
export const metadata = {
title: "Billing",
@ -38,38 +46,30 @@ export default async function BillingPage() {
heading="Billing"
text="Manage billing and your subscription plan."
/>
<div className="grid gap-10">
<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,
}}
/>
<Card>
<Card.Header>
<Card.Title>Note</Card.Title>
</Card.Header>
<Card.Content className="space-y-4 pb-6 text-sm">
<p>
Taxonomy app is a demo app using a Stripe test environment.{" "}
<strong>
You can test the upgrade and won&apos;t be charged.
</strong>
</p>
<p>
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>
.
</p>
</Card.Content>
</Card>
</div>
</DashboardShell>
)

View file

@ -4,6 +4,7 @@ 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 {
@ -20,9 +21,9 @@ export default async function DashboardLayout({
}
return (
<div className="mx-auto flex flex-col space-y-6">
<header className="container sticky top-0 z-40 bg-white">
<div className="flex h-16 items-center justify-between border-b border-b-slate-200 py-4">
<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={{
@ -33,7 +34,7 @@ export default async function DashboardLayout({
/>
</div>
</header>
<div className="container grid gap-12 md:grid-cols-[200px_1fr]">
<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>
@ -41,6 +42,7 @@ export default async function DashboardLayout({
{children}
</main>
</div>
<SiteFooter className="border-t" />
</div>
)
}

View file

@ -9,7 +9,7 @@ export default function DashboardLoading() {
<DashboardHeader heading="Posts" text="Create and manage posts.">
<PostCreateButton />
</DashboardHeader>
<div className="divide-y divide-neutral-200 rounded-md border border-slate-200">
<div className="divide-border-200 divide-y rounded-md border">
<PostItem.Skeleton />
<PostItem.Skeleton />
<PostItem.Skeleton />

View file

@ -1,26 +1,28 @@
import { cache } from "react"
import { redirect } from "next/navigation"
import { User } from "@prisma/client"
import { authOptions } from "@/lib/auth"
import { db } from "@/lib/db"
import { getCurrentUser } from "@/lib/session"
import { cn } from "@/lib/utils"
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"
import { buttonVariants } from "@/components/ui/button"
export const metadata = {
title: "Dashboard",
}
const getPostsForUser = cache(async (userId: User["id"]) => {
return await db.post.findMany({
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
const posts = await db.post.findMany({
where: {
authorId: userId,
authorId: user.id,
},
select: {
id: true,
@ -32,16 +34,6 @@ const getPostsForUser = cache(async (userId: User["id"]) => {
updatedAt: "desc",
},
})
})
export default async function DashboardPage() {
const user = await getCurrentUser()
if (!user) {
redirect(authOptions?.pages?.signIn || "/login")
}
const posts = await getPostsForUser(user.id)
return (
<DashboardShell>
@ -50,7 +42,7 @@ export default async function DashboardPage() {
</DashboardHeader>
<div>
{posts?.length ? (
<div className="divide-y divide-neutral-200 rounded-md border border-slate-200">
<div className="divide-y divide-border rounded-md border">
{posts.map((post) => (
<PostItem key={post.id} post={post} />
))}
@ -62,12 +54,7 @@ export default async function DashboardPage() {
<EmptyPlaceholder.Description>
You don&apos;t have any posts yet. Start creating content.
</EmptyPlaceholder.Description>
<PostCreateButton
className={cn(
buttonVariants({ variant: "outline" }),
"text-slate-900"
)}
/>
<PostCreateButton variant="outline" />
</EmptyPlaceholder>
)}
</div>

View file

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

View file

@ -2,10 +2,11 @@ import { notFound } from "next/navigation"
import { allDocs } from "contentlayer/generated"
import { getTableOfContents } from "@/lib/toc"
import { Mdx } from "@/components/mdx"
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"
@ -92,7 +93,7 @@ export default async function DocPage({ params }: DocPageProps) {
<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 border-slate-200 md:my-6" />
<hr className="my-4 md:my-6" />
<DocsPager doc={doc} />
</div>
<div className="hidden text-sm xl:block">

View file

@ -8,7 +8,7 @@ interface DocsLayoutProps {
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 border-r-slate-100 py-6 pr-2 md:sticky md:block lg:py-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}

View file

@ -4,13 +4,15 @@ import { allGuides } from "contentlayer/generated"
import { getTableOfContents } from "@/lib/toc"
import { Icons } from "@/components/icons"
import { Mdx } from "@/components/mdx"
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 } from "@/lib/utils"
import { absoluteUrl, cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
interface GuidePageProps {
params: {
@ -93,11 +95,11 @@ export default async function GuidePage({ params }: GuidePageProps) {
<div>
<DocsPageHeader heading={guide.title} text={guide.description} />
<Mdx code={guide.body.code} />
<hr className="my-4 border-slate-200" />
<hr className="my-4" />
<div className="flex justify-center py-6 lg:py-10">
<Link
href="/guides"
className="mb-4 inline-flex items-center justify-center text-sm font-medium text-slate-600 hover:text-slate-900"
className={cn(buttonVariants({ variant: "ghost" }))}
>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
See all guides

View file

@ -29,24 +29,24 @@ export default function GuidesPage() {
{guides.map((guide) => (
<article
key={guide._id}
className="group relative rounded-lg border border-slate-200 bg-white p-6 shadow-md transition-shadow hover:shadow-lg"
className="group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg"
>
{guide.featured && (
<span className="absolute top-4 right-4 rounded-full bg-slate-100 px-3 py-1 text-xs font-medium">
<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 text-slate-900">
<h2 className="text-xl font-medium tracking-tight">
{guide.title}
</h2>
{guide.description && (
<p className="text-slate-700">{guide.description}</p>
<p className="text-muted-foreground">{guide.description}</p>
)}
</div>
{guide.date && (
<p className="text-sm text-slate-600">
<p className="text-sm text-muted-foreground">
{formatDate(guide.date)}
</p>
)}

View file

@ -15,7 +15,7 @@ interface DocsLayoutProps {
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 border-b-slate-200 bg-white">
<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} />
@ -38,7 +38,7 @@ export default function DocsLayout({ children }: DocsLayoutProps) {
</div>
</header>
<div className="container flex-1">{children}</div>
<SiteFooter />
<SiteFooter className="border-t" />
</div>
)
}

View file

@ -7,7 +7,7 @@ export default function Loading() {
<Skeleton className="h-[38px] w-[90px]" />
<Skeleton className="h-[38px] w-[80px]" />
</div>
<div className="prose prose-stone mx-auto w-[800px] space-y-6">
<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" />

View file

@ -1,5 +1,6 @@
import Link from "next/link"
import { buttonVariants } from "@/components/ui/button"
import { EmptyPlaceholder } from "@/components/empty-placeholder"
export default function NotFound() {
@ -10,10 +11,7 @@ export default function NotFound() {
<EmptyPlaceholder.Description>
This post cound not be found. Please try again.
</EmptyPlaceholder.Description>
<Link
href="/dashboard"
className="text-brand-900 relative inline-flex h-9 items-center rounded-md border border-slate-200 bg-white px-4 py-2 text-sm font-medium hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-500 focus:ring-offset-2"
>
<Link href="/dashboard" className={buttonVariants({ variant: "ghost" })}>
Go to Dashboard
</Link>
</EmptyPlaceholder>

View file

@ -1,7 +1,8 @@
import { notFound } from "next/navigation"
import { allPages } from "contentlayer/generated"
import { Mdx } from "@/components/mdx"
import { Mdx } from "@/components/mdx-components"
import "@/styles/mdx.css"
import { Metadata } from "next"
@ -81,16 +82,16 @@ export default async function PagePage({ params }: PageProps) {
}
return (
<article className="container max-w-3xl py-6 lg:py-10">
<article className="container max-w-3xl py-6 lg:py-12">
<div className="space-y-4">
<h1 className="inline-block text-4xl font-extrabold tracking-tight text-slate-900 lg:text-5xl">
<h1 className="inline-block font-heading text-4xl font-extrabold lg:text-5xl">
{page.title}
</h1>
{page.description && (
<p className="text-xl text-slate-600">{page.description}</p>
<p className="text-xl text-muted-foreground">{page.description}</p>
)}
</div>
<hr className="my-4 border-slate-200" />
<hr className="my-4" />
<Mdx code={page.body.code} />
</article>
)

View file

@ -1,13 +1,15 @@
import { notFound } from "next/navigation"
import { allAuthors, allPosts } from "contentlayer/generated"
import { Mdx } from "@/components/mdx"
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, formatDate } from "@/lib/utils"
import { absoluteUrl, cn, formatDate } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
interface PostPageProps {
@ -95,18 +97,24 @@ export default async function PostPage({ params }: PostPageProps) {
<article className="container relative max-w-3xl py-6 lg:py-10">
<Link
href="/blog"
className="absolute top-14 left-[-200px] hidden items-center justify-center text-sm font-medium text-slate-600 hover:text-slate-900 xl:inline-flex"
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-slate-600">
<time
dateTime={post.date}
className="block text-sm text-muted-foreground"
>
Published on {formatDate(post.date)}
</time>
)}
<h1 className="mt-2 inline-block text-4xl font-extrabold leading-tight text-slate-900 lg:text-5xl">
<h1 className="mt-2 inline-block font-heading text-4xl font-extrabold leading-tight lg:text-5xl">
{post.title}
</h1>
{authors?.length ? (
@ -123,11 +131,11 @@ export default async function PostPage({ params }: PostPageProps) {
alt={author.title}
width={42}
height={42}
className="rounded-full"
className="rounded-full bg-white"
/>
<div className="flex-1 text-left leading-tight">
<p className="font-medium text-slate-900">{author.title}</p>
<p className="text-[12px] text-slate-600">
<p className="font-medium">{author.title}</p>
<p className="text-[12px] text-muted-foreground">
@{author.twitter}
</p>
</div>
@ -143,17 +151,14 @@ export default async function PostPage({ params }: PostPageProps) {
alt={post.title}
width={720}
height={405}
className="my-8 rounded-md border border-slate-200 bg-slate-200 transition-colors group-hover:border-slate-900"
className="my-8 rounded-md border bg-muted transition-colors"
priority
/>
)}
<Mdx code={post.body.code} />
<hr className="my-4 border-slate-200" />
<hr className="mt-12" />
<div className="flex justify-center py-6 lg:py-10">
<Link
href="/blog"
className="inline-flex items-center justify-center text-sm font-medium text-slate-600 hover:text-slate-900"
>
<Link href="/blog" className={cn(buttonVariants({ variant: "ghost" }))}>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
See all posts
</Link>

View file

@ -20,15 +20,15 @@ export default async function BlogPage() {
<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 text-4xl font-extrabold tracking-tight text-slate-900 lg:text-5xl">
<h1 className="inline-block font-heading text-4xl font-extrabold tracking-tight lg:text-5xl">
Blog
</h1>
<p className="text-xl text-slate-600">
<p className="text-xl text-muted-foreground">
A blog built using Contentlayer. Posts are written in MDX.
</p>
</div>
</div>
<hr className="my-8 border-slate-200" />
<hr className="my-8" />
{posts?.length ? (
<div className="grid gap-10 sm:grid-cols-2">
{posts.map((post, index) => (
@ -42,16 +42,16 @@ export default async function BlogPage() {
alt={post.title}
width={804}
height={452}
className="rounded-md border border-slate-200 bg-slate-200 transition-colors group-hover:border-slate-900"
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-slate-600">{post.description}</p>
<p className="text-muted-foreground">{post.description}</p>
)}
{post.date && (
<p className="text-sm text-slate-600">
<p className="text-sm text-muted-foreground">
{formatDate(post.date)}
</p>
)}

View file

@ -2,9 +2,9 @@ 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"
import { buttonVariants } from "@/components/ui/button"
interface MarketingLayoutProps {
children: React.ReactNode
@ -15,13 +15,16 @@ export default async function MarketingLayout({
}: MarketingLayoutProps) {
return (
<div className="flex min-h-screen flex-col">
<header className="container sticky top-0 z-40 bg-white">
<div className="flex h-16 items-center justify-between border-b border-b-slate-200 py-4">
<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({ size: "sm" }), "px-4")}
className={cn(
buttonVariants({ variant: "secondary", size: "sm" }),
"px-4"
)}
>
Login
</Link>

View file

@ -1,10 +1,8 @@
import Image from "next/image"
import Link from "next/link"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import hero from "../../public/images/hero.png"
async function getGitHubStars(): Promise<string | null> {
try {
@ -38,99 +36,107 @@ export default async function IndexPage() {
return (
<>
<section className="container grid items-center justify-center gap-6 pt-6 pb-8 md:pt-10 md:pb-12 lg:pt-16 lg:pb-24">
<Image src={hero} width={250} alt="Hero image" priority />
<div className="mx-auto flex flex-col items-start gap-4 lg:w-[52rem]">
<h1 className="text-3xl font-bold leading-[1.1] tracking-tighter sm:text-5xl md:text-6xl">
What&apos;s going on here?
<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-slate-700 sm:text-xl sm:leading-8">
<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>
<div className="flex gap-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 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>
<hr className="border-slate-200" />
<section className="container grid justify-center gap-6 py-8 md:py-12 lg:py-24">
<div className="mx-auto flex flex-col gap-4 md:max-w-[52rem]">
<h2 className="text-3xl font-bold leading-[1.1] tracking-tighter sm:text-3xl md:text-6xl">
<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-slate-700 sm:text-lg sm:leading-7">
<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="grid justify-center gap-4 sm:grid-cols-2 md:max-w-[56rem] md:grid-cols-3">
<div className="relative overflow-hidden rounded-lg border border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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 text-slate-100">Next.js 13</h3>
<p className="text-sm text-slate-100">
<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 border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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 text-slate-100">React 18</h3>
<p className="text-sm text-slate-100">
<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 border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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 text-slate-100">Database</h3>
<p className="text-sm text-slate-100">
<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 border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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 text-slate-100">Components</h3>
<p className="text-sm text-slate-100">
<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 border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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"
@ -141,43 +147,42 @@ export default async function IndexPage() {
<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 text-slate-100">Authentication</h3>
<p className="text-sm text-slate-100">
<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 border-slate-200 bg-white p-2 shadow-2xl">
<div className="flex h-[180px] flex-col justify-between rounded-md bg-[#000000] p-6 text-slate-200">
<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 text-slate-100">Subscriptions</h3>
<p className="text-sm text-slate-100">
<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 flex flex-col gap-4 md:max-w-[52rem]">
<p className="max-w-[85%] leading-normal text-slate-700 sm:text-lg sm:leading-7">
<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>
<hr className="border-slate-200" />
<section className="container grid justify-center gap-6 py-8 md:py-12 lg:py-24">
<div className="mx-auto flex flex-col gap-4 md:max-w-[52rem]">
<h2 className="text-3xl font-bold leading-[1.1] tracking-tighter sm:text-3xl md:text-6xl">
<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-slate-700 sm:text-lg sm:leading-7">
Taxonomy is open source and powered by open source software. The
code is available on{" "}
<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"
@ -187,37 +192,33 @@ export default async function IndexPage() {
GitHub
</Link>
.{" "}
<Link href="/docs" className="underline underline-offset-4">
I&apos;m also documenting everything here
</Link>
.
</p>
</div>
{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-slate-600 bg-slate-800">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="currentColor"
viewBox="0 0 24 24"
className="h-5 w-5 text-white"
>
<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-r-8 border-l-0 border-solid border-y-transparent border-r-slate-800"></div>
<div className="flex h-10 items-center rounded-md border border-slate-800 bg-slate-800 px-4 font-medium text-slate-200">
{stars} stars on GitHub
{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>
</Link>
)}
<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,8 +1,8 @@
import Link from "next/link"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
export const metadata = {
title: "Pricing",
@ -11,20 +11,20 @@ export const metadata = {
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-[52rem]">
<h2 className="text-3xl font-bold leading-[1.1] tracking-tighter sm:text-3xl md:text-6xl">
<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-slate-700 sm:text-lg sm:leading-7">
<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 border-slate-200 p-10 md:grid-cols-[1fr_200px]">
<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-slate-600 sm:grid-cols-2">
<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>
@ -49,15 +49,17 @@ export default function PricingPage() {
<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-slate-600">Billed Monthly</p>
<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 flex-col gap-4 md:max-w-[52rem]">
<p className="max-w-[85%] leading-normal text-slate-700 sm:leading-7">
<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>

View file

@ -0,0 +1,7 @@
import NextAuth from "next-auth"
import { authOptions } from "@/lib/auth"
const handler = NextAuth(authOptions)
export { handler as GET, handler as POST }

View file

@ -1,21 +1,18 @@
import { NextRequest } from "next/server"
import { ImageResponse } from "@vercel/og"
import { ogImageSchema } from "@/lib/validations/og"
export const config = {
runtime: "edge",
}
export const runtime = "edge"
const interRegular = fetch(
new URL("../../assets/fonts/Inter-Regular.ttf", import.meta.url)
new URL("../../../assets/fonts/Inter-Regular.ttf", import.meta.url)
).then((res) => res.arrayBuffer())
const interBold = fetch(
new URL("../../assets/fonts/Inter-Bold.ttf", import.meta.url)
new URL("../../../assets/fonts/CalSans-SemiBold.ttf", import.meta.url)
).then((res) => res.arrayBuffer())
export default async function handler(req: NextRequest) {
export async function GET(req: Request) {
try {
const fontRegular = await interRegular
const fontBold = await interBold
@ -81,10 +78,10 @@ export default async function handler(req: NextRequest) {
{values.type}
</div>
<div
tw="flex leading-[1.1] text-[80px] font-bold tracking-tighter"
tw="flex leading-[1.1] text-[80px] font-bold"
style={{
fontFamily: "Inter",
fontWeight: "bolder",
fontFamily: "Cal Sans",
fontWeight: "bold",
marginLeft: "-3px",
fontSize,
}}
@ -135,7 +132,7 @@ export default async function handler(req: NextRequest) {
style: "normal",
},
{
name: "Inter",
name: "Cal Sans",
data: fontBold,
weight: 700,
style: "normal",

View file

@ -0,0 +1,93 @@
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
}

92
app/api/posts/route.ts Normal file
View file

@ -0,0 +1,92 @@
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

@ -0,0 +1,50 @@
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

@ -0,0 +1,61 @@
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,23 +1,12 @@
import { NextApiRequest, NextApiResponse } from "next"
import rawBody from "raw-body"
import { headers } from "next/headers"
import Stripe from "stripe"
import { db } from "@/lib/db"
import { stripe } from "@/lib/stripe"
export const config = {
api: {
// Turn off the body parser so we can access raw body for verification.
bodyParser: false,
},
}
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
const body = await rawBody(req)
const signature = req.headers["stripe-signature"] as string
export async function POST(req: Request) {
const body = await req.text()
const signature = headers().get("Stripe-Signature") as string
let event: Stripe.Event
@ -28,7 +17,7 @@ export default async function handler(
process.env.STRIPE_WEBHOOK_SECRET || ""
)
} catch (error) {
return res.status(400).send(`Webhook Error: ${error.message}`)
return new Response(`Webhook Error: ${error.message}`, { status: 400 })
}
const session = event.data.object as Stripe.Checkout.Session
@ -77,5 +66,5 @@ export default async function handler(
})
}
return res.json({})
return new Response(null, { status: 200 })
}

View file

@ -1,15 +1,23 @@
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 { Toaster } from "@/components/ui/toaster"
import { ThemeProvider } from "@/components/theme-provider"
const fontSans = FontSans({
subsets: ["latin"],
variable: "--font-inter",
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 {
@ -47,14 +55,6 @@ export const metadata = {
title: siteConfig.name,
description: siteConfig.description,
siteName: siteConfig.name,
images: [
{
url: absoluteUrl("/og.jpg"),
width: 1200,
height: 630,
alt: siteConfig.name,
},
],
},
twitter: {
card: "summary_large_image",
@ -73,19 +73,21 @@ export const metadata = {
export default function RootLayout({ children }: RootLayoutProps) {
return (
<html
lang="en"
className={cn(
"bg-white font-sans text-slate-900 antialiased",
fontSans.variable
)}
>
<html lang="en" suppressHydrationWarning>
<head />
<body className="min-h-screen">
{children}
<Analytics />
<Toaster />
<TailwindIndicator />
<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>
)

BIN
app/opengraph-image.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

10
app/robots.ts Normal file
View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,13 +1,20 @@
"use client"
import * as React from "react"
import { toast } from "@/hooks/use-toast"
import { UserSubscriptionPlan } from "types"
import { cn, formatDate } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
interface BillingFormProps extends React.HTMLAttributes<HTMLFormElement> {
subscriptionPlan: UserSubscriptionPlan & {
@ -49,15 +56,15 @@ export function BillingForm({
return (
<form className={cn(className)} onSubmit={onSubmit} {...props}>
<Card>
<Card.Header>
<Card.Title>Plan</Card.Title>
<Card.Description>
<CardHeader>
<CardTitle>Subscription Plan</CardTitle>
<CardDescription>
You are currently on the <strong>{subscriptionPlan.name}</strong>{" "}
plan.
</Card.Description>
</Card.Header>
<Card.Content>{subscriptionPlan.description}</Card.Content>
<Card.Footer className="flex flex-col items-start space-y-2 md:flex-row md:justify-between md:space-x-0">
</CardDescription>
</CardHeader>
<CardContent>{subscriptionPlan.description}</CardContent>
<CardFooter className="flex flex-col items-start space-y-2 md:flex-row md:justify-between md:space-x-0">
<button
type="submit"
className={cn(buttonVariants())}
@ -76,7 +83,7 @@ export function BillingForm({
{formatDate(subscriptionPlan.stripeCurrentPeriodEnd)}.
</p>
) : null}
</Card.Footer>
</CardFooter>
</Card>
</form>
)

View file

@ -15,7 +15,6 @@ export function Callout({
return (
<div
className={cn("my-6 flex items-start rounded-md border border-l-4 p-4", {
"border-slate-900 bg-slate-50": type === "default",
"border-red-900 bg-red-50": type === "danger",
"border-yellow-900 bg-yellow-50": type === "warning",
})}

View file

@ -0,0 +1,17 @@
import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
export function CardSkeleton() {
return (
<Card>
<CardHeader className="gap-2">
<Skeleton className="h-5 w-1/5" />
<Skeleton className="h-4 w-4/5" />
</CardHeader>
<CardContent className="h-10" />
<CardFooter>
<Skeleton className="h-8 w-[120px]" />
</CardFooter>
</Card>
)
}

View file

@ -3,7 +3,6 @@
import * as React from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { toast } from "@/hooks/use-toast"
import EditorJS from "@editorjs/editorjs"
import { zodResolver } from "@hookform/resolvers/zod"
import { Post } from "@prisma/client"
@ -11,10 +10,12 @@ import { useForm } from "react-hook-form"
import TextareaAutosize from "react-textarea-autosize"
import * as z from "zod"
import "@/styles/editor.css"
import { cn } from "@/lib/utils"
import { postPatchSchema } from "@/lib/validations/post"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
interface EditorProps {
post: Pick<Post, "id" | "title" | "content" | "published">
@ -133,7 +134,7 @@ export function Editor({ post }: EditorProps) {
Back
</>
</Link>
<p className="text-sm text-slate-600">
<p className="text-sm text-muted-foreground">
{post.published ? "Published" : "Draft"}
</p>
</div>
@ -144,19 +145,19 @@ export function Editor({ post }: EditorProps) {
<span>Save</span>
</button>
</div>
<div className="prose prose-stone mx-auto w-[800px]">
<div className="prose prose-stone mx-auto w-[800px] dark:prose-invert">
<TextareaAutosize
autoFocus
id="title"
defaultValue={post.title}
placeholder="Post title"
className="w-full resize-none appearance-none overflow-hidden text-5xl font-bold focus:outline-none"
className="w-full resize-none appearance-none overflow-hidden bg-transparent text-5xl font-bold focus:outline-none"
{...register("title")}
/>
<div id="editor" className="min-h-[500px]" />
<p className="text-sm text-gray-500">
Use{" "}
<kbd className="rounded-md border bg-slate-50 px-1 text-xs uppercase">
<kbd className="rounded-md border bg-muted px-1 text-xs uppercase">
Tab
</kbd>{" "}
to open the command menu.

View file

@ -42,7 +42,7 @@ EmptyPlaceholder.Icon = function EmptyPlaceHolderIcon({
}
return (
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-slate-100">
<div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted">
<Icon className={cn("h-10 w-10", className)} {...props} />
</div>
)
@ -70,7 +70,7 @@ EmptyPlaceholder.Description = function EmptyPlaceholderDescription({
return (
<p
className={cn(
"mt-3 mb-8 text-center text-sm font-normal leading-6 text-slate-700",
"mb-8 mt-2 text-center text-sm font-normal leading-6 text-muted-foreground",
className
)}
{...props}

View file

@ -10,12 +10,12 @@ export function DashboardHeader({
children,
}: DashboardHeaderProps) {
return (
<div className="flex justify-between px-2">
<div className="flex items-center justify-between px-2">
<div className="grid gap-1">
<h1 className="text-2xl font-bold tracking-wide text-slate-900">
<h1 className="font-heading text-3xl font-bold md:text-4xl">
{heading}
</h1>
{text && <p className="text-neutral-500">{text}</p>}
{text && <p className="text-lg text-muted-foreground">{text}</p>}
</div>
{children}
</div>

View file

@ -8,15 +8,17 @@ import {
CreditCard,
File,
FileText,
Github,
HelpCircle,
Image,
Laptop,
Loader2,
LucideProps,
Moon,
MoreVertical,
Pizza,
Plus,
Settings,
SunMedium,
Trash,
Twitter,
User,
@ -45,6 +47,9 @@ export const Icons = {
arrowRight: ArrowRight,
help: HelpCircle,
pizza: Pizza,
sun: SunMedium,
moon: Moon,
laptop: Laptop,
gitHub: ({ ...props }: LucideProps) => (
<svg
aria-hidden="true"

View file

@ -34,8 +34,10 @@ export function MainNav({ items, children }: MainNavProps) {
key={index}
href={item.disabled ? "#" : item.href}
className={cn(
"flex items-center text-lg font-semibold text-slate-600 sm:text-sm",
item.href.startsWith(`/${segment}`) && "text-slate-900",
"flex items-center text-lg font-medium transition-colors hover:text-foreground/80 sm:text-sm",
item.href.startsWith(`/${segment}`)
? "text-foreground"
: "text-foreground/60",
item.disabled && "cursor-not-allowed opacity-80"
)}
>

View file

@ -7,7 +7,7 @@ interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
disabled?: boolean
}
export function Card({
export function MdxCard({
href,
className,
children,
@ -17,14 +17,14 @@ export function Card({
return (
<div
className={cn(
"group relative rounded-lg border border-slate-200 bg-white p-6 shadow-md transition-shadow hover:shadow-lg",
"group relative rounded-lg border p-6 shadow-md transition-shadow hover:shadow-lg",
disabled && "cursor-not-allowed opacity-60",
className
)}
{...props}
>
<div className="flex flex-col justify-between space-y-4">
<div className="space-y-2 [&>p]:text-slate-600 [&>h4]:!mt-0 [&>h3]:!mt-0">
<div className="space-y-2 [&>h3]:!mt-0 [&>h4]:!mt-0 [&>p]:text-muted-foreground">
{children}
</div>
</div>

View file

@ -4,7 +4,7 @@ import { useMDXComponent } from "next-contentlayer/hooks"
import { cn } from "@/lib/utils"
import { Callout } from "@/components/callout"
import { Card } from "@/components/card"
import { MdxCard } from "@/components/mdx-card"
const components = {
h1: ({ className, ...props }) => (
@ -19,7 +19,7 @@ const components = {
h2: ({ className, ...props }) => (
<h2
className={cn(
"mt-10 scroll-m-20 border-b border-b-slate-200 pb-1 text-3xl font-semibold tracking-tight first:mt-0",
"mt-10 scroll-m-20 border-b pb-1 text-3xl font-semibold tracking-tight first:mt-0",
className
)}
{...props}
@ -63,10 +63,7 @@ const components = {
),
a: ({ className, ...props }) => (
<a
className={cn(
"font-medium text-slate-900 underline underline-offset-4",
className
)}
className={cn("font-medium underline underline-offset-4", className)}
{...props}
/>
),
@ -88,7 +85,7 @@ const components = {
blockquote: ({ className, ...props }) => (
<blockquote
className={cn(
"mt-6 border-l-2 border-slate-300 pl-6 italic text-slate-800 [&>*]:text-slate-600",
"mt-6 border-l-2 pl-6 italic [&>*]:text-muted-foreground",
className
)}
{...props}
@ -100,15 +97,9 @@ const components = {
...props
}: React.ImgHTMLAttributes<HTMLImageElement>) => (
// eslint-disable-next-line @next/next/no-img-element
<img
className={cn("rounded-md border border-slate-200", className)}
alt={alt}
{...props}
/>
),
hr: ({ ...props }) => (
<hr className="my-4 border-slate-200 md:my-8" {...props} />
<img className={cn("rounded-md border", className)} alt={alt} {...props} />
),
hr: ({ ...props }) => <hr className="my-4 md:my-8" {...props} />,
table: ({ className, ...props }: React.HTMLAttributes<HTMLTableElement>) => (
<div className="my-6 w-full overflow-y-auto">
<table className={cn("w-full", className)} {...props} />
@ -116,17 +107,14 @@ const components = {
),
tr: ({ className, ...props }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr
className={cn(
"m-0 border-t border-slate-300 p-0 even:bg-slate-100",
className
)}
className={cn("m-0 border-t p-0 even:bg-muted", className)}
{...props}
/>
),
th: ({ className, ...props }) => (
<th
className={cn(
"border border-slate-200 px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right",
"border px-4 py-2 text-left font-bold [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
{...props}
@ -135,7 +123,7 @@ const components = {
td: ({ className, ...props }) => (
<td
className={cn(
"border border-slate-200 px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right",
"border px-4 py-2 text-left [&[align=center]]:text-center [&[align=right]]:text-right",
className
)}
{...props}
@ -144,7 +132,7 @@ const components = {
pre: ({ className, ...props }) => (
<pre
className={cn(
"mt-6 mb-4 overflow-x-auto rounded-lg bg-slate-900 py-4",
"mb-4 mt-6 overflow-x-auto rounded-lg border bg-black py-4",
className
)}
{...props}
@ -153,7 +141,7 @@ const components = {
code: ({ className, ...props }) => (
<code
className={cn(
"relative rounded border bg-slate-300/25 py-[0.2rem] px-[0.3rem] font-mono text-sm text-slate-600",
"relative rounded border px-[0.3rem] py-[0.2rem] font-mono text-sm",
className
)}
{...props}
@ -161,7 +149,7 @@ const components = {
),
Image,
Callout,
Card,
Card: MdxCard,
}
interface MdxProps {

View file

@ -1,55 +0,0 @@
import { allDocuments } from "contentlayer/generated"
import * as z from "zod"
import { absoluteUrl } from "@/lib/utils"
import { ogImageSchema } from "@/lib/validations/og"
interface MdxHeadProps {
params: {
slug?: string[]
}
og?: z.infer<typeof ogImageSchema>
}
export default function MdxHead({ params, og }: MdxHeadProps) {
const slug = params?.slug?.join("/") || ""
const mdxDoc = allDocuments.find((doc) => doc.slugAsParams === slug)
if (!mdxDoc) {
return null
}
const title = `${mdxDoc.title} - Taxonomy`
const url = process.env.NEXT_PUBLIC_APP_URL
let ogUrl = new URL(`${url}/og.jpg`)
const ogTitle = og?.heading || mdxDoc.title
const ogDescription = mdxDoc.description
if (og?.type) {
ogUrl = new URL(`${url}/api/og`)
ogUrl.searchParams.set("heading", ogTitle)
ogUrl.searchParams.set("type", og.type)
ogUrl.searchParams.set("mode", og.mode || "dark")
}
return (
<>
<title>{title}</title>
<link rel="canonical" href={absoluteUrl(mdxDoc.slug)} />
<meta name="description" content={ogDescription} />
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width" />
<meta property="og:type" content="website" />
<meta property="og:title" content={ogTitle} />
<meta property="og:description" content={ogDescription} />
<meta property="og:url" content={url} />
<meta property="og:image" content={ogUrl.toString()} />
<meta name="twitter:title" content={ogTitle} />
<meta name="twitter:description" content={ogDescription} />
<meta name="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={url} />
<meta name="twitter:image" content={ogUrl.toString()} />
</>
)
}

View file

@ -1,10 +1,10 @@
import * as React from "react"
import Link from "next/link"
import { useLockBody } from "@/hooks/use-lock-body"
import { MainNavItem } from "types"
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { useLockBody } from "@/hooks/use-lock-body"
import { Icons } from "@/components/icons"
interface MobileNavProps {

View file

@ -0,0 +1,43 @@
"use client"
import * as React from "react"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Icons } from "@/components/icons"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 px-0">
<Icons.sun className="rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Icons.moon className="absolute rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme("light")}>
<Icons.sun className="mr-2 h-4 w-4" />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
<Icons.moon className="mr-2 h-4 w-4" />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("system")}>
<Icons.laptop className="mr-2 h-4 w-4" />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View file

@ -27,8 +27,8 @@ export function DashboardNav({ items }: DashboardNavProps) {
<Link key={index} href={item.disabled ? "/" : item.href}>
<span
className={cn(
"group flex items-center rounded-md px-3 py-2 text-sm font-medium text-slate-800 hover:bg-slate-100",
path === item.href ? "bg-slate-200" : "transparent",
"group flex items-center rounded-md px-3 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground",
path === item.href ? "bg-accent" : "transparent",
item.disabled && "cursor-not-allowed opacity-80"
)}
>

View file

@ -14,12 +14,12 @@ export function DocsPageHeader({
return (
<>
<div className={cn("space-y-4", className)} {...props}>
<h1 className="inline-block text-4xl font-black tracking-tight text-slate-900 lg:text-5xl">
<h1 className="inline-block font-heading text-4xl font-black lg:text-5xl">
{heading}
</h1>
{text && <p className="text-xl text-slate-600">{text}</p>}
{text && <p className="text-xl text-muted-foreground">{text}</p>}
</div>
<hr className="my-4 border-slate-200" />
<hr className="my-4" />
</>
)
}

View file

@ -2,6 +2,8 @@ import Link from "next/link"
import { Doc } from "contentlayer/generated"
import { docsConfig } from "@/config/docs"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
import { Icons } from "@/components/icons"
interface DocsPagerProps {
@ -20,7 +22,7 @@ export function DocsPager({ doc }: DocsPagerProps) {
{pager?.prev && (
<Link
href={pager.prev.href}
className="inline-flex items-center justify-center rounded-lg border border-transparent bg-transparent py-2 px-3 text-center text-sm font-medium text-slate-900 hover:border-slate-200 hover:bg-slate-100 focus:z-10 focus:outline-none focus:ring-4 focus:ring-slate-200"
className={cn(buttonVariants({ variant: "ghost" }))}
>
<Icons.chevronLeft className="mr-2 h-4 w-4" />
{pager.prev.title}
@ -29,7 +31,7 @@ export function DocsPager({ doc }: DocsPagerProps) {
{pager?.next && (
<Link
href={pager.next.href}
className="ml-auto inline-flex items-center justify-center rounded-lg border border-transparent bg-transparent py-2 px-3 text-center text-sm font-medium text-slate-900 hover:border-slate-200 hover:bg-slate-100 focus:z-10 focus:outline-none focus:ring-4 focus:ring-slate-200"
className={cn(buttonVariants({ variant: "ghost" }), "ml-auto")}
>
{pager.next.title}
<Icons.chevronRight className="ml-2 h-4 w-4" />

View file

@ -2,17 +2,17 @@
import * as React from "react"
import { useRouter } from "next/navigation"
import { toast } from "@/hooks/use-toast"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
interface PostCreateButtonProps
extends React.HTMLAttributes<HTMLButtonElement> {}
interface PostCreateButtonProps extends ButtonProps {}
export function PostCreateButton({
className,
variant,
...props
}: PostCreateButtonProps) {
const router = useRouter()
@ -61,7 +61,7 @@ export function PostCreateButton({
<button
onClick={onClick}
className={cn(
buttonVariants(),
buttonVariants({ variant }),
{
"cursor-not-allowed opacity-60": isLoading,
},

View file

@ -2,8 +2,8 @@ import Link from "next/link"
import { Post } from "@prisma/client"
import { formatDate } from "@/lib/utils"
import { PostOperations } from "@/components/post-operations"
import { Skeleton } from "@/components/ui/skeleton"
import { PostOperations } from "@/components/post-operations"
interface PostItemProps {
post: Pick<Post, "id" | "title" | "published" | "createdAt">
@ -20,13 +20,12 @@ export function PostItem({ post }: PostItemProps) {
{post.title}
</Link>
<div>
<p className="text-sm text-slate-600">
<p className="text-sm text-muted-foreground">
{formatDate(post.createdAt?.toDateString())}
</p>
</div>
</div>
<PostOperations post={{ id: post.id, title: post.title }} />
{/* <PostDeleteButton post={{ id: post.id, title: post.title }} /> */}
</div>
)
}

View file

@ -3,10 +3,8 @@
import * as React from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { toast } from "@/hooks/use-toast"
import { Post } from "@prisma/client"
import { Icons } from "@/components/icons"
import {
AlertDialog,
AlertDialogAction,
@ -24,6 +22,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
async function deletePost(postId: string) {
const response = await fetch(`/api/posts/${postId}`, {
@ -53,7 +53,7 @@ export function PostOperations({ post }: PostOperationsProps) {
return (
<>
<DropdownMenu>
<DropdownMenuTrigger className="flex h-8 w-8 items-center justify-center rounded-md border transition-colors hover:bg-slate-50">
<DropdownMenuTrigger className="flex h-8 w-8 items-center justify-center rounded-md border transition-colors hover:bg-muted">
<Icons.ellipsis className="h-4 w-4" />
<span className="sr-only">Open</span>
</DropdownMenuTrigger>
@ -65,7 +65,7 @@ export function PostOperations({ post }: PostOperationsProps) {
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="flex cursor-pointer items-center text-red-600 focus:bg-red-50"
className="flex cursor-pointer items-center text-destructive focus:text-destructive"
onSelect={() => setShowDeleteAlert(true)}
>
Delete

View file

@ -1,9 +1,10 @@
"use client"
import * as React from "react"
import { toast } from "@/hooks/use-toast"
import { cn } from "@/lib/utils"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
interface DocsSearchProps extends React.HTMLAttributes<HTMLFormElement> {}
@ -23,12 +24,12 @@ export function DocsSearch({ className, ...props }: DocsSearchProps) {
className={cn("relative w-full", className)}
{...props}
>
<input
<Input
type="search"
placeholder="Search documentation..."
className="block h-8 w-full appearance-none rounded-md border border-slate-200 bg-slate-100 py-2 px-3 text-sm placeholder:text-slate-400 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 sm:w-64 sm:pr-12"
className="h-8 w-full sm:w-64 sm:pr-12"
/>
<kbd className="pointer-events-none absolute top-1.5 right-1.5 hidden h-5 select-none items-center gap-1 rounded border bg-white px-1.5 font-mono text-[10px] font-medium text-slate-600 opacity-100 sm:flex">
<kbd className="pointer-events-none absolute right-1.5 top-1.5 hidden h-5 select-none items-center gap-1 rounded border bg-background px-1.5 font-mono text-[10px] font-medium text-muted-foreground opacity-100 sm:flex">
<span className="text-xs"></span>K
</kbd>
</form>

View file

@ -41,15 +41,14 @@ export function DocsSidebarNavItems({
return items?.length ? (
<div className="grid grid-flow-row auto-rows-max text-sm">
{items.map((item, index) =>
item.href ? (
!item.disabled && item.href ? (
<Link
key={index}
href={item.disabled ? "#" : item.href}
href={item.href}
className={cn(
"flex w-full items-center rounded-md p-2 hover:underline",
item.disabled && "cursor-not-allowed opacity-60",
{
"bg-slate-100": pathname === item.href,
"bg-muted": pathname === item.href,
}
)}
target={item.external ? "_blank" : ""}
@ -57,7 +56,11 @@ export function DocsSidebarNavItems({
>
{item.title}
</Link>
) : null
) : (
<span className="flex w-full cursor-not-allowed items-center rounded-md p-2 opacity-60">
{item.title}
</span>
)
)}
</div>
) : null

View file

@ -1,10 +1,14 @@
import { siteConfig } from "@/config/site"
import { Icons } from "@/components/icons"
import * as React from "react"
export function SiteFooter() {
import { siteConfig } from "@/config/site"
import { cn } from "@/lib/utils"
import { Icons } from "@/components/icons"
import { ModeToggle } from "@/components/mode-toggle"
export function SiteFooter({ className }: React.HTMLAttributes<HTMLElement>) {
return (
<footer className="container bg-white text-slate-600">
<div className="flex flex-col items-center justify-between gap-4 border-t border-t-slate-200 py-10 md:h-24 md:flex-row md:py-0">
<footer className={cn(className)}>
<div className="container flex flex-col items-center justify-between gap-4 py-10 md:h-24 md:flex-row md:py-0">
<div className="flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0">
<Icons.logo />
<p className="text-center text-sm leading-loose md:text-left">
@ -35,21 +39,19 @@ export function SiteFooter() {
>
Popsy
</a>
. The source code is available on{" "}
<a
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
GitHub
</a>
.
</p>
</div>
<p className="text-center text-sm md:text-left">
The source code is available on{" "}
<a
href={siteConfig.links.github}
target="_blank"
rel="noreferrer"
className="font-medium underline underline-offset-4"
>
GitHub
</a>
.
</p>
<ModeToggle />
</div>
</footer>
)

View file

@ -0,0 +1,9 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View file

@ -1,10 +1,10 @@
"use client"
import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import { TableOfContents } from "@/lib/toc"
import { cn } from "@/lib/utils"
import { useMounted } from "@/hooks/use-mounted"
interface TocProps {
toc: TableOfContents
@ -97,8 +97,8 @@ function Tree({ tree, level = 1, activeItem }: TreeProps) {
className={cn(
"inline-block no-underline",
item.url === `#${activeItem}`
? "text-state-900 font-medium"
: "text-sm text-slate-600 hover:text-slate-900"
? "font-medium text-primary"
: "text-sm text-muted-foreground"
)}
>
{item.title}

View file

@ -14,10 +14,7 @@ const AccordionItem = React.forwardRef<
>(({ className, ...props }, ref) => (
<AccordionPrimitive.Item
ref={ref}
className={cn(
"border-b border-b-slate-200 dark:border-b-slate-700",
className
)}
className={cn("border-b", className)}
{...props}
/>
))
@ -50,12 +47,12 @@ const AccordionContent = React.forwardRef<
<AccordionPrimitive.Content
ref={ref}
className={cn(
"overflow-hidden text-sm transition-all data-[state=open]:animate-accordion-down data-[state=closed]:animate-accordion-up",
"overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down",
className
)}
{...props}
>
<div className="pt-0 pb-4">{children}</div>
<div className="pb-4 pt-0">{children}</div>
</AccordionPrimitive.Content>
))
AccordionContent.displayName = AccordionPrimitive.Content.displayName

View file

@ -4,6 +4,7 @@ import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
@ -28,7 +29,7 @@ const AlertDialogOverlay = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-opacity animate-in fade-in",
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-opacity animate-in fade-in",
className
)}
{...props}
@ -46,8 +47,7 @@ const AlertDialogContent = React.forwardRef<
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 bg-white p-6 opacity-100 animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
"dark:bg-slate-900",
"fixed z-50 grid w-full max-w-lg scale-100 gap-4 border bg-background p-6 opacity-100 shadow-lg animate-in fade-in-90 slide-in-from-bottom-10 sm:rounded-lg sm:zoom-in-90 sm:slide-in-from-bottom-0 md:w-full",
className
)}
{...props}
@ -90,11 +90,7 @@ const AlertDialogTitle = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900",
"dark:text-slate-50",
className
)}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
@ -106,7 +102,7 @@ const AlertDialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500", "dark:text-slate-400", className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
@ -119,10 +115,7 @@ const AlertDialogAction = React.forwardRef<
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-slate-900 py-2 px-4 text-sm font-semibold text-white transition-colors hover:bg-slate-700 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:bg-slate-100 dark:text-slate-900 dark:hover:bg-slate-200 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
className
)}
className={cn(buttonVariants(), className)}
{...props}
/>
))
@ -135,7 +128,8 @@ const AlertDialogCancel = React.forwardRef<
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
"mt-2 inline-flex h-10 items-center justify-center rounded-md border border-slate-200 bg-transparent py-2 px-4 text-sm font-semibold text-slate-900 transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 sm:mt-0",
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}

59
components/ui/alert.tsx Normal file
View file

@ -0,0 +1,59 @@
import * as React from "react"
import { VariantProps, cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const alertVariants = cva(
"relative w-full rounded-lg border p-4 [&>svg]:absolute [&>svg]:text-foreground [&>svg]:left-4 [&>svg]:top-4 [&>svg+div]:translate-y-[-3px] [&:has(svg)]:pl-11",
{
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"text-destructive border-destructive/50 dark:border-destructive [&>svg]:text-destructive text-destructive",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Alert = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement> & VariantProps<typeof alertVariants>
>(({ className, variant, ...props }, ref) => (
<div
ref={ref}
role="alert"
className={cn(alertVariants({ variant }), className)}
{...props}
/>
))
Alert.displayName = "Alert"
const AlertTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h5
ref={ref}
className={cn("mb-1 font-medium leading-none tracking-tight", className)}
{...props}
/>
))
AlertTitle.displayName = "AlertTitle"
const AlertDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("text-sm [&_p]:leading-relaxed", className)}
{...props}
/>
))
AlertDescription.displayName = "AlertDescription"
export { Alert, AlertTitle, AlertDescription }

View file

@ -39,7 +39,7 @@ const AvatarFallback = React.forwardRef<
<AvatarPrimitive.Fallback
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-slate-100 dark:bg-slate-700",
"flex h-full w-full items-center justify-center rounded-full bg-muted",
className
)}
{...props}

36
components/ui/badge.tsx Normal file
View file

@ -0,0 +1,36 @@
import * as React from "react"
import { VariantProps, cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"bg-primary hover:bg-primary/80 border-transparent text-primary-foreground",
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View file

@ -4,25 +4,23 @@ import { VariantProps, cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 dark:hover:bg-slate-800 dark:hover:text-slate-100 disabled:opacity-50 dark:focus:ring-slate-400 disabled:pointer-events-none dark:focus:ring-offset-slate-900 data-[state=open]:bg-slate-100 dark:data-[state=open]:bg-slate-800",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background",
{
variants: {
variant: {
default:
"bg-slate-900 text-white hover:bg-slate-700 dark:bg-slate-50 dark:text-slate-900",
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-red-500 text-white hover:bg-red-600 dark:hover:bg-red-600",
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700 dark:text-slate-100",
subtle:
"bg-slate-100 text-slate-900 hover:bg-slate-200 dark:bg-slate-700 dark:text-slate-100",
ghost:
"bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-transparent dark:data-[state=open]:bg-transparent",
link: "bg-transparent underline-offset-4 hover:underline text-slate-900 dark:text-slate-100 hover:bg-transparent dark:hover:bg-transparent",
"border border-input hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "underline-offset-4 hover:underline text-primary",
},
size: {
default: "h-10 py-2 px-4",
sm: "h-9 px-2 rounded-md",
sm: "h-9 px-3 rounded-md",
lg: "h-11 px-8 rounded-md",
},
},

View file

@ -0,0 +1,64 @@
"use client"
import * as React from "react"
import { ChevronLeft, ChevronRight } from "lucide-react"
import { DayPicker } from "react-day-picker"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
export type CalendarProps = React.ComponentProps<typeof DayPicker>
function Calendar({
className,
classNames,
showOutsideDays = true,
...props
}: CalendarProps) {
return (
<DayPicker
showOutsideDays={showOutsideDays}
className={cn("p-3", className)}
classNames={{
months: "flex flex-col sm:flex-row space-y-4 sm:space-x-4 sm:space-y-0",
month: "space-y-4",
caption: "flex justify-center pt-1 relative items-center",
caption_label: "text-sm font-medium",
nav: "space-x-1 flex items-center",
nav_button: cn(
buttonVariants({ variant: "outline" }),
"h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100"
),
nav_button_previous: "absolute left-1",
nav_button_next: "absolute right-1",
table: "w-full border-collapse space-y-1",
head_row: "flex",
head_cell:
"text-muted-foreground rounded-md w-9 font-normal text-[0.8rem]",
row: "flex w-full mt-2",
cell: "text-center text-sm p-0 relative [&:has([aria-selected])]:bg-accent first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md focus-within:relative focus-within:z-20",
day: cn(
buttonVariants({ variant: "ghost" }),
"h-9 w-9 p-0 font-normal aria-selected:opacity-100"
),
day_selected:
"bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
day_today: "bg-accent text-accent-foreground",
day_outside: "text-muted-foreground opacity-50",
day_disabled: "text-muted-foreground opacity-50",
day_range_middle:
"aria-selected:bg-accent aria-selected:text-accent-foreground",
day_hidden: "invisible",
...classNames,
}}
components={{
IconLeft: ({ ...props }) => <ChevronLeft className="h-4 w-4" />,
IconRight: ({ ...props }) => <ChevronRight className="h-4 w-4" />,
}}
{...props}
/>
)
}
Calendar.displayName = "Calendar"
export { Calendar }

View file

@ -1,67 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
import { Skeleton } from "@/components/ui/skeleton"
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {}
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
export function Card({ className, ...props }: CardProps) {
return (
<div
className={cn("overflow-hidden rounded-lg border", className)}
{...props}
/>
)
}
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
interface CardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {}
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
Card.Header = function CardHeader({ className, ...props }: CardHeaderProps) {
return <div className={cn("grid gap-1 p-6", className)} {...props} />
}
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
interface CardContentProps extends React.HTMLAttributes<HTMLDivElement> {}
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
Card.Content = function CardContent({ className, ...props }: CardContentProps) {
return <div className={cn("px-6 pb-4", className)} {...props} />
}
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(" flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
interface CardFooterProps extends React.HTMLAttributes<HTMLDivElement> {}
Card.Footer = function CardFooter({ className, ...props }: CardFooterProps) {
return (
<div
className={cn("border-t bg-slate-50 px-6 py-4", className)}
{...props}
/>
)
}
interface CardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {}
Card.Title = function CardTitle({ className, ...props }: CardTitleProps) {
return <h4 className={cn("text-lg font-medium", className)} {...props} />
}
interface CardDescriptionProps
extends React.HTMLAttributes<HTMLParagraphElement> {}
Card.Description = function CardDescription({
className,
...props
}: CardDescriptionProps) {
return <p className={cn("text-sm text-gray-600", className)} {...props} />
}
Card.Skeleton = function CardSeleton() {
return (
<Card>
<Card.Header className="gap-2">
<Skeleton className="h-5 w-1/5" />
<Skeleton className="h-4 w-4/5" />
</Card.Header>
<Card.Content className="h-10" />
<Card.Footer>
<Skeleton className="h-8 w-[120px] bg-slate-200" />
</Card.Footer>
</Card>
)
}
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View file

@ -13,13 +13,13 @@ const Checkbox = React.forwardRef<
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-slate-300 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
"peer h-4 w-4 shrink-0 rounded-sm border border-input ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-primary",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
className={cn("flex items-center justify-center")}
className={cn("flex items-center justify-center text-primary")}
>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>

View file

@ -2,8 +2,8 @@
import * as React from "react"
import { DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive, useCommandState } from "cmdk"
import { ChevronsUpDown, Search } from "lucide-react"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
@ -15,7 +15,7 @@ const Command = React.forwardRef<
<CommandPrimitive
ref={ref}
className={cn(
"flex h-full w-full flex-col overflow-hidden rounded-lg bg-white dark:bg-slate-800",
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
className
)}
{...props}
@ -28,8 +28,8 @@ interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-2xl [&_[dialog-overlay]]:bg-red-100">
<Command className="[&_[cmdk-group]]:px-2 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-slate-500 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-input]]:h-12 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-group]_+[cmdk-group]]:pt-0">
<DialogContent className="overflow-hidden p-0 shadow-2xl">
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@ -41,15 +41,12 @@ const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div
className="flex items-center border-b border-b-slate-100 px-4 dark:border-b-slate-700"
cmdk-input-wrapper=""
>
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="mr-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none placeholder:text-slate-400 disabled:cursor-not-allowed disabled:opacity-50 dark:text-slate-50",
"placeholder:text-foreground-muted flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
@ -92,7 +89,7 @@ const CommandGroup = React.forwardRef<
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden py-3 px-2 text-slate-700 dark:text-slate-400 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:pb-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:font-semibold [&_[cmdk-group-heading]]:text-slate-900 [&_[cmdk-group-heading]]:dark:text-slate-300",
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
@ -107,7 +104,7 @@ const CommandSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator
ref={ref}
className={cn("-mx-1 h-px bg-slate-100 dark:bg-slate-700", className)}
className={cn("-mx-1 h-px bg-border", className)}
{...props}
/>
))
@ -120,7 +117,7 @@ const CommandItem = React.forwardRef<
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-md py-1.5 px-2 text-sm font-medium outline-none aria-selected:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:aria-selected:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none aria-selected:bg-accent aria-selected:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -136,7 +133,7 @@ const CommandShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}

View file

@ -27,7 +27,7 @@ const ContextMenuSubTrigger = React.forwardRef<
<ContextMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
@ -46,7 +46,7 @@ const ContextMenuSubContent = React.forwardRef<
<ContextMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in slide-in-from-left-1",
className
)}
{...props}
@ -62,7 +62,7 @@ const ContextMenuContent = React.forwardRef<
<ContextMenuPrimitive.Content
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in fade-in-80 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-80",
className
)}
{...props}
@ -80,7 +80,7 @@ const ContextMenuItem = React.forwardRef<
<ContextMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
@ -96,7 +96,7 @@ const ContextMenuCheckboxItem = React.forwardRef<
<ContextMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@ -120,7 +120,7 @@ const ContextMenuRadioItem = React.forwardRef<
<ContextMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -144,7 +144,7 @@ const ContextMenuLabel = React.forwardRef<
<ContextMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300",
"px-2 py-1.5 text-sm font-semibold text-foreground",
inset && "pl-8",
className
)}
@ -159,7 +159,7 @@ const ContextMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<ContextMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
className={cn("-mx-1 my-1 h-px bg-border", className)}
{...props}
/>
))
@ -172,7 +172,7 @@ const ContextMenuShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}

View file

@ -26,14 +26,14 @@ DialogPortal.displayName = DialogPrimitive.Portal.displayName
const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/50 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=open]:fade-in data-[state=closed]:fade-out",
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}
ref={ref}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
@ -47,14 +47,13 @@ const DialogContent = React.forwardRef<
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed z-50 grid w-full gap-4 rounded-b-lg bg-white p-6 animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
"dark:bg-slate-900",
"fixed z-50 grid w-full gap-4 rounded-b-lg border bg-background p-6 shadow-lg animate-in data-[state=open]:fade-in-90 data-[state=open]:slide-in-from-bottom-10 sm:max-w-lg sm:rounded-lg sm:zoom-in-90 data-[state=open]:sm:slide-in-from-bottom-0",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800">
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
@ -69,7 +68,7 @@ const DialogHeader = ({
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
@ -98,8 +97,7 @@ const DialogTitle = React.forwardRef<
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold text-slate-900",
"dark:text-slate-50",
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
@ -113,7 +111,7 @@ const DialogDescription = React.forwardRef<
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-slate-500", "dark:text-slate-400", className)}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))

View file

@ -27,7 +27,7 @@ const DropdownMenuSubTrigger = React.forwardRef<
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
@ -47,7 +47,7 @@ const DropdownMenuSubContent = React.forwardRef<
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in slide-in-from-left-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"text-on-popover z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}
@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in data-[side=right]:slide-in-from-left-2 data-[side=left]:slide-in-from-right-2 data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
@ -99,7 +99,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@ -123,7 +123,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -147,7 +147,7 @@ const DropdownMenuLabel = React.forwardRef<
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300",
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
@ -162,7 +162,7 @@ const DropdownMenuSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
@ -174,10 +174,7 @@ const DropdownMenuShortcut = ({
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
className
)}
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)

View file

@ -18,7 +18,7 @@ const HoverCardContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-64 rounded-md border border-slate-100 bg-white p-4 shadow-md outline-none animate-in zoom-in-90 dark:border-slate-800 dark:bg-slate-800",
"z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in zoom-in-90",
className
)}
{...props}

View file

@ -6,11 +6,12 @@ export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, ...props }, ref) => {
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
"flex h-10 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View file

@ -2,19 +2,22 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { VariantProps, cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
className
)}
className={cn(labelVariants(), className)}
{...props}
/>
))

View file

@ -23,7 +23,7 @@ const Menubar = React.forwardRef<
<MenubarPrimitive.Root
ref={ref}
className={cn(
"flex h-10 items-center space-x-1 rounded-md border border-slate-300 bg-white p-1 dark:border-slate-700 dark:bg-slate-800",
"flex h-10 items-center space-x-1 rounded-md border bg-background p-1",
className
)}
{...props}
@ -38,7 +38,7 @@ const MenubarTrigger = React.forwardRef<
<MenubarPrimitive.Trigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-[0.2rem] py-1.5 px-3 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
"flex cursor-default select-none items-center rounded-sm px-3 py-1.5 text-sm font-medium outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
className
)}
{...props}
@ -55,7 +55,7 @@ const MenubarSubTrigger = React.forwardRef<
<MenubarPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[state=open]:bg-slate-100 dark:focus:bg-slate-700 dark:data-[state=open]:bg-slate-700",
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground",
inset && "pl-8",
className
)}
@ -74,7 +74,7 @@ const MenubarSubContent = React.forwardRef<
<MenubarPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 shadow-md animate-in slide-in-from-left-1 dark:border-slate-700 dark:bg-slate-800",
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}
@ -97,7 +97,7 @@ const MenubarContent = React.forwardRef<
alignOffset={alignOffset}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[12rem] overflow-hidden rounded-md border border-slate-100 bg-white p-1 text-slate-700 shadow-md animate-in slide-in-from-top-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"z-50 min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in slide-in-from-top-1",
className
)}
{...props}
@ -116,7 +116,7 @@ const MenubarItem = React.forwardRef<
<MenubarPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 px-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
@ -132,7 +132,7 @@ const MenubarCheckboxItem = React.forwardRef<
<MenubarPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
@ -155,7 +155,7 @@ const MenubarRadioItem = React.forwardRef<
<MenubarPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -179,7 +179,7 @@ const MenubarLabel = React.forwardRef<
<MenubarPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold text-slate-900 dark:text-slate-300",
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
@ -194,7 +194,7 @@ const MenubarSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<MenubarPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
@ -207,7 +207,7 @@ const MenubarShortcut = ({
return (
<span
className={cn(
"ml-auto text-xs tracking-widest text-slate-500",
"ml-auto text-xs tracking-widest text-muted-foreground",
className
)}
{...props}

View file

@ -30,7 +30,7 @@ const NavigationMenuList = React.forwardRef<
<NavigationMenuPrimitive.List
ref={ref}
className={cn(
"group flex flex-1 list-none items-center justify-center",
"group flex flex-1 list-none items-center justify-center space-x-1",
className
)}
{...props}
@ -41,7 +41,7 @@ NavigationMenuList.displayName = NavigationMenuPrimitive.List.displayName
const NavigationMenuItem = NavigationMenuPrimitive.Item
const navigationMenuTriggerStyle = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-slate-100 disabled:opacity-50 dark:focus:bg-slate-800 disabled:pointer-events-none bg-transparent hover:bg-slate-100 dark:hover:bg-slate-800 dark:text-slate-100 dark:hover:text-slate-100 data-[state=open]:bg-slate-50 dark:data-[state=open]:bg-slate-800 h-10 py-2 px-4 group w-max"
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus:outline-none focus:bg-accent focus:text-accent-foreground disabled:opacity-50 disabled:pointer-events-none bg-background hover:bg-accent hover:text-accent-foreground data-[state=open]:bg-accent/50 data-[active]:bg-accent/50 h-10 py-2 px-4 group w-max"
)
const NavigationMenuTrigger = React.forwardRef<
@ -69,7 +69,7 @@ const NavigationMenuContent = React.forwardRef<
<NavigationMenuPrimitive.Content
ref={ref}
className={cn(
"top-0 left-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=to-start]:slide-out-to-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=from-end]:slide-in-from-right-52 md:absolute md:w-auto ",
"left-0 top-0 w-full data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 md:absolute md:w-auto ",
className
)}
{...props}
@ -86,7 +86,7 @@ const NavigationMenuViewport = React.forwardRef<
<div className={cn("absolute left-0 top-full flex justify-center")}>
<NavigationMenuPrimitive.Viewport
className={cn(
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border border-slate-200 bg-white shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:zoom-in-90 data-[state=closed]:zoom-out-95 dark:border-slate-700 dark:bg-slate-800 md:w-[var(--radix-navigation-menu-viewport-width)]",
"origin-top-center relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
ref={ref}
@ -104,12 +104,12 @@ const NavigationMenuIndicator = React.forwardRef<
<NavigationMenuPrimitive.Indicator
ref={ref}
className={cn(
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=visible]:fade-in data-[state=hidden]:fade-out",
"top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in",
className
)}
{...props}
>
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-slate-200 shadow-md dark:bg-slate-800" />
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
</NavigationMenuPrimitive.Indicator>
))
NavigationMenuIndicator.displayName =

View file

@ -19,7 +19,7 @@ const PopoverContent = React.forwardRef<
align={align}
sideOffset={sideOffset}
className={cn(
"z-50 w-72 rounded-md border border-slate-100 bg-white p-4 shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=top]:slide-in-from-bottom-2 data-[side=right]:slide-in-from-left-2 data-[side=left]:slide-in-from-right-2 dark:border-slate-800 dark:bg-slate-800",
"z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none animate-in data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}

View file

@ -12,13 +12,13 @@ const Progress = React.forwardRef<
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800",
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-slate-900 transition-all dark:bg-slate-400"
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>

View file

@ -28,13 +28,13 @@ const RadioGroupItem = React.forwardRef<
<RadioGroupPrimitive.Item
ref={ref}
className={cn(
"text:fill-slate-50 h-4 w-4 rounded-full border border-slate-300 text-slate-900 hover:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-100 dark:hover:text-slate-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
"h-4 w-4 rounded-full border border-input ring-offset-background focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator className="flex items-center justify-center">
<Circle className="h-2.5 w-2.5 fill-slate-900 dark:fill-slate-50" />
<Circle className="h-2.5 w-2.5 fill-primary text-primary" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)

View file

@ -40,7 +40,7 @@ const ScrollBar = React.forwardRef<
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-slate-300 dark:bg-slate-700" />
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName

View file

@ -19,13 +19,15 @@ const SelectTrigger = React.forwardRef<
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
{children}
<ChevronDown className="h-4 w-4 opacity-50" />
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
@ -33,17 +35,25 @@ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, ...props }, ref) => (
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border border-slate-100 bg-white text-slate-700 shadow-md animate-in fade-in-80 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-80",
position === "popper" && "translate-y-1",
className
)}
position={position}
{...props}
>
<SelectPrimitive.Viewport className="p-1">
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
</SelectPrimitive.Content>
@ -57,10 +67,7 @@ const SelectLabel = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn(
"py-1.5 pr-2 pl-8 text-sm font-semibold text-slate-900 dark:text-slate-300",
className
)}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
@ -73,7 +80,7 @@ const SelectItem = React.forwardRef<
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pr-2 pl-8 text-sm font-medium outline-none focus:bg-slate-100 data-[disabled]:pointer-events-none data-[disabled]:opacity-50 dark:focus:bg-slate-700",
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
@ -95,7 +102,7 @@ const SelectSeparator = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-slate-100 dark:bg-slate-700", className)}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))

View file

@ -18,7 +18,7 @@ const Separator = React.forwardRef<
decorative={decorative}
orientation={orientation}
className={cn(
"bg-slate-200 dark:bg-slate-700",
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}

230
components/ui/sheet.tsx Normal file
View file

@ -0,0 +1,230 @@
"use client"
import * as React from "react"
import * as SheetPrimitive from "@radix-ui/react-dialog"
import { VariantProps, cva } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const Sheet = SheetPrimitive.Root
const SheetTrigger = SheetPrimitive.Trigger
const portalVariants = cva("fixed inset-0 z-50 flex", {
variants: {
position: {
top: "items-start",
bottom: "items-end",
left: "justify-start",
right: "justify-end",
},
},
defaultVariants: { position: "right" },
})
interface SheetPortalProps
extends SheetPrimitive.DialogPortalProps,
VariantProps<typeof portalVariants> {}
const SheetPortal = ({
position,
className,
children,
...props
}: SheetPortalProps) => (
<SheetPrimitive.Portal className={cn(className)} {...props}>
<div className={portalVariants({ position })}>{children}</div>
</SheetPrimitive.Portal>
)
SheetPortal.displayName = SheetPrimitive.Portal.displayName
const SheetOverlay = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Overlay>
>(({ className, children, ...props }, ref) => (
<SheetPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-background/80 backdrop-blur-sm transition-all duration-100 data-[state=closed]:animate-out data-[state=closed]:fade-out data-[state=open]:fade-in",
className
)}
{...props}
ref={ref}
/>
))
SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
const sheetVariants = cva(
"fixed z-50 scale-100 gap-4 bg-background p-6 opacity-100 shadow-lg border",
{
variants: {
position: {
top: "animate-in slide-in-from-top w-full duration-300",
bottom: "animate-in slide-in-from-bottom w-full duration-300",
left: "animate-in slide-in-from-left h-full duration-300",
right: "animate-in slide-in-from-right h-full duration-300",
},
size: {
content: "",
default: "",
sm: "",
lg: "",
xl: "",
full: "",
},
},
compoundVariants: [
{
position: ["top", "bottom"],
size: "content",
class: "max-h-screen",
},
{
position: ["top", "bottom"],
size: "default",
class: "h-1/3",
},
{
position: ["top", "bottom"],
size: "sm",
class: "h-1/4",
},
{
position: ["top", "bottom"],
size: "lg",
class: "h-1/2",
},
{
position: ["top", "bottom"],
size: "xl",
class: "h-5/6",
},
{
position: ["top", "bottom"],
size: "full",
class: "h-screen",
},
{
position: ["right", "left"],
size: "content",
class: "max-w-screen",
},
{
position: ["right", "left"],
size: "default",
class: "w-1/3",
},
{
position: ["right", "left"],
size: "sm",
class: "w-1/4",
},
{
position: ["right", "left"],
size: "lg",
class: "w-1/2",
},
{
position: ["right", "left"],
size: "xl",
class: "w-5/6",
},
{
position: ["right", "left"],
size: "full",
class: "w-screen",
},
],
defaultVariants: {
position: "right",
size: "default",
},
}
)
export interface DialogContentProps
extends React.ComponentPropsWithoutRef<typeof SheetPrimitive.Content>,
VariantProps<typeof sheetVariants> {}
const SheetContent = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Content>,
DialogContentProps
>(({ position, size, className, children, ...props }, ref) => (
<SheetPortal position={position}>
<SheetOverlay />
<SheetPrimitive.Content
ref={ref}
className={cn(sheetVariants({ position, size }), className)}
{...props}
>
{children}
<SheetPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-secondary">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
</SheetPrimitive.Content>
</SheetPortal>
))
SheetContent.displayName = SheetPrimitive.Content.displayName
const SheetHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
SheetHeader.displayName = "SheetHeader"
const SheetFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
SheetFooter.displayName = "SheetFooter"
const SheetTitle = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Title>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold text-foreground", className)}
{...props}
/>
))
SheetTitle.displayName = SheetPrimitive.Title.displayName
const SheetDescription = React.forwardRef<
React.ElementRef<typeof SheetPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof SheetPrimitive.Description>
>(({ className, ...props }, ref) => (
<SheetPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
SheetDescription.displayName = SheetPrimitive.Description.displayName
export {
Sheet,
SheetTrigger,
SheetContent,
SheetHeader,
SheetFooter,
SheetTitle,
SheetDescription,
}

View file

@ -1,15 +1,15 @@
import { cn } from "@/lib/utils"
interface SkeletonProps extends React.HTMLAttributes<HTMLDivElement> {}
export function Skeleton({ className, ...props }: SkeletonProps) {
function Skeleton({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"h-5 w-2/5 animate-pulse rounded-lg bg-slate-100",
className
)}
className={cn("animate-pulse rounded-md bg-muted", className)}
{...props}
/>
)
}
export { Skeleton }

View file

@ -17,10 +17,10 @@ const Slider = React.forwardRef<
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800">
<SliderPrimitive.Range className="absolute h-full bg-slate-900 dark:bg-slate-400" />
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-slate-900 bg-white transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:border-slate-100 dark:bg-slate-400 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900" />
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
))
Slider.displayName = SliderPrimitive.Root.displayName

View file

@ -11,7 +11,7 @@ const Switch = React.forwardRef<
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=unchecked]:bg-slate-200 data-[state=checked]:bg-slate-900 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=unchecked]:bg-slate-700 dark:data-[state=checked]:bg-slate-400",
"peer inline-flex h-[24px] w-[44px] shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
@ -19,7 +19,7 @@ const Switch = React.forwardRef<
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-white shadow-lg ring-0 transition-transform data-[state=unchecked]:translate-x-0 data-[state=checked]:translate-x-5"
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>

View file

@ -14,7 +14,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex items-center justify-center rounded-md bg-slate-100 p-1 dark:bg-slate-800",
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
className
)}
{...props}
@ -27,12 +27,12 @@ const TabsTrigger = React.forwardRef<
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex min-w-[100px] items-center justify-center rounded-[0.185rem] px-3 py-1.5 text-sm font-medium text-slate-700 transition-all disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-white data-[state=active]:text-slate-900 data-[state=active]:shadow-sm dark:text-slate-200 dark:data-[state=active]:bg-slate-900 dark:data-[state=active]:text-slate-100",
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
className
)}
{...props}
ref={ref}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
@ -42,12 +42,12 @@ const TabsContent = React.forwardRef<
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 rounded-md border border-slate-200 p-6 dark:border-slate-700",
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className
)}
{...props}
ref={ref}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName

View file

@ -10,7 +10,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex h-20 w-full rounded-md border border-slate-300 bg-transparent py-2 px-3 text-sm placeholder:text-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-slate-700 dark:text-slate-50 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900",
"flex h-20 w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}

View file

@ -14,7 +14,7 @@ const ToastViewport = React.forwardRef<
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:top-auto sm:bottom-0 sm:right-0 sm:flex-col md:max-w-[420px]",
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
@ -23,14 +23,13 @@ const ToastViewport = React.forwardRef<
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"data-[swipe=move]:transition-none grow-1 group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full mt-4 data-[state=closed]:slide-out-to-right-full dark:border-slate-700 last:mt-0 sm:last:mt-4",
"data-[swipe=move]:transition-none group relative pointer-events-auto flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full data-[state=closed]:slide-out-to-right-full",
{
variants: {
variant: {
default:
"bg-white border-slate-200 dark:bg-slate-800 dark:border-slate-700",
default: "bg-background border",
destructive:
"group destructive bg-red-600 text-white border-red-600 dark:border-red-600",
"group destructive border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
@ -61,7 +60,7 @@ const ToastAction = React.forwardRef<
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-slate-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-slate-100 focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-red-100 group-[.destructive]:hover:border-slate-50 group-[.destructive]:hover:bg-red-100 group-[.destructive]:hover:text-red-600 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:border-slate-700 dark:text-slate-100 dark:hover:bg-slate-700 dark:hover:text-slate-100 dark:focus:ring-slate-400 dark:focus:ring-offset-slate-900 dark:data-[state=open]:bg-slate-800",
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-destructive/30 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
@ -76,7 +75,7 @@ const ToastClose = React.forwardRef<
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute top-2 right-2 rounded-md p-1 text-slate-500 opacity-0 transition-opacity hover:text-slate-900 focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600 dark:hover:text-slate-50",
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""

View file

@ -1,7 +1,5 @@
"use client"
import { useToast } from "@/hooks/use-toast"
import {
Toast,
ToastClose,
@ -10,6 +8,7 @@ import {
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()

View file

@ -7,13 +7,13 @@ import { VariantProps, cva } from "class-variance-authority"
import { cn } from "@/lib/utils"
const toggleVariants = cva(
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-slate-200 dark:hover:bg-slate-800 dark:data-[state=on]:bg-slate-700 focus:outline-none dark:text-slate-100 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:focus:ring-offset-slate-900 hover:bg-slate-100 dark:hover:text-slate-100 dark:data-[state=on]:text-slate-100",
"inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background hover:bg-muted hover:text-muted-foreground",
{
variants: {
variant: {
default: "bg-transparent",
outline:
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-700",
"bg-transparent border border-input hover:bg-accent hover:text-accent-foreground",
},
size: {
default: "h-10 px-3",

View file

@ -7,8 +7,7 @@ import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
const Tooltip = ({ ...props }) => <TooltipPrimitive.Root {...props} />
Tooltip.displayName = TooltipPrimitive.Tooltip.displayName
const Tooltip = TooltipPrimitive.Root
const TooltipTrigger = TooltipPrimitive.Trigger
@ -20,7 +19,7 @@ const TooltipContent = React.forwardRef<
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border border-slate-100 bg-white px-3 py-1.5 text-sm text-slate-700 shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=top]:slide-in-from-bottom-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 dark:border-slate-800 dark:bg-slate-800 dark:text-slate-400",
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-50 data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1",
className
)}
{...props}

View file

@ -4,7 +4,7 @@ import * as React from "react"
import { ToastActionElement, type ToastProps } from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
@ -85,7 +85,7 @@ export const reducer = (state: State, action: Action): State => {
),
}
case "DISMISS_TOAST":
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
@ -109,6 +109,7 @@ export const reducer = (state: State, action: Action): State => {
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {

View file

@ -4,7 +4,6 @@ import Link from "next/link"
import { User } from "next-auth"
import { signOut } from "next-auth/react"
import { siteConfig } from "@/config/site"
import {
DropdownMenu,
DropdownMenuContent,
@ -32,7 +31,7 @@ export function UserAccountNav({ user }: UserAccountNavProps) {
<div className="flex flex-col space-y-1 leading-none">
{user.name && <p className="font-medium">{user.name}</p>}
{user.email && (
<p className="w-[200px] truncate text-sm text-slate-600">
<p className="w-[200px] truncate text-sm text-muted-foreground">
{user.email}
</p>
)}

View file

@ -2,7 +2,6 @@
import * as React from "react"
import { useSearchParams } from "next/navigation"
import { toast } from "@/hooks/use-toast"
import { zodResolver } from "@hookform/resolvers/zod"
import { signIn } from "next-auth/react"
import { useForm } from "react-hook-form"
@ -10,10 +9,11 @@ import * as z from "zod"
import { cn } from "@/lib/utils"
import { userAuthSchema } from "@/lib/validations/auth"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
interface UserAuthFormProps extends React.HTMLAttributes<HTMLDivElement> {}
@ -90,10 +90,12 @@ export function UserAuthForm({ className, ...props }: UserAuthFormProps) {
</form>
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t border-slate-300" />
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-white px-2 text-slate-600">Or continue with</span>
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<button

View file

@ -1,8 +1,8 @@
import { User } from "@prisma/client"
import { AvatarProps } from "@radix-ui/react-avatar"
import { Icons } from "@/components/icons"
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"
import { Icons } from "@/components/icons"
interface UserAvatarProps extends AvatarProps {
user: Pick<User, "image" | "name">

View file

@ -2,7 +2,6 @@
import * as React from "react"
import { useRouter } from "next/navigation"
import { toast } from "@/hooks/use-toast"
import { zodResolver } from "@hookform/resolvers/zod"
import { User } from "@prisma/client"
import { useForm } from "react-hook-form"
@ -10,11 +9,19 @@ import * as z from "zod"
import { cn } from "@/lib/utils"
import { userNameSchema } from "@/lib/validations/user"
import { Icons } from "@/components/icons"
import { buttonVariants } from "@/components/ui/button"
import { Card } from "@/components/ui/card"
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { toast } from "@/components/ui/use-toast"
import { Icons } from "@/components/icons"
interface UserNameFormProps extends React.HTMLAttributes<HTMLFormElement> {
user: Pick<User, "id" | "name">
@ -73,14 +80,14 @@ export function UserNameForm({ user, className, ...props }: UserNameFormProps) {
{...props}
>
<Card>
<Card.Header>
<Card.Title>Your Name</Card.Title>
<Card.Description>
<CardHeader>
<CardTitle>Your Name</CardTitle>
<CardDescription>
Please enter your full name or a display name you are comfortable
with.
</Card.Description>
</Card.Header>
<Card.Content>
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid gap-1">
<Label className="sr-only" htmlFor="name">
Name
@ -95,8 +102,8 @@ export function UserNameForm({ user, className, ...props }: UserNameFormProps) {
<p className="px-1 text-xs text-red-600">{errors.name.message}</p>
)}
</div>
</Card.Content>
<Card.Footer>
</CardContent>
<CardFooter>
<button
type="submit"
className={cn(buttonVariants(), className)}
@ -107,7 +114,7 @@ export function UserNameForm({ user, className, ...props }: UserNameFormProps) {
)}
<span>Save</span>
</button>
</Card.Footer>
</CardFooter>
</Card>
</form>
)

View file

@ -4,8 +4,7 @@ export const marketingConfig: MarketingConfig = {
mainNav: [
{
title: "Features",
href: "/features",
disabled: true,
href: "/#features",
},
{
title: "Pricing",
@ -19,10 +18,5 @@ export const marketingConfig: MarketingConfig = {
title: "Documentation",
href: "/docs",
},
{
title: "Contact",
href: "/contact",
disabled: true,
},
],
}

Some files were not shown because too many files have changed in this diff Show more