mirror of
https://github.com/shadcn-ui/taxonomy
synced 2026-05-24 09:48:32 +00:00
feat: update to Next 13.3
This commit is contained in:
parent
03ef292d26
commit
c606420fe7
124 changed files with 4791 additions and 4091 deletions
|
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
17
README.md
17
README.md
|
|
@ -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
|
||||
|
||||

|
||||
|
||||
## 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?
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 />
|
||||
|
|
|
|||
|
|
@ -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't have any posts yet. Start creating content.
|
||||
</EmptyPlaceholder.Description>
|
||||
<PostCreateButton
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"text-slate-900"
|
||||
)}
|
||||
/>
|
||||
<PostCreateButton variant="outline" />
|
||||
</EmptyPlaceholder>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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'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'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'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>
|
||||
</>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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'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't be charged.</strong>
|
||||
</p>
|
||||
|
|
|
|||
7
app/api/auth/[...nextauth]/route.ts
Normal file
7
app/api/auth/[...nextauth]/route.ts
Normal 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 }
|
||||
|
|
@ -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",
|
||||
93
app/api/posts/[postId]/route.ts
Normal file
93
app/api/posts/[postId]/route.ts
Normal 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
92
app/api/posts/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
50
app/api/users/[userId]/route.ts
Normal file
50
app/api/users/[userId]/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
61
app/api/users/stripe/route.ts
Normal file
61
app/api/users/stripe/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
|
|
@ -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 })
|
||||
}
|
||||
|
|
@ -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
BIN
app/opengraph-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
10
app/robots.ts
Normal file
10
app/robots.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { MetadataRoute } from "next"
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
},
|
||||
}
|
||||
}
|
||||
BIN
assets/fonts/CalSans-SemiBold.ttf
Normal file
BIN
assets/fonts/CalSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/CalSans-SemiBold.woff
Normal file
BIN
assets/fonts/CalSans-SemiBold.woff
Normal file
Binary file not shown.
BIN
assets/fonts/CalSans-SemiBold.woff2
Normal file
BIN
assets/fonts/CalSans-SemiBold.woff2
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
})}
|
||||
|
|
|
|||
17
components/card-skeleton.tsx
Normal file
17
components/card-skeleton.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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()} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
43
components/mode-toggle.tsx
Normal file
43
components/mode-toggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
|
|
@ -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"
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
9
components/theme-provider.tsx
Normal file
9
components/theme-provider.tsx
Normal 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>
|
||||
}
|
||||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
59
components/ui/alert.tsx
Normal 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 }
|
||||
|
|
@ -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
36
components/ui/badge.tsx
Normal 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 }
|
||||
|
|
@ -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",
|
||||
},
|
||||
},
|
||||
|
|
|
|||
64
components/ui/calendar.tsx
Normal file
64
components/ui/calendar.tsx
Normal 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 }
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
))
|
||||
|
|
|
|||
|
|
@ -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
230
components/ui/sheet.tsx
Normal 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,
|
||||
}
|
||||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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=""
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in a new issue