├── .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 |
11 |
12 |
13 | ## Dashboard
14 |
15 |
16 |
17 |
18 |
19 | ## Multi-tenancy (Teams)
20 |
21 |
22 |
23 |
24 |
25 | ## Account Settings
26 |
27 |
28 |
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 |
23 |
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 |
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 | setTheme("dark")}
16 | className="dark:hidden"
17 | >
18 |
19 |
20 |
21 | setTheme("light")}
25 | className="hidden dark:flex"
26 | >
27 |
28 |
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 |
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 |
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 |
73 | {props.items?.map((item, index) => (
74 |
84 | {item.title}
85 |
86 | ))}
87 |
88 |
91 |
92 |
93 |
94 | );
95 | }
96 |
97 | function DesktopItems(props: NavProps) {
98 | const segment = useSelectedLayoutSegment();
99 |
100 | return (
101 |
102 | {props.items?.map((item, index) => (
103 |
116 | {item.title}
117 |
118 | ))}
119 |
120 | );
121 | }
122 |
123 | export function LandingPageHeader(props: NavProps) {
124 | const [showMobileMenu, setShowMobileMenu] = React.useState(false);
125 |
126 | return (
127 |
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) => )
13 | Breadcrumb.displayName = "Breadcrumb"
14 |
15 | const BreadcrumbList = React.forwardRef<
16 | HTMLOListElement,
17 | React.ComponentPropsWithoutRef<"ol">
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | BreadcrumbList.displayName = "BreadcrumbList"
29 |
30 | const BreadcrumbItem = React.forwardRef<
31 | HTMLLIElement,
32 | React.ComponentPropsWithoutRef<"li">
33 | >(({ className, ...props }, ref) => (
34 |
39 | ))
40 | BreadcrumbItem.displayName = "BreadcrumbItem"
41 |
42 | const BreadcrumbLink = React.forwardRef<
43 | HTMLAnchorElement,
44 | React.ComponentPropsWithoutRef<"a"> & {
45 | asChild?: boolean
46 | }
47 | >(({ asChild, className, ...props }, ref) => {
48 | const Comp = asChild ? Slot : "a"
49 |
50 | return (
51 |
56 | )
57 | })
58 | BreadcrumbLink.displayName = "BreadcrumbLink"
59 |
60 | const BreadcrumbPage = React.forwardRef<
61 | HTMLSpanElement,
62 | React.ComponentPropsWithoutRef<"span">
63 | >(({ className, ...props }, ref) => (
64 |
72 | ))
73 | BreadcrumbPage.displayName = "BreadcrumbPage"
74 |
75 | const BreadcrumbSeparator = ({
76 | children,
77 | className,
78 | ...props
79 | }: React.ComponentProps<"li">) => (
80 | svg]:size-3.5", className)}
84 | {...props}
85 | >
86 | {children ?? }
87 |
88 | )
89 | BreadcrumbSeparator.displayName = "BreadcrumbSeparator"
90 |
91 | const BreadcrumbEllipsis = ({
92 | className,
93 | ...props
94 | }: React.ComponentProps<"span">) => (
95 |
101 |
102 | More
103 |
104 | )
105 | BreadcrumbEllipsis.displayName = "BreadcrumbElipssis"
106 |
107 | export {
108 | Breadcrumb,
109 | BreadcrumbList,
110 | BreadcrumbItem,
111 | BreadcrumbLink,
112 | BreadcrumbPage,
113 | BreadcrumbSeparator,
114 | BreadcrumbEllipsis,
115 | }
116 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default:
13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90",
14 | destructive:
15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
16 | outline:
17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
18 | secondary:
19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
20 | ghost: "hover:bg-accent hover:text-accent-foreground",
21 | link: "text-primary underline-offset-4 hover:underline",
22 | },
23 | size: {
24 | default: "h-9 px-4 py-2",
25 | sm: "h-8 rounded-md px-3 text-xs",
26 | lg: "h-10 rounded-md px-8",
27 | icon: "h-9 w-9",
28 | },
29 | },
30 | defaultVariants: {
31 | variant: "default",
32 | size: "default",
33 | },
34 | }
35 | )
36 |
37 | export interface ButtonProps
38 | extends React.ButtonHTMLAttributes,
39 | VariantProps {
40 | asChild?: boolean
41 | }
42 |
43 | const Button = React.forwardRef(
44 | ({ className, variant, size, asChild = false, ...props }, ref) => {
45 | const Comp = asChild ? Slot : "button"
46 | return (
47 |
52 | )
53 | }
54 | )
55 | Button.displayName = "Button"
56 |
57 | export { Button, buttonVariants }
58 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
41 | ))
42 | CardTitle.displayName = "CardTitle"
43 |
44 | const CardDescription = React.forwardRef<
45 | HTMLParagraphElement,
46 | React.HTMLAttributes
47 | >(({ className, ...props }, ref) => (
48 |
53 | ))
54 | CardDescription.displayName = "CardDescription"
55 |
56 | const CardContent = React.forwardRef<
57 | HTMLDivElement,
58 | React.HTMLAttributes
59 | >(({ className, ...props }, ref) => (
60 |
61 | ))
62 | CardContent.displayName = "CardContent"
63 |
64 | const CardFooter = React.forwardRef<
65 | HTMLDivElement,
66 | React.HTMLAttributes
67 | >(({ className, ...props }, ref) => (
68 |
73 | ))
74 | CardFooter.displayName = "CardFooter"
75 |
76 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
77 |
--------------------------------------------------------------------------------
/components/ui/chart.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as RechartsPrimitive from "recharts"
5 | import {
6 | NameType,
7 | Payload,
8 | ValueType,
9 | } from "recharts/types/component/DefaultTooltipContent"
10 |
11 | import { cn } from "@/lib/utils"
12 |
13 | // Format: { THEME_NAME: CSS_SELECTOR }
14 | const THEMES = { light: "", dark: ".dark" } as const
15 |
16 | export type ChartConfig = {
17 | [k in string]: {
18 | label?: React.ReactNode
19 | icon?: React.ComponentType
20 | } & (
21 | | { color?: string; theme?: never }
22 | | { color?: never; theme: Record }
23 | )
24 | }
25 |
26 | type ChartContextProps = {
27 | config: ChartConfig
28 | }
29 |
30 | const ChartContext = React.createContext(null)
31 |
32 | function useChart() {
33 | const context = React.useContext(ChartContext)
34 |
35 | if (!context) {
36 | throw new Error("useChart must be used within a ")
37 | }
38 |
39 | return context
40 | }
41 |
42 | const ChartContainer = React.forwardRef<
43 | HTMLDivElement,
44 | React.ComponentProps<"div"> & {
45 | config: ChartConfig
46 | children: React.ComponentProps<
47 | typeof RechartsPrimitive.ResponsiveContainer
48 | >["children"]
49 | }
50 | >(({ id, className, children, config, ...props }, ref) => {
51 | const uniqueId = React.useId()
52 | const chartId = `chart-${id || uniqueId.replace(/:/g, "")}`
53 |
54 | return (
55 |
56 |
65 |
66 |
67 | {children}
68 |
69 |
70 |
71 | )
72 | })
73 | ChartContainer.displayName = "Chart"
74 |
75 | const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => {
76 | const colorConfig = Object.entries(config).filter(
77 | ([_, config]) => config.theme || config.color
78 | )
79 |
80 | if (!colorConfig.length) {
81 | return null
82 | }
83 |
84 | return (
85 |