This commit is contained in:
Saxon Fletcher committed 2025-08-26 20:33:27 +10:00
1 parent 0199a270e0
commit d3c2e144ea
5 files changed
+261 -31

No files matched your search

@@ -1,8 +1,8 @@
import { PermissionAction } from '@supabase/shared-types/out/constants'
import { useParams } from 'common'
import { ExternalLink, Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { useMemo, useState } from 'react'
import { parseAsBoolean, parseAsString, useQueryState } from 'nuqs'
import { useEffect, useMemo, useState } from 'react'
import { DatabaseConnectionString } from 'components/interfaces/Connect/DatabaseConnectionString'
import { ButtonTooltip } from 'components/ui/ButtonTooltip'
@@ -55,6 +55,16 @@ export const Connect = () => {
parseAsBoolean.withDefault(false)
)
const [connectTab, setConnectTab] = useQueryState(
'connectTab',
parseAsString.withDefault('direct').withOptions({ clearOnDefault: true })
)
const [prefFramework] = useQueryState(
'framework',
parseAsString.withDefault('').withOptions({ clearOnDefault: true })
)
const [connectionObject, setConnectionObject] = useState<ConnectionType[]>(frameworks)
const [selectedParent, setSelectedParent] = useState(connectionObject[0].key) // aka nextjs
const [selectedChild, setSelectedChild] = useState(
@@ -185,6 +195,33 @@ export const Connect = () => {
selectedGrandchild,
})
// Sync initial tab and framework from query params
useEffect(() => {
if (connectTab === 'frameworks') {
setConnectionObject(frameworks)
handleConnectionTypeChange(frameworks)
if (prefFramework) {
const exists = frameworks.find((f) => f.key === prefFramework)
if (exists) {
setSelectedParent(prefFramework)
// Reset children/grandchildren based on the new parent
const firstChild = exists.children?.[0]?.key ?? ''
setSelectedChild(firstChild)
const firstGrand = exists.children?.[0]?.children?.[0]?.key ?? ''
setSelectedGrandchild(firstGrand)
}
}
}
if (connectTab === 'mobiles') {
setConnectionObject(MOBILES)
handleConnectionTypeChange(MOBILES)
}
if (connectTab === 'orms') {
setConnectionObject(ORMS)
handleConnectionTypeChange(ORMS)
}
}, [connectTab, prefFramework])
if (!isActiveHealthy) {
return (
<ButtonTooltip
@@ -219,7 +256,13 @@ export const Connect = () => {
</DialogDescription>
</DialogHeader>
<Tabs_Shadcn_ defaultValue="direct" onValueChange={(value) => handleConnectionType(value)}>
<Tabs_Shadcn_
value={connectTab}
onValueChange={(value) => {
handleConnectionType(value)
setConnectTab(value)
}}
>
<TabsList_Shadcn_ className={cn('flex overflow-x-scroll gap-x-4', DIALOG_PADDING_X)}>
{connectionTypes.map((type) => (
<TabsTrigger_Shadcn_ key={type.key} value={type.key} className="px-0">
@@ -154,7 +154,7 @@ export const AdvisorWidget = () => {
}}
/>
</CardHeader>
<CardContent className="pt-16 flex flex-col justify-end flex-1 overflow-auto">
<CardContent className="p-6 pt-16 flex flex-col justify-end flex-1 overflow-auto">
{lint.detail ? getFirstSentence(lint.detail) : lint.title}
</CardContent>
</Card>
@@ -0,0 +1,89 @@
import { useState } from 'react'
import {
Button,
CommandEmpty_Shadcn_,
CommandGroup_Shadcn_,
CommandInput_Shadcn_,
CommandItem_Shadcn_,
CommandList_Shadcn_,
Command_Shadcn_,
PopoverContent_Shadcn_,
PopoverTrigger_Shadcn_,
Popover_Shadcn_,
cn,
} from 'ui'
import { Box, Check, ChevronDown } from 'lucide-react'
import { ConnectionType } from 'components/interfaces/Connect/Connect.constants'
import { ConnectionIcon } from 'components/interfaces/Connect/ConnectionIcon'
interface FrameworkSelectorProps {
value: string
onChange: (value: string) => void
items: ConnectionType[]
label?: string
className?: string
}
export const FrameworkSelector = ({
value,
onChange,
items,
label = 'Framework',
className,
}: FrameworkSelectorProps) => {
const [open, setOpen] = useState(false)
const selectedItem = items.find((item) => item.key === value)
function handleSelect(key: string) {
onChange(key)
setOpen(false)
}
return (
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
<div className={cn('flex', className)}>
<PopoverTrigger_Shadcn_ asChild>
<Button
size="tiny"
type="default"
className="gap-0 rounded-l-none"
iconRight={<ChevronDown strokeWidth={1.5} />}
>
<div className="flex items-center gap-2">
{selectedItem?.icon ? <ConnectionIcon icon={selectedItem.icon} /> : <Box size={12} />}
{selectedItem?.label}
</div>
</Button>
</PopoverTrigger_Shadcn_>
</div>
<PopoverContent_Shadcn_ className="p-0 max-w-48" side="bottom" align="start">
<Command_Shadcn_>
<CommandInput_Shadcn_ placeholder="Search..." />
<CommandList_Shadcn_>
<CommandEmpty_Shadcn_>No results found.</CommandEmpty_Shadcn_>
<CommandGroup_Shadcn_>
{items.map((item) => (
<CommandItem_Shadcn_
key={item.key}
value={item.key}
onSelect={() => handleSelect(item.key)}
className="flex gap-2 items-center"
>
{item.icon ? <ConnectionIcon icon={item.icon} /> : <Box size={12} />}
{item.label}
<Check
size={15}
className={cn('ml-auto', item.key === value ? 'opacity-100' : 'opacity-0')}
/>
</CommandItem_Shadcn_>
))}
</CommandGroup_Shadcn_>
</CommandList_Shadcn_>
</Command_Shadcn_>
</PopoverContent_Shadcn_>
</Popover_Shadcn_>
)
}
export default FrameworkSelector
@@ -9,6 +9,7 @@ export type GettingStartedAction = {
onClick?: () => void
variant?: React.ComponentProps<typeof Button>['type']
icon?: React.ReactNode
component?: React.ReactNode
}
export type GettingStartedStep = {
@@ -17,6 +18,7 @@ export type GettingStartedStep = {
icon?: React.ReactNode
title: string
description: string
image?: React.ReactNode
actions: GettingStartedAction[]
}
@@ -36,7 +38,7 @@ export function GettingStarted({ steps, className }: GettingStartedProps) {
</div>
<Row columns={[3, 2, 1]} className="items-stretch">
{steps.map((step) => (
<Card key={step.key} className="overflow-hidden h-full">
<Card key={step.key} className="overflow-hidden h-full flex flex-col">
<CardHeader className="flex flex-row space-y-0 justify-between items-center border-b-0">
<div className="flex flex-row items-center gap-3">
{step.icon && <div>{step.icon}</div>}
@@ -49,10 +51,14 @@ export function GettingStarted({ steps, className }: GettingStartedProps) {
{step.status}
</Badge>
</CardHeader>
<CardContent className="space-y-3 pt-16">
<CardContent className="p-6 pt-16 flex-1 flex flex-col justify-end">
{step.image && <div className="w-full">{step.image}</div>}
<p className="text-base text-foreground">{step.description}</p>
<div className="flex flex-wrap gap-2">
<div className="flex flex-wrap gap-2 mt-4">
{step.actions.map((action, i) => {
if (action.component) {
return <span key={`${step.key}-action-${i}`}>{action.component}</span>
}
const content = (
<Button
key={`${step.key}-action-${i}`}
@@ -2,11 +2,22 @@ import { useParams } from 'common'
import { useTablesQuery } from 'data/tables/tables-query'
import { useSelectedProjectQuery } from 'hooks/misc/useSelectedProject'
import GettingStarted, { GettingStartedStep } from './GettingStarted'
import { Code, Database, File, Table, User } from 'lucide-react'
import { Code, Database, File, Table, User, Upload, UserPlus, BarChart3 } from 'lucide-react'
import { useAiAssistantStateSnapshot } from 'state/ai-assistant-state'
import { useRouter } from 'next/router'
import { useState } from 'react'
import FrameworkSelector from './FrameworkSelector'
import { FRAMEWORKS } from 'components/interfaces/Connect/Connect.constants'
import { AiIconAnimation } from 'ui'
export default function GettingStartedSection() {
const { data: project } = useSelectedProjectQuery()
const { ref } = useParams()
const aiSnap = useAiAssistantStateSnapshot()
const router = useRouter()
// Local state for framework selector preview
const [selectedFramework, setSelectedFramework] = useState<string>(FRAMEWORKS[0]?.key ?? 'nextjs')
const { data: tablesData } = useTablesQuery({
projectRef: project?.ref,
@@ -25,10 +36,21 @@ export default function GettingStartedSection() {
title: 'Design your database',
icon: <Database strokeWidth={1} className="text-foreground-muted" size={16} />,
description:
'Start building your app by creating tables with the table editor, sql editor or describe what you want with the assistant',
'Start building your app by creating tables with the table editor, SQL editor, or describe what you want with the assistant.',
actions: [
{ label: 'View tables', href: `/project/${ref}/editor`, variant: 'default' },
{ label: 'Do it for me', href: `/project/${ref}/sql/templates`, variant: 'default' },
{
label: 'Do it for me',
variant: 'default',
icon: <AiIconAnimation size={14} />,
onClick: () =>
aiSnap.newChat({
name: 'Design my database',
open: true,
initialInput:
'I want to design my database schema. Please propose tables, relationships, and SQL to create them for my app. Ask clarifying questions if needed.',
}),
},
],
},
{
@@ -37,32 +59,31 @@ export default function GettingStartedSection() {
title: 'Add some data',
icon: <Table strokeWidth={1} className="text-foreground-muted" size={16} />,
description:
'Use the table editor to create some sample data, or ask the Assistant to generate some data for you.',
'Use the table editor to create sample data, or ask the Assistant to generate it for you.',
actions: [
{ label: 'Table editor', href: `/project/${ref}/editor`, variant: 'default' },
{ label: 'Run SQL', href: `/project/${ref}/sql/new`, variant: 'default' },
{
label: 'Do it for me',
variant: 'default',
icon: <AiIconAnimation size={14} />,
onClick: () =>
aiSnap.newChat({
name: 'Generate sample data',
open: true,
initialInput:
'Generate SQL INSERT statements to add realistic sample data to my existing tables. Use safe defaults and avoid overwriting data.',
}),
},
],
},
{
key: 'implement-auth',
key: 'setup-auth',
status: 'incomplete',
title: 'Implement auth',
title: 'Set up authentication',
icon: <User strokeWidth={1} className="text-foreground-muted" size={16} />,
description: 'Set up authentication and manage users using Supabase Auth.',
description: 'Configure authentication providers and manage settings for Supabase Auth.',
actions: [
{ label: 'Users', href: `/project/${ref}/auth/users`, variant: 'default' },
{ label: 'Auth settings', href: `/project/${ref}/auth/settings`, variant: 'default' },
],
},
{
key: 'implement-storage',
status: 'incomplete',
title: 'Implement storage',
icon: <File strokeWidth={1} className="text-foreground-muted" size={16} />,
description: 'Create buckets and manage files with Supabase Storage.',
actions: [
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
{ label: 'Policies', href: `/project/${ref}/storage/policies`, variant: 'default' },
{ label: 'Configure', href: `/project/${ref}/auth/providers`, variant: 'default' },
],
},
{
@@ -71,12 +92,83 @@ export default function GettingStartedSection() {
title: 'Connect your app',
icon: <Code strokeWidth={1} className="text-foreground-muted" size={16} />,
description:
'Install a client library and connect to your Supabase project to start querying your data',
'Install a client library and connect to your Supabase project to start querying your data.',
actions: [
{ label: 'Client libraries', href: `/project/${ref}/settings/api`, variant: 'default' },
{ label: 'Quickstarts', href: `/project/${ref}/sql/quickstarts`, variant: 'default' },
{
label: 'Framework selector',
component: (
<FrameworkSelector
value={selectedFramework}
onChange={setSelectedFramework}
items={FRAMEWORKS}
label="Framework"
/>
),
},
{
label: 'Connect',
variant: 'default',
onClick: () =>
router.push(
{
pathname: router.pathname,
query: {
...router.query,
showConnect: true,
connectTab: 'frameworks',
framework: selectedFramework,
},
},
undefined,
{ shallow: true }
),
},
],
},
{
key: 'signup-first-user',
status: 'incomplete',
title: 'Sign up your first user',
icon: <UserPlus strokeWidth={1} className="text-foreground-muted" size={16} />,
description: 'Create your first user account to test authentication flows.',
actions: [
{
label: 'Read docs',
href: 'https://supabase.com/docs/guides/auth',
variant: 'default',
},
],
},
{
key: 'upload-file',
status: 'incomplete',
title: 'Upload a file',
icon: <Upload strokeWidth={1} className="text-foreground-muted" size={16} />,
description: 'Create a bucket and upload your first file with Supabase Storage.',
actions: [
{ label: 'Buckets', href: `/project/${ref}/storage/buckets`, variant: 'default' },
],
},
{
key: 'create-edge-function',
status: 'incomplete',
title: 'Create your first edge function',
icon: <Code strokeWidth={1} className="text-foreground-muted" size={16} />,
description:
'Build and deploy an edge function to run server-side logic close to your users.',
actions: [
{ label: 'Create function', href: `/project/${ref}/functions/new`, variant: 'default' },
{ label: 'View functions', href: `/project/${ref}/functions`, variant: 'default' },
],
},
{
key: 'monitor-progress',
status: 'incomplete',
title: 'Monitor your progress',
icon: <BarChart3 strokeWidth={1} className="text-foreground-muted" size={16} />,
description: 'Create custom reports to track API, database, storage, and auth activity.',
actions: [{ label: 'Reports', href: `/project/${ref}/reports`, variant: 'default' }],
},
]}
/>
)