├── .nvmrc ├── middleware.ts ├── app ├── api │ ├── auth │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── user │ │ └── route.ts │ └── webhooks │ │ └── stripe │ │ └── route.ts ├── opengraph-image.jpg ├── robots.ts ├── (protected) │ ├── dashboard │ │ ├── loading.tsx │ │ ├── settings │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── billing │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── charts │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ └── page.tsx │ ├── admin │ │ ├── orders │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ └── layout.tsx ├── (marketing) │ ├── blog │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── category │ │ │ └── [slug] │ │ │ └── page.tsx │ ├── error.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── not-found.tsx │ ├── pricing │ │ ├── loading.tsx │ │ └── page.tsx │ └── [slug] │ │ └── page.tsx ├── (auth) │ ├── layout.tsx │ ├── login │ │ └── page.tsx │ └── register │ │ └── page.tsx ├── (docs) │ ├── layout.tsx │ ├── docs │ │ ├── layout.tsx │ │ └── [[...slug]] │ │ │ └── page.tsx │ └── guides │ │ ├── page.tsx │ │ └── [slug] │ │ └── page.tsx └── layout.tsx ├── .prettierignore ├── .commitlintrc.json ├── public ├── favicon.ico ├── _static │ ├── og.jpg │ ├── avatars │ │ ├── shadcn.jpeg │ │ └── mickasmt.png │ ├── blog │ │ ├── blog-post-1.jpg │ │ ├── blog-post-2.jpg │ │ ├── blog-post-3.jpg │ │ └── blog-post-4.jpg │ ├── docs │ │ └── gg-auth-config.jpg │ ├── favicons │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── apple-touch-icon.png │ │ ├── android-chrome-192x192.png │ │ └── android-chrome-512x512.png │ └── illustrations │ │ └── work-from-home.jpg └── site.webmanifest ├── .husky ├── commit-msg └── pre-commit ├── assets └── fonts │ ├── GeistVF.woff2 │ ├── Inter-Bold.ttf │ ├── Inter-Regular.ttf │ ├── CalSans-SemiBold.ttf │ ├── CalSans-SemiBold.woff2 │ └── index.ts ├── postcss.config.js ├── lib ├── validations │ ├── auth.ts │ ├── og.ts │ └── user.ts ├── exceptions.ts ├── stripe.ts ├── session.ts ├── db.ts ├── user.ts ├── email.ts ├── toc.ts └── subscription.ts ├── components ├── analytics.tsx ├── ui │ ├── aspect-ratio.tsx │ ├── skeleton.tsx │ ├── collapsible.tsx │ ├── label.tsx │ ├── textarea.tsx │ ├── separator.tsx │ ├── progress.tsx │ ├── input.tsx │ ├── toaster.tsx │ ├── hover-card.tsx │ ├── sonner.tsx │ ├── checkbox.tsx │ ├── popover.tsx │ ├── slider.tsx │ ├── tooltip.tsx │ ├── badge.tsx │ ├── switch.tsx │ ├── avatar.tsx │ ├── radio-group.tsx │ ├── toggle.tsx │ ├── scroll-area.tsx │ ├── alert.tsx │ ├── toggle-group.tsx │ ├── tabs.tsx │ ├── card.tsx │ ├── button.tsx │ ├── accordion.tsx │ └── calendar.tsx ├── shared │ ├── max-width-wrapper.tsx │ ├── card-skeleton.tsx │ ├── blur-image.tsx │ ├── header-section.tsx │ ├── user-avatar.tsx │ ├── section-skeleton.tsx │ ├── copy-button.tsx │ ├── callout.tsx │ └── empty-placeholder.tsx ├── dashboard │ ├── header.tsx │ ├── info-card.tsx │ ├── section-columns.tsx │ ├── upgrade-card.tsx │ └── delete-account.tsx ├── content │ ├── blog-posts.tsx │ ├── mdx-card.tsx │ ├── author.tsx │ └── blog-card.tsx ├── tailwind-indicator.tsx ├── modals │ ├── providers.tsx │ └── sign-in-modal.tsx ├── sections │ ├── preview-landing.tsx │ ├── testimonials.tsx │ └── info-landing.tsx ├── forms │ ├── customer-portal-button.tsx │ ├── billing-form-button.tsx │ └── newsletter-form.tsx ├── docs │ ├── page-header.tsx │ ├── search.tsx │ ├── pager.tsx │ └── sidebar-nav.tsx ├── layout │ └── mode-toggle.tsx ├── pricing │ ├── billing-info.tsx │ └── pricing-faq.tsx └── charts │ ├── radar-chart-simple.tsx │ └── radial-chart-grid.tsx ├── hooks ├── use-mounted.ts ├── use-lock-body.ts ├── use-scroll.ts ├── use-local-storage.ts ├── use-intersection-observer.ts └── use-media-query.ts ├── content ├── docs │ ├── in-progress.mdx │ ├── index.mdx │ ├── configuration │ │ ├── database.mdx │ │ ├── email.mdx │ │ ├── config-files.mdx │ │ ├── authentification.mdx │ │ └── layouts.mdx │ └── installation.mdx └── pages │ ├── privacy.mdx │ └── terms.mdx ├── config ├── marketing.ts ├── blog.ts ├── site.ts ├── dashboard.ts └── docs.ts ├── components.json ├── types ├── next-auth.d.ts └── index.d.ts ├── auth.config.ts ├── .gitignore ├── next.config.js ├── .eslintrc.json ├── actions ├── update-user-name.ts ├── update-user-role.ts ├── open-customer-portal.ts └── generate-user-stripe.ts ├── tsconfig.json ├── prettier.config.js ├── LICENSE.md ├── styles ├── mdx.css └── globals.css ├── .env.example ├── auth.ts ├── env.mjs ├── emails └── magic-link-email.tsx └── prisma ├── schema.prisma └── migrations └── 0_init └── migration.sql /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | export { auth as middleware} from "@/auth" -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | export { GET, POST } from "@/auth" -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | .contentlayer -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-conventional"] 3 | } 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | . "$(dirname -- "$0")/_/husky.sh" 3 | 4 | npx pretty-quick --staged 5 | -------------------------------------------------------------------------------- /public/_static/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/og.jpg -------------------------------------------------------------------------------- /app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/app/opengraph-image.jpg -------------------------------------------------------------------------------- /assets/fonts/GeistVF.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/assets/fonts/GeistVF.woff2 -------------------------------------------------------------------------------- /assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/CalSans-SemiBold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/assets/fonts/CalSans-SemiBold.ttf -------------------------------------------------------------------------------- /public/_static/avatars/shadcn.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/avatars/shadcn.jpeg -------------------------------------------------------------------------------- /assets/fonts/CalSans-SemiBold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/assets/fonts/CalSans-SemiBold.woff2 -------------------------------------------------------------------------------- /lib/validations/auth.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const userAuthSchema = z.object({ 4 | email: z.string().email(), 5 | }) 6 | -------------------------------------------------------------------------------- /public/_static/avatars/mickasmt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/avatars/mickasmt.png -------------------------------------------------------------------------------- /public/_static/blog/blog-post-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/blog/blog-post-1.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/blog/blog-post-2.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/blog/blog-post-3.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/blog/blog-post-4.jpg -------------------------------------------------------------------------------- /public/_static/docs/gg-auth-config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/docs/gg-auth-config.jpg -------------------------------------------------------------------------------- /public/_static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/_static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/_static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/_static/illustrations/work-from-home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/illustrations/work-from-home.jpg -------------------------------------------------------------------------------- /lib/exceptions.ts: -------------------------------------------------------------------------------- 1 | export class RequiresProPlanError extends Error { 2 | constructor(message = "This action requires a pro plan") { 3 | super(message) 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /public/_static/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/_static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mickasmt/next-saas-stripe-starter/HEAD/public/_static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /components/analytics.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Analytics as VercelAnalytics } from "@vercel/analytics/react" 4 | 5 | export function Analytics() { 6 | return 7 | } 8 | -------------------------------------------------------------------------------- /components/ui/aspect-ratio.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" 4 | 5 | const AspectRatio = AspectRatioPrimitive.Root 6 | 7 | export { AspectRatio } 8 | -------------------------------------------------------------------------------- /lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe" 2 | 3 | import { env } from "@/env.mjs" 4 | 5 | export const stripe = new Stripe(env.STRIPE_API_KEY, { 6 | apiVersion: "2024-04-10", 7 | typescript: true, 8 | }) 9 | -------------------------------------------------------------------------------- /lib/validations/og.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const ogImageSchema = z.object({ 4 | heading: z.string(), 5 | type: z.string(), 6 | mode: z.enum(["light", "dark"]).default("dark"), 7 | }) 8 | -------------------------------------------------------------------------------- /app/robots.ts: -------------------------------------------------------------------------------- 1 | import { MetadataRoute } from "next" 2 | 3 | export default function robots(): MetadataRoute.Robots { 4 | return { 5 | rules: { 6 | userAgent: "*", 7 | allow: "/", 8 | }, 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /hooks/use-mounted.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | export function useMounted() { 4 | const [mounted, setMounted] = React.useState(false) 5 | 6 | React.useEffect(() => { 7 | setMounted(true) 8 | }, []) 9 | 10 | return mounted 11 | } 12 | -------------------------------------------------------------------------------- /lib/validations/user.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@prisma/client"; 2 | import * as z from "zod"; 3 | 4 | export const userNameSchema = z.object({ 5 | name: z.string().min(3).max(32), 6 | }); 7 | 8 | export const userRoleSchema = z.object({ 9 | role: z.nativeEnum(UserRole), 10 | }); 11 | -------------------------------------------------------------------------------- /lib/session.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { cache } from "react"; 4 | import { auth } from "@/auth"; 5 | 6 | export const getCurrentUser = cache(async () => { 7 | const session = await auth(); 8 | if (!session?.user) { 9 | return undefined; 10 | } 11 | return session.user; 12 | }); -------------------------------------------------------------------------------- /content/docs/in-progress.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Not Implemented 3 | description: This page is in progress. 4 | --- 5 | 6 | 7 | 8 | This site is a work in progress. If you see dummy text on a page, it means I'm still working on it. You can follow updates on Twitter [@shadcn](https://twitter.com/shadcn). 9 | 10 | 11 | -------------------------------------------------------------------------------- /components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ 4 | className, 5 | ...props 6 | }: React.HTMLAttributes) { 7 | return ( 8 |
12 | ) 13 | } 14 | 15 | export { Skeleton } 16 | -------------------------------------------------------------------------------- /config/marketing.ts: -------------------------------------------------------------------------------- 1 | import { MarketingConfig } from "types" 2 | 3 | export const marketingConfig: MarketingConfig = { 4 | mainNav: [ 5 | { 6 | title: "Pricing", 7 | href: "/pricing", 8 | }, 9 | { 10 | title: "Blog", 11 | href: "/blog", 12 | }, 13 | { 14 | title: "Documentation", 15 | href: "/docs", 16 | }, 17 | ], 18 | } 19 | -------------------------------------------------------------------------------- /app/(protected)/dashboard/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { DashboardHeader } from "@/components/dashboard/header"; 3 | 4 | export default function DashboardLoading() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | const Collapsible = CollapsiblePrimitive.Root 6 | 7 | const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger 8 | 9 | const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent 10 | 11 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 12 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { UserRole } from "@prisma/client"; 2 | import { User } from "next-auth"; 3 | import { JWT } from "next-auth/jwt"; 4 | 5 | export type ExtendedUser = User & { 6 | role: UserRole; 7 | }; 8 | 9 | declare module "next-auth/jwt" { 10 | interface JWT { 11 | role: UserRole; 12 | } 13 | } 14 | 15 | declare module "next-auth" { 16 | interface Session { 17 | user: ExtendedUser; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /hooks/use-lock-body.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | // @see https://usehooks.com/useLockBodyScroll. 4 | export function useLockBody() { 5 | React.useLayoutEffect((): (() => void) => { 6 | const originalStyle: string = window.getComputedStyle( 7 | document.body 8 | ).overflow 9 | document.body.style.overflow = "hidden" 10 | return () => (document.body.style.overflow = originalStyle) 11 | }, []) 12 | } 13 | -------------------------------------------------------------------------------- /app/(protected)/admin/orders/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { DashboardHeader } from "@/components/dashboard/header"; 3 | 4 | export default function OrdersLoading() { 5 | return ( 6 | <> 7 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /app/(marketing)/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import { BlogHeaderLayout } from "@/components/content/blog-header-layout"; 2 | import MaxWidthWrapper from "@/components/shared/max-width-wrapper"; 3 | 4 | export default function BlogLayout({ 5 | children, 6 | }: { 7 | children: React.ReactNode; 8 | }) { 9 | return ( 10 | <> 11 | 12 | {children} 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /app/(protected)/admin/layout.tsx: -------------------------------------------------------------------------------- 1 | import { notFound, redirect } from "next/navigation"; 2 | 3 | import { getCurrentUser } from "@/lib/session"; 4 | 5 | interface ProtectedLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default async function Dashboard({ children }: ProtectedLayoutProps) { 10 | const user = await getCurrentUser(); 11 | if (!user || user.role !== "ADMIN") redirect("/login"); 12 | 13 | return <>{children}; 14 | } 15 | -------------------------------------------------------------------------------- /lib/db.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | import "server-only"; 3 | 4 | declare global { 5 | // eslint-disable-next-line no-var 6 | var cachedPrisma: PrismaClient 7 | } 8 | 9 | export let prisma: PrismaClient 10 | if (process.env.NODE_ENV === "production") { 11 | prisma = new PrismaClient() 12 | } else { 13 | if (!global.cachedPrisma) { 14 | global.cachedPrisma = new PrismaClient() 15 | } 16 | prisma = global.cachedPrisma 17 | } 18 | -------------------------------------------------------------------------------- /public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SaaS Starter", 3 | "short_name": "SaaS Starter", 4 | "icons": [ 5 | { 6 | "src": "./favicons/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "./favicons/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#ffffff", 17 | "background_color": "#ffffff", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import { getCurrentUser } from "@/lib/session"; 4 | 5 | interface AuthLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default async function AuthLayout({ children }: AuthLayoutProps) { 10 | const user = await getCurrentUser(); 11 | 12 | if (user) { 13 | if (user.role === "ADMIN") redirect("/admin"); 14 | redirect("/dashboard"); 15 | } 16 | 17 | return
{children}
; 18 | } 19 | -------------------------------------------------------------------------------- /hooks/use-scroll.ts: -------------------------------------------------------------------------------- 1 | import { useCallback, useEffect, useState } from "react"; 2 | 3 | export function useScroll(threshold: number) { 4 | const [scrolled, setScrolled] = useState(false); 5 | 6 | const onScroll = useCallback(() => { 7 | setScrolled(window.pageYOffset > threshold); 8 | }, [threshold]); 9 | 10 | useEffect(() => { 11 | window.addEventListener("scroll", onScroll); 12 | return () => window.removeEventListener("scroll", onScroll); 13 | }, [onScroll]); 14 | 15 | return scrolled; 16 | } 17 | -------------------------------------------------------------------------------- /components/shared/max-width-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { ReactNode } from "react"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | export default function MaxWidthWrapper({ 6 | className, 7 | children, 8 | large = false, 9 | }: { 10 | className?: string; 11 | large?: boolean; 12 | children: ReactNode; 13 | }) { 14 | return ( 15 |
22 | {children} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/(marketing)/error.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Button } from '@/components/ui/button'; 4 | 5 | export default function Error({ 6 | reset, 7 | }: { 8 | reset: () => void; 9 | }) { 10 | 11 | return ( 12 |
13 |

Something went wrong!

14 | 21 |
22 | ); 23 | } -------------------------------------------------------------------------------- /assets/fonts/index.ts: -------------------------------------------------------------------------------- 1 | import localFont from "next/font/local"; 2 | import { Inter as FontSans, Urbanist } from "next/font/google"; 3 | 4 | export const fontSans = FontSans({ 5 | subsets: ["latin"], 6 | variable: "--font-sans", 7 | }) 8 | 9 | export const fontUrban = Urbanist({ 10 | subsets: ["latin"], 11 | variable: "--font-urban", 12 | }) 13 | 14 | export const fontHeading = localFont({ 15 | src: "./CalSans-SemiBold.woff2", 16 | variable: "--font-heading", 17 | }) 18 | 19 | export const fontGeist = localFont({ 20 | src: "./GeistVF.woff2", 21 | variable: "--font-geist", 22 | }) 23 | -------------------------------------------------------------------------------- /app/(protected)/dashboard/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { DashboardHeader } from "@/components/dashboard/header"; 2 | import { SkeletonSection } from "@/components/shared/section-skeleton"; 3 | 4 | export default function DashboardSettingsLoading() { 5 | return ( 6 | <> 7 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/dashboard/header.tsx: -------------------------------------------------------------------------------- 1 | interface DashboardHeaderProps { 2 | heading: string; 3 | text?: string; 4 | children?: React.ReactNode; 5 | } 6 | 7 | export function DashboardHeader({ 8 | heading, 9 | text, 10 | children, 11 | }: DashboardHeaderProps) { 12 | return ( 13 |
14 |
15 |

{heading}

16 | {text &&

{text}

} 17 |
18 | {children} 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /app/(marketing)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NavBar } from "@/components/layout/navbar"; 2 | import { SiteFooter } from "@/components/layout/site-footer"; 3 | import { NavMobile } from "@/components/layout/mobile-nav"; 4 | 5 | interface MarketingLayoutProps { 6 | children: React.ReactNode; 7 | } 8 | 9 | export default function MarketingLayout({ children }: MarketingLayoutProps) { 10 | return ( 11 |
12 | 13 | 14 |
{children}
15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /components/shared/card-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardContent, 4 | CardFooter, 5 | CardHeader, 6 | } from "@/components/ui/card"; 7 | import { Skeleton } from "@/components/ui/skeleton"; 8 | 9 | export function CardSkeleton() { 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /content/docs/index.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Introduction 3 | description: Welcome to the Next SaaS Stripe Starter documentation. 4 | --- 5 | 6 | Next SaaS Stripe Starter is Open Source Boilerplate. 7 | 8 | Built on the [Taxonomy](https://github.com/shadcn-ui/taxonomy) app by shadcn, it integrates Next.js 14, Prisma, Neon, Auth.js v5, Resend, React Email, Shadcn/ui, and Stripe. 9 | 10 | With Next SaaS Stripe Starter, you get a solid foundation for your SaaS journey. 11 | 12 | This documentation is your go-to resource for configuring and using the starter effectively. 13 | 14 | Let's get started and happy coding! 15 | -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | import Google from "next-auth/providers/google"; 3 | import Resend from "next-auth/providers/resend"; 4 | 5 | import { env } from "@/env.mjs"; 6 | import { sendVerificationRequest } from "@/lib/email"; 7 | 8 | export default { 9 | providers: [ 10 | Google({ 11 | clientId: env.GOOGLE_CLIENT_ID, 12 | clientSecret: env.GOOGLE_CLIENT_SECRET, 13 | }), 14 | Resend({ 15 | apiKey: env.RESEND_API_KEY, 16 | from: env.EMAIL_FROM, 17 | // sendVerificationRequest, 18 | }), 19 | ], 20 | } satisfies NextAuthConfig; 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # email 40 | /.react-email/ 41 | 42 | .vscode 43 | .contentlayer -------------------------------------------------------------------------------- /app/(protected)/dashboard/billing/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton"; 2 | import { DashboardHeader } from "@/components/dashboard/header"; 3 | import { CardSkeleton } from "@/components/shared/card-skeleton"; 4 | 5 | export default function DashboardBillingLoading() { 6 | return ( 7 | <> 8 | 12 |
13 | 14 | 15 |
16 | 17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /lib/user.ts: -------------------------------------------------------------------------------- 1 | import { prisma } from "@/lib/db"; 2 | 3 | export const getUserByEmail = async (email: string) => { 4 | try { 5 | const user = await prisma.user.findUnique({ 6 | where: { 7 | email: email, 8 | }, 9 | select: { 10 | name: true, 11 | emailVerified: true, 12 | }, 13 | }); 14 | 15 | return user; 16 | } catch { 17 | return null; 18 | } 19 | }; 20 | 21 | export const getUserById = async (id: string) => { 22 | try { 23 | const user = await prisma.user.findUnique({ where: { id } }); 24 | 25 | return user; 26 | } catch { 27 | return null; 28 | } 29 | }; -------------------------------------------------------------------------------- /components/content/blog-posts.tsx: -------------------------------------------------------------------------------- 1 | import { Post } from "@/.contentlayer/generated"; 2 | 3 | import { BlogCard } from "./blog-card"; 4 | 5 | export function BlogPosts({ 6 | posts, 7 | }: { 8 | posts: (Post & { 9 | blurDataURL: string; 10 | })[]; 11 | }) { 12 | return ( 13 |
14 | 15 | 16 |
17 | {posts.slice(1).map((post, idx) => ( 18 | 19 | ))} 20 |
21 |
22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/shared/blur-image.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useState } from "react"; 4 | import type { ComponentProps } from "react"; 5 | import Image from "next/image"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | export default function BlurImage(props: ComponentProps) { 10 | const [isLoading, setLoading] = useState(true); 11 | 12 | return ( 13 | {props.alt} setLoading(false)} 22 | /> 23 | ); 24 | } 25 | -------------------------------------------------------------------------------- /components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null 3 | 4 | return ( 5 |
6 |
xs
7 |
sm
8 |
md
9 |
lg
10 |
xl
11 |
2xl
12 |
13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /app/api/user/route.ts: -------------------------------------------------------------------------------- 1 | import { auth } from "@/auth"; 2 | 3 | import { prisma } from "@/lib/db"; 4 | 5 | export const DELETE = auth(async (req) => { 6 | if (!req.auth) { 7 | return new Response("Not authenticated", { status: 401 }); 8 | } 9 | 10 | const currentUser = req.auth.user; 11 | if (!currentUser) { 12 | return new Response("Invalid user", { status: 401 }); 13 | } 14 | 15 | try { 16 | await prisma.user.delete({ 17 | where: { 18 | id: currentUser.id, 19 | }, 20 | }); 21 | } catch (error) { 22 | return new Response("Internal server error", { status: 500 }); 23 | } 24 | 25 | return new Response("User deleted successfully!", { status: 200 }); 26 | }); 27 | -------------------------------------------------------------------------------- /app/(docs)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { NavMobile } from "@/components/layout/mobile-nav"; 2 | import { NavBar } from "@/components/layout/navbar"; 3 | import { SiteFooter } from "@/components/layout/site-footer"; 4 | import MaxWidthWrapper from "@/components/shared/max-width-wrapper"; 5 | 6 | interface DocsLayoutProps { 7 | children: React.ReactNode; 8 | } 9 | 10 | export default function DocsLayout({ children }: DocsLayoutProps) { 11 | return ( 12 |
13 | 14 | 15 | 16 | {children} 17 | 18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /config/blog.ts: -------------------------------------------------------------------------------- 1 | export const BLOG_CATEGORIES: { 2 | title: string; 3 | slug: "news" | "education"; 4 | description: string; 5 | }[] = [ 6 | { 7 | title: "News", 8 | slug: "news", 9 | description: "Updates and announcements from Next SaaS Starter.", 10 | }, 11 | { 12 | title: "Education", 13 | slug: "education", 14 | description: "Educational content about SaaS management.", 15 | }, 16 | ]; 17 | 18 | export const BLOG_AUTHORS = { 19 | mickasmt: { 20 | name: "mickasmt", 21 | image: "/_static/avatars/mickasmt.png", 22 | twitter: "miickasmt", 23 | }, 24 | shadcn: { 25 | name: "shadcn", 26 | image: "/_static/avatars/shadcn.jpeg", 27 | twitter: "shadcn", 28 | }, 29 | }; 30 | -------------------------------------------------------------------------------- /components/dashboard/info-card.tsx: -------------------------------------------------------------------------------- 1 | import { Users } from "lucide-react" 2 | 3 | import { 4 | Card, 5 | CardContent, 6 | CardHeader, 7 | CardTitle, 8 | } from "@/components/ui/card" 9 | 10 | export default function InfoCard() { 11 | return ( 12 | 13 | 14 | Subscriptions 15 | 16 | 17 | 18 |
+2350
19 |

+180.1% from last month

20 |
21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /components/modals/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, Dispatch, ReactNode, SetStateAction } from "react"; 4 | 5 | import { useSignInModal } from "@/components/modals//sign-in-modal"; 6 | 7 | export const ModalContext = createContext<{ 8 | setShowSignInModal: Dispatch>; 9 | }>({ 10 | setShowSignInModal: () => {}, 11 | }); 12 | 13 | export default function ModalProvider({ children }: { children: ReactNode }) { 14 | const { SignInModal, setShowSignInModal } = useSignInModal(); 15 | 16 | return ( 17 | 22 | 23 | {children} 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/dashboard/section-columns.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | interface SectionColumnsType { 4 | title: string; 5 | description?: string; 6 | children: React.ReactNode; 7 | } 8 | 9 | export function SectionColumns({ 10 | title, 11 | description, 12 | children, 13 | }: SectionColumnsType) { 14 | return ( 15 |
16 |
17 |

{title}

18 |

19 | {description} 20 |

21 |
22 |
{children}
23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /hooks/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | const useLocalStorage = ( 4 | key: string, 5 | initialValue: T, 6 | ): [T, (value: T) => void] => { 7 | const [storedValue, setStoredValue] = useState(initialValue); 8 | 9 | useEffect(() => { 10 | // Retrieve from localStorage 11 | const item = window.localStorage.getItem(key); 12 | if (item) { 13 | setStoredValue(JSON.parse(item)); 14 | } 15 | }, [key]); 16 | 17 | const setValue = (value: T) => { 18 | // Save state 19 | setStoredValue(value); 20 | // Save to localStorage 21 | window.localStorage.setItem(key, JSON.stringify(value)); 22 | }; 23 | return [storedValue, setValue]; 24 | }; 25 | 26 | export default useLocalStorage; 27 | -------------------------------------------------------------------------------- /app/(docs)/docs/layout.tsx: -------------------------------------------------------------------------------- 1 | import { ScrollArea } from "@/components/ui/scroll-area"; 2 | import { DocsSidebarNav } from "@/components/docs/sidebar-nav"; 3 | 4 | interface DocsLayoutProps { 5 | children: React.ReactNode; 6 | } 7 | 8 | export default function DocsLayout({ children }: DocsLayoutProps) { 9 | return ( 10 |
11 | 16 | {children} 17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | const { withContentlayer } = require("next-contentlayer2"); 2 | 3 | import("./env.mjs"); 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | reactStrictMode: true, 8 | swcMinify: true, 9 | images: { 10 | remotePatterns: [ 11 | { 12 | protocol: "https", 13 | hostname: "avatars.githubusercontent.com", 14 | }, 15 | { 16 | protocol: "https", 17 | hostname: "lh3.googleusercontent.com", 18 | }, 19 | { 20 | protocol: "https", 21 | hostname: "randomuser.me", 22 | }, 23 | ], 24 | }, 25 | experimental: { 26 | serverComponentsExternalPackages: ["@prisma/client"], 27 | }, 28 | }; 29 | 30 | module.exports = withContentlayer(nextConfig); 31 | -------------------------------------------------------------------------------- /components/shared/header-section.tsx: -------------------------------------------------------------------------------- 1 | interface HeaderSectionProps { 2 | label?: string; 3 | title: string; 4 | subtitle?: string; 5 | } 6 | 7 | export function HeaderSection({ label, title, subtitle }: HeaderSectionProps) { 8 | return ( 9 |
10 | {label ? ( 11 |
12 | {label} 13 |
14 | ) : null} 15 |

16 | {title} 17 |

18 | {subtitle ? ( 19 |

20 | {subtitle} 21 |

22 | ) : null} 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/(marketing)/blog/page.tsx: -------------------------------------------------------------------------------- 1 | import { allPosts } from "contentlayer/generated"; 2 | 3 | import { constructMetadata, getBlurDataURL } from "@/lib/utils"; 4 | import { BlogPosts } from "@/components/content/blog-posts"; 5 | 6 | export const metadata = constructMetadata({ 7 | title: "Blog – SaaS Starter", 8 | description: "Latest news and updates from Next SaaS Starter.", 9 | }); 10 | 11 | export default async function BlogPage() { 12 | const posts = await Promise.all( 13 | allPosts 14 | .filter((post) => post.published) 15 | .sort((a, b) => b.date.localeCompare(a.date)) 16 | .map(async (post) => ({ 17 | ...post, 18 | blurDataURL: await getBlurDataURL(post.image), 19 | })), 20 | ); 21 | 22 | return ; 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/eslintrc", 3 | "root": true, 4 | "extends": [ 5 | "next/core-web-vitals", 6 | "prettier", 7 | "plugin:tailwindcss/recommended" 8 | ], 9 | "plugins": ["tailwindcss"], 10 | "rules": { 11 | "@next/next/no-html-link-for-pages": "off", 12 | "react/jsx-key": "off", 13 | "tailwindcss/no-custom-classname": "off", 14 | "tailwindcss/classnames-order": "error" 15 | }, 16 | "settings": { 17 | "tailwindcss": { 18 | "callees": ["cn"], 19 | "config": "tailwind.config.ts" 20 | }, 21 | "next": { 22 | "rootDir": true 23 | } 24 | }, 25 | "overrides": [ 26 | { 27 | "files": ["*.ts", "*.tsx"], 28 | "parser": "@typescript-eslint/parser" 29 | } 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /components/shared/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client" 2 | import { AvatarProps } from "@radix-ui/react-avatar" 3 | 4 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 5 | import { Icons } from "@/components/shared/icons" 6 | 7 | interface UserAvatarProps extends AvatarProps { 8 | user: Pick 9 | } 10 | 11 | export function UserAvatar({ user, ...props }: UserAvatarProps) { 12 | return ( 13 | 14 | {user.image ? ( 15 | 16 | ) : ( 17 | 18 | {user.name} 19 | 20 | 21 | )} 22 | 23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /components/dashboard/upgrade-card.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardHeader, 7 | CardTitle, 8 | } from "@/components/ui/card"; 9 | 10 | export function UpgradeCard() { 11 | return ( 12 | 13 | 14 | Upgrade to Pro 15 | 16 | Unlock all features and get unlimited access to our support team. 17 | 18 | 19 | 20 | 23 | 24 | 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { VariantProps, cva } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /app/(marketing)/page.tsx: -------------------------------------------------------------------------------- 1 | import { infos } from "@/config/landing"; 2 | import BentoGrid from "@/components/sections/bentogrid"; 3 | import Features from "@/components/sections/features"; 4 | import HeroLanding from "@/components/sections/hero-landing"; 5 | import InfoLanding from "@/components/sections/info-landing"; 6 | import Powered from "@/components/sections/powered"; 7 | import PreviewLanding from "@/components/sections/preview-landing"; 8 | import Testimonials from "@/components/sections/testimonials"; 9 | 10 | export default function IndexPage() { 11 | return ( 12 | <> 13 | 14 | 15 | 16 | 17 | 18 | {/* */} 19 | 20 | 21 | 22 | ); 23 | } 24 | -------------------------------------------------------------------------------- /components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface TextareaProps 6 | extends React.TextareaHTMLAttributes {} 7 | 8 | const Textarea = React.forwardRef( 9 | ({ className, ...props }, ref) => { 10 | return ( 11 |