├── .nvmrc ├── set-random-value.js ├── 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 ├── (auth) │ ├── layout.tsx │ ├── signin │ │ └── page.tsx │ ├── test │ │ └── page.tsx │ ├── forgot-password │ │ └── page.tsx │ └── signup │ │ └── address │ │ └── page.tsx ├── (marketing) │ ├── error.tsx │ ├── layout.tsx │ ├── page.tsx │ ├── not-found.tsx │ ├── pricing │ │ ├── loading.tsx │ │ └── page.tsx │ └── [slug] │ │ └── page.tsx └── layout.tsx ├── .prettierignore ├── .commitlintrc.json ├── public ├── logo.png ├── favicon.ico ├── _static │ ├── og.jpg │ ├── modal.png │ ├── landing.png │ ├── summery.png │ ├── avatars │ │ ├── mickasmt.png │ │ └── shadcn.jpeg │ ├── 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 ├── postcss.config.js ├── assets └── fonts │ ├── Inter-Bold.ttf │ ├── Inter-Regular.ttf │ ├── GT-Walsheim-Bold.ttf │ ├── GT-Walsheim-Medium.ttf │ ├── GT-Walsheim-Regular.ttf │ └── index.ts ├── lib ├── exceptions.ts ├── validations │ ├── auth.ts │ ├── og.ts │ └── user.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 │ └── modal.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 │ └── search-command.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 │ ├── features.tsx │ └── hero-landing.tsx ├── forms │ ├── customer-portal-button.tsx │ ├── billing-form-button.tsx │ └── newsletter-form.tsx ├── layout │ ├── mode-toggle.tsx │ └── site-footer.tsx ├── pricing │ ├── billing-info.tsx │ └── pricing-faq.tsx └── charts │ ├── radar-chart-simple.tsx │ ├── radial-chart-grid.tsx │ ├── line-chart-multiple.tsx │ ├── bar-chart-mixed.tsx │ └── area-chart-stacked.tsx ├── hooks ├── use-mounted.ts ├── use-lock-body.ts ├── use-scroll.ts ├── use-local-storage.ts ├── use-intersection-observer.ts └── use-media-query.ts ├── components.json ├── types ├── next-auth.d.ts └── index.d.ts ├── config ├── marketing.ts ├── blog.ts ├── site.ts ├── dashboard.ts └── docs.ts ├── .gitignore ├── actions ├── getStripe.ts ├── update-user-name.ts ├── update-user-role.ts ├── open-customer-portal.ts ├── generate-stripe.ts └── generate-user-stripe.ts ├── next.config.js ├── .eslintrc.json ├── tsconfig.json ├── prettier.config.js ├── auth.config.ts ├── LICENSE.md ├── styles ├── mdx.css └── globals.css ├── .env.example ├── auth.ts ├── emails └── magic-link-email.tsx ├── prisma ├── schema.prisma └── migrations │ └── 0_init │ └── migration.sql └── env.mjs /.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /set-random-value.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/logo.png -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/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 | -------------------------------------------------------------------------------- /app/opengraph-image.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/app/opengraph-image.jpg -------------------------------------------------------------------------------- /public/_static/og.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/og.jpg -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/_static/modal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/modal.png -------------------------------------------------------------------------------- /assets/fonts/Inter-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/assets/fonts/Inter-Bold.ttf -------------------------------------------------------------------------------- /public/_static/landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/landing.png -------------------------------------------------------------------------------- /public/_static/summery.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/summery.png -------------------------------------------------------------------------------- /assets/fonts/Inter-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/assets/fonts/Inter-Regular.ttf -------------------------------------------------------------------------------- /assets/fonts/GT-Walsheim-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/assets/fonts/GT-Walsheim-Bold.ttf -------------------------------------------------------------------------------- /assets/fonts/GT-Walsheim-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/assets/fonts/GT-Walsheim-Medium.ttf -------------------------------------------------------------------------------- /public/_static/avatars/mickasmt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/avatars/mickasmt.png -------------------------------------------------------------------------------- /public/_static/avatars/shadcn.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/avatars/shadcn.jpeg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/blog/blog-post-1.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/blog/blog-post-2.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/blog/blog-post-3.jpg -------------------------------------------------------------------------------- /public/_static/blog/blog-post-4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/blog/blog-post-4.jpg -------------------------------------------------------------------------------- /assets/fonts/GT-Walsheim-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/assets/fonts/GT-Walsheim-Regular.ttf -------------------------------------------------------------------------------- /public/_static/docs/gg-auth-config.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/docs/gg-auth-config.jpg -------------------------------------------------------------------------------- /public/_static/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/_static/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/_static/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/_static/illustrations/work-from-home.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/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/versatility519/choreless-boilerplate/HEAD/public/_static/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/_static/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/versatility519/choreless-boilerplate/HEAD/public/_static/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /lib/validations/auth.ts: -------------------------------------------------------------------------------- 1 | import * as z from "zod" 2 | 3 | export const userAuthSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string().min(8), 6 | }) 7 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/(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 | } -------------------------------------------------------------------------------- /config/marketing.ts: -------------------------------------------------------------------------------- 1 | import { MarketingConfig } from "types" 2 | 3 | export const marketingConfig: MarketingConfig = { 4 | mainNav: [ 5 | { 6 | title: "Summery", 7 | href: "/summery", 8 | }, 9 | { 10 | title: "Pricing", 11 | href: "/pricing", 12 | }, 13 | { 14 | title: "Subscription", 15 | href: "/subscription", 16 | }, 17 | { 18 | title: "FAQ", 19 | href: "/faq", 20 | }, 21 | { 22 | title: "Choreless for Business", 23 | href: "/choreless-for-business", 24 | }, 25 | ], 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /actions/getStripe.ts: -------------------------------------------------------------------------------- 1 | import { loadStripe, Stripe } from "@stripe/stripe-js"; 2 | 3 | let stripePromise: Promise; 4 | /** 5 | * Returns a Stripe promise that can be used to initialize Stripe. 6 | * The Stripe promise is cached and reused across multiple calls. 7 | * 8 | * @returns {Promise} A promise that resolves to the Stripe object. 9 | */ 10 | const getStripe = () => { 11 | if (!stripePromise) { 12 | console.log("Stripe Promise is null, creating a new one"); 13 | stripePromise = loadStripe( 14 | "pk_test_51NnDfFJJKYvls7hTtsa2g4cSwViNaLmL8RsajaylZkrEG980Z7Ad2Mitrbgqpzbrwq2eY7jyWFDd6yzEPGnrBGmK002kELgwCF", 15 | ); 16 | } 17 | return stripePromise; 18 | }; 19 | 20 | export default getStripe; 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 fontWalsheimBold = localFont({ 15 | src: "./GT-Walsheim-Bold.ttf", 16 | variable: "--font-Walsheim-Bold", 17 | }) 18 | 19 | export const fontWalsheimMedium = localFont({ 20 | src: "./GT-Walsheim-Medium.ttf", 21 | variable: "--font-Walsheim-Medium", 22 | }) 23 | 24 | export const fontWalsheimRegular = localFont({ 25 | src: "./GT-Walsheim-Regular.ttf", 26 | variable: "--font-Walsheim-Regular", 27 | }) 28 | -------------------------------------------------------------------------------- /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 | 33 | 34 | 35 | Continue 36 | 37 |
38 | 39 |
40 | © {new Date().getFullYear()} Choreless. All rights reserved. 41 |
42 | 43 | ) 44 | } -------------------------------------------------------------------------------- /types/index.d.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@prisma/client"; 2 | import type { Icon } from "lucide-react"; 3 | 4 | import { Icons } from "@/components/shared/icons"; 5 | 6 | export type SiteConfig = { 7 | name: string; 8 | description: string; 9 | url: string; 10 | ogImage: string; 11 | mailSupport: string; 12 | links: { 13 | twitter: string; 14 | github: string; 15 | }; 16 | }; 17 | 18 | export type NavItem = { 19 | title: string; 20 | href: string; 21 | badge?: number; 22 | disabled?: boolean; 23 | external?: boolean; 24 | authorizeOnly?: UserRole; 25 | icon?: keyof typeof Icons; 26 | }; 27 | 28 | export type MainNavItem = NavItem; 29 | 30 | export type MarketingConfig = { 31 | mainNav: MainNavItem[]; 32 | }; 33 | 34 | export type SidebarNavItem = { 35 | title: string; 36 | items: NavItem[]; 37 | authorizeOnly?: UserRole; 38 | icon?: keyof typeof Icons; 39 | }; 40 | 41 | export type DocsConfig = { 42 | mainNav: MainNavItem[]; 43 | sidebarNav: SidebarNavItem[]; 44 | }; 45 | 46 | // subcriptions 47 | export type SubscriptionPlan = { 48 | title: string; 49 | description: string; 50 | benefits: string[]; 51 | limitations: string[]; 52 | prices: { 53 | monthly: number; 54 | yearly: number; 55 | }; 56 | stripeIds: { 57 | monthly: string | null; 58 | yearly: string | null; 59 | }; 60 | }; 61 | 62 | export type UserSubscriptionPlan = SubscriptionPlan & 63 | Pick & { 64 | stripeCurrentPeriodEnd: number; 65 | isPaid: boolean; 66 | interval: "month" | "year" | null; 67 | isCanceled?: boolean; 68 | }; 69 | 70 | // compare plans 71 | export type ColumnType = string | boolean | null; 72 | export type PlansRow = { feature: string; tooltip?: string } & { 73 | [key in (typeof plansColumns)[number]]: ColumnType; 74 | }; 75 | 76 | // landing sections 77 | export type InfoList = { 78 | icon: keyof typeof Icons; 79 | title: string; 80 | description: string; 81 | }; 82 | 83 | export type InfoLdg = { 84 | title: string; 85 | image: string; 86 | description: string; 87 | list: InfoList[]; 88 | }; 89 | 90 | export type FeatureLdg = { 91 | title: string; 92 | description: string; 93 | link: string; 94 | icon: keyof typeof Icons; 95 | }; 96 | 97 | export type TestimonialType = { 98 | name: string; 99 | job: string; 100 | image: string; 101 | review: string; 102 | }; 103 | -------------------------------------------------------------------------------- /components/pricing/pricing-faq.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | AccordionContent, 4 | AccordionItem, 5 | AccordionTrigger, 6 | } from "@/components/ui/accordion"; 7 | 8 | import { HeaderSection } from "../shared/header-section"; 9 | 10 | const pricingFaqData = [ 11 | { 12 | id: "item-1", 13 | question: "What is the cost of the free plan?", 14 | answer: 15 | "Our free plan is completely free, with no monthly or annual charges. It's a great way to get started and explore our basic features.", 16 | }, 17 | { 18 | id: "item-2", 19 | question: "How much does the Basic Monthly plan cost?", 20 | answer: 21 | "The Basic Monthly plan is priced at $15 per month. It provides access to our core features and is billed on a monthly basis.", 22 | }, 23 | { 24 | id: "item-3", 25 | question: "What is the price of the Pro Monthly plan?", 26 | answer: 27 | "The Pro Monthly plan is available for $25 per month. It offers advanced features and is billed on a monthly basis for added flexibility.", 28 | }, 29 | { 30 | id: "item-4", 31 | question: "Do you offer any annual subscription plans?", 32 | answer: 33 | "Yes, we offer annual subscription plans for even more savings. The Basic Annual plan is $144 per year, and the Pro Annual plan is $300 per year.", 34 | }, 35 | { 36 | id: "item-5", 37 | question: "Is there a trial period for the paid plans?", 38 | answer: 39 | "We offer a 14-day free trial for both the Pro Monthly and Pro Annual plans. It's a great way to experience all the features before committing to a paid subscription.", 40 | }, 41 | ]; 42 | 43 | export function PricingFaq() { 44 | return ( 45 |
46 | 53 | 54 | 55 | {pricingFaqData.map((faqItem) => ( 56 | 57 | {faqItem.question} 58 | 59 | {faqItem.answer} 60 | 61 | 62 | ))} 63 | 64 |
65 | ); 66 | } 67 | -------------------------------------------------------------------------------- /components/content/blog-card.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { Post } from "contentlayer/generated"; 3 | 4 | import { cn, formatDate, placeholderBlurhash } from "@/lib/utils"; 5 | import BlurImage from "@/components/shared/blur-image"; 6 | 7 | import Author from "./author"; 8 | 9 | export function BlogCard({ 10 | data, 11 | priority, 12 | horizontale = false, 13 | }: { 14 | data: Post & { 15 | blurDataURL: string; 16 | }; 17 | priority?: boolean; 18 | horizontale?: boolean; 19 | }) { 20 | return ( 21 |
29 | {data.image && ( 30 |
31 | 45 |
46 | )} 47 |
53 |
54 |

55 | {data.title} 56 |

57 | {data.description && ( 58 |

59 | {data.description} 60 |

61 | )} 62 |
63 |
64 |
65 | {data.authors.map((author) => ( 66 | 67 | ))} 68 |
69 | 70 | {data.date && ( 71 |

72 | {formatDate(data.date)} 73 |

74 | )} 75 |
76 |
77 | 78 | View Article 79 | 80 |
81 | ); 82 | } 83 | -------------------------------------------------------------------------------- /components/ui/calendar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ChevronLeft, ChevronRight } from "lucide-react" 5 | import { DayPicker } from "react-day-picker" 6 | 7 | import { cn } from "@/lib/utils" 8 | import { buttonVariants } from "@/components/ui/button" 9 | 10 | export type CalendarProps = React.ComponentProps 11 | 12 | function Calendar({ 13 | className, 14 | classNames, 15 | showOutsideDays = true, 16 | ...props 17 | }: CalendarProps) { 18 | return ( 19 | , 56 | IconRight: ({ ...props }) => , 57 | }} 58 | {...props} 59 | /> 60 | ) 61 | } 62 | Calendar.displayName = "Calendar" 63 | 64 | export { Calendar } 65 | -------------------------------------------------------------------------------- /prisma/migrations/0_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE `accounts` ( 3 | `id` VARCHAR(191) NOT NULL, 4 | `userId` VARCHAR(191) NOT NULL, 5 | `type` VARCHAR(191) NOT NULL, 6 | `provider` VARCHAR(191) NOT NULL, 7 | `providerAccountId` VARCHAR(191) NOT NULL, 8 | `refresh_token` TEXT NULL, 9 | `access_token` TEXT NULL, 10 | `expires_at` INTEGER NULL, 11 | `token_type` VARCHAR(191) NULL, 12 | `scope` VARCHAR(191) NULL, 13 | `id_token` TEXT NULL, 14 | `session_state` VARCHAR(191) NULL, 15 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 16 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 17 | 18 | INDEX `accounts_userId_idx`(`userId`), 19 | UNIQUE INDEX `accounts_provider_providerAccountId_key`(`provider`, `providerAccountId`), 20 | PRIMARY KEY (`id`) 21 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 22 | 23 | -- CreateTable 24 | CREATE TABLE `sessions` ( 25 | `id` VARCHAR(191) NOT NULL, 26 | `sessionToken` VARCHAR(191) NOT NULL, 27 | `userId` VARCHAR(191) NOT NULL, 28 | `expires` DATETIME(3) NOT NULL, 29 | 30 | UNIQUE INDEX `sessions_sessionToken_key`(`sessionToken`), 31 | INDEX `sessions_userId_idx`(`userId`), 32 | PRIMARY KEY (`id`) 33 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 34 | 35 | -- CreateTable 36 | CREATE TABLE `users` ( 37 | `id` VARCHAR(191) NOT NULL, 38 | `name` VARCHAR(191) NULL, 39 | `email` VARCHAR(191) NULL, 40 | `emailVerified` DATETIME(3) NULL, 41 | `image` VARCHAR(191) NULL, 42 | `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 43 | `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), 44 | `stripe_customer_id` VARCHAR(191) NULL, 45 | `stripe_subscription_id` VARCHAR(191) NULL, 46 | `stripe_price_id` VARCHAR(191) NULL, 47 | `stripe_current_period_end` DATETIME(3) NULL, 48 | 49 | UNIQUE INDEX `users_email_key`(`email`), 50 | UNIQUE INDEX `users_stripe_customer_id_key`(`stripe_customer_id`), 51 | UNIQUE INDEX `users_stripe_subscription_id_key`(`stripe_subscription_id`), 52 | PRIMARY KEY (`id`) 53 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 54 | 55 | -- CreateTable 56 | CREATE TABLE `verification_tokens` ( 57 | `identifier` VARCHAR(191) NOT NULL, 58 | `token` VARCHAR(191) NOT NULL, 59 | `expires` DATETIME(3) NOT NULL, 60 | 61 | UNIQUE INDEX `verification_tokens_token_key`(`token`), 62 | UNIQUE INDEX `verification_tokens_identifier_token_key`(`identifier`, `token`) 63 | ) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; 64 | 65 | -------------------------------------------------------------------------------- /env.mjs: -------------------------------------------------------------------------------- 1 | import { createEnv } from "@t3-oss/env-nextjs"; 2 | import { z } from "zod"; 3 | 4 | export const env = createEnv({ 5 | server: { 6 | // This is optional because it's only used in development. 7 | // See https://next-auth.js.org/deployment. 8 | NEXTAUTH_URL: z.string().url().optional(), 9 | AUTH_SECRET: z.string().min(1), 10 | GOOGLE_CLIENT_ID: z.string().min(1), 11 | GOOGLE_CLIENT_SECRET: z.string().min(1), 12 | GITHUB_OAUTH_TOKEN: z.string().min(1), 13 | DATABASE_URL: z.string().min(1), 14 | RESEND_API_KEY: z.string().min(1), 15 | EMAIL_FROM: z.string().min(1), 16 | STRIPE_API_KEY: z.string().min(1), 17 | STRIPE_WEBHOOK_SECRET: z.string().min(1), 18 | }, 19 | client: { 20 | NEXT_PUBLIC_APP_URL: z.string().min(1), 21 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: z.string().min(1), 22 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: z.string().min(1), 23 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: z.string().min(1), 24 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: z.string().min(1), 25 | }, 26 | runtimeEnv: { 27 | NEXTAUTH_URL: process.env.NEXTAUTH_URL, 28 | AUTH_SECRET: process.env.AUTH_SECRET, 29 | GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, 30 | GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET, 31 | GITHUB_OAUTH_TOKEN: process.env.GITHUB_OAUTH_TOKEN, 32 | DATABASE_URL: process.env.DATABASE_URL, 33 | RESEND_API_KEY: process.env.RESEND_API_KEY, 34 | EMAIL_FROM: process.env.EMAIL_FROM, 35 | NEXT_PUBLIC_APP_URL: process.env.NEXT_PUBLIC_APP_URL, 36 | // Stripe 37 | STRIPE_API_KEY: process.env.STRIPE_API_KEY, 38 | STRIPE_WEBHOOK_SECRET: process.env.STRIPE_WEBHOOK_SECRET, 39 | NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID: 40 | process.env.NEXT_PUBLIC_STRIPE_PRO_MONTHLY_PLAN_ID, 41 | NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID: 42 | process.env.NEXT_PUBLIC_STRIPE_PRO_YEARLY_PLAN_ID, 43 | NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID: 44 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_MONTHLY_PLAN_ID, 45 | NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID: 46 | process.env.NEXT_PUBLIC_STRIPE_BUSINESS_YEARLY_PLAN_ID, 47 | plan1: process.env.NEXT_PUBLIC_PLAN1, 48 | plan2: process.env.NEXT_PUBLIC_PLAN2, 49 | plan3: process.env.NEXT_PUBLIC_PLAN3, 50 | plan4: process.env.NEXT_PUBLIC_PLAN4, 51 | plan5: process.env.NEXT_PUBLIC_PLAN5, 52 | plan6: process.env.NEXT_PUBLIC_PLAN6, 53 | plan7: process.env.NEXT_PUBLIC_PLAN7, 54 | plan8: process.env.NEXT_PUBLIC_PLAN8, 55 | plan9: process.env.NEXT_PUBLIC_PLAN9, 56 | plan10: process.env.NEXT_PUBLIC_PLAN10, 57 | }, 58 | }); 59 | -------------------------------------------------------------------------------- /components/charts/radial-chart-grid.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { TrendingUp } from "lucide-react" 4 | import { PolarGrid, RadialBar, RadialBarChart } from "recharts" 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { 15 | ChartConfig, 16 | ChartContainer, 17 | ChartTooltip, 18 | ChartTooltipContent, 19 | } from "@/components/ui/chart" 20 | const chartData = [ 21 | { browser: "chrome", visitors: 275, fill: "var(--color-chrome)" }, 22 | { browser: "safari", visitors: 200, fill: "var(--color-safari)" }, 23 | { browser: "firefox", visitors: 187, fill: "var(--color-firefox)" }, 24 | { browser: "edge", visitors: 173, fill: "var(--color-edge)" }, 25 | { browser: "other", visitors: 90, fill: "var(--color-other)" }, 26 | ] 27 | 28 | const chartConfig = { 29 | visitors: { 30 | label: "Visitors", 31 | }, 32 | chrome: { 33 | label: "Chrome", 34 | color: "hsl(var(--chart-1))", 35 | }, 36 | safari: { 37 | label: "Safari", 38 | color: "hsl(var(--chart-2))", 39 | }, 40 | firefox: { 41 | label: "Firefox", 42 | color: "hsl(var(--chart-3))", 43 | }, 44 | edge: { 45 | label: "Edge", 46 | color: "hsl(var(--chart-4))", 47 | }, 48 | other: { 49 | label: "Other", 50 | color: "hsl(var(--chart-5))", 51 | }, 52 | } satisfies ChartConfig 53 | 54 | export function RadialChartGrid() { 55 | return ( 56 | 57 | {/* 58 | Radial Chart - Grid 59 | January - June 2024 60 | */} 61 | 62 | 66 | 67 | } 70 | /> 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | Trending up by 5.2% this month 79 |
80 |
81 | Showing total visitors for the last 6 months 82 |
83 |
84 |
85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /components/modals/sign-in-modal.tsx: -------------------------------------------------------------------------------- 1 | import { signIn } from "next-auth/react"; 2 | import { 3 | Dispatch, 4 | SetStateAction, 5 | useCallback, 6 | useMemo, 7 | useState, 8 | } from "react"; 9 | 10 | import { Icons } from "@/components/shared/icons"; 11 | import { Button } from "@/components/ui/button"; 12 | import { Modal } from "@/components/ui/modal"; 13 | import { siteConfig } from "@/config/site"; 14 | 15 | function SignInModal({ 16 | showSignInModal, 17 | setShowSignInModal, 18 | }: { 19 | showSignInModal: boolean; 20 | setShowSignInModal: Dispatch>; 21 | }) { 22 | const [signInClicked, setSignInClicked] = useState(false); 23 | 24 | return ( 25 | 26 |
27 |
28 | 29 | 30 | 31 |

Sign In

32 |

33 | This is strictly for demo purposes - only your email and profile 34 | picture will be stored. 35 |

36 |
37 | 38 |
39 | 58 |
59 |
60 |
61 | ); 62 | } 63 | 64 | export function useSignInModal() { 65 | const [showSignInModal, setShowSignInModal] = useState(false); 66 | 67 | const SignInModalCallback = useCallback(() => { 68 | return ( 69 | 73 | ); 74 | }, [showSignInModal, setShowSignInModal]); 75 | 76 | return useMemo( 77 | () => ({ 78 | setShowSignInModal, 79 | SignInModal: SignInModalCallback, 80 | }), 81 | [setShowSignInModal, SignInModalCallback], 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /app/api/webhooks/stripe/route.ts: -------------------------------------------------------------------------------- 1 | import { headers } from "next/headers"; 2 | import Stripe from "stripe"; 3 | 4 | import { env } from "@/env.mjs"; 5 | import { prisma } from "@/lib/db"; 6 | import { stripe } from "@/lib/stripe"; 7 | 8 | export async function POST(req: Request) { 9 | const body = await req.text(); 10 | const signature = headers().get("Stripe-Signature") as string; 11 | 12 | let event: Stripe.Event; 13 | 14 | try { 15 | event = stripe.webhooks.constructEvent( 16 | body, 17 | signature, 18 | env.STRIPE_WEBHOOK_SECRET, 19 | ); 20 | } catch (error) { 21 | return new Response(`Webhook Error: ${error.message}`, { status: 400 }); 22 | } 23 | 24 | if (event.type === "checkout.session.completed") { 25 | const session = event.data.object as Stripe.Checkout.Session; 26 | 27 | // Retrieve the subscription details from Stripe. 28 | const subscription = await stripe.subscriptions.retrieve( 29 | session.subscription as string, 30 | ); 31 | 32 | // Update the user stripe into in our database. 33 | // Since this is the initial subscription, we need to update 34 | // the subscription id and customer id. 35 | await prisma.user.update({ 36 | where: { 37 | id: session?.metadata?.userId, 38 | }, 39 | data: { 40 | stripeSubscriptionId: subscription.id, 41 | stripeCustomerId: subscription.customer as string, 42 | stripePriceId: subscription.items.data[0].price.id, 43 | stripeCurrentPeriodEnd: new Date( 44 | subscription.current_period_end * 1000, 45 | ), 46 | }, 47 | }); 48 | } 49 | 50 | if (event.type === "invoice.payment_succeeded") { 51 | const session = event.data.object as Stripe.Invoice; 52 | 53 | // If the billing reason is not subscription_create, it means the customer has updated their subscription. 54 | // If it is subscription_create, we don't need to update the subscription id and it will handle by the checkout.session.completed event. 55 | if (session.billing_reason != "subscription_create") { 56 | // Retrieve the subscription details from Stripe. 57 | const subscription = await stripe.subscriptions.retrieve( 58 | session.subscription as string, 59 | ); 60 | 61 | // Update the price id and set the new period end. 62 | await prisma.user.update({ 63 | where: { 64 | stripeSubscriptionId: subscription.id, 65 | }, 66 | data: { 67 | stripePriceId: subscription.items.data[0].price.id, 68 | stripeCurrentPeriodEnd: new Date( 69 | subscription.current_period_end * 1000, 70 | ), 71 | }, 72 | }); 73 | } 74 | } 75 | 76 | return new Response(null, { status: 200 }); 77 | } 78 | -------------------------------------------------------------------------------- /components/sections/features.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { features } from "@/config/landing"; 4 | import { Button } from "@/components/ui/button"; 5 | import { HeaderSection } from "@/components/shared/header-section"; 6 | import { Icons } from "@/components/shared/icons"; 7 | import MaxWidthWrapper from "@/components/shared/max-width-wrapper"; 8 | 9 | export default function Features() { 10 | return ( 11 |
12 |
13 | 14 | 20 | 21 |
22 | {features.map((feature) => { 23 | const Icon = Icons[feature.icon || "nextjs"]; 24 | return ( 25 |
29 | 57 | ); 58 | })} 59 |
60 | 61 |
62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/charts/line-chart-multiple.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { TrendingUp } from "lucide-react" 4 | import { CartesianGrid, Line, LineChart, XAxis } from "recharts" 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { 15 | ChartConfig, 16 | ChartContainer, 17 | ChartTooltip, 18 | ChartTooltipContent, 19 | } from "@/components/ui/chart" 20 | const chartData = [ 21 | { month: "January", desktop: 186, mobile: 80 }, 22 | { month: "February", desktop: 305, mobile: 200 }, 23 | { month: "March", desktop: 237, mobile: 120 }, 24 | { month: "April", desktop: 73, mobile: 190 }, 25 | { month: "May", desktop: 209, mobile: 130 }, 26 | { month: "June", desktop: 214, mobile: 140 }, 27 | ] 28 | 29 | const chartConfig = { 30 | desktop: { 31 | label: "Desktop", 32 | color: "hsl(var(--chart-1))", 33 | }, 34 | mobile: { 35 | label: "Mobile", 36 | color: "hsl(var(--chart-2))", 37 | }, 38 | } satisfies ChartConfig 39 | 40 | export function LineChartMultiple() { 41 | return ( 42 | 43 | 44 | Line Chart - Multiple 45 | January - June 2024 46 | 47 | 48 | 49 | 57 | 58 | value.slice(0, 3)} 64 | /> 65 | } /> 66 | 73 | 80 | 81 | 82 | 83 | 84 |
85 | Trending up by 5.2% this month 86 |
87 |
88 | Showing total visitors for the last 6 months 89 |
90 |
91 |
92 | ) 93 | } 94 | -------------------------------------------------------------------------------- /components/dashboard/search-command.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | import { useRouter } from "next/navigation"; 5 | import { SidebarNavItem } from "@/types"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | import { Button } from "@/components/ui/button"; 9 | import { 10 | CommandDialog, 11 | CommandEmpty, 12 | CommandGroup, 13 | CommandInput, 14 | CommandItem, 15 | CommandList, 16 | } from "@/components/ui/command"; 17 | import { Icons } from "@/components/shared/icons"; 18 | 19 | export function SearchCommand({ links }: { links: SidebarNavItem[] }) { 20 | const [open, setOpen] = React.useState(false); 21 | const router = useRouter(); 22 | 23 | React.useEffect(() => { 24 | const down = (e: KeyboardEvent) => { 25 | if (e.key === "k" && (e.metaKey || e.ctrlKey)) { 26 | e.preventDefault(); 27 | setOpen((open) => !open); 28 | } 29 | }; 30 | document.addEventListener("keydown", down); 31 | return () => document.removeEventListener("keydown", down); 32 | }, []); 33 | 34 | const runCommand = React.useCallback((command: () => unknown) => { 35 | setOpen(false); 36 | command(); 37 | }, []); 38 | 39 | return ( 40 | <> 41 | 56 | 57 | 58 | 59 | 60 | No results found. 61 | {links.map((section) => ( 62 | 63 | {section.items.map((item) => { 64 | const Icon = Icons[item.icon || "arrowRight"]; 65 | return ( 66 | { 69 | runCommand(() => router.push(item.href as string)); 70 | }} 71 | > 72 | 73 | {item.title} 74 | 75 | ); 76 | })} 77 | 78 | ))} 79 | 80 | 81 | 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer utilities { 6 | .hidden-scrollbar { 7 | scrollbar-width: none; /* Firefox */ 8 | } 9 | 10 | .hidden-scrollbar::-webkit-scrollbar { 11 | display: none; /* Chrome, Safari and Opera */ 12 | } 13 | } 14 | 15 | @layer base { 16 | :root { 17 | --background: 0 0% 100%; 18 | --foreground: 0 0% 3.9%; 19 | --card: 0 0% 100%; 20 | --card-foreground: 0 0% 3.9%; 21 | --popover: 0 0% 100%; 22 | --popover-foreground: 0 0% 3.9%; 23 | --primary: 0 0% 9%; 24 | --primary-foreground: 0 0% 98%; 25 | --secondary: 0 0% 96.1%; 26 | --secondary-foreground: 0 0% 9%; 27 | --muted: 0 0% 96.1%; 28 | --muted-foreground: 0 0% 45.1%; 29 | --accent: 0 0% 98%; 30 | --accent-foreground: 0 0% 9%; 31 | --destructive: 0 84.2% 60.2%; 32 | --destructive-foreground: 0 0% 98%; 33 | --border: 0 0% 89.8%; 34 | --input: 0 0% 89.8%; 35 | --ring: 0 0% 3.9%; 36 | --radius: 0.5rem; 37 | 38 | --chart-1: 271.5 81.3% 55.9%; 39 | --chart-2: 270 95% 75%; 40 | --chart-3: 270 91% 65%; 41 | --chart-4: 269 97% 85%; 42 | --chart-5: 269 100% 92%; 43 | } 44 | 45 | .dark { 46 | --background: 0 0% 3.9%; 47 | --foreground: 0 0% 98%; 48 | --card: 0 0% 3.9%; 49 | --card-foreground: 0 0% 98%; 50 | --popover: 0 0% 3.9%; 51 | --popover-foreground: 0 0% 98%; 52 | --primary: 0 0% 98%; 53 | --primary-foreground: 0 0% 9%; 54 | --secondary: 0 0% 14.9%; 55 | --secondary-foreground: 0 0% 98%; 56 | --muted: 0 0% 14.9%; 57 | --muted-foreground: 0 0% 63.9%; 58 | --accent: 0 0% 8%; 59 | --accent-foreground: 0 0% 98%; 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | --border: 0 0% 14.9%; 63 | --input: 0 0% 14.9%; 64 | --ring: 0 0% 83.1%; 65 | 66 | --chart-1: 271.5 81.3% 55.9%; 67 | --chart-2: 270 95% 75%; 68 | --chart-3: 270 91% 65%; 69 | --chart-4: 269 97% 85%; 70 | --chart-5: 269 100% 92%; 71 | } 72 | } 73 | 74 | @layer base { 75 | * { 76 | @apply border-border; 77 | } 78 | 79 | body { 80 | @apply bg-background text-foreground; 81 | font-feature-settings: "rlig" 1, "calt" 1; 82 | } 83 | } 84 | 85 | @layer utilities { 86 | .step { 87 | counter-increment: step; 88 | } 89 | 90 | .step:before { 91 | @apply absolute w-9 h-9 bg-muted rounded-full font-mono font-medium text-center text-base inline-flex items-center justify-center -indent-px border-4 border-background; 92 | @apply ml-[-50px] mt-[-4px]; 93 | content: counter(step); 94 | } 95 | } 96 | 97 | .text-gradient_indigo-purple { 98 | background: linear-gradient(90deg, #6366f1 0%, rgb(168 85 247 / 0.8) 100%); 99 | -webkit-background-clip: text; 100 | -webkit-text-fill-color: transparent; 101 | background-clip: text; 102 | } -------------------------------------------------------------------------------- /components/sections/hero-landing.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | import { env } from "@/env.mjs"; 4 | import { siteConfig } from "@/config/site"; 5 | import { cn, nFormatter } from "@/lib/utils"; 6 | import { buttonVariants } from "@/components/ui/button"; 7 | import { Icons } from "@/components/shared/icons"; 8 | 9 | export default async function HeroLanding() { 10 | const { stargazers_count: stars } = await fetch( 11 | "https://api.github.com/repos/mickasmt/next-saas-stripe-starter", 12 | { 13 | ...(env.GITHUB_OAUTH_TOKEN && { 14 | headers: { 15 | Authorization: `Bearer ${process.env.GITHUB_OAUTH_TOKEN}`, 16 | "Content-Type": "application/json", 17 | }, 18 | }), 19 | // data will revalidate every hour 20 | next: { revalidate: 3600 }, 21 | }, 22 | ) 23 | .then((res) => res.json()) 24 | .catch((e) => console.log(e)); 25 | 26 | return ( 27 |
28 |
29 | 30 |

31 | Kick off with a bang with{" "} 32 | 33 | SaaS Starter 34 | 35 |

36 | 37 |

41 | Build your next project using Next.js 14, Prisma, Neon, Auth.js v5, 42 | Resend, React Email, Shadcn/ui, Stripe. 43 |

44 | 45 |
49 | 57 | Go Pricing 58 | 59 | 60 | 73 | 74 |

75 | Star on GitHub{" "} 76 | {nFormatter(stars)} 77 |

78 | 79 |
80 |
81 |
82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /components/ui/modal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Dispatch, SetStateAction } from "react"; 4 | // import { useRouter } from "next/router"; 5 | import { Drawer } from "vaul"; 6 | 7 | import { Dialog, DialogContent } from "@/components/ui/dialog"; 8 | import { useMediaQuery } from "@/hooks/use-media-query"; 9 | import { cn } from "@/lib/utils"; 10 | 11 | interface ModalProps { 12 | children: React.ReactNode; 13 | className?: string; 14 | showModal?: boolean; 15 | setShowModal?: Dispatch>; 16 | onClose?: () => void; 17 | desktopOnly?: boolean; 18 | preventDefaultClose?: boolean; 19 | } 20 | 21 | export function Modal({ 22 | children, 23 | className, 24 | showModal, 25 | setShowModal, 26 | onClose, 27 | desktopOnly, 28 | preventDefaultClose, 29 | }: ModalProps) { 30 | // const router = useRouter(); 31 | 32 | const closeModal = ({ dragged }: { dragged?: boolean } = {}) => { 33 | if (preventDefaultClose && !dragged) { 34 | return; 35 | } 36 | // fire onClose event if provided 37 | onClose && onClose(); 38 | 39 | // if setShowModal is defined, use it to close modal 40 | if (setShowModal) { 41 | setShowModal(false); 42 | } 43 | // else, this is intercepting route @modal 44 | // else { 45 | // router.back(); 46 | // } 47 | }; 48 | const { isMobile } = useMediaQuery(); 49 | 50 | if (isMobile && !desktopOnly) { 51 | return ( 52 | { 55 | if (!open) { 56 | closeModal({ dragged: true }); 57 | } 58 | }} 59 | > 60 | 61 | 62 | 68 |
69 |
70 |
71 | {children} 72 | 73 | 74 | 75 | 76 | ); 77 | } 78 | return ( 79 | { 82 | if (!open) { 83 | closeModal(); 84 | } 85 | }} 86 | > 87 | e.preventDefault()} 89 | onCloseAutoFocus={(e) => e.preventDefault()} 90 | className={cn( 91 | "mx-4 overflow-hidden p-0 text-black md:max-w-md md:rounded-2xl md:border", 92 | className, 93 | )} 94 | > 95 | {children} 96 | 97 | 98 | ); 99 | } 100 | -------------------------------------------------------------------------------- /components/charts/bar-chart-mixed.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { TrendingUp } from "lucide-react"; 4 | import { Bar, BarChart, XAxis, YAxis } from "recharts"; 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card"; 14 | import { 15 | ChartConfig, 16 | ChartContainer, 17 | ChartTooltip, 18 | ChartTooltipContent, 19 | } from "@/components/ui/chart"; 20 | 21 | const chartData = [ 22 | { browser: "chrome", visitors: 275, fill: "var(--color-chrome)" }, 23 | { browser: "safari", visitors: 200, fill: "var(--color-safari)" }, 24 | { browser: "firefox", visitors: 187, fill: "var(--color-firefox)" }, 25 | { browser: "edge", visitors: 173, fill: "var(--color-edge)" }, 26 | { browser: "other", visitors: 90, fill: "var(--color-other)" }, 27 | ]; 28 | 29 | const chartConfig = { 30 | visitors: { 31 | label: "Visitors", 32 | }, 33 | chrome: { 34 | label: "Chrome", 35 | color: "hsl(var(--chart-1))", 36 | }, 37 | safari: { 38 | label: "Safari", 39 | color: "hsl(var(--chart-2))", 40 | }, 41 | firefox: { 42 | label: "Firefox", 43 | color: "hsl(var(--chart-3))", 44 | }, 45 | edge: { 46 | label: "Edge", 47 | color: "hsl(var(--chart-4))", 48 | }, 49 | other: { 50 | label: "Other", 51 | color: "hsl(var(--chart-5))", 52 | }, 53 | } satisfies ChartConfig; 54 | 55 | export function BarChartMixed() { 56 | return ( 57 | 58 | 59 | {/* Bar Chart - Mixed 60 | January - June 2024 */} 61 | 62 | 63 | 64 | 72 | 79 | chartConfig[value as keyof typeof chartConfig]?.label 80 | } 81 | /> 82 | 83 | } 86 | /> 87 | 88 | 89 | 90 | 91 | 92 |
93 | Trending up by 5.2% this month 94 |
95 |
96 | Results for the top 5 browsers 97 |
98 |
99 |
100 | ); 101 | } 102 | -------------------------------------------------------------------------------- /components/layout/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import Link from "next/link"; 3 | 4 | import { footerLinks, siteConfig } from "@/config/site"; 5 | import { cn } from "@/lib/utils"; 6 | import { ModeToggle } from "@/components/layout/mode-toggle"; 7 | 8 | import { NewsletterForm } from "../forms/newsletter-form"; 9 | import { Icons } from "../shared/icons"; 10 | 11 | export function SiteFooter({ className }: React.HTMLAttributes) { 12 | return ( 13 |
14 |
15 | {footerLinks.map((section) => ( 16 |
17 | 18 | {section.title} 19 | 20 |
    21 | {section.items?.map((link) => ( 22 |
  • 23 | 27 | {link.title} 28 | 29 |
  • 30 | ))} 31 |
32 |
33 | ))} 34 |
35 | 36 |
37 |
38 | 39 |
40 |
41 | {/* 42 | Copyright © 2024. All rights reserved. 43 | */} 44 |

45 | Built by{" "} 46 | 52 | mickasmt 53 | 54 | . Hosted on{" "} 55 | 61 | Vercel 62 | 63 | . Illustrations by{" "} 64 | 70 | Popsy 71 | 72 |

73 | 74 |
75 | 81 | 82 | 83 | 84 |
85 |
86 |
87 |
88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /components/charts/area-chart-stacked.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { TrendingUp } from "lucide-react"; 4 | import { Area, AreaChart, CartesianGrid, XAxis } from "recharts"; 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card"; 14 | import { 15 | ChartConfig, 16 | ChartContainer, 17 | ChartTooltip, 18 | ChartTooltipContent, 19 | } from "@/components/ui/chart"; 20 | 21 | const chartData = [ 22 | { month: "January", desktop: 186, mobile: 80 }, 23 | { month: "February", desktop: 305, mobile: 200 }, 24 | { month: "March", desktop: 237, mobile: 120 }, 25 | { month: "April", desktop: 73, mobile: 190 }, 26 | { month: "May", desktop: 209, mobile: 130 }, 27 | { month: "June", desktop: 214, mobile: 140 }, 28 | ]; 29 | 30 | const chartConfig = { 31 | desktop: { 32 | label: "Desktop", 33 | color: "hsl(var(--chart-1))", 34 | }, 35 | mobile: { 36 | label: "Mobile", 37 | color: "hsl(var(--chart-2))", 38 | }, 39 | } satisfies ChartConfig; 40 | 41 | export function AreaChartStacked() { 42 | return ( 43 | 44 | 45 | {/* Area Chart - Stacked 46 | 47 | Showing total visitors for the last 6 months 48 | */} 49 | 50 | 51 | 52 | 60 | 61 | value.slice(0, 3)} 67 | /> 68 | } 71 | /> 72 | 80 | 88 | 89 | 90 | 91 | 92 |
93 | Trending up by 5.2% this month 94 |
95 |
96 | January - June 2024 97 |
98 |
99 |
100 | ); 101 | } 102 | --------------------------------------------------------------------------------