├── src ├── app │ ├── favicon.ico │ ├── dashboard │ │ ├── (personalAccount) │ │ │ ├── settings │ │ │ │ ├── teams │ │ │ │ │ └── page.tsx │ │ │ │ ├── page.tsx │ │ │ │ ├── billing │ │ │ │ │ └── page.tsx │ │ │ │ └── layout.tsx │ │ │ ├── page.tsx │ │ │ └── layout.tsx │ │ └── [accountSlug] │ │ │ ├── page.tsx │ │ │ ├── settings │ │ │ ├── page.tsx │ │ │ ├── billing │ │ │ │ └── page.tsx │ │ │ ├── members │ │ │ │ └── page.tsx │ │ │ └── layout.tsx │ │ │ └── layout.tsx │ ├── invitation │ │ └── page.tsx │ ├── auth │ │ └── callback │ │ │ └── route.ts │ ├── layout.tsx │ ├── globals.css │ ├── login │ │ └── page.tsx │ └── page.tsx ├── lib │ ├── full-invitation-url.ts │ ├── utils.ts │ ├── supabase │ │ ├── client.ts │ │ ├── handle-edge-error.ts │ │ ├── server.ts │ │ └── middleware.ts │ ├── actions │ │ ├── personal-account.ts │ │ ├── members.ts │ │ ├── billing.ts │ │ ├── teams.ts │ │ └── invitations.ts │ └── hooks │ │ └── use-accounts.ts ├── components │ ├── dashboard │ │ ├── dashboard-title.tsx │ │ ├── navigation-account-selector.tsx │ │ ├── settings-navigation.tsx │ │ └── dashboard-header.tsx │ ├── ui │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── input.tsx │ │ ├── checkbox.tsx │ │ ├── badge.tsx │ │ ├── popover.tsx │ │ ├── submit-button.tsx │ │ ├── alert.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── table.tsx │ │ ├── dialog.tsx │ │ ├── sheet.tsx │ │ ├── command.tsx │ │ ├── select.tsx │ │ └── dropdown-menu.tsx │ ├── basejump │ │ ├── create-team-invitation-button.tsx │ │ ├── delete-team-member-form.tsx │ │ ├── accept-team-invitation.tsx │ │ ├── new-team-form.tsx │ │ ├── delete-team-invitation-button.tsx │ │ ├── manage-teams.tsx │ │ ├── edit-team-name.tsx │ │ ├── edit-personal-account-name.tsx │ │ ├── manage-team-members.tsx │ │ ├── edit-team-slug.tsx │ │ ├── user-account-button.tsx │ │ ├── account-billing-status.tsx │ │ ├── edit-team-member-role-form.tsx │ │ ├── manage-team-invitations.tsx │ │ ├── team-member-options.tsx │ │ ├── new-invitation-form.tsx │ │ └── account-selector.tsx │ └── getting-started │ │ ├── basejump-logo.tsx │ │ ├── header.tsx │ │ ├── next-logo.tsx │ │ └── supabase-logo.tsx └── middleware.ts ├── public └── images │ ├── basejump-logo.png │ └── basejump-team-page.png ├── next.config.js ├── postcss.config.js ├── components.json ├── .env.example ├── .gitignore ├── tsconfig.json ├── LICENSE ├── package.json ├── tailwind.config.ts └── README.md /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usebasejump/basejump-next/HEAD/src/app/favicon.ico -------------------------------------------------------------------------------- /public/images/basejump-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usebasejump/basejump-next/HEAD/public/images/basejump-logo.png -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | module.exports = nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/images/basejump-team-page.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usebasejump/basejump-next/HEAD/public/images/basejump-team-page.png -------------------------------------------------------------------------------- /src/lib/full-invitation-url.ts: -------------------------------------------------------------------------------- 1 | export default function fullInvitationUrl(token: string) { 2 | return `${process.env.NEXT_PUBLIC_URL}/invitation?token=${token}`; 3 | } -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /src/lib/supabase/client.ts: -------------------------------------------------------------------------------- 1 | import { createBrowserClient } from "@supabase/ssr"; 2 | 3 | export const createClient = () => 4 | createBrowserClient( 5 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 6 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 7 | ); 8 | -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/settings/teams/page.tsx: -------------------------------------------------------------------------------- 1 | import ManageTeams from "@/components/basejump/manage-teams"; 2 | 3 | export default async function PersonalAccountTeamsPage() { 4 | 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } -------------------------------------------------------------------------------- /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": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Update these with your Supabase details from your project settings > API 2 | # https://app.supabase.com/project/_/settings/api 3 | NEXT_PUBLIC_SUPABASE_URL=your-project-url 4 | NEXT_PUBLIC_SUPABASE_ANON_KEY=your-anon-key 5 | 6 | # The NEXT_PUBLIC_URL is used to generate invitation links and billing return URLs 7 | # It should be the hostname of your application with protocol (e.g. https://example.com) 8 | NEXT_PUBLIC_URL=http://localhost:3000 -------------------------------------------------------------------------------- /src/components/dashboard/dashboard-title.tsx: -------------------------------------------------------------------------------- 1 | interface Props { 2 | title: string; 3 | description: string; 4 | } 5 | export default function DashboardTitle({title, description}: Props) { 6 | return ( 7 |
8 |

{title}

9 |

10 | {description} 11 |

