├── .env.local.example ├── .eslintrc.json ├── .gitignore ├── LICENSE ├── README.md ├── app ├── (landing-page) │ ├── layout.tsx │ └── page.tsx ├── dashboard │ ├── [teamId] │ │ ├── (overview) │ │ │ ├── graph.tsx │ │ │ ├── page.tsx │ │ │ └── recent-sales.tsx │ │ ├── [placeholder] │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── page-client.tsx │ └── page.tsx ├── favicon.ico ├── globals.css ├── handler │ ├── [...stack] │ │ └── page.tsx │ └── layout.tsx ├── layout.tsx ├── loading.tsx └── provider.tsx ├── assets ├── account-settings.png ├── dashboard-overview.png ├── landing-page.png ├── team-switcher.png └── thumbnail.png ├── components.json ├── components ├── color-mode-switcher.tsx ├── features.tsx ├── footer.tsx ├── handler-header.tsx ├── hero.tsx ├── landing-page-header.tsx ├── logo.tsx ├── pricing.tsx ├── sidebar-layout.tsx └── ui │ ├── avatar.tsx │ ├── breadcrumb.tsx │ ├── button.tsx │ ├── card.tsx │ ├── chart.tsx │ ├── input.tsx │ ├── label.tsx │ ├── separator.tsx │ ├── sheet.tsx │ └── skeleton.tsx ├── lib └── utils.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── stack.tsx ├── tailwind.config.ts └── tsconfig.json /.env.local.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_STACK_PROJECT_ID=# generate from the Stack Dashboard at app.stack-auth.com 2 | NEXT_PUBLIC_STACK_PUBLISHABLE_CLIENT_KEY=# generate from the Stack Dashboard at app.stack-auth.com 3 | STACK_SECRET_SERVER_KEY=# generate from the Stack Dashboard at app.stack-auth.com -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Stack Auth 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Multi-tenant Starter Template 2 | 3 | A minimalistic multi-tenant Next.js starter template with minimal setup and a modular design. Bring your own backend and database. 4 | 5 | [Demo](https://stack-template.vercel.app/) 6 | 7 | ## Landing Page 8 | 9 |
10 | Teams 11 |
12 | 13 | ## Dashboard 14 | 15 |
16 | Teams 17 |
18 | 19 | ## Multi-tenancy (Teams) 20 | 21 |
22 | Teams 23 |
24 | 25 | ## Account Settings 26 | 27 |
28 | Teams 29 |
30 | 31 | ## Getting Started 32 | 33 | 1. Clone the repository 34 | 35 | ```bash 36 | git clone git@github.com:stack-auth/stack-template.git 37 | ``` 38 | 39 | 2. Install dependencies 40 | 41 | ```bash 42 | npm install 43 | ``` 44 | 45 | 3. Register an account on [Stack Auth](https://stack-auth.com), copy the keys from the dashboard, and paste them into the `.env.local` file. Then, enable "client team creation" on the team settings tab. 46 | 47 | If you want to learn more about Stack Auth or self-host it, check out the [Docs](https://docs.stack-auth.com) and [GitHub](https://github.com/stack-auth/stack). 48 | 49 | 4. Start the development server and go to [http://localhost:3000](http://localhost:3000) 50 | 51 | ```bash 52 | npm run dev 53 | ``` 54 | 55 | ## Features & Tech Stack 56 | 57 | - Next.js 14 app router 58 | - TypeScript 59 | - Tailwind & Shadcn UI 60 | - Stack Auth 61 | - Multi-tenancy (teams/orgs) 62 | - Dark mode 63 | 64 | ## Inspired by 65 | 66 | - [Shadcn UI](https://github.com/shadcn-ui/ui) 67 | - [Shadcn Taxonomy](https://github.com/shadcn-ui/taxonomy) 68 | -------------------------------------------------------------------------------- /app/(landing-page)/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Footer } from "@/components/footer"; 2 | import { LandingPageHeader } from "@/components/landing-page-header"; 3 | 4 | export default function Layout(props: { children: React.ReactNode }) { 5 | return ( 6 |
7 | 15 |
{props.children}
16 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /app/(landing-page)/page.tsx: -------------------------------------------------------------------------------- 1 | import { FeatureGrid } from "@/components/features"; 2 | import { Hero } from "@/components/hero"; 3 | import { PricingGrid } from "@/components/pricing"; 4 | import { stackServerApp } from "@/stack"; 5 | import { GitHubLogoIcon } from "@radix-ui/react-icons"; 6 | import { ComponentIcon, Users } from "lucide-react"; 7 | 8 | export default async function IndexPage() { 9 | const project = await stackServerApp.getProject(); 10 | if (!project.config.clientTeamCreationEnabled) { 11 | return ( 12 |
13 |
14 |

Setup Required

15 |

16 | { 17 | "To start using this project, please enable client-side team creation in the Stack Auth dashboard (Project > Team Settings). This message will disappear once the feature is enabled." 18 | } 19 |

20 |
21 |
22 | ); 23 | } 24 | 25 | return ( 26 | <> 27 | 38 | Crafted with ❤️ by{" "} 39 | 45 | Stack Auth 46 | 47 | 48 | } 49 | /> 50 | 51 |
52 | 59 | 60 | 61 | ), 62 | title: "Next.js 14", 63 | description: 64 | "Utilize the latest features: App Router, Layouts, Suspense.", 65 | }, 66 | { 67 | icon: ( 68 | 73 | 74 | 85 | 96 | 97 | ), 98 | title: "Shadcn UI", 99 | description: 100 | "Modern and fully customizable UI components based on Tailwind CSS.", 101 | }, 102 | { 103 | icon: ( 104 | 112 | 113 | 114 | ), 115 | title: "Stack Auth", 116 | description: 117 | "Comprehensive Authentication: OAuth, User Management, and more.", 118 | }, 119 | { 120 | icon: , 121 | title: "Multi-tenancy & RBAC", 122 | description: "Built-in Teams and Permissions.", 123 | }, 124 | { 125 | icon: , 126 | title: "100% Open-source", 127 | description: "Open-source and self-hostable codebase.", 128 | }, 129 | { 130 | icon: , 131 | title: "Modular Design", 132 | description: "Easily extend and customize. No spaghetti code.", 133 | }, 134 | ]} 135 | /> 136 | 137 |
138 | 187 | 188 | ); 189 | } 190 | -------------------------------------------------------------------------------- /app/dashboard/[teamId]/(overview)/graph.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Bar, BarChart, ResponsiveContainer, XAxis, YAxis } from "recharts" 4 | 5 | const data = [ 6 | { 7 | name: "Jan", 8 | total: Math.floor(Math.random() * 5000) + 1000, 9 | }, 10 | { 11 | name: "Feb", 12 | total: Math.floor(Math.random() * 5000) + 1000, 13 | }, 14 | { 15 | name: "Mar", 16 | total: Math.floor(Math.random() * 5000) + 1000, 17 | }, 18 | { 19 | name: "Apr", 20 | total: Math.floor(Math.random() * 5000) + 1000, 21 | }, 22 | { 23 | name: "May", 24 | total: Math.floor(Math.random() * 5000) + 1000, 25 | }, 26 | { 27 | name: "Jun", 28 | total: Math.floor(Math.random() * 5000) + 1000, 29 | }, 30 | { 31 | name: "Jul", 32 | total: Math.floor(Math.random() * 5000) + 1000, 33 | }, 34 | { 35 | name: "Aug", 36 | total: Math.floor(Math.random() * 5000) + 1000, 37 | }, 38 | { 39 | name: "Sep", 40 | total: Math.floor(Math.random() * 5000) + 1000, 41 | }, 42 | { 43 | name: "Oct", 44 | total: Math.floor(Math.random() * 5000) + 1000, 45 | }, 46 | { 47 | name: "Nov", 48 | total: Math.floor(Math.random() * 5000) + 1000, 49 | }, 50 | { 51 | name: "Dec", 52 | total: Math.floor(Math.random() * 5000) + 1000, 53 | }, 54 | ] 55 | 56 | export function Graph() { 57 | return ( 58 | 59 | 60 | 67 | `$${value}`} 73 | /> 74 | 80 | 81 | 82 | ) 83 | } 84 | -------------------------------------------------------------------------------- /app/dashboard/[teamId]/(overview)/page.tsx: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | 3 | import { RecentSales } from "@/app/dashboard/[teamId]/(overview)/recent-sales"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardHeader, 9 | CardTitle, 10 | } from "@/components/ui/card"; 11 | import { Graph } from "./graph"; 12 | 13 | export const metadata: Metadata = { 14 | title: "Dashboard", 15 | description: "Example dashboard app built using the components.", 16 | }; 17 | 18 | export default function DashboardPage() { 19 | return ( 20 | <> 21 |
22 |
23 |
24 |

Overview

25 |
26 |
27 | 28 | 29 | 30 | Total Revenue 31 | 32 | 42 | 43 | 44 | 45 | 46 |
$45,231.89
47 |

48 | +20.1% from last month 49 |

50 |
51 |
52 | 53 | 54 | 55 | Subscriptions 56 | 57 | 67 | 68 | 69 | 70 | 71 | 72 | 73 |
+2350
74 |

75 | +180.1% from last month 76 |

77 |
78 |
79 | 80 | 81 | Sales 82 | 92 | 93 | 94 | 95 | 96 | 97 |
+12,234
98 |

99 | +19% from last month 100 |

101 |
102 |
103 | 104 | 105 | 106 | Active Now 107 | 108 | 118 | 119 | 120 | 121 | 122 |
+573
123 |

124 | +201 since last hour 125 |

126 |
127 |
128 |
129 |
130 | 131 | 132 | Overview 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | Recent Sales 141 | 142 | You made 265 sales this month. 143 | 144 | 145 | 146 | 147 | 148 | 149 |
150 |
151 |
152 | 153 | ); 154 | } 155 | -------------------------------------------------------------------------------- /app/dashboard/[teamId]/(overview)/recent-sales.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Avatar, 3 | AvatarFallback, 4 | AvatarImage, 5 | } from "@/components/ui/avatar" 6 | 7 | export function RecentSales() { 8 | return ( 9 |
10 |
11 | 12 | 13 | OM 14 | 15 |
16 |

Olivia Martin

17 |

18 | olivia.martin@email.com 19 |

20 |
21 |
+$1,999.00
22 |
23 |
24 | 25 | 26 | JL 27 | 28 |
29 |

Jackson Lee

30 |

jackson.lee@email.com

31 |
32 |
+$39.00
33 |
34 |
35 | 36 | 37 | IN 38 | 39 |
40 |

Isabella Nguyen

41 |

42 | isabella.nguyen@email.com 43 |

44 |
45 |
+$299.00
46 |
47 |
48 | 49 | 50 | WK 51 | 52 |
53 |

William Kim

54 |

will@email.com

55 |
56 |
+$99.00
57 |
58 |
59 | 60 | 61 | SD 62 | 63 |
64 |

Sofia Davis

65 |

sofia.davis@email.com

66 |
67 |
+$39.00
68 |
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /app/dashboard/[teamId]/[placeholder]/page.tsx: -------------------------------------------------------------------------------- 1 | export default function Page() { 2 | return ( 3 |
4 |

Example Page

5 |
6 | ) 7 | } -------------------------------------------------------------------------------- /app/dashboard/[teamId]/layout.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import SidebarLayout, { SidebarItem } from "@/components/sidebar-layout"; 4 | import { SelectedTeamSwitcher, useUser } from "@stackframe/stack"; 5 | import { BadgePercent, BarChart4, Columns3, Globe, Locate, Settings2, ShoppingBag, ShoppingCart, Users } from "lucide-react"; 6 | import { useParams, useRouter } from "next/navigation"; 7 | 8 | const navigationItems: SidebarItem[] = [ 9 | { 10 | name: "Overview", 11 | href: "/", 12 | icon: Globe, 13 | type: "item", 14 | }, 15 | { 16 | type: 'label', 17 | name: 'Management', 18 | }, 19 | { 20 | name: "Products", 21 | href: "/products", 22 | icon: ShoppingBag, 23 | type: "item", 24 | }, 25 | { 26 | name: "People", 27 | href: "/people", 28 | icon: Users, 29 | type: "item", 30 | }, 31 | { 32 | name: "Segments", 33 | href: "/segments", 34 | icon: Columns3, 35 | type: "item", 36 | }, 37 | { 38 | name: "Regions", 39 | href: "/regions", 40 | icon: Locate, 41 | type: "item", 42 | }, 43 | { 44 | type: 'label', 45 | name: 'Monetization', 46 | }, 47 | { 48 | name: "Revenue", 49 | href: "/revenue", 50 | icon: BarChart4, 51 | type: "item", 52 | }, 53 | { 54 | name: "Orders", 55 | href: "/orders", 56 | icon: ShoppingCart, 57 | type: "item", 58 | }, 59 | { 60 | name: "Discounts", 61 | href: "/discounts", 62 | icon: BadgePercent, 63 | type: "item", 64 | }, 65 | { 66 | type: 'label', 67 | name: 'Settings', 68 | }, 69 | { 70 | name: "Configuration", 71 | href: "/configuration", 72 | icon: Settings2, 73 | type: "item", 74 | }, 75 | ]; 76 | 77 | export default function Layout(props: { children: React.ReactNode }) { 78 | const params = useParams<{ teamId: string }>(); 79 | const user = useUser({ or: 'redirect' }); 80 | const team = user.useTeam(params.teamId); 81 | const router = useRouter(); 82 | 83 | if (!team) { 84 | router.push('/dashboard'); 85 | return null; 86 | } 87 | 88 | return ( 89 | `/dashboard/${team.id}`} 95 | />} 96 | baseBreadcrumb={[{ 97 | title: team.displayName, 98 | href: `/dashboard/${team.id}`, 99 | }]} 100 | > 101 | {props.children} 102 | 103 | ); 104 | } -------------------------------------------------------------------------------- /app/dashboard/page-client.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { Input } from "@/components/ui/input"; 6 | import { Label } from "@radix-ui/react-label"; 7 | import { useUser } from "@stackframe/stack"; 8 | import { useRouter } from "next/navigation"; 9 | 10 | export function PageClient() { 11 | const router = useRouter(); 12 | const user = useUser({ or: "redirect" }); 13 | const teams = user.useTeams(); 14 | const [teamDisplayName, setTeamDisplayName] = React.useState(""); 15 | 16 | React.useEffect(() => { 17 | if (teams.length > 0 && !user.selectedTeam) { 18 | user.setSelectedTeam(teams[0]); 19 | } 20 | }, [teams, user]); 21 | 22 | if (teams.length === 0) { 23 | return ( 24 |
25 |
26 |

Welcome!

27 |

28 | Create a team to get started 29 |

30 |
{ 33 | e.preventDefault(); 34 | user.createTeam({ displayName: teamDisplayName }); 35 | }} 36 | > 37 |
38 | 39 | setTeamDisplayName(e.target.value)} 43 | /> 44 |
45 | 46 |
47 |
48 |
49 | ); 50 | } else if (user.selectedTeam) { 51 | router.push(`/dashboard/${user.selectedTeam.id}`); 52 | } 53 | 54 | return null; 55 | } 56 | -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { PageClient } from "./page-client"; 2 | 3 | export const metadata = { 4 | title: "Dashboard - Stack Template", 5 | }; 6 | 7 | export default function Dashboard() { 8 | return ; 9 | } 10 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 240 10% 3.9%; 9 | --card: 0 0% 100%; 10 | --card-foreground: 240 10% 3.9%; 11 | --popover: 0 0% 100%; 12 | --popover-foreground: 240 10% 3.9%; 13 | --primary: 240 5.9% 10%; 14 | --primary-foreground: 0 0% 98%; 15 | --secondary: 240 4.8% 95.9%; 16 | --secondary-foreground: 240 5.9% 10%; 17 | --muted: 240 4.8% 95.9%; 18 | --muted-foreground: 240 3.8% 46.1%; 19 | --accent: 240 4.8% 95.9%; 20 | --accent-foreground: 240 5.9% 10%; 21 | --destructive: 0 84.2% 60.2%; 22 | --destructive-foreground: 0 0% 98%; 23 | --border: 240 5.9% 90%; 24 | --input: 240 5.9% 90%; 25 | --ring: 240 10% 3.9%; 26 | --radius: 0.5rem; 27 | --chart-1: 12 76% 61%; 28 | --chart-2: 173 58% 39%; 29 | --chart-3: 197 37% 24%; 30 | --chart-4: 43 74% 66%; 31 | --chart-5: 27 87% 67%; 32 | } 33 | 34 | .dark { 35 | --background: 240 10% 3.9%; 36 | --foreground: 0 0% 98%; 37 | --card: 240 10% 3.9%; 38 | --card-foreground: 0 0% 98%; 39 | --popover: 240 10% 3.9%; 40 | --popover-foreground: 0 0% 98%; 41 | --primary: 0 0% 98%; 42 | --primary-foreground: 240 5.9% 10%; 43 | --secondary: 240 3.7% 15.9%; 44 | --secondary-foreground: 0 0% 98%; 45 | --muted: 240 3.7% 15.9%; 46 | --muted-foreground: 240 5% 64.9%; 47 | --accent: 240 3.7% 15.9%; 48 | --accent-foreground: 0 0% 98%; 49 | --destructive: 0 62.8% 30.6%; 50 | --destructive-foreground: 0 0% 98%; 51 | --border: 240 3.7% 15.9%; 52 | --input: 240 3.7% 15.9%; 53 | --ring: 240 4.9% 83.9%; 54 | --chart-1: 220 70% 50%; 55 | --chart-2: 160 60% 45%; 56 | --chart-3: 30 80% 55%; 57 | --chart-4: 280 65% 60%; 58 | --chart-5: 340 75% 55%; 59 | } 60 | } 61 | 62 | @layer base { 63 | * { 64 | @apply border-border; 65 | } 66 | body { 67 | @apply bg-background text-foreground; 68 | } 69 | } 70 | 71 | .loader { 72 | top: 0; 73 | left: 0; 74 | right: 0; 75 | height: 3px; 76 | position: fixed; 77 | background: transparent; 78 | overflow: hidden; 79 | z-index: 9999; 80 | } 81 | .loader::after { 82 | content: ''; 83 | width: 40%; 84 | height: 3px; 85 | position: absolute; 86 | top: 0; 87 | left: 0; 88 | box-sizing: border-box; 89 | animation: animloader 1s linear infinite; 90 | @apply bg-primary; 91 | } 92 | 93 | @media (min-width: 800px) { 94 | .loader::after { 95 | width: 20%; 96 | animation: animloader 2s linear infinite; 97 | } 98 | } 99 | 100 | @keyframes animloader { 101 | 0% { 102 | left: 0; 103 | transform: translateX(-100%); 104 | } 105 | 100% { 106 | left: 100%; 107 | transform: translateX(0%); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /app/handler/[...stack]/page.tsx: -------------------------------------------------------------------------------- 1 | import { StackHandler } from "@stackframe/stack"; 2 | import { stackServerApp } from "@/stack"; 3 | 4 | export default function Handler(props: unknown) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /app/handler/layout.tsx: -------------------------------------------------------------------------------- 1 | import HandlerHeader from "@/components/handler-header"; 2 | 3 | export default function Layout(props: { children: React.ReactNode }) { 4 | return ( 5 |
6 | 7 |
8 | {props.children} 9 |
10 |
11 | ) 12 | } -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { StackProvider, StackTheme } from "@stackframe/stack"; 2 | import type { Metadata } from "next"; 3 | import { Inter } from "next/font/google"; 4 | import { stackServerApp } from "../stack"; 5 | import "./globals.css"; 6 | import { Provider } from "./provider"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Stack Template", 12 | description: "A Multi-tenant Next.js Starter Template", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/loading.tsx: -------------------------------------------------------------------------------- 1 | export default function Loading() { 2 | // Stack uses React Suspense, which will render this page while user data is being fetched. 3 | // See: https://nextjs.org/docs/app/api-reference/file-conventions/loading 4 | return ; 5 | } 6 | -------------------------------------------------------------------------------- /app/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ThemeProvider } from "next-themes"; 4 | 5 | 6 | export function Provider(props: { children?: React.ReactNode }) { 7 | return ( 8 | 9 | {props.children} 10 | 11 | ); 12 | } -------------------------------------------------------------------------------- /assets/account-settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/assets/account-settings.png -------------------------------------------------------------------------------- /assets/dashboard-overview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/assets/dashboard-overview.png -------------------------------------------------------------------------------- /assets/landing-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/assets/landing-page.png -------------------------------------------------------------------------------- /assets/team-switcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/assets/team-switcher.png -------------------------------------------------------------------------------- /assets/thumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stack-auth/multi-tenant-starter-template/7630776f94e60b4ed15062f6bf51101c06f6d0f8/assets/thumbnail.png -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /components/color-mode-switcher.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Moon, Sun } from "lucide-react"; 4 | import { useTheme } from "next-themes"; 5 | import { Button } from "./ui/button"; 6 | 7 | export function ColorModeSwitcher() { 8 | const { setTheme } = useTheme(); 9 | 10 | return ( 11 | <> 12 | 20 | 21 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/features.tsx: -------------------------------------------------------------------------------- 1 | export function FeatureGridItem(props: { 2 | icon: React.ReactNode; 3 | title: string; 4 | description: string; 5 | }) { 6 | return ( 7 |
8 |
9 | {props.icon} 10 |
11 |

{props.title}

12 |

{props.description}

13 |
14 |
15 |
16 | ); 17 | } 18 | 19 | export function FeatureGrid(props: { 20 | title: string; 21 | subtitle: string; 22 | items: { 23 | icon: React.ReactNode; 24 | title: string; 25 | description: string; 26 | }[]; 27 | }) { 28 | return ( 29 |
33 |
34 |

35 | {props.title} 36 |

37 |

38 | {props.subtitle} 39 |

40 |
41 | 42 |
43 | {props.items.map((item, index) => ( 44 | 45 | ))} 46 |
47 |
48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /components/footer.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/button"; 2 | import { 3 | GitHubLogoIcon, 4 | LinkedInLogoIcon, 5 | TwitterLogoIcon, 6 | } from "@radix-ui/react-icons"; 7 | import Link from "next/link"; 8 | 9 | export function Footer(props: { 10 | builtBy: string; 11 | builtByLink: string; 12 | githubLink: string; 13 | twitterLink: string; 14 | linkedinLink: string; 15 | }) { 16 | return ( 17 |
18 |
19 |
20 |

21 | Built by{" "} 22 | 28 | {props.builtBy} 29 | 30 | . The source code is available on{" "} 31 | 37 | GitHub 38 | 39 | . 40 |

41 |
42 | 43 |
44 | {( 45 | [ 46 | { href: props.twitterLink, icon: TwitterLogoIcon }, 47 | { href: props.linkedinLink, icon: LinkedInLogoIcon }, 48 | { href: props.githubLink, icon: GitHubLogoIcon }, 49 | ] as const 50 | ).map((link, index) => ( 51 | 56 | 57 | 58 | ))} 59 |
60 |
61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /components/handler-header.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { UserButton, useUser } from "@stackframe/stack"; 4 | import { useTheme } from "next-themes"; 5 | import { Logo } from "./logo"; 6 | 7 | export default function HandlerHeader() { 8 | const user = useUser(); 9 | const { theme, setTheme } = useTheme(); 10 | 11 | return ( 12 | <> 13 |
14 | 15 | 16 |
17 | setTheme(theme === 'dark' ? 'light' : 'dark')} /> 18 |
19 |
20 |
{/* Placeholder for fixed header */} 21 | 22 | ); 23 | } -------------------------------------------------------------------------------- /components/hero.tsx: -------------------------------------------------------------------------------- 1 | import { buttonVariants } from "@/components/ui/button"; 2 | import { cn } from "@/lib/utils"; 3 | import Link from "next/link"; 4 | 5 | export function Hero(props: { 6 | capsuleText: string; 7 | capsuleLink: string; 8 | title: string; 9 | subtitle: string; 10 | credits?: React.ReactNode; 11 | primaryCtaText: string; 12 | primaryCtaLink: string; 13 | secondaryCtaText: string; 14 | secondaryCtaLink: string; 15 | }) { 16 | return ( 17 |
18 |
19 | 24 | {props.capsuleText} 25 | 26 |

27 | {props.title} 28 |

29 |

30 | {props.subtitle} 31 |

32 |
33 | 37 | {props.primaryCtaText} 38 | 39 | 40 | 46 | {props.secondaryCtaText} 47 | 48 |
49 | 50 | {props.credits && ( 51 |

{props.credits}

52 | )} 53 |
54 |
55 | ); 56 | } 57 | -------------------------------------------------------------------------------- /components/landing-page-header.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { useStackApp, useUser } from "@stackframe/stack"; 5 | import { Menu, X } from "lucide-react"; 6 | import Link from "next/link"; 7 | import { useSelectedLayoutSegment } from "next/navigation"; 8 | import * as React from "react"; 9 | import { ColorModeSwitcher } from "./color-mode-switcher"; 10 | import { Logo } from "./logo"; 11 | import { Button, buttonVariants } from "./ui/button"; 12 | 13 | interface NavProps { 14 | items?: { 15 | title: string; 16 | href: string; 17 | disabled?: boolean; 18 | external?: boolean; 19 | }[]; 20 | } 21 | 22 | function SignInSignUpButtons() { 23 | const app = useStackApp(); 24 | return ( 25 | <> 26 | 30 | Sign In 31 | 32 | 33 | 37 | Sign Up 38 | 39 | 40 | ); 41 | } 42 | 43 | function AuthButtonsInner() { 44 | const user = useUser(); 45 | 46 | if (user) { 47 | return ( 48 | 52 | Dashboard 53 | 54 | ); 55 | } else { 56 | return ; 57 | } 58 | } 59 | 60 | function AuthButtons() { 61 | return ( 62 | }> 63 | 64 | 65 | ); 66 | } 67 | 68 | function MobileItems(props: NavProps) { 69 | return ( 70 |
71 |
72 | 92 |
93 |
94 | ); 95 | } 96 | 97 | function DesktopItems(props: NavProps) { 98 | const segment = useSelectedLayoutSegment(); 99 | 100 | return ( 101 | 120 | ); 121 | } 122 | 123 | export function LandingPageHeader(props: NavProps) { 124 | const [showMobileMenu, setShowMobileMenu] = React.useState(false); 125 | 126 | return ( 127 |
128 |
129 |
130 | 131 | 132 | {props.items?.length ? : null} 133 | 134 | 146 | 147 | 148 | 149 | {showMobileMenu && props.items && } 150 |
151 | 152 |
153 | 154 | 157 |
158 |
159 |
160 | ); 161 | } 162 | -------------------------------------------------------------------------------- /components/logo.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import Link from "next/link"; 3 | 4 | export function Logo(props: { className?: string, link?: string }) { 5 | return ( 6 | 7 | Stack Template 8 | 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /components/pricing.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { Check } from "lucide-react"; 3 | import { Button, buttonVariants } from "@/components/ui/button"; 4 | import { 5 | Card, 6 | CardContent, 7 | CardDescription, 8 | CardFooter, 9 | CardHeader, 10 | CardTitle, 11 | } from "@/components/ui/card"; 12 | import Link from "next/link"; 13 | 14 | type PricingCardProps = { 15 | title: string; 16 | price: string; 17 | description: string; 18 | features: string[]; 19 | buttonText: string; 20 | buttonHref: string; 21 | isPopular?: boolean; 22 | }; 23 | 24 | export function PricingCard(props: PricingCardProps) { 25 | return ( 26 | 31 | 32 | {props.title} 33 | {props.description} 34 | 35 | 36 |
37 | {props.price} 38 | /month 39 |
40 |
    41 | {props.features.map((feature, index) => ( 42 |
  • 43 | 44 | {feature} 45 |
  • 46 | ))} 47 |
48 |
49 | 50 | 56 | {props.buttonText} 57 | 58 | 59 |
60 | ); 61 | } 62 | 63 | export function PricingGrid(props: { 64 | title: string; 65 | subtitle: string; 66 | items: PricingCardProps[]; 67 | }) { 68 | return ( 69 |
73 |
74 |

{props.title}

75 |

76 | {props.subtitle} 77 |

78 |
79 | 80 |
81 | {props.items.map((item, index) => ( 82 | 83 | ))} 84 |
85 |
86 | ); 87 | } 88 | -------------------------------------------------------------------------------- /components/sidebar-layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { UserButton } from "@stackframe/stack"; 5 | import { LucideIcon, Menu } from "lucide-react"; 6 | import { useTheme } from "next-themes"; 7 | import Link from "next/link"; 8 | import { usePathname } from "next/navigation"; 9 | import { useState } from "react"; 10 | import { 11 | Breadcrumb, 12 | BreadcrumbItem, 13 | BreadcrumbLink, 14 | BreadcrumbList, 15 | BreadcrumbPage, 16 | BreadcrumbSeparator, 17 | } from "./ui/breadcrumb"; 18 | import { buttonVariants } from "./ui/button"; 19 | import { Separator } from "./ui/separator"; 20 | import { Sheet, SheetContent, SheetTrigger } from "./ui/sheet"; 21 | 22 | function useSegment(basePath: string) { 23 | const path = usePathname(); 24 | const result = path.slice(basePath.length, path.length); 25 | return result ? result : "/"; 26 | } 27 | 28 | type Item = { 29 | name: React.ReactNode; 30 | href: string; 31 | icon: LucideIcon; 32 | type: "item"; 33 | }; 34 | 35 | type Sep = { 36 | type: "separator"; 37 | }; 38 | 39 | type Label = { 40 | name: React.ReactNode; 41 | type: "label"; 42 | }; 43 | 44 | export type SidebarItem = Item | Sep | Label; 45 | 46 | function NavItem(props: { 47 | item: Item; 48 | onClick?: () => void; 49 | basePath: string; 50 | }) { 51 | const segment = useSegment(props.basePath); 52 | const selected = segment === props.item.href; 53 | 54 | return ( 55 | 65 | 66 | {props.item.name} 67 | 68 | ); 69 | } 70 | 71 | function SidebarContent(props: { 72 | onNavigate?: () => void; 73 | items: SidebarItem[]; 74 | sidebarTop?: React.ReactNode; 75 | basePath: string; 76 | }) { 77 | const path = usePathname(); 78 | const segment = useSegment(props.basePath); 79 | 80 | return ( 81 |
82 |
83 | {props.sidebarTop} 84 |
85 |
86 | {props.items.map((item, index) => { 87 | if (item.type === "separator") { 88 | return ; 89 | } else if (item.type === "item") { 90 | return ( 91 |
92 | 97 |
98 | ); 99 | } else { 100 | return ( 101 |
102 |
103 | {item.name} 104 |
105 |
106 | ); 107 | } 108 | })} 109 | 110 |
111 |
112 |
113 | ); 114 | } 115 | 116 | export type HeaderBreadcrumbItem = { title: string; href: string }; 117 | 118 | function HeaderBreadcrumb(props: { items: SidebarItem[], baseBreadcrumb?: HeaderBreadcrumbItem[], basePath: string }) { 119 | const segment = useSegment(props.basePath); 120 | console.log(segment) 121 | const item = props.items.find((item) => item.type === 'item' && item.href === segment); 122 | const title: string | undefined = (item as any)?.name 123 | 124 | return ( 125 | 126 | 127 | {props.baseBreadcrumb?.map((item, index) => ( 128 | <> 129 | 130 | {item.title} 131 | 132 | 133 | 134 | ))} 135 | 136 | 137 | {title} 138 | 139 | 140 | 141 | ); 142 | } 143 | 144 | export default function SidebarLayout(props: { 145 | children?: React.ReactNode; 146 | baseBreadcrumb?: HeaderBreadcrumbItem[]; 147 | items: SidebarItem[]; 148 | sidebarTop?: React.ReactNode; 149 | basePath: string; 150 | }) { 151 | const [sidebarOpen, setSidebarOpen] = useState(false); 152 | const { resolvedTheme, setTheme } = useTheme(); 153 | 154 | return ( 155 |
156 |
157 | 158 |
159 |
160 |
161 |
162 | 163 |
164 | 165 |
166 | setSidebarOpen(open)} 168 | open={sidebarOpen} 169 | > 170 | 171 | 172 | 173 | 174 | setSidebarOpen(false)} 176 | items={props.items} 177 | sidebarTop={props.sidebarTop} 178 | basePath={props.basePath} 179 | /> 180 | 181 | 182 | 183 |
184 | 185 |
186 |
187 | 188 | 190 | setTheme(resolvedTheme === "light" ? "dark" : "light") 191 | } 192 | /> 193 |
194 |
{props.children}
195 |
196 |
197 | ); 198 | } 199 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /components/ui/breadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" 3 | import { Slot } from "@radix-ui/react-slot" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const Breadcrumb = React.forwardRef< 8 | HTMLElement, 9 | React.ComponentPropsWithoutRef<"nav"> & { 10 | separator?: React.ReactNode 11 | } 12 | >(({ ...props }, ref) =>