diff --git a/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx index 4612f6c4b7606..4e7d6a8a0e40b 100644 --- a/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx +++ b/apps/studio/components/interfaces/HomeNew/ActivityStats.tsx @@ -55,7 +55,7 @@ export const ActivityStats = () => { return (
-
+
(steps[0]?.key ?? null) + + useEffect(() => { + if (steps.length === 0) { + setActiveStepKey(null) + return + } + + const hasActiveStep = activeStepKey ? steps.some((step) => step.key === activeStepKey) : false + + if (!hasActiveStep) { + setActiveStepKey(steps[0]?.key ?? null) + } + }, [steps, activeStepKey]) + + const activeStep = steps.find((step) => step.key === activeStepKey) ?? steps[0] + const activeStepIndex = activeStep ? steps.findIndex((step) => step.key === activeStep.key) : -1 + const previousStep = activeStepIndex > 0 ? steps[activeStepIndex - 1] : null + const nextStep = + activeStepIndex > -1 && activeStepIndex < steps.length - 1 ? steps[activeStepIndex + 1] : null + + const handleSelectPrevious = () => { + if (previousStep) { + setActiveStepKey(previousStep.key) + } + } + + const handleSelectNext = () => { + if (nextStep) { + setActiveStepKey(nextStep.key) + } + } + + if (!activeStep) { + return null + } + return ( - - {steps.map((step, index) => ( - - -
- {step.icon &&
{step.icon}
} - - {index + 1}. {step.title} - + +
+ + + +
+ + {activeStepIndex + 1} + +
+ + +
+
+
+ {activeStep.image ? ( + {activeStep.title} + ) : ( +
+ Supabase Grafana + Supabase Grafana +
+ )} +
+
+
+
+

{activeStep.title}

+ + {activeStep.status} +
- - {step.status} - - - - {step.image &&
{step.image}
} -

{step.description}

-
- {step.actions.map((action, i) => { +

{activeStep.description}

+
+ {activeStep.actions.map((action, i) => { if (action.component) { - return
{action.component}
+ return
{action.component}
} + if (action.href) { return ( ) })}
- - - ))} - +
+
+
+ ) } diff --git a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx index 9d91f50447d79..571e8df0ca250 100644 --- a/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx +++ b/apps/studio/components/interfaces/HomeNew/GettingStarted/GettingStartedSection.tsx @@ -1,7 +1,13 @@ import { useParams } from 'common' +import { useBranchesQuery } from 'data/branches/branches-query' import { useTablesQuery } from 'data/tables/tables-query' import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject' +import { useRouter } from 'next/router' +import { useCallback, useMemo, useState } from 'react' +import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' + import { GettingStarted } from './GettingStarted' +import { FrameworkSelector } from './FrameworkSelector' import { Code, Database, @@ -14,11 +20,6 @@ import { Table2, GitBranch, } from 'lucide-react' -import { useBranchesQuery } from 'data/branches/branches-query' -import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state' -import { useRouter } from 'next/router' -import { useCallback, useMemo, useState } from 'react' -import { FrameworkSelector } from './FrameworkSelector' import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants' import { AiIconAnimation, @@ -46,7 +47,7 @@ export type GettingStartedStep = { icon?: React.ReactNode title: string description: string - image?: React.ReactNode + image?: string actions: GettingStartedAction[] } @@ -82,6 +83,11 @@ export function GettingStartedSection({ const hasNonDefaultBranch = (branchesData ?? []).some((b) => !b.is_default) || isDefaultProject === false + const selectedFrameworkMeta = useMemo( + () => FRAMEWORKS.find((item) => item.key === selectedFramework), + [selectedFramework] + ) + // Helpers const openAiChat = useCallback( (name: string, initialInput: string) => aiSnap.newChat({ name, open: true, initialInput }), @@ -118,11 +124,11 @@ export function GettingStartedSection({ }, { label: 'Connect', - variant: 'default', + variant: 'primary', onClick: openConnect, }, ], - [openConnect, selectedFramework] + [openConnect, openAiChat, selectedFramework, selectedFrameworkMeta?.label] ) const codeSteps: GettingStartedStep[] = useMemo( @@ -133,7 +139,7 @@ export function GettingStartedSection({ title: 'Install the Supabase CLI', icon: , description: - 'To get started, install the Supabase CLI to manage your project locally, handle migrations, and seed data.', + 'To get started, install the Supabase CLI—our command-line toolkit for managing projects locally, handling migrations, and seeding data—using the npm command below to add it to your workspace.', actions: [ { label: 'Install via npm', @@ -150,7 +156,9 @@ export function GettingStartedSection({ status: tablesCount > 0 ? 'complete' : 'incomplete', title: 'Design your database schema', icon: , - description: 'Next, create a schema file that defines the structure of your database.', + image: '/img/getting-started/declarative-schemas.png', + description: + 'Next, create a schema file that defines the structure of your database, either following our declarative schema guide or asking the AI assistant to generate one for you.', actions: [ { label: 'Create schema file', @@ -174,7 +182,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Seed your database with data', icon: , - description: 'Now, create a seed file to populate your database with initial data.', + description: + 'Now, create a seed file to populate your database with initial data, using the docs for guidance or letting the AI assistant draft realistic inserts.', actions: [ { label: 'Create a seed file', @@ -199,7 +208,7 @@ export function GettingStartedSection({ title: 'Secure your data with RLS policies', icon: , description: - "Let's secure your data by enabling Row Level Security and defining access policies in a migration file.", + "Let's secure your data by enabling Row Level Security (per-row access rules that decide who can read or write specific records) and defining policies in a migration file, either configuring them manually or letting the AI assistant draft policies for your tables.", actions: [ { label: 'Create a migration file', @@ -224,7 +233,7 @@ export function GettingStartedSection({ title: 'Configure authentication', icon: , description: - "It's time to configure your authentication providers and settings for Supabase Auth.", + "It's time to configure your authentication providers and settings for Supabase Auth, so jump into the configuration page and tailor the providers you need.", actions: [ { label: 'Configure', href: `/project/${ref}/auth/providers`, variant: 'default' }, ], @@ -234,7 +243,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Connect your application', icon: , - description: 'Your project is ready. Connect your app using one of our client libraries.', + description: + 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow with the client library you prefer.', actions: connectActions, }, { @@ -242,7 +252,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Sign up your first user', icon: , - description: 'Test your authentication setup by creating the first user account.', + description: + 'Test your authentication setup by creating the first user account, following the docs if you need a step-by-step walkthrough.', actions: [ { label: 'Read docs', @@ -256,7 +267,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Upload a file', icon: , - description: 'Integrate file storage by creating a bucket and uploading a file.', + description: + 'Integrate file storage by creating a bucket and uploading a file, starting from the buckets dashboard linked below.', actions: [ { label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' }, ], @@ -266,7 +278,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Deploy an Edge Function', icon: , - description: 'Add server-side logic by creating and deploying your first Edge Function.', + description: + 'Add server-side logic by creating and deploying your first Edge Function—a lightweight TypeScript or JavaScript function that runs close to your users—then revisit the list to monitor and iterate on it.', actions: [ { label: 'Create a function', @@ -282,7 +295,7 @@ export function GettingStartedSection({ title: "Monitor your project's usage", icon: , description: - "Track your project's activity by creating custom reports for API, database, and auth events.", + "Track your project's activity by creating custom reports for API, database, and auth events right from the reports dashboard.", actions: [{ label: 'Reports', href: `/project/${ref}/reports`, variant: 'default' }], }, { @@ -291,7 +304,7 @@ export function GettingStartedSection({ title: 'Connect to GitHub', icon: , description: - 'Streamline your development workflow by connecting your project to GitHub to automatically manage branches.', + 'Streamline your development workflow by connecting your project to GitHub, using the integrations page to automate branch management.', actions: [ { label: 'Connect to GitHub', @@ -311,8 +324,9 @@ export function GettingStartedSection({ status: tablesCount > 0 ? 'complete' : 'incomplete', title: 'Create your first table', icon: , + image: '/img/getting-started/sample.png', description: - "To kick off your new project, let's start by creating your very first database table using either the table editor or AI Assistant.", + "To kick off your new project, let's start by creating your very first database table using either the table editor or the AI assistant to shape the structure for you.", actions: [ { label: 'Create a table', href: `/project/${ref}/editor`, variant: 'default' }, { @@ -333,7 +347,7 @@ export function GettingStartedSection({ title: 'Add sample data', icon:
, description: - "Next, let's add some sample data that you can play with once you connect your app.", + "Next, let's add some sample data that you can play with once you connect your app, either by inserting rows yourself or letting the AI assistant craft realistic examples.", actions: [ { label: 'Add data', href: `/project/${ref}/editor`, variant: 'default' }, { @@ -354,7 +368,7 @@ export function GettingStartedSection({ title: 'Secure your data with Row Level Security', icon: , description: - "Now that you have some data, let's secure it by enabling Row Level Security and creating policies.", + "Now that you have some data, let's secure it by enabling Row Level Security (row-specific access rules that control who can view or modify records) and creating policies yourself or with help from the AI assistant.", actions: [ { label: 'Create a policy', @@ -378,7 +392,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Set up authentication', icon: , - description: "It's time to set up authentication so you can start signing up users.", + description: + "It's time to set up authentication so you can start signing up users, configuring providers and settings from the auth dashboard.", actions: [ { label: 'Configure auth', @@ -392,7 +407,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Connect your application', icon: , - description: "Your project is ready. Let's connect your application to Supabase.", + description: + 'Your project is ready; use the framework selector to preview starter code and launch the Connect flow to wire up your app.', actions: connectActions, }, { @@ -400,7 +416,8 @@ export function GettingStartedSection({ status: 'incomplete', title: 'Sign up your first user', icon: , - description: 'Test your authentication by signing up your first user.', + description: + 'Test your authentication by signing up your first user, referencing the docs if you need sample flows or troubleshooting tips.', actions: [ { label: 'Read docs', @@ -415,7 +432,7 @@ export function GettingStartedSection({ title: 'Upload a file', icon: , description: - "Let's add file storage to your app by creating a bucket and uploading your first file.", + "Let's add file storage to your app by creating a bucket and uploading your first file from the buckets dashboard.", actions: [ { label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' }, ], @@ -426,7 +443,7 @@ export function GettingStartedSection({ title: 'Add server-side logic', icon: , description: - "Extend your app's functionality by creating an Edge Function for server-side logic.", + "Extend your app's functionality by creating an Edge Function—a lightweight serverless function that executes close to your users—for server-side logic directly from the functions page.", actions: [ { label: 'Create a function', @@ -441,7 +458,7 @@ export function GettingStartedSection({ title: "Monitor your project's health", icon: , description: - "Keep an eye on your project's performance and usage by setting up custom reports.", + "Keep an eye on your project's performance and usage by setting up custom reports from the reports dashboard.", actions: [ { label: 'Create a report', href: `/project/${ref}/reports`, variant: 'default' }, ], @@ -452,7 +469,7 @@ export function GettingStartedSection({ title: 'Create a branch to test changes', icon: , description: - 'Safely test changes by creating a preview branch before deploying to production.', + 'Safely test changes by creating a preview branch before deploying to production, using the branches view to spin one up.', actions: [ { label: 'Create a branch', href: `/project/${ref}/branches`, variant: 'default' }, ], @@ -476,16 +493,20 @@ export function GettingStartedSection({ - + + Code - + + No-code + + + ) : ( + <> +
+

You have been invited to join

+

+ {organizationName}

-
- - -
+ {isSuccessInvitation && ( +

{`Organization slug: ${slug}`}

+ )}
- )} -
- {hasError && !isNotSignedInError && ( - - )} - {isSuccess && !hasError && ( -
- - +
+ {profile === undefined && ( +
+

+ Sign in or create an account first to view this invitation +

+
+ + +
+
+ )} +
+ {!!profile && hasError && ( + + )} + {isSuccessInvitation && !hasError && ( +
+ + +
+ )}
- )} -
-
- +
+ + )} + ) } diff --git a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx index 32d5bc01de2ed..0e4907cd7fd48 100644 --- a/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx +++ b/apps/studio/components/interfaces/OrganizationInvite/OrganizationInviteError.tsx @@ -9,7 +9,7 @@ import { cn } from 'ui' interface OrganizationInviteError { data?: OrganizationInviteByToken - error?: ResponseError + error?: ResponseError | null isError: boolean } diff --git a/apps/studio/components/ui/ActionCard.tsx b/apps/studio/components/ui/ActionCard.tsx index 549e51a2ad706..401112986a1d2 100644 --- a/apps/studio/components/ui/ActionCard.tsx +++ b/apps/studio/components/ui/ActionCard.tsx @@ -29,8 +29,10 @@ export const ActionCard = (card: { > {card.icon} -
-

{card.title}

+
+

+ {card.title} +

{typeof card.description === 'string' ? (
{card.description}
) : ( diff --git a/apps/studio/pages/join.tsx b/apps/studio/pages/join.tsx index 58f63aa217ca0..75823e77d9d4f 100644 --- a/apps/studio/pages/join.tsx +++ b/apps/studio/pages/join.tsx @@ -1,39 +1,48 @@ import Link from 'next/link' -import { useRouter } from 'next/router' import { OrganizationInvite } from 'components/interfaces/OrganizationInvite/OrganizationInvite' +import { BASE_PATH } from 'lib/constants' +import { useTheme } from 'next-themes' +import { useEffect, useMemo, useState } from 'react' import { NextPageWithLayout } from 'types' import { cn } from 'ui' const JoinOrganizationPage: NextPageWithLayout = () => { - const router = useRouter() + const { resolvedTheme } = useTheme() + const isDarkMode = resolvedTheme?.includes('dark') + + const [mounted, setMounted] = useState(false) + + const imgUrl = useMemo( + () => + isDarkMode ? `${BASE_PATH}/img/supabase-dark.svg` : `${BASE_PATH}/img/supabase-light.svg`, + [isDarkMode] + ) + + useEffect(() => setMounted(true), []) return ( -
+ <> - Supabase - -
)} - > - -
-
+ + + ) } +JoinOrganizationPage.getLayout = (page) => ( +
+ {page} +
+) + export default JoinOrganizationPage diff --git a/apps/studio/public/img/getting-started/declarative-schemas.png b/apps/studio/public/img/getting-started/declarative-schemas.png new file mode 100644 index 0000000000000..431873fb90ae3 Binary files /dev/null and b/apps/studio/public/img/getting-started/declarative-schemas.png differ diff --git a/apps/studio/public/img/getting-started/sample.png b/apps/studio/public/img/getting-started/sample.png new file mode 100644 index 0000000000000..ff22f119687c3 Binary files /dev/null and b/apps/studio/public/img/getting-started/sample.png differ diff --git a/packages/ui/src/components/shadcn/ui/toggle.tsx b/packages/ui/src/components/shadcn/ui/toggle.tsx index a74a089e6c34f..c60fd746f1c06 100644 --- a/packages/ui/src/components/shadcn/ui/toggle.tsx +++ b/packages/ui/src/components/shadcn/ui/toggle.tsx @@ -7,7 +7,7 @@ import * as React from 'react' import { cn } from '../../../lib/utils/cn' const toggleVariants = cva( - 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors data-[state=on]:bg-accent data-[state=on]:bg-surface-300 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background bg-surface-200 hover:bg-surface-300 px-3 py-1 h-auto text-foreground transition-all', + 'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors text-foreground-light data-[state=on]:bg-accent data-[state=on]:bg-surface-300 data-[state=on]:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 ring-offset-background bg-surface-200 hover:bg-surface-300 px-3 py-1 h-auto transition-all', { variants: { variant: {