├── 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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
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 |
49 |
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 |
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 | 
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 |
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 |
{" "}
75 | Back
76 |
77 |
78 |
115 |
116 | );
117 | }
118 |
--------------------------------------------------------------------------------
/src/components/getting-started/next-logo.tsx:
--------------------------------------------------------------------------------
1 | export default function NextLogo() {
2 | return (
3 |
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 | -
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
30 | -
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 |
35 | -
36 | Flesh out the dashboard with any additional functionality you need. Check out the Basejump API docs here
37 |
38 | -
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 |
41 |
42 | Resources
43 |
57 |
61 |
62 |
63 |
64 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------