12 |
13 | ) 14 | } -------------------------------------------------------------------------------- /src/app/invitation/page.tsx: -------------------------------------------------------------------------------- 1 | import AcceptTeamInvitation from "@/components/basejump/accept-team-invitation"; 2 | import { redirect } from "next/navigation" 3 | 4 | export default async function AcceptInvitationPage({searchParams}: {searchParams: {token?: string}}) { 5 | 6 | if (!searchParams.token) { 7 | redirect("/"); 8 | } 9 | 10 | return ( 11 |
12 | 13 |
14 | ) 15 | } -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import EditPersonalAccountName from "@/components/basejump/edit-personal-account-name"; 2 | import {createClient} from "@/lib/supabase/server"; 3 | 4 | export default async function PersonalAccountSettingsPage() { 5 | const supabaseClient = createClient(); 6 | const {data: personalAccount} = await supabaseClient.rpc('get_personal_account'); 7 | 8 | return ( 9 |
10 | 11 |
12 | ) 13 | } -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/page.tsx: -------------------------------------------------------------------------------- 1 | import { PartyPopper } from "lucide-react"; 2 | 3 | export default function PersonalAccountPage() { 4 | return ( 5 |
6 | 7 |

Team Account

8 |

Here's where you'll put all your awesome team dashboard items

9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /src/lib/actions/personal-account.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "../supabase/server"; 2 | 3 | export async function editPersonalAccountName(prevState: any, formData: FormData) { 4 | "use server"; 5 | 6 | const name = formData.get("name") as string; 7 | const accountId = formData.get("accountId") as string; 8 | const supabase = createClient(); 9 | 10 | const { error } = await supabase.rpc('update_account', { 11 | name, 12 | account_id: accountId 13 | }); 14 | 15 | if (error) { 16 | return { 17 | message: error.message 18 | }; 19 | } 20 | }; -------------------------------------------------------------------------------- /src/components/dashboard/navigation-account-selector.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import {useRouter} from "next/navigation"; 4 | import AccountSelector from "@/components/basejump/account-selector"; 5 | 6 | interface Props { 7 | accountId: string; 8 | } 9 | export default function NavigatingAccountSelector({accountId}: Props) { 10 | const router = useRouter(); 11 | 12 | return ( 13 | router.push(account?.personal_account ? `/dashboard` : `/dashboard/${account?.slug}`)} 16 | /> 17 | ) 18 | } -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/page.tsx: -------------------------------------------------------------------------------- 1 | import { PartyPopper } from "lucide-react"; 2 | 3 | export default function PersonalAccountPage() { 4 | return ( 5 |
6 | 7 |

Personal Account

8 |

Here's where you'll put all your awesome personal account items. If you only want to support team accounts, you can just remove these pages

9 |
10 | ) 11 | } -------------------------------------------------------------------------------- /.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 | 38 | # supabase for testing 39 | supabase/.branches 40 | supabase/.temp 41 | supabase/**/*.env 42 | -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/settings/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from "@/lib/supabase/server"; 2 | import AccountBillingStatus from "@/components/basejump/account-billing-status"; 3 | 4 | const returnUrl = process.env.NEXT_PUBLIC_URL as string; 5 | 6 | export default async function PersonalAccountBillingPage() { 7 | const supabaseClient = createClient(); 8 | const {data: personalAccount} = await supabaseClient.rpc('get_personal_account'); 9 | 10 | 11 | return ( 12 |
13 | 14 |
15 | ) 16 | } -------------------------------------------------------------------------------- /src/lib/hooks/use-accounts.ts: -------------------------------------------------------------------------------- 1 | import useSWR, {SWRConfiguration} from "swr"; 2 | import { createClient } from "../supabase/client"; 3 | import { GetAccountsResponse } from "@usebasejump/shared"; 4 | 5 | export const useAccounts = (options?: SWRConfiguration) => { 6 | const supabaseClient = createClient(); 7 | return useSWR( 8 | !!supabaseClient && ["accounts"], 9 | async () => { 10 | const {data, error} = await supabaseClient.rpc("get_accounts"); 11 | 12 | if (error) { 13 | throw new Error(error.message); 14 | } 15 | 16 | return data; 17 | }, 18 | options 19 | ); 20 | }; -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import { type NextRequest } from "next/server"; 2 | import { validateSession } from "@/lib/supabase/middleware"; 3 | 4 | export async function middleware(request: NextRequest) { 5 | return await validateSession(request); 6 | } 7 | 8 | export const config = { 9 | matcher: [ 10 | /* 11 | * Match all request paths except: 12 | * - _next/static (static files) 13 | * - _next/image (image optimization files) 14 | * - favicon.ico (favicon file) 15 | * - images - .svg, .png, .jpg, .jpeg, .gif, .webp 16 | * Feel free to modify this pattern to include more paths. 17 | */ 18 | "/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)", 19 | ], 20 | }; 21 | -------------------------------------------------------------------------------- /src/lib/supabase/handle-edge-error.ts: -------------------------------------------------------------------------------- 1 | import { FunctionsFetchError, FunctionsHttpError, FunctionsRelayError } from "@supabase/supabase-js" 2 | 3 | export default async function handleEdgeFunctionError(error: any) { 4 | if (error instanceof FunctionsHttpError) { 5 | const errorMessage = await error.context.json() 6 | return { 7 | message: Boolean(errorMessage.error) ? errorMessage.error : JSON.stringify(errorMessage) 8 | } 9 | } else if (error instanceof FunctionsRelayError) { 10 | return { 11 | message: error.message 12 | } 13 | } else if (error instanceof FunctionsFetchError) { 14 | return { 15 | message: error.message 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import EditTeamName from "@/components/basejump/edit-team-name"; 2 | import EditTeamSlug from "@/components/basejump/edit-team-slug"; 3 | import { createClient } from "@/lib/supabase/server"; 4 | 5 | export default async function TeamSettingsPage({ params: { accountSlug } }: { params: { accountSlug: string } }) { 6 | const supabaseClient = createClient(); 7 | const { data: teamAccount } = await supabaseClient.rpc('get_account_by_slug', { 8 | slug: accountSlug 9 | }); 10 | 11 | return ( 12 |
13 | 14 | 15 |
16 | ) 17 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /src/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 { cva, type VariantProps } 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 | -------------------------------------------------------------------------------- /src/app/auth/callback/route.ts: -------------------------------------------------------------------------------- 1 | import { createClient } from "@/lib/supabase/server"; 2 | import { NextResponse } from "next/server"; 3 | 4 | export async function GET(request: Request) { 5 | // The `/auth/callback` route is required for the server-side auth flow implemented 6 | // by the SSR package. It exchanges an auth code for the user's session. 7 | // https://supabase.com/docs/guides/auth/server-side/nextjs 8 | const requestUrl = new URL(request.url); 9 | const code = requestUrl.searchParams.get("code"); 10 | const returnUrl = requestUrl.searchParams.get("returnUrl"); 11 | const origin = requestUrl.origin; 12 | 13 | if (code) { 14 | const supabase = createClient(); 15 | await supabase.auth.exchangeCodeForSession(code); 16 | } 17 | 18 | // URL to redirect to after sign up process completes 19 | return NextResponse.redirect([origin, returnUrl || '/dashboard'].join('')); 20 | } 21 | -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/layout.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from "@/lib/supabase/server"; 2 | import DashboardHeader from "@/components/dashboard/dashboard-header"; 3 | 4 | export default async function PersonalAccountDashboard({children}: {children: React.ReactNode}) { 5 | 6 | const supabaseClient = createClient(); 7 | 8 | const {data: personalAccount, error} = await supabaseClient.rpc('get_personal_account'); 9 | 10 | const navigation = [ 11 | { 12 | name: 'Overview', 13 | href: '/dashboard', 14 | }, 15 | { 16 | name: 'Settings', 17 | href: '/dashboard/settings' 18 | } 19 | ] 20 | 21 | return ( 22 | <> 23 | 24 |
{children}
25 | 26 | ) 27 | 28 | } -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Separator = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >( 12 | ( 13 | { className, orientation = "horizontal", decorative = true, ...props }, 14 | ref 15 | ) => ( 16 | 27 | ) 28 | ) 29 | Separator.displayName = SeparatorPrimitive.Root.displayName 30 | 31 | export { Separator } 32 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/settings/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from "@/lib/supabase/server"; 2 | import AccountBillingStatus from "@/components/basejump/account-billing-status"; 3 | import { Alert } from "@/components/ui/alert"; 4 | 5 | const returnUrl = process.env.NEXT_PUBLIC_URL as string; 6 | 7 | export default async function TeamBillingPage({params: {accountSlug}}: {params: {accountSlug: string}}) { 8 | const supabaseClient = createClient(); 9 | const {data: teamAccount} = await supabaseClient.rpc('get_account_by_slug', { 10 | slug: accountSlug 11 | }); 12 | 13 | if (teamAccount.account_role !== 'owner') { 14 | return ( 15 | You do not have permission to access this page 16 | ) 17 | } 18 | 19 | 20 | return ( 21 |
22 | 23 |
24 | ) 25 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Inter as FontSans } from "next/font/google" 2 | import "./globals.css"; 3 | import { cn } from "@/lib/utils"; 4 | 5 | const defaultUrl = process.env.NEXT_PUBLIC_URL as string || "http://localhost:3000"; 6 | 7 | const fontSans = FontSans({ 8 | subsets: ["latin"], 9 | variable: "--font-sans", 10 | }) 11 | 12 | export const metadata = { 13 | metadataBase: new URL(defaultUrl), 14 | title: "Basejump starter kit", 15 | description: "The fastest way to build apps with Next.js and Supabase", 16 | }; 17 | 18 | export default function RootLayout({ 19 | children, 20 | }: { 21 | children: React.ReactNode; 22 | }) { 23 | return ( 24 | 28 | 29 |
30 | {children} 31 |
32 | 33 | 34 | ); 35 | } 36 | -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/settings/members/page.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from "@/lib/supabase/server"; 2 | import ManageTeamMembers from "@/components/basejump/manage-team-members"; 3 | import ManageTeamInvitations from "@/components/basejump/manage-team-invitations"; 4 | import { Alert } from "@/components/ui/alert"; 5 | 6 | export default async function TeamMembersPage({params: {accountSlug}}: {params: {accountSlug: string}}) { 7 | const supabaseClient = createClient(); 8 | const {data: teamAccount} = await supabaseClient.rpc('get_account_by_slug', { 9 | slug: accountSlug 10 | }); 11 | 12 | if (teamAccount.account_role !== 'owner') { 13 | return ( 14 | You do not have permission to access this page 15 | ) 16 | } 17 | 18 | return ( 19 |
20 | 21 | 22 |
23 | ) 24 | } -------------------------------------------------------------------------------- /src/components/basejump/create-team-invitation-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog" 12 | import NewInvitationForm from "./new-invitation-form" 13 | 14 | type Props = { 15 | accountId: string 16 | } 17 | 18 | export default function CreateTeamInvitationButton({accountId}: Props) { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | Create a new invitation 27 | 28 | Invitation links can be given to anyone to join your team 29 | 30 | 31 | 32 | 33 | 34 | ) 35 | } 36 | -------------------------------------------------------------------------------- /src/components/basejump/delete-team-member-form.tsx: -------------------------------------------------------------------------------- 1 | import { SubmitButton } from "../ui/submit-button" 2 | import { removeTeamMember } from "@/lib/actions/members"; 3 | import { GetAccountMembersResponse } from "@usebasejump/shared"; 4 | import { usePathname } from "next/navigation"; 5 | 6 | type Props = { 7 | accountId: string; 8 | teamMember: GetAccountMembersResponse[0]; 9 | } 10 | 11 | export default function DeleteTeamMemberForm({ accountId, teamMember }: Props) { 12 | const pathName = usePathname(); 13 | 14 | return ( 15 |
16 | 17 | 18 | 19 | 20 | 21 | Remove member 22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/dashboard/(personalAccount)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import SettingsNavigation from "@/components/dashboard/settings-navigation"; 2 | import DashboardTitle from "@/components/dashboard/dashboard-title"; 3 | import {Separator} from "@/components/ui/separator"; 4 | 5 | export default function PersonalAccountSettingsPage({children}) { 6 | const items = [ 7 | { name: "Profile", href: "/dashboard/settings" }, 8 | { name: "Teams", href: "/dashboard/settings/teams" }, 9 | { name: "Billing", href: "/dashboard/settings/billing" }, 10 | ] 11 | return ( 12 |
13 | 14 | 15 |
16 | 19 |
{children}
20 |
21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 usebasejump.com 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 | -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/layout.tsx: -------------------------------------------------------------------------------- 1 | import {createClient} from "@/lib/supabase/server"; 2 | import DashboardHeader from "@/components/dashboard/dashboard-header"; 3 | import { redirect } from "next/navigation"; 4 | 5 | export default async function PersonalAccountDashboard({children, params: {accountSlug}}: {children: React.ReactNode, params: {accountSlug: string}}) { 6 | const supabaseClient = createClient(); 7 | 8 | const {data: teamAccount, error} = await supabaseClient.rpc('get_account_by_slug', { 9 | slug: accountSlug 10 | }); 11 | 12 | if (!teamAccount) { 13 | redirect('/dashboard'); 14 | } 15 | 16 | const navigation = [ 17 | { 18 | name: 'Overview', 19 | href: `/dashboard/${accountSlug}`, 20 | }, 21 | { 22 | name: 'Settings', 23 | href: `/dashboard/${accountSlug}/settings` 24 | } 25 | ] 26 | 27 | return ( 28 | <> 29 | 30 |
{children}
31 | 32 | ) 33 | 34 | } -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { Check } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Checkbox = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, ...props }, ref) => ( 13 | 21 | 24 | 25 | 26 | 27 | )) 28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName 29 | 30 | export { Checkbox } 31 | -------------------------------------------------------------------------------- /src/app/dashboard/[accountSlug]/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import SettingsNavigation from "@/components/dashboard/settings-navigation"; 2 | import DashboardTitle from "@/components/dashboard/dashboard-title"; 3 | import {Separator} from "@/components/ui/separator"; 4 | 5 | export default function TeamSettingsPage({children, params: {accountSlug}}: {children: React.ReactNode, params: {accountSlug: string}}) { 6 | const items = [ 7 | { name: "Account", href: `/dashboard/${accountSlug}/settings` }, 8 | { name: "Members", href: `/dashboard/${accountSlug}/settings/members` }, 9 | { name: "Billing", href: `/dashboard/${accountSlug}/settings/billing` }, 10 | ] 11 | return ( 12 |
13 | 14 | 15 |
16 | 19 |
{children}
20 |
21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /src/components/ui/badge.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const badgeVariants = cva( 7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", 8 | { 9 | variants: { 10 | variant: { 11 | default: 12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", 13 | secondary: 14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", 15 | destructive: 16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", 17 | outline: "text-foreground", 18 | }, 19 | }, 20 | defaultVariants: { 21 | variant: "default", 22 | }, 23 | } 24 | ) 25 | 26 | export interface BadgeProps 27 | extends React.HTMLAttributes, 28 | VariantProps {} 29 | 30 | function Badge({ className, variant, ...props }: BadgeProps) { 31 | return ( 32 |
33 | ) 34 | } 35 | 36 | export { Badge, badgeVariants } 37 | -------------------------------------------------------------------------------- /src/lib/supabase/server.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { cookies } from "next/headers"; 3 | 4 | export const createClient = () => { 5 | const cookieStore = cookies(); 6 | 7 | return createServerClient( 8 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 9 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 10 | { 11 | cookies: { 12 | get(name: string) { 13 | return cookieStore.get(name)?.value; 14 | }, 15 | set(name: string, value: string, options: CookieOptions) { 16 | try { 17 | cookieStore.set({ name, value, ...options }); 18 | } catch (error) { 19 | // The `set` method was called from a Server Component. 20 | // This can be ignored if you have middleware refreshing 21 | // user sessions. 22 | } 23 | }, 24 | remove(name: string, options: CookieOptions) { 25 | try { 26 | cookieStore.set({ name, value: "", ...options }); 27 | } catch (error) { 28 | // The `delete` method was called from a Server Component. 29 | // This can be ignored if you have middleware refreshing 30 | // user sessions. 31 | } 32 | }, 33 | }, 34 | }, 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "next dev", 5 | "build": "next build", 6 | "start": "next start" 7 | }, 8 | "dependencies": { 9 | "@radix-ui/react-checkbox": "^1.0.4", 10 | "@radix-ui/react-dialog": "^1.0.5", 11 | "@radix-ui/react-dropdown-menu": "^2.0.6", 12 | "@radix-ui/react-label": "^2.0.2", 13 | "@radix-ui/react-popover": "^1.0.7", 14 | "@radix-ui/react-select": "^2.0.0", 15 | "@radix-ui/react-separator": "^1.0.3", 16 | "@radix-ui/react-slot": "^1.0.2", 17 | "@supabase/ssr": "latest", 18 | "@supabase/supabase-js": "latest", 19 | "@usebasejump/shared": "^0.0.3", 20 | "autoprefixer": "10.4.17", 21 | "class-variance-authority": "^0.7.0", 22 | "clsx": "^2.1.0", 23 | "cmdk": "^0.2.0", 24 | "date-fns": "^3.6.0", 25 | "geist": "^1.2.1", 26 | "lucide-react": "^0.368.0", 27 | "next": "latest", 28 | "next-themes": "^0.3.0", 29 | "postcss": "8.4.33", 30 | "react": "18.2.0", 31 | "react-dom": "18.2.0", 32 | "swr": "^2.2.5", 33 | "tailwind-merge": "^2.2.2", 34 | "tailwindcss": "3.4.1", 35 | "tailwindcss-animate": "^1.0.7", 36 | "typescript": "5.3.3" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "20.11.5", 40 | "@types/react": "18.2.48", 41 | "@types/react-dom": "18.2.18", 42 | "encoding": "^0.1.13" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Popover = PopoverPrimitive.Root 9 | 10 | const PopoverTrigger = PopoverPrimitive.Trigger 11 | 12 | const PopoverContent = React.forwardRef< 13 | React.ElementRef, 14 | React.ComponentPropsWithoutRef 15 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 16 | 17 | 27 | 28 | )) 29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 30 | 31 | export { Popover, PopoverTrigger, PopoverContent } 32 | -------------------------------------------------------------------------------- /src/components/dashboard/settings-navigation.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import {usePathname} from "next/navigation" 5 | 6 | import {cn} from "@/lib/utils" 7 | import {buttonVariants} from "@/components/ui/button" 8 | 9 | interface SidebarNavProps extends React.HTMLAttributes { 10 | items: { 11 | href: string 12 | name: string 13 | }[] 14 | } 15 | 16 | export default function SettingsNavigation({ className, items, ...props }: SidebarNavProps) { 17 | const pathname = usePathname() 18 | 19 | return ( 20 | 43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/components/getting-started/basejump-logo.tsx: -------------------------------------------------------------------------------- 1 | /* istanbul ignore file */ 2 | import Image from "next/image"; 3 | import {cn} from "@/lib/utils"; 4 | 5 | type Props = { 6 | size?: "sm" | "lg"; 7 | className?: string; 8 | logoOnly?: boolean; 9 | }; 10 | 11 | const Logo = ({ size = "sm", className, logoOnly = false }: Props) => { 12 | const height = size === "sm" ? 40 : 150; 13 | const width = size === "sm" ? 40 : 150; 14 | return ( 15 |
25 |
31 | Basejump Logo 37 |
38 | {!logoOnly && ( 39 |

45 | Basejump 46 |

)} 47 |
48 | ); 49 | }; 50 | 51 | export default Logo; 52 | -------------------------------------------------------------------------------- /src/components/ui/submit-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState, useFormStatus } from "react-dom"; 4 | import { type ComponentProps } from "react"; 5 | import { Button } from "@/components/ui/button"; 6 | import { Alert, AlertDescription } from "./alert"; 7 | import { AlertTriangle } from "lucide-react"; 8 | 9 | type Props = Omit, 'formAction'> & { 10 | pendingText?: string; 11 | formAction: (prevState: any, formData: FormData) => Promise; 12 | errorMessage?: string; 13 | }; 14 | 15 | const initialState = { 16 | message: "", 17 | }; 18 | 19 | export function SubmitButton({ children, formAction, errorMessage, pendingText = "Submitting...", ...props }: Props) { 20 | const { pending, action } = useFormStatus(); 21 | const [state, internalFormAction] = useFormState(formAction, initialState); 22 | 23 | 24 | const isPending = pending && action === internalFormAction; 25 | 26 | return ( 27 |
28 | {Boolean(errorMessage || state?.message) && ( 29 | 30 | 31 | 32 | {errorMessage || state?.message} 33 | 34 | 35 | )} 36 |
37 | 40 |
41 |
42 | ); 43 | } 44 | -------------------------------------------------------------------------------- /src/components/basejump/accept-team-invitation.tsx: -------------------------------------------------------------------------------- 1 | import { acceptInvitation } from "@/lib/actions/invitations"; 2 | import { createClient } from "@/lib/supabase/server"; 3 | import { Alert } from "../ui/alert"; 4 | import { Card, CardContent } from "../ui/card"; 5 | import { SubmitButton } from "../ui/submit-button"; 6 | 7 | type Props = { 8 | token: string; 9 | } 10 | export default async function AcceptTeamInvitation({ token }: Props) { 11 | const supabaseClient = createClient(); 12 | const { data: invitation } = await supabaseClient.rpc('lookup_invitation', { 13 | lookup_invitation_token: token 14 | }); 15 | 16 | return ( 17 | 18 | 19 |
20 |

You've been invited to join

21 |

{invitation.account_name}

22 |
23 | {Boolean(invitation.active) ? ( 24 |
25 | 26 | Accept invitation 27 |
28 | ) : ( 29 | 30 | This invitation has been deactivated. Please contact the account owner for a new invitation. 31 | 32 | )} 33 |
34 |
35 | ) 36 | } -------------------------------------------------------------------------------- /src/components/basejump/new-team-form.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input" 2 | import { SubmitButton } from "../ui/submit-button" 3 | import { createTeam } from "@/lib/actions/teams"; 4 | import { Label } from "../ui/label"; 5 | 6 | export default function NewTeamForm() { 7 | 8 | 9 | return ( 10 |
11 |
12 | 15 | 20 |
21 |
22 | 25 |
26 | 27 | https://your-app.com/ 28 | 29 | 34 |
35 |
36 | 40 | Create team 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /src/lib/actions/members.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { redirect } from "next/navigation"; 4 | import { createClient } from "../supabase/server"; 5 | 6 | export async function removeTeamMember(prevState: any, formData: FormData) { 7 | "use server"; 8 | 9 | const userId = formData.get("userId") as string; 10 | const accountId = formData.get("accountId") as string; 11 | const returnUrl = formData.get("returnUrl") as string; 12 | const supabase = createClient(); 13 | 14 | const { error } = await supabase.rpc('remove_account_member', { 15 | user_id: userId, 16 | account_id: accountId 17 | }); 18 | 19 | if (error) { 20 | return { 21 | message: error.message 22 | }; 23 | } 24 | 25 | redirect(returnUrl); 26 | }; 27 | 28 | 29 | export async function updateTeamMemberRole(prevState: any, formData: FormData) { 30 | "use server"; 31 | 32 | const userId = formData.get("userId") as string; 33 | const accountId = formData.get("accountId") as string; 34 | const newAccountRole = formData.get("accountRole") as string; 35 | const returnUrl = formData.get("returnUrl") as string; 36 | const makePrimaryOwner = formData.get("makePrimaryOwner"); 37 | 38 | const supabase = createClient(); 39 | 40 | const { error } = await supabase.rpc('update_account_user_role', { 41 | user_id: userId, 42 | account_id: accountId, 43 | new_account_role: newAccountRole, 44 | make_primary_owner: makePrimaryOwner 45 | }); 46 | 47 | if (error) { 48 | return { 49 | message: error.message 50 | }; 51 | } 52 | 53 | redirect(returnUrl); 54 | }; 55 | -------------------------------------------------------------------------------- /src/components/basejump/delete-team-invitation-button.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Dialog, 6 | DialogContent, 7 | DialogDescription, 8 | DialogHeader, 9 | DialogTitle, 10 | DialogTrigger, 11 | } from "@/components/ui/dialog" 12 | import { useState } from "react" 13 | import { Trash } from "lucide-react" 14 | import { SubmitButton } from "../ui/submit-button" 15 | import { deleteInvitation } from "@/lib/actions/invitations" 16 | import { usePathname } from "next/navigation" 17 | 18 | type Props = { 19 | invitationId: string 20 | } 21 | 22 | export default function DeleteTeamInvitationButton({invitationId}: Props) { 23 | const [open, setOpen] = useState(false) 24 | const returnPath = usePathname(); 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | Cancel pending invitation 33 | 34 | Are you sure? This cannot be undone 35 | 36 | 37 |
38 | 39 | 40 | 41 | Cancel invitation 42 | 43 |
44 |
45 |
46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/actions/billing.ts: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { createClient } from "../supabase/server"; 3 | import handleEdgeFunctionError from "../supabase/handle-edge-error"; 4 | 5 | export async function setupNewSubscription(prevState: any, formData: FormData) { 6 | "use server"; 7 | 8 | const accountId = formData.get("accountId") as string; 9 | const returnUrl = formData.get("returnUrl") as string; 10 | const supabaseClient = createClient(); 11 | 12 | const { data, error } = await supabaseClient.functions.invoke('billing-functions', { 13 | body: { 14 | action: "get_new_subscription_url", 15 | args: { 16 | account_id: accountId, 17 | success_url: returnUrl, 18 | cancel_url: returnUrl 19 | } 20 | } 21 | }); 22 | 23 | if (error) { 24 | return await handleEdgeFunctionError(error); 25 | } 26 | 27 | redirect(data.url); 28 | }; 29 | 30 | export async function manageSubscription(prevState: any, formData: FormData) { 31 | "use server"; 32 | 33 | const accountId = formData.get("accountId") as string; 34 | const returnUrl = formData.get("returnUrl") as string; 35 | const supabaseClient = createClient(); 36 | 37 | const { data, error } = await supabaseClient.functions.invoke('billing-functions', { 38 | body: { 39 | action: "get_billing_portal_url", 40 | args: { 41 | account_id: accountId, 42 | return_url: returnUrl 43 | } 44 | } 45 | }); 46 | 47 | if (error) { 48 | return await handleEdgeFunctionError(error); 49 | } 50 | 51 | redirect(data.url); 52 | }; -------------------------------------------------------------------------------- /src/lib/actions/teams.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { redirect } from "next/navigation"; 4 | import { createClient } from "../supabase/server"; 5 | 6 | export async function createTeam(prevState: any, formData: FormData) { 7 | "use server"; 8 | 9 | const name = formData.get("name") as string; 10 | const slug = formData.get("slug") as string; 11 | const supabase = createClient(); 12 | 13 | const { data, error } = await supabase.rpc('create_account', { 14 | name, 15 | slug, 16 | }); 17 | 18 | if (error) { 19 | return { 20 | message: error.message 21 | }; 22 | } 23 | 24 | redirect(`/dashboard/${data.slug}`); 25 | }; 26 | 27 | 28 | export async function editTeamName(prevState: any, formData: FormData) { 29 | "use server"; 30 | 31 | const name = formData.get("name") as string; 32 | const accountId = formData.get("accountId") as string; 33 | const supabase = createClient(); 34 | 35 | const { error } = await supabase.rpc('update_account', { 36 | name, 37 | account_id: accountId 38 | }); 39 | 40 | if (error) { 41 | return { 42 | message: error.message 43 | }; 44 | } 45 | }; 46 | 47 | export async function editTeamSlug(prevState: any, formData: FormData) { 48 | "use server"; 49 | 50 | const slug = formData.get("slug") as string; 51 | const accountId = formData.get("accountId") as string; 52 | const supabase = createClient(); 53 | 54 | const { data, error } = await supabase.rpc('update_account', { 55 | slug, 56 | account_id: accountId 57 | }); 58 | 59 | if (error) { 60 | return { 61 | message: error.message 62 | }; 63 | } 64 | 65 | redirect(`/dashboard/${data.slug}/settings`); 66 | }; -------------------------------------------------------------------------------- /src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /src/components/basejump/manage-teams.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; 2 | import { createClient } from "@/lib/supabase/server"; 3 | import { Table, TableRow, TableBody, TableCell } from "../ui/table"; 4 | import { Button } from "../ui/button"; 5 | import Link from "next/link"; 6 | import { Badge } from "../ui/badge"; 7 | 8 | 9 | 10 | export default async function ManageTeams() { 11 | const supabaseClient = createClient(); 12 | 13 | const { data } = await supabaseClient.rpc('get_accounts'); 14 | 15 | const teams: any[] = data?.filter((team: any) => team.personal_account === false); 16 | 17 | return ( 18 | 19 | 20 | Teams 21 | 22 | These are the teams you belong to 23 | 24 | 25 | 26 | 27 | 28 | {teams.map((team) => ( 29 | 30 | 31 |
32 | {team.name} 33 | {team.is_primary_owner ? 'Primary Owner' : team.account_role}
34 |
35 | 36 |
37 | ))} 38 |
39 |
40 |
41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/basejump/edit-team-name.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input" 2 | import { SubmitButton } from "../ui/submit-button" 3 | import { editTeamName } from "@/lib/actions/teams"; 4 | import { Label } from "../ui/label"; 5 | import { GetAccountResponse } from "@usebasejump/shared"; 6 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card"; 7 | 8 | type Props = { 9 | account: GetAccountResponse; 10 | } 11 | 12 | 13 | export default function EditTeamName({ account }: Props) { 14 | 15 | return ( 16 | 17 | 18 | Team Info 19 | 20 | Your team name and identifier are unique for your team 21 | 22 | 23 |
24 | 25 | 26 |
27 | 30 | 36 |
37 |
38 | 39 | 43 | Save 44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/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: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: 212.7 26.8% 83.9%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/components/basejump/edit-personal-account-name.tsx: -------------------------------------------------------------------------------- 1 | import { Input } from "@/components/ui/input" 2 | import { SubmitButton } from "../ui/submit-button" 3 | import { Label } from "../ui/label"; 4 | import { GetAccountResponse } from "@usebasejump/shared"; 5 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card"; 6 | import { editPersonalAccountName } from "@/lib/actions/personal-account"; 7 | 8 | type Props = { 9 | account: GetAccountResponse; 10 | } 11 | 12 | 13 | export default function EditPersonalAccountName({ account }: Props) { 14 | 15 | return ( 16 | 17 | 18 | Your info 19 | 20 | Your name is used on your personal profile as well as in your teams 21 | 22 | 23 |
24 | 25 | 26 |
27 | 30 | 36 |
37 |
38 | 39 | 43 | Save 44 | 45 | 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/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 ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "text-primary underline-offset-4 hover:underline", 21 | }, 22 | size: { 23 | default: "h-10 px-4 py-2", 24 | sm: "h-9 rounded-md px-3", 25 | lg: "h-11 rounded-md px-8", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /src/lib/actions/invitations.ts: -------------------------------------------------------------------------------- 1 | 'use server' 2 | 3 | import { revalidatePath } from "next/cache"; 4 | import { createClient } from "../supabase/server"; 5 | import { redirect } from "next/navigation"; 6 | 7 | export async function createInvitation(prevState: any, formData: FormData): Promise<{ token?: string, message?: string}> { 8 | "use server"; 9 | 10 | const invitationType = formData.get("invitationType") as string; 11 | const accountId = formData.get("accountId") as string; 12 | const accountRole = formData.get("accountRole") as string; 13 | 14 | const supabase = createClient(); 15 | 16 | const { data, error } = await supabase.rpc('create_invitation', { 17 | account_id: accountId, 18 | invitation_type: invitationType, 19 | account_role: accountRole 20 | }); 21 | 22 | if (error) { 23 | return { 24 | message: error.message 25 | }; 26 | } 27 | 28 | revalidatePath(`/dashboard/[accountSlug]/settings/members/page`); 29 | 30 | return { 31 | token: data.token as string 32 | } 33 | }; 34 | 35 | export async function deleteInvitation(prevState: any, formData: FormData) { 36 | "use server"; 37 | 38 | const invitationId = formData.get("invitationId") as string; 39 | const returnPath = formData.get("returnPath") as string; 40 | 41 | const supabase = createClient(); 42 | 43 | const { error } = await supabase.rpc('delete_invitation', { 44 | invitation_id: invitationId 45 | }); 46 | 47 | if (error) { 48 | return { 49 | message: error.message 50 | }; 51 | } 52 | redirect(returnPath); 53 | 54 | }; 55 | 56 | export async function acceptInvitation(prevState: any, formData: FormData) { 57 | "use server"; 58 | 59 | const token = formData.get("token") as string; 60 | 61 | const supabase = createClient(); 62 | 63 | const { error, data } = await supabase.rpc('accept_invitation', { 64 | lookup_invitation_token: token 65 | }); 66 | 67 | if (error) { 68 | return { 69 | message: error.message 70 | }; 71 | } 72 | redirect(`/dashboard/${data.slug}`); 73 | 74 | }; -------------------------------------------------------------------------------- /src/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 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /src/components/basejump/manage-team-members.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; 2 | import { createClient } from "@/lib/supabase/server"; 3 | import { Table, TableRow, TableBody, TableCell } from "../ui/table"; 4 | 5 | import { Badge } from "../ui/badge"; 6 | import TeamMemberOptions from "./team-member-options"; 7 | 8 | type Props = { 9 | accountId: string; 10 | } 11 | 12 | export default async function ManageTeamMembers({accountId}: Props) { 13 | const supabaseClient = createClient(); 14 | 15 | const { data: members } = await supabaseClient.rpc('get_account_members', { 16 | account_id: accountId 17 | }); 18 | 19 | const {data} = await supabaseClient.auth.getUser(); 20 | const isPrimaryOwner = members?.find((member: any) => member.user_id === data?.user?.id)?.is_primary_owner; 21 | 22 | 23 | return ( 24 | 25 | 26 | Team Members 27 | 28 | These are the users in your team 29 | 30 | 31 | 32 | 33 | 34 | {members?.map((member: any) => ( 35 | 36 | 37 |
38 | {member.name} 39 | {member.is_primary_owner ? 'Primary Owner' : member.account_role}
40 |
41 | 42 | {!Boolean(member.is_primary_owner) && } 43 | 44 |
45 | ))} 46 |
47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/components/basejump/edit-team-slug.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Input } from "@/components/ui/input" 4 | import { SubmitButton } from "../ui/submit-button" 5 | import { editTeamSlug } from "@/lib/actions/teams"; 6 | import { Label } from "../ui/label"; 7 | import { GetAccountResponse } from "@usebasejump/shared"; 8 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card"; 9 | 10 | type Props = { 11 | account: GetAccountResponse; 12 | } 13 | 14 | export default function EditTeamSlug({ account }: Props) { 15 | 16 | return ( 17 | 18 | 19 | Team Identifier 20 | 21 | Your team identifier must be unique 22 | 23 | 24 |
25 | 26 | 27 |
28 | 31 |
32 | 33 | https://your-app.com/ 34 | 35 | 41 |
42 |
43 |
44 | 45 | 49 | Save 50 | 51 | 52 |
53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/components/getting-started/header.tsx: -------------------------------------------------------------------------------- 1 | import NextLogo from './next-logo' 2 | import SupabaseLogo from './supabase-logo' 3 | import BasejumpLogo from "./basejump-logo"; 4 | 5 | export default function Header() { 6 | return ( 7 |
8 |
9 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 |

Basejump, Supabase and Next.js Starter Template

30 |

31 | The fastest way to ship using {' '} 32 | 38 | Basejump 39 | {', '} 40 | 46 | Supabase 47 | {' '} 48 | and{' '} 49 | 55 | Next.js 56 | 57 |

58 |
59 |
60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | const { fontFamily } = require("tailwindcss/defaultTheme") 3 | 4 | const config = { 5 | darkMode: ["class"], 6 | content: [ 7 | './pages/**/*.{ts,tsx}', 8 | './components/**/*.{ts,tsx}', 9 | './app/**/*.{ts,tsx}', 10 | './src/**/*.{ts,tsx}', 11 | ], 12 | prefix: "", 13 | theme: { 14 | container: { 15 | center: true, 16 | padding: "2rem", 17 | screens: { 18 | "2xl": "1400px", 19 | }, 20 | }, 21 | extend: { 22 | fontFamily: { 23 | sans: ["var(--font-sans)", ...fontFamily.sans], 24 | }, 25 | colors: { 26 | border: "hsl(var(--border))", 27 | input: "hsl(var(--input))", 28 | ring: "hsl(var(--ring))", 29 | background: "hsl(var(--background))", 30 | foreground: "hsl(var(--foreground))", 31 | primary: { 32 | DEFAULT: "hsl(var(--primary))", 33 | foreground: "hsl(var(--primary-foreground))", 34 | }, 35 | secondary: { 36 | DEFAULT: "hsl(var(--secondary))", 37 | foreground: "hsl(var(--secondary-foreground))", 38 | }, 39 | destructive: { 40 | DEFAULT: "hsl(var(--destructive))", 41 | foreground: "hsl(var(--destructive-foreground))", 42 | }, 43 | muted: { 44 | DEFAULT: "hsl(var(--muted))", 45 | foreground: "hsl(var(--muted-foreground))", 46 | }, 47 | accent: { 48 | DEFAULT: "hsl(var(--accent))", 49 | foreground: "hsl(var(--accent-foreground))", 50 | }, 51 | popover: { 52 | DEFAULT: "hsl(var(--popover))", 53 | foreground: "hsl(var(--popover-foreground))", 54 | }, 55 | card: { 56 | DEFAULT: "hsl(var(--card))", 57 | foreground: "hsl(var(--card-foreground))", 58 | }, 59 | }, 60 | borderRadius: { 61 | lg: "var(--radius)", 62 | md: "calc(var(--radius) - 2px)", 63 | sm: "calc(var(--radius) - 4px)", 64 | }, 65 | keyframes: { 66 | "accordion-down": { 67 | from: { height: "0" }, 68 | to: { height: "var(--radix-accordion-content-height)" }, 69 | }, 70 | "accordion-up": { 71 | from: { height: "var(--radix-accordion-content-height)" }, 72 | to: { height: "0" }, 73 | }, 74 | }, 75 | animation: { 76 | "accordion-down": "accordion-down 0.2s ease-out", 77 | "accordion-up": "accordion-up 0.2s ease-out", 78 | }, 79 | }, 80 | }, 81 | plugins: [require("tailwindcss-animate")], 82 | } satisfies Config 83 | 84 | export default config -------------------------------------------------------------------------------- /src/components/basejump/user-account-button.tsx: -------------------------------------------------------------------------------- 1 | import {Button} from "@/components/ui/button" 2 | import { 3 | DropdownMenu, 4 | DropdownMenuContent, 5 | DropdownMenuGroup, 6 | DropdownMenuItem, 7 | DropdownMenuLabel, 8 | DropdownMenuSeparator, 9 | DropdownMenuTrigger, 10 | } from "@/components/ui/dropdown-menu" 11 | import Link from "next/link"; 12 | import {UserIcon} from "lucide-react"; 13 | import {createClient} from "@/lib/supabase/server"; 14 | import {redirect} from "next/navigation"; 15 | 16 | export default async function UserAccountButton() { 17 | const supabaseClient = createClient(); 18 | const {data: personalAccount} = await supabaseClient.rpc('get_personal_account'); 19 | 20 | const signOut = async () => { 21 | 'use server' 22 | 23 | const supabase = createClient() 24 | await supabase.auth.signOut() 25 | return redirect('/') 26 | } 27 | 28 | return ( 29 | 30 | 31 | 34 | 35 | 36 | 37 |
38 |

{personalAccount.name}

39 |

40 | {personalAccount.email} 41 |

42 |
43 |
44 | 45 | 46 | 47 | My Account 48 | 49 | 50 | Settings 51 | 52 | 53 | Teams 54 | 55 | 56 | 57 | 58 |
59 | 60 |
61 |
62 |
63 |
64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /src/components/basejump/account-billing-status.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "../ui/card"; 2 | import { Alert, AlertDescription } from "../ui/alert"; 3 | import { createClient } from "@/lib/supabase/server"; 4 | import { SubmitButton } from "../ui/submit-button"; 5 | import { manageSubscription, setupNewSubscription } from "@/lib/actions/billing"; 6 | 7 | type Props = { 8 | accountId: string; 9 | returnUrl: string; 10 | } 11 | 12 | export default async function AccountBillingStatus({ accountId, returnUrl }: Props) { 13 | const supabaseClient = createClient(); 14 | 15 | 16 | const { data, error } = await supabaseClient.functions.invoke('billing-functions', { 17 | body: { 18 | action: "get_billing_status", 19 | args: { 20 | account_id: accountId 21 | } 22 | } 23 | }); 24 | 25 | return ( 26 | 27 | 28 | Billing Status 29 | 30 | A quick overview of your billing status 31 | 32 | 33 | 34 | {!Boolean(data?.billing_enabled) ? ( 35 | 36 | 37 | Billing is not enabled for this account. Check out usebasejump.com for more info or remove this component if you don't plan on enabling billing. 38 | 39 | 40 | ) : ( 41 |
42 |

Status: {data.status}

43 |
44 | )} 45 | 46 |
47 | {Boolean(data?.billing_enabled) && ( 48 | 49 |
50 | 51 | 52 | {data.status === 'not_setup' ? ( 53 | Setup your Subscription 54 | ) : ( 55 | Manage Subscription 56 | )} 57 |
58 |
59 | )} 60 |
61 | ) 62 | } 63 | -------------------------------------------------------------------------------- /src/components/basejump/edit-team-member-role-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { SubmitButton } from "../ui/submit-button" 4 | import { Label } from "../ui/label"; 5 | import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; 6 | import { updateTeamMemberRole } from "@/lib/actions/members"; 7 | import { GetAccountMembersResponse } from "@usebasejump/shared"; 8 | import { useState } from "react"; 9 | import { Checkbox } from "../ui/checkbox"; 10 | import { usePathname } from "next/navigation"; 11 | 12 | type Props = { 13 | accountId: string; 14 | isPrimaryOwner: boolean; 15 | teamMember: GetAccountMembersResponse[0]; 16 | } 17 | 18 | const memberOptions = [ 19 | { label: 'Owner', value: 'owner' }, 20 | { label: 'Member', value: 'member' }, 21 | ] 22 | 23 | 24 | export default function EditTeamMemberRoleForm({ accountId, teamMember, isPrimaryOwner }: Props) { 25 | const [teamRole, setTeamRole] = useState(teamMember.account_role as string) 26 | const pathName = usePathname(); 27 | 28 | return ( 29 |
30 | 31 | 32 | 33 |
34 | 37 | 49 |
50 | {teamRole === 'owner' && isPrimaryOwner && ( 51 |
52 | 53 | 59 |
60 | )} 61 | 65 | Update Role 66 | 67 |
68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /src/components/dashboard/dashboard-header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import UserAccountButton from "@/components/basejump/user-account-button"; 3 | import BasejumpLogo from "@/components/getting-started/basejump-logo"; 4 | import NavigatingAccountSelector from "@/components/dashboard/navigation-account-selector"; 5 | import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from "../ui/sheet"; 6 | import { Menu } from "lucide-react"; 7 | 8 | 9 | interface Props { 10 | accountId: string; 11 | navigation?: { 12 | name: string; 13 | href: string; 14 | }[] 15 | } 16 | export default function DashboardHeader({ accountId, navigation = [] }: Props) { 17 | 18 | return ( 19 | 57 | ) 58 | 59 | } -------------------------------------------------------------------------------- /src/components/basejump/manage-team-invitations.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"; 2 | import { createClient } from "@/lib/supabase/server"; 3 | import { Table, TableRow, TableBody, TableCell } from "../ui/table"; 4 | import { Badge } from "../ui/badge"; 5 | import CreateTeamInvitationButton from "./create-team-invitation-button"; 6 | import { formatDistanceToNow } from "date-fns"; 7 | import DeleteTeamInvitationButton from "./delete-team-invitation-button"; 8 | 9 | type Props = { 10 | accountId: string; 11 | } 12 | 13 | export default async function ManageTeamInvitations({ accountId }: Props) { 14 | const supabaseClient = createClient(); 15 | 16 | const { data: invitations } = await supabaseClient.rpc('get_account_invitations', { 17 | account_id: accountId 18 | }); 19 | 20 | return ( 21 | 22 | 23 |
24 |
25 | Pending Invitations 26 | 27 | These are the pending invitations for your team 28 | 29 |
30 | 31 |
32 |
33 | {Boolean(invitations?.length) && ( 34 | 35 | 36 | 37 | {invitations?.map((invitation: any) => ( 38 | 39 | 40 |
41 | {formatDistanceToNow(invitation.created_at, { addSuffix: true })} 42 | {invitation.invitation_type} 43 | {invitation.account_role} 44 |
45 |
46 | 47 | 48 | 49 |
50 | ))} 51 |
52 |
53 |
54 | )} 55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/components/basejump/team-member-options.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { Ellipsis } from "lucide-react"; 4 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "../ui/dropdown-menu"; 5 | import { Button } from "../ui/button"; 6 | import { GetAccountMembersResponse } from "@usebasejump/shared"; 7 | import { useEffect, useState } from "react"; 8 | import { DialogHeader, Dialog, DialogContent, DialogTitle, DialogDescription, DialogTrigger, DialogPortal, DialogOverlay } from "@/components/ui/dialog"; 9 | import EditTeamMemberRoleForm from "./edit-team-member-role-form"; 10 | import { SubmitButton } from "../ui/submit-button"; 11 | import { removeTeamMember as removeTeamMemberAction } from "@/lib/actions/members"; 12 | import DeleteTeamMemberForm from "./delete-team-member-form"; 13 | 14 | type Props = { 15 | accountId: string; 16 | teamMember: GetAccountMembersResponse[0]; 17 | isPrimaryOwner: boolean; 18 | } 19 | 20 | export default function TeamMemberOptions({ teamMember, accountId, isPrimaryOwner }: Props) { 21 | const [updateTeamRole, toggleUpdateTeamRole] = useState(false); 22 | const [removeTeamMember, toggleRemoveTeamMember] = useState(false); 23 | 24 | useEffect(() => { 25 | if (updateTeamRole) { 26 | toggleUpdateTeamRole(false); 27 | } 28 | }, [teamMember.account_role]) 29 | return ( 30 | <> 31 | 32 | 33 | 34 | toggleUpdateTeamRole(true)}>Change role 35 | toggleRemoveTeamMember(true)} className="text-red-600">Remove member 36 | 37 | 38 | 39 | 40 | 41 | Update team member role 42 | 43 | Update a member's role in your team 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | Remove team member 53 | 54 | Remove this user from the team 55 | 56 | 57 | 58 | 59 | 60 | 61 | ) 62 | } -------------------------------------------------------------------------------- /src/lib/supabase/middleware.ts: -------------------------------------------------------------------------------- 1 | import { createServerClient, type CookieOptions } from "@supabase/ssr"; 2 | import { type NextRequest, NextResponse } from "next/server"; 3 | 4 | function forceLoginWithReturn(request: NextRequest) { 5 | const originalUrl = new URL(request.url); 6 | const path = originalUrl.pathname; 7 | const query = originalUrl.searchParams.toString(); 8 | return NextResponse.redirect(new URL(`/login?returnUrl=${encodeURIComponent(path + (query ? `?${query}` : ''))}`, request.url)); 9 | } 10 | 11 | export const validateSession = async (request: NextRequest) => { 12 | // This `try/catch` block is only here for the interactive tutorial. 13 | // Feel free to remove once you have Supabase connected. 14 | try { 15 | // Create an unmodified response 16 | let response = NextResponse.next({ 17 | request: { 18 | headers: request.headers, 19 | }, 20 | }); 21 | 22 | const supabase = createServerClient( 23 | process.env.NEXT_PUBLIC_SUPABASE_URL!, 24 | process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, 25 | { 26 | cookies: { 27 | get(name: string) { 28 | return request.cookies.get(name)?.value; 29 | }, 30 | set(name: string, value: string, options: CookieOptions) { 31 | // If the cookie is updated, update the cookies for the request and response 32 | request.cookies.set({ 33 | name, 34 | value, 35 | ...options, 36 | }); 37 | response = NextResponse.next({ 38 | request: { 39 | headers: request.headers, 40 | }, 41 | }); 42 | response.cookies.set({ 43 | name, 44 | value, 45 | ...options, 46 | }); 47 | }, 48 | remove(name: string, options: CookieOptions) { 49 | // If the cookie is removed, update the cookies for the request and response 50 | request.cookies.set({ 51 | name, 52 | value: "", 53 | ...options, 54 | }); 55 | response = NextResponse.next({ 56 | request: { 57 | headers: request.headers, 58 | }, 59 | }); 60 | response.cookies.set({ 61 | name, 62 | value: "", 63 | ...options, 64 | }); 65 | }, 66 | }, 67 | }, 68 | ); 69 | 70 | // This will refresh session if expired - required for Server Components 71 | // https://supabase.com/docs/guides/auth/server-side/nextjs 72 | const { data: { user } } = await supabase.auth.getUser(); 73 | 74 | const protectedRoutes = ['/dashboard', '/invitation']; 75 | 76 | if (!user && protectedRoutes.some(path => request.nextUrl.pathname.startsWith(path))) { 77 | // redirect to /login 78 | return forceLoginWithReturn(request); 79 | } 80 | 81 | return response; 82 | } catch (e) { 83 | // If you are here, a Supabase client could not be created! 84 | // This is likely because you have not set up environment variables. 85 | // Check out http://localhost:3000 for Next Steps. 86 | return NextResponse.next({ 87 | request: { 88 | headers: request.headers, 89 | }, 90 | }); 91 | } 92 | }; 93 | -------------------------------------------------------------------------------- /src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | tr]:last:border-b-0", 47 | className 48 | )} 49 | {...props} 50 | /> 51 | )) 52 | TableFooter.displayName = "TableFooter" 53 | 54 | const TableRow = React.forwardRef< 55 | HTMLTableRowElement, 56 | React.HTMLAttributes 57 | >(({ className, ...props }, ref) => ( 58 | 66 | )) 67 | TableRow.displayName = "TableRow" 68 | 69 | const TableHead = React.forwardRef< 70 | HTMLTableCellElement, 71 | React.ThHTMLAttributes 72 | >(({ className, ...props }, ref) => ( 73 |
81 | )) 82 | TableHead.displayName = "TableHead" 83 | 84 | const TableCell = React.forwardRef< 85 | HTMLTableCellElement, 86 | React.TdHTMLAttributes 87 | >(({ className, ...props }, ref) => ( 88 | 93 | )) 94 | TableCell.displayName = "TableCell" 95 | 96 | const TableCaption = React.forwardRef< 97 | HTMLTableCaptionElement, 98 | React.HTMLAttributes 99 | >(({ className, ...props }, ref) => ( 100 |
105 | )) 106 | TableCaption.displayName = "TableCaption" 107 | 108 | export { 109 | Table, 110 | TableHeader, 111 | TableBody, 112 | TableFooter, 113 | TableHead, 114 | TableRow, 115 | TableCell, 116 | TableCaption, 117 | } 118 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basejump Nextjs Starter 2 | 3 | Adds a Nextjs starter app on top of [Basejump core](https://github.com/usebasejump/basejump). This is a complete interface with support for personal accounts, team accounts, invitations, managing members/permissions and subscription billing. 4 | 5 | [Learn more at usebasejump.com](https://usebasejump.com). Ask questions [on X / Twitter](https://twitter.com/tiniscule) 6 | 7 | ![Image Description](./public/images/basejump-team-page.png) 8 | 9 | ## Basejump Core Features 10 | 11 | - **Personal accounts**: Every user that signs up using Supabase auth automatically gets their own personal account. 12 | Billing on personal accounts can be enabled/disabled. 13 | - **Team accounts**: Team accounts are billable accounts that can be shared by multiple users. Team accounts can be 14 | disabled if you only wish to allow personal accounts. Billing on team accounts can also be disabled. 15 | - **Permissions**: Permissions are handled using RLS, just like you're used to with Supabase. Basejump provides 16 | convenience methods that let you restrict access to rows based on a user's account access and role within an account 17 | - **Billing**: Basejump provides out of the box billing support for Stripe, but you can add your own providers easily. 18 | If you do, please consider contributing them so others can benefit! 19 | - **Testing**: Basejump is fully tested itself, but also provides a suite of testing tools that make it easier to test 20 | your own Supabase functions and schema. You can check it out 21 | at [database.dev/basejump/supabase_test_helpers](https://database.dev/basejump/supabase_test_helpers). You do not need 22 | to be using Basejump to use the testing tools. 23 | 24 | ## Next Frontend Features 25 | 26 | - **Basic Dashboard**: A basic dashboard implementation restricted to authenticated users 27 | - **User Authentication**: Support for email/password - but add any auth provider supported by Supabase 28 | - **Personal accounts**: Every user that signs up using Supabase auth automatically gets their own personal account. 29 | Billing on personal accounts can be enabled/disabled. 30 | - **Team accounts**: Team accounts are billable accounts that can be shared by multiple users. Team accounts can be 31 | disabled if you only wish to allow personal accounts. Billing on team accounts can also be disabled. 32 | - **Billing**: Basejump provides out of the box billing support for Stripe, but you can add your own providers easily. 33 | If you do, please consider contributing them so others can benefit! 34 | 35 | ## Quick Start 36 | 37 | 1. Run `yarn install` 38 | 2. Run `supabase start` 39 | 3. Create a `.env.local` copy of the `.env.example` file with the correct values for Supabase 40 | 4. Run `yarn dev` 41 | 42 | When you're ready to work on billing, you'll need to set up a Stripe account and add your Stripe keys to your `supabase/functions/.env` file. There's an example file you can copy. 43 | 44 | ## Helpful Links 45 | 46 | - [Basejump Docs](https://usebasejump.com/docs) 47 | - [Creating new protected tables](https://usebasejump.com/docs/example-schema) 48 | - [Testing your Supabase functions](https://usebasejump.com/docs/testing) 49 | 50 | ## Contributing 51 | 52 | Yes please! Please submit a PR with your changes to [the basejump-next github repo](https://github.com/usebasejump/basejump-next). 53 | 54 | You can contribute in the following places: 55 | - [Basejump core](https://github.com/usebasejump/basejump) 56 | - [Basejump Nextjs](https://github.com/usebasejump/basejump-next) 57 | - [Basejump edge functions / billing functions](https://github.com/usebasejump/basejump-deno-packages) 58 | - [Supabase Test Helpers](https://github.com/usebasejump/supabase-test-helpers) -------------------------------------------------------------------------------- /src/components/basejump/new-invitation-form.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | import { SubmitButton } from "../ui/submit-button" 3 | import { Label } from "../ui/label"; 4 | import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectLabel, SelectItem } from "@/components/ui/select"; 5 | import { createInvitation } from "@/lib/actions/invitations"; 6 | import { useFormState } from "react-dom"; 7 | import fullInvitationUrl from "@/lib/full-invitation-url"; 8 | 9 | type Props = { 10 | accountId: string 11 | } 12 | 13 | const invitationOptions = [ 14 | { label: '24 Hour', value: '24_hour' }, 15 | { label: 'One time use', value: 'one_time' }, 16 | ] 17 | 18 | const memberOptions = [ 19 | { label: 'Owner', value: 'owner' }, 20 | { label: 'Member', value: 'member' }, 21 | 22 | ] 23 | 24 | const initialState = { 25 | message: "", 26 | token: "" 27 | }; 28 | 29 | export default function NewInvitationForm({ accountId }: Props) { 30 | 31 | const [state, formAction] = useFormState(createInvitation, initialState) 32 | 33 | return ( 34 |
35 | {Boolean(state?.token) ? ( 36 |
37 | {fullInvitationUrl(state.token!)} 38 |
39 | ) : ( 40 | <> 41 | 42 |
43 | 46 | 58 |
59 |
60 | 63 | 75 |
76 | formAction(formData)} 78 | errorMessage={state?.message} 79 | pendingText="Creating..." 80 | > 81 | Create invitation 82 | 83 | 84 | )} 85 |
86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/app/login/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | import { headers } from "next/headers"; 3 | import { createClient } from "@/lib/supabase/server"; 4 | import { redirect } from "next/navigation"; 5 | import { SubmitButton } from "@/components/ui/submit-button"; 6 | import { Input } from "@/components/ui/input"; 7 | 8 | export default function Login({ 9 | searchParams, 10 | }: { 11 | searchParams: { message: string, returnUrl?: string }; 12 | }) { 13 | const signIn = async (_prevState: any, formData: FormData) => { 14 | "use server"; 15 | 16 | const email = formData.get("email") as string; 17 | const password = formData.get("password") as string; 18 | const supabase = createClient(); 19 | 20 | const { error } = await supabase.auth.signInWithPassword({ 21 | email, 22 | password 23 | }); 24 | 25 | if (error) { 26 | return redirect(`/login?message=Could not authenticate user&returnUrl=${searchParams.returnUrl}`); 27 | } 28 | 29 | return redirect(searchParams.returnUrl || "/dashboard"); 30 | }; 31 | 32 | const signUp = async (_prevState: any, formData: FormData) => { 33 | "use server"; 34 | 35 | const origin = headers().get("origin"); 36 | const email = formData.get("email") as string; 37 | const password = formData.get("password") as string; 38 | const supabase = createClient(); 39 | 40 | const { error } = await supabase.auth.signUp({ 41 | email, 42 | password, 43 | options: { 44 | emailRedirectTo: `${origin}/auth/callback?returnUrl=${searchParams.returnUrl}`, 45 | }, 46 | }); 47 | 48 | if (error) { 49 | return redirect(`/login?message=Could not authenticate user&returnUrl=${searchParams.returnUrl}`); 50 | } 51 | 52 | return redirect(`/login?message=Check email to continue sign in process&returnUrl=${searchParams.returnUrl}`); 53 | }; 54 | 55 | return ( 56 |
57 | 61 | 73 | 74 | {" "} 75 | Back 76 | 77 | 78 |
79 | 82 | 87 | 90 | 96 | 100 | Sign In 101 | 102 | 107 | Sign Up 108 | 109 | {searchParams?.message && ( 110 |

111 | {searchParams.message} 112 |

113 | )} 114 |
115 |
116 | ); 117 | } 118 | -------------------------------------------------------------------------------- /src/components/getting-started/next-logo.tsx: -------------------------------------------------------------------------------- 1 | export default function NextLogo() { 2 | return ( 3 | 10 | 14 | 18 | 22 | 26 | 32 | 36 | 40 | 44 | 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DialogPrimitive from "@radix-ui/react-dialog" 5 | import { X } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Dialog = DialogPrimitive.Root 10 | 11 | const DialogTrigger = DialogPrimitive.Trigger 12 | 13 | const DialogPortal = DialogPrimitive.Portal 14 | 15 | const DialogClose = DialogPrimitive.Close 16 | 17 | const DialogOverlay = React.forwardRef< 18 | React.ElementRef, 19 | React.ComponentPropsWithoutRef 20 | >(({ className, ...props }, ref) => ( 21 | 29 | )) 30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName 31 | 32 | const DialogContent = React.forwardRef< 33 | React.ElementRef, 34 | React.ComponentPropsWithoutRef 35 | >(({ className, children, ...props }, ref) => ( 36 | 37 | 38 | 46 | {children} 47 | 48 | 49 | Close 50 | 51 | 52 | 53 | )) 54 | DialogContent.displayName = DialogPrimitive.Content.displayName 55 | 56 | const DialogHeader = ({ 57 | className, 58 | ...props 59 | }: React.HTMLAttributes) => ( 60 |
67 | ) 68 | DialogHeader.displayName = "DialogHeader" 69 | 70 | const DialogFooter = ({ 71 | className, 72 | ...props 73 | }: React.HTMLAttributes) => ( 74 |
81 | ) 82 | DialogFooter.displayName = "DialogFooter" 83 | 84 | const DialogTitle = React.forwardRef< 85 | React.ElementRef, 86 | React.ComponentPropsWithoutRef 87 | >(({ className, ...props }, ref) => ( 88 | 96 | )) 97 | DialogTitle.displayName = DialogPrimitive.Title.displayName 98 | 99 | const DialogDescription = React.forwardRef< 100 | React.ElementRef, 101 | React.ComponentPropsWithoutRef 102 | >(({ className, ...props }, ref) => ( 103 | 108 | )) 109 | DialogDescription.displayName = DialogPrimitive.Description.displayName 110 | 111 | export { 112 | Dialog, 113 | DialogPortal, 114 | DialogOverlay, 115 | DialogClose, 116 | DialogTrigger, 117 | DialogContent, 118 | DialogHeader, 119 | DialogFooter, 120 | DialogTitle, 121 | DialogDescription, 122 | } 123 | -------------------------------------------------------------------------------- /src/components/ui/sheet.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SheetPrimitive from "@radix-ui/react-dialog" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | import { X } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | const Sheet = SheetPrimitive.Root 11 | 12 | const SheetTrigger = SheetPrimitive.Trigger 13 | 14 | const SheetClose = SheetPrimitive.Close 15 | 16 | const SheetPortal = SheetPrimitive.Portal 17 | 18 | const SheetOverlay = React.forwardRef< 19 | React.ElementRef, 20 | React.ComponentPropsWithoutRef 21 | >(({ className, ...props }, ref) => ( 22 | 30 | )) 31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName 32 | 33 | const sheetVariants = cva( 34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", 35 | { 36 | variants: { 37 | side: { 38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", 39 | bottom: 40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", 41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", 42 | right: 43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", 44 | }, 45 | }, 46 | defaultVariants: { 47 | side: "right", 48 | }, 49 | } 50 | ) 51 | 52 | interface SheetContentProps 53 | extends React.ComponentPropsWithoutRef, 54 | VariantProps {} 55 | 56 | const SheetContent = React.forwardRef< 57 | React.ElementRef, 58 | SheetContentProps 59 | >(({ side = "right", className, children, ...props }, ref) => ( 60 | 61 | 62 | 67 | {children} 68 | 69 | 70 | Close 71 | 72 | 73 | 74 | )) 75 | SheetContent.displayName = SheetPrimitive.Content.displayName 76 | 77 | const SheetHeader = ({ 78 | className, 79 | ...props 80 | }: React.HTMLAttributes) => ( 81 |
88 | ) 89 | SheetHeader.displayName = "SheetHeader" 90 | 91 | const SheetFooter = ({ 92 | className, 93 | ...props 94 | }: React.HTMLAttributes) => ( 95 |
102 | ) 103 | SheetFooter.displayName = "SheetFooter" 104 | 105 | const SheetTitle = React.forwardRef< 106 | React.ElementRef, 107 | React.ComponentPropsWithoutRef 108 | >(({ className, ...props }, ref) => ( 109 | 114 | )) 115 | SheetTitle.displayName = SheetPrimitive.Title.displayName 116 | 117 | const SheetDescription = React.forwardRef< 118 | React.ElementRef, 119 | React.ComponentPropsWithoutRef 120 | >(({ className, ...props }, ref) => ( 121 | 126 | )) 127 | SheetDescription.displayName = SheetPrimitive.Description.displayName 128 | 129 | export { 130 | Sheet, 131 | SheetPortal, 132 | SheetOverlay, 133 | SheetTrigger, 134 | SheetClose, 135 | SheetContent, 136 | SheetHeader, 137 | SheetFooter, 138 | SheetTitle, 139 | SheetDescription, 140 | } 141 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Header from '@/components/getting-started/header' 2 | import { Button } from '@/components/ui/button'; 3 | import Link from 'next/link'; 4 | 5 | export default async function Index() { 6 | 7 | return ( 8 |
9 | 21 | 22 |
23 |
24 |
25 |

Next steps

26 |
    27 |
  1. 28 | Decide if you want to support both personal and team accounts. Personal accounts can't be disabled, but you can remove the dashboard sections that display them. 29 | Learn more here
  2. 30 |
  3. 31 |

    Generate additional tables in Supabase using the Basejump CLI

    32 |
    npx @usebasejump/cli@latest generate table posts title body published:boolean published_at:date 
    33 |

    The CLI isn't required, but it'll help you learn the RLS policy options available to you. Learn more here

    34 |
  4. 35 |
  5. 36 | Flesh out the dashboard with any additional functionality you need. Check out the Basejump API docs here 37 |
  6. 38 |
  7. 39 | Setup subscription billing. Determine if you want to bill for both personal and team accounts, update your
    basejump.config
    table accordingly. Learn more about setting up Stripe here 40 |
  8. 41 |
42 |

Resources

43 | 57 |
58 | Questions? 59 | 60 |
61 |
62 |
63 | 64 |
65 |

👦🐯

66 |

67 | There's treasure everywhere 68 |

69 |
70 |
71 | ) 72 | } 73 | -------------------------------------------------------------------------------- /src/components/ui/command.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { type DialogProps } from "@radix-ui/react-dialog" 5 | import { Command as CommandPrimitive } from "cmdk" 6 | import { Search } from "lucide-react" 7 | 8 | import { cn } from "@/lib/utils" 9 | import { Dialog, DialogContent } from "@/components/ui/dialog" 10 | 11 | const Command = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef 14 | >(({ className, ...props }, ref) => ( 15 | 23 | )) 24 | Command.displayName = CommandPrimitive.displayName 25 | 26 | interface CommandDialogProps extends DialogProps {} 27 | 28 | const CommandDialog = ({ children, ...props }: CommandDialogProps) => { 29 | return ( 30 | 31 | 32 | 33 | {children} 34 | 35 | 36 | 37 | ) 38 | } 39 | 40 | const CommandInput = React.forwardRef< 41 | React.ElementRef, 42 | React.ComponentPropsWithoutRef 43 | >(({ className, ...props }, ref) => ( 44 |
45 | 46 | 54 |
55 | )) 56 | 57 | CommandInput.displayName = CommandPrimitive.Input.displayName 58 | 59 | const CommandList = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, ...props }, ref) => ( 63 | 68 | )) 69 | 70 | CommandList.displayName = CommandPrimitive.List.displayName 71 | 72 | const CommandEmpty = React.forwardRef< 73 | React.ElementRef, 74 | React.ComponentPropsWithoutRef 75 | >((props, ref) => ( 76 | 81 | )) 82 | 83 | CommandEmpty.displayName = CommandPrimitive.Empty.displayName 84 | 85 | const CommandGroup = React.forwardRef< 86 | React.ElementRef, 87 | React.ComponentPropsWithoutRef 88 | >(({ className, ...props }, ref) => ( 89 | 97 | )) 98 | 99 | CommandGroup.displayName = CommandPrimitive.Group.displayName 100 | 101 | const CommandSeparator = React.forwardRef< 102 | React.ElementRef, 103 | React.ComponentPropsWithoutRef 104 | >(({ className, ...props }, ref) => ( 105 | 110 | )) 111 | CommandSeparator.displayName = CommandPrimitive.Separator.displayName 112 | 113 | const CommandItem = React.forwardRef< 114 | React.ElementRef, 115 | React.ComponentPropsWithoutRef 116 | >(({ className, ...props }, ref) => ( 117 | 125 | )) 126 | 127 | CommandItem.displayName = CommandPrimitive.Item.displayName 128 | 129 | const CommandShortcut = ({ 130 | className, 131 | ...props 132 | }: React.HTMLAttributes) => { 133 | return ( 134 | 141 | ) 142 | } 143 | CommandShortcut.displayName = "CommandShortcut" 144 | 145 | export { 146 | Command, 147 | CommandDialog, 148 | CommandInput, 149 | CommandList, 150 | CommandEmpty, 151 | CommandGroup, 152 | CommandItem, 153 | CommandShortcut, 154 | CommandSeparator, 155 | } 156 | -------------------------------------------------------------------------------- /src/components/ui/select.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SelectPrimitive from "@radix-ui/react-select" 5 | import { Check, ChevronDown, ChevronUp } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const Select = SelectPrimitive.Root 10 | 11 | const SelectGroup = SelectPrimitive.Group 12 | 13 | const SelectValue = SelectPrimitive.Value 14 | 15 | const SelectTrigger = React.forwardRef< 16 | React.ElementRef, 17 | React.ComponentPropsWithoutRef 18 | >(({ className, children, ...props }, ref) => ( 19 | span]:line-clamp-1", 23 | className 24 | )} 25 | {...props} 26 | > 27 | {children} 28 | 29 | 30 | 31 | 32 | )) 33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName 34 | 35 | const SelectScrollUpButton = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | 48 | 49 | )) 50 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName 51 | 52 | const SelectScrollDownButton = React.forwardRef< 53 | React.ElementRef, 54 | React.ComponentPropsWithoutRef 55 | >(({ className, ...props }, ref) => ( 56 | 64 | 65 | 66 | )) 67 | SelectScrollDownButton.displayName = 68 | SelectPrimitive.ScrollDownButton.displayName 69 | 70 | const SelectContent = React.forwardRef< 71 | React.ElementRef, 72 | React.ComponentPropsWithoutRef 73 | >(({ className, children, position = "popper", ...props }, ref) => ( 74 | 75 | 86 | 87 | 94 | {children} 95 | 96 | 97 | 98 | 99 | )) 100 | SelectContent.displayName = SelectPrimitive.Content.displayName 101 | 102 | const SelectLabel = React.forwardRef< 103 | React.ElementRef, 104 | React.ComponentPropsWithoutRef 105 | >(({ className, ...props }, ref) => ( 106 | 111 | )) 112 | SelectLabel.displayName = SelectPrimitive.Label.displayName 113 | 114 | const SelectItem = React.forwardRef< 115 | React.ElementRef, 116 | React.ComponentPropsWithoutRef 117 | >(({ className, children, ...props }, ref) => ( 118 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | {children} 133 | 134 | )) 135 | SelectItem.displayName = SelectPrimitive.Item.displayName 136 | 137 | const SelectSeparator = React.forwardRef< 138 | React.ElementRef, 139 | React.ComponentPropsWithoutRef 140 | >(({ className, ...props }, ref) => ( 141 | 146 | )) 147 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName 148 | 149 | export { 150 | Select, 151 | SelectGroup, 152 | SelectValue, 153 | SelectTrigger, 154 | SelectContent, 155 | SelectLabel, 156 | SelectItem, 157 | SelectSeparator, 158 | SelectScrollUpButton, 159 | SelectScrollDownButton, 160 | } 161 | -------------------------------------------------------------------------------- /src/components/getting-started/supabase-logo.tsx: -------------------------------------------------------------------------------- 1 | export default function SupabaseLogo() { 2 | return ( 3 | 11 | 12 | 13 | 17 | 22 | 26 | 27 | 31 | 35 | 39 | 43 | 47 | 51 | 55 | 59 | 60 | 61 | 69 | 70 | 71 | 72 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 92 | 98 | 99 | 100 | 101 | ); 102 | } 103 | -------------------------------------------------------------------------------- /src/components/basejump/account-selector.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ComponentPropsWithoutRef, useMemo, useState } from "react" 4 | import { Check, ChevronsUpDown, PlusCircle, } from "lucide-react"; 5 | 6 | import { cn } from "@/lib/utils" 7 | import { Button } from "@/components/ui/button" 8 | import { 9 | Command, 10 | CommandEmpty, 11 | CommandGroup, 12 | CommandInput, 13 | CommandItem, 14 | CommandList, 15 | CommandSeparator, 16 | } from "@/components/ui/command" 17 | import { 18 | Dialog, 19 | DialogContent, 20 | DialogDescription, 21 | DialogHeader, 22 | DialogTitle, 23 | DialogTrigger, 24 | } from "@/components/ui/dialog" 25 | import { Popover, PopoverContent, PopoverTrigger, } from "@/components/ui/popover" 26 | import NewTeamForm from "@/components/basejump/new-team-form"; 27 | import { useAccounts } from "@/lib/hooks/use-accounts"; 28 | 29 | type PopoverTriggerProps = ComponentPropsWithoutRef; 30 | 31 | type SelectedAccount = NonNullable["data"]>[0]; 32 | 33 | interface AccountSelectorProps extends PopoverTriggerProps { 34 | accountId: string; 35 | placeholder?: string; 36 | onAccountSelected?: (account: SelectedAccount) => void; 37 | } 38 | 39 | export default function AccountSelector({ className, accountId, onAccountSelected, placeholder = "Select an account..." }: AccountSelectorProps) { 40 | 41 | const [open, setOpen] = useState(false) 42 | const [showNewTeamDialog, setShowNewTeamDialog] = useState(false) 43 | 44 | const { data: accounts } = useAccounts(); 45 | 46 | const { teamAccounts, personalAccount, selectedAccount } = useMemo(() => { 47 | const personalAccount = accounts?.find((account) => account.personal_account); 48 | const teamAccounts = accounts?.filter((account) => !account.personal_account); 49 | const selectedAccount = accounts?.find((account) => account.account_id === accountId); 50 | 51 | return { 52 | personalAccount, 53 | teamAccounts, 54 | selectedAccount, 55 | } 56 | }, [accounts, accountId]); 57 | 58 | return ( 59 | 60 | 61 | 62 | 72 | 73 | 74 | 75 | 76 | 77 | No account found. 78 | 79 | { 82 | if (onAccountSelected) { 83 | onAccountSelected(personalAccount!) 84 | } 85 | setOpen(false) 86 | }} 87 | className="text-sm" 88 | > 89 | {personalAccount?.name} 90 | 98 | 99 | 100 | {Boolean(teamAccounts?.length) && ( 101 | 102 | {teamAccounts?.map((team) => ( 103 | { 106 | if (onAccountSelected) { 107 | onAccountSelected(team) 108 | } 109 | 110 | setOpen(false) 111 | }} 112 | className="text-sm" 113 | > 114 | {team.name} 115 | 123 | 124 | ))} 125 | 126 | )} 127 | 128 | 129 | 130 | 131 | 132 | { 135 | setOpen(false) 136 | setShowNewTeamDialog(true) 137 | }} 138 | > 139 | 140 | Create Team 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | Create a new team 151 | 152 | Create a team to collaborate with others. 153 | 154 | 155 | 156 | 157 | 158 | ) 159 | } 160 | -------------------------------------------------------------------------------- /src/components/ui/dropdown-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu" 5 | import { Check, ChevronRight, Circle } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const DropdownMenu = DropdownMenuPrimitive.Root 10 | 11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger 12 | 13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group 14 | 15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal 16 | 17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub 18 | 19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup 20 | 21 | const DropdownMenuSubTrigger = React.forwardRef< 22 | React.ElementRef, 23 | React.ComponentPropsWithoutRef & { 24 | inset?: boolean 25 | } 26 | >(({ className, inset, children, ...props }, ref) => ( 27 | 36 | {children} 37 | 38 | 39 | )) 40 | DropdownMenuSubTrigger.displayName = 41 | DropdownMenuPrimitive.SubTrigger.displayName 42 | 43 | const DropdownMenuSubContent = React.forwardRef< 44 | React.ElementRef, 45 | React.ComponentPropsWithoutRef 46 | >(({ className, ...props }, ref) => ( 47 | 55 | )) 56 | DropdownMenuSubContent.displayName = 57 | DropdownMenuPrimitive.SubContent.displayName 58 | 59 | const DropdownMenuContent = React.forwardRef< 60 | React.ElementRef, 61 | React.ComponentPropsWithoutRef 62 | >(({ className, sideOffset = 4, ...props }, ref) => ( 63 | 64 | 73 | 74 | )) 75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName 76 | 77 | const DropdownMenuItem = React.forwardRef< 78 | React.ElementRef, 79 | React.ComponentPropsWithoutRef & { 80 | inset?: boolean 81 | } 82 | >(({ className, inset, ...props }, ref) => ( 83 | 92 | )) 93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName 94 | 95 | const DropdownMenuCheckboxItem = React.forwardRef< 96 | React.ElementRef, 97 | React.ComponentPropsWithoutRef 98 | >(({ className, children, checked, ...props }, ref) => ( 99 | 108 | 109 | 110 | 111 | 112 | 113 | {children} 114 | 115 | )) 116 | DropdownMenuCheckboxItem.displayName = 117 | DropdownMenuPrimitive.CheckboxItem.displayName 118 | 119 | const DropdownMenuRadioItem = React.forwardRef< 120 | React.ElementRef, 121 | React.ComponentPropsWithoutRef 122 | >(({ className, children, ...props }, ref) => ( 123 | 131 | 132 | 133 | 134 | 135 | 136 | {children} 137 | 138 | )) 139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName 140 | 141 | const DropdownMenuLabel = React.forwardRef< 142 | React.ElementRef, 143 | React.ComponentPropsWithoutRef & { 144 | inset?: boolean 145 | } 146 | >(({ className, inset, ...props }, ref) => ( 147 | 156 | )) 157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName 158 | 159 | const DropdownMenuSeparator = React.forwardRef< 160 | React.ElementRef, 161 | React.ComponentPropsWithoutRef 162 | >(({ className, ...props }, ref) => ( 163 | 168 | )) 169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName 170 | 171 | const DropdownMenuShortcut = ({ 172 | className, 173 | ...props 174 | }: React.HTMLAttributes) => { 175 | return ( 176 | 180 | ) 181 | } 182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut" 183 | 184 | export { 185 | DropdownMenu, 186 | DropdownMenuTrigger, 187 | DropdownMenuContent, 188 | DropdownMenuItem, 189 | DropdownMenuCheckboxItem, 190 | DropdownMenuRadioItem, 191 | DropdownMenuLabel, 192 | DropdownMenuSeparator, 193 | DropdownMenuShortcut, 194 | DropdownMenuGroup, 195 | DropdownMenuPortal, 196 | DropdownMenuSub, 197 | DropdownMenuSubContent, 198 | DropdownMenuSubTrigger, 199 | DropdownMenuRadioGroup, 200 | } 201 | --------------------------------------------------------------------------